跳转到内容

07. Claude 后端执行链

ClaudeAgent 是 Claude Agent SDK 的产品化适配器。它不自己实现模型循环,而是在 SDK query 周围加入 Craft 的 prompt、source、session tools、权限、恢复、事件归一化与持久生命周期。

源码主文件:packages/shared/src/agent/claude-agent.ts

flowchart LR
SM["SessionManager"] --> CA["ClaudeAgent"]
CA --> BA["BaseAgent modules"]
CA --> SDK["Claude Agent SDK query()"]
CA --> EA["ClaudeEventAdapter"]
SDK --> H["PreToolUse / PostToolUse hooks"]
H --> P["runPreToolUseChecks"]
SDK --> ST["session MCP tools"]
SDK --> MT["MCP proxy tools"]
SDK --> EA
EA --> EV["AgentEvent"]
EV --> SM

构造阶段保存 config、建立 BaseAgent modules、创建 event adapter,并读取 branch 参数。它不急着调用 SDK query。

postInit() 位于 claude-agent.ts,负责首 turn 前的准备:

  • 解析 Anthropic API key/OAuth/Bedrock 等 auth env;
  • 清理冲突的 provider routing 环境变量;
  • 检查 SDK 可执行 runtime;
  • 建初始 source/session tools;
  • 返回 PostInitResult,让 SessionManager 将 auth warning 告知 UI。

Auth 注入必须在 query 子进程启动前完成,因为 SDK 往往从环境读取 credential。

入口:ClaudeAgent.chatImpl。可以拆成七段:

  • 防止重入;
  • 设置当前 abort controller/query;
  • 清理上轮临时状态;
  • 消费一次性 recovery/branch/transfer context;
  • 刷新 model/thinking/source 配置。
  • 用户文本;
  • attachments 转 SDK text/image/document block;
  • stable/volatile context;
  • skill/source mentions;
  • provider resume/fork options。

Claude 路径把 Craft context blocks 放在 user-turn tail;这是为了配合 SDK system prompt/resume 行为,具体缓存差异在第 10 章讲。

关键项:

  • model、cwd、env、system prompt;
  • allowed tools / MCP servers;
  • thinking/effort;
  • hooks;
  • permissionMode: bypassPermissions 风格配置;
  • resume/fork;
  • abort controller;
  • persistent streaming input 选项。

“bypass SDK permission”听起来危险,但这里是把权限决定权移到 Craft 的统一 PreToolUse,不是无条件放行。

普通模式每 turn 调 query({prompt, options});keep-background-tasks-alive 模式可使用 createPushableInputStream() 建一个长期 query,后续 turn 把 SDKUserMessage push 进去。

ClaudeEventAdapter 把 system/assistant/user/result/stream message 映射为 AgentEventToolIndex 用 toolUseId 关联 start/result,并提取 workflow 子 agent 事件。

对无效 resume、branch cutoff 锚点缺失、auth 刷新、上下文溢出等做有限 fallback;每次 fallback 要清理对应 provider state,避免下次启动无限重试同一个坏句柄。

记录 usage/session id,排空 source activation,处理未投递 steer,yield complete/error,清 query 引用。

普通恢复:

{ resume: this.sessionId }

严格分支:

{
resume: branchFromSdkSessionId,
forkSession: true,
resumeSessionAt: branchFromSdkTurnId
}

源码:claude-agent.ts

branchFromSdkCwd 也必须生效,因为 SDK 从按 cwd hash 的目录找 parent transcript。若目标 cwd 在当前机器不存在,backend 会通知上层原子清掉全部 branch fork metadata,转 seeded/fallback;只清 sdkSessionId 会导致重启后坏 branch 字段再次复活。

ensureBranchReady() 会在创建 UI session 前实际触发/验证 fork,见 claude-agent.ts

SDK hook 位于约 1260–1520。流程:

sequenceDiagram
participant SDK as Claude SDK
participant CA as ClaudeAgent hook
participant C as Central checks
participant UI as Permission callback
SDK->>CA: PreToolUse(tool_name, input)
CA->>CA: normalize / image-size preflight
CA->>C: runPreToolUseChecks(context)
alt allow
C-->>CA: allow or modified input
CA-->>SDK: continue
else block
C-->>CA: block(reason)
CA-->>SDK: deny
else ask
C-->>CA: ask(request metadata)
CA->>UI: permission_request
UI-->>CA: approve / deny
CA-->>SDK: continue / deny
end

在中央检查前还有一个图片 Read guard:过大的图片一旦进入 SDK tool result,可能已经超过 API base64 限制,所以必须在执行 Read 前请求 host resize 或阻止。

代码仍提供 canUseTool callback,但注释说明所有真实权限逻辑在 PreToolUse hook。它主要满足 SDK API/兜底,不应成为第二套 policy。若未来把某些检查塞回 canUseTool,Claude 与 Pi 就会产生安全漂移。

Claude SDK 可直接接 createSdkMcpServer()。Craft 每次构建 fresh wrapper,但复用内部 tool definitions,避免 SDK transport 报 “Already connected to a transport”。

这些工具包括 plan、config/source validation、OAuth/credential、browser、spawn session、call_llm、session status/labels、task 与 inter-session message。

具体业务通过 callback registry 回到当前 session 的 SessionManager,不让工具闭包长期持有已销毁 manager 状态。

Source 的远程 MCP/API connection 由主进程 McpClientPool 持有,Claude 看见 proxy tool definition。执行 proxy 时回 pool;这样:

  • source token 刷新可重连 pool;
  • Claude/Pi 工具列表一致;
  • credential 不必写 SDK cache 文件;
  • 多 session 可复用连接生命周期策略。

ClaudeEventAdapter 负责把 SDK message 变成稳定事件:

  • assistant content text → delta/complete;
  • tool_use → tool_start
  • tool_result → tool_result
  • result usage → complete.usage
  • task notification → background/workflow events;
  • SDK error → typed Craft error。

把映射从巨型 chat loop 抽出来有两个价值:provider SDK schema 变化集中修,且可用纯 fixture 测试事件转换。

Claude SDK 的某些 background task notification 会在原 turn 结束后到达。若每 turn query 都销毁,通知会丢。代码支持长期 streaming-input query:

first turn: create input stream + query()
next turn: input.push(user message)
between turns: query 仍接收 task notification
destroy: input.end() + abort query

SessionManager 通过 background event sink 接收没有活跃 for await 消费者时的事件。

这增加了复杂性:currentQuery 必须始终指向 live query,redirect/abort 不能打到一个已结束的临时对象。

Claude 没有像 Pi 一样直接 session.steer() 的通用路径。实现会保存 pending steer message,并尝试在下一个 PreToolUse hook 通过 additional context 注入;若 turn 再也不调用工具,消息可能无法送达,于是结束时发 steer_undelivered,让 SessionManager/UI 明确处理,而不是静默丢消息。

源码:redirect

这也解释了为什么 connection 默认可以选择 queue:原生 steer 能力并不对称。

可能原因:

  • transcript 不存在/跨机器;
  • sdkCwd 不匹配;
  • SDK schema 升级;
  • parent branch anchor 不存在。

处理原则:

  1. 识别可恢复错误;
  2. 清失效 sdk id;
  3. 从最近持久消息构建隐藏 recovery context;
  4. 新 SDK session 重试一次;
  5. 把新 sdk id 回写 Craft session。

优先 retry/fallback 到已建立 child fork 或 seeded summary;不能继续把 parent 完整未来上下文带入,否则违反硬截止。

resolveClaudeThinkingOptions() 处理:

  • Claude adaptive thinking;
  • Haiku/非 Claude 模型差异;
  • Mythos/Fable 类 always-on thinking;
  • off/low/... 到 effort/max tokens;
  • mini completion 强制降低 thinking。

它不是简单 thinkingLevel === off ? disabled,因为有些模型拒绝 disabled。支持 1M context 的 Opus 还由 config 开关控制,以平衡能力和 quota。

runMiniCompletion 要求 miniModel,用最小 query options 消费最终文本。它服务:

  • 标题;
  • conversation/branch summary;
  • large response summary;
  • call_llm 等辅助动作。

错误通常返回 null 或向上抛(取决于调用语义),不能把失败当主会话 error。

destroy() 需要:

  • abort current query;
  • end persistent input;
  • 清 tool/session callback 与 source activation;
  • dispose event adapter/index;
  • 调 BaseAgent cleanup;
  • 防止迟到 SDK event 被继续投影。

源码:claude-agent.ts

优点:

  • 充分利用 SDK 原生 agent loop、resume/fork;
  • 权限和事件被拉回产品统一层;
  • persistent input 支持跨 turn background;
  • recovery 清楚区分 provider transcript 与 Craft messages。

代价:

  • hooks、SDK message types、query 生命周期耦合较深;
  • provider SDK 的重复/乱序事件需要 ToolIndex 去重;
  • redirect 不是完全原生,存在“没有下一个 PreToolUse”的不可投递窗口;
  • chatImpl 分支仍很大,升级 SDK 需要高强度回归。

ClaudeAgent 的本质是“in-process 控制器 + SDK 子进程协议的产品适配层”。它把 Craft 的稳定语义注入 SDK hooks,再把 SDK 输出归一化回来。

下一章看完全不同的 Pi 路线:为什么再加一层独立 Node/Bun 子进程,以及 parent/subprocess 如何通过双向 JSONL 实现工具、权限、steer 与压缩。