You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add agent runtime with turn loop and run command (#3)
* feat: add agent runtime with turn loop, session buffer, and run command
Implement the core agent turn loop (Step 4) that ties providers and tools
together. Adds in-memory session buffer with system prompt injection,
streaming response accumulation with fragmented tool call reassembly,
safety-tier-aware tool dispatch (ReadOnly parallel, SideEffecting sequential),
context budget warning, and `yantra run "prompt"` CLI subcommand.
Includes 9 test cases covering all runtime paths with race detection.
* fix: wire WorkspaceDir, extend turn timeout to cover tool dispatch
P1: ToolExecutionContext.WorkspaceDir was never set, causing all built-in
file tools to fail security policy checks. Now wired through AgentRuntime
via a workspaceDir field set at construction.
P1: Turn timeout only covered provider streaming — dispatchTools ran on
the parent context with no deadline. Now turnCtx covers both phases.
Added classifyError to normalize context.DeadlineExceeded to ErrTimeout
(turn budget exceeded) vs ErrCancelled (parent cancelled).
Added TestRun_TurnTimeout and TestRun_TurnTimeoutDuringToolExecution
(11 tests total, all passing with -race).
* fix: address Qodo review — dispatch ordering, signal handling, duplicate progress
1. Tool dispatch ordering: Rewrite dispatchTools to use contiguous-block
approach — iterates in model-provided order, accumulates contiguous
ReadOnly calls into parallel blocks, flushes before any SideEffecting
call. Preserves write_file → read_file ordering correctness.
2. CLI signal handling: Wire signal.NotifyContext (SIGINT/SIGTERM) into
root command via ExecuteContext, pass cmd.Context() into runAgent so
Ctrl-C propagates into provider streaming and tool execution.
3. Duplicate progress: Remove ProgressToolExecution emission from
runtime.executeTool — ToolRegistry.Execute already emits it.
* docs: update README and architecture docs for runtime layer
- README: update architecture diagram (runtime no longer planned), add
yantra run usage to quick start, add runtime section explaining the
turn loop, add runtime/ to project structure
- architecture.md: add Layer 4 (Runtime) covering session buffer, turn
loop, stream accumulation, tool dispatch ordering, error handling,
and context budget. Update "what's next" table. Fix safety tier
description to reflect contiguous-block parallel dispatch.
- config.md: clarify turn_timeout_secs covers both streaming and tools
- tools.md: update SafetyTier dispatch description for accuracy
The runtime is the core agent loop that ties providers and tools together:
141
+
142
+
1. User message is added to an in-memory session
143
+
2. Session context (system prompt + messages + tool schemas) is streamed to the provider
144
+
3. Response is accumulated, including fragmented tool call deltas
145
+
4. If the LLM returns tool calls, they're dispatched respecting safety tiers:
146
+
-**ReadOnly** tools in a contiguous block run in parallel
147
+
-**SideEffecting/Privileged** tools run sequentially at their original position
148
+
- Model-provided tool call order is preserved (e.g., `write_file` before `read_file`)
149
+
5. Tool results are appended to the session, and the loop repeats
150
+
6. When the LLM responds with text only (no tool calls), the loop ends
151
+
152
+
The turn timeout covers both provider streaming and tool execution as a single budget. Ctrl-C (SIGINT/SIGTERM) propagates cleanly into the runtime via context cancellation.
Copy file name to clipboardExpand all lines: docs/architecture.md
+89-31Lines changed: 89 additions & 31 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -38,6 +38,7 @@ Everything in Yantra exists to make this loop work well:
38
38
-**Tools** give the LLM hands
39
39
-**Security** prevents the LLM from doing damage
40
40
-**Config** makes it all customizable
41
+
-**Runtime** runs the think → act → observe loop
41
42
-**Memory** (planned) lets the agent remember across sessions
42
43
-**Gateway** (planned) lets you control it remotely
43
44
@@ -129,10 +130,10 @@ const (
129
130
)
130
131
```
131
132
132
-
These tiers inform the runtime how to handle tools:
133
-
-**ReadOnly** tools can run in parallel safely
134
-
-**SideEffecting** tools should run sequentially (they change state)
135
-
-**Privileged** tools need extra checks and may require user confirmation
133
+
These tiers inform the runtime how to dispatch tools:
134
+
-**ReadOnly** tools run in parallel when contiguous in the call list
135
+
-**SideEffecting** tools run sequentially (they change state)
136
+
-**Privileged** tools run sequentially and may require user confirmation in future
136
137
137
138
### Configuration
138
139
@@ -385,49 +386,106 @@ type ToolExecutionContext struct {
385
386
`WorkspaceDir` is the most important — it's the root directory for all file operations. `Progress` is an optional channel for emitting status updates (the gateway can forward these to the UI).
386
387
387
388
388
-
## How the pieces connect
389
+
## Layer 4: Runtime (`internal/runtime/`)
390
+
391
+
The runtime is the brain — it ties providers and tools together in a turn loop.
389
392
390
-
Here's how everything flows when the runtime (Step 4) is built:
393
+
### Session buffer
394
+
395
+
`Session` is an in-memory conversation buffer. The system prompt is stored separately and injected by `Context()` when building the payload for the provider. This keeps the message list clean for turn counting and future summarization.
396
+
397
+
```go
398
+
session:=NewSession("You are a helpful assistant.", toolSchemas)
399
+
session.Append(Message{Role: "user", Content: "fix the bug"})
400
+
401
+
ctx:= session.Context()
402
+
// → Messages: [system prompt, user message]
403
+
// → Tools: [read_file, write_file, ...]
404
+
```
405
+
406
+
### The turn loop
407
+
408
+
`AgentRuntime.Run()` is the main entry point:
391
409
392
410
```
393
411
1. User runs: yantra run "add error handling to server.go"
Tool call deltas arrive in chunks — the first delta for an index carries `ID` + `Name`, subsequent deltas append to `Arguments` via a `strings.Builder`. This handles all three providers (OpenAI, Anthropic, Gemini) uniformly.
404
438
405
-
5. Get tool schemas for LLM
406
-
→ registry.Schemas(nil) → []FunctionDecl
439
+
### Tool dispatch ordering
407
440
408
-
6. Build initial messages
409
-
→ [system prompt, user message]
441
+
Tools are dispatched in model-provided order with parallelism for contiguous ReadOnly blocks:
410
442
411
-
7. AGENT LOOP:
412
-
a. Call provider.Complete(ctx, &Context{Messages, Tools})
413
-
b. LLM returns Message with ToolCalls
414
-
c. For each ToolCall:
415
-
- registry.Execute(ctx, name, args, execCtx)
416
-
- Policy check → timeout → execute → truncate
417
-
- Create tool result Message
418
-
d. Append assistant message + tool results to history
419
-
e. Check budget (turns, tokens, cost)
420
-
f. Go to step a
443
+
```
444
+
Call order from LLM: [read_file, read_file, write_file, read_file]
- Tool execution errors → placed in message content (the LLM sees them and can recover)
461
+
462
+
### Context budget
421
463
422
-
8. LLM returns text-only response → done
423
-
→ Print final answer to user
464
+
After each tool dispatch, the runtime estimates token usage (chars/4) and logs a warning if the session is approaching the context limit (`TriggerRatio * MaxContextTokens`). Actual summarization is deferred to Step 5 (Memory).
Copy file name to clipboardExpand all lines: docs/config.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -112,7 +112,7 @@ max_cost = 0.0 # Max dollar cost (0 = unlimited)
112
112
113
113
**max_turns** prevents infinite loops. If the LLM keeps calling tools without converging on an answer, this stops it.
114
114
115
-
**turn_timeout_secs** is the timeout for a single turn (LLM call + tool executions). Not per-tool — that's the tool's own Timeout().
115
+
**turn_timeout_secs** is the timeout for a single turn. It covers both the provider streaming phase and tool execution as one budget. Individual tools also have their own Timeout() applied by the registry.
116
116
117
117
**max_cost** tracks token usage cost and stops if exceeded. Useful for preventing runaway spend.
The runtime uses these to decide execution strategy. ReadOnly tools can run in parallel. SideEffecting tools run sequentially. Privileged tools might prompt the user for confirmation.
81
+
The runtime uses these to decide execution strategy. Contiguous ReadOnly tools run in parallel; SideEffecting and Privileged tools run sequentially at their original position in the call list. This preserves model-provided ordering for cross-tool dependencies (e.g., `write_file` then `read_file`) while maximizing parallelism where safe.
0 commit comments