跳转至

06 · SDK 工厂与 AgentSession

6.1 从 createAgentSession() 找入口

研究入口:packages/coding-agent/src/core/sdk.ts,462 行。它是组装工厂,不是另一个推理算法。

flowchart TD O[CreateAgentSessionOptions] --> C[cwd 与 agentDir] C --> M[ModelRuntime] C --> S[SettingsManager + SessionManager] C --> R[ResourceLoader.reload] S --> H[恢复分支上下文与模型选择] M --> H R --> A[创建 Agent] H --> A A --> AS[创建 AgentSession] AS --> RET[session + extensionsResult + fallback warning]

6.2 选项的真正含义

选项 作用 常见误解
cwd 工作目录、发现资源、工具默认路径、会话分组 不是文件访问沙箱
agentDir 自己的配置/凭据目录 不是自动隔离账户
modelRuntime provider、认证和目录 不是仅一个 model ID
model 显式模型优先 找到对象不代表请求必成功
tools 初始允许的工具名列表 不是 AgentTool 实例列表
noTools all 或 builtin 不是 boolean
customTools 注册额外 ToolDefinition 仍受 tools / excludeTools 控制
resourceLoader 所有资源发现入口 不只是 skills
sessionManager canonical 会话树 不等同 state.messages
settingsManager 配置和运行覆盖 默认会加载个人配置

新产品应显式设置 cwd、agentDir、tools。否则默认配置可能包含你个人机器上的扩展与凭据,结果难以重现。

6.3 模型和推理级别恢复

先从 SessionManager 的 active branch 找 selection,不能只看最后一个 assistant 的 model:虚拟模型实际路由后,assistant 记录的是物理模型。

如果没有显式 model,就试着恢复会话选择;不可用时产生 fallback warning,再使用配置默认等选择策略。你的产品应把 fallback 告诉用户,避免「原本在本地模型、恢复后悄悄用了云服务」的行为被忽略。

thinking level 恢复或从设置取值,然后按模型能力 clamp。不是所有模型支持 xhigh 或 max;某些 provider 用 effort,另一些用 token budget,不应该把同一个字符串理解成完全一致的算力。

6.4 工具加载和图像处理

工厂构建允许和禁止集合,再由 AgentSession 将实际定义包装成 AgentTool。

blockImages 时 convertToLlmWithBlockImages 会过滤 user / toolResult 图片并添加占位文字,这是请求层防护;图像还会在输入和工具结果路径归一化。只在 UI 隐藏图片不能减少 payload,也不能防止 provider 收到它。

6.5 为什么还需要 AgentSession

Core 只知道消息和工具轮次。AgentSession 增加:

  • 模板和 skill 指令展开。
  • extension commands 和 input handlers。
  • 模型选择、虚拟路由、认证与 headers。
  • JSONL 存储、会话树、上下文编辑。
  • 自动 compaction、错误重试、溢出恢复。
  • 事件边界、嵌套工具与 settle。

这解释了文件为什么有 4348 行:许多业务边界集中在这里。学习时先理解其中的主干,再研究你实际需要的路径。

6.6 prompt() 的准确顺序

agent-session.ts 从 prompt() 的实现往下读:

  1. 若正在发 agent_settled,把新操作延迟处理,避免重入破坏状态。
  2. 优先尝试已注册 extension command,命令可以在 streaming 时执行。
  3. 手工 compaction 进行中则拒绝普通新 prompt。
  4. input handlers 可以消费或变换输入。
  5. 展开 /skill:name 和 prompt templates。
  6. 如果 streaming,必须明确 streamingBehavior: "steer" | "followUp",否则拒绝。
  7. 非 streaming 时 flush 待写消息,校验 model 和认证。
  8. 检查已有响应是否需要 compaction。
  9. before_agent_start 可改系统 prompt、工具、模型等。
  10. 用 _limitsModel() 当时可得的模型限制归一化图片:物理模型通常就是当前 selection;virtual selection 下优先取最近成功 response 的 routedModel,否则取当前 selection。
  11. 组装 user、custom、system 更新消息。
  12. _runAgentPrompt() 运行底层 Agent。

虚拟模型时序:图片处理之后,真正的本次 resolveModel() 才在 prepareRequest 阶段执行。上次路由到 A、本次转到 B,不意味着此前图片一定已经按 B 限制处理。不要把历史 routedModel 与本次最终路由模型合并成同一状态。

sequenceDiagram participant P as prompt 预处理 participant H as 历史成功 response participant R as prepareRequest / router P->>H: _limitsModel 读取 routedModel 或 selection H-->>P: 当前可用限制 P->>P: 归一化图片 P->>R: 组装请求后路由 R-->>P: 本次物理模型

你不能期待普通 /template 等同 executable command;模板最终仍是 user 文本。也不能让多个 web 请求不加协调地同时调用同一个 session.prompt。

6.7 canonical projection:每次请求重新建立事实

_installAgentRequestProjection() 安装 prepareRequest hook,从 SessionManager.buildSessionProjection() 取 messages,把 runtime tools 保留为执行集合,再处理虚拟模型路由。

所以:

session.agent.state.messages = importedHistory;

不能替代 SDK 历史导入。你应该提供已经包含正确 entries 的 SessionManager,或用正式 session runtime import API。

课程契约测试先存入 CANONICAL 历史,再把内存替成 MEMORY_ONLY,下一次 Faux Provider 断言前者存在、后者不进入请求。这是实际 1.0 的运行测试,不只是解释。

6.8 持久化顺序与屏障

_handleAgentEvent() 先处理嵌套调用和 queue 展示状态,向 extensions 和 public listeners 分发,再在 message_end 下持久化完整消息,建立消息到 entry ID 的映射。

整个处理函数作为 Core 的 awaited subscriber 完成后,低层才能继续工具预检。但 public session listener 的 message_end 到来本身,不保证此刻磁盘追加已经发生。 若业务依赖 persisted entry ID,应使用正式 boundary,而不要在任意 observer 中抢读。

_dispatchTurnEndBoundary() 对每个完成轮次解析 persisted assistant / tool result IDs,extensions 可以提交草稿 entry 与 continuation 决策;_commitBoundaryDrafts() 再统一写入。

这能避免把自定义消息插在 assistant 工具请求和其结果之间,形成无效的模型协议序列。

6.9 低层结束与真正稳定

_runAgentPrompt() 外面还有循环:

await agent.prompt
→ 检查可重试错误
→ 检查 overflow / compaction
→ 检查 agent_end 阶段新队列
→ agent_before_settle boundary
→ 必要时 agent.continue
→ agent_settled

如果 UI 在第一个 agent_end 就显示「全部完成」,可能马上又自动重试。使用 agent_settled 展示稳定状态;等待 prompt 的 Promise 获取整次接受运行完成。

Core 会把某些失败编码为消息,SDK 的一些前置校验会 throw。你的错误处理需要两者:try/catch + 完成后检查最终错误状态,而不是只处理 Promise rejection。

6.10 SDK 默认不自动启动 CLI 内建扩展

普通 createAgentSession 不等于运行完整 CLI。MCP、Code Mode、tool_search 要通过 DefaultResourceLoader.extensionFactories 显式加入,MCP 要 bindExtensions 触发 session_start。

如果你只要研究项目,禁用自动 extensions,用明确 tools allowlist 很合理;若需要 MCP,见第 09 与 14 章的启动配方。

6.11 正确清理

session.dispose() 会中止活动工作、取消资源、使扩展上下文失效并移除监听器。放在 finally。运行时 switch / fork 会换 session,旧 subscription 不会神奇绑定到新对象,宿主需要重新绑定。

const { session } = await createAgentSession({ cwd: "/path/to/project" });
try {
  await session.prompt("说明入口文件");
  const text = session.getLastAssistantText();
  console.log(text);
} finally {
  session.dispose();
}

源码:工厂、请求投影 L759、事件处理 L1074、运行恢复 L1775。