06. AgentBackend 与动态路由
Craft Agents 要在同一个 session 产品模型里运行 Claude、OpenAI/Codex、Google、Copilot 等连接。它没有让 SessionManager 到处判断 provider,而是把差异集中在 AgentBackend、factory 和 driver/runtime resolver。
6.1 抽象目标
Section titled “6.1 抽象目标”backend/types.ts 明确四个目标:
- provider-agnostic events;
- capability-driven UI;
- callback wiring,而非 backend 依赖外层;
AsyncGenerator统一流式接口。
最核心签名:
interface AgentBackend { chat(message, attachments?, options?): AsyncGenerator<AgentEvent> abort(reason?): Promise<void> forceAbort(reason: AbortReason): void redirect(message: string): boolean interruptForHandoff(reason: AbortReason): void runMiniCompletion(prompt: string): Promise<string | null> postInit(): Promise<PostInitResult> ensureBranchReady(): Promise<void> destroy(): void}完整接口在 AgentBackend。
6.2 为什么用 AsyncGenerator
Section titled “6.2 为什么用 AsyncGenerator”Agent 执行不是单响应,而是交错序列:
statustext_delta*tool_startpermission_request?tool_resulttext_complete...complete | errorAsyncGenerator<AgentEvent> 有几个优点:
- 天然支持背压和
for await; - backend 内部可以等待 SDK、子进程或 callback;
- SessionManager 用同一循环做持久化和事件投影;
- 测试可构造有限事件流;
- 不把 Node EventEmitter 的订阅/解绑泄漏给上层。
但 background task 可能在 turn generator 结束后继续发事件,因此接口另有可选 setBackgroundEventSink(),专门覆盖“between turns”事件。
6.3 生命周期不是只有 chat()
Section titled “6.3 生命周期不是只有 chat()”接口把几个容易混淆的动作分开:
postInit()
Section titled “postInit()”构造完成、callback 已接好,但首条 chat 前执行。用于注入 auth、准备 provider config、返回可向用户展示的 auth warning。
ensureBranchReady()
Section titled “ensureBranchReady()”分支创建时 preflight。普通 backend 可 no-op;Claude/Pi 要实际建立/验证 provider fork,防止假分支。
destroy()/dispose()
Section titled “destroy()/dispose()”释放 SDK query、subprocess、watcher、session tools 等。不是删除产品 session。
disposeForRestart()
Section titled “disposeForRestart()”可选异步钩子,允许子进程真正退出后再构造新 backend,避免短时间堆积。
6.4 中断语义
Section titled “6.4 中断语义”接口故意没有一个万能 stop():
| 方法 | 语义 | 典型调用者 |
|---|---|---|
abort(reason?) |
正常请求 SDK 停止 | 用户 cancel |
forceAbort(AbortReason) |
硬停止与明确原因 | teardown、redirect fallback |
interruptForHandoff() |
plan/auth 等 UI 接管 | SessionManager handoff |
redirect(message) |
中途改变当前运行 | midstream steer |
redirect() 返回 boolean 是一个很聪明的能力协商:
true:backend 原生 steer 成功,旧 generator 继续产事件;false:backend 已 abort 或无法 steer,上层必须 queue + 新 turn。
上层因此不必知道 Pi 的 session.steer() 或 Claude 的 hook 注入细节。
6.5 配置结构
Section titled “6.5 配置结构”CoreBackendConfig 不是只含 model/key。它传入:
- workspace/session;
- model、miniModel、thinking;
- host runtime paths;
- headless/debug/config watcher;
- per-session env override;
- MCP pool 与 pool server URL;
- initial sources;
- SDK id/branch invalidation callback;
- recovery/branch seed/transfer summary provider;
- image resize host callback;
- automation system;
- plan/auth/source/browser/session callbacks。
这是一种“显式 context object + callback ports”。好处是 backend 可在 shared 包中;代价是 config 很大,字段组合必须验证。
6.6 BaseAgent:共享能力模块
Section titled “6.6 BaseAgent:共享能力模块”ClaudeAgent 与 PiAgent 都继承 BaseAgent。BaseAgent 组合:
PermissionManager:pending approval/remembered approval;SourceManager:source selection/activation;PromptBuilder:稳定/易变/recovery 上下文;PathProcessor:路径与附件处理;ConfigWatcher:可选运行时配置观察;UsageTracker:token/cost;PrerequisiteManager:skill/source 文档前置读取。
它还统一:
- callback setter;
- model/thinking/source 基础状态;
chat()外壳与 processing 标志;- session tool 名称/mini agent tool 集;
- destroy cleanup。
子类实现 chatImpl() 和 provider-specific lifecycle,避免复制所有产品规则。
6.7 Factory 两阶段解析
Section titled “6.7 Factory 两阶段解析”backend/factory.ts 不直接按 model 字符串 if/else new,而是:
resolveBackendContext(workspace, session/options) → resolve connection slug → load connection config → provider type + auth type + model → resolve backend kind/driver → build runtime-specific config
createBackendFromResolvedContext(context, callbacks) → instantiate registered backend → wire callbacks → return backend + resolved metadata分两阶段的价值:SessionManager 可以先检查“是否换 backend/是否锁 connection/是否需 restart”,再真正创建昂贵 runtime。
6.8 Connection 解析优先级
Section titled “6.8 Connection 解析优先级”connection 通常按:
session.llmConnection > workspace default connection > global default connection模型也会结合 session override 与 connection 默认解析。完成后必须验证 model 是否属于该 connection/provider 的可用集合,或由 custom endpoint 配置允许动态注册。
源码定位:resolveLlmConnection、resolveBackendContext。
6.9 Provider 与 backend 不是同义词
Section titled “6.9 Provider 与 backend 不是同义词”当前 factory 的关键映射是:
- Anthropic direct →
ClaudeAgent; pi/pi_compat路径 →PiAgent;- OpenAI/Codex、Google、Copilot、自定义 endpoint 可由 Pi runtime 的 model/provider registry 承载。
这说明 provider type 是 credential/API 语义,backend 是执行引擎语义。多个 provider 可以共享 Pi agent loop;同一 model 名也可能在不同 connection 下有不同 endpoint/auth。
不要把 model.startsWith('gpt') 当作 backend 路由,否则 custom model 与兼容 endpoint 会出错。
6.10 Capability-driven UI
Section titled “6.10 Capability-driven UI”Backend 暴露当前 model、thinking、provider/capability 信息。UI 应据此决定:
- 能否选择 reasoning level;
- 是否支持 image;
- context window;
- 是否支持 native branch/steer;
- 可选 model 列表。
能力是 runtime 事实,不应仅用品牌名硬编码。自定义 endpoint 的 supportsImages、contextWindow 就是例子。
6.11 Runtime update
Section titled “6.11 Runtime update”updateRuntimeConfig(update) 返回 boolean:
true:backend 已原地应用;false/未实现:SessionManager 应在 idle 时重建。
更新对象包含 model、provider/auth type、baseUrl、Pi auth provider、custom endpoint/models 等。不同 backend 可接受的原地范围不同:Pi subprocess 可收到 update_runtime_config;某些改变仍要求重启。
返回值比盲目 mutate 更诚实:backend 自己最清楚活跃 SDK session 是否允许切换。
6.12 Bridge update 与 source update
Section titled “6.12 Bridge update 与 source update”接口区分:
setSources/update sources:产品层 enabled source 与工具集合;applyBridgeUpdates(context):某些 provider bridge 的 config/token/server URL 更新;McpClientPool:真正持有连接;- proxy tool registration:backend 看见的 schema。
因此 source 热更新不是给 backend 塞一份新数组就结束,而是一条 reconcile 链。
6.13 Mini completion
Section titled “6.13 Mini completion”标题、conversation summary、large response summary、task input compression 都不应启动完整 agent turn。runMiniCompletion() 复用同 connection/auth,但:
- 使用
miniModel; - 工具集最小或无工具;
- 有独立 timeout/error 处理;
- 返回
string | null,让非关键摘要可降级。
这把“辅助模型调用”留在 provider backend 内,避免上层再实现一套 auth/router。
6.14 抽象泄漏是刻意的
Section titled “6.14 抽象泄漏是刻意的”接口保留了 branch anchor、pool server URL、persistent background sink 等看似 provider-specific 的概念。这不是失败,而是避免“最低共同能力陷阱”。好的抽象不等于所有实现完全相同,而是:
- 上层核心语义统一;
- 差异通过明确 capability/optional method 暴露;
- 不让 provider 类型蔓延到 UI 和 SessionManager 每个分支。
6.15 新增 backend 的检查表
Section titled “6.15 新增 backend 的检查表”- 注册 driver/factory mapping,不靠 model 前缀猜。
- 实现
chat()到统一AgentEvent的完整映射。 - 进入中央 pre-tool-use 与 session tools,不另造权限语义。
- 明确 abort、handoff、redirect 的行为。
- 保存/清理 provider session ID。
- 说明是否支持严格 branch,不能支持就明确 fallback。
- 支持 source proxy tools 和动态变化。
- 实现 mini completion 与 auth refresh。
- 给 runtime update 返回诚实能力结果。
- 覆盖 destroy、subprocess crash、resume failure 与 usage mapping 测试。
6.16 本章小结
Section titled “6.16 本章小结”AgentBackend 统一的是 Craft 产品需要的“会话执行生命周期”,而不是强行统一每家 SDK 的全部 API。Factory 再把 connection/provider/model 解析与实例化分开,使 runtime 切换可验证、可锁定、可热更新。
接下来分别进入两个真实实现。先看 in-process 的 ClaudeAgent 如何把 SDK query、hooks、persistent input 与统一事件接起来。