Skip to content
签到签到

DeepSeek Harness 源码与架构学习教程(全书模式)

本页由 book.json 与分章真源自动生成,请勿直接编辑。建议使用浏览器打印功能导出 PDF。


0. 分析范围与可信度说明

本教程以只读方式分析本地源码 <DEEPSEEK_HARNESS_SOURCE>,锁定 master 提交 4e84901e6471b79ec0338099867ebb4606d12bb5,版本 0.1.2-alpha.4。教程站真源位于 本站工作区;未修改上游仓库及其中现有未跟踪文件。

版本变化

项目旧教程基线当前基线结论
版本0.1.0-rc.50.1.2-alpha.4开发预览版本继续演进
提交47f943859bef60e4160492346772ded9b24f765a4e84901e6471b79ec0338099867ebb4606d12bb5全量复核,不只换行号
变化8,212 files;+448,477 / -161,788目录与职责发生实质变化
workspace packages221251新包在架构图和源码地图分层说明
Clientclient/runtimeclient/web-reactapi/session-controllerui-conversationui-renderer旧路径已移除
工具程序化调用旧 Code Mode 叙述当前 PTC 调度、日志和错误语义run_code 心智模型废弃

实际使用的能力

  • 阅读 README、package.json、lockfile、Profile/Bundle/Patch、源码、测试和中文开发文档。
  • 使用 rg、Git 只读命令搜索文件、符号与版本差异。
  • 核对 CLI、Session、Agent Loop、System Prompt、LLM、Tools、Session Controller、Conversation Assembler、UI Renderer 的当前入口。
  • 用 Archify 根据真实源码证据生成并校验 10 张独立图。
  • 【已执行并验证】11 个定向测试文件共 660 个用例通过;pnpm dsh --help 与隔离 DSH_HOME 的 Web 默认配置 dump 通过。
  • 【仓库声明但未执行】真实模型调用、全仓测试、发布与部署流程。

可信度分类

  • 【已验证】:由当前提交源码、配置、测试或本轮实际命令直接证明。例如 ReactLoopAgent 位于 packages/core/agent-loop/src/agent.ts:70SessionController.page() 位于 packages/api/session-controller/src/index.ts:369
  • 【合理推断】:基于结构作出的教学归纳,例如把 Client Session 类比前端外部 Store;类比不代表仓库原文。
  • 【待确认】:需要真实 API Key、外部服务、用户环境或发布权限的行为。

文档与源码差异

旧教程的主要失配是已移除 Client 包、旧快照职责边界、旧持久化描述和旧 Code Mode 语义。本版以当前源码为准;full.md 不手工编辑,而由分章真源通过 pnpm prepare:learning 生成。

保护边界

上游仓库中已有未跟踪学习文档和 learning/ 目录,它们不属于本站真源,未读取后写回、未删除、未加入 Git。所有教程修改、图规格和 HTML 仅落在学习站仓库。


1. 项目全景概览

1.1 一句话解释

DeepSeek Harness 是一个“一切皆插件”的 AI Agent 运行平台,它把模型调用、工具执行、会话记录、Agent 主循环、权限和 UI 都组织成可替换、可热更新的 Cordis 插件。

依据:

  • README.mdIt uses an architecture where everything is a plugin
  • docs/architecture.mdEvery part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself

1.2 它解决什么问题

传统上,要做“能调用工具的大模型 Agent”,开发者通常会手写一个循环:

text
读用户输入 -> 拼 Prompt -> 调模型 -> 看模型是否要求调用工具 -> 执行工具 -> 把结果塞回上下文 -> 再调模型

如果只有一两个工具,这种方式可行。但能力多了以后,问题会迅速出现:

  • 工具、模型、文件系统、子进程、权限、持久化、UI 都耦合在同一个循环里。
  • 每增加一种能力,都要改主循环或复制分支。
  • 无法只替换模型厂商、沙箱或文件系统,而不影响其他能力。
  • 流式输出、取消、断线重连、会话恢复、回放和审计没有统一的真相来源。
  • 前后端和自动化协议各自实现一套,容易类型漂移。

DeepSeek Harness 用四个核心设计回应这些问题:

  1. Cordis 插件化上下文:能力作为服务注册到 ctx,依赖关系通过 inject 声明,注册都是可撤销 effect。
  2. capability seam 三件套Service Definition / Service Provider / Consumer 把“接口、实现、调用方”分离。
  3. event-sourced Session log:只追加的 SessionEvent 是唯一真相,模型上下文从日志派生。
  4. profile + bundle + patch:同一个内核通过配置层组合成 webheadless、ACP 等不同运行形态。

1.3 没有它时,开发者通常怎样解决

常见做法是使用某个模型的 SDK 自己写 Agent loop:

  • fetch 或 OpenAI 兼容 SDK 调模型。
  • JSON.parse 判断工具调用。
  • 用一个大的 switch 或工具注册表执行工具。
  • 用内存数组保存消息。
  • 需要恢复时自己设计 JSON 文件或数据库。
  • 需要 Web UI 时再写一套 REST/WebSocket 协议。

这种方案在 demo 中很快,但缺少统一的事件系统、可撤销注册、热更新、会话恢复、工具策略和跨进程类型安全。

1.4 目标用户

  • 想构建或扩展 AI Agent 产品的工程师。
  • 想给 Agent 增加自定义工具、模型适配器、权限策略或 UI 的插件作者。
  • 想理解“Agent Harness 应该有哪些组成”的架构学习者。

1.5 典型使用场景

  • 通过 dsh --profile web 启动一个本地 Web 编码 Agent。
  • 通过 dsh --profile headless "task" 跑一次性任务。
  • 通过 ACP 自动化服务器把 Agent 暴露给外部自动化客户端。
  • 在自己的 cordis.yml 中组合工具、模型、沙箱和 UI 插件,形成定制 Agent。
  • 增加 dsh-tool-* 插件,让模型读写文件、执行命令、搜索网络、启动子 Agent。

1.6 核心价值

  • 可替换性:换模型、换沙箱、换持久化后端、换 Agent 驱动器,不改主循环。
  • 可回放性:模型可见内容都能从 Session log 重建。
  • 可组合性:同一个内核通过 profile 组合成多种产品形态。
  • 可观测与可测试:关键路径有 typed events、invariant、unit/snapshot/e2e 测试。
  • 前端同样插件化:浏览器端也运行一棵 Cordis 插件树,UI 通过 slot 动态组合。

1.7 它不适合解决什么

  • 它当前是 developer preview,兼容性明确不稳定,不适合当作稳定生产底座直接锁定。
  • 它不是某个具体 Agent 产品的即开即用替代品,理解成本比简单 SDK 调用高。
  • 它不负责训练、模型推理服务、向量数据库或复杂的多人权限体系。
  • 如果你只需要一个一次性脚本调用模型,它带来的框架成本可能过高。

1.8 与主要同类方案的差异

基于当前仓库文档可以确认其定位是“插件化 Agent Harness”,而不是单纯的 SDK:

  • 相对于直接使用模型 SDK:Harness 提供了 loop、工具管线、会话日志、持久化、UI 和协议边界。
  • 相对于普通后端 MVC 服务:Harness 以 typed events 和插件 effect 作为主要扩展方式,而不是在 Controller/Service 里加分支。
  • 相对于前端微服务/微前端:它用同一套 Cordis 插件模型同时组织 Host 和 Browser 两棵树。

对于其他具体产品的功能对比,本教程不凭印象罗列差异;如果后续需要,应查阅对应官方文档、仓库和发布说明。

1.9 成熟度与边界

仓库和文档明确声明 developer preview,会出现破坏性变更;SESSION_FORMAT_VERSION 当前为 0,不承诺兼容,见 packages/core/session/src/types.ts:34-56。核心 API 包标为 product,部分包如 E2B 标为 POC,见 packages/README.md

1.10 30 秒解释版本

DeepSeek Harness 是一个用 Cordis 写的 Agent 运行框架。模型、工具、会话日志和 Agent 循环都是插件,插件通过 ctx 注册服务、监听 typed events,并且注册都可以撤销。Agent 每次请求的上下文都从只追加的 Session log 推导出来,因此可以回放、恢复、分叉和持久化。

1.11 5 分钟解释版本

运行一个 dsh,实际上是在加载一棵由 bundle patchcordis.patch.yml 组成的插件树。基础 bundle 装上模型适配器、工具、会话、沙箱、权限和遥测;webheadless bundle 再补上不同表层。

当用户提交一条消息:

  1. 消息进入 Agent 的 inbox。
  2. ReactLoopAgent 打开一个 turn
  3. preStep() 认领消息,组装 system prompt 和 tool schema。
  4. step()Session.deriveMessages() 得到模型历史。
  5. LlmRuntime 按 provider/model 找到 adapter,流式返回 chunk。
  6. 如果模型要求工具,ToolRuntime 经过 pre/guard/around/post/result 管线执行。
  7. 所有模型可见内容都作为 SessionEvent 写回日志,下一轮再从中推导。

想扩展系统,通常不修改 loop,而是在 ctx.toolsctx.llmctx.systemPromptctx.agents 或 typed events 上注册插件。


2. 开始学习前需要掌握的概念

2.1 概念分层

必须先掌握

  • Cordis 的 ContextServiceinjectctx.effect()、typed events。
  • Service Definition / Service Provider / Consumer 能力三件套。
  • SessionEvent append-only log 与 deriveMessages()
  • Agentturnstep、inbox 和 followup/steer/inject
  • profile / bundle / cordis.patch.yml 的层叠组合。

可以边读边学

  • surfaceSurfaceOp 的 append/replace。
  • SystemPrompt section/context/variable/tool provider。
  • ToolRuntime 的 pre-execute/execute/post-execute/result 管线。
  • LlmAdapterprepareCall()StreamChunk
  • session persistence / projection / telemetry。

进阶阶段再学习

  • dsh-scope 的 scoped registration 与 agent-scope 隔离。
  • Typert 类型图生成与类型安全 RPC。
  • Web 客户端 slot 系统、React-free object layer、useSyncExternalStore
  • 子智能体(subagent)、工作流(workflow)、程序化工具调用(PTC)、上下文压缩(compaction)、钩子(hooks) 的具体实现。

2.2 概念依赖关系

打开交互图解:整体系统与插件边界

2.3 Cordis Context、Service、inject、effect

  1. 定义Context 是运行时服务仓库;Service 是声明了稳定 ctx.<key> 的插件;inject 声明依赖;ctx.effect() 创建可撤销注册。
  2. 为什么需要:如果插件直接 import 具体实现,就无法替换模型、沙箱、驱动器或 UI 组件。
  3. 项目体现AgentLoopstatic inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt'],见 packages/core/agent-loop/src/index.ts:297
  4. 源码位置docs/cordis-primer.mdpackages/core/agent-loop/src/index.ts:296-350
  5. 例子:一个工具插件只依赖 ctx.tools,不依赖 dsh-agent-loop
  6. 前端类比Context 类似前端 DI 容器或 React Context;inject 类似构造函数依赖注入。
  7. 类比局限:Cordis 的依赖解析和 fiber 生命周期更强,服务会随插件卸载和热更新一起撤销,不只是“共享数据”。
  8. 常见误区:把 ctx.effect() 当成普通副作用函数。它必须能撤销,热更新和卸载才能干净。

2.4 Typed events 和四种派发模式

  1. 定义:通过 TypeScript declaration merging 声明事件名,运行时分 emitwaterfallparallelserial 派发。
  2. 为什么需要:循环和工具管线需要让策略插件在不改核心代码的情况下介入。
  3. 项目体现agent/pre-stepagent/requestllm/streamtools/* 都是 waterfall,见 packages/core/agent/src/runtime-types.ts:231-278packages/core/tools/src/index.ts:152-197
  4. 源码位置docs/cordis-primer.md#dispatch-modespackages/core/agent/src/runtime-types.ts:146-291
  5. 例子llm/stream 的 listener 调用 next() 得到 adapter 原始流,也可以直接返回自己的 async iterable 短路。
  6. 前端类比:waterfall 很接近 Koa / Redux middleware / server-side onion model。
  7. 类比局限:Cordis waterfall 是服务级事件合同,返回值和派发模式都写进类型;不是任意全局事件总线。
  8. 常见误区:waterfall listener 忘记调用 next(),会短路后续监听器。仓库明确要求必须调用 next() 才能委托。

2.5 Capability Seam:定义、提供、消费

  1. 定义:一个可替换能力由 Service Definition、Service Provider、Consumer 三部分组成。
  2. 为什么需要:如果只有接口或只有实现,替换能力时仍会耦合到具体包。
  3. 项目体现dsh-llm 同时定义 LlmRuntime 和 adapter 接口;dsh-llm-deepseek 是 provider;dsh-agent-loop 是 consumer。
  4. 源码位置docs/architecture.md#capability-seamspackages/llm/llm/src/index.ts:180-284
  5. 例子ctx.llm.registerAdapter(['deepseek-official'], adapter) 注册 provider;循环只调用 ctx.llm.prepareCall(),不 import DeepSeek。
  6. 前端类比:类似“接口 + 实现 + 调用方”,也像浏览器里的 fetch 被不同网络层实现。
  7. 类比局限:seam 还包含事件、生命周期和 disposer;不是单纯接口继承。
  8. 常见误区:只写一个 Service Definition 就认为能力完成。仓库规定完整 seam 至少要三个角色,拆分只在角色独立演进时进行。

2.6 Session Log 与 Event Sourcing

  1. 定义:会话是只追加、连续 seq 的 SessionEvent 数组;消息历史由 log 投影得到。
  2. 为什么需要:持久化、回放、分叉、标题、遥测和 UI 都需要同一个真相来源。
  3. 项目体现Session.deriveMessages() 只遍历 surface.nodes,见 packages/core/session/src/index.ts:726-747
  4. 源码位置packages/core/session/src/index.ts:417-758packages/core/session/src/surface.ts:83-114
  5. 例子user/messageassistant/messagetool/result 是 surface event;turn/startassistant/chunk 不直接进入历史。
  6. 前端类比:像 Redux 的 action log 或 CRDT/event log;当前 UI 是 log 的投影。
  7. 类比局限:这里不是简单状态 reducer,而是同时记录原始流、边界、来源和 surface 操作,模型上下文需要精确重建。
  8. 常见误区:以为日志里每个事件都会发给模型。实际上只有带 surfaceOp 的 message-producing 事件进入 deriveMessages()

2.7 Model-visible 等价于 Logged

  1. 定义:任何送到模型请求的内容,都必须能从 Session log 重建。
  2. 为什么需要:防止 UI 和回放看到的内容与模型实际看到的内容不一致。
  3. 项目体现docs/architecture.md 写明 Model-visible means loggedpackages/core/agent-loop/src/invariant.ts:39 检查 session.deriveMessages()
  4. 源码位置docs/architecture.md#session-logpackages/core/agent-loop/src/invariant.ts:21-64
  5. 例子:文件变化通知通过 agent.inject() 成为 user/message 后,才可能进入模型上下文。
  6. 前端类比:类似“服务端渲染的数据必须能由 store/URL 恢复”,不能依赖组件局部状态拼请求。
  7. 类比局限:这里的约束是运行时 invariant 和持久化格式,不只是代码风格。
  8. 常见误区:把只用于 UI 的临时变量直接塞进 agent/request。这样做会破坏回放和审计。

2.8 Agent、Turn、Step、Inbox

  1. 定义Agent 是公开生命周期接口;turn 是 0 或多个 step 的执行区间;step 是一次模型请求加它触发的工具执行。
  2. 为什么需要:模型会连续调用工具,必须有一个明确的执行边界和输入队列。
  3. 项目体现ReactLoopAgentturn()step()packages/core/agent-loop/src/agent.ts:246-401
  4. 源码位置packages/core/agent/src/runtime-types.ts:64-144packages/core/agent-loop/src/agent.ts:210-401
  5. 例子followup() 追加到 next-turn 并唤醒;steer() 追加到 next-step 并唤醒;inject() 追加到 next-step 但不唤醒。
  6. 前端类比turn 像用户一次提交对应的“任务批次”,step 像任务中的一次模型调用。
  7. 类比局限:模型工具调用会连续产生 step,用户不会直接感知每个 step。
  8. 常见误区:认为一次用户输入一定等于一次模型请求。工具调用会继续当前 turn,可能产生多个 step。

2.9 SystemPrompt 与 Tool Schema 装配

  1. 定义SystemPrompt 按顺序收集 section、context、variable 和 tool schema,最后渲染成 prompt。
  2. 为什么需要:不同插件可以贡献 persona、工具指引、AGENTS.md、时间上下文,而不需要知道彼此。
  3. 项目体现SystemPrompt.assemble() 收集全局和 scoped layer,见 packages/core/system-prompt/src/index.ts:467-530
  4. 源码位置packages/core/system-prompt/src/index.ts:52-217
  5. 例子:工具插件调用 ctx.systemPrompt.tools(...) 贡献 schema;tool guidance 自动进入请求。
  6. 前端类比:类似多个 provider 拼页面内容,或 CSS layers 的合并。
  7. 类比局限:这里有严格 name/order/scope/complete 规则,变量错误会让装配失败。
  8. 常见误区:以为系统提示词是一个写死的字符串。它是由多个插件动态组装并参与请求日志的。

2.10 Tool Registry 与执行管线

  1. 定义ctx.tools 保存 ToolDefinition,执行时经过策略事件,最终产生 lossless JSON 结果。
  2. 为什么需要:工具调用不能直接信任模型参数,必须有 schema、权限、超时、取消和审计。
  3. 项目体现ToolRuntime.execute() 入口在 packages/core/tools/src/index.ts:1342,内部调度器在 796-801。
  4. 源码位置packages/core/tools/src/index.ts:222-288packages/core/tools/src/index.ts:787-1362
  5. 例子:模型请求 bash 工具后,先 tools/pre-execute 决定 allow/deny/ask,再 tools/execute 执行,最后 tools/result 观察冻结结果。
  6. 前端类比:像表单校验 + 请求拦截器 + 响应拦截器。
  7. 类比局限:工具输出要回到模型,因此结果是 lossless JSON、不可变且必须可回放。
  8. 常见误区:在 tools/result 中尝试修改结果。它是 observe-only,修改应在 post-executefinalizeContent

2.11 LLM Adapter 与 Prepared Call

  1. 定义LlmAdapter 只负责把 GenerateOptions 转成 StreamChunkLlmRuntime 负责路由、默认值、准备调用和 llm/stream waterfall。
  2. 为什么需要:模型厂商协议和重试策略应封装在 provider,loop 不应知道 HTTP 细节。
  3. 项目体现LlmRuntime.prepareCall()packages/llm/llm/src/index.ts:779-814 返回一次性 PreparedLlmCall
  4. 源码位置packages/llm/llm/src/index.ts:180-233packages/llm/llm/src/index.ts:730-928
  5. 例子agent-loop 调用 ctx.llm.prepareCall(config),拿到 stream() 后逐 chunk 写 assistant/chunk
  6. 前端类比:类似 fetch 的适配层 + streaming response parser。
  7. 类比局限:prepared call 会固定 adapter registration,防止 HMR 混用不同 adapter 的能力结果。
  8. 常见误区:直接手写 provider HTTP 请求,而不是实现 LlmAdapter,会失去重试、取消、流式协议和路由能力。

2.12 Profile、Bundle、Patch Layer

  1. 定义:profile 是命名的插件树组合;bundle 是发布为 npm 包的一组 patch;patch 按 id 覆盖行配置。
  2. 为什么需要:同一个 base bundle 可以生成 webheadless,用户还能在不改 bundle 的情况下覆盖配置。
  3. 项目体现PROFILE_TEMPLATES 定义 webheadless 的 bundle 顺序,见 packages/boot/app-boot/src/profile.ts:114-117
  4. 源码位置packages/boot/app-boot/src/profile.ts:35-125apps/cli/src/profile-boot.ts:142-171
  5. 例子dsh-base 作为第一层;dsh-web-appdsh-headless 第二层;用户 profile cordis.patch.yml 最后覆盖。
  6. 前端类比:类似 CSS cascade、环境配置覆盖、或 Docker layer。
  7. 类比局限:patch 替换整行 config,不是深度 merge。
  8. 常见误区:以为 patch 会 merge 字段。文档明确说明它替换目标行完整 config,覆盖时必须重述保留字段。

2.13 Web Client 的 React-free Object Layer、Slots、uSES

  1. 定义:浏览器里业务数据在 React 外对象层,React 只订阅不可变快照;UI 通过 slot 注册组合。
  2. 为什么需要:流式 token 高频更新时,避免整棵 React 树抖动;也避免业务状态绑死 React。
  3. 项目体现SessionNotifierpackages/api/session-controller/src/client/sessions/ConversationNodeAssemblerpackages/client/ui-conversation/src/client/conversation/assembler.tsuseSyncExternalStorepackages/client/ui-renderer/src/client/scoped-slots.tsx
  4. 源码位置packages/api/session-controller/src/client/sessions/session.ts:81packages/client/ui-conversation/src/client/conversation/assembler.ts:158packages/client/ui-renderer/src/client/scoped-slots.tsx:287
  5. 例子Session.getSnapshot() 返回缓存快照;useSyncExternalStore(subscribe, getSnapshot) 驱动组件。
  6. 前端类比:像 zustand/observable store,但业务状态机和流式累积完全不依赖 React。
  7. 类比局限:这里不只是一个全局 store,而是每个 Session 有事件窗口、重连状态和不可变投影。
  8. 常见误区:把所有共享 UI 状态塞进 React Context。项目明确区分业务数据、共享 store、owner props、component local state。

3. 技术栈与工程系统

3.1 技术栈表

技术或依赖在项目中的用途核心依赖对应配置或源码位置学习优先级建议掌握程度
TypeScript全部源码、typed events、declaration mergingtsconfig.base.jsonpackage.json能读高级泛型和 declaration merging
Node.jsHost 运行时、子进程、FS、HTTP、PTYpackage.json.engines理解事件循环、stream、AbortSignal
pnpm workspacesMonorepo 包管理pnpm-workspace.yaml、根 package.json会用 filter/workspace
Cordis插件运行时、Context、fiber、events、effectvendor/docs/cordis-primer.md理解五思想和四种派发
schemastery配置 schema 校验packages/core/agent-loop/src/index.ts:300-311能读 Config
Typert类型图、生成 RPC 契约packages/typert/docs/subsystems/typert.md进阶知道它解决什么
tsxsource launch ESMpackage.jsondsh script知道 source/build 差异
tsdown / tsc构建 Host/Client libpackage.json build scripts知道 build 阶段
ViteWeb shell / 前端构建apps/webpackages/client/web能调试前端
Vitest测试运行器vitest.config.ts、根 scripts会跑单测
React + useSyncExternalStoreWeb UI 渲染packages/client/ui-rendererapps/web理解外部 store 订阅
SQLite/JSONL会话持久化与检索核心辅助packages/session/session-persistence-*session-query-sqlite理解 append log 与查询
OpenTelemetry会话遥测辅助packages/session/session-telemetry-otel知道边界

3.2 包管理和依赖管理

package.jsonpnpm@11.7.0,workspaces 覆盖 vendor/*packages/*/*native/landlock-runapps/*website。所有 npm 包使用 @deepseek-ai/dsh-*,vendored 包 rescope。扩展插件依赖 Service Definition,不依赖 concrete provider,见 packages/README.md#dependencies

3.3 构建流程

根脚本显示:

text
build
  build:lib
    build:lib:host
    build:lib:client
  build:web

Host 和 Client 是两个独立 TypeScript aggregate,不能压成一个 program,否则两端对 Context 的 declaration merging 会冲突。typert 只在 Host tsdown 阶段生成类型图。

依据:package.jsondocs/development.md#typescript-project-layout

3.4 开发模式

source launch 使用:

sh
pnpm dsh --profile headless "task"

根 script 实际是 node --import tsx/esm apps/cli/src/bin.ts。源码运行必须保持 ESM。Web HMR 由 pnpm run dev:web 重建客户端 bundle,client-hmr 行保持挂载但空闲。

3.5 测试体系

  • pnpm run test:Vitest 单测。
  • pnpm run test:coverage:CI 覆盖率门禁,packages/*/*/src 每文件 100%。
  • pnpm run test:e2e:真实 API e2e,无 key 自动跳过。
  • pnpm run test:snapshot:无 key 的 ACP/headless 回放对比。
  • pnpm run test:web:浏览器快照,需要构建。

这些命令来自根 package.json,本次未执行。

3.6 配置与环境变量

cordis.ymlconfig 支持 !!js 表达式。真实 API 读取 DEEPSEEK_API_KEY,可选 DEEPSEEK_BASE_URL。权限相关可读 DSH_PERMISSION_MODE;遥测可读 DSH_TELEMETRY_MODEDSH_TELEMETRY_DISABLED。这些在 packages/bundle/base/cordis.patch.yml 有体现。

3.7 发布和部署方式

从仓库声明看,存在 release:dshrelease:packrelease:publishpublish:npm-baseline。产品安装后由 apps/cli/lib/bin.js 启动。本教程未执行发布流程。

3.8 开发与生产差异

source launch 依赖 tsx ESM;built launch 依赖 lib/ 产物。测试默认解析 source plane,只有 built artifact smoke 显式运行 lib/。开发模式还有配置 patch 热更新,生产或安装形式仍以配置组合为基础。


4. 整体架构

4.1 架构风格

从源码和文档可以判断,这不是传统 MVC,而是:

  • 插件化微内核 / 可组合运行时:所有产品能力都通过插件挂到 ctx
  • 事件驱动与事件溯源混合:typed events 负责扩展点,Session log 负责持久真相。
  • capability seam 分层:Service Definition / Provider / Consumer 分离。
  • Host / Browser 双插件树:同一套 Cordis 思想贯穿后端和前端。
  • 类型安全 RPC:Typert 从 Host 类型生成客户端契约。

判断依据:

4.2 模块划分

模块名称主要职责对外接口上游依赖下游依赖核心主要源码位置
core/sessionappend-only 会话日志与内存 storectx.sessionsSessionSessionEventdsh-llmdsh-scope几乎所有模块packages/core/session/src/
core/system-prompt组装 prompt、context、tool schema、变量ctx.systemPromptdsh-scopedsh-llmagent-looptoolspackages/core/system-prompt/src/index.ts
core/tools工具注册、展示、执行管线ctx.toolsdsh-system-promptdsh-scopeagent-loop、所有 dsh-tool-*packages/core/tools/src/index.ts
core/agentAgent 公共接口、registry、事件词汇ctx.agentsdsh-sessiondsh-llmUI、protocol、looppackages/core/agent/src/index.ts
core/agent-loop默认 Agent 驱动器ctx.agentLoopagentssessionsllmtoolssystemPrompt无(实现)packages/core/agent-loop/src/
core/scopeper-agent scoped registration 原语library,无 ctx keysessionsystem-prompttoolspackages/core/scope/src/
llm/llm模型消息、stream、adapter registryctx.llmCordisagent-loopllm-deepseekllm-pi-aipackages/llm/llm/src/index.ts
session/session-persistence持久化 seam 和 coordinatorctx.sessionPersistencedsh-sessionJSONL/SQLite 后端packages/session/session-persistence/src/
session/session-projection从事件流驱动纯投影ctx.sessionProjectionsdsh-sessiontoken meter、UI carriers重要packages/session/session-projection/src/index.ts
boot/app-bootprofile 发现、patch 组合函数 APICordis loaderCLIpackages/boot/app-boot/src/profile.ts
apps/clidsh 启动器CLIapp-boot、bundlesapps/cli/src/bin.ts
api/session-controllerHost 分页/跟随传输、Client Session 与快照page/follow、client servicesSession Log、wire/RPCUI 插件packages/api/session-controller/src/
client/ui-conversation + ui-renderer对话节点装配、React 外部 Store 桥与 keyed Slotassembler、slot rendererClient Session、ui-slotsReact UIpackages/client/ui-conversation/src/packages/client/ui-renderer/src/
client/ui-slotsslot registry 核心与 props 类型slot APIruntimeUI 插件packages/client/ui-slots/
typert/*类型图生成、加载、运行时 registryTypertTypeScript AST/type systemAPI gateway、SDK重要packages/typert/
sdk/*JSON-RPC 协议、server、TS clientSDKdsh-agent、Typert外部自动化重要packages/sdk/
bundle/*安装式 profile patch layersbundle patchbase + mode重要packages/bundle/base/cordis.patch.yml

4.3 架构图

打开交互图解:整体系统与插件边界

4.4 核心数据流

选择最典型的 dsh --profile headless "run the tests",因为它绕过 Web UI 和 ACP,最能直接暴露核心 loop。

  1. CLI 解析命令:apps/cli/src/bin.ts:27-53
  2. profile 组合:apps/cli/src/profile-boot.ts:207-259
  3. dsh-headless 的 runner 创建 Agent、提交任务、等待空闲:packages/bundle/headless/README.md:7-11
  4. ReactLoopAgent.turn() 打开 turn,preStep() 组装请求,step() 派生历史并调用模型。
  5. 模型返回工具调用时,executeToolCalls() 调用 ctx.tools
  6. 每一步都写 turn/startstep/startuser/messageassistant/messagetool/resultstep/endturn/end 等 Session event。
  7. headless runner flush 后从持久事件区间提取最后 assistant 文本并退出。

4.5 启动流程

打开交互图解:一次 Agent Turn

关键源码:

  • packages/boot/app-boot/src/profile.ts:371-403 解析 profile 和 bundle patch。
  • packages/boot/app-boot/src/profile.ts:413-419 将 layers 合并成 entry list。
  • apps/cli/src/profile-boot.ts:248-259 调用 boot() 并注入 commandline/env。
  • packages/core/agent-loop/src/index.ts:349-381 注册 factory 和配置驱动 agents。

4.6 关键调用链

入口:

text
agent.followup(userMessage)

调用链:

text
packages/core/agent-loop/src/agent.ts:122
-> Agent.send(message, 'next-turn', true)
-> packages/core/agent-loop/src/agent.ts:113-120
-> Inbox.splice + wakeDriver
-> packages/core/agent-loop/src/agent.ts:172-193
-> withInitiator -> kick()
-> packages/core/agent-loop/src/agent.ts:210-223
-> turn()
-> packages/core/agent-loop/src/agent.ts:246-330
-> preStep()
-> packages/core/agent-loop/src/agent.ts:225-243
-> ctx.systemPrompt.assemble(assembleContextFor(this, signal))
-> runtimeContext.project(joinContextSections(sections), sections)
-> ctx.waterfall('agent/pre-step')
-> session.append('step/start')
-> session.append('user/message')
-> step(assembly)
-> packages/core/agent-loop/src/agent.ts:332-401
-> buildRequest()
-> session.deriveMessages()
-> ctx.llm.prepareCall()
-> preparedCall.stream(request) or ctx.llm.stream(request)
-> for await chunk -> session.append('assistant/chunk')
-> assembler.finish -> createAssistantMessage
-> session.append('assistant/message')
-> if tool calls -> executeToolCalls()
-> packages/core/agent-loop/src/tool-calls.ts:67-204
-> ctx.tools scheduler prepare/dispatch/finalize
-> session.append('tool/result')
-> repeat step while tools owe request or steering arrives
-> session.append('step/end')
-> dispatch.serial('agent/turn-stopping')
-> session.append('turn/end')

每一步的依据都在上述行号附近,属于已验证源码结构。


5. 源码地图与推荐阅读顺序

推荐顺序不是按目录字母排序,而是按认知依赖和运行链路排序。

第一批:建立项目全景

文件阅读目的关键符号或配置读完应能回答
README.md知道产品入口、运行方式和 preview 状态npx @deepseek-ai/dsh web这个项目如何快速启动
docs/cordis-primer.md理解 Cordis 五思想Serviceinjectctx.effect、四种 dispatch为什么“一切皆插件”能成立
docs/architecture.md理解核心包、turn flow、session log、extension pointsCore packagesTurn flowWhere new behavior goes新功能应该挂在哪里
packages/core/README.md认识产品 API spinectx.sessions/llm/tools/agents/agentLoop六个核心服务各负责什么
packages/bundle/base/cordis.patch.yml看真实默认组合llmsessiontoolsagent-loop、sandbox rows一个最小 Agent 需要哪些插件
apps/cli/src/bin.ts找到真正入口runProfileCLI 如何进入 profile

第二批:理解主执行链路

文件阅读目的关键符号或行号读完应能回答
packages/core/session/src/types.ts理解事件词汇和 formatSessionEventMapSurfaceEventTypeSESSION_FORMAT_VERSION哪些事件会进入模型历史
packages/core/session/src/index.ts理解 append 和派生历史Session.appendderiveMessagesrequestHeader模型上下文如何生成
packages/core/session/src/surface.ts理解 surface append/replacederiveEventMessageSurfaceManagercompaction 如何替换历史
packages/core/agent/src/runtime-types.ts理解 Agent 接口和事件AgentPreStepDecisionagent/pre-stepUI 如何驱动 Agent
packages/core/agent-loop/src/agent.ts理解 turn/step/stream/toolReactLoopAgentturn()step()一个请求怎样变成模型调用和工具执行
packages/core/agent-loop/src/index.ts理解创建/恢复/销毁AgentLoopcreate/resumesetupAndPublishAgent 生命周期如何管理
packages/core/agent-loop/src/tool-calls.ts理解工具调度和并发executeToolCalls工具如何并行或串行
packages/core/system-prompt/src/index.ts理解 prompt 和 schema 装配section/context/tools/variable/assemble插件如何给模型加上下文
packages/core/tools/src/index.ts理解工具注册和管线register/restrict/guard/execute工具如何被校验和执行
packages/llm/llm/src/index.ts理解模型流式和 adapterregisterAdapter/prepareCall/stream如何接入新模型厂商

第三批:理解组合、持久化和前端

文件阅读目的关键符号读完应能回答
packages/boot/app-boot/src/profile.ts理解 profile/bundle/patchloadProfilecomposeEntries配置树如何生成
apps/cli/src/profile-boot.ts理解 runProfile 和 HMRrunProfilecomposeLive启动时如何 mount/热更新
packages/session/session-persistence/src/index.ts理解持久化 seamSessionPersistence如何换 JSONL/SQLite
packages/session/session-persistence/src/coordinator.ts理解 write coordinationappendflushresume会话如何可靠落盘
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md理解浏览器插件树和对象层React-free object layerslot systemWeb UI 为什么这样设计
packages/api/session-controller/src/client/sessions/session.ts理解前端 Session 快照getSnapshotsubscribeReact 如何订阅流式状态
packages/client/ui-renderer/src/client/scoped-slots.tsx理解 slot 渲染和 uSESuseSyncExternalStoreslot 如何变成 React 树
docs/cookbook/extension-cookbook.md理解扩展点地图feature -> mechanism新功能用哪种扩展机制

调试时需要关注的文件

  • packages/core/agent-loop/src/agent.ts:断点放在 turn()preStep()step()buildRequest()executeToolCalls() 附近。
  • packages/core/agent-loop/src/tool-calls.ts:观察工具组并发和结果。
  • packages/core/session/src/index.ts:观察每次 append()seqsurface
  • packages/core/system-prompt/src/index.ts:观察组装出的 sections/contexts/tools/variables。
  • packages/llm/llm/src/index.ts:观察 adapter selection 和 llm/stream waterfall。

二次开发时最可能修改的文件

  • 增加工具:新建 packages/<group>/<pkg>,在 src/index.ts 调用 ctx.tools.register(),并准备 README/tests。
  • 增加模型:新建 packages/llm/<adapter>,继承 LlmAdapter,调用 ctx.llm.registerAdapter()
  • 增加 prompt/context:注册 ctx.systemPrompt.section/context/variable
  • 增加权限:监听 tools/pre-execute 或使用 ctx.tools.guard()
  • 增加 UI:在 client plugin 的 src/client/index.ts 注册 slot。

暂时可以跳过的目录

  • vendor/:除非要深入 Cordis 本身或修改 vendored 包。
  • website/:文档站点投影,学习架构时不是主链路。
  • python/native/:涉及 Python SDK 或原生 Landlock,不是理解核心 Agent 的第一优先。
  • docs/subsystems/:可在需要查具体服务类型时再读。
  • 生成式 catalog:docs/config-catalog.mddocs/tool-catalog.md 等,适合查询不适合通读。

6. 完整学习路线

以下按“前端背景、能投入持续学习”的默认情况设计。若每天投入 1.5 到 2 小时,建议总周期 4 到 6 周;只做快速理解可压缩到 1 到 2 周。

阶段 0:建立项目全景

  • 目标:能解释项目解决什么问题、入口和核心价值。
  • 推荐时间:1 到 2 天。
  • 学习章节:第 1、2、3 章。
  • 阅读:README.mddocs/architecture.mddocs/cordis-primer.mdpackages/core/README.md
  • 实践:运行 git status、列出目录、画一张自己的模块草图。
  • 验收:不看文档,能说出“插件化、session log、turn/step、profile/bundle”。

阶段 1:搭建并运行项目

  • 目标:完成依赖安装和最小运行,记录真实成功或失败。
  • 推荐时间:1 到 2 天。
  • 学习章节:第 9 章。
  • 阅读:docs/development.md#setup-tutorial、根 package.json
  • 实践:pnpm installpnpm run typecheck;如环境允许,再尝试 pnpm run testpnpm dsh --profile headless
  • 验收:能区分环境问题、依赖问题和配置问题。

阶段 2:掌握 Cordis 插件模型

  • 目标:能写一个最小插件,理解 inject/effect/events。
  • 推荐时间:2 到 3 天。
  • 学习章节:第 2 章中的 Cordis 部分、第 3 章。
  • 阅读:docs/cordis-tutorial/01-first-plugin.md07-into-the-harness.md
  • 实践:在 tmp/cordis-tutorial 建插件,注册 service 和 event。
  • 验收:能解释为什么 plugin 卸载后注册会消失。

阶段 3:理解 Session Log 和派生历史

  • 目标:掌握事件、surface、deriveMessages。
  • 推荐时间:2 天。
  • 学习章节:第 2 章 session 部分、第 7 章相关章。
  • 阅读:packages/core/session/src/types.tsindex.tssurface.ts
  • 实践:写一个脚本 Session.createappend 几条事件,观察 deriveMessages()
  • 验收:能判断某个事件是否进入模型历史。

阶段 4:追踪 Agent 主调用链

  • 目标:沿 followup -> turn -> step -> llm -> tools 读源码。
  • 推荐时间:3 到 5 天。
  • 学习章节:第 4、8 章。
  • 阅读:packages/core/agent-loop/src/agent.tsindex.tstool-calls.ts
  • 实践:在 turn()step() 加断点,观察 phase、inbox、session.events。
  • 验收:能用 Archify 图解释时序并标出关键行号。

阶段 5:深入 LLM 和 Tools

  • 目标:理解 provider 替换、流式协议、工具策略。
  • 推荐时间:3 天。
  • 学习章节:第 7 章相关章。
  • 阅读:packages/llm/llm/src/index.tspackages/core/tools/src/index.tsdocs/cookbook/adding-an-llm-adapter.mdadding-a-tool.md
  • 实践:阅读现有 adapter;为已有工具补一个 tools/pre-execute 观察 listener。
  • 验收:能说出 prepareCall() 为什么返回一次性 handle。

阶段 6:学习测试和调试

  • 目标:会跑单测、snapshot,理解测试分层。
  • 推荐时间:2 天。
  • 学习章节:第 9 章。
  • 阅读:docs/testing.mdpackages/core/agent-loop/tests/
  • 实践:跑一个 package 单测;定位一次失败。
  • 验收:能说明 unit/coverage/e2e/snapshot 分别验证什么。

阶段 7:修改一个已有功能

  • 目标:做低风险改动并验证。
  • 推荐时间:2 到 3 天。
  • 学习章节:第 10 章实践二。
  • 实践:修改 cordis.patch.yml 中的 system-prompt.personatoolOrder,或在一个测试 fixture 中观察输出变化。
  • 验收:能说明改动影响、恢复方式和测试命令。

阶段 8:排查一个模拟故障

  • 目标:掌握调用链、日志和断点。
  • 推荐时间:2 到 3 天。
  • 学习章节:第 10 章实践三。
  • 实践:禁用某个工具行、填一个错误模型路由、或写一个 pre-step reject listener,观察 fail-loud。
  • 验收:能定位故障发生在哪个 layer、哪个服务、哪个事件。

阶段 9:完成一个小型扩展

  • 目标:使用正式扩展机制增加一个能力。
  • 推荐时间:3 到 5 天。
  • 学习章节:第 10 章实践四、第 11 章。
  • 实践:新增一个 tool plugin 或 prompt section,补 README 和 tests。
  • 验收:能通过配置加载,能被模型 schema 看到,并有测试覆盖。

阶段 10:自己设计一个最小 Agent Harness

  • 目标:能把核心抽象迁移到自己的项目。
  • 推荐时间:1 周左右。
  • 学习章节:第 11 章末尾的“自己动手设计”。
  • 实践:用 TypeScript 实现最小 ContextToolRegistrySessionLogAgentLoop
  • 验收:最小系统能处理“文本 -> 模型 -> 工具 -> 模型”的一轮循环,并记录可回放日志。

第 1 章:启动入口与配置档案(Profile)

1. 本章定位

先把 dsh 看成“插件树启动器”,而不是一个写死行为的 CLI。新版同时提供 webheadlesssdksdk-minimalacp 五类配置档案(Profile)。

打开本章交互图解:Profile 启动链

2. 学习目标

  • 解释 Profile、组合包(Bundle)与补丁(Patch)的叠加关系。
  • apps/cli/src/bin.ts 追到 runProfile()
  • 区分启动时重载与运行时插件重载。
  • 用配置 dump 定位“参数由哪一层消费”。

3. 前置知识

把 Profile 类比为前端应用入口,Bundle 类似可复用 preset,Patch 类似环境覆盖。局限是 Patch 操作插件树条目,顺序和条目 ID 都是行为,不是普通对象深合并。

4. 对应源码

5. 工作原理

启动器解析 invocation,选择 Profile,按声明顺序读取 Bundle,再叠加 Profile、Harness Home 和 --patch 覆盖,最后交给 Loader 激活 Cordis 插件。启动时变化需要重新准备配置;只有进入 Loader 管理且声明可重载的插件行为才属于运行时重载。

6. 执行流程

无图回退:dsh → 解析 invocation → runProfile() → 准备 Profile → 顺序叠加 Bundle/Patch → Loader 挂载插件 → 启动 Web、Headless、SDK 或 ACP 表面。

7. 关键源码讲解

apps/cli/src/bin.ts:28 不创建 Agent,只把控制权交给 Profile 启动。packages/boot/app-boot/src/profile.ts:139-155 明确列出五类表面,packages/boot/app-boot/src/profile.ts:166 决定默认 Bundle。新增表面优先组合 Bundle;只有公共启动语义变化才修改 boot 核心。

8. 调试与观察方法

  • 断点:runProfile() 与 Profile 准备完成处。
  • 观察:Profile 名、Bundle 顺序、Patch 来源、最终条目列表。
  • 【已执行并验证】pnpm dsh --help(见第 9 节验证记录)。
  • 【已执行并验证】使用隔离的临时 DSH_HOME 执行 --profile web --dump-default-config,成功输出 base 与 web Bundle 组合树。
  • dump 已失败时,模型尚未调用,不要先排查 API Key。

9. 本章实践任务

用临时目录创建一个只改变非敏感配置的 overlay,比较默认与最终 dump;再移除 overlay,确认恢复。验收:能指出每个字段来自哪个层级,且未修改用户现有 Profile。

10. 常见误区

  • 把 Profile 当单文件,而忽略 Bundle 顺序。
  • 把启动参数与应用参数混为一谈。
  • 认为所有 Patch 都能在线热更新。

11. 自测题

  1. 五个内置 Profile 分别面向什么表面?
  2. Bundle 顺序为什么会改变行为?
  3. --patch 适合哪类实验?
  4. 配置 dump 失败说明调用链停在哪里?
  5. 何时应该新增 Bundle,而不是修改 boot 核心?

第 2 章:Cordis 插件、依赖注入与生命周期

1. 本章定位

Cordis 是 Harness 的组合语法:服务、事件、能力贡献和 UI 插槽都随作用域创建与撤销。

打开本章交互图解:Cordis 插件生命周期

2. 学习目标

  • 区分函数插件、服务定义(Service Definition)、提供者与消费者。
  • 解释依赖注入(Dependency Injection)和作用域(Scope)。
  • 用副作用/释放器(Effect/Disposer)证明插件可卸载。
  • 区分 emitserialparallelwaterfall

3. 前置知识

可把 Context 类比为带生命周期的前端依赖容器,把 ctx.effect() 类比为 useEffect 的 cleanup。局限是 Cordis 还管理依赖等待、子作用域、typed event 和插件 fiber,不等于 React Context。

4. 对应源码

5. 工作原理

函数插件导出 apply(ctx, config);服务插件先定义 ctx.<key> 的类型和契约,再由 provider 注册实现,consumer 用 inject 声明运行时依赖。依赖未满足时插件等待;满足后在自己的 Scope 激活。所有监听器、服务和注册都应产生 Disposer,卸载时反向清理。

6. 执行流程

无图回退:Loader 创建插件 Scope → 检查 inject → 等待依赖或调用 apply() → 注册 service/listener/effect → 正常运行 → dispose → 反向撤销副作用。

7. 关键源码讲解

AgentLoop 是真实样例:packages/core/agent-loop/src/index.ts:353 声明所需服务,packages/core/agent-loop/src/index.ts:412 通过 effect 注册 Agent factory,packages/core/agent-loop/src/index.ts:416 注册系统提示词变量。重点不是某个 Map,而是谁拥有注册、何时可见、卸载后是否完全消失。

8. 调试与观察方法

在插件 apply()、依赖就绪回调和 Disposer 处断点;观察 Scope、服务键、监听器顺序。常见故障是 inject 键拼错、waterfall 忘记 next()、异步释放未完成。

9. 本章实践任务

实现 greeter provider 和 consumer:先缺省 provider 证明 consumer 等待,再挂载并调用,最后卸载两次确认没有残留监听器。验收:不使用全局单例,重复挂载结果一致。

10. 常见误区

  • inject 不只是类型提示,它决定激活时机。
  • waterfall 不调用 next() 会有意或无意地短路。
  • 在插件作用域外注册会失去自动清理。

11. 自测题

  1. Service Definition、provider、consumer 各负责什么?
  2. Scope 为什么比全局容器安全?
  3. Effect 的 Disposer 何时运行?
  4. serialparallel 如何选择?
  5. 如何证明插件卸载没有资源泄漏?

第 3 章:会话日志(Session Log)、Surface 与投影

1. 本章定位

Agent 的记忆不是一个可随意修改的消息数组,而是只追加事件事实与若干可重建视图。

打开本章交互图解:Session Log 与投影

2. 学习目标

  • 区分原始日志、Surface、模型历史、持久化和前端投影。
  • 解释 deriveMessages()surface.replace
  • 理解格式版本、checkpoint 与重放边界。
  • 从事件序列手工推出模型可见消息。

3. 前置知识

可类比 Redux action log 与 selector:日志记录发生过什么,投影计算当前视图。局限是 Session 事件有序号、冻结 JSON、格式版本和持久化合同,不能重放任意副作用。

4. 对应源码

5. 工作原理

Session Log 保存 durable event;Surface 决定当前哪些消息对模型可见;deriveMessages() 从 Surface 派生下一次模型请求的历史;projection、浏览器快照和统计都可从事件重建。replace 只改变可见区间,不删除旧日志。JSONL backend 负责落盘,checkpoint 缩短恢复成本,但不能取代原始语义。

6. 执行流程

无图回退:事件 → append-only log → Surface / persistence / page()+follow()deriveMessages()、projection、Client Session → LLM 上下文和 React UI。

7. 关键源码讲解

SESSION_FORMAT_VERSION 是跨版本恢复边界;Session 保证事件序列;deriveMessages() 只读取模型可见节点。新增事件类型时必须同步检查 envelope schema、Surface 规则、持久化 codec、projection 和 UI assembler,否则会出现“日志可写但无法恢复或展示”。

8. 调试与观察方法

断点放在 append、Surface 校验和 deriveMessages();观察 seqtypesurfaceOp、Surface nodes。先用最小事件序列复现,再接 JSONL 与 UI。常见故障:seq 断裂、replace 范围错误、格式版本不兼容、checkpoint 与日志偏移不一致。

9. 本章实践任务

追加 user、turn/start、assistant/chunk、assistant/message,再用 replace 写入摘要;分别断言物理日志长度、Surface 节点和派生消息。验收:chunk 不直接成为模型历史,replace 后旧事件仍可审计。

10. 常见误区

  • 把每个 chunk 当模型消息。
  • 把 replace 当物理删除。
  • 把 projection backend 当 persistence backend。

11. 自测题

  1. 为什么 chunk 不直接进入模型历史?
  2. deriveMessages() 的事实输入是什么?
  3. checkpoint 能否替代日志?
  4. 新事件类型至少影响哪些消费者?
  5. 如何证明一个投影可重建?

第 4 章:智能体(Agent)、收件箱(Inbox)与轮次/步骤

1. 本章定位

这一章沿新版 ReactLoopAgent 追踪一次输入如何变成多个模型与工具步骤。

打开本章交互图解:一次 Agent Turn

2. 学习目标

  • 区分 followup()steer()inject()
  • 解释轮次(Turn)与步骤(Step)。
  • 追踪 turn()step()buildRequest()prepareCall()
  • 解释取消如何穿过模型流与工具执行。

3. 前置知识

可把 Agent Loop 类比为 reducer 加 effect 的状态机,把 Inbox 类比为带边界的事件队列。局限是模型和工具是长异步任务,取消是协作式的,Turn/Step 边界还必须写入 durable log。

4. 对应源码

5. 工作原理

followup 建立下一 Turn,steer 在下一 Step 改变正在运行的方向,inject 只排队、不主动唤醒。一次 Turn 可以包含多个 Step:每个 Step 派生历史、装配 Prompt、准备 LLM 调用、消费 chunk;若得到 tool/call,就执行工具、写回 tool/result,再进入下一 Step。

6. 执行流程

无图回退:UI → Inbox → turn()step() → SystemPrompt + deriveMessages()prepareCall() → LLM stream → ToolRuntime(可选)→ Session Log → 下一 Step 或 turn/end。

7. 关键源码讲解

公开输入方法只返回入队结果,不代表最终回答。最终状态通过 Session events 被 UI 或协议层观察。prepareCall() 把适配器选择冻结到一次调用;取消信号需要同时约束 LLM iterator、工具任务和 Turn 收尾,不能只改变 UI 状态。

8. 调试与观察方法

断点:turn() 开始/结束、step()prepareCall()、工具结果写回处。观察 Inbox 边界、当前 phase、AbortSignal、最后事件 seq。事件停在 request/start 后通常指向 LLM;停在 tool/call 后通常指向策略、批准或工具 body。

9. 本章实践任务

用 mock LLM 先返回一个 tool/call,再返回最终文本;断言一个 Turn 包含两个 Step,tool/result 位于两次模型调用之间。再中途 abort,证明终态只有一次。

10. 常见误区

  • 一个用户输入不等于一个 Step。
  • inject() 不保证立即执行。
  • 取消不是删除已经写入的 durable event。

11. 自测题

  1. followupsteerinject 的唤醒语义有何不同?
  2. 为什么工具结果会触发下一 Step?
  3. prepareCall() 在哪一层发生?
  4. 如何从事件判断卡在 LLM 还是 Tool?
  5. 设计一个“取消恰好发生在工具完成时”的测试。

第 5 章:系统提示词(System Prompt)装配

1. 本章定位

系统提示词不是一个常量,而是插件按作用域贡献并在每次调用前装配的结构。

打开本章交互图解:System Prompt 装配

2. 学习目标

  • 区分 sectioncontexttoolsvariable
  • 解释注册顺序、作用域和 complete section。
  • assemble() 追到 renderPrompt()
  • 识别提示词顺序对模型缓存和行为的影响。

3. 前置知识

可把贡献项类比为命名 Slot,把本次 Scope 类比为组件 render 的 props。局限是输出顺序会影响模型约束与 KV Cache,不能把 section 当任意 UI 子树重排。

4. 对应源码

5. 工作原理

插件注册四类贡献;装配时读取当前 Scope,解析 shadow/complete 语义与顺序,经 system-prompt/assemble waterfall 允许扩展,最后严格替换变量并生成本次模型请求的 system 文本和工具说明。

6. 执行流程

无图回退:section/context/tools/variable → 当前 Prompt Scope → SystemPrompt.assemble() → waterfall → renderPrompt()PreparedLlmCall

7. 关键源码讲解

section()context() 解决内容贡献,tools() 保证模型看到的能力说明与当前作用域一致,variable() 延迟提供运行时值。assemble() 的顺序是行为;动态内容插入稳定前缀前会降低 provider 缓存复用。waterfall 若直接替换结果,必须保留其他插件的贡献。

8. 调试与观察方法

assemble() 返回处查看贡献来源、顺序和 Scope;在 renderPrompt() 未知变量分支设置异常断点。工具执行存在但 Schema 不可见时,先比较 Prompt 的工具集合与 ToolRuntime restriction。

9. 本章实践任务

注册一个 study:note section 和变量,断言全局/局部 Scope、顺序、未知变量错误和卸载恢复。验收:测试不是只查字符串包含,还验证顺序与 Disposer。

10. 常见误区

  • 把 persona 当唯一提示词来源。
  • 在 waterfall 中丢掉已有贡献。
  • 把高频动态内容放到稳定前缀前。

11. 自测题

  1. 四类贡献分别解决什么问题?
  2. complete section 有什么边界?
  3. 未注册变量如何失败?
  4. 顺序为何影响 KV Cache?
  5. scoped shadow 比修改全局贡献安全在哪里?

第 6 章:大模型能力接缝(LLM Seam)与流式调用

1. 本章定位

理解模型供应商协议如何被适配为 Agent Loop 可消费的统一异步流。

打开本章交互图解:LLM 流式调用

2. 学习目标

  • 区分 LlmRuntimeLlmAdapterPreparedLlmCall
  • 解释 llm/stream waterfall。
  • 追踪 chunk、重试、取消与最终错误。
  • 找到 DeepSeek API 扩展字段的边界。

3. 前置知识

可把 Adapter 类比请求库的 transport adapter,把规范化 chunk 类比领域事件。局限是模型流只能消费一次,包含 reasoning、tool call、usage 和终态,且取消会跨越多个异步层。

4. 对应源码

5. 工作原理

prepareCall() 解析 provider/model 并冻结本次 adapter registration;stream() 经过 waterfall,让插件包装、观测、替换或短路调用;Adapter 只处理鉴权、供应商请求和 wire chunk 规范化。是否进行 Agent 级重试由上层运行状态决定,不应塞进供应商解析器。

6. 执行流程

无图回退:Agent → prepareCall()PreparedLlmCallllm/stream waterfall → Adapter → DeepSeek API → 规范化 chunk → Agent → Session Log;可重试错误回到策略,最终错误写入日志。

7. 关键源码讲解

Prepared Call 防止热更新期间 adapter 选择漂移,也阻止重复消费同一流。DeepSeek 私有字段停留在扩展包与 Adapter 边界;Agent Loop 只依赖统一 chunk。新增 chunk 类型必须同步检查 assembler、Session schema、前端对话装配和快照测试。

8. 调试与观察方法

断点:prepareCall()、waterfall 前后、Adapter stream。观察 resolved provider/model、chunk 序列、AbortSignal、finish reason。分别测试首 chunk 前失败、部分 chunk 后失败、正常结束与取消。

9. 本章实践任务

用 mock adapter 产生 text、tool call、usage、error 四类流,断言每条流只有一个终态;再 abort 验证 transport 收到信号。无需真实 API Key。

10. 常见误区

  • 在 Agent Loop 判断厂商字符串。
  • 在 Adapter 决定整个 Turn 是否重试。
  • 把每个 delta 当独立模型消息。

11. 自测题

  1. 为什么要有 Prepared Call?
  2. waterfall 可以做什么?
  3. 哪些重试属于 transport,哪些属于 Agent?
  4. 私有字段为何不能泄漏到 Agent?
  5. 新 chunk 类型有哪些下游消费者?

第 7 章:工具(Tools)、批准与程序化调用(PTC)

1. 本章定位

工具是模型意图通往真实副作用的安全边界;新版以程序化工具调用(Programmatic Tool Calling,PTC)统一批量/程序化调度,不再沿用旧版 Code Mode 叙述。

打开本章交互图解:工具执行与批准

2. 学习目标

  • 编写带输入/输出 Schema 的工具。
  • 区分 allow、deny、ask 与 Guard。
  • 追踪 prepare、dispatch、finalize 三阶段。
  • 解释 PTC、后台任务和 UI 展示元数据。

3. 前置知识

可把 Tool Registry 类比命令总线,把 pre/execute/post 类比中间件。局限是这里还要处理模型 Schema、权限批准、并发、取消、durable result 与跨进程 UI 合同。

4. 对应源码

5. 工作原理

工具调用先解析并验证参数,再经过 tools/pre-execute 得到 allow/deny/ask;ask 交给 Approval UI。Guard 采用单调拒绝:任何 deny 都不能被后层重新放行。允许后进入调度与 body,随后 post-execute、输出验证、materialize,最终写入 tool/result。PTC 和后台任务仍受相同策略、日志与结果语义约束。

6. 执行流程

无图回退:tool/call 或 PTC → Schema → pre-execute → Guard → ask/allow/deny → execute → post-execute → canonical output → tool/result.meta → 下一模型 Step 和工具卡片。

7. 关键源码讲解

可见性、lookup 和执行限制必须来自同一 Scope。output.render 生成模型可读文本,presentationMeta 是持久的 UI 提示;二者都不能替代 canonical output。PTC 只是调用表达方式,不是绕过批准或日志的后门。

8. 调试与观察方法

断点:执行入口、prepare、dispatch、finalize。记录工具名、解析参数、Scope、策略决策、body 是否进入、canonical value、materialized content 与最终 result。外部副作用已发生却无结果时,优先检查 post/output/finalize,避免盲目重试。

9. 本章实践任务

先实现返回固定值的 workspace_summary,覆盖未知工具、非法参数、ask→approve、Guard deny、输出错误、abort 六条路径;再接只读目录统计。完整实现见第 11 节。

10. 常见误区

  • 把 PTC 当作旧 run_code transport 的改名。
  • 只在 Prompt 隐藏危险工具,不做执行时策略。
  • 认为用户批准后可跳过路径与 Schema 校验。

11. 自测题

  1. pre-execute、Guard、execute、post-execute 各管什么?
  2. 为什么 Guard 必须单调拒绝?
  3. PTC 如何保持相同日志语义?
  4. output.renderpresentationMeta 有何区别?
  5. 工具产生副作用后输出校验失败,应怎样设计幂等性?

第 8 章:持久化、投影与 Web 客户端

1. 本章定位

把 Host 的 durable Session 事件追到新版 Client Session、对话节点装配和 React keyed Slot。

打开本章交互图解:Host 到 React 数据流

2. 学习目标

  • 区分 persistence、projection、checkpoint 与 Client snapshot。
  • 解释 page()/follow() 的分页和追帧模型。
  • 追踪 SessionNotifierConversationNodeAssembler
  • 解释 useSyncExternalStore 与 keyed Slot。

3. 前置知识

可把 Client Session 类比框架无关的外部 Store,把 projection 类比纯 selector,把 keyed Slot 类比插件化渲染插槽。局限是 Host Log 才是 durable truth,浏览器快照可随时丢弃重建。

4. 对应源码

5. 工作原理

Host 用 page() 提供历史窗口,用 follow() 继续发送有序事件;Client Session 维护游标、去重和对象层状态;Assembler 把事件变成对话节点;Notifier 合批后发布引用稳定的 snapshot;React 通过 useSyncExternalStore 订阅。keyed Slot 根据 wire tool name 选择工具卡片。

6. 执行流程

无图回退:Session Log → SessionController page/follow → Client Session → Assembler/Notifier → immutable snapshot → useSyncExternalStore → conversation 与 tool card。

7. 关键源码讲解

getSnapshot() 在没有领域变化时必须返回同一引用,否则 React 会无意义重渲染。浏览器插件不应自行配对原始事件,也不应导入 Host 工具实现;Client Session 和 tool/result.meta 才是跨进程合同。

8. 调试与观察方法

按 Host seq → page/follow frame → Client cursor → assembler nodes → snapshot 引用 → React render 次数定位。常见故障:重复帧、断号、每 token 重建根对象、slot key 冲突、malformed meta 未回退。

9. 本章实践任务

在现有 Client 测试追加两个 chunk 与一个 tool/result,记录通知批次和 snapshot 引用;再注入重复/断号帧,并卸载一个 keyed Slot provider。验收:能区分“Host 已 durable 但 UI 未刷新”和“UI 有流式内容但最终 message 未形成”。

10. 常见误区

  • 把浏览器 Store 当持久化真相。
  • 让 React 直接消费网络帧。
  • 让可选 UI 插件互相 import,绕开 Slot 生命周期。

11. 自测题

  1. page()follow() 如何配合?
  2. Notifier 为什么要合批?
  3. snapshot 引用稳定性为何重要?
  4. Assembler 与 React Renderer 各负责什么?
  5. keyed Slot 如何让 Host/Browser 解耦?

8. 核心模块深度分析

这里重点拆 dsh-agent-loopdsh-session,因为它们是项目最能体现“Agent 是怎么回事”的模块。

8.1 模块要解决的问题

dsh-agent-loop 要解决:给定一个 Session 和模型能力,如何可靠地把输入、模型请求、工具调用、流式输出、取消、恢复和停止组织成可观察的 turn/step 循环。

dsh-session 要解决:如何让模型看到的上下文、UI 看到的聊天记录、磁盘上的会话记录和回放重建使用同一份事实。

8.2 核心抽象

  • Agent:公开生命周期接口。
  • AgentRegistry / AgentFactory:创建与恢复。
  • ReactLoopAgent:具体 driver。
  • Inbox:待处理输入的持久投影。
  • Session / SurfaceManager:事件日志与模型可见 surface。

8.3 关键数据结构

  • Phaseidle | maintenance | running,见 packages/core/agent-loop/src/agent.ts:38-46
  • PreparedStepreject | enter(messages, assembly),见 packages/core/agent-loop/src/agent.ts:50-52
  • SessionEvent<T>type/seq/time/data,surface 类型额外有 surfaceOp/sourceEventSeqs,见 packages/core/session/src/types.ts:404-430
  • PromptAssemblysections/contexts/tools/variables,见 packages/core/system-prompt/src/index.ts:115-120

8.4 公共接口

Agent 的公共接口包括:

  • followup(message)steer(message)inject(message)
  • cancel(cause, options?)
  • whenIdle()
  • runMaintenance(task)
  • status
  • session
  • ctx

依据:packages/core/agent/src/runtime-types.ts:64-144

8.5 内部协作关系

AgentLoop 依赖 agentssessionsllmtoolssystemPromptReactLoopAgent 不直接依赖具体 LLM adapter 或具体工具 provider;它依赖 ctx.llmctx.tools,所以两者可替换。

8.6 算法或处理流程

turn() 使用一个循环:

  1. 打开 turn。
  2. preStep() 认领输入并组装 prompt。
  3. 若 reject 或空输入,关闭 turn。
  4. 写入 step/start 和 user/message。
  5. step() 循环请求模型。
  6. 流式 chunk 写入日志并组装 assistant message。
  7. 有 tool-call 则执行工具,继续 step。
  8. 无 tool-call 且无 next-step 输入,则 agent/turn-stopping,关闭 turn。

对应源码:packages/core/agent-loop/src/agent.ts:246-330

8.7 状态生命周期

Agent 从 idle 开始。waking input 进入 running。一个 driver 可能连续跑多个 turn。维护任务运行时状态仍对外表现为 idle,但内部是 maintenance。dispose 会执行 disposed cancel、等待 whenIdle()、退出 registry、unwind scope。

8.8 错误处理

循环把两类错误分开:

  • 模型请求失败:以 finish chunk 的 error/aborted 形式进入 agent/request-error,监听器可返回 retry。
  • 中间件、结果处理、工具等扩展失败:直接 throw,结束当前 turn。

turn() 的 catch 会生成 turn/enderror reason,并通过 agent/error 报告,见 packages/core/agent-loop/src/agent.ts:302-322

8.9 性能考虑

  • Session.deriveMessages() 缓存每个 surface 节点,仅新增节点时投影。
  • Session.events 缓存快照,直到下次 append。
  • ReactLoopAgent.dispatch 在构造时构建一次,热路径不再分配。
  • SystemPrompt 组装会 clone tool schema,避免 provider 可变对象进入请求。

依据:packages/core/agent-loop/src/agent.ts:73-75packages/core/session/src/index.ts:701-747

8.10 并发或异步行为

  • 工具调度支持并发,但由 isConcurrencySafe() 决定;否则 exclusive。
  • tools/execute wrapper 可以替换 signal,但不能替换 call identity。
  • cancellation 是协作式,不能硬杀同一进程代码。
  • Agent 创建/恢复用 AbortSignal 和 teardown 合并,防止卸载时泄漏。

8.11 测试策略

仓库政策要求:

  • 单测覆盖核心状态机、错误路径、事件顺序、并发竞态。
  • e2e 用真实 API 验证“世界是否改变”,不能只检查模型自述。
  • snapshot 验证真实组合产出的 transcript。
  • 每个 registry 有 HMR cleanup 测试。

依据:docs/testing.md

8.12 设计模式

  • 微内核 + 插件:所有扩展通过服务与事件。
  • 注册表:tools、agents、adapters、projections。
  • strategy / provider:LLM adapter、persistence backend、sandbox backend。
  • pipeline / middleware:tools pre/execute/post、llm/stream、agent/pre-step。
  • event sourcing:Session log。
  • scope / layered registry:全局与 per-agent 注册。

8.13 当前设计的优点和代价

优点:

  • 可替换能力强。
  • 模型上下文可重建、可回放。
  • 扩展点类型化,容易静态发现。
  • 热更新和安全卸载有明确机制。

代价:

  • 学习曲线高。
  • 简单功能也要经过多个抽象层。
  • 运行时不变量和测试门禁要求高。
  • developer preview 下兼容性不稳定。

8.14 可替代实现

  • Agent loop 可以不是 ReactLoopAgent;公开接口在 dsh-agent,实现包在 dsh-agent-loop
  • Session 持久化可以换成 SQLite、JSONL 或自定义 backend。
  • 工具呈现可以用 native、code 或 both。
  • UI 可以从 Web 换成 ACP 或自定义协议 driver。

8.15 修改该模块时最容易破坏的约束

  • 不要绕过 Session log 直接给模型塞未记录内容。
  • 不要修改 agent-loop 主循环去加普通功能;应使用事件。
  • 不要让 waterfall listener 忘记 next()
  • 不要让工具返回不可 JSON 序列化的值。
  • 不要假设 agent-loop 是唯一实现;插件应依赖 dsh-agent

9. 运行、测试和调试指南

9.1 环境要求

仓库声明需要:

  • Node.js ^22.19 || >=24
  • Corepack 启用的 pnpm@11.7.0
  • Git 2.26+。
  • 可选 DEEPSEEK_API_KEY

9.2 命令分类

已执行并验证

2026-09-02 在证据提交 4e84901e 上执行:

sh
pnpm exec vitest run <11 个定向测试文>
## 11 test files passed; 660 tests passed

pnpm dsh --help
## 成功输出 launcher 选项、web/plugin 子命令与示例

DSH_HOME=<temp> pnpm dsh --profile web --dump-default-config
## 成功输出 base 与 web Bundle 组合树;临时目录随后删除

此外执行了 Git 提交、状态、文件和符号的只读检查。上面的通过结果只覆盖指定核心包与 CLI 帮助,不代表真实模型、Web UI、全部 E2E 或发布流程已经通过。

仓库声明但未执行

sh
pnpm run clean
pnpm run test # 全仓测试未执行;只执行了三个核心包
pnpm run test:coverage
pnpm run test:e2e
pnpm run test:snapshot
pnpm run typecheck
pnpm run lint
pnpm run build
pnpm run hygiene
pnpm run doc-sync
pnpm dsh --profile headless "task"
pnpm run demo:cordis
pnpm run demo:acp

推荐的最小复验

sh
pnpm exec vitest run packages/core/session/tests/session.spec.ts packages/core/agent-loop/tests/loop.spec.ts packages/core/tools/tests/tools.spec.ts packages/core/tools/tests/ptc.spec.ts

学习者仍应先用 rg --files <package> 确认测试范围,避免把路径拼错后“零测试通过”误记为成功。

9.3 安装步骤

建议流程:

sh
pnpm install
pnpm run typecheck

如果只是读代码,不安装依赖也可以完成大部分静态分析。真实运行、构建和测试需要依赖。

9.4 最小启动步骤

Web:

sh
pnpm run build
pnpm dsh web

Headless:

sh
pnpm run build
pnpm dsh --profile headless "summarize this workspace"

这些启动命令来自根 README.mddocs/development.md,【待确认】本次未进行需要真实 Key 的端到端启动。第一次实践优先使用 mock LLM 或只 dump 配置。

9.5 常用开发命令

  • pnpm run dev:web:重建客户端 bundle,配合 Web HMR。
  • pnpm run mock:llm:启动 mock LLM server。
  • pnpm dsh --profile web --dump-config:查看组合树。

9.6 测试命令

优先从 package 级测试入手,不要一上来跑全量。根 package.jsontestvitest run

9.7 如何运行单个测试

Vitest 通常支持:

sh
pnpm run test -- <test-file-pattern>

当前仓库 Vitest 已验证可直接接受 package 路径。运行单文件时应检查输出中的 Test Files/Tests 数量,确保过滤条件确实选中了目标。

9.8 如何开启调试

Node/tsx 可配合 IDE 断点。核心断点:

9.9 日志位置

源码里常见 ctx.logger.warn,不是统一文件日志。Session 持久化默认 root 来自 dshHomePath('sessions'),见 packages/bundle/base/cordis.patch.yml。浏览器和 telemetry 有独立机制。

9.10 环境变量

常用:

text
DEEPSEEK_API_KEY
DEEPSEEK_BASE_URL
DSH_PERMISSION_MODE
DSH_TELEMETRY_MODE
DSH_TELEMETRY_DISABLED
DSH_TELEMETRY_OTLP_URL
DSH_TOOLS_MODE

9.11 常见启动失败原因

  • 没有安装依赖。
  • 没有先 build,导致缺 Typert artifacts 或 client bundle。
  • Node/pnpm 版本不匹配。
  • bundle/patch 行引用了不存在的插件或 id。
  • 缺少 DEEPSEEK_API_KEY 时,真实模型调用失败。
  • 端口被占用。
  • Windows 与 POSIX shell 平台差异导致 bash/pwsh 行选择不同。

9.12 最小故障排查流程

  1. 看 CLI 第一阶段报错是启动器还是应用参数。
  2. --dump-config 看组合树是否正确。
  3. 检查 profile package.jsoncordis.patch.yml
  4. --patch overlay 做最小复现。
  5. runProfileboot()、Agent 创建处断点。
  6. 如果与模型相关,确认 adapter 注册、provider/model、key、base URL。
  7. 如果与工具相关,确认 ctx.tools 注册、schema、权限和 guard。

9.13 一张可复用的故障分层表

最后成功点下一检查点不应优先检查
CLI help参数所有权与 Profile 路径模型输出质量
dump configCordis entry 激活与 inject浏览器 CSS
Agent 创建inbox、pre-step 与 Session append发布脚本
request/headerAdapter、Key、base URL 与 streamTool body
tool/callschema、restriction、guard 与 schedulerProfile 查找
Session eventpersistence/downlink/projectionLLM 路由

每次排查保存:复现命令、提交、环境版本、最小输入、最后成功点、首个错误和恢复步骤。只有这些 信息齐全,故障记录才可被另一个开发者重放。


10. 渐进式实战项目

实践一:成功运行并观察核心流程

  • 任务背景:熟悉开发环境,看到真实启动和日志。
  • 学习目标:能安装、构建、运行并判断失败阶段。
  • 涉及模块:根工程、CLI、bundle。
  • 推荐修改点:先不修改代码,运行命令。
  • 实现步骤:
    1. 记录 node --versionpnpm --versiongit rev-parse HEAD
    2. pnpm install 后先运行 pnpm dsh --help
    3. 运行三个核心包测试:pnpm exec vitest run packages/core/session packages/core/agent-loop packages/core/tools
    4. 比较 web/headless 的 default config dump;不需要 API Key。
    5. 只有前述步骤通过后,才选择 mock LLM 或真实 Key 启动。
    6. 记录每个命令的退出码、成功/失败和首个错误文本。
  • 测试方法:以实际命令输出为准,不推测。
  • 验收标准:提交一份环境记录和启动分层图;能说出失败属于依赖、配置、插件激活、模型还是缺 Key。
  • 恢复方式:实践一不修改 tracked 文件;关闭服务并删除临时日志即可。
  • 可能踩坑:Windows 上 bash/pwsh 平台差异;未 build 缺 artifacts;网络受限。
  • 进一步挑战:使用 --dump-config 观察 web/headless 组合差异。

实践二:修改一个低风险已有功能

  • 任务背景:理解配置与测试流程。
  • 学习目标:做一个小改动并验证。
  • 涉及模块:system-prompt、bundle patch。
  • 推荐修改点:官方 scratch-plugin 中新增一个 study:note Prompt section,使用临时 --patch 挂载。
  • 实现步骤:
    1. docs/user/develop/basic/ 创建 scratch-plugin
    2. 注册固定名称与 order 的 Prompt section,并写 assemble/render 单测。
    3. extra.yml 挂载插件,比较 --dump-default-config--dump-config
    4. 增加 scoped 同名 section,验证 shadow 只影响目标 Agent。
    5. 卸载插件,确认基础快照恢复。
  • 测试方法:断言 section 文本、顺序、scope 和 disposer;dump 只用于证明真实入口。
  • 验收标准:单测通过,能解释 Patch 整行替换与 Prompt scoped shadow 是两种不同机制。
  • 恢复方式:移除 --patch 并删除 scratch-plugin;不修改默认 Bundle。
  • 可能踩坑:保留字段漏写;id 不匹配。
  • 进一步挑战:把 persona 改为模板变量,观察变量解析错误。

实践三:定位并修复一个模拟问题

  • 任务背景:掌握调用链、日志和断点。
  • 学习目标:定位故障发生在哪个模块。
  • 涉及模块:Agent loop、tools、LLM、Session log。
  • 推荐修改点:使用 mock LLM 确定性地产生一个不存在的工具名,再增加一个永远 deny 的 guard。
  • 实现步骤:
    1. 让 mock LLM 发出 missing_tool,在 lookup/execute 入口观察 UNKNOWN_TOOL
    2. 改为真实已注册的无副作用工具,确认成功基线。
    3. 注册 guard deny,确认 body 没有进入且结果被规范化。
    4. 移除 guard,故意返回错误 output 类型,定位失败发生在 body 之后、提交结果之前。
    5. 对三次运行分别保存 Session event 序列。
  • 测试方法:使用断言验证 body 调用次数、错误 kind 和是否存在最终 tool/result
  • 验收标准:能独立区分 unknown、policy deny、body/output failure,且修复后原测试通过。
  • 恢复方式:移除临时 guard 和 mock response,重新运行目标 package 测试。
  • 可能踩坑:工具可能没被模型选中,需选确定性任务或 mock LLM。
  • 进一步挑战:模拟 agent/pre-step reject,观察空 turn 的 turn/end

实践四:实现一个小型扩展

  • 任务背景:使用正式扩展机制增加能力,证明具备初步二次开发能力。
  • 学习目标:新增一个工具或 prompt section,并补测试。
  • 涉及模块:ctx.toolsctx.systemPromptcordis.yml
  • 推荐修改点:在官方 scratch-plugin 中实现 workspace_summary,避免第一次实践就修改核心包。
  • 实现步骤:
    1. 参数定义为 path(相对工作区)和 maxFiles;禁止 ..、绝对路径和额外字段。
    2. canonical output 包含 pathfileCount、按扩展名统计和截断标记;为模型 render 简短文本。
    3. defineTool 声明输入/输出 schema,在 apply(ctx) 中注册。
    4. 使用工作区/文件系统公开能力读取目录,不直接依赖某个私有 provider 类。
    5. 用临时 cordis.yml/Patch 挂载,dump 配置并检查工具 schema。
    6. 单测覆盖空目录、混合扩展名、maxFiles 截断、越界路径、取消和 dispose。
    7. mock LLM 调用工具,断言最终 Session tool/result;有真实 Key 时只做额外人工验收。
  • 测试方法:纯函数统计测试 → 插件注册测试 → Tool Runtime 集成测试 → mock LLM 主链测试。
  • 验收标准:无 API Key 也能完成全部自动化验收;工具只读、路径受限、结果可序列化,卸载后 registry 无残留。
  • 可能踩坑:符号链接逃逸、把绝对路径暴露给模型、遍历无上限、output 与 render 不一致、错误地声明并发安全。
  • 恢复方式:从 Patch 移除插件并删除 scratch-plugin;工具为只读,不产生业务数据。
  • 进一步挑战:增加 keyed UI card 或 Prompt section,但不得把这些表现层代码写进 Tool Runtime 核心。

11. 二次开发指南:理解“一切皆插件”

打开交互图解:workspace_summary 端到端双面插件

11.1 “一切皆插件”到底是什么意思

它不是“系统没有核心”。核心仍定义不能破坏的不变量:Session 事件格式、Agent Turn/Step、Tool 策略顺序、LLM 流合同和 Client Slot 合同。所谓“一切皆插件”,是实际行为尽量由可装配、可作用域化、可撤销的注册贡献形成,而不是硬编码进主循环。

五种常见形态:

形态作用最小特征
Cordis 函数插件注册监听器或能力apply(ctx, config)
服务插件提供可替换能力Service Definition + Provider + Consumer
Profile/Bundle组合插件树dsh.profile.bundles / dsh.bundle.patch
Host 工具插件给模型真实能力inject = ['tools'] + defineTool()
Browser 插件给 Web 增加视图package.json.dsh.client + keyed Slot

判断规则:普通功能优先插件;供应商差异优先 Adapter;只有公开语义或跨模块不变量变化才修改 core/*

11.2 第一步:可由 --patch 加载的 Host 工具

在上游仓库的 scratch-plugin/ 中建立:

text
scratch-plugin/
├─ package.json
├─ cordis.yml
└─ src/
   └─ index.ts

接口固定为:

ts
export type WorkspaceSummaryInput = {
  path: string
  maxFiles?: number
}

export type WorkspaceSummaryOutput = {
  path: string
  fileCount: number
  byExtension: Record<string, number>
  truncated: boolean
}

src/index.ts

ts
import { lstat, readdir, realpath } from 'node:fs/promises'
import { extname, isAbsolute, relative, resolve, sep } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'workspace-summary'
export const inject = ['tools']

export interface Config {
  workspaceRoot: string
}

type Output = {
  path: string
  fileCount: number
  byExtension: Record<string, number>
  truncated: boolean
}

function inside(root: string, candidate: string): boolean {
  const rel = relative(root, candidate)
  return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel))
}

async function summarize(rootInput: string, requested: string, maxFiles: number, signal: AbortSignal): Promise<Output> {
  signal.throwIfAborted()
  const root = await realpath(rootInput)
  const target = resolve(root, requested)
  if (!inside(root, target)) throw new Error('path must stay inside workspaceRoot')
  const targetReal = await realpath(target)
  if (!inside(root, targetReal)) throw new Error('resolved path escapes workspaceRoot')

  const files: string[] = []
  const walk = async (directory: string): Promise<void> => {
    signal.throwIfAborted()
    const entries = await readdir(directory, { withFileTypes: true })
    entries.sort((a, b) => a.name.localeCompare(b.name))
    for (const entry of entries) {
      signal.throwIfAborted()
      const absolute = resolve(directory, entry.name)
      const stat = await lstat(absolute)
      if (stat.isSymbolicLink()) continue
      if (stat.isDirectory()) await walk(absolute)
      else if (stat.isFile()) files.push(relative(root, absolute).split(sep).join('/'))
    }
  }
  await walk(targetReal)
  files.sort()
  const selected = files.slice(0, maxFiles)
  const byExtension: Record<string, number> = {}
  for (const file of selected) {
    const extension = extname(file).toLowerCase() || '[no extension]'
    byExtension[extension] = (byExtension[extension] ?? 0) + 1
  }
  return {
    path: relative(root, targetReal).split(sep).join('/') || '.',
    fileCount: selected.length,
    byExtension,
    truncated: files.length > selected.length,
  }
}

export function apply(ctx: Context, config: Config): void {
  ctx.tools.register(defineTool({
    name: 'workspace_summary',
    description: '统计工作区内目录的文件数量与扩展名分布,不读取文件内容。',
    parameters: {
      path: { type: 'string', required: true, description: '工作区内的相对路径' },
      maxFiles: { type: 'number', description: '排序后最多统计 1 到 500 个文件' },
    },
    output: {
      schema: {
        type: 'object', additionalProperties: false,
        properties: {
          path: { type: 'string', required: true },
          fileCount: { type: 'number', required: true },
          byExtension: { type: 'object', required: true, additionalProperties: true },
          truncated: { type: 'boolean', required: true },
        },
      },
      render: (_args, value) => [{
        type: 'text',
        text: `${value.path}: ${value.fileCount} files${value.truncated ? ' (truncated)' : ''}\n`
          + Object.entries(value.byExtension).map(([ext, count]) => `${ext}: ${count}`).join('\n'),
      }],
      presentationMeta: (_args, value) => ({
        kind: 'workspace-summary', ...value,
      }),
    },
    async execute(args, exec) {
      if (args.path === '' || args.path.includes('\0')) throw new Error('path must be a non-empty relative path')
+      if (Object.keys(args).some(key => key !== 'path' && key !== 'maxFiles')) throw new Error('unknown argument')
      const maxFiles = args.maxFiles ?? 100
      if (!Number.isInteger(maxFiles) || maxFiles < 1 || maxFiles > 500) {
        throw new Error('maxFiles must be an integer from 1 to 500')
      }
      return summarize(config.workspaceRoot, args.path, maxFiles, exec.signal)
    },
  }))
}

这里的安全边界是:只读、不读文件内容、不跟随符号链接、realpath 后再次确认仍在工作区、稳定排序后截断,并在循环中响应 AbortSignaloutput.render 面向模型;presentationMeta 持久化前端重放所需事实。当前 Value Schema 要求 additionalProperties 为布尔值,因此动态扩展名 Map 由实现构造为数字值,并由单测锁定该约束。

最小 Patch(实际 Loader 语法以 scratch-plugin 教程生成的插件入口为准):

yaml
- id: workspace-summary
  plugin: ./src/index.ts
  config:
    workspaceRoot: /path/to/deepseek-harness

【仓库声明但未执行】启动:

bash
pnpm dsh web --patch ./scratch-plugin/cordis.yml

撤销:停止进程并从临时 Patch 移除该 entry;不要修改默认 Bundle。

11.3 第二步:整理成 Host + Browser 双面包

text
packages/learning/workspace-summary/
├─ package.json
└─ src/
   ├─ index.ts              # 上面的 Host 工具
   └─ client/
      ├─ index.tsx          # Browser apply
      └─ locales.ts

package.json 的关键公开接口:

json
{
  "name": "@example/dsh-workspace-summary",
  "type": "module",
  "main": "lib/index.js",
  "exports": {
    ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
    "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" }
  },
  "dsh": {
    "client": {
      "inject": [
        "@deepseek-ai/dsh-client-locale",
        "@deepseek-ai/dsh-client-ui-renderer",
        "@deepseek-ai/dsh-client-ui-tool"
      ],
      "platform": "web"
    }
  }
}

若一个包只有 UI,Host src/index.ts 可以像 packages/client/ui-skill/src/index.ts 一样只导出空 apply(): void {},让 Loader 挂载后由 Client Module 系统发现 dsh.client,无需重建整个 Web 应用。当前示例同包还含 Host 工具,所以根入口使用上一节 apply()

src/client/locales.ts

ts
export const NS = 'workspaceSummary'
export const zh = {
  'title': '工作区摘要', 'pending': '正在统计', 'success': '统计完成',
  'error': '统计失败', 'truncated': '结果已截断', 'malformed': '结果格式无法识别',
} satisfies Record<string, string>
export type Key = keyof typeof zh
export const en = {
  title: 'Workspace summary', pending: 'Scanning', success: 'Complete',
  error: 'Failed', truncated: 'Result truncated', malformed: 'Malformed result',
} satisfies Record<Key, string>

src/client/index.tsx(刻意只依赖 wire block,不导入 Host 实现):

tsx
import type { Context } from '@deepseek-ai/cordis'
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import type {} from '@deepseek-ai/dsh-client-locale/client'
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
import { en, NS, zh } from './locales.js'

type SummaryMeta = {
  kind: 'workspace-summary'; path: string; fileCount: number
  byExtension: Record<string, number>; truncated: boolean
}

function parseMeta(value: unknown): SummaryMeta | null {
  if (typeof value !== 'object' || value === null) return null
  const v = value as Record<string, unknown>
  if (v.kind !== 'workspace-summary' || typeof v.path !== 'string'
    || typeof v.fileCount !== 'number' || typeof v.truncated !== 'boolean'
    || typeof v.byExtension !== 'object' || v.byExtension === null) return null
  if (!Object.values(v.byExtension as Record<string, unknown>).every(n => typeof n === 'number')) return null
  return v as SummaryMeta
}

function WorkspaceSummaryRow({ block }: ToolCallViewProps) {
  if (!('kind' in block)) return <div>工作区摘要:正在统计…</div>
  if (block.isError) return <div role="alert">工作区摘要:统计失败</div>
  const meta = parseMeta(block.meta)
  if (meta === null) return <div>工作区摘要:结果格式无法识别</div>
  return (
    <section data-tool="workspace_summary">
      <strong>{meta.path} · {meta.fileCount} 个文件</strong>
      {meta.truncated ? <span>(结果已截断)</span> : null}
      <ul>{Object.entries(meta.byExtension).map(([ext, count]) => <li key={ext}>{ext}: {count}</li>)}</ul>
    </section>
  )
}

export const inject = ['slots', 'locale']
export function apply(ctx: Context): void {
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'workspace-summary: dictionaries')
  ctx.slots.inject('tool.call.toolview', () => ctx.slots.register(
    { name: 'tool.call.toolview', key: 'workspace_summary', locale: NS },
    WorkspaceSummaryRow,
  ))
}

当前 wire 类型把持久结果元数据挂在 settled block 上;当前 ToolCallBlock 将持久结果元数据暴露为 meta。升级依赖时以该类型声明为准并同步测试,不能用 any 静默绕过。

11.4 测试:把安全和回放当一等公民

至少覆盖:

ts
it.each([
  ['.', 10, false],
  ['src', 1, true],
])('returns a stable summary', async (path, maxFiles, mayTruncate) => { /* fixture + assertions */ })

it('rejects absolute, escaping paths and extra arguments', async () => { /* no traversal occurs */ })
it('does not follow symlinks', async () => { /* escaped target is absent */ })
it('stops when exec.signal aborts', async () => { /* rejects once */ })
it('renders pending, success, error and truncated states', () => { /* client component */ })
it('falls back for malformed metadata', () => { /* never crashes replay */ })

【仓库声明但未执行】包内测试与类型检查:

bash
pnpm exec vitest run packages/learning/workspace-summary/tests
pnpm --filter @example/dsh-workspace-summary bundle

11.5 两侧不可跨越的边界

  • Browser 插件不得导入 Host 的目录遍历或工具定义。
  • Browser 不自行配对 tool/calltool/result,也不重建调用树;ui-tool 提供冻结 block。
  • wire tool name workspace_summary 是 keyed Slot 的选择键。
  • tool/result 与持久 presentationMeta 是实时展示和回放的共同合同。
  • malformed/旧日志必须回退到 generic 行,展示错误不能让会话回放崩溃。

11.6 提交前检查清单

  • 是否优先使用正式扩展点而非修改 Agent Loop?
  • 输入和 canonical output 是否都经过 Schema?
  • 路径、权限、取消和并发是否在 Host 强制?
  • Client 是否只依赖 wire contract,并覆盖五种状态?
  • 注册是否随 Scope dispose?
  • 是否补 package README、定向测试、类型检查和 Loader smoke?
  • 是否保留 Session 事件兼容与可重建性?

12. 风险区域与设计取舍

12.1 高耦合区域

  • agent-loopsession/system-prompt/tools/llm 高度耦合,但耦合点在服务接口,而不是具体实现。
  • surfacederiveMessages 被持久化、compaction、UI、回放共同依赖,改动影响面大。

12.2 隐式约束

  • seq = log.length,见 packages/core/session/src/index.ts:564-567
  • surface event 必须带 surfaceOp
  • model-visible 必须 logged。
  • waterfall listener 必须调用 next() 才委托。
  • 同一 agent/session 使用同一 id。

12.3 全局状态

  • Host 端有 ctx.sessionsctx.agentsctx.tools 等全局 registry。
  • 浏览器端有 slot registry 和 session manager。
  • 这些状态通过 effect、scope 和 disposer 管理,不是无主全局变量。

12.4 并发或异步风险

  • 工具并发要求 isConcurrencySafe() 正确;错误声明会导致共享状态竞争。
  • tools/execute 只能替换 signal,不能替换 call identity,避免取消漂移。
  • Agent 创建/恢复和 HMR 同时发生时,生命周期必须可回滚。
  • 浏览器流式 token 高频更新必须合批,避免 React 抖动。

12.5 缓存一致性

  • Session.deriveMessages()events 都缓存快照;append 后失效。
  • surface replacement 会使 replaceGeneration 增加,派生历史重建。
  • projection 缓存与事件流、持久化恢复之间需要 checkpoint。

12.6 数据迁移或兼容性

  • 当前 SESSION_FORMAT_VERSION = 0,不承诺兼容。
  • 新增普通 event 类型靠 ignorable;结构变化才 bump format。
  • 后端会拒绝旧 on-disk 格式。

12.7 性能瓶颈

  • 每个 token 都 append assistant/chunk,高频日志路径必须避免阻塞 I/O。
  • 大历史派生需要 projection 和 compaction。
  • Web 客户端必须避免每个 token 重建整棵 React 树。

12.8 安全边界

  • 沙箱、permission、approval、fs policy 是独立能力。
  • 程序化工具调用(PTC)通过生成的工具 SDK 调度可见工具;逻辑调用仍重新进入权限、Guard、执行、结果和日志管线。
  • credentials 不进入日志。
  • Web browser trust 是 Host/Client 边界之一。

12.9 测试覆盖薄弱区域

从文档看,项目强调高覆盖,但需要真实 API 的行为只能在有 key 时验证。无 key 的 CI 自动跳过 e2e,因此“真实模型 + 真实工具”的回归依赖本地或专用 CI。

12.10 文档与实现差异

当前未发现明显 README 与源码冲突。由于项目迭代快,生成 catalog 和 package README 可能滞后,应以当前源码和配置为准。

12.11 新手不应首先修改的代码

  • core/session 的 format/surface 规则。
  • core/agent-loop 的 phase/cancel/error 状态机。
  • core/tools 的调度与 materialize 逻辑。
  • boot/app-boot 的 patch 解析和 module fallback。
  • vendor/

13. 最终源码阅读清单

按顺序逐项勾选。

文件或目录关键符号阅读目标预计难度完成标准
README.md运行命令知道入口能说出 web/headless
docs/cordis-primer.mdService/inject/effect/dispatch理解插件框架能解释 waterfall
docs/architecture.mdturn flow/seams/extension map建立架构图能画核心流
packages/core/README.mdctx 键认识 spine能背六项核心服务
packages/bundle/base/cordis.patch.ymlrows认识默认组合能找到 tools/agent-loop/llm
apps/cli/src/bin.tsparseDshArgs/runProfile找到入口能追踪到 boot
apps/cli/src/profile-boot.tscomposeProfile/runProfile理解启动能说明 patch 顺序
packages/core/session/src/types.tsSessionEventMap掌握事件词汇中高能区分 surface/log-only
packages/core/session/src/index.tsSession.append/deriveMessages理解派生历史能解释增量投影
packages/core/session/src/surface.tsSurfaceManager理解 replace能解释 compaction 投影
packages/core/agent/src/runtime-types.tsAgent/PreStepDecision掌握公开接口中高能解释 followup/steer/inject
packages/core/agent-loop/src/agent.tsReactLoopAgent/turn/step追踪主循环能画出时序图
packages/core/agent-loop/src/index.tsAgentLoop/create/resume理解生命周期能解释 teardown
packages/core/agent-loop/src/tool-calls.tsexecuteToolCalls理解工具调度能解释并发决策
packages/core/system-prompt/src/index.tsassemble/section/tools理解 prompt 装配能注册一个 section
packages/core/tools/src/index.tsToolRuntime/register/execute理解工具管线能写一个 tool
packages/llm/llm/src/index.tsLlmRuntime/LlmAdapter理解模型 seam能接入 adapter
packages/boot/app-boot/src/profile.tsloadProfile/composeEntries理解配置组合能写 patch
docs/cookbook/extension-cookbook.mdfeature -> mechanism掌握扩展地图能选扩展点
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.mdobject layer/slots理解 Web client中高能解释三层

14. 掌握程度检查

初级:能够使用

  • 能说出项目目标、入口和 profile/bundle。
  • 能按仓库命令安装依赖并启动一种 profile。
  • 能定位 packages/core 六项核心服务。
  • 能查看 --dump-config 或相关配置。

可观察验收:

  1. 画出 dsh-baseweb/headless 的关系。
  2. 在源码中找到 ctx.toolsctx.llmctx.sessions 的定义。
  3. 解释 pnpm dsh --profile web 为何会启动 UI。

中级:能够解释

  • 能解释 Session log 与 deriveMessages()
  • 能解释 turn/step 和工具循环。
  • 能解释 capability seam 三件套。
  • 能解释 Cordis effect 和 waterfall。

可观察验收:

  1. 独立画出 followup -> preStep -> llm -> tools -> session 时序图。
  2. 说明 agent/pre-step reject 时日志如何记录。
  3. 说明为什么替换 persistence backend 不应修改 loop。

高级:能够修改和扩展

  • 能定位故障并修改功能。
  • 能增加一个 tool、prompt section 或 LLM adapter。
  • 能补测试和必要 snapshot。
  • 能设计一个最小 Agent Harness。

可观察验收:

  1. 新增一个工具,模型 schema 能看到它。
  2. 修改一个 tools/pre-execute 策略,并证明拒绝生效。
  3. 用 100 到 300 行 TypeScript 实现简化版 Context + SessionLog + ToolRegistry + AgentLoop

15. 自测题参考答案

以下按关键要点给分,每题 2 分。

第 1 章

  1. web 提供浏览器表面,headless 运行单任务,sdk 提供完整库组合,sdk-minimal 提供最小 SDK 组合,acp 提供 Agent Client Protocol 表面。
  2. Bundle/Patch 后写层覆盖前层;条目顺序还决定插件装配与依赖可见性。
  3. --patch 适合不改默认 Profile 的临时验证、调试和功能试装。
  4. 停在 CLI/Profile/Patch/Loader 准备阶段,尚未进入 Agent 和 LLM。
  5. 能用现有稳定能力组合出新表面时新增 Bundle;只有公共启动不变量变化才改 boot 核心。

第 2 章

  1. Definition 定义 ctx.<key> 合同,provider 提供实现,consumer 通过 inject 使用。
  2. Scope 记录所有权和可见范围,能让同名能力隔离并随插件撤销。
  3. Scope dispose 时反向执行,撤销服务、监听器和其他注册。
  4. 有顺序依赖用 serial,互不依赖且都需完成用 parallel
  5. 连续挂载/卸载两次,断言服务键、监听器数量和输出都恢复到初始状态。

第 3 章

  1. chunk 是流式/replay 事实,模型历史使用组装完成的 assistant message,避免半成品进入下次请求。
  2. 当前 Session Log 经 Surface 选择后的可见节点。
  3. 不能;checkpoint 是恢复加速点,日志语义仍是事实源。
  4. envelope、Surface、持久化 codec、projection、Client assembler、UI 和兼容测试。
  5. 从空状态按序重放同一事件应得到相同结果,丢弃投影后可再次生成。

第 4 章

  1. followup 唤醒下一 Turn;steer 唤醒并进入下一 Step;inject 进入下一 Step 但不主动唤醒。
  2. tool/result 成为新模型上下文,Loop 需要下一 Step 让模型继续推理。
  3. step() 构建请求后、真正消费 LLM 流之前。
  4. request/start 后无 chunk 多指向 LLM;tool/call 后无 result 多指向策略、批准、调度或 body。
  5. 用可控 barrier 让 abort 与工具 resolve 同时发生,断言只提交一个权威终态且无重复 result。

第 5 章

  1. section 贡献文本片段,context 提供上下文,tools 贡献可见能力,variable 延迟提供模板值。
  2. complete 表示该贡献形成权威完整提示词边界,不能再按普通片段随意拼接。
  3. renderPrompt() 严格失败,避免把未解析占位符发给模型。
  4. 动态前缀会让后续 token 变化,降低 provider KV Cache 的前缀复用。
  5. shadow 只影响目标 Scope,dispose 后自动恢复,不污染其他 Agent。

第 6 章

  1. 冻结 adapter registration 和解析配置,避免热更新导致准备与 dispatch 使用不同实现,也保证一次性消费。
  2. 可观测、包装、缓存、替换、短路或施加跨 Adapter 策略。
  3. 握手、限流等供应商瞬时错误可属 transport;需要预算、历史和 Turn 状态的重试属 Agent。
  4. 否则核心会依赖厂商协议,无法保持统一 chunk 与可替换 Adapter。
  5. assembler、Session event、持久化/replay、Conversation assembler、UI 与 snapshot tests。

第 7 章

  1. pre 决定 allow/deny/ask;Guard 做最终单调拒绝;execute 管 body 生命周期;post 处理结果收尾。
  2. 任何安全插件拒绝后都不能被后层重新放行,否则组合顺序会削弱策略。
  3. PTC 逻辑调用重新进入正常 ToolRuntime,因此仍产生相同 tool/call、策略、result 和错误语义。
  4. render 是模型可见文本;presentationMeta 是持久、可回放的 Client 结构化事实。
  5. 输入校验前置,使用幂等键/事务,并记录外部操作标识,避免输出失败后盲目重试。

第 8 章

  1. page() 获取历史窗口,follow() 从游标继续追踪新事件;序号用于补齐和去重。
  2. 把高频事件合并成较少的外部 Store 通知,降低 React 重渲染。
  3. useSyncExternalStore 用引用判断快照是否变化;无变化却返回新对象会导致无意义渲染。
  4. Assembler 把事件变为领域对话节点;Renderer 把稳定节点与 Slot 组合成 React 树。
  5. Host 只输出 wire tool name 和 result/meta;Browser 业务包用相同 key 注册视图,两侧无需互相 import。

16. 未确认事项与后续调查清单

16.1 本次未能验证

  • 未运行依赖安装、构建、测试、启动和 demo。
  • 未验证真实模型流式、重试、token usage。
  • 未验证 JSONL/SQLite 落盘、恢复和损坏修复。
  • 未验证 Web UI 实际渲染、HMR、浏览器快照。
  • 未验证 Windows 与 POSIX 的 sandbox/shell 差异。

16.2 合理推断但未被直接证明

  • “该架构类似事件溯源”是学习类比,不是仓库对自身的正式定义。
  • “类似微内核/洋葱中间件”是为了帮助前端理解,不代表仓库官方标签。
  • 本教程中的建议断点和最小故障排查顺序来自源码结构推断,尚未实际运行。

16.3 需要查看 Issue、PR、Git 历史或官方文档才能回答的问题

  • 某些历史决策为何被替换或归档。
  • 具体 provider 的最新能力与限制。
  • 与竞品的差异细节。
  • 发布路线和兼容性承诺的后续变化。

16.4 建议下一步执行的调查操作

  1. pnpm install 后运行 pnpm run typecheck,记录真实结果。
  2. 运行最小 unit test,确认本机环境。
  3. 尝试 pnpm run build,然后跑 pnpm dsh --profile headless "..."
  4. --dump-config 比较 web/headless 组合。
  5. 打开 Web 或 ACP demo,观察真实流。
  6. 阅读一个真实 packages/llm/llm-deepseek 测试,理解 wire 协议。
  7. 选择一个 cookbook 教程,完成最小工具插件。