《DeepSeek Harness 架构详解手册》全书面向零基础读者,从 Cordis 插件地基讲到轮次流程、会话日志、工具流水线,最后落到动手开发 Agent。本文把全书四个附录合在一起,是一份可以随时回查的速查表。
四个部分互不依赖,按需翻阅即可:
- 附录 A|扩展点速查表:知道要做什么,反查该挂在哪。
- 附录 B|核心 ctx 键速查表:写服务名时对照,避免拼错或搞混层级。
- 附录 C|新手最常见的十二个坑:写完代码后自查一遍。
- 附录 D|资料来源与核对说明:想知道某个说法的依据,或跟同事口径不一致时查。
其中附录 D 还专门记录了整理过程中发现的四处口径差异(事件数量、默认端口、两种「上下文」、以及事件记录与进入派生历史的区别)——这几处最容易在团队讨论时产生分歧。
附录 A 扩展点速查表
这张表回答一个问题:「我想加 X,应该挂在哪?」它是按官方架构文档的「新行为的归属位置」表与实操手册的「功能→机制」映射表合并整理而成。
A.1 按目标查机制
| 我想做的事 | 挂在哪 |
|---|---|
| 添加模型提供方 | 在ctx.llm上注册它的适配器 |
| 添加面向模型的能力 | 在ctx.tools上注册;它的 schema 会自动加入提示词组装 |
| 让某个会话拥有不同的能力集合 | 组装一个 agent preset;其中的服务行需要isolaterealm |
| 添加 shell 执行 | 注册ctx.shell后端;本地后端通过ctx.subprocessspawn 进程 |
| 添加持久化终端执行 | 注册ctx.terminals后端和dsh-tool-terminal |
| 添加用户命令 | 在ctx.commands上注册;它无需模型轮次即可分派 |
| 管理后台任务 | 在ctx.jobs上注册;job_*工具读取或停止任务 |
| 从外部 webhook 启动 Session | 在ctx.webhookRuntime上注册可信规则,并挂载提供方适配器 |
| 添加文件系统访问或策略 | 注册ctx.fs提供方,或监听fs/*事件 |
| 限制所启动的进程 | 使用ctx.sandbox后端;消费方在启动进程前包装 argv |
| 拦截请求、工具或轮次 | 使用相应的agent/*或tools/*事件;agent/turn-stopping会停止轮次 |
| 添加模型可见的上下文 | 调用agent.inject();它会落到下一次获准的请求中 |
| 添加 UI 或编辑器集成 | 驱动ctx.agents并从session/event渲染 |
| 添加 Web Client Chat 节点 | 注册ConversationNodeDefinition+ keyed renderer |
| 添加持久会话状态 | 扩展SessionEventMap;从日志渲染和回放 |
| 生成会话标题 | 注册唯一的ctx.sessionTitle提供方 |
| 管理同会话目标 | 使用ctx.goals;通过agent/*续跑 |
| 在轮次边界 fork 会话 | ctx.agents.create({ sessionId, seed, meta: { parentSession, seedLength } })——只有经 agent-loop 发布的会话才会持久化 |
| 在新后端存储会话 | 基于共享的句柄脚手架实现SessionPersistence(create/open/stat/list/export) |
| 将注册项限定到单个 agent | 使用该 agent 的agent.ctx |
A.2 按产品功能查插件机制
| 产品功能 | 插件机制 |
|---|---|
| 钩子系统(用户级 + 项目级) | agent/created、agent/pre-step、agent/request、tools/pre-execute、tools/post-execute、agent/turn-stopping上的监听器。dsh-hooks-claude-code/dsh-hooks-codex把钩子配置文件映射到这些扩展点 |
/goal | ctx.goals管理持久状态,dsh-goal-round-driver通过公共Agent调度同会话 Round |
/loop | 在turn/end会话事件上followup()下一次迭代;或强制继续 |
| 动态工作流 | ctx.workflowEngine+ PTC 工作流引擎 +workflow工具 |
| 排队消息 + steering | 核心Agent.followup()/Agent.steer() |
| 上下文压缩(自动 + 手动) | ctx.compactionseam +dsh-compaction-basic。自动压力检查跑在串行agent/pre-step,溢出恢复跑在agent/request-error,手动调用方用同一个压缩服务 |
| 系统提示词可配置性 | ctx.systemPrompt.section(),支持排序与作用域局部覆盖 |
| AGENTS.md(根目录) | 一个读取该文件的 section 提供方 |
| AGENTS.md(子目录,按需触发)+ 文件变更通知 | 从 watcher 或工具结果监听器调用agent.inject() |
| 内置工具 | ctx.tools.register();schema 自动流入装配。dsh-tool-*系列(bash、fs、web、subagent、todo)是已交付的示例 |
| ToolSearch / 渐进式披露 | 当可见集变化时替换一个作用域化的ctx.tools.restrict()注册 |
| 工具截止时间 / 重试 / 指标 | 用tools/execute包裹核心分发 |
| 最终工具结果指标 / 审计 / 捕获 | 用tools/result观察不可变的权威结果 |
| 单调终端轮次策略 | 从成功的终端工具调用ToolExecution.concludeTurn() |
| 子进程沙箱(landlock / sandbox-exec) | 通过dsh-bash-sandbox使用ctx.sandbox后端;能力级别的拒绝用tools/pre-execute |
| 权限系统 / AskUserQuestion | 从tools/pre-execute返回ask并通过ctx.approval应答;为普通用户提问注册一个独立的面向模型的 ask 工具 |
| Plan mode | @deepseek-ai/dsh-plan-mode:落日志的plan/mode状态、plan:policy引导段、/plan入口、经用户评审的exit_plan_mode出口。强制约束留在独立的沙箱/审批轴上 |
| subagent 委派 | ctx.subagents提供方注册表(六个提供方)+dsh-tool-subagent向模型暴露一个已配置的提供方 |
| MCP | 每个服务器一个插件:发现工具 →ctx.tools.register() |
| skill(技能) | section + 工具注册;调用时通过inject()注入 skill 内容 |
| 记忆 | section 提供方 + 工具 |
| 定时任务(cron) | 插件注册面向模型的调度工具;定时器触发 → 空闲时followup(...),忙碌时inject()通知 |
| UI(GUI;CLI 输出 JSONL) | 监听agent/assistant-stream的实时 chunk,并监听session/event的持久结算、边界与工具活动;输入 →followup() |
| 遥测 / 可回放 trace | session/event→ JSONL;回放 =sessions.create(id, { seed }) |
| 模型适配器 | 通过registerAdapter注册LlmAdapter子类 |
| 插件热重载 | 每个注册都是一个ctx.effect,所以随仓库提供的 HMR 直接生效 |
A.3 所有事件一览(按分发模式分组)
| 模式 | 事件 | 用途 |
|---|---|---|
waterfall | agent/pre-step | 拒绝或改写要进入步骤的消息。请求发出前的唯一拦截点 |
waterfall | agent/request | 替换冻结的调用配置(换提供方/模型/推理强度/采样参数) |
waterfall | agent/request-error | 处理一次失败的模型请求尝试。返回{kind:'retry'}触发重试 |
waterfall | llm/stream | 环绕每一次流式模型调用(重试、回放、路由) |
waterfall | tools/pre-execute | 允许/拒绝/取消/询问 |
waterfall | tools/execute | 环绕分派:超时、重试、指标 |
waterfall | tools/post-execute | 接受/替换/阻止 / 附加下文 |
waterfall | tools/ptc-dispatch-log | 允许替换run_code子分派结果的日志副本内容 |
waterfall | system-prompt/assemble | 专家级整体装配变换。返回值具有权威性 |
waterfall | approval/request | 一次性权限决策的分派 |
serial | agent/created | agent 已就绪,可做每个 agent 的初始化。全部 await 完成前创建不算成功 |
serial | agent/turn-stopping | 轮次即将关闭的终局检查点 |
parallel | session/flush | 等待的并行持久化检查点:每个监听器都跑,调用方 await 全部 |
emit | agent/status | 状态在idle⇄running之间切换 |
emit | agent/assistant-stream | 实时的助手流发布:start、chunk、end |
emit | agent/inbox/inserted·claimed·discarded | 跟踪单条消息的入队、领取、丢弃 |
emit | agent/disposed·agent/error | agent 离开注册表 / 步骤或轮次出错 |
emit | session/created·session/disposed | 会话发布与离开 |
emit | session/event | 提交后的追加通知源。可回放数据都从这里读 |
emit | tools/change·tools/result | 工具集变化 / 观察冻结的最终结果 |
emit | system-prompt/change·llm/adapters-updated | 提示词提供方变化 / 提供方拓扑变化 |
emit | api-session/* | 会话列表的活动、加入、移除、状态、错误 |
emit | agent-preset/selected | 某个会话向它的持久日志提交了不同的 agent preset |
附录 B 核心 ctx 键速查表
写插件时你最常做的事,就是在某个ctx键上注册东西。这张表按「你关心的领域」分组,列出最常用的键。完整清单见官方的能力图。
| ctx 键 | 角色 | 你会用它做什么 |
|---|---|---|
| 核心四件套 | ||
ctx.agents | core | 创建/恢复/查找 agent;传播发起方(currentInitiator/withInitiator) |
ctx.agentLoop | bundle | 唯一的具体循环实现。扩展包不要依赖它,依赖ctx.agents |
ctx.sessions | core | 创建会话、注册消息投影、fork、flush 检查点 |
ctx.tools | core | 注册工具、设置守卫、限制工具、按作用域查询 schema |
ctx.systemPrompt | core | 注册提示词段落、动态上下文、工具 schema 提供方、变量 |
ctx.llm | seam | 注册模型适配器;直接发起一次模型调用;查询模型能力 |
| 执行环境 | ||
ctx.fs | seam | 文件系统读写(本地/沙箱/远程) |
ctx.subprocess | seam | 启动进程(Bash、PTY、LSP、外部 subagent 都靠它) |
ctx.shell | seam | 面向模型的 shell 执行后端 |
ctx.terminals | seam | 持久化的交互式终端 |
ctx.sandbox | seam | 进程沙箱:包装即将执行的 argv |
ctx.sandboxPolicy | core | 部署默认沙箱模式与工作区根目录 |
ctx.lsp | seam | 语言服务导航(只有四种标准化操作) |
ctx.ssh | core | 一条经过认证的 OpenSSH 连接及其远端提供方 |
| 安全与审批 | ||
ctx.approval | seam | 一次性权限决策的分派与应答 |
ctx.permissionPresets | core | 用户可见的权限预设切换 |
ctx.credentials | seam | 机密信息的引用。配置只携带引用,实际值归提供方所有;消费方按操作解析,所以轮换的凭据会在下一次请求就生效 |
ctx.authorization | seam | 授权流程注册(怎么拿到某份凭据) |
ctx.deepseekAccount | seam | DeepSeek 账号。UI 使用方只接收不含 token 的状态 |
| 会话数据 | ||
ctx.sessionPersistence | seam | 把会话事件持久化到磁盘 |
ctx.sessionProjections | core | 注册状态折叠单元;读单个类型化状态 |
ctx.sessionProjectionCache | core | 按会话持久保存投影检查点,加速恢复 |
ctx.sessionQuery | seam | 会话的读取、过滤、追踪、搜索 |
ctx.sessionTitle | seam | 会话标题生成 |
ctx.storage/ctx.storageDomain | seam / core | 非会话存储的中枢/类型化持久状态 |
ctx.attachments | seam | 持久的二进制附件存储(图片、文件) |
ctx.spillStore | seam | 过大的工具文本的暂存与定位 |
| 协作与外部世界 | ||
ctx.subagents | seam | 子 agent 的提供方注册与延续编排 |
ctx.agentTeams | core | 实验性多 agent 协作(roster、mailbox、任务 DAG) |
ctx.jobs | seam | 后台任务的注册与控制 |
ctx.web | seam | web 搜索与抓取的提供方注册 |
ctx.skills | seam | 技能目录 |
ctx.mcpResources | seam | MCP 资源访问 |
ctx.browserUse/ctx.computerUse | seam | 浏览器操作/桌面自动化 |
ctx.webhookRuntime | core | 外部 webhook 触发会话创建 |
ctx.schedule | core | 定时消息 |
ctx.commands | core | 面向人的命令(无需模型轮次) |
ctx.userQuestions | seam | 工具向人提问的通道 |
ctx.planMode | core | 计划模式的协作状态 |
ctx.goals | core | 同会话目标 |
| 应用与界面 | ||
ctx.agentPresets | core | 按会话的 agent 组合(YAML preset) |
ctx.webServer | core | 注册 HTTP 路由 |
ctx.clientModules | core | 客户端插件图 |
ctx.connection | core | 浏览器认证与共享 HTTP 请求分发 |
ctx.settings | core | 插件配置表单 |
ctx.configEditor | core | 在应用文件锁与 HMR 队列下持久化 profile 配置 patch |
ctx.pluginManager | core | 当前 profile 的插件与组合包管理 |
ctx.otel/ctx.sessionTelemetry | service / seam | 共享的遥测上报通道 |
附录 C 新手最常见的十二个坑
这一章是按「错误认知 → 正确认知」的方式整理的。这些坑大多来自「用别的框架的经验硬套过来」,而不是来自不会写代码。
| # | 常见错误认知 | 正确认知 |
|---|---|---|
| 1 | 「我应该去找核心抽象层/基类」 | 这里没有特权内核。你要找的是挂载点——某个ctx键或某个事件。见第 1、10 章 |
| 2 | 「patch 是深度合并,我只写要改的字段」 | patch 按 id 定位后整体替换config。你必须把想保留的字段全部重述。见第 2 章 |
| 3 | 「我把系统提示词清空了,但模型还记得旧指令」 | 空渲染文本会清除所有生效的系统节点。如果旧指令还在起作用,它大概不在系统提示词里——检查 user 消息和 skill 注入。见第 5 章 |
| 4 | 「往内存里的 messages 数组塞东西,模型就能看到」 | 模型历史是从日志派生的。要写事件,或者调用agent.inject()。见第 4 章 |
| 5 | 「状态是 running 说明它正在处理我这条消息」 | running跨越整个驱动器排空区间,可能包含多个轮次。要知道单条消息的进度,看 inbox 通知和轮次事件。见第 3 章 |
| 6 | 「我想阻止轮次结束,就返回一个 veto 值」 | agent/turn-stopping是 serial,没有next()。想阻止就 steer 一条消息,让机器重新读 inbox。见第 3 章 |
| 7 | 「并发安全默认应该是开的吧」 | 默认是exclusive(独占),而且 fail-closed。想并行要主动返回严格true。见第 6 章 |
| 8 | 「我把结果内容替换掉了,就算保密了」 | 内容替换是展示策略。程序仍然能拿到value。要保密就用block或替换value。见第 6 章 |
| 9 | 「命令返回非零,所以工具该抛异常」 | 非零退出是正常的领域结果,应该作为值返回,由渲染器解释。**只有基础设施故障才抛异常。**见第 6 章 |
| 10 | 「重试应该在适配器里做」 | 一次适配器调用 = 一次提供方尝试。适配器禁用库重试,重试是 agent 层的职责。见第 7 章 |
| 11 | 「currentInitiator()有值,所以这次调用是被授权的」 | 归因不等于授权。发起方方法只提供同进程内的因果归因。授权走审批 seam。见第 9 章 |
| 12 | 「我加了一个新事件类型,读取时应该跳过不认识的事件」 | 默认是必需:遇到不认识的、又没有ignorable标记的事件,必须拒绝重建。**宁可多报一次错,也不要静默恢复出一个被掏空的会话。**见第 4 章 |
附录 D 资料来源与核对说明
这份手册的内容全部来自官方仓库的文档。这一章说明资料来源、范围边界,以及我在整理过程中发现并处理的几处需要留意的地方。
D.1 主要资料来源
| 文档 | 本手册用它来支撑 |
|---|---|
docs/architecture.zh.md | 整本书的骨架:Cordis 定位、profile 与组合包、核心包表、事件三域、轮次流程伪代码、会话日志、能力 seam、归属位置表 |
docs/cordis-primer.zh.md | 第 1 章:五个核心概念、五种分发模式、waterfall 语义、实践规则 |
docs/cordis-tutorial/index.zh.md | 第 10 章:七章动手教程的路径与准备工作 |
docs/agent-lifecycle.zh.md | 第 3 章:轮次与步骤的时序细节、失败与重试的处理位置 |
docs/tool-execution-pipeline.zh.md | 第 6 章:工具流水线的完整关卡顺序、PTC 与上下文附加 |
docs/capability-seams.zh.md | 第 8 章与附录 B:seam 列表、角色分类、各键的职责与消费方 |
docs/subsystems/core.zh.md | 第 3、4 章:Agent 句柄与全部方法、inbox、取消语义、两个类型模式、会话事件清单 |
docs/subsystems/session.zh.md | 第 4 章:事件表全字段、surface 与 surfaceOp、派生规则、持久化约定、fork |
docs/subsystems/tools.zh.md | 第 6 章:ToolDefinition 全字段、schema DSL、执行类型、决策类型、UI 卡片词汇 |
docs/subsystems/system-prompt.zh.md | 第 5 章:PromptSection 全字段、三种更新方式、工具提供方结果 |
docs/subsystems/llm-streaming.zh.md | 第 7 章:内容块、StreamChunk、BlockAssembler、适配器约定、重试策略、TokenUsage |
docs/subsystems/scope.zh.md | 第 9 章:ScopeKey、Scoped、Scope、ScopedLayers 与容器原语 |
docs/cookbook/extension-cookbook.zh.md | 第 1 章的门禁示例、第 10 章的实操模式、附录 A 的功能映射表 |
docs/cookbook/adding-a-tool.zh.md | 第 6、10 章:工具约定、PTC 设计建议、后台任务、UI 展示规则 |
docs/user/develop/basic/tool.zh.md | 第 10 章:最小可运行的工具示例与--patch开发方式 |
packages/boot/app-boot/README.zh.md | 第 2 章:启动五步、失败模式表、profile 与 runtime resolution |
README.zh.md | 封面信息、运行方式、项目状态与许可 |
D.2 核对中发现并处理的四处地方
为了让这份手册不把读者带进沟里,以下几处我在整理时专门做了处理,并把结论写在这里。
| # | 发现的问题 | 处理方式 |
|---|---|---|
| 1 | **会话事件的数量存在两种口径。**核心包文档写「十三种核心事件变体」并列出十三项;而会话子系统的事件表里还登记了developer/message,以及由插件合并扩展的compaction/*、hook/*等 | 第 4 章明确采用「核心十三种」的口径,并单独用提示框解释了两种数字的关系(一个说「核心」,一个说「当前全部」),避免读者以为文档自相矛盾 |
| 2 | **两个默认端口容易混。**README 里的 Web 默认地址是127.0.0.1:3080,而桌面应用文档里的默认端口是19387 | 第 2 章专门加了一个提示框把两个数字并列说明,并指出 19387 是桌面端且可被 profile 覆盖,避免读者看到 19387 时误判为配置错误 |
| 3 | 「上下文」一词在此框架中有两个完全不同的含义。ctx(Context)指插件共用的服务容器;而contextWindow指模型能处理的 token 上限 | 术语表(第 11 章)为这两个词分别立条,并在 Context 条目下明确写出「不是『语言模型的上下文窗口』」。第 7 章也再做了一次区分 |
| 4 | **assistant/message与assistant/attempt的「是否进入模型历史」表述容易误读。**文档说法是后者「不添加模型历史」,前者「内容为空的也会跳过」 | 第 3 章用一张两列表把两者并排对比,并特别标注「记录事件」与「进入派生历史」是两件独立的事——空内容的 assistant 消息仍然会记录,只是不进派生历史 |
D.3 范围边界
说明一下这份手册没有做什么,以免读者产生错误的期待:
- **没有逐字段抄录所有生成型文档。**官方仓库里有一批由脚本生成的参考页(
config-catalog、persistence-catalog、各子系统的cordis-surface代码块)。一个包里面就可能有几十页 JSDoc 原文。这些属于「查」而不是「读」,本手册只在需要说明机制时引用其结论,没有整体搬运。 - **没有覆盖每一个实验性包。**实验性的浏览器操作、计算机操作、语音识别、Python 版 PTC 运行时等只在 seam 列表里点到,没有展开。
- **没有给出可复制的完整插件工程。**本手册的代码片段用于说明「为什么这么写」,很多省略了 import 与辅助实现——官方也明确说了这一点。要跑起来,请走第 10 章的路径并参考官方教程。
- **没有承诺长期有效。**项目处于开发者预览阶段,官方明确写着「未来将出现破坏兼容性的变更」。本手册描述的是这组文档所记录的架构契约与设计取舍——这部分相对稳定;具体字段名、命令参数请以你手上的代码为准。
D.4 一份实用的一句话总结
这套架构只讲了一件事:把「行为」和「策略」都变成可以挂载、可以替换、可以撤销的插件,然后用一条仅追加的日志保证「发生过什么」永远可查、可重建、可回放。
学它的顺序,也应该是这个顺序:先接受「日志是唯一真源」,再接受「一切皆插件」,最后学会问「这件事该挂在哪个扩展点上」。这三步走完,剩下的都是查表。
使用建议
如果你只想把一张纸贴在显示器边上,我建议是附录 C 的十二个坑,因为它覆盖面最广:
几乎每一条都对应着手册某一章里反复强调过、但在动手时又特别容易忘的点。
四份表都只是速查,想看某个为什么这么设计,回到正文对应的章节即可,那里有完整解释与配图。
内容整理自 DeepSeek Harness 官方仓库
docs/architecture.zh.md及其引用的全部子系统、教程、
实操手册与核心包文档。官方项目处于开发者预览阶段,具体字段与命令请以你手上的代码为准。