|
| 1 | +--- |
| 2 | +title: 'Architecture' |
| 3 | +description: 'Session construction, run scope, governed invocation, events, and persistence' |
| 4 | +--- |
| 5 | + |
| 6 | +# Architecture |
| 7 | + |
| 8 | +A3S Code separates configuration resolution, per-run execution, stable wire |
| 9 | +contracts, and persistence. The TUI and SDKs use the same runtime kernel; they do |
| 10 | +not get separate execution paths. |
| 11 | + |
| 12 | +```text |
| 13 | +CodeConfig + SessionOptions |
| 14 | + -> validate and resolve async resources |
| 15 | + -> ResolvedSessionConfig |
| 16 | + -> AgentSession |
| 17 | + -> single-flight run admission |
| 18 | + -> InvocationContext |
| 19 | + ├─ LLM invoker -> provider calls |
| 20 | + ├─ tool invoker -> model, nested, delegated, and host-direct calls |
| 21 | + └─ events -> EventEnvelopeV1 -> Rust / Node / Python consumers |
| 22 | + -> SessionSnapshotV1 -> atomic store generation |
| 23 | +``` |
| 24 | + |
| 25 | +## Async-First Session Construction |
| 26 | + |
| 27 | +`SessionOptions` is a public patch, not partially initialized runtime state. The |
| 28 | +async construction path merges it with `CodeConfig`, validates conflicting |
| 29 | +options, initializes async resources, and produces one internal |
| 30 | +`ResolvedSessionConfig`. Session assembly consumes that resolved value instead |
| 31 | +of resolving the same choice in multiple layers. |
| 32 | + |
| 33 | +For Rust hosts, prefer: |
| 34 | + |
| 35 | +```rust |
| 36 | +let session = agent |
| 37 | + .session_builder("/repo") |
| 38 | + .options(options) |
| 39 | + .build() |
| 40 | + .await?; |
| 41 | +``` |
| 42 | + |
| 43 | +`Agent::session_async`, `resume_session_async`, `session_for_agent_async`, and |
| 44 | +`session_for_worker_async` use the same construction kernel. File-backed memory |
| 45 | +and session stores, queues, trajectory recording, and session MCP discovery are |
| 46 | +initialized asynchronously and return typed `SessionConfiguration` or |
| 47 | +`SessionInitialization` errors with the failing resource. |
| 48 | + |
| 49 | +The synchronous `Agent::session` method exists only for compatibility with |
| 50 | +hosts that explicitly supply a pre-initialized memory store and have already |
| 51 | +initialized every other resource. It never starts or blocks a Tokio runtime. |
| 52 | +Configuration that requires async work fails with |
| 53 | +`CodeError::AsyncSessionBuildRequired`; the runtime does not silently replace |
| 54 | +the requested resource with an easier backend. A manager passed through |
| 55 | +`SessionOptions::with_mcp` always requires async capability discovery. The sync |
| 56 | +path can only inherit agent-global MCP tools already cached at agent startup. |
| 57 | + |
| 58 | +## Single-Flight Conversation State |
| 59 | + |
| 60 | +Conversation history is serialized by admission, not by optimistic locking. |
| 61 | +Only one transcript-affecting operation may be active on a session. This covers |
| 62 | +`send`, `stream`, both attachment variants, slash commands, and `resume_run`. |
| 63 | +An overlap fails immediately with `CodeError::SessionBusy` before history is |
| 64 | +read or a command is dispatched. |
| 65 | + |
| 66 | +A stream retains its admission lease until the stream runtime finishes. Dropping |
| 67 | +or aborting the public handle does not briefly admit a second operation while |
| 68 | +the original producer is still writing events or history. Direct host tool |
| 69 | +calls are control-plane operations and do not claim the conversation lease. |
| 70 | + |
| 71 | +## Invocation Context |
| 72 | + |
| 73 | +Each admitted run creates one immutable `InvocationContext` containing: |
| 74 | + |
| 75 | +- run ID and session ID |
| 76 | +- the run cancellation token |
| 77 | +- the event sender |
| 78 | +- the governance snapshot, including the active budget guard |
| 79 | + |
| 80 | +That context is the source of truth for provider and tool work. It installs the |
| 81 | +same cancellation token and session identity into `ToolContext`, so cancellation |
| 82 | +reaches queued tools, nested `batch`/`program` calls, delegated work, planning, |
| 83 | +structured-output repair, compaction, and other run-owned helper calls. |
| 84 | + |
| 85 | +## LLM Invocation Boundary |
| 86 | + |
| 87 | +Run-owned provider work uses one scoped LLM invoker. It checks budget and |
| 88 | +cancellation before each provider call, records usage after each successful |
| 89 | +response, proxies streaming completion so terminal usage is recorded, and |
| 90 | +combines caller and run cancellation. Normal turns, planning, structured output |
| 91 | +and repair, compaction, and memory/helper paths use this boundary instead of |
| 92 | +maintaining independent budget logic. |
| 93 | + |
| 94 | +A hard budget denial is an error and is never converted into an ungoverned |
| 95 | +fallback. Soft limits emit a `budget_threshold_hit` event and allow the call to |
| 96 | +continue. |
| 97 | + |
| 98 | +## Tool Invocation Boundary |
| 99 | + |
| 100 | +The tool invoker is the governance kernel for model-selected, nested, |
| 101 | +programmatic, delegated, and direct host calls. For model-owned work it applies |
| 102 | +active-skill restrictions, permission policy, pre/post hooks, budget checks, |
| 103 | +human confirmation, queue/timeout handling, cancellation, recursive-invocation |
| 104 | +protection, and output sanitization. `batch` and `program` receive the scoped |
| 105 | +invoker rather than calling a raw registry, so inner calls cannot escape those |
| 106 | +checks. |
| 107 | + |
| 108 | +Direct SDK helpers have an explicit `HostDirectPolicy::TrustedControlPlane` |
| 109 | +origin. |
| 110 | +The host is the authority that chose the operation, so model-facing permission |
| 111 | +and confirmation decisions are skipped. Pre-hooks can still block the call, and |
| 112 | +budget, queue/timeout, cancellation, recursion protection, post-hooks, and |
| 113 | +output sanitization remain active. Applications must authorize end users before |
| 114 | +exposing this privileged path. |
| 115 | + |
| 116 | +Trust propagation is structural rather than ambient. Only built-in |
| 117 | +control-plane orchestrators can convert an explicit host-direct call into the |
| 118 | +internal trusted-nested origin for host-selected children. Public |
| 119 | +`InvocationRuntime::invoke_tool` always creates an ordinary governed nested |
| 120 | +call, even when the extension itself was called directly. Model dispatch also |
| 121 | +removes inherited host-direct policy before it creates a `ToolContext`, so a |
| 122 | +Skill, Task, or other model sub-run cannot use `batch` or `program` to amplify |
| 123 | +its parent's authority. |
| 124 | + |
| 125 | +## Stable Event Protocol |
| 126 | + |
| 127 | +`AgentEvent` is the internal Rust runtime enum. The cross-language contract is |
| 128 | +the lossless envelope: |
| 129 | + |
| 130 | +```json |
| 131 | +{ |
| 132 | + "version": 1, |
| 133 | + "type": "tool_end", |
| 134 | + "payload": {}, |
| 135 | + "metadata": {} |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +The v1 event catalog and the exhaustive Rust mapping share one source of truth, |
| 140 | +so adding a runtime variant without a canonical wire name is a compile-time |
| 141 | +failure. Node and Python consume the centralized projection for convenience |
| 142 | +fields such as `text`, `toolName`, and `tool_name`. `type` remains an open |
| 143 | +string: unknown future types preserve their full payload and metadata rather |
| 144 | +than collapsing to an `unknown` sentinel. |
| 145 | + |
| 146 | +## Atomic Session Persistence |
| 147 | + |
| 148 | +`SessionSnapshotV1` is one versioned persistence generation. It contains the |
| 149 | +conversation plus artifacts, trace events, run records, verification reports, |
| 150 | +and delegated-task snapshots. `session.save()` materializes that aggregate and |
| 151 | +calls `SessionStore::save_snapshot` once. |
| 152 | + |
| 153 | +The file store writes one complete JSON envelope through a synced temporary |
| 154 | +file and atomic replacement. The memory store replaces one aggregate entry |
| 155 | +under one lock. Both advertise atomic snapshot capability. Historical bare |
| 156 | +`SessionData` and fragment directories remain loadable for migration, but new |
| 157 | +saves do not publish fragmented generations. A custom store must implement |
| 158 | +aggregate save explicitly; the default method returns an error rather than |
| 159 | +acknowledging a partial or no-op save. |
| 160 | + |
| 161 | +## MCP Ownership And Isolation |
| 162 | + |
| 163 | +MCP managers have explicit ownership: |
| 164 | + |
| 165 | +1. The agent-global manager owns servers loaded from global configuration. |
| 166 | +2. A host-supplied manager in session options is an inherited, read-only |
| 167 | + capability source. |
| 168 | +3. Every session owns a new private live manager. |
| 169 | + |
| 170 | +Capabilities are assembled in that order, so a session-local tool can shadow an |
| 171 | +inherited tool only inside that session. Live `add_mcp_server` and |
| 172 | +`remove_mcp_server` calls mutate only the private manager; removing a local |
| 173 | +shadow reveals the inherited capability again. Sibling sessions cannot mutate |
| 174 | +one another, and delegated children inherit the ordered manager sources needed |
| 175 | +to call the same tools without transferring ownership. |
| 176 | + |
| 177 | +A local stdio transport also owns the complete server process lifetime. It |
| 178 | +starts the server as a Unix process-group leader, drains protocol output and |
| 179 | +stderr separately, and terminates the group on close or drop. Outstanding |
| 180 | +requests fail when either pipe closes instead of waiting for an unrelated |
| 181 | +request deadline. |
| 182 | + |
| 183 | +## Programmatic Tool Calling |
| 184 | + |
| 185 | +The `program` tool runs JavaScript in an embedded QuickJS VM with a controlled |
| 186 | +`ctx` object. The VM has no direct filesystem, network, subprocess, or |
| 187 | +environment access. Its useful capabilities are tool calls routed back through |
| 188 | +the scoped tool invoker. Use ordinary model tool calls when the next action |
| 189 | +requires judgment; use a bounded program when the action sequence is already |
| 190 | +known and only its result needs model interpretation. |
| 191 | + |
| 192 | +## Extension Points |
| 193 | + |
| 194 | +Use typed session options, skills, agent definitions, hooks, MCP servers, |
| 195 | +memory/session stores, security providers, queue configuration, and workspace |
| 196 | +services to adapt the runtime. Prefer explicit policy and replayable evidence |
| 197 | +over alternate execution paths. |
0 commit comments