跳转到内容

06. AgentBackend 与动态路由

Craft Agents 要在同一个 session 产品模型里运行 Claude、OpenAI/Codex、Google、Copilot 等连接。它没有让 SessionManager 到处判断 provider,而是把差异集中在 AgentBackend、factory 和 driver/runtime resolver。

backend/types.ts 明确四个目标:

  1. provider-agnostic events;
  2. capability-driven UI;
  3. callback wiring,而非 backend 依赖外层;
  4. 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

Agent 执行不是单响应,而是交错序列:

status
text_delta*
tool_start
permission_request?
tool_result
text_complete
...
complete | error

AsyncGenerator<AgentEvent> 有几个优点:

  • 天然支持背压和 for await
  • backend 内部可以等待 SDK、子进程或 callback;
  • SessionManager 用同一循环做持久化和事件投影;
  • 测试可构造有限事件流;
  • 不把 Node EventEmitter 的订阅/解绑泄漏给上层。

但 background task 可能在 turn generator 结束后继续发事件,因此接口另有可选 setBackgroundEventSink(),专门覆盖“between turns”事件。

接口把几个容易混淆的动作分开:

构造完成、callback 已接好,但首条 chat 前执行。用于注入 auth、准备 provider config、返回可向用户展示的 auth warning。

分支创建时 preflight。普通 backend 可 no-op;Claude/Pi 要实际建立/验证 provider fork,防止假分支。

释放 SDK query、subprocess、watcher、session tools 等。不是删除产品 session。

可选异步钩子,允许子进程真正退出后再构造新 backend,避免短时间堆积。

接口故意没有一个万能 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 注入细节。

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。

源码:CoreBackendConfig

这是一种“显式 context object + callback ports”。好处是 backend 可在 shared 包中;代价是 config 很大,字段组合必须验证。

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,避免复制所有产品规则。

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。

connection 通常按:

session.llmConnection
> workspace default connection
> global default connection

模型也会结合 session override 与 connection 默认解析。完成后必须验证 model 是否属于该 connection/provider 的可用集合,或由 custom endpoint 配置允许动态注册。

源码定位:resolveLlmConnectionresolveBackendContext

当前 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 会出错。

Backend 暴露当前 model、thinking、provider/capability 信息。UI 应据此决定:

  • 能否选择 reasoning level;
  • 是否支持 image;
  • context window;
  • 是否支持 native branch/steer;
  • 可选 model 列表。

能力是 runtime 事实,不应仅用品牌名硬编码。自定义 endpoint 的 supportsImagescontextWindow 就是例子。

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 是否允许切换。

接口区分:

  • setSources/update sources:产品层 enabled source 与工具集合;
  • applyBridgeUpdates(context):某些 provider bridge 的 config/token/server URL 更新;
  • McpClientPool:真正持有连接;
  • proxy tool registration:backend 看见的 schema。

因此 source 热更新不是给 backend 塞一份新数组就结束,而是一条 reconcile 链。

标题、conversation summary、large response summary、task input compression 都不应启动完整 agent turn。runMiniCompletion() 复用同 connection/auth,但:

  • 使用 miniModel
  • 工具集最小或无工具;
  • 有独立 timeout/error 处理;
  • 返回 string | null,让非关键摘要可降级。

这把“辅助模型调用”留在 provider backend 内,避免上层再实现一套 auth/router。

接口保留了 branch anchor、pool server URL、persistent background sink 等看似 provider-specific 的概念。这不是失败,而是避免“最低共同能力陷阱”。好的抽象不等于所有实现完全相同,而是:

  1. 上层核心语义统一;
  2. 差异通过明确 capability/optional method 暴露;
  3. 不让 provider 类型蔓延到 UI 和 SessionManager 每个分支。
  1. 注册 driver/factory mapping,不靠 model 前缀猜。
  2. 实现 chat() 到统一 AgentEvent 的完整映射。
  3. 进入中央 pre-tool-use 与 session tools,不另造权限语义。
  4. 明确 abort、handoff、redirect 的行为。
  5. 保存/清理 provider session ID。
  6. 说明是否支持严格 branch,不能支持就明确 fallback。
  7. 支持 source proxy tools 和动态变化。
  8. 实现 mini completion 与 auth refresh。
  9. 给 runtime update 返回诚实能力结果。
  10. 覆盖 destroy、subprocess crash、resume failure 与 usage mapping 测试。

AgentBackend 统一的是 Craft 产品需要的“会话执行生命周期”,而不是强行统一每家 SDK 的全部 API。Factory 再把 connection/provider/model 解析与实例化分开,使 runtime 切换可验证、可锁定、可热更新。

接下来分别进入两个真实实现。先看 in-process 的 ClaudeAgent 如何把 SDK query、hooks、persistent input 与统一事件接起来。