DeepSeek Harness 源码与架构学习教程(全书模式)
本页由
book.json与分章真源自动生成,请勿直接编辑。建议使用浏览器打印功能导出 PDF。
0. 分析范围与可信度说明
本教程以只读方式分析本地源码 <DEEPSEEK_HARNESS_SOURCE>,锁定 master 提交 4e84901e6471b79ec0338099867ebb4606d12bb5,版本 0.1.2-alpha.4。教程站真源位于 本站工作区;未修改上游仓库及其中现有未跟踪文件。
版本变化
| 项目 | 旧教程基线 | 当前基线 | 结论 |
|---|---|---|---|
| 版本 | 0.1.0-rc.5 | 0.1.2-alpha.4 | 开发预览版本继续演进 |
| 提交 | 47f943859bef60e4160492346772ded9b24f765a | 4e84901e6471b79ec0338099867ebb4606d12bb5 | 全量复核,不只换行号 |
| 变化 | — | 8,212 files;+448,477 / -161,788 | 目录与职责发生实质变化 |
| workspace packages | 221 | 251 | 新包在架构图和源码地图分层说明 |
| Client | client/runtime、client/web-react | api/session-controller、ui-conversation、ui-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:70,SessionController.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.md:It uses an architecture where everything is a plugin。 docs/architecture.md:Every 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”,开发者通常会手写一个循环:
读用户输入 -> 拼 Prompt -> 调模型 -> 看模型是否要求调用工具 -> 执行工具 -> 把结果塞回上下文 -> 再调模型如果只有一两个工具,这种方式可行。但能力多了以后,问题会迅速出现:
- 工具、模型、文件系统、子进程、权限、持久化、UI 都耦合在同一个循环里。
- 每增加一种能力,都要改主循环或复制分支。
- 无法只替换模型厂商、沙箱或文件系统,而不影响其他能力。
- 流式输出、取消、断线重连、会话恢复、回放和审计没有统一的真相来源。
- 前后端和自动化协议各自实现一套,容易类型漂移。
DeepSeek Harness 用四个核心设计回应这些问题:
- Cordis 插件化上下文:能力作为服务注册到
ctx,依赖关系通过inject声明,注册都是可撤销 effect。 - capability seam 三件套:
Service Definition/Service Provider/Consumer把“接口、实现、调用方”分离。 - event-sourced Session log:只追加的
SessionEvent是唯一真相,模型上下文从日志派生。 - profile + bundle + patch:同一个内核通过配置层组合成
web、headless、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 patch 和 cordis.patch.yml 组成的插件树。基础 bundle 装上模型适配器、工具、会话、沙箱、权限和遥测;web 或 headless bundle 再补上不同表层。
当用户提交一条消息:
- 消息进入
Agent的 inbox。 ReactLoopAgent打开一个turn。preStep()认领消息,组装 system prompt 和 tool schema。step()从Session.deriveMessages()得到模型历史。LlmRuntime按 provider/model 找到 adapter,流式返回 chunk。- 如果模型要求工具,
ToolRuntime经过 pre/guard/around/post/result 管线执行。 - 所有模型可见内容都作为
SessionEvent写回日志,下一轮再从中推导。
想扩展系统,通常不修改 loop,而是在 ctx.tools、ctx.llm、ctx.systemPrompt、ctx.agents 或 typed events 上注册插件。
2. 开始学习前需要掌握的概念
2.1 概念分层
必须先掌握
- Cordis 的
Context、Service、inject、ctx.effect()、typed events。 Service Definition / Service Provider / Consumer能力三件套。SessionEventappend-only log 与deriveMessages()。Agent、turn、step、inbox 和followup/steer/inject。- profile / bundle /
cordis.patch.yml的层叠组合。
可以边读边学
surface与SurfaceOp的 append/replace。SystemPromptsection/context/variable/tool provider。ToolRuntime的 pre-execute/execute/post-execute/result 管线。LlmAdapter、prepareCall()、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
- 定义:
Context是运行时服务仓库;Service是声明了稳定ctx.<key>的插件;inject声明依赖;ctx.effect()创建可撤销注册。 - 为什么需要:如果插件直接
import具体实现,就无法替换模型、沙箱、驱动器或 UI 组件。 - 项目体现:
AgentLoop的static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt'],见packages/core/agent-loop/src/index.ts:297。 - 源码位置:
docs/cordis-primer.md、packages/core/agent-loop/src/index.ts:296-350。 - 例子:一个工具插件只依赖
ctx.tools,不依赖dsh-agent-loop。 - 前端类比:
Context类似前端 DI 容器或 React Context;inject类似构造函数依赖注入。 - 类比局限:Cordis 的依赖解析和 fiber 生命周期更强,服务会随插件卸载和热更新一起撤销,不只是“共享数据”。
- 常见误区:把
ctx.effect()当成普通副作用函数。它必须能撤销,热更新和卸载才能干净。
2.4 Typed events 和四种派发模式
- 定义:通过 TypeScript declaration merging 声明事件名,运行时分
emit、waterfall、parallel、serial派发。 - 为什么需要:循环和工具管线需要让策略插件在不改核心代码的情况下介入。
- 项目体现:
agent/pre-step、agent/request、llm/stream、tools/*都是 waterfall,见packages/core/agent/src/runtime-types.ts:231-278和packages/core/tools/src/index.ts:152-197。 - 源码位置:
docs/cordis-primer.md#dispatch-modes、packages/core/agent/src/runtime-types.ts:146-291。 - 例子:
llm/stream的 listener 调用next()得到 adapter 原始流,也可以直接返回自己的 async iterable 短路。 - 前端类比:waterfall 很接近 Koa / Redux middleware / server-side onion model。
- 类比局限:Cordis waterfall 是服务级事件合同,返回值和派发模式都写进类型;不是任意全局事件总线。
- 常见误区:waterfall listener 忘记调用
next(),会短路后续监听器。仓库明确要求必须调用next()才能委托。
2.5 Capability Seam:定义、提供、消费
- 定义:一个可替换能力由 Service Definition、Service Provider、Consumer 三部分组成。
- 为什么需要:如果只有接口或只有实现,替换能力时仍会耦合到具体包。
- 项目体现:
dsh-llm同时定义LlmRuntime和 adapter 接口;dsh-llm-deepseek是 provider;dsh-agent-loop是 consumer。 - 源码位置:
docs/architecture.md#capability-seams、packages/llm/llm/src/index.ts:180-284。 - 例子:
ctx.llm.registerAdapter(['deepseek-official'], adapter)注册 provider;循环只调用ctx.llm.prepareCall(),不 import DeepSeek。 - 前端类比:类似“接口 + 实现 + 调用方”,也像浏览器里的
fetch被不同网络层实现。 - 类比局限:seam 还包含事件、生命周期和 disposer;不是单纯接口继承。
- 常见误区:只写一个 Service Definition 就认为能力完成。仓库规定完整 seam 至少要三个角色,拆分只在角色独立演进时进行。
2.6 Session Log 与 Event Sourcing
- 定义:会话是只追加、连续 seq 的
SessionEvent数组;消息历史由 log 投影得到。 - 为什么需要:持久化、回放、分叉、标题、遥测和 UI 都需要同一个真相来源。
- 项目体现:
Session.deriveMessages()只遍历surface.nodes,见packages/core/session/src/index.ts:726-747。 - 源码位置:
packages/core/session/src/index.ts:417-758、packages/core/session/src/surface.ts:83-114。 - 例子:
user/message、assistant/message、tool/result是 surface event;turn/start和assistant/chunk不直接进入历史。 - 前端类比:像 Redux 的 action log 或 CRDT/event log;当前 UI 是 log 的投影。
- 类比局限:这里不是简单状态 reducer,而是同时记录原始流、边界、来源和 surface 操作,模型上下文需要精确重建。
- 常见误区:以为日志里每个事件都会发给模型。实际上只有带
surfaceOp的 message-producing 事件进入deriveMessages()。
2.7 Model-visible 等价于 Logged
- 定义:任何送到模型请求的内容,都必须能从 Session log 重建。
- 为什么需要:防止 UI 和回放看到的内容与模型实际看到的内容不一致。
- 项目体现:
docs/architecture.md写明Model-visible means logged;packages/core/agent-loop/src/invariant.ts:39检查session.deriveMessages()。 - 源码位置:
docs/architecture.md#session-log、packages/core/agent-loop/src/invariant.ts:21-64。 - 例子:文件变化通知通过
agent.inject()成为user/message后,才可能进入模型上下文。 - 前端类比:类似“服务端渲染的数据必须能由 store/URL 恢复”,不能依赖组件局部状态拼请求。
- 类比局限:这里的约束是运行时 invariant 和持久化格式,不只是代码风格。
- 常见误区:把只用于 UI 的临时变量直接塞进
agent/request。这样做会破坏回放和审计。
2.8 Agent、Turn、Step、Inbox
- 定义:
Agent是公开生命周期接口;turn是 0 或多个 step 的执行区间;step是一次模型请求加它触发的工具执行。 - 为什么需要:模型会连续调用工具,必须有一个明确的执行边界和输入队列。
- 项目体现:
ReactLoopAgent的turn()和step()在packages/core/agent-loop/src/agent.ts:246-401。 - 源码位置:
packages/core/agent/src/runtime-types.ts:64-144、packages/core/agent-loop/src/agent.ts:210-401。 - 例子:
followup()追加到next-turn并唤醒;steer()追加到next-step并唤醒;inject()追加到next-step但不唤醒。 - 前端类比:
turn像用户一次提交对应的“任务批次”,step像任务中的一次模型调用。 - 类比局限:模型工具调用会连续产生 step,用户不会直接感知每个 step。
- 常见误区:认为一次用户输入一定等于一次模型请求。工具调用会继续当前 turn,可能产生多个 step。
2.9 SystemPrompt 与 Tool Schema 装配
- 定义:
SystemPrompt按顺序收集 section、context、variable 和 tool schema,最后渲染成 prompt。 - 为什么需要:不同插件可以贡献 persona、工具指引、AGENTS.md、时间上下文,而不需要知道彼此。
- 项目体现:
SystemPrompt.assemble()收集全局和 scoped layer,见packages/core/system-prompt/src/index.ts:467-530。 - 源码位置:
packages/core/system-prompt/src/index.ts:52-217。 - 例子:工具插件调用
ctx.systemPrompt.tools(...)贡献 schema;tool guidance 自动进入请求。 - 前端类比:类似多个 provider 拼页面内容,或 CSS layers 的合并。
- 类比局限:这里有严格 name/order/scope/complete 规则,变量错误会让装配失败。
- 常见误区:以为系统提示词是一个写死的字符串。它是由多个插件动态组装并参与请求日志的。
2.10 Tool Registry 与执行管线
- 定义:
ctx.tools保存 ToolDefinition,执行时经过策略事件,最终产生 lossless JSON 结果。 - 为什么需要:工具调用不能直接信任模型参数,必须有 schema、权限、超时、取消和审计。
- 项目体现:
ToolRuntime.execute()入口在packages/core/tools/src/index.ts:1342,内部调度器在 796-801。 - 源码位置:
packages/core/tools/src/index.ts:222-288、packages/core/tools/src/index.ts:787-1362。 - 例子:模型请求
bash工具后,先tools/pre-execute决定 allow/deny/ask,再tools/execute执行,最后tools/result观察冻结结果。 - 前端类比:像表单校验 + 请求拦截器 + 响应拦截器。
- 类比局限:工具输出要回到模型,因此结果是 lossless JSON、不可变且必须可回放。
- 常见误区:在
tools/result中尝试修改结果。它是 observe-only,修改应在post-execute或finalizeContent。
2.11 LLM Adapter 与 Prepared Call
- 定义:
LlmAdapter只负责把GenerateOptions转成StreamChunk;LlmRuntime负责路由、默认值、准备调用和llm/streamwaterfall。 - 为什么需要:模型厂商协议和重试策略应封装在 provider,loop 不应知道 HTTP 细节。
- 项目体现:
LlmRuntime.prepareCall()在packages/llm/llm/src/index.ts:779-814返回一次性PreparedLlmCall。 - 源码位置:
packages/llm/llm/src/index.ts:180-233、packages/llm/llm/src/index.ts:730-928。 - 例子:
agent-loop调用ctx.llm.prepareCall(config),拿到stream()后逐 chunk 写assistant/chunk。 - 前端类比:类似
fetch的适配层 + streaming response parser。 - 类比局限:prepared call 会固定 adapter registration,防止 HMR 混用不同 adapter 的能力结果。
- 常见误区:直接手写 provider HTTP 请求,而不是实现
LlmAdapter,会失去重试、取消、流式协议和路由能力。
2.12 Profile、Bundle、Patch Layer
- 定义:profile 是命名的插件树组合;bundle 是发布为 npm 包的一组 patch;patch 按 id 覆盖行配置。
- 为什么需要:同一个 base bundle 可以生成
web和headless,用户还能在不改 bundle 的情况下覆盖配置。 - 项目体现:
PROFILE_TEMPLATES定义web和headless的 bundle 顺序,见packages/boot/app-boot/src/profile.ts:114-117。 - 源码位置:
packages/boot/app-boot/src/profile.ts:35-125、apps/cli/src/profile-boot.ts:142-171。 - 例子:
dsh-base作为第一层;dsh-web-app或dsh-headless第二层;用户 profilecordis.patch.yml最后覆盖。 - 前端类比:类似 CSS cascade、环境配置覆盖、或 Docker layer。
- 类比局限:patch 替换整行
config,不是深度 merge。 - 常见误区:以为 patch 会 merge 字段。文档明确说明它替换目标行完整 config,覆盖时必须重述保留字段。
2.13 Web Client 的 React-free Object Layer、Slots、uSES
- 定义:浏览器里业务数据在 React 外对象层,React 只订阅不可变快照;UI 通过 slot 注册组合。
- 为什么需要:流式 token 高频更新时,避免整棵 React 树抖动;也避免业务状态绑死 React。
- 项目体现:
Session与Notifier在packages/api/session-controller/src/client/sessions/;ConversationNodeAssembler在packages/client/ui-conversation/src/client/conversation/assembler.ts;useSyncExternalStore在packages/client/ui-renderer/src/client/scoped-slots.tsx。 - 源码位置:
packages/api/session-controller/src/client/sessions/session.ts:81、packages/client/ui-conversation/src/client/conversation/assembler.ts:158、packages/client/ui-renderer/src/client/scoped-slots.tsx:287。 - 例子:
Session.getSnapshot()返回缓存快照;useSyncExternalStore(subscribe, getSnapshot)驱动组件。 - 前端类比:像 zustand/observable store,但业务状态机和流式累积完全不依赖 React。
- 类比局限:这里不只是一个全局 store,而是每个 Session 有事件窗口、重连状态和不可变投影。
- 常见误区:把所有共享 UI 状态塞进 React Context。项目明确区分业务数据、共享 store、owner props、component local state。
3. 技术栈与工程系统
3.1 技术栈表
| 技术或依赖 | 在项目中的用途 | 核心依赖 | 对应配置或源码位置 | 学习优先级 | 建议掌握程度 |
|---|---|---|---|---|---|
| TypeScript | 全部源码、typed events、declaration merging | 是 | tsconfig.base.json、package.json | 高 | 能读高级泛型和 declaration merging |
| Node.js | Host 运行时、子进程、FS、HTTP、PTY | 是 | package.json.engines | 中 | 理解事件循环、stream、AbortSignal |
| pnpm workspaces | Monorepo 包管理 | 是 | pnpm-workspace.yaml、根 package.json | 高 | 会用 filter/workspace |
| Cordis | 插件运行时、Context、fiber、events、effect | 是 | vendor/、docs/cordis-primer.md | 高 | 理解五思想和四种派发 |
| schemastery | 配置 schema 校验 | 是 | packages/core/agent-loop/src/index.ts:300-311 | 中 | 能读 Config |
| Typert | 类型图、生成 RPC 契约 | 是 | packages/typert/、docs/subsystems/typert.md | 进阶 | 知道它解决什么 |
| tsx | source launch ESM | 是 | 根 package.json 的 dsh script | 中 | 知道 source/build 差异 |
| tsdown / tsc | 构建 Host/Client lib | 是 | package.json build scripts | 中 | 知道 build 阶段 |
| Vite | Web shell / 前端构建 | 是 | apps/web、packages/client/web | 中 | 能调试前端 |
| Vitest | 测试运行器 | 是 | vitest.config.ts、根 scripts | 高 | 会跑单测 |
React + useSyncExternalStore | Web UI 渲染 | 是 | packages/client/ui-renderer、apps/web | 高 | 理解外部 store 订阅 |
| SQLite/JSONL | 会话持久化与检索 | 核心辅助 | packages/session/session-persistence-*、session-query-sqlite | 中 | 理解 append log 与查询 |
| OpenTelemetry | 会话遥测 | 辅助 | packages/session/session-telemetry-otel | 低 | 知道边界 |
3.2 包管理和依赖管理
根 package.json 是 pnpm@11.7.0,workspaces 覆盖 vendor/*、packages/*/*、native/landlock-run、apps/*、website。所有 npm 包使用 @deepseek-ai/dsh-*,vendored 包 rescope。扩展插件依赖 Service Definition,不依赖 concrete provider,见 packages/README.md#dependencies。
3.3 构建流程
根脚本显示:
build
build:lib
build:lib:host
build:lib:client
build:webHost 和 Client 是两个独立 TypeScript aggregate,不能压成一个 program,否则两端对 Context 的 declaration merging 会冲突。typert 只在 Host tsdown 阶段生成类型图。
依据:package.json、docs/development.md#typescript-project-layout。
3.4 开发模式
source launch 使用:
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.yml 的 config 支持 !!js 表达式。真实 API 读取 DEEPSEEK_API_KEY,可选 DEEPSEEK_BASE_URL。权限相关可读 DSH_PERMISSION_MODE;遥测可读 DSH_TELEMETRY_MODE、DSH_TELEMETRY_DISABLED。这些在 packages/bundle/base/cordis.patch.yml 有体现。
3.7 发布和部署方式
从仓库声明看,存在 release:dsh、release:pack、release:publish 和 publish: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 类型生成客户端契约。
判断依据:
docs/architecture.md明确 “There is no privileged core to patch”。packages/core/agent-loop/src/index.ts:349-350用ctx.effect()注册 factory。packages/core/session/src/index.ts:417-425将 Session 定义为 event-sourced log。.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md:15说明两端都跑 cordis。
4.2 模块划分
| 模块名称 | 主要职责 | 对外接口 | 上游依赖 | 下游依赖 | 核心 | 主要源码位置 |
|---|---|---|---|---|---|---|
core/session | append-only 会话日志与内存 store | ctx.sessions、Session、SessionEvent | dsh-llm、dsh-scope | 几乎所有模块 | 是 | packages/core/session/src/ |
core/system-prompt | 组装 prompt、context、tool schema、变量 | ctx.systemPrompt | dsh-scope、dsh-llm | agent-loop、tools | 是 | packages/core/system-prompt/src/index.ts |
core/tools | 工具注册、展示、执行管线 | ctx.tools | dsh-system-prompt、dsh-scope | agent-loop、所有 dsh-tool-* | 是 | packages/core/tools/src/index.ts |
core/agent | Agent 公共接口、registry、事件词汇 | ctx.agents | dsh-session、dsh-llm | UI、protocol、loop | 是 | packages/core/agent/src/index.ts |
core/agent-loop | 默认 Agent 驱动器 | ctx.agentLoop | agents、sessions、llm、tools、systemPrompt | 无(实现) | 是 | packages/core/agent-loop/src/ |
core/scope | per-agent scoped registration 原语 | library,无 ctx key | 无 | session、system-prompt、tools | 是 | packages/core/scope/src/ |
llm/llm | 模型消息、stream、adapter registry | ctx.llm | Cordis | agent-loop、llm-deepseek、llm-pi-ai | 是 | packages/llm/llm/src/index.ts |
session/session-persistence | 持久化 seam 和 coordinator | ctx.sessionPersistence | dsh-session | JSONL/SQLite 后端 | 是 | packages/session/session-persistence/src/ |
session/session-projection | 从事件流驱动纯投影 | ctx.sessionProjections | dsh-session | token meter、UI carriers | 重要 | packages/session/session-projection/src/index.ts |
boot/app-boot | profile 发现、patch 组合 | 函数 API | Cordis loader | CLI | 是 | packages/boot/app-boot/src/profile.ts |
apps/cli | dsh 启动器 | CLI | app-boot、bundles | 无 | 是 | apps/cli/src/bin.ts |
api/session-controller | Host 分页/跟随传输、Client Session 与快照 | page/follow、client services | Session Log、wire/RPC | UI 插件 | 是 | packages/api/session-controller/src/ |
client/ui-conversation + ui-renderer | 对话节点装配、React 外部 Store 桥与 keyed Slot | assembler、slot renderer | Client Session、ui-slots | React UI | 是 | packages/client/ui-conversation/src/、packages/client/ui-renderer/src/ |
client/ui-slots | slot registry 核心与 props 类型 | slot API | runtime | UI 插件 | 是 | packages/client/ui-slots/ |
typert/* | 类型图生成、加载、运行时 registry | Typert | TypeScript AST/type system | API gateway、SDK | 重要 | packages/typert/ |
sdk/* | JSON-RPC 协议、server、TS client | SDK | dsh-agent、Typert | 外部自动化 | 重要 | packages/sdk/ |
bundle/* | 安装式 profile patch layers | bundle patch | base + mode | 无 | 重要 | packages/bundle/base/cordis.patch.yml |
4.3 架构图
4.4 核心数据流
选择最典型的 dsh --profile headless "run the tests",因为它绕过 Web UI 和 ACP,最能直接暴露核心 loop。
- CLI 解析命令:
apps/cli/src/bin.ts:27-53。 - profile 组合:
apps/cli/src/profile-boot.ts:207-259。 dsh-headless的 runner 创建 Agent、提交任务、等待空闲:packages/bundle/headless/README.md:7-11。ReactLoopAgent.turn()打开 turn,preStep()组装请求,step()派生历史并调用模型。- 模型返回工具调用时,
executeToolCalls()调用ctx.tools。 - 每一步都写
turn/start、step/start、user/message、assistant/message、tool/result、step/end、turn/end等 Session event。 - headless runner flush 后从持久事件区间提取最后 assistant 文本并退出。
4.5 启动流程
关键源码:
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 关键调用链
入口:
agent.followup(userMessage)调用链:
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 五思想 | Service、inject、ctx.effect、四种 dispatch | 为什么“一切皆插件”能成立 |
docs/architecture.md | 理解核心包、turn flow、session log、extension points | Core packages、Turn flow、Where new behavior goes | 新功能应该挂在哪里 |
packages/core/README.md | 认识产品 API spine | ctx.sessions/llm/tools/agents/agentLoop | 六个核心服务各负责什么 |
packages/bundle/base/cordis.patch.yml | 看真实默认组合 | llm、session、tools、agent-loop、sandbox rows | 一个最小 Agent 需要哪些插件 |
apps/cli/src/bin.ts | 找到真正入口 | runProfile | CLI 如何进入 profile |
第二批:理解主执行链路
| 文件 | 阅读目的 | 关键符号或行号 | 读完应能回答 |
|---|---|---|---|
packages/core/session/src/types.ts | 理解事件词汇和 format | SessionEventMap、SurfaceEventType、SESSION_FORMAT_VERSION | 哪些事件会进入模型历史 |
packages/core/session/src/index.ts | 理解 append 和派生历史 | Session.append、deriveMessages、requestHeader | 模型上下文如何生成 |
packages/core/session/src/surface.ts | 理解 surface append/replace | deriveEventMessage、SurfaceManager | compaction 如何替换历史 |
packages/core/agent/src/runtime-types.ts | 理解 Agent 接口和事件 | Agent、PreStepDecision、agent/pre-step | UI 如何驱动 Agent |
packages/core/agent-loop/src/agent.ts | 理解 turn/step/stream/tool | ReactLoopAgent、turn()、step() | 一个请求怎样变成模型调用和工具执行 |
packages/core/agent-loop/src/index.ts | 理解创建/恢复/销毁 | AgentLoop、create/resume、setupAndPublish | Agent 生命周期如何管理 |
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 | 理解模型流式和 adapter | registerAdapter/prepareCall/stream | 如何接入新模型厂商 |
第三批:理解组合、持久化和前端
| 文件 | 阅读目的 | 关键符号 | 读完应能回答 |
|---|---|---|---|
packages/boot/app-boot/src/profile.ts | 理解 profile/bundle/patch | loadProfile、composeEntries | 配置树如何生成 |
apps/cli/src/profile-boot.ts | 理解 runProfile 和 HMR | runProfile、composeLive | 启动时如何 mount/热更新 |
packages/session/session-persistence/src/index.ts | 理解持久化 seam | SessionPersistence | 如何换 JSONL/SQLite |
packages/session/session-persistence/src/coordinator.ts | 理解 write coordination | append、flush、resume | 会话如何可靠落盘 |
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md | 理解浏览器插件树和对象层 | React-free object layer、slot system | Web UI 为什么这样设计 |
packages/api/session-controller/src/client/sessions/session.ts | 理解前端 Session 快照 | getSnapshot、subscribe | React 如何订阅流式状态 |
packages/client/ui-renderer/src/client/scoped-slots.tsx | 理解 slot 渲染和 uSES | useSyncExternalStore | slot 如何变成 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()后seq和surface。packages/core/system-prompt/src/index.ts:观察组装出的 sections/contexts/tools/variables。packages/llm/llm/src/index.ts:观察 adapter selection 和llm/streamwaterfall。
二次开发时最可能修改的文件
- 增加工具:新建
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.md、docs/tool-catalog.md等,适合查询不适合通读。
6. 完整学习路线
以下按“前端背景、能投入持续学习”的默认情况设计。若每天投入 1.5 到 2 小时,建议总周期 4 到 6 周;只做快速理解可压缩到 1 到 2 周。
阶段 0:建立项目全景
- 目标:能解释项目解决什么问题、入口和核心价值。
- 推荐时间:1 到 2 天。
- 学习章节:第 1、2、3 章。
- 阅读:
README.md、docs/architecture.md、docs/cordis-primer.md、packages/core/README.md。 - 实践:运行
git status、列出目录、画一张自己的模块草图。 - 验收:不看文档,能说出“插件化、session log、turn/step、profile/bundle”。
阶段 1:搭建并运行项目
- 目标:完成依赖安装和最小运行,记录真实成功或失败。
- 推荐时间:1 到 2 天。
- 学习章节:第 9 章。
- 阅读:
docs/development.md#setup-tutorial、根package.json。 - 实践:
pnpm install、pnpm run typecheck;如环境允许,再尝试pnpm run test或pnpm dsh --profile headless。 - 验收:能区分环境问题、依赖问题和配置问题。
阶段 2:掌握 Cordis 插件模型
- 目标:能写一个最小插件,理解 inject/effect/events。
- 推荐时间:2 到 3 天。
- 学习章节:第 2 章中的 Cordis 部分、第 3 章。
- 阅读:
docs/cordis-tutorial/01-first-plugin.md到07-into-the-harness.md。 - 实践:在
tmp/cordis-tutorial建插件,注册 service 和 event。 - 验收:能解释为什么 plugin 卸载后注册会消失。
阶段 3:理解 Session Log 和派生历史
- 目标:掌握事件、surface、deriveMessages。
- 推荐时间:2 天。
- 学习章节:第 2 章 session 部分、第 7 章相关章。
- 阅读:
packages/core/session/src/types.ts、index.ts、surface.ts。 - 实践:写一个脚本
Session.create后append几条事件,观察deriveMessages()。 - 验收:能判断某个事件是否进入模型历史。
阶段 4:追踪 Agent 主调用链
- 目标:沿
followup -> turn -> step -> llm -> tools读源码。 - 推荐时间:3 到 5 天。
- 学习章节:第 4、8 章。
- 阅读:
packages/core/agent-loop/src/agent.ts、index.ts、tool-calls.ts。 - 实践:在
turn()和step()加断点,观察 phase、inbox、session.events。 - 验收:能用 Archify 图解释时序并标出关键行号。
阶段 5:深入 LLM 和 Tools
- 目标:理解 provider 替换、流式协议、工具策略。
- 推荐时间:3 天。
- 学习章节:第 7 章相关章。
- 阅读:
packages/llm/llm/src/index.ts、packages/core/tools/src/index.ts、docs/cookbook/adding-an-llm-adapter.md、adding-a-tool.md。 - 实践:阅读现有 adapter;为已有工具补一个
tools/pre-execute观察 listener。 - 验收:能说出
prepareCall()为什么返回一次性 handle。
阶段 6:学习测试和调试
- 目标:会跑单测、snapshot,理解测试分层。
- 推荐时间:2 天。
- 学习章节:第 9 章。
- 阅读:
docs/testing.md、packages/core/agent-loop/tests/。 - 实践:跑一个 package 单测;定位一次失败。
- 验收:能说明 unit/coverage/e2e/snapshot 分别验证什么。
阶段 7:修改一个已有功能
- 目标:做低风险改动并验证。
- 推荐时间:2 到 3 天。
- 学习章节:第 10 章实践二。
- 实践:修改
cordis.patch.yml中的system-prompt.persona或toolOrder,或在一个测试 fixture 中观察输出变化。 - 验收:能说明改动影响、恢复方式和测试命令。
阶段 8:排查一个模拟故障
- 目标:掌握调用链、日志和断点。
- 推荐时间:2 到 3 天。
- 学习章节:第 10 章实践三。
- 实践:禁用某个工具行、填一个错误模型路由、或写一个
pre-stepreject listener,观察 fail-loud。 - 验收:能定位故障发生在哪个 layer、哪个服务、哪个事件。
阶段 9:完成一个小型扩展
- 目标:使用正式扩展机制增加一个能力。
- 推荐时间:3 到 5 天。
- 学习章节:第 10 章实践四、第 11 章。
- 实践:新增一个 tool plugin 或 prompt section,补 README 和 tests。
- 验收:能通过配置加载,能被模型 schema 看到,并有测试覆盖。
阶段 10:自己设计一个最小 Agent Harness
- 目标:能把核心抽象迁移到自己的项目。
- 推荐时间:1 周左右。
- 学习章节:第 11 章末尾的“自己动手设计”。
- 实践:用 TypeScript 实现最小
Context、ToolRegistry、SessionLog、AgentLoop。 - 验收:最小系统能处理“文本 -> 模型 -> 工具 -> 模型”的一轮循环,并记录可回放日志。
第 1 章:启动入口与配置档案(Profile)
1. 本章定位
先把 dsh 看成“插件树启动器”,而不是一个写死行为的 CLI。新版同时提供 web、headless、sdk、sdk-minimal、acp 五类配置档案(Profile)。
2. 学习目标
- 解释 Profile、组合包(Bundle)与补丁(Patch)的叠加关系。
- 从
apps/cli/src/bin.ts追到runProfile()。 - 区分启动时重载与运行时插件重载。
- 用配置 dump 定位“参数由哪一层消费”。
3. 前置知识
把 Profile 类比为前端应用入口,Bundle 类似可复用 preset,Patch 类似环境覆盖。局限是 Patch 操作插件树条目,顺序和条目 ID 都是行为,不是普通对象深合并。
4. 对应源码
apps/cli/src/bin.ts:28:动态调用runProfile()。apps/cli/src/profile-boot.ts:209:Profile 启动入口。packages/boot/app-boot/src/profile.ts:139:五个内置 Profile。packages/boot/app-boot/src/profile.ts:166:DEFAULT_PROFILE_BUNDLES。
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. 自测题
- 五个内置 Profile 分别面向什么表面?
- Bundle 顺序为什么会改变行为?
--patch适合哪类实验?- 配置 dump 失败说明调用链停在哪里?
- 何时应该新增 Bundle,而不是修改 boot 核心?
第 2 章:Cordis 插件、依赖注入与生命周期
1. 本章定位
Cordis 是 Harness 的组合语法:服务、事件、能力贡献和 UI 插槽都随作用域创建与撤销。
2. 学习目标
- 区分函数插件、服务定义(Service Definition)、提供者与消费者。
- 解释依赖注入(Dependency Injection)和作用域(Scope)。
- 用副作用/释放器(Effect/Disposer)证明插件可卸载。
- 区分
emit、serial、parallel、waterfall。
3. 前置知识
可把 Context 类比为带生命周期的前端依赖容器,把 ctx.effect() 类比为 useEffect 的 cleanup。局限是 Cordis 还管理依赖等待、子作用域、typed event 和插件 fiber,不等于 React Context。
4. 对应源码
docs/cordis-primer.mddocs/cordis-tutorial/01-first-plugin.md至07-into-the-harness.mdpackages/core/agent-loop/src/index.ts:353:static inject。packages/core/agent-loop/src/index.ts:412:可撤销 factory 注册。
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. 自测题
- Service Definition、provider、consumer 各负责什么?
- Scope 为什么比全局容器安全?
- Effect 的 Disposer 何时运行?
serial与parallel如何选择?- 如何证明插件卸载没有资源泄漏?
第 3 章:会话日志(Session Log)、Surface 与投影
1. 本章定位
Agent 的记忆不是一个可随意修改的消息数组,而是只追加事件事实与若干可重建视图。
2. 学习目标
- 区分原始日志、Surface、模型历史、持久化和前端投影。
- 解释
deriveMessages()与surface.replace。 - 理解格式版本、checkpoint 与重放边界。
- 从事件序列手工推出模型可见消息。
3. 前置知识
可类比 Redux action log 与 selector:日志记录发生过什么,投影计算当前视图。局限是 Session 事件有序号、冻结 JSON、格式版本和持久化合同,不能重放任意副作用。
4. 对应源码
packages/core/session/src/types.ts:87:SESSION_FORMAT_VERSION。packages/core/session/src/types.ts:425:Session。packages/core/session/src/index.ts:790:deriveMessages()。packages/core/session/src/surface.ts:81:Surface 规则。packages/session/session-persistence/packages/session/session-persistence-jsonl/packages/session/session-log-deepseek/
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();观察 seq、type、surfaceOp、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. 自测题
- 为什么 chunk 不直接进入模型历史?
deriveMessages()的事实输入是什么?- checkpoint 能否替代日志?
- 新事件类型至少影响哪些消费者?
- 如何证明一个投影可重建?
第 4 章:智能体(Agent)、收件箱(Inbox)与轮次/步骤
1. 本章定位
这一章沿新版 ReactLoopAgent 追踪一次输入如何变成多个模型与工具步骤。
2. 学习目标
- 区分
followup()、steer()、inject()。 - 解释轮次(Turn)与步骤(Step)。
- 追踪
turn()、step()、buildRequest()、prepareCall()。 - 解释取消如何穿过模型流与工具执行。
3. 前置知识
可把 Agent Loop 类比为 reducer 加 effect 的状态机,把 Inbox 类比为带边界的事件队列。局限是模型和工具是长异步任务,取消是协作式的,Turn/Step 边界还必须写入 durable log。
4. 对应源码
packages/core/agent-loop/src/agent.ts:70:ReactLoopAgent。packages/core/agent-loop/src/agent.ts:122:send/followup/steer/inject。packages/core/agent-loop/src/agent.ts:255:turn()。packages/core/agent-loop/src/agent.ts:341:step()。packages/core/agent-loop/src/agent.ts:453:buildRequest()。packages/core/agent-loop/src/agent.ts:489:prepareCall()。
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. 自测题
followup、steer、inject的唤醒语义有何不同?- 为什么工具结果会触发下一 Step?
prepareCall()在哪一层发生?- 如何从事件判断卡在 LLM 还是 Tool?
- 设计一个“取消恰好发生在工具完成时”的测试。
第 5 章:系统提示词(System Prompt)装配
1. 本章定位
系统提示词不是一个常量,而是插件按作用域贡献并在每次调用前装配的结构。
2. 学习目标
- 区分
section、context、tools、variable。 - 解释注册顺序、作用域和 complete section。
- 从
assemble()追到renderPrompt()。 - 识别提示词顺序对模型缓存和行为的影响。
3. 前置知识
可把贡献项类比为命名 Slot,把本次 Scope 类比为组件 render 的 props。局限是输出顺序会影响模型约束与 KV Cache,不能把 section 当任意 UI 子树重排。
4. 对应源码
packages/core/system-prompt/src/index.ts:263:renderPrompt()。packages/core/system-prompt/src/index.ts:302:context 渲染。packages/core/system-prompt/src/index.ts:389:SystemPrompt。packages/core/system-prompt/src/index.ts:432:section()。packages/core/system-prompt/src/index.ts:467:context()。packages/core/system-prompt/src/index.ts:499:tools()。packages/core/system-prompt/src/index.ts:515:variable()。packages/core/system-prompt/src/index.ts:536:assemble()。
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. 自测题
- 四类贡献分别解决什么问题?
- complete section 有什么边界?
- 未注册变量如何失败?
- 顺序为何影响 KV Cache?
- scoped shadow 比修改全局贡献安全在哪里?
第 6 章:大模型能力接缝(LLM Seam)与流式调用
1. 本章定位
理解模型供应商协议如何被适配为 Agent Loop 可消费的统一异步流。
2. 学习目标
- 区分
LlmRuntime、LlmAdapter、PreparedLlmCall。 - 解释
llm/streamwaterfall。 - 追踪 chunk、重试、取消与最终错误。
- 找到 DeepSeek API 扩展字段的边界。
3. 前置知识
可把 Adapter 类比请求库的 transport adapter,把规范化 chunk 类比领域事件。局限是模型流只能消费一次,包含 reasoning、tool call、usage 和终态,且取消会跨越多个异步层。
4. 对应源码
packages/llm/llm/src/index.ts:158:PreparedLlmCall。packages/llm/llm/src/index.ts:193:LlmAdapter。packages/llm/llm/src/index.ts:326:LlmRuntime。packages/llm/llm/src/index.ts:380:registerAdapter()。packages/llm/llm/src/index.ts:890:prepareCall()。packages/llm/llm/src/index.ts:1051:stream()。packages/llm/deepseek-llm-api-extensions/src/types.ts:31:DeepSeek 扩展字段。
5. 工作原理
prepareCall() 解析 provider/model 并冻结本次 adapter registration;stream() 经过 waterfall,让插件包装、观测、替换或短路调用;Adapter 只处理鉴权、供应商请求和 wire chunk 规范化。是否进行 Agent 级重试由上层运行状态决定,不应塞进供应商解析器。
6. 执行流程
无图回退:Agent → prepareCall() → PreparedLlmCall → llm/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. 自测题
- 为什么要有 Prepared Call?
- waterfall 可以做什么?
- 哪些重试属于 transport,哪些属于 Agent?
- 私有字段为何不能泄漏到 Agent?
- 新 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. 对应源码
packages/core/tools/src/index.ts:210:presentationMeta。packages/core/tools/src/index.ts:261:并发声明。packages/core/tools/src/index.ts:780:ToolRuntime。packages/core/tools/src/index.ts:1028:register()。packages/core/tools/src/index.ts:1333:执行入口。packages/core/tools/src/index.ts:1450、:1560、:1600:prepare/dispatch/finalize。packages/core/tools/src/index.ts:1797:结果展示元数据。
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_codetransport 的改名。 - 只在 Prompt 隐藏危险工具,不做执行时策略。
- 认为用户批准后可跳过路径与 Schema 校验。
11. 自测题
- pre-execute、Guard、execute、post-execute 各管什么?
- 为什么 Guard 必须单调拒绝?
- PTC 如何保持相同日志语义?
output.render与presentationMeta有何区别?- 工具产生副作用后输出校验失败,应怎样设计幂等性?
第 8 章:持久化、投影与 Web 客户端
1. 本章定位
把 Host 的 durable Session 事件追到新版 Client Session、对话节点装配和 React keyed Slot。
2. 学习目标
- 区分 persistence、projection、checkpoint 与 Client snapshot。
- 解释
page()/follow()的分页和追帧模型。 - 追踪
Session、Notifier、ConversationNodeAssembler。 - 解释
useSyncExternalStore与 keyed Slot。
3. 前置知识
可把 Client Session 类比框架无关的外部 Store,把 projection 类比纯 selector,把 keyed Slot 类比插件化渲染插槽。局限是 Host Log 才是 durable truth,浏览器快照可随时丢弃重建。
4. 对应源码
packages/api/session-controller/src/index.ts:369:page()。packages/api/session-controller/src/index.ts:380:follow()。packages/api/session-controller/src/client/sessions/session.ts:81:ClientSession。packages/api/session-controller/src/client/sessions/session.ts:459:subscribe()。packages/api/session-controller/src/client/sessions/session.ts:467:getSnapshot()。packages/api/session-controller/src/client/sessions/notifier.ts:8:Notifier。packages/client/ui-conversation/src/client/conversation/assembler.ts:158:ConversationNodeAssembler。packages/client/ui-renderer/src/client/scoped-slots.tsx:287:useSyncExternalStore。
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. 自测题
page()与follow()如何配合?- Notifier 为什么要合批?
- snapshot 引用稳定性为何重要?
- Assembler 与 React Renderer 各负责什么?
- keyed Slot 如何让 Host/Browser 解耦?
8. 核心模块深度分析
这里重点拆 dsh-agent-loop 与 dsh-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 关键数据结构
Phase:idle | maintenance | running,见packages/core/agent-loop/src/agent.ts:38-46。PreparedStep:reject | 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。PromptAssembly:sections/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)statussessionctx
依据:packages/core/agent/src/runtime-types.ts:64-144。
8.5 内部协作关系
AgentLoop 依赖 agents、sessions、llm、tools、systemPrompt。ReactLoopAgent 不直接依赖具体 LLM adapter 或具体工具 provider;它依赖 ctx.llm 和 ctx.tools,所以两者可替换。
8.6 算法或处理流程
turn() 使用一个循环:
- 打开 turn。
preStep()认领输入并组装 prompt。- 若 reject 或空输入,关闭 turn。
- 写入 step/start 和 user/message。
step()循环请求模型。- 流式 chunk 写入日志并组装 assistant message。
- 有 tool-call 则执行工具,继续 step。
- 无 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/end 的 error 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-75、packages/core/session/src/index.ts:701-747。
8.10 并发或异步行为
- 工具调度支持并发,但由
isConcurrencySafe()决定;否则 exclusive。 tools/executewrapper 可以替换 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 上执行:
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 或发布流程已经通过。
仓库声明但未执行
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推荐的最小复验
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 安装步骤
建议流程:
pnpm install
pnpm run typecheck如果只是读代码,不安装依赖也可以完成大部分静态分析。真实运行、构建和测试需要依赖。
9.4 最小启动步骤
Web:
pnpm run build
pnpm dsh webHeadless:
pnpm run build
pnpm dsh --profile headless "summarize this workspace"这些启动命令来自根 README.md 和 docs/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.json 的 test 是 vitest run。
9.7 如何运行单个测试
Vitest 通常支持:
pnpm run test -- <test-file-pattern>当前仓库 Vitest 已验证可直接接受 package 路径。运行单文件时应检查输出中的 Test Files/Tests 数量,确保过滤条件确实选中了目标。
9.8 如何开启调试
Node/tsx 可配合 IDE 断点。核心断点:
packages/core/agent-loop/src/agent.ts:255的turn()。packages/core/agent-loop/src/agent.ts:341的step()。packages/core/agent-loop/src/agent.ts:453的buildRequest()。packages/core/tools/src/index.ts:1333的execute()。
9.9 日志位置
源码里常见 ctx.logger.warn,不是统一文件日志。Session 持久化默认 root 来自 dshHomePath('sessions'),见 packages/bundle/base/cordis.patch.yml。浏览器和 telemetry 有独立机制。
9.10 环境变量
常用:
DEEPSEEK_API_KEY
DEEPSEEK_BASE_URL
DSH_PERMISSION_MODE
DSH_TELEMETRY_MODE
DSH_TELEMETRY_DISABLED
DSH_TELEMETRY_OTLP_URL
DSH_TOOLS_MODE9.11 常见启动失败原因
- 没有安装依赖。
- 没有先 build,导致缺 Typert artifacts 或 client bundle。
- Node/pnpm 版本不匹配。
- bundle/patch 行引用了不存在的插件或 id。
- 缺少
DEEPSEEK_API_KEY时,真实模型调用失败。 - 端口被占用。
- Windows 与 POSIX shell 平台差异导致 bash/pwsh 行选择不同。
9.12 最小故障排查流程
- 看 CLI 第一阶段报错是启动器还是应用参数。
--dump-config看组合树是否正确。- 检查 profile
package.json和cordis.patch.yml。 - 用
--patchoverlay 做最小复现。 - 在
runProfile、boot()、Agent 创建处断点。 - 如果与模型相关,确认 adapter 注册、provider/model、key、base URL。
- 如果与工具相关,确认
ctx.tools注册、schema、权限和 guard。
9.13 一张可复用的故障分层表
| 最后成功点 | 下一检查点 | 不应优先检查 |
|---|---|---|
| CLI help | 参数所有权与 Profile 路径 | 模型输出质量 |
| dump config | Cordis entry 激活与 inject | 浏览器 CSS |
| Agent 创建 | inbox、pre-step 与 Session append | 发布脚本 |
| request/header | Adapter、Key、base URL 与 stream | Tool body |
| tool/call | schema、restriction、guard 与 scheduler | Profile 查找 |
| Session event | persistence/downlink/projection | LLM 路由 |
每次排查保存:复现命令、提交、环境版本、最小输入、最后成功点、首个错误和恢复步骤。只有这些 信息齐全,故障记录才可被另一个开发者重放。
10. 渐进式实战项目
实践一:成功运行并观察核心流程
- 任务背景:熟悉开发环境,看到真实启动和日志。
- 学习目标:能安装、构建、运行并判断失败阶段。
- 涉及模块:根工程、CLI、bundle。
- 推荐修改点:先不修改代码,运行命令。
- 实现步骤:
- 记录
node --version、pnpm --version、git rev-parse HEAD。 pnpm install后先运行pnpm dsh --help。- 运行三个核心包测试:
pnpm exec vitest run packages/core/session packages/core/agent-loop packages/core/tools。 - 比较 web/headless 的 default config dump;不需要 API Key。
- 只有前述步骤通过后,才选择 mock LLM 或真实 Key 启动。
- 记录每个命令的退出码、成功/失败和首个错误文本。
- 记录
- 测试方法:以实际命令输出为准,不推测。
- 验收标准:提交一份环境记录和启动分层图;能说出失败属于依赖、配置、插件激活、模型还是缺 Key。
- 恢复方式:实践一不修改 tracked 文件;关闭服务并删除临时日志即可。
- 可能踩坑:Windows 上 bash/pwsh 平台差异;未 build 缺 artifacts;网络受限。
- 进一步挑战:使用
--dump-config观察 web/headless 组合差异。
实践二:修改一个低风险已有功能
- 任务背景:理解配置与测试流程。
- 学习目标:做一个小改动并验证。
- 涉及模块:
system-prompt、bundle patch。 - 推荐修改点:官方
scratch-plugin中新增一个study:notePrompt section,使用临时--patch挂载。 - 实现步骤:
- 按
docs/user/develop/basic/创建scratch-plugin。 - 注册固定名称与 order 的 Prompt section,并写 assemble/render 单测。
- 用
extra.yml挂载插件,比较--dump-default-config与--dump-config。 - 增加 scoped 同名 section,验证 shadow 只影响目标 Agent。
- 卸载插件,确认基础快照恢复。
- 按
- 测试方法:断言 section 文本、顺序、scope 和 disposer;dump 只用于证明真实入口。
- 验收标准:单测通过,能解释 Patch 整行替换与 Prompt scoped shadow 是两种不同机制。
- 恢复方式:移除
--patch并删除 scratch-plugin;不修改默认 Bundle。 - 可能踩坑:保留字段漏写;id 不匹配。
- 进一步挑战:把 persona 改为模板变量,观察变量解析错误。
实践三:定位并修复一个模拟问题
- 任务背景:掌握调用链、日志和断点。
- 学习目标:定位故障发生在哪个模块。
- 涉及模块:Agent loop、tools、LLM、Session log。
- 推荐修改点:使用 mock LLM 确定性地产生一个不存在的工具名,再增加一个永远 deny 的 guard。
- 实现步骤:
- 让 mock LLM 发出
missing_tool,在 lookup/execute 入口观察UNKNOWN_TOOL。 - 改为真实已注册的无副作用工具,确认成功基线。
- 注册 guard deny,确认 body 没有进入且结果被规范化。
- 移除 guard,故意返回错误 output 类型,定位失败发生在 body 之后、提交结果之前。
- 对三次运行分别保存 Session event 序列。
- 让 mock LLM 发出
- 测试方法:使用断言验证 body 调用次数、错误 kind 和是否存在最终
tool/result。 - 验收标准:能独立区分 unknown、policy deny、body/output failure,且修复后原测试通过。
- 恢复方式:移除临时 guard 和 mock response,重新运行目标 package 测试。
- 可能踩坑:工具可能没被模型选中,需选确定性任务或 mock LLM。
- 进一步挑战:模拟
agent/pre-stepreject,观察空 turn 的turn/end。
实践四:实现一个小型扩展
- 任务背景:使用正式扩展机制增加能力,证明具备初步二次开发能力。
- 学习目标:新增一个工具或 prompt section,并补测试。
- 涉及模块:
ctx.tools、ctx.systemPrompt、cordis.yml。 - 推荐修改点:在官方
scratch-plugin中实现workspace_summary,避免第一次实践就修改核心包。 - 实现步骤:
- 参数定义为
path(相对工作区)和maxFiles;禁止..、绝对路径和额外字段。 - canonical output 包含
path、fileCount、按扩展名统计和截断标记;为模型 render 简短文本。 - 用
defineTool声明输入/输出 schema,在apply(ctx)中注册。 - 使用工作区/文件系统公开能力读取目录,不直接依赖某个私有 provider 类。
- 用临时
cordis.yml/Patch 挂载,dump 配置并检查工具 schema。 - 单测覆盖空目录、混合扩展名、maxFiles 截断、越界路径、取消和 dispose。
- 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. 二次开发指南:理解“一切皆插件”
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/ 中建立:
scratch-plugin/
├─ package.json
├─ cordis.yml
└─ src/
└─ index.ts接口固定为:
export type WorkspaceSummaryInput = {
path: string
maxFiles?: number
}
export type WorkspaceSummaryOutput = {
path: string
fileCount: number
byExtension: Record<string, number>
truncated: boolean
}src/index.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 后再次确认仍在工作区、稳定排序后截断,并在循环中响应 AbortSignal。output.render 面向模型;presentationMeta 持久化前端重放所需事实。当前 Value Schema 要求 additionalProperties 为布尔值,因此动态扩展名 Map 由实现构造为数字值,并由单测锁定该约束。
最小 Patch(实际 Loader 语法以 scratch-plugin 教程生成的插件入口为准):
- id: workspace-summary
plugin: ./src/index.ts
config:
workspaceRoot: /path/to/deepseek-harness【仓库声明但未执行】启动:
pnpm dsh web --patch ./scratch-plugin/cordis.yml撤销:停止进程并从临时 Patch 移除该 entry;不要修改默认 Bundle。
11.3 第二步:整理成 Host + Browser 双面包
packages/learning/workspace-summary/
├─ package.json
└─ src/
├─ index.ts # 上面的 Host 工具
└─ client/
├─ index.tsx # Browser apply
└─ locales.tspackage.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:
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 实现):
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 测试:把安全和回放当一等公民
至少覆盖:
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 */ })【仓库声明但未执行】包内测试与类型检查:
pnpm exec vitest run packages/learning/workspace-summary/tests
pnpm --filter @example/dsh-workspace-summary bundle11.5 两侧不可跨越的边界
- Browser 插件不得导入 Host 的目录遍历或工具定义。
- Browser 不自行配对
tool/call与tool/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-loop与session/system-prompt/tools/llm高度耦合,但耦合点在服务接口,而不是具体实现。surface和deriveMessages被持久化、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.sessions、ctx.agents、ctx.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.md | Service/inject/effect/dispatch | 理解插件框架 | 低 | 能解释 waterfall |
docs/architecture.md | turn flow/seams/extension map | 建立架构图 | 中 | 能画核心流 |
packages/core/README.md | ctx 键 | 认识 spine | 低 | 能背六项核心服务 |
packages/bundle/base/cordis.patch.yml | rows | 认识默认组合 | 中 | 能找到 tools/agent-loop/llm |
apps/cli/src/bin.ts | parseDshArgs/runProfile | 找到入口 | 低 | 能追踪到 boot |
apps/cli/src/profile-boot.ts | composeProfile/runProfile | 理解启动 | 中 | 能说明 patch 顺序 |
packages/core/session/src/types.ts | SessionEventMap | 掌握事件词汇 | 中高 | 能区分 surface/log-only |
packages/core/session/src/index.ts | Session.append/deriveMessages | 理解派生历史 | 高 | 能解释增量投影 |
packages/core/session/src/surface.ts | SurfaceManager | 理解 replace | 高 | 能解释 compaction 投影 |
packages/core/agent/src/runtime-types.ts | Agent/PreStepDecision | 掌握公开接口 | 中高 | 能解释 followup/steer/inject |
packages/core/agent-loop/src/agent.ts | ReactLoopAgent/turn/step | 追踪主循环 | 高 | 能画出时序图 |
packages/core/agent-loop/src/index.ts | AgentLoop/create/resume | 理解生命周期 | 高 | 能解释 teardown |
packages/core/agent-loop/src/tool-calls.ts | executeToolCalls | 理解工具调度 | 高 | 能解释并发决策 |
packages/core/system-prompt/src/index.ts | assemble/section/tools | 理解 prompt 装配 | 中 | 能注册一个 section |
packages/core/tools/src/index.ts | ToolRuntime/register/execute | 理解工具管线 | 高 | 能写一个 tool |
packages/llm/llm/src/index.ts | LlmRuntime/LlmAdapter | 理解模型 seam | 高 | 能接入 adapter |
packages/boot/app-boot/src/profile.ts | loadProfile/composeEntries | 理解配置组合 | 中 | 能写 patch |
docs/cookbook/extension-cookbook.md | feature -> mechanism | 掌握扩展地图 | 中 | 能选扩展点 |
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md | object layer/slots | 理解 Web client | 中高 | 能解释三层 |
14. 掌握程度检查
初级:能够使用
- 能说出项目目标、入口和 profile/bundle。
- 能按仓库命令安装依赖并启动一种 profile。
- 能定位
packages/core六项核心服务。 - 能查看
--dump-config或相关配置。
可观察验收:
- 画出
dsh-base与web/headless的关系。 - 在源码中找到
ctx.tools、ctx.llm、ctx.sessions的定义。 - 解释
pnpm dsh --profile web为何会启动 UI。
中级:能够解释
- 能解释 Session log 与
deriveMessages()。 - 能解释 turn/step 和工具循环。
- 能解释 capability seam 三件套。
- 能解释 Cordis effect 和 waterfall。
可观察验收:
- 独立画出
followup -> preStep -> llm -> tools -> session时序图。 - 说明
agent/pre-stepreject 时日志如何记录。 - 说明为什么替换 persistence backend 不应修改 loop。
高级:能够修改和扩展
- 能定位故障并修改功能。
- 能增加一个 tool、prompt section 或 LLM adapter。
- 能补测试和必要 snapshot。
- 能设计一个最小 Agent Harness。
可观察验收:
- 新增一个工具,模型 schema 能看到它。
- 修改一个
tools/pre-execute策略,并证明拒绝生效。 - 用 100 到 300 行 TypeScript 实现简化版
Context + SessionLog + ToolRegistry + AgentLoop。
15. 自测题参考答案
以下按关键要点给分,每题 2 分。
第 1 章
web提供浏览器表面,headless运行单任务,sdk提供完整库组合,sdk-minimal提供最小 SDK 组合,acp提供 Agent Client Protocol 表面。- Bundle/Patch 后写层覆盖前层;条目顺序还决定插件装配与依赖可见性。
--patch适合不改默认 Profile 的临时验证、调试和功能试装。- 停在 CLI/Profile/Patch/Loader 准备阶段,尚未进入 Agent 和 LLM。
- 能用现有稳定能力组合出新表面时新增 Bundle;只有公共启动不变量变化才改 boot 核心。
第 2 章
- Definition 定义
ctx.<key>合同,provider 提供实现,consumer 通过inject使用。 - Scope 记录所有权和可见范围,能让同名能力隔离并随插件撤销。
- Scope dispose 时反向执行,撤销服务、监听器和其他注册。
- 有顺序依赖用
serial,互不依赖且都需完成用parallel。 - 连续挂载/卸载两次,断言服务键、监听器数量和输出都恢复到初始状态。
第 3 章
- chunk 是流式/replay 事实,模型历史使用组装完成的 assistant message,避免半成品进入下次请求。
- 当前 Session Log 经 Surface 选择后的可见节点。
- 不能;checkpoint 是恢复加速点,日志语义仍是事实源。
- envelope、Surface、持久化 codec、projection、Client assembler、UI 和兼容测试。
- 从空状态按序重放同一事件应得到相同结果,丢弃投影后可再次生成。
第 4 章
followup唤醒下一 Turn;steer唤醒并进入下一 Step;inject进入下一 Step 但不主动唤醒。- tool/result 成为新模型上下文,Loop 需要下一 Step 让模型继续推理。
step()构建请求后、真正消费 LLM 流之前。- request/start 后无 chunk 多指向 LLM;tool/call 后无 result 多指向策略、批准、调度或 body。
- 用可控 barrier 让 abort 与工具 resolve 同时发生,断言只提交一个权威终态且无重复 result。
第 5 章
- section 贡献文本片段,context 提供上下文,tools 贡献可见能力,variable 延迟提供模板值。
- complete 表示该贡献形成权威完整提示词边界,不能再按普通片段随意拼接。
renderPrompt()严格失败,避免把未解析占位符发给模型。- 动态前缀会让后续 token 变化,降低 provider KV Cache 的前缀复用。
- shadow 只影响目标 Scope,dispose 后自动恢复,不污染其他 Agent。
第 6 章
- 冻结 adapter registration 和解析配置,避免热更新导致准备与 dispatch 使用不同实现,也保证一次性消费。
- 可观测、包装、缓存、替换、短路或施加跨 Adapter 策略。
- 握手、限流等供应商瞬时错误可属 transport;需要预算、历史和 Turn 状态的重试属 Agent。
- 否则核心会依赖厂商协议,无法保持统一 chunk 与可替换 Adapter。
- assembler、Session event、持久化/replay、Conversation assembler、UI 与 snapshot tests。
第 7 章
- pre 决定 allow/deny/ask;Guard 做最终单调拒绝;execute 管 body 生命周期;post 处理结果收尾。
- 任何安全插件拒绝后都不能被后层重新放行,否则组合顺序会削弱策略。
- PTC 逻辑调用重新进入正常 ToolRuntime,因此仍产生相同 tool/call、策略、result 和错误语义。
- render 是模型可见文本;presentationMeta 是持久、可回放的 Client 结构化事实。
- 输入校验前置,使用幂等键/事务,并记录外部操作标识,避免输出失败后盲目重试。
第 8 章
page()获取历史窗口,follow()从游标继续追踪新事件;序号用于补齐和去重。- 把高频事件合并成较少的外部 Store 通知,降低 React 重渲染。
useSyncExternalStore用引用判断快照是否变化;无变化却返回新对象会导致无意义渲染。- Assembler 把事件变为领域对话节点;Renderer 把稳定节点与 Slot 组合成 React 树。
- 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 建议下一步执行的调查操作
pnpm install后运行pnpm run typecheck,记录真实结果。- 运行最小 unit test,确认本机环境。
- 尝试
pnpm run build,然后跑pnpm dsh --profile headless "..."。 - 用
--dump-config比较 web/headless 组合。 - 打开 Web 或 ACP demo,观察真实流。
- 阅读一个真实
packages/llm/llm-deepseek测试,理解 wire 协议。 - 选择一个 cookbook 教程,完成最小工具插件。