返回 CodeWhale
AGENT_RUNTIME.md
根目录 / docs / AGENT_RUNTIME.md
1 # The Codewhale Agent Runtime — one durable substrate, familiar launchers
2
3 > 阅读简体中文版:[zh_hans/AGENT_RUNTIME.md](zh_hans/AGENT_RUNTIME.md)
4
5 This document explains how sub-agents, the headless `exec` path, Agent Fleet,
6 and Runtime relate. These concepts had drifted into *two* parallel "worker"
7 systems. The fix is to make the **Runtime worker run** the durable execution
8 primitive: fleet owns Agent identity, membership, and selection; Runtime owns
9 execution, authority, and lifecycle. "Sub-agent" remains useful product
10 vocabulary for a nested role, but it must not imply a separate execution
11 substrate with weaker lifecycle semantics. It also answers the open direction
12 question in #2972 ("how much Claude Code convergence is right?").
13
14 ## The core idea
15
16 There is exactly **one** thing that runs detached Agent work: a **headless
17 Runtime worker** with a durable execution lifecycle. It is a model loop with
18 the full, authority-gated tool surface that can, in turn, delegate child work
19 through the same lifecycle. Everything else is a way to select, launch, or
20 observe that one Runtime.
21
22 ```
23 ┌──────────────────────────────────────┐
24 │ headless Runtime │
25 │ execution · authority · lifecycle │
26 │ can spawn child workers │
27 └───────────────┬──────────────────────┘
28
29 ┌───────────────┴──────────────────────┐
30 │ one durable execution substrate │
31 └───────┬───────────────┬──────────────┘
32 │ launches │ launches
33 ┌──────────────┴──────┐ ┌─────┴────────────────┐ ┌──────────────────────┐
34 │ TUI turn │ │ `codewhale exec` │ │ Agent fleet │
35 │ interactive, in-proc │ │ headless CLI │ │ identity · membership │
36 │ │ │ full tools · stream │ │ · selection │
37 └─────────────────────┘ └──────────────────────┘ └──────────┬───────────┘
38
39 └─ selects a Runtime worker
40 ```
41
42 - A **sub-agent** is the user-facing name for a *nested assignment* with a role
43 (`explore`, `review`, `implementer`, `verifier`, ...). It should be backed by
44 the same Runtime worker lifecycle used for a fleet-selected Agent. `agent` is
45 the model-facing launcher, not a second runtime.
46 - **`codewhale exec`** is the headless front door: usable by anyone at any time
47 (CI, scripts, another agent), full tools, emits a `stream-json` event stream,
48 and can spawn sub-agents. It is *the* runtime with a CLI on it.
49 - A **fleet-selected Agent** executes as a Runtime `codewhale exec` run. fleet
50 supplies identity, membership, and selection; it does not re-implement
51 execution. Runtime owns the durable ledger, scheduling/leasing/retry,
52 authority, local or SSH transport, and terminal lifecycle.
53
54 So "fleet vs sub-agent" is not a choice between execution substrates. fleet
55 answers **who** is eligible and selected, Runtime answers **how and where** the
56 authorized work executes, and sub-agent remains the role/UX vocabulary for a
57 nested assignment.
58
59 ## The cutover rule
60
61 If a detached `agent` child can fail on a one-off provider timeout with no
62 retry while an equivalent Runtime worker would retry and preserve ledger
63 evidence, then the cutover is incomplete. Treat that as a Codewhale Runtime
64 gap, not as normal "sub-agent behavior".
65
66 The compatibility `agent` runtime now retries transient provider header,
67 stream, and timeout failures with backoff before marking a worker interrupted;
68 when retries are exhausted it preserves a checkpoint and returns a continuation
69 handle. The remaining convergence work is to keep that lifecycle durable across
70 process restarts, remote execution, and full Runtime-ledger scheduling.
71
72 The target rule is:
73
74 - durable or long-running work goes through the Runtime worker lifecycle;
75 - `agent` should enqueue
76 or observe a Runtime worker run instead of owning an independent
77 lifecycle;
78 - in-process children are allowed only as a small compatibility/latency
79 optimization, and they must expose the same terminal states, retry semantics,
80 receipts, and inspection handles as the durable Runtime path.
81
82 In product language it is fine to say "open a sub-agent". In architecture
83 language that means "start a nested Runtime worker with this role", optionally
84 using a member selected from fleet.
85
86 ## Why this shape (and why it fixes the lag)
87
88 The motivating problem: spawning many in-process sub-agents made the TUI lag,
89 because each child cloned a heavy runtime and rebuilt the whole tool registry,
90 *and* the TUI rendered a full card/transcript per child.
91
92 Surveying Claude Code, Codex, and Kimi, the thing that keeps an orchestrator
93 light at high fanout is **not** a process boundary — all three run sub-agents
94 in-process. It is **isolation + a compact event stream**:
95
96 - a child's transcript **never** flows back into the parent — the parent gets a
97 result summary and a small lifecycle event stream;
98 - the UI renders **counts** (`2 running / 3 done`), not a child session per
99 worker;
100 - each worker's tool surface is built directly from a **role/capability
101 profile**, not "build everything then filter".
102
103 "Headless" therefore means *the execution is not shaped like the UI* — it does
104 **not** mean fewer abilities. A headless worker keeps the full toolset and can
105 spawn sub-agents.
106
107 When the work also needs to be **durable** (survive the TUI closing, a laptop
108 sleeping) or **remote** (SSH), Runtime runs the worker out-of-process as
109 `codewhale exec`. fleet may supply the selected Agent identity, but Runtime
110 retains execution authority and lifecycle ownership. The heavy construction
111 then lives in another process entirely, so the orchestrator stays smooth
112 regardless of fanout, and the run survives restarts — the day-scale autonomy
113 goal of #3154.
114
115 ## One recursion axis
116
117 A worker runs at `spawn_depth = 0` and may spawn children while
118 `spawn_depth + 1 ≤ max_spawn_depth`, so a budget of `N` affords `N` nested
119 delegation levels. Sub-agents and fleet-selected Runtime workers share **one**
120 axis, sourced from `codewhale_config`:
121
122 - `DEFAULT_SPAWN_DEPTH = 3` — the default budget for both standalone sub-agents
123 and fleet-selected Runtime workers (so they cannot drift into "two moving
124 targets");
125 - `MAX_SPAWN_DEPTH_CEILING = 8` — the opt-in cap that every configured Runtime
126 value, including fleet execution `max_spawn_depth`, clamps to.
127
128 The model-facing `agent` schema intentionally omits `max_depth`. The parser
129 still accepts `max_depth`, `maxDepth`, and `max_spawn_depth` for saved
130 transcripts, ACP/MCP clients, and internal compatibility callers, and rejects
131 values above 8. Current model-authored calls inherit the Runtime configuration
132 instead of negotiating recursion depth in the tool schema.
133
134 Workflow IR has a separate default structural validation limit of five nested nodes.
135 That limit constrains the orchestration document's shape; it does not grant or
136 consume Runtime child-delegation depth.
137
138 The root worker always runs even at budget 0; the budget gates *child*
139 delegation. The default affords at least three nested levels.
140
141 ## Event vocabulary
142
143 The Runtime execution ledger persists the worker's own event stream rather than
144 a separate, simulated taxonomy. Compatibility APIs and types still expose this
145 through the `Fleet...` prefix. `codewhale exec --output-format stream-json`
146 emits
147 `{"type": "content" | "tool_use" | "tool_result" | "sandbox_denied" |
148 "workflow_event" | "session_capture" | "turn_usage" | "metadata" | "done" |
149 "error"}` lines, which map onto the Runtime ledger's compatibility type
150 `FleetWorkerEventPayload` (`RunningTool`, `WorkflowEvent`, `Running`,
151 `Completed`, `Failed`, …). `workflow_event` carries the typed
152 run/phase/task/gate receipt while a Workflow is in flight and is retained as a
153 typed `WorkflowEvent` in the Runtime execution ledger; the enclosing Runtime
154 worker still owns the terminal `done` or `error`. One vocabulary, two surfaces.
155
156 `session_capture` is emitted once, when the exec run persisted its transcript
157 as a saved session, and carries the recoverable id in exactly one place:
158
159 ```json
160 {"type": "session_capture", "schema": "codewhale.exec-stream", "schema_version": 1,
161 "content": "<redacted:…>", "saved_session_id": "01J…"}
162 ```
163
164 - `saved_session_id` is the raw saved-session id, emitted only after a
165 successful save. For local Fleet workers, the parent assigns a fresh ID and
166 shares the Runtime's existing session directory. The executor advertises
167 `FleetReceipt.saved_session_id` only when that exact ID was reported and its
168 saved transcript can be loaded. A client with Runtime API access can then
169 read the reply through `GET /v1/sessions/{id}`. SSH workers retain their
170 excerpt and remote log, but do not advertise an unavailable local session
171 link. An ID is a lookup key, not a substitute for Runtime authentication.
172 - `content` is the same redacted fingerprint the terminal `metadata.session_id`
173 carries, so a captured `metadata` receipt stays safe to log on its own and
174 the two events can still be correlated. `metadata.resume_command` therefore
175 names this field (`codewhale exec --resume <session_capture.saved_session_id>`)
176 rather than carrying the id itself.
177
178 The terminal `metadata` receipt also carries the worker's visible final answer:
179 `visible_final_answer_chars` is the real character count of the final
180 assistant reply, and `visible_final_answer_excerpt` is a bounded (4,000
181 characters, `...` when cut), secret-redacted excerpt of it, omitted when the
182 current turn produced no visible answer. Resumed turns never reuse an older
183 reply, and failed/interrupted receipts may carry partial current-turn text;
184 the receipt status remains authoritative. The Runtime executor reads the excerpt from
185 this receipt — never from the streamed `content` deltas, which are the run
186 thinking out loud — and attaches it to `Completed.summary` and, for a task
187 with no scorer and no file artifact, to the receipt notes as the task's
188 deliverable. Lifecycle event labels and worker inspection summaries show a
189 short excerpt; the event `payload` and the receipt keep the full excerpt.
190
191 `turn_usage` is the per-model-call usage receipt, emitted once per model
192 request (turn-step) when the provider reported usage for that call:
193
194 ```json
195 {"type": "turn_usage", "schema": "codewhale.exec-stream", "schema_version": 1,
196 "turn": 1, "input_tokens": 1200, "output_tokens": 180,
197 "reasoning_tokens": 90, "prompt_cache_hit_tokens": 900,
198 "prompt_cache_miss_tokens": 300, "prompt_cache_write_tokens": 0,
199 "reasoning_replay_tokens": 40, "duration_ms": 1834}
200 ```
201
202 - `turn` is the 1-based index of the model call within the exec run;
203 `input_tokens`, `output_tokens`, and `duration_ms` are always present.
204 - Optional token fields are **omitted** when the provider does not report
205 them — never emitted as null and never backfilled with zeros. Field names
206 mirror the terminal `metadata` receipt: `prompt_cache_hit_tokens` is the
207 provider's cache-read count (Anthropic `cache_read_input_tokens`),
208 `prompt_cache_write_tokens` the cache-creation count
209 (`cache_creation_input_tokens`). `reasoning_tokens` appears only for
210 provider paths that report it (OpenAI-compatible
211 `completion_tokens_details` / Responses `output_tokens_details`; Anthropic
212 does not report a thinking-token count). `reasoning_replay_tokens` is a
213 client-side estimate for DeepSeek V4 interleaved-thinking replays.
214 - When a provider reports no usage at all for a call, the whole event is
215 skipped for that call. Latency/convergence analysis should sum
216 `turn_usage` events instead of inferring per-step tokens from wall time;
217 the terminal `metadata` receipt still carries the cumulative totals.
218
219 ## Convergence with Claude Code (#2972)
220
221 Codewhale should converge with Claude Code on **shape**, not on branding:
222
223 - **Adopt**: a headless runtime with a real CLI/SDK front door; sub-agents as
224 isolated runs that return summaries (not transcripts); a compact, event-driven
225 fanout projection; capability/role tool profiles; the skills ecosystem
226 (#2743); structured run receipts.
227 - **Keep distinct**: Codewhale branding and first-class DeepSeek/GLM/MiniMax/
228 multi-provider support; the local-first **Agent fleet** as the identity,
229 membership, and selection layer; durable local/SSH execution and authority in
230 Runtime; Workflow as the ordering overlay.
231 - **Do not** fork execution semantics per surface. The TUI, `agent`,
232 `exec`, and the Runtime API must all drive the *same* Runtime and observe the
233 *same* event stream. fleet selections are passed to that Runtime rather than
234 creating a second execution path — divergence there is what produced the
235 "two moving targets" this document exists to prevent.
236
237 The litmus test for any new agent surface: *does it launch and observe the one
238 runtime, or does it invent a second one?* Only the former is allowed.
239
240 ## What remains after v0.9.0
241
242 Refreshed 2026-08-17 from a full audit of the older 0.9-era documents. Those
243 plans are evidence, not a second source of truth. v0.9.0 consolidated the
244 underwater shell, message-first Operate, permission postures, the wired
245 Workflow engine and durable run journal, Lane CLI/runtime, setup with
246 `operate_ready`, constitution rebalance, and ProviderLake/Models.dev. The
247 remaining work belongs to later releases:
248
249 1. **Rebrand completion** — the `deepseek`/`deepseek-tui` binary shims and
250 shim release assets were removed in v0.9.0; the remaining obligation is the
251 Homebrew `codewhale` formula rollout (`docs/REBRAND.md`).
252 2. **Operate as a value stream** — a control-board surface over the underwater
253 shell (WIP, queue age, bottleneck); phase history (#4039); Workrooms Phase 2
254 (#3209/#3210) as the inbox substrate;
255 receipt reconciliation.
256 3. **Flow control** — real WIP limits and visible queues (#4015, #4016),
257 reconciled with the shipped 16-concurrent/1k-run access model (#4292).
258 4. **fleet identity and Runtime/Workflow convergence residuals** — live
259 tmux/verifier-gate dogfood closing #4175/#4177/#4178/#4179; fleet consuming
260 canonical AgentProfiles and selecting members while Runtime owns execution;
261 Conductor/topology (#4010, #4012) as stretch.
262 5. **TTC design implementation** (design doc in `codewhale-ops`) — approved and now unblocked after v0.9.0.
263 6. **HarnessProfile completion** — the status/UX display lane
264 (`docs/rfcs/HARNESS_PROFILE_CUTLINE.md`).
265 7. **File decomposition, landed** — the v0.9.0-era offenders were split out:
266 `main.rs` is now a thin stub and `ui.rs` has been decomposed into focused
267 modules under `crates/tui/src/tui/` (~3.9k lines today; the
268 `docs/rfcs/FILE_DECOMPOSITION_0_9_0.md` figures are the 0.9.0-era snapshot).
269 The remaining work is the "thin TUI over core" north star tracked in
270 `POST_0_9_1_SEAMS.md`.
271
272 Explicitly deferred by their own documents: external workflow memory (boundary
273 only), automatic harness evolution, hosted workrooms, `constitution_modules`
274 (needs sign-off), permission profiles (#3211, needs design), and plan-ceiling
275 probing (needs a product decision).
276
277 ## Public launch contract for an external harness (#4641)
278
279 An external evaluation harness (for example a future Verifiers v1 built-in
280 harness) embeds Codewhale by launching the public `codewhale exec` front door
281 against an interception endpoint it owns. Codewhale owns only its **launch
282 contract**; the harness owns interception, traces, model-call timing, token
283 accounting, retries, rollout limits, and runtime orchestration. Do not add a
284 harness runtime, trace parser, or receipt schema to Codewhale.
285
286 A reproducible headless launch uses only existing generic surfaces:
287
288 - an explicit temporary config that names the route and the credential
289 **environment variable**, never the secret itself:
290
291 ```toml
292 provider = "openai"
293
294 [providers.openai]
295 base_url = "" # the harness fills in its interception endpoint
296 model = "" # the harness fills in the target model
297 api_key_env = "VF_CODEWHALE_API_KEY"
298 ```
299
300 - `CODEWHALE_HOME` set to a fresh per-run directory;
301 - `CODEWHALE_SECRET_BACKEND=file`;
302 - `CODEWHALE_MCP_CONFIG` pointing to a generated per-run MCP JSON file that
303 contains only the task servers the harness supplies
304 (`{"mcpServers":{"task-tools":{"url":""}}}`; the `mcpServers` alias and
305 URL-based Streamable HTTP / SSE transports already exist);
306 - `CODEWHALE_MEMORY=false` and `CODEWHALE_TELEMETRY=false`. The 0.9.12 source
307 defaults usage counting on with an opt-out. Every sealed harness explicitly
308 sets the run-scoped kill switch so a test cannot collect or send from a
309 fresh or reused home. Ordinary enabled sessions send aggregate counts to an endpoint
310 (`https://telemetry.codewhale.net/v1/telemetry`, the shipped default) rather
311 than to a local file. It is a hard floor — an explicit "off" in the
312 environment beats `--telemetry true` and `telemetry = true` in config. Set
313 `CODEWHALE_TELEMETRY_ENDPOINT=` (empty) instead if a harness wants an enabled
314 home to keep buffering locally without contacting anything. See
315 [`docs/TELEMETRY.md`](TELEMETRY.md);
316 - `CODEWHALE_ALLOW_INSECURE_HTTP=1` **only** when the harness supplies a
317 trusted `http://` interception endpoint (container/tunnel endpoints are not
318 always loopback);
319 - `--append-system-prompt` and `--disallowed-tools` when the caller supplies
320 them.
321
322 The interception secret stays in the child environment (resolved through the
323 route's `api_key_env`); it is never written into argv, the route config, logs,
324 the `stream-json` stream, or any generated file.
325
326 The exact argument order is:
327
328 ```sh
329 codewhale \
330 --config .vf-codewhale/config.toml \
331 --workspace . \
332 --no-project-config \
333 --skip-onboarding \
334 exec \
335 --auto \
336 --sandbox danger-full-access \
337 --output-format stream-json \
338 -- "<task prompt>"
339 ```
340
341 `--no-project-config` must appear **before** the subcommand (like
342 `--skip-onboarding`). The public dispatcher parses it and forwards it ahead of
343 the TUI subcommand; `Exec` then skips the workspace-specific
344 `[workspace]`/`[projects]` user-config overlay so the config surface depends
345 only on the explicit `--config`. `crates/tui/tests/integration/verifiers_harness_contract.rs`
346 is the provider-free acceptance lock for this contract.
347
348 ### Future upstream checklist (out of scope here — do not run)
349
350 Actually adding Codewhale as a built-in harness lives in the external Verifiers
351 repository; the public, immutable Codewhale GitHub Releases with checksum
352 manifests it needs have existed since v0.9.1 (latest published release is
353 v0.9.13, published 2026-09-14; the workspace source version is 0.9.13).
354 That upstream change is expected to be limited to a new
355 `verifiers/v1/harnesses/codewhale/` package plus its test-matrix and docs
356 registration, with `CodewhaleHarnessConfig` pinning the target release,
357 `setup()` downloading and verifying the released archive, and `launch()`
358 writing the temporary route/MCP files above and calling `runtime.run_program(...)`.
359
360 Holdouts, explicitly **not** performed by this contract work: tagging,
361 publishing, or creating a Codewhale release; opening or submitting the upstream
362 Verifiers PR; running its credentialed E2E matrix; or claiming
363 runtime/architecture support before the exact released archive has run in that
364 upstream runtime.
365
365 lines MARKDOWN