06 · SDK 工厂与 AgentSession¶
6.1 从 createAgentSession() 找入口¶
研究入口:packages/coding-agent/src/core/sdk.ts,462 行。它是组装工厂,不是另一个推理算法。
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() 的实现往下读:
- 若正在发 agent_settled,把新操作延迟处理,避免重入破坏状态。
- 优先尝试已注册 extension command,命令可以在 streaming 时执行。
- 手工 compaction 进行中则拒绝普通新 prompt。
inputhandlers 可以消费或变换输入。- 展开
/skill:name和 prompt templates。 - 如果 streaming,必须明确
streamingBehavior: "steer" | "followUp",否则拒绝。 - 非 streaming 时 flush 待写消息,校验 model 和认证。
- 检查已有响应是否需要 compaction。
before_agent_start可改系统 prompt、工具、模型等。- 用
_limitsModel()当时可得的模型限制归一化图片:物理模型通常就是当前 selection;virtual selection 下优先取最近成功 response 的 routedModel,否则取当前 selection。 - 组装 user、custom、system 更新消息。
_runAgentPrompt()运行底层 Agent。
虚拟模型时序:图片处理之后,真正的本次 resolveModel() 才在 prepareRequest 阶段执行。上次路由到 A、本次转到 B,不意味着此前图片一定已经按 B 限制处理。不要把历史 routedModel 与本次最终路由模型合并成同一状态。
你不能期待普通 /template 等同 executable command;模板最终仍是 user 文本。也不能让多个 web 请求不加协调地同时调用同一个 session.prompt。
6.7 canonical projection:每次请求重新建立事实¶
_installAgentRequestProjection() 安装 prepareRequest hook,从 SessionManager.buildSessionProjection() 取 messages,把 runtime tools 保留为执行集合,再处理虚拟模型路由。
所以:
不能替代 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 不会神奇绑定到新对象,宿主需要重新绑定。