| 1 | # Codewhale 智能体运行时:一个持久的底座,多种熟悉的启动方式 |
| 2 | |
| 3 | > 英文原文:[AGENT_RUNTIME.md](../AGENT_RUNTIME.md)。 |
| 4 | > 最后与英文同步日期(last synced with English revision):2026-09-29。 |
| 5 | |
| 6 | 本文说明子智能体(sub-agent)、无头 `exec` 路径、Agent Fleet 和 Runtime 之间的关系。这些概念一度漂移成了*两套*并行的 "worker" 体系。修复方式是让 **Runtime worker run(Runtime 的 worker 运行)** 成为持久的执行原语:Fleet 负责智能体的身份、成员和选择;Runtime 负责执行、授权和生命周期。"子智能体"作为嵌套角色的产品用语仍然有用,但不能因此暗示存在一个生命周期语义更弱的独立执行底座。本文也回答了 #2972 中悬而未决的方向问题("与 Claude Code 收敛到什么程度才合适?")。 |
| 7 | |
| 8 | ## 核心思想 |
| 9 | |
| 10 | 在后台(detached)运行智能体工作的东西**只有一个**:一个带有持久执行生命周期的**无头 Runtime worker**。它是一个模型循环,拥有完整的、受授权把关的工具面,并且可以通过同一套生命周期继续委派子工作。其他一切都只是选择、启动或观察这一个 Runtime 的方式。 |
| 11 | |
| 12 | ``` |
| 13 | ┌──────────────────────────────────────┐ |
| 14 | │ headless Runtime │ |
| 15 | │ execution · authority · lifecycle │ |
| 16 | │ can spawn child workers │ |
| 17 | └───────────────┬──────────────────────┘ |
| 18 | │ |
| 19 | ┌───────────────┴──────────────────────┐ |
| 20 | │ one durable execution substrate │ |
| 21 | └───────┬───────────────┬──────────────┘ |
| 22 | │ launches │ launches |
| 23 | ┌──────────────┴──────┐ ┌─────┴────────────────┐ ┌──────────────────────┐ |
| 24 | │ TUI turn │ │ `codewhale exec` │ │ Agent fleet │ |
| 25 | │ interactive, in-proc │ │ headless CLI │ │ identity · membership │ |
| 26 | │ │ │ full tools · stream │ │ · selection │ |
| 27 | └─────────────────────┘ └──────────────────────┘ └──────────┬───────────┘ |
| 28 | │ |
| 29 | └─ selects a Runtime worker |
| 30 | ``` |
| 31 | |
| 32 | - **子智能体**是面向用户的名称,指带有角色的*嵌套任务*(`explore`、`reviewer`、`implement`、`test` 等,规范的七种角色见 `docs/SUBAGENTS.md`)。它应当由与 Fleet 所选智能体相同的 Runtime worker 生命周期支撑。`agent` 是面向模型的启动器,不是第二个运行时。 |
| 33 | - **`codewhale exec`** 是无头入口:任何人在任何时候都可以使用(CI、脚本、另一个智能体),拥有完整工具,输出 `stream-json` 事件流,并且可以派生子智能体。它就是带 CLI 的那个运行时。 |
| 34 | - **Fleet 选中的智能体**以 Runtime 的一次 `codewhale exec` 运行来执行。Fleet 提供身份、成员和选择,不会重新实现执行。持久账本、调度/租约/重试、授权、本地或 SSH 传输,以及终态生命周期,都由 Runtime 负责。 |
| 35 | |
| 36 | 所以 "Fleet 还是子智能体" 并不是在两种执行底座之间二选一。Fleet 回答**谁**有资格并被选中,Runtime 回答已授权的工作**如何、在哪里**执行,子智能体则仍是嵌套任务的角色/UX 用语。 |
| 37 | |
| 38 | ## 切换规则 |
| 39 | |
| 40 | 如果一个后台(detached)的 `agent` 子级会因一次性的提供商(provider)超时而失败且不重试,而等价的 Runtime worker 会重试并保留账本证据,那么切换就没有完成。应把这视为 Codewhale Runtime 的缺口,而不是正常的 "子智能体行为"。 |
| 41 | |
| 42 | 兼容性的 `agent` 运行时现在会对提供商响应头、流和超时这类瞬时失败做带退避的重试,之后才把 worker 标记为中断;重试耗尽时会保留检查点并返回一个续接句柄。剩下的收敛工作,是让这套生命周期在进程重启、远程执行和完整的 Runtime 账本调度下保持持久。 |
| 43 | |
| 44 | 目标规则是: |
| 45 | |
| 46 | - 持久的或长时间运行的工作走 Runtime worker 生命周期; |
| 47 | - `agent` 应当把工作入队给 Runtime worker 运行或观察它,而不是自己拥有独立的生命周期; |
| 48 | - 进程内子级只允许作为小范围的兼容性/延迟优化,并且必须暴露与持久 Runtime 路径相同的终态、重试语义、回执和检视句柄。 |
| 49 | |
| 50 | 在产品语言里,说 "打开一个子智能体" 没问题。在架构语言里,这意味着 "以此角色启动一个嵌套的 Runtime worker",也可以选用 Fleet 选出的成员。 |
| 51 | |
| 52 | ## 为什么是这种形态(以及为什么它能解决卡顿) |
| 53 | |
| 54 | 起因是:派生大量进程内子智能体会让 TUI 卡顿,因为每个子级都会克隆一个沉重的运行时并重建整个工具注册表,*而且* TUI 会为每个子级渲染完整的卡片/对话记录(transcript)。 |
| 55 | |
| 56 | 调研 Claude Code、Codex 和 Kimi 之后发现,让编排器在高扇出下保持轻量的,**不是**进程边界——三者都在进程内运行子智能体。真正起作用的是**隔离 + 紧凑的事件流**: |
| 57 | |
| 58 | - 子级的对话记录**绝不**回流到父级——父级只拿到结果摘要和一条小的生命周期事件流; |
| 59 | - UI 渲染的是**计数**(`2 running / 3 done`),而不是每个 worker 一个子会话; |
| 60 | - 每个 worker 的工具面直接根据**角色/能力配置**构建,而不是"先全部构建再过滤"。 |
| 61 | |
| 62 | 因此,"无头" 的意思是*执行不再以 UI 为形状*,**并不**意味着能力变少。无头 worker 保留完整工具集,并且可以派生子智能体。 |
| 63 | |
| 64 | 当工作还需要**持久**(TUI 关闭、笔记本休眠后仍能继续)或**远程**(SSH)时,Runtime 会把 worker 作为 `codewhale exec` 在进程外运行。Fleet 可以提供被选中的智能体身份,但执行权和生命周期归属仍在 Runtime。这样,沉重的构造完全放在另一个进程里,编排器无论扇出多大都保持流畅,运行也能在重启后存活——这就是 #3154 的以天计的自主运行目标。 |
| 65 | |
| 66 | ## 单一递归轴 |
| 67 | |
| 68 | worker 从 `spawn_depth = 0` 起运行,只要满足 `spawn_depth + 1 ≤ max_spawn_depth` 就可以派生子级,所以预算 `N` 提供 `N` 层嵌套委派。子智能体和 Fleet 选中的 Runtime worker 共用**同一条**轴,其来源是 `codewhale_config`: |
| 69 | |
| 70 | - `DEFAULT_SPAWN_DEPTH = 3`:独立子智能体和 Fleet 选中的 Runtime worker 共同的默认预算(这样两者不会漂移成"两个移动靶"); |
| 71 | - `MAX_SPAWN_DEPTH_CEILING = 8`:需主动开启的上限,所有配置的 Runtime 值(包括 Fleet 执行配置中的 `max_spawn_depth`)都会被钳制到这个值。 |
| 72 | |
| 73 | 面向模型的 `agent` schema 有意不包含 `max_depth`。解析器仍然接受 `max_depth`、`maxDepth` 和 `max_spawn_depth`,以兼容已保存的对话记录、ACP/MCP 客户端和内部调用方,并拒绝大于 8 的值。当前由模型发起的调用继承 Runtime 的配置,而不是在工具 schema 里协商递归深度。 |
| 74 | |
| 75 | Workflow IR 另有一个默认的结构校验上限:最多五层嵌套节点。这个上限约束的是编排文档的形状,既不授予也不消耗 Runtime 的子级委派深度。 |
| 76 | |
| 77 | 根 worker 即使预算为 0 也总会运行;预算限制的是*子级*委派。默认值至少提供三层嵌套。 |
| 78 | |
| 79 | ## 事件词汇 |
| 80 | |
| 81 | Runtime 执行账本持久化的是 worker 自己的事件流,而不是另一套模拟出来的分类。兼容性 API 和类型仍以 `Fleet...` 前缀暴露它。`codewhale exec --output-format stream-json` 会输出 `{"type": "content" | "tool_use" | "tool_result" | "sandbox_denied" | "workflow_event" | "session_capture" | "turn_usage" | "metadata" | "done" | "error"}` 形式的行,它们映射到 Runtime 账本的兼容类型 `FleetWorkerEventPayload`(`RunningTool`、`WorkflowEvent`、`Running`、`Completed`、`Failed` 等)。`workflow_event` 在 Workflow 运行期间携带带类型的 run/phase/task/gate 回执,并作为带类型的 `WorkflowEvent` 保留在 Runtime 执行账本中;终态的 `done` 或 `error` 仍由外层 Runtime worker 负责。一套词汇,两个表面。 |
| 82 | |
| 83 | `session_capture` 在 exec 运行把自己的对话记录保存为会话时发出一次,并且只在一个位置携带可恢复的 id: |
| 84 | |
| 85 | ```json |
| 86 | {"type": "session_capture", "schema": "codewhale.exec-stream", "schema_version": 1, |
| 87 | "content": "<redacted:…>", "saved_session_id": "01J…"} |
| 88 | ``` |
| 89 | |
| 90 | - `saved_session_id` 是原始的已保存会话 id,只在保存成功之后才发出。对本地 Fleet worker,父级会分配一个新 ID,并共用 Runtime 现有的会话目录。只有当报告的恰好是这个 ID、且对应的已保存对话记录可以加载时,执行器才会公布 `FleetReceipt.saved_session_id`。拥有 Runtime API 访问权限的客户端随后可以通过 `GET /v1/sessions/{id}` 读取回复。SSH worker 保留其摘录和远程日志,但不会公布一个不可用的本地会话链接。id 只是查找键,不能代替 Runtime 的身份认证。 |
| 91 | - `content` 是与终态 `metadata.session_id` 相同的脱敏指纹,所以单独截获的 `metadata` 回执仍可安全写入日志,两个事件之间也仍能关联。因此 `metadata.resume_command` 指向的是这个字段(`codewhale exec --resume <session_capture.saved_session_id>`),而不是自己携带 id。 |
| 92 | |
| 93 | 终态 `metadata` 回执还携带 worker 可见的最终回答:`visible_final_answer_chars` 是最终助手回复的真实字符数,`visible_final_answer_excerpt` 是它的摘录,有长度上限(4,000 个字符,被截断时以 `...` 结尾),并已脱敏;当前回合没有产生可见回答时,该字段省略。恢复的回合绝不会复用旧的回复,失败或中断的回执可能携带当前回合的部分文本;以回执状态为准。Runtime 执行器从这个回执读取摘录——绝不从流式 `content` 增量读取,那些是运行过程中的"边想边说"——并把它附加到 `Completed.summary`;对于没有评分器、也没有文件产物的任务,还会把它作为该任务的交付物写入回执备注。生命周期事件标签和 worker 检视摘要只显示一小段摘录;事件 `payload` 和回执保留完整摘录。 |
| 94 | |
| 95 | `turn_usage` 是每次模型调用的用量回执:当提供商报告了该次调用的用量时,每个模型请求(回合内的步骤)发出一次: |
| 96 | |
| 97 | ```json |
| 98 | {"type": "turn_usage", "schema": "codewhale.exec-stream", "schema_version": 1, |
| 99 | "turn": 1, "input_tokens": 1200, "output_tokens": 180, |
| 100 | "reasoning_tokens": 90, "prompt_cache_hit_tokens": 900, |
| 101 | "prompt_cache_miss_tokens": 300, "prompt_cache_write_tokens": 0, |
| 102 | "reasoning_replay_tokens": 40, "duration_ms": 1834} |
| 103 | ``` |
| 104 | |
| 105 | - `turn` 是这次 exec 运行内该模型调用的序号,从 1 开始;`input_tokens`、`output_tokens` 和 `duration_ms` 始终存在。 |
| 106 | - 提供商没有报告的可选 token 字段会被**省略**——绝不输出为 null,也绝不用 0 回填。字段名与终态 `metadata` 回执保持一致:`prompt_cache_hit_tokens` 是提供商的缓存读取计数(Anthropic 的 `cache_read_input_tokens`),`prompt_cache_write_tokens` 是缓存创建计数(`cache_creation_input_tokens`)。`reasoning_tokens` 只出现在会报告它的提供商路径上(OpenAI 兼容的 `completion_tokens_details` / Responses 的 `output_tokens_details`;Anthropic 不报告思考 token 计数)。`reasoning_replay_tokens` 是对 DeepSeek V4 交错思考(interleaved-thinking)重放的客户端估算值。 |
| 107 | - 当提供商对某次调用完全没有报告用量时,该次调用的整个事件都会被跳过。做延迟/收敛分析时,应当对 `turn_usage` 事件求和,而不是根据墙钟时间去推断每一步的 token;终态 `metadata` 回执仍然携带累计总数。 |
| 108 | |
| 109 | ## 与 Claude Code 的收敛(#2972) |
| 110 | |
| 111 | Codewhale 应当在**形态**上与 Claude Code 收敛,而不是在品牌上: |
| 112 | |
| 113 | - **采纳**:带有真正的 CLI/SDK 入口的无头运行时;作为隔离运行、返回摘要(而非对话记录)的子智能体;紧凑的、事件驱动的扇出投影;能力/角色工具配置;技能生态(#2743);结构化的运行回执。 |
| 114 | - **保持不同**:Codewhale 品牌,以及对 DeepSeek/GLM/MiniMax 和多提供商的一等支持;本地优先的 **Agent fleet**,作为身份、成员和选择层;由 Runtime 负责的持久本地/SSH 执行和授权;以 Workflow 作为排序覆盖层。 |
| 115 | - **不要**按表面分叉执行语义。TUI、`agent`、`exec` 和 Runtime API 都必须驱动*同一个* Runtime,并观察*同一条*事件流。Fleet 的选择会传给这个 Runtime,而不是另建一条执行路径——正是这里的分歧造成了"两个移动靶",本文档的存在就是为了防止它。 |
| 116 | |
| 117 | 检验任何新智能体表面的试金石是:*它是启动并观察那唯一的运行时,还是另造了一个?* 只有前者被允许。 |
| 118 | |
| 119 | ## 历史说明:v0.9.0 之后还剩什么 |
| 120 | |
| 121 | 已归档的路线图快照——实时状态以 issue 跟踪器为准,而不是这份列表。2026-08-17 根据对较早的 0.9 时代文档的全面审计刷新。这些计划是证据,不是第二个真相来源。v0.9.0 整合了水下 shell(underwater shell)、消息优先的 Operate、权限姿态(permission postures)、已接通的 Workflow 引擎和持久运行日志、Lane CLI/运行时、带 `operate_ready` 的设置流程、宪章(constitution)再平衡,以及 ProviderLake/Models.dev。剩余工作属于后续版本: |
| 122 | |
| 123 | 1. **品牌重塑收尾**:`deepseek`/`deepseek-tui` 二进制 shim 及其 shim 发布资源已在 v0.9.0 移除;剩下的义务是 Homebrew `codewhale` formula 的推出(`docs/REBRAND.md`)。 |
| 124 | 2. **把 Operate 做成价值流**:在水下 shell 之上做一个控制面板表面(WIP、队列年龄、瓶颈);阶段历史(#4039);以 Workrooms Phase 2(#3209/#3210)作为收件箱底座;回执对账。 |
| 125 | 3. **流量控制**:真正的 WIP 上限和可见的队列(#4015、#4016),与已发布的 16 并发/1k 运行访问模型(#4292)协调一致。 |
| 126 | 4. **Fleet 身份与 Runtime/Workflow 收敛的遗留项**:实时 tmux/verifier-gate 自用验证,以关闭 #4175/#4177/#4178/#4179;Fleet 使用规范的 AgentProfiles 并选择成员,而由 Runtime 负责执行;Conductor/topology(#4010、#4012)作为延伸目标。 |
| 127 | 5. **TTC 设计实现**(设计文档在 `codewhale-ops` 中):已批准,v0.9.0 之后不再受阻。 |
| 128 | 6. **HarnessProfile 收尾**:状态/UX 展示线(`docs/rfcs/HARNESS_PROFILE_CUTLINE.md`)。 |
| 129 | 7. **文件拆分,已落地**:v0.9.0 时代的超大文件已拆开:`main.rs` 现在只是一个薄桩,`ui.rs` 已拆分成 `crates/tui/src/tui/` 下职责集中的模块(如今约 3.9k 行;`docs/rfcs/FILE_DECOMPOSITION_0_9_0.md` 中的数字是 0.9.0 时代的快照)。剩下的工作是 `POST_0_9_1_SEAMS.md` 中跟踪的"核心之上的薄 TUI"这一北极星目标。 |
| 130 | |
| 131 | 这些文档自己明确推迟的事项:外部工作流记忆(仅定边界)、自动 harness 演化、托管 workroom、`constitution_modules`(需要签字确认)、权限配置(#3211,需要设计),以及 plan 上限探测(需要产品决策)。 |
| 132 | |
| 133 | ## 外部 harness 的公开启动契约(#4641) |
| 134 | |
| 135 | 外部评测 harness(例如未来 Verifiers v1 的内置 harness)通过启动公开的 `codewhale exec` 入口来嵌入 Codewhale,并让它指向自己拥有的拦截端点。Codewhale 只拥有自己的**启动契约**;拦截、轨迹、模型调用计时、token 计量、重试、rollout 限制和运行时编排都归 harness。不要往 Codewhale 里添加 harness 运行时、轨迹解析器或回执 schema。 |
| 136 | |
| 137 | 可复现的无头启动只使用现有的通用接口: |
| 138 | |
| 139 | - 一份显式的临时配置,写明路由和凭据的**环境变量**,绝不写密钥本身: |
| 140 | |
| 141 | ```toml |
| 142 | provider = "openai" |
| 143 | |
| 144 | [providers.openai] |
| 145 | base_url = "" # the harness fills in its interception endpoint |
| 146 | model = "" # the harness fills in the target model |
| 147 | api_key_env = "VF_CODEWHALE_API_KEY" |
| 148 | ``` |
| 149 | |
| 150 | (`base_url` 由 harness 填入它的拦截端点;`model` 由 harness 填入目标模型。) |
| 151 | |
| 152 | - `CODEWHALE_HOME` 设为每次运行全新的目录; |
| 153 | - `CODEWHALE_SECRET_BACKEND=file`; |
| 154 | - `CODEWHALE_MCP_CONFIG` 指向一个为每次运行生成的 MCP JSON 文件,其中只包含 harness 提供的任务服务器(`{"mcpServers":{"task-tools":{"url":""}}}`;`mcpServers` 别名以及基于 URL 的 Streamable HTTP / SSE 传输已经存在); |
| 155 | - `CODEWHALE_MEMORY=false` 和 `CODEWHALE_TELEMETRY=false`。0.9.12 的源码默认开启用量计数,并提供退出开关。每个封闭的 harness 都要显式设置这个运行级的关闭开关,这样测试就不会从全新或复用的 home 中采集或发送数据。普通的已启用会话会把聚合计数发送到一个端点(`https://telemetry.codewhale.net/v1/telemetry`,即随发行版附带的默认值),而不是本地文件。这是一条硬底线:环境变量中显式的 "off" 优先于 `--telemetry true` 和配置里的 `telemetry = true`。如果 harness 希望已启用的 home 继续在本地缓冲、而不联系任何地方,请改为设置 `CODEWHALE_TELEMETRY_ENDPOINT=`(留空)。参见 [`docs/TELEMETRY.md`](./TELEMETRY.md); |
| 156 | - 仅当 harness 提供受信任的 `http://` 拦截端点时,才设置 `CODEWHALE_ALLOW_INSECURE_HTTP=1`(容器/隧道端点并不总是回环地址); |
| 157 | - 调用方提供时,再加上 `--append-system-prompt` 和 `--disallowed-tools`。 |
| 158 | |
| 159 | 拦截密钥只留在子进程环境中(通过路由的 `api_key_env` 解析);它绝不会被写入 argv、路由配置、日志、`stream-json` 流或任何生成的文件。 |
| 160 | |
| 161 | 确切的参数顺序如下: |
| 162 | |
| 163 | ```sh |
| 164 | codewhale \ |
| 165 | --config .vf-codewhale/config.toml \ |
| 166 | --workspace . \ |
| 167 | --no-project-config \ |
| 168 | --skip-onboarding \ |
| 169 | exec \ |
| 170 | --auto \ |
| 171 | --sandbox danger-full-access \ |
| 172 | --output-format stream-json \ |
| 173 | -- "<task prompt>" |
| 174 | ``` |
| 175 | |
| 176 | `--no-project-config` 必须出现在子命令**之前**(和 `--skip-onboarding` 一样)。公开的分发器会解析它,并把它转发到 TUI 子命令之前;随后 `Exec` 会跳过按工作区区分的 `[workspace]`/`[projects]` 用户配置叠加层,使配置面只取决于显式的 `--config`。`crates/tui/tests/integration/verifiers_harness_contract.rs` 是这份契约的、不依赖提供商的验收锁。 |
| 177 | |
| 178 | ### 未来的上游清单(不在本文范围内,不要执行) |
| 179 | |
| 180 | 真正把 Codewhale 加为内置 harness 的工作在外部的 Verifiers 仓库里进行;它所需要的、带校验和清单的公开且不可变的 Codewhale GitHub Releases,自 v0.9.1 起就已存在(最新已发布版本是 v0.9.13,发布于 2026-09-14;工作区源码版本是 0.9.13)。预计这项上游改动仅限于一个新的 `verifiers/v1/harnesses/codewhale/` 包,以及它的测试矩阵和文档注册:其中 `CodewhaleHarnessConfig` 固定目标发布版本,`setup()` 下载并校验已发布的归档,`launch()` 写入上文的临时路由/MCP 文件并调用 `runtime.run_program(...)`。 |
| 181 | |
| 182 | 明确**不**由这项契约工作完成的遗留事项:打标签、发布或创建 Codewhale release;打开或提交上游 Verifiers PR;运行其需要凭据的 E2E 矩阵;以及在确切的已发布归档尚未在该上游运行时里跑过之前,宣称对该运行时/架构的支持。 |
| 183 |