| 1 | # The CodeWhale Agent Runtime — one durable substrate, familiar launchers |
| 2 | |
| 3 | This document explains how sub-agents, the headless `exec` path, and Agent Fleet |
| 4 | relate. It exists because these had drifted into *two* parallel "worker" |
| 5 | systems, and the fix is to make the **fleet-backed worker run** the durable |
| 6 | primitive. "Sub-agent" remains useful product vocabulary for a nested role, but |
| 7 | it must not imply a separate execution substrate with weaker lifecycle |
| 8 | semantics. It also answers the open direction question in #2972 ("how much |
| 9 | Claude Code convergence is right?"). |
| 10 | |
| 11 | ## The core idea |
| 12 | |
| 13 | There is exactly **one** thing that runs detached agent work: a **headless agent |
| 14 | runtime** wrapped in a durable worker lifecycle. It is a model loop with the |
| 15 | full (policy-gated) tool surface that can, in turn, delegate child work through |
| 16 | the same lifecycle. Everything else is just a different way to *launch* that one |
| 17 | runtime, or a different way to *observe* it. |
| 18 | |
| 19 | ``` |
| 20 | ┌───────────────────────────────┐ |
| 21 | │ headless agent runtime │ |
| 22 | │ (full tools + can sub-spawn) │ |
| 23 | └───────────────────────────────┘ |
| 24 | ▲ ▲ ▲ |
| 25 | launches │ │ │ launches |
| 26 | │ │ │ |
| 27 | ┌────────────┴───┐ ┌───────┴────────┐ ┌──┴───────────────────┐ |
| 28 | │ TUI turn │ │ `codewhale │ │ Agent Fleet │ |
| 29 | │ (interactive, │ │ exec` │ │ (durable: ledger, │ |
| 30 | │ in-process) │ │ (headless CLI,│ │ scheduler, SSH, │ |
| 31 | │ │ │ anyone/any- │ │ alerts) — launches │ |
| 32 | │ │ │ time) │ │ `codewhale exec` │ |
| 33 | └────────────────┘ └────────────────┘ │ per worker │ |
| 34 | └───────────────────────┘ |
| 35 | ``` |
| 36 | |
| 37 | - A **sub-agent** is the user-facing name for a *nested assignment* with a role |
| 38 | (`explore`, `review`, `implementer`, `verifier`, ...). It should be backed by |
| 39 | the same worker run lifecycle as fleet. `agent` is the model-facing launcher, |
| 40 | not a second runtime. |
| 41 | - **`codewhale exec`** is the headless front door: usable by anyone at any time |
| 42 | (CI, scripts, another agent), full tools, emits a `stream-json` event stream, |
| 43 | and can spawn sub-agents. It is *the* runtime with a CLI on it. |
| 44 | - A **fleet worker** *is* a `codewhale exec` run that the fleet launches and |
| 45 | tracks durably — locally as a subprocess, or remotely as |
| 46 | `ssh host … codewhale exec …`. The fleet does not re-implement execution; it |
| 47 | adds **orchestration** (durable ledger, scheduling/leasing/retry, host |
| 48 | transport, alert escalation) *over* the one runtime. |
| 49 | |
| 50 | So "fleet vs sub-agent" is not two categories. It is **the same headless run**: |
| 51 | Fleet is the durable control plane, while sub-agent is the role/UX vocabulary |
| 52 | for a nested worker. |
| 53 | |
| 54 | ## The cutover rule |
| 55 | |
| 56 | If a detached `agent` child can fail on a one-off provider timeout with no |
| 57 | retry while an equivalent fleet worker would retry and preserve ledger evidence, |
| 58 | then the cutover is incomplete. Treat that as a CodeWhale runtime gap, not as |
| 59 | normal "sub-agent behavior". |
| 60 | |
| 61 | The compatibility `agent` runtime now retries transient provider header, |
| 62 | stream, and timeout failures with backoff before marking a worker interrupted; |
| 63 | when retries are exhausted it preserves a checkpoint and returns a continuation |
| 64 | handle. The remaining convergence work is to keep that lifecycle durable across |
| 65 | process restarts, remote execution, and full fleet-ledger scheduling. |
| 66 | |
| 67 | The target rule is: |
| 68 | |
| 69 | - durable or long-running work goes through the fleet worker lifecycle; |
| 70 | - `agent` should enqueue |
| 71 | or observe a fleet-backed worker run instead of owning an independent |
| 72 | lifecycle; |
| 73 | - in-process children are allowed only as a small compatibility/latency |
| 74 | optimization, and they must expose the same terminal states, retry semantics, |
| 75 | receipts, and inspection handles as the fleet path. |
| 76 | |
| 77 | In product language it is fine to say "open a sub-agent". In architecture |
| 78 | language that means "start a nested fleet worker with this role". |
| 79 | |
| 80 | ## Why this shape (and why it fixes the lag) |
| 81 | |
| 82 | The motivating problem: spawning many in-process sub-agents made the TUI lag, |
| 83 | because each child cloned a heavy runtime and rebuilt the whole tool registry, |
| 84 | *and* the TUI rendered a full card/transcript per child. |
| 85 | |
| 86 | Surveying Claude Code, Codex, and Kimi, the thing that keeps an orchestrator |
| 87 | light at high fanout is **not** a process boundary — all three run sub-agents |
| 88 | in-process. It is **isolation + a compact event stream**: |
| 89 | |
| 90 | - a child's transcript **never** flows back into the parent — the parent gets a |
| 91 | result summary and a small lifecycle event stream; |
| 92 | - the UI renders **counts** (`2 running / 3 done`), not a child session per |
| 93 | worker; |
| 94 | - each worker's tool surface is built directly from a **role/capability |
| 95 | profile**, not "build everything then filter". |
| 96 | |
| 97 | "Headless" therefore means *the execution is not shaped like the UI* — it does |
| 98 | **not** mean fewer abilities. A headless worker keeps the full toolset and can |
| 99 | spawn sub-agents. |
| 100 | |
| 101 | When the work also needs to be **durable** (survive the TUI closing, a laptop |
| 102 | sleeping) or **remote** (SSH), the fleet runs the worker out-of-process as |
| 103 | `codewhale exec`. The heavy construction then lives in another process entirely, |
| 104 | so the orchestrator stays smooth regardless of fanout, and the run survives |
| 105 | restarts — the day-scale autonomy goal of #3154. |
| 106 | |
| 107 | ## One recursion axis |
| 108 | |
| 109 | A worker runs at `spawn_depth = 0` and may spawn children while |
| 110 | `spawn_depth + 1 ≤ max_spawn_depth`, so a budget of `N` affords `N` nested |
| 111 | delegation levels. Sub-agents and fleet workers share **one** axis, sourced from |
| 112 | `codewhale_config`: |
| 113 | |
| 114 | - `DEFAULT_SPAWN_DEPTH = 3` — the default budget for both standalone sub-agents |
| 115 | and fleet workers (so they cannot drift into "two moving targets"); |
| 116 | - `MAX_SPAWN_DEPTH_CEILING = 8` — the opt-in cap that every configured value |
| 117 | (fleet `max_spawn_depth`, `agent`'s `max_depth`) clamps to. |
| 118 | |
| 119 | Note the parser and the advertised schema do not agree on `agent`'s `max_depth`: |
| 120 | the parser clamps to 8 (`tools/subagent/mod.rs:10601-10617`) while the JSON |
| 121 | schema the model is shown declares `"maximum": 3` (`mod.rs:6845-6848`). A model |
| 122 | therefore cannot request a depth the runtime would honour. Tracked here as a |
| 123 | code discrepancy, not a doc one. |
| 124 | |
| 125 | The root worker always runs even at budget 0; the budget gates *child* |
| 126 | delegation. The default affords at least three nested levels. |
| 127 | |
| 128 | ## Event vocabulary |
| 129 | |
| 130 | The fleet ledger persists the worker's own event stream rather than a separate, |
| 131 | simulated taxonomy. `codewhale exec --output-format stream-json` emits |
| 132 | `{"type": "content" | "tool_use" | "tool_result" | "sandbox_denied" | |
| 133 | "workflow_event" | "session_capture" | "turn_usage" | "metadata" | "done" | |
| 134 | "error"}` lines, which map onto the fleet ledger's |
| 135 | `FleetWorkerEventPayload` (`RunningTool`, `WorkflowEvent`, `Running`, |
| 136 | `Completed`, `Failed`, …). `workflow_event` carries the typed |
| 137 | run/phase/task/gate receipt while a Workflow is in flight and is retained as a |
| 138 | typed `WorkflowEvent` in the Fleet ledger; the enclosing worker still owns the |
| 139 | terminal `done` or `error`. One vocabulary, two surfaces. |
| 140 | |
| 141 | `turn_usage` is the per-model-call usage receipt, emitted once per model |
| 142 | request (turn-step) when the provider reported usage for that call: |
| 143 | |
| 144 | ```json |
| 145 | {"type": "turn_usage", "schema": "codewhale.exec-stream", "schema_version": 1, |
| 146 | "turn": 1, "input_tokens": 1200, "output_tokens": 180, |
| 147 | "reasoning_tokens": 90, "prompt_cache_hit_tokens": 900, |
| 148 | "prompt_cache_miss_tokens": 300, "prompt_cache_write_tokens": 0, |
| 149 | "reasoning_replay_tokens": 40, "duration_ms": 1834} |
| 150 | ``` |
| 151 | |
| 152 | - `turn` is the 1-based index of the model call within the exec run; |
| 153 | `input_tokens`, `output_tokens`, and `duration_ms` are always present. |
| 154 | - Optional token fields are **omitted** when the provider does not report |
| 155 | them — never emitted as null and never backfilled with zeros. Field names |
| 156 | mirror the terminal `metadata` receipt: `prompt_cache_hit_tokens` is the |
| 157 | provider's cache-read count (Anthropic `cache_read_input_tokens`), |
| 158 | `prompt_cache_write_tokens` the cache-creation count |
| 159 | (`cache_creation_input_tokens`). `reasoning_tokens` appears only for |
| 160 | provider paths that report it (OpenAI-compatible |
| 161 | `completion_tokens_details` / Responses `output_tokens_details`; Anthropic |
| 162 | does not report a thinking-token count). `reasoning_replay_tokens` is a |
| 163 | client-side estimate for DeepSeek V4 interleaved-thinking replays. |
| 164 | - When a provider reports no usage at all for a call, the whole event is |
| 165 | skipped for that call. Latency/convergence analysis should sum |
| 166 | `turn_usage` events instead of inferring per-step tokens from wall time; |
| 167 | the terminal `metadata` receipt still carries the cumulative totals. |
| 168 | |
| 169 | ## Convergence with Claude Code (#2972) |
| 170 | |
| 171 | CodeWhale should converge with Claude Code on **shape**, not on branding: |
| 172 | |
| 173 | - **Adopt**: a headless runtime with a real CLI/SDK front door; sub-agents as |
| 174 | isolated runs that return summaries (not transcripts); a compact, event-driven |
| 175 | fanout projection; capability/role tool profiles; the skills ecosystem |
| 176 | (#2743); structured run receipts. |
| 177 | - **Keep distinct**: CodeWhale branding and first-class DeepSeek/GLM/MiniMax/ |
| 178 | multi-provider support; the local-first **Agent Fleet** (durable, SSH-capable |
| 179 | orchestration) as CodeWhale's own layer above the shared runtime; Workflow as |
| 180 | the orchestration overlay. |
| 181 | - **Do not** fork execution semantics per surface. The TUI, `agent`, |
| 182 | `exec`, the Runtime API, and the fleet must all drive the *same* runtime and |
| 183 | observe the *same* event stream — divergence there is what produced the "two |
| 184 | moving targets" this document exists to prevent. |
| 185 | |
| 186 | The litmus test for any new agent surface: *does it launch and observe the one |
| 187 | runtime, or does it invent a second one?* Only the former is allowed. |
| 188 | |
| 189 | ## What remains after v0.9.0 |
| 190 | |
| 191 | Refreshed 2026-07-15 from a full audit of the older 0.9-era documents. Those |
| 192 | plans are evidence, not a second source of truth. v0.9.0 consolidates the |
| 193 | underwater shell, message-first Operate, permission postures, the wired |
| 194 | Workflow engine and durable run journal, Lane CLI/runtime, setup with |
| 195 | `operate_ready`, constitution rebalance, and ProviderLake/Models.dev. The |
| 196 | remaining work belongs to later releases: |
| 197 | |
| 198 | 1. **Rebrand completion** — the only hard-dated obligations: remove the |
| 199 | `deepseek`/`deepseek-tui` binary shims and shim release assets; finish the |
| 200 | Homebrew `codewhale` formula rollout (`docs/REBRAND.md`). |
| 201 | 2. **Operate as a value stream** — a control-board surface over the underwater |
| 202 | shell (WIP, queue age, bottleneck); model-visible Work state (#3983); phase |
| 203 | ledger (#4039); Workrooms Phase 2 (#3209/#3210) as the inbox substrate; |
| 204 | receipt reconciliation. |
| 205 | 3. **Flow control** — real WIP limits and visible queues (#4015, #4016), |
| 206 | reconciled with the shipped 16-concurrent/1k-run access model (#4292). |
| 207 | 4. **Fleet/Workflow convergence residuals** — live tmux/verifier-gate dogfood |
| 208 | closing #4175/#4177/#4178/#4179; Fleet consuming canonical AgentProfiles; |
| 209 | Conductor/topology (#4010, #4012) as stretch. |
| 210 | 5. **TTC_DESIGN implementation** — approved and now unblocked after v0.9.0. |
| 211 | 6. **HarnessProfile completion** — the status/UX display lane |
| 212 | (`docs/rfcs/HARNESS_PROFILE_CUTLINE.md`). |
| 213 | 7. **File decomposition, re-scoped** — `ui.rs` (~19.1k lines) and `main.rs` |
| 214 | (~17.6k) are the current offenders (`docs/rfcs/FILE_DECOMPOSITION_0_9_0.md`, |
| 215 | whose own ~13.6k/~12.1k figures are the 0.9.0-era snapshot). Both have grown |
| 216 | since; measure before quoting. |
| 217 | |
| 218 | Explicitly deferred by their own documents: external workflow memory (boundary |
| 219 | only), automatic harness evolution, hosted workrooms, `constitution_modules` |
| 220 | (needs sign-off), permission profiles (#3211, needs design), and plan-ceiling |
| 221 | probing (needs a product decision). |
| 222 | |
| 223 | ## Public launch contract for an external harness (#4641) |
| 224 | |
| 225 | An external evaluation harness (for example a future Verifiers v1 built-in |
| 226 | harness) embeds CodeWhale by launching the public `codewhale exec` front door |
| 227 | against an interception endpoint it owns. CodeWhale owns only its **launch |
| 228 | contract**; the harness owns interception, traces, model-call timing, token |
| 229 | accounting, retries, rollout limits, and runtime orchestration. Do not add a |
| 230 | harness runtime, trace parser, or receipt schema to CodeWhale. |
| 231 | |
| 232 | A reproducible headless launch uses only existing generic surfaces: |
| 233 | |
| 234 | - an explicit temporary config that names the route and the credential |
| 235 | **environment variable**, never the secret itself: |
| 236 | |
| 237 | ```toml |
| 238 | provider = "openai" |
| 239 | |
| 240 | [providers.openai] |
| 241 | base_url = "" # the harness fills in its interception endpoint |
| 242 | model = "" # the harness fills in the target model |
| 243 | api_key_env = "VF_CODEWHALE_API_KEY" |
| 244 | ``` |
| 245 | |
| 246 | - `CODEWHALE_HOME` set to a fresh per-run directory; |
| 247 | - `CODEWHALE_SECRET_BACKEND=file`; |
| 248 | - `CODEWHALE_MCP_CONFIG` pointing to a generated per-run MCP JSON file that |
| 249 | contains only the task servers the harness supplies |
| 250 | (`{"mcpServers":{"task-tools":{"url":""}}}`; the `mcpServers` alias and |
| 251 | URL-based Streamable HTTP / SSE transports already exist); |
| 252 | - `CODEWHALE_MEMORY=false` and `CODEWHALE_TELEMETRY=false`. Product telemetry |
| 253 | is already opt-in and off by default, and a fresh `CODEWHALE_HOME` carries no |
| 254 | first-run notice decision, so a sealed harness run collects nothing either |
| 255 | way; setting the variable makes that explicit and survives a home the caller |
| 256 | reuses — which is the case that now matters, because a reused home whose owner |
| 257 | answered the first-run notice with Enable sends to a live endpoint |
| 258 | (`https://telemetry.codewhale.net/v1/telemetry`, the shipped default) rather |
| 259 | than to a local file. It is a hard floor — an explicit "off" in the |
| 260 | environment beats `--telemetry true` and `telemetry = true` in config. Set |
| 261 | `CODEWHALE_TELEMETRY_ENDPOINT=` (empty) instead if a harness wants an enabled |
| 262 | home to keep buffering locally without contacting anything. See |
| 263 | [`docs/TELEMETRY.md`](TELEMETRY.md); |
| 264 | - `CODEWHALE_ALLOW_INSECURE_HTTP=1` **only** when the harness supplies a |
| 265 | trusted `http://` interception endpoint (container/tunnel endpoints are not |
| 266 | always loopback); |
| 267 | - `--append-system-prompt` and `--disallowed-tools` when the caller supplies |
| 268 | them. |
| 269 | |
| 270 | The interception secret stays in the child environment (resolved through the |
| 271 | route's `api_key_env`); it is never written into argv, the route config, logs, |
| 272 | the `stream-json` stream, or any generated file. |
| 273 | |
| 274 | The exact argument order is: |
| 275 | |
| 276 | ```sh |
| 277 | codewhale \ |
| 278 | --config .vf-codewhale/config.toml \ |
| 279 | --workspace . \ |
| 280 | --no-project-config \ |
| 281 | --skip-onboarding \ |
| 282 | exec \ |
| 283 | --auto \ |
| 284 | --sandbox danger-full-access \ |
| 285 | --output-format stream-json \ |
| 286 | -- "<task prompt>" |
| 287 | ``` |
| 288 | |
| 289 | `--no-project-config` must appear **before** the subcommand (like |
| 290 | `--skip-onboarding`). The public dispatcher parses it and forwards it ahead of |
| 291 | the TUI subcommand; `Exec` then skips the workspace-specific |
| 292 | `[workspace]`/`[projects]` user-config overlay so the config surface depends |
| 293 | only on the explicit `--config`. `crates/tui/tests/verifiers_harness_contract.rs` |
| 294 | is the provider-free acceptance lock for this contract. |
| 295 | |
| 296 | ### Future upstream checklist (out of scope here — do not run) |
| 297 | |
| 298 | Actually adding CodeWhale as a built-in harness lives in the external Verifiers |
| 299 | repository, **after** a public, immutable CodeWhale `v0.9.1` GitHub Release and |
| 300 | checksum manifest exist. That upstream change is expected to be limited to a new |
| 301 | `verifiers/v1/harnesses/codewhale/` package plus its test-matrix and docs |
| 302 | registration, with `CodewhaleHarnessConfig` pinning `0.9.1`, `setup()` |
| 303 | downloading and verifying the released archive, and `launch()` writing the |
| 304 | temporary route/MCP files above and calling `runtime.run_program(...)`. |
| 305 | |
| 306 | Holdouts, explicitly **not** performed by this contract work: tagging, |
| 307 | publishing, or creating a CodeWhale release; opening or submitting the upstream |
| 308 | Verifiers PR; running its credentialed E2E matrix; or claiming |
| 309 | runtime/architecture support before the exact released archive has run in that |
| 310 | upstream runtime. |
| 311 |