| 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 |