第一章 项目概览与入门
本章导读
本章是《DeepSeek Harness 实践指南》的起点,也是全书唯一的「从零开始」章节。我们将依次回答四个问题:
- DeepSeek Harness 是什么------它解决什么问题、由谁开发、架构上有什么与众不同的地方;
- 它处于什么产品阶段------开发者预览意味着什么,对你使用它的稳定性预期有什么影响;
- 如何把它跑起来 ------
npm(npx)一键运行与从源码 clone +pnpm install+pnpm run build两种路径的前置要求、命令与行为细节; - 如何完成第一个任务------通过 Web UI(Web 用户界面)配置模型、选择工作区、运行一个真实任务,并理解权限审批闸门。
读完本章,你应当能够在自己的机器上拥有一个可用的 DeepSeek Harness 实例,并完成一个端到端的最小任务。后续章节将在此基础上展开模型提供方的高级配置、插件体系、工作区与权限策略、以及开发实战。
读者对象与前置知识
本章面向以下读者:
- 新用户:第一次接触 DeepSeek Harness,希望尽快跑通一个实例;
- 贡献者:希望从源码构建并理解项目结构,为后续开发章节做准备;
- 集成方 :评估是否将
dsh纳入自己的工具链,需要了解其架构理念与许可证。
前置知识要求:
| 知识 | 要求 | 说明 |
|---|---|---|
| 命令行 | 熟悉常用 shell 命令(cd、git clone 等) |
安装步骤全程在终端中完成 |
| Node.js | 已安装,或知道如何安装 | npx 方式运行的前置条件 |
pnpm |
仅源码运行需要 | pnpm install、pnpm run build 的前置条件 |
| LLM API | 拥有至少一个可用模型密钥 | 推荐使用 DeepSeek API 密钥;也可用其他 OpenAI 兼容提供方 |
如果你完全没有命令行经验,请先补一节基础 shell 教程再来读本章;
dsh的安装与运行步骤均以命令行为载体。
1.1 什么是 DeepSeek Harness
DeepSeek Harness(命令行简称为 dsh)是由 DeepSeek AI 开发的开源 agent harness(智能体框架)。它面向的核心场景是:为大语言模型(Large Language Model,LLM)提供一套可运行、可观测、可扩展的智能体(agent)执行环境------模型在其中读取与编辑文件、运行命令、委派子任务,并在多轮(turn)对话中持续推进一个目标。
1.1.1 「harness」一词的含义
在智能体(agent)语境下,harness(运行框架 / 执行环境) 指包裹在大语言模型之外的那一层软件:它负责把模型的文本输出转化为真实世界的操作------调用工具、执行命令、读写文件------再把操作结果反馈给模型,如此循环。与「框架」(framework)强调代码组织方式不同,「harness」强调对模型的驾驭:模型本身只提供推理与决策,而它接触外部世界的每一寸缰绳,都握在 harness 手里。
dsh 正是这样一层缰绳。具体而言,它为模型提供:
- 工具(tool)层:读写文件、执行 shell 命令、搜索代码、委派子智能体等工具的统一注册与执行流水线;
- 会话(session)层:仅追加(append-only)的会话事件日志,使每一轮(turn)、每一步(step)都可回放、可审计;
- 权限层:权限策略与审批闸门,决定哪些操作可以直接执行、哪些必须先经人工批准;
- 模型适配层:将多家提供方(provider)的模型统一适配到同一套请求/响应形态之下,使模型可以即插即换。
1.1.2 它解决什么问题(为什么需要 harness)
如果把「让一个 LLM 帮我完成一个多步骤任务」当作一个工程问题,那么裸模型只有推理能力,没有任何手段接触外部世界。要让它真正干活,你需要自己解决一连串问题:如何把工具描述喂给模型?如何解析它的工具调用并执行?如何把执行结果拼回上下文?如何控制它能碰哪些文件、跑哪些命令?如何让它把大任务拆成子任务并分派?如何做日志与回放?
dsh 的工程价值就在于:把这一整套基础设施做成一个开箱即用、又处处可替换的开源实现。你不必从零搭一个「模型 + 工具 + 权限 + 会话」的执行环境;同时,因为一切皆插件,当默认实现不合你意时,你可以替换其中任意一层,而不必放弃其余部分。
理解 dsh 的架构,只需要抓住两个关键词:
1.1.3 一切皆插件
dsh 采用**一切皆插件(everything-is-a-plugin)**的架构。这意味着产品的每一部分------模型适配器、工具(tool)注册表、会话(session)日志、权限与审批策略,乃至驱动智能体循环(agent loop)本身------都是一个插件,挂载在一棵共享的插件树上。其直接后果是:
- 没有特权内核 :不存在某个必须打补丁的核心模块;扩展
dsh的方式,是把新插件挂载到既有插件旁边。 - 一切皆注册,注册皆可撤销:插件通过贡献服务、类型化事件和可逆副作用参与运行,卸载插件时这些副作用会被撤销。
- 一切皆可从配置替换:因为每一部分都是插件,所以每一部分都可以被你的配置覆盖。你可以用自定义插件替换默认的模型路由,也可以覆盖默认的工具集,而无需修改发行版代码。
1.1.4 基于 Cordis 驱动
插件树的运行时由 Cordis 提供。Cordis 的设计思想参见论文 A Programming Paradigm for Spatiotemporal Composability (一种时空可组合性的编程范式)。在 dsh 中,一个运行中的实例可以被理解为一棵插件树,由启动时按序叠加的各层(profile,档位 与 bundle,捆绑包)组装而成。对于使用者而言,这意味着:
你看到的每一个内置能力,本质上都是一个可替换、可叠加、可撤销的插件;而你在配置文件里写的每一条覆盖,都是在对这棵插件树的某一层做声明式修改。
如果你希望深入理解插件树、profile 与 bundle 的组合机制,后续章节会给出系统性介绍,并推荐官方 架构文档 与 Cordis 入门教程作为延伸阅读。
1.1.5 核心概念速览
在继续之前,先把本章及全书将反复出现的核心概念集中定义一次,避免后文不断解释:
| 术语 | 英文 | 定义 |
|---|---|---|
| 智能体 | agent | 在 harness 中运行、由模型驱动的执行单元,能调用工具、委派子任务并维护计划 |
| 会话 | session | 一次持续对话的完整记录,以仅追加的事件日志形式存储,可回放、可审计 |
| 轮次 | turn | 会话中的一个往返:用户(或自动机制)给出输入,智能体产生输出 |
| 步骤 | step | 轮次内的最小动作单位,如一次工具调用或一段模型输出 |
| 事件 | event | 会话日志中的单条记录,描述发生的某件事实(消息、工具调用、结果等) |
| 工具 | tool | 智能体可调用的能力单元(如读文件、执行命令),统一注册、带把关地执行 |
| 插件 | plugin | 向插件树贡献服务、类型化事件与可逆副作用的扩展单元;dsh 的一切皆插件 |
| 提供方 | provider | 提供模型服务的来源(如 DeepSeek、Anthropic、OpenAI 或自建网关) |
| 模型 | model | 具体可调用的大语言模型实例,隶属于某个提供方 |
| 档位 | profile | 存放在 Harness home 中的具名插件组装,列出自己叠放的捆绑包与用户补丁 |
| 捆绑包 | bundle | Cordis 配置项及其挂载代码的分发格式,是插件树中可叠加的一层 |
| 能力接缝 | seam | 插件之间约定的扩展/替换点,使一层可以在不修改另一层的前提下接管某项能力 |
| 工作区 | workspace | 智能体被授权操作的文件系统目录,选中前会话输入框不可用 |
1.2 产品阶段:开发者预览
注意: DeepSeek Harness 目前处于 开发者预览 (developer preview)阶段,正在快速迭代。未来将出现破坏兼容性的变更(breaking changes)。
作为读者,这意味着:
- 接口可能不稳定:命令行参数、配置文件字段、插件挂载点均可能在版本之间变化,文档可能暂时滞后于代码。
- 请固定你依赖的版本:如果你的工作流依赖某个版本的行为,建议记录并在升级前阅读变更说明。
- 欢迎反馈 :预览期的价值在于共建。遇到 bug 或有改进想法,请通过 GitHub Discussions 反馈(详见 1.6 节)。
1.2.1 预览期使用建议
- 生产使用请谨慎:预览阶段不承诺稳定性,若你的业务对中断敏感,建议先在隔离环境中评估。
- 备份重要数据 :
$DSH_HOME下存放着你的凭据与设置,建议在重要改动前保留备份。 - 跟随仓库 README 与 CHANGELOG:官方文档(尤其是仓库 README 与各插件的 README)是行为的最终依据;本指南在事实与仓库文档冲突时,以仓库文档为准。
- 报告问题时提供版本 :提交 bug 时附上
dsh的版本号、操作系统与可复现步骤,能显著加快定位速度。
1.3 安装与运行
dsh 提供两条运行路径,按你的目的二选一。两条路径的对照如下:
| 维度 | npm(npx)方式 |
源码方式 |
|---|---|---|
| 前置要求 | 已安装 Node.js | Node.js + pnpm |
| 获取方式 | npx 自动拉取发行包 |
git clone 官方仓库 |
| 是否需要构建 | 否 | 是(pnpm run build) |
| 适用场景 | 日常使用、快速体验 | 开发、调试、贡献、深度定制 |
| 升级方式 | npx 自动获取新版 |
手动 git pull + 重新构建 |
1.3.1 通过 npm 运行(推荐)
先安装 Node.js,然后在任意目录执行:
sh
npx @deepseek-ai/dsh web
该命令会完成「获取发行包并启动 Web UI」的全部过程,无需手工安装。关于它的行为细节:
- 默认在
http://127.0.0.1:3080启动 Web UI; - 在本机(非 SSH)启动时,会尝试用默认浏览器自动打开页面;
- 通过 SSH 远程启动时,命令只会打印宿主机 URL ------因为本地端口转发地址由你的 SSH 客户端或编辑器持有,
dsh无法替你打开; - 传入
--no-open参数可仅启动服务器而不打开浏览器,例如:
sh
npx @deepseek-ai/dsh web --no-open
1.3.2 从源码运行
如果你需要阅读源码、调试插件或参与贡献,从仓库源码运行:
sh
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
两条命令的分工值得明确:
pnpm run build负责准备仓库产物(构建各包);pnpm dsh web直接复用这些已构建产物启动,不会重新构建。
因此修改源码后,通常需要重新执行 pnpm run build 再启动。
1.3.3 仓库结构速览
clone 下来后,仓库的顶层布局有助于你定位后续开发章节会涉及的内容。几个值得记住的位置:
| 路径 | 内容 |
|---|---|
README.md / README.zh.md |
项目入口说明(含本指南 1.3 节引用的全部运行命令) |
CONTRIBUTING.md / CONTRIBUTING.zh.md |
贡献指南 |
LICENSE |
MIT 许可证 |
THIRD_PARTY_NOTICES.md |
第三方依赖及其许可证 |
AGENTS.md |
面向 agent 的开发约定 |
packages/ |
各插件包(核心包、模型适配器、工具、持久化等),dsh-base 等捆绑包也位于此 |
apps/ |
应用入口(如 Web UI 的 apps/web 与 CLI) |
docs/ |
文档,含 development.zh.md(开发指南)、architecture.zh.md(架构)、user/guide/(用户指南)等 |
提示:本指南后续章节会频繁引用
packages/与docs/下的具体文件。现在先建立「文档与源码同仓、中文文档以.zh.md后缀命名」这一印象即可。
1.4 启动 Web UI
无论走哪条路径,入口都是 dsh web 子命令(npx 方式写作 npx @deepseek-ai/dsh web)。启动成功后,终端会打印访问地址,默认即为 http://127.0.0.1:3080。
有几个初始状态概念需要先建立:
$DSH_HOME:Harness 的主目录,存放settings.yaml(设置)、.credentials.yaml(凭据)等文件。- 默认文件系统位置 :
dsh进程会把启动时所在的目录 作为默认文件系统位置。换言之,你在哪个目录下执行dsh web,工作区选择时就会默认关联到哪个目录。 - 全新的 Web UI 不会自动选中任何工作区------你需要手动添加并选中一个(见 1.5.2 节)。选中工作区之前,会话输入框是不可用的。
1.4.1 关于访问地址的常见疑问
| 场景 | 行为 | 说明 |
|---|---|---|
| 本机启动 | 打印 http://127.0.0.1:3080 并尝试用默认浏览器打开 |
最典型的使用方式 |
本机启动 + --no-open |
只打印地址,不打开浏览器 | 适合你已在本机浏览器中打开、或希望控制打开时机的场景 |
| 通过 SSH 远程启动 | 只打印宿主机 URL | 本地端口转发地址由你的 SSH 客户端或编辑器持有,dsh 无法替你打开浏览器;你需要自行完成端口转发 |
| 端口被占用 | 启动可能失败 | 请释放 3080 端口,或通过启动参数指定其他端口 待核实 |
提示:如果你通过 VS Code 的 Remote-SSH 或类似远程开发环境使用
dsh,编辑器的端口转发机制会自动把远程的 3080 端口映射到本地,此时在本地浏览器打开http://127.0.0.1:3080即可。
1.5 快速开始:四步完成第一个任务
假设 Web UI 已经启动。以下四步构成一次完整的最小运行闭环:配置模型 → 选择工作区 → 运行任务 → 理解审批。
1.5.1 第一步:配置模型
打开 Web UI 的 设置 → 模型 页面:
- 在 DeepSeek 卡片中输入你的 DeepSeek API 密钥;
- 点击保存。
两点值得注意:
- 无需重启:模型路由配置保存后立即可用,服务器不重启。
- 密钥只写 :密钥保存后,页面只会收到脱敏描述符,永远不会回传明文;密钥实际存储在
$DSH_HOME/.credentials.yaml中,设置文件(settings)只保留它的凭据引用。
除 DeepSeek 之外,你还可以添加 Anthropic、OpenAI 等目录提供方,或接入公司网关、自建服务器等自定义提供方 (OpenAI 兼容端点)。完整的提供方配置、自定义端点与常见排错(如
MISSING_CREDENTIAL、UNKNOWN_MODEL、网关拒绝请求等)详见官方《配置模型》指南,本指南后续章节也会给出深入讲解。
1.5.2 第二步:选择工作区
点击 Web UI 中的 选择工作区:
- 添加你启动
dsh时所在的项目目录(或任意你希望智能体操作的目录); - 选中该工作区。
再次强调:选中工作区之前,会话输入框不可用------这是预期行为,不是故障。
工作区是智能体能力的边界:它决定了智能体可以读写哪些文件、在哪个目录里运行命令。如果你需要智能体同时操作多个目录,可以添加多个工作区 待核实;日常使用一个工作区即可。
1.5.3 第三步:运行第一个任务
启动一个会话,发送这样一条指令:
Summarize this repository and identify its main packages.
(中文大意:总结这个仓库,并识别其主要包。)
接下来你会观察到智能体的典型工作方式:它可以读取和编辑工作区文件、运行命令、委派工作(如派生子智能体)并维护计划(plan) ,在多轮中逐步完成任务。这就是 dsh 的核心循环------模型在框架提供的工具与权限边界内自主行动,Web UI 则把每一步呈现给你。
1.5.4 第四步:理解权限与审批
dsh 内置权限策略(permission policy)。当智能体想执行某项操作、而该操作按当前权限策略需要审批 时,Web UI 会先向你发起询问,由你决定是否放行。这构成了预览阶段使用 dsh 的基本安全模型:能力是开放的,但敏感操作有人工闸门。
理解审批机制,有三个要点:
- 审批由权限策略驱动:不是所有操作都要审批。策略会区分哪些操作可以自动执行(如读取文件)、哪些必须先经人工批准(如修改系统配置、删除文件等,具体边界以策略为准 待核实)。
- 审批是同步的:在 Web UI 中,当审批请求出现时,智能体会暂停等待你的决定,不会绕过闸门继续执行。
- 审批结果可追溯:每一次审批决定都会记录在会话日志中,便于事后审计。
安全提示:审批闸门是人机协作的最后一道防线,但并非银弹。在不受信任的目录中运行智能体时,建议保持谨慎,不要对高风险操作盲目批准。
1.6 社区与支持
- GitHub Discussions :欢迎通过 GitHub Discussions 提交反馈或 bug 报告。这是预览期最直接的沟通渠道。
- 插件生态 :如果你在开发插件,可以为你的插件仓库添加
dsh-plugin话题(topic),便于其他用户在 GitHub 上发现它。 - 企业微信群:欢迎加入 DeepSeek Harness 企微群------扫码添加企微小助手并填写入群问卷,完成后小助手会邀请你入群。
- 贡献代码 :参与贡献请阅读仓库中的
CONTRIBUTING.zh.md;深入开发请先阅读官方开发指南与架构文档。
1.7 许可证
DeepSeek Harness 采用 MIT 许可证发布,源代码见仓库 LICENSE 文件。第三方依赖及其各自的许可证见仓库中的 THIRD_PARTY_NOTICES.md。
MIT 许可证意味着你可以在保留版权声明的前提下,自由地使用、复制、修改、合并、发布该软件的副本;同时该库按「现状」提供,作者不提供任何明示或暗示的保证,也不承担任何责任。
1.8 本章小结与下一步
本章建立了使用 DeepSeek Harness 的四个基础认知:
- 它是什么:DeepSeek AI 开源的智能体框架,一切皆插件,由 Cordis 驱动;
- 它处于什么阶段:开发者预览,会有破坏性变更,请固定版本并积极反馈;
- 怎么跑起来 :
npx @deepseek-ai/dsh web一键体验,或 clone +pnpm install+pnpm run build从源码运行; - 第一个任务怎么走:配置模型(设置 → 模型,填 API 密钥)→ 选择工作区 → 发送指令运行任务 → 理解审批闸门。
下一步建议按以下顺序推进:
- 深入模型配置:了解目录提供方、自定义提供方(OpenAI 兼容端点)与常见错误排错;
- 理解插件与 profile :认识
dsh的插件树、profile 与 bundle 的组合机制,以及dsh --profile web --dump-config这类自省命令; - 进入开发:阅读官方开发指南与架构文档,尝试编写你的第一个插件。
1.8.1 常见问题(FAQ)
Q:npx @deepseek-ai/dsh web 提示找不到包或下载失败?
A:请确认本机已安装 Node.js(建议较新的 LTS 版本),并检查网络是否能访问 npm 官方源。如使用国内网络,可考虑配置 npm 镜像源后重试。
Q:启动后浏览器没有自动打开,我该怎么办?
A:手动在浏览器中打开 http://127.0.0.1:3080。如果你是通过 SSH 远程启动,需要先完成端口转发,再在本地浏览器打开该地址。
Q:为什么会话输入框是灰的、不能输入?
A:你还没有选中工作区。点击「选择工作区」,添加一个项目目录并选中它,输入框即可使用。
Q:配置模型后需要重启服务器吗?
A:不需要。模型路由配置保存后在下一次请求时生效,服务器无需重启。
Q:$DSH_HOME 默认在哪里?
A:$DSH_HOME 是 Harness 的主目录,存放 settings.yaml 与 .credentials.yaml 等文件,其默认位置以项目文档为准 待核实。
Q:我可以同时运行多个 dsh 实例吗?
A:可以,每个实例使用独立的端口(默认 3080,多实例时请注意避免端口冲突 待核实)。
Q:我修改了源码,为什么启动后没有生效?
A:源码方式下,pnpm dsh web 直接复用已构建产物,不会重新构建。修改源码后请重新执行 pnpm run build,再启动。
Q:MIT 许可证意味着我可以做什么、不能做什么?
A:你可以自由使用、复制、修改、分发(包括商用),只需保留版权声明。不能向作者主张任何担保或责任。详见 1.7 节。
1.9 参考资料与延伸阅读
本章内容基于以下素材整理(均见 DeepSeek Harness 官方仓库):
- 仓库
README.zh.md------项目定位、开发者预览声明、运行命令与社区入口; docs/user/guide/index.zh.md------Web UI 使用指南(配置模型、选择工作区、运行任务);docs/user/guide/providers.zh.md------模型提供方配置(DeepSeek、目录提供方、自定义提供方与排错);docs/architecture.zh.md------架构文档(Cordis、profile 与 bundle、核心包);- Cordis 入门教程与论文 A Programming Paradigm for Spatiotemporal Composability。
再次提醒:本文所有命令与配置均以项目当前版本为准;由于项目处于开发者预览阶段,如与你的实际版本不符,请以仓库 README 与官方指南的最新内容为准。文中以 待核实 标注的条目为作者无法从既有素材中确认的细节,请以仓库实际行为为准。
第二章 核心架构
本章导读
第一章回答的是「怎么把 DeepSeek Harness 跑起来」;本章回答的是「它到底是怎么构成的」。一旦你打算修改 packages/ 下的任何内容、为自己的产品定制 dsh,或者仅仅想理解为什么替换一个配置项就能改变整个产品的行为,你就需要本章的内容。
本章沿着一条主线展开:dsh 是一个全插件架构------不存在特权内核。围绕这一论断,我们将依次讨论:
- Cordis 框架------dsh 底层的插件框架,插件如何通过共享上下文、类型化事件与可逆副作用协作;
- 档位(profile)与捆绑包(bundle)------一棵插件树在启动时如何分层叠加组合;
- 核心包------向 Cordis 树贡献关键服务的七个基础包;
- 事件模型------会话事件、agent(智能体)域事件与能力事件的三个领域,以及如何选择扩展点;
- 轮次(turn)与步骤(step)流程------一次模型交互在架构中的完整生命周期;
- 会话日志与派生态------「模型可见即已记录」这一运行时不变量;
- 能力接缝(seam)------可替换能力的三角色模型;
- 「新行为放哪里」------一份从目标到机制的决策表。
读完本章,你应当能够在阅读任何 dsh 子系统文档之前,先在自己的头脑中建立起一张完整的架构地图,并知道每一项常见改动应该落在哪个扩展点上。
前置知识 :本章假定你已通读第一章并跑通过一个
dsh实例。若你对 Cordis 完全陌生,建议同时翻阅仓库中的docs/cordis-primer.zh.md(Cordis 入门)与docs/cordis-tutorial/(Cordis 教程),本文只做框架级概述,不重复教程内容。
2.1 全插件架构:没有特权内核
理解 dsh 架构只需先记住一句话:
产品的每一部分都是插件------包括模型适配器、工具注册表、会话日志,乃至智能体循环(agent loop)本身------因此每一部分都可以从配置替换。
通常的智能体框架都假定一个「核心」:模型调用、工具分发、消息管理被硬编码在内核中,外围功能以钩子(hook)形式挂接。dsh 走的是相反的路。它的底层是 Cordis 插件框架(以 vendor 方式引入,源码见 vendor/ 目录),dsh 的全部产品能力都被拆解为 Cordis 插件:
- 模型提供方(如 DeepSeek 适配器)是插件;
- 每个工具(
bash、文件读取等)是插件; - 会话日志与持久化是插件;
- 智能体循环驱动器是插件(
dsh-agent-loop,它是 harness 中唯一包含具体循环逻辑的包); - 甚至沙箱、审批策略、设置、凭据、遥测都是插件。
这个设计带来三条直接后果:
- 扩展方式是把新插件挂载到已有插件旁边,而不是给内核打补丁。仓库中不存在需要补丁化的特权内核代码路径。
- 各项注册都是副作用(effect) 。插件在挂载时通过
ctx.effect()或ctx.on()安装提示词片段、工具 schema、适配器、监听器等;插件卸载(teardown)时这些副作用按预期一一撤销。 - 一切可替换性最终落在配置层 。你不需要 fork 代码,只需在配置(profile 的
cordis.patch.yml等)中把某一行指向另一个实现,行为即改变------这正是 2.7 节「能力接缝」机制的由来。
2.1.1 Cordis 的五个核心概念
在深入 dsh 各层之前,先把 Cordis 框架的五个核心概念过一遍,后文的所有术语都建立在这之上。
| 概念 | 说明 |
|---|---|
| 插件(plugin) | 插件是实现 Service 的对象。它可以是一个带有可选 inject 和 apply(ctx) 字段的函数,也可以是一个 Service 子类,其生命周期由 Cordis 挂载到当前上下文中。 |
| 上下文(context) | 上下文是服务的容器。一个服务占据一个稳定的 ctx.<key> 位置(如 ctx.tools、ctx.llm、ctx.sessions);其他插件通过 key 查找服务,而非导入具体实现。 |
| 依赖注入(inject) | 插件通过 inject 声明所需的服务;Cordis 会等待这些服务就绪才启动该插件。加载顺序由此通过服务依赖表达,而不是手动编排启动序列。 |
| 类型化事件 | 服务通过 TypeScript 声明合并注册事件名,然后以四种模式之一分发:emit(观察)、waterfall(瀑布式包装)、parallel(并行扇出)、serial(按序执行)。 |
| 可逆副作用 | 提示词片段、工具 schema、适配器、提供方和监听器通过 ctx.effect() 或 ctx.on() 安装,在 reload 和 teardown 时按预期撤销。每个注册都应有对应的 disposer(资源释放函数)。 |
四种分发模式是事件的公开约定的一部分,新事件必须以 @mode 标签记录其模式,以便生成的目录能把声明与分发调用点做交叉校验:
| 模式 | 是否 await? | 分发顺序 | 是否有返回值? |
|---|---|---|---|
emit |
否 | 监听器按注册顺序观察 | 否 |
waterfall |
否 | 监听器按注册顺序观察 | 是 |
parallel |
是 | 所有监听器并行观察事件 | 否 |
serial |
是 | 监听器按注册顺序观察 | 是 |
其中 waterfall(瀑布式事件) 的语义值得多说几句,因为 dsh 的关键拦截点几乎都用它。ctx.waterfall 是环绕中间件:监听器接收 (...args, next);调用 next() 会执行下游监听器,下游返回值通过 next() 返回当前包装层,可被该层包装后继续向外返回;不调用 next() 直接返回则短路 。协作式监听器通常修改一个共享的请求或决策对象,然后委托;策略监听器在拥有决策权时可以不调用 next() 直接返回,而仅做观察的监听器则必须委托。
Cordis 还有一套 Loader 配置机制:@deepseek-ai/cordis-plugin-include 插件将 !!js 标记解析为表达式节点,Loader 在声明的注入激活后基于插件上下文插值条目的 config,并在每次挂载决策时插值其 disabled 字段。这个机制在 2.2 节的 profile 组装中会再次出现(例如按平台门控 shell 栈)。
2.2 档位(profile)与捆绑包(bundle)
2.2.1 运行中的 dsh 是一棵插件树
运行中的 dsh 是一棵插件树 ,由启动时按序叠加的各层组合而成。树的每一个「条目」(entry)指向一个可加载的插件,并携带其 config;多个条目可以引用同一个插件包,以不同的配置或禁用状态出现。
两个一等公民概念支配着这棵树的组成:
- 档位(profile) :存放在 Harness home(
$DSH_HOME,默认~/.dsh)中的具名组装 。一个 profile 是一个~/.dsh/profiles/<name>目录,包含一个package.json(声明树外插件依赖与 profile 清单)和用户自己的cordis.patch.yml。它做三件事:列出自己叠放哪些捆绑包、存放自己安装的树外插件、保存用户自己的 patch 文件。 - 捆绑包(bundle) :Cordis 配置项及其挂载代码的分发格式 。一个捆绑包就是一个 npm 包,它的 patch 文件描述要往树里插入哪些条目;因为是配置项的形式,它插入的内容始终可被其上各层 patch。
两者都在各自的 package.json 中通过 dsh 字段声明自己:dsh.profile 列出一个 profile 的捆绑包列表(dsh.profile.bundles),dsh.bundle 指向一个捆绑包的 patch 文件(如 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } })。profile 组合器通过 manifest 的 dsh.bundle.patch 字段解析 patch,绝不通过代码导入。
2.2.2 随发行版交付的捆绑包分层
发行版自带一个基础捆绑包和两个表层捆绑包,构成典型的三层结构:
| 捆绑包 | 定位 | 主要贡献 |
|---|---|---|
dsh-base |
每个 profile 的第一层,共享 dsh 核心 | 模型适配器(DeepSeek 等)、工具、持久化、沙箱与审批策略、设置、凭据、遥测、核心 spawn/fork subagent 提供方。它以一次 insert 在空的 profile 根之上插入全部基础插件行,自身不贡献任何模型可见文本,也没有运行时 API。 |
dsh-web-app |
浏览器应用表层,叠加在 dsh-base 之上 |
设置 coding persona,插入 Web 宿主行(webserver、API 网关、workspace、投影缓存、存储)、浏览器插件名录与客户端插件重载链(dsh-client-hmr),并挂载 web-runtime 粘合插件(配置项含 openBrowser、printUrl、surfaceContext、trustedHosts)。普通 web-startup 提供方注入 ctx.cmdlineArgs,解析 --host、--port、可重复的 --trusted-host、--no-open 等参数。 |
dsh-headless |
一次性任务表层,与 web 同级、同样叠加在 dsh-base 之上 |
提供编码 persona 和工具模式、禁用 HMR(热模块替换)、挂载 headless-runner 插件(配置项为 {task})。不挂载任何 Host、HTTP server、Web runtime 或浏览器插件 ------runner 通过 ctx.agents 创建一个全新持久化 agent,把任务作为普通用户消息提交,等待完全停稳后把最后一条非空 assistant 文本写入 stdout,再经 ctx.appExit 请求退出(最终 turn/end 正常完成 → 退出码 0,否则 1)。 |
两个值得注意的设计细节:
dsh-base的 patch 会按平台门控 shell 栈 :bash-sandbox/tool-bash行携带disabled: !!js process.platform === 'win32',其孪生行pwsh-sandbox/tool-pwsh以取反的表达式仅在 win32 挂载------同一份 patch 文件,每个宿主恰好挂载一个 shell 栈。- patch 替换整行
config,不做深度合并 。因此某个按模式取值不同的行不会放在dsh-base里,而应放在各模式捆绑包中,由模式捆绑包重述完整配置。用户覆盖同理:必须重述该行需要保留的每个字段。
可选的 Codex 与 Claude Code subagent 提供方不属于 dsh-base 及其生产依赖闭包;profile 仅在需要时通过 dsh plugin --profile <name> add ... 安装对应的产品 provider 捆绑包,重启后生效。
2.2.3 叠加顺序与 patch 语义
各层按固定顺序应用在空条目列表之上:
text
空条目列表
→ 按 profile 列出的顺序逐个应用每个捆绑包的 patch
→ 应用 profile 的 cordis.patch.yml
→ 应用 home 级的 cordis.patch.yml(~/.dsh/cordis.patch.yml)
→ 应用任意 --patch overlay
后应用者优先级更高(最后写入者按行获胜)。一条 patch 的两种操作:
- 按 id 定位 某个条目并替换其整个
config; insert插入新条目。
!!js 表达式在挂载时插值,可用于环境选择(如平台门控)。如果 patch 指定的条目 id 不在组合后的树中,会输出一条 stderr 警告。
每次 profile 启动都会持续监视 cordis.patch.yml 的变更(watchUserPatches):变更串行处理,按层次顺序重新组合用户 patch 层(捆绑包层在下、overlay 在上);读取或解析失败时,最后一个可用树继续运行。
2.2.4 查看你机器的实际启动树
要查看你的机器实际启动的配置树,执行:
sh
dsh --profile web --dump-config
这条命令打印出 profile 组合器结算后的完整条目树。它有一个重要含义:它打印出的任何条目,都可以由你自己的 patch 替换 。换句话说,--dump-config 的输出就是完整的「可替换面」------发行版交付的每一行都在你的覆盖范围之内。
组装机制的完整文档在 packages/boot/app-boot 的 README(Profile 一节);每个可配置字段的完整参考见 docs/config-catalog.zh.md(插件配置目录,由 pnpm run gen-config-catalog 从源码生成)。
2.3 核心包
以下是向 Cordis 树贡献内容的部分核心包。它们共同构成 dsh 的「骨架」------注意,即便这个骨架本身也是插件,只是恰好随发行版交付。
| 包 | 职责 | ctx 键 |
|---|---|---|
core/session |
仅追加的 SessionEvent 日志和内存存储 |
ctx.sessions |
core/system-prompt |
提示词片段与工具 schema 的组装 | ctx.systemPrompt |
core/tools |
作用域化的工具注册表和带把关的执行流水线 | ctx.tools |
core/agent |
Agent 接口、活跃 agent 注册表和 agent/* 事件 |
ctx.agents |
core/agent-loop |
实现该接口的默认驱动器 | ctx.agentLoop |
core/scope |
按 agent 划分作用域的注册原语 | 库,无 ctx 键 |
llm/llm |
消息与流式词汇表,以及适配器 seam | ctx.llm |
2.3.1 core/session:事件溯源的会话日志
dsh-session 是 Session 类与 SessionStore 服务所在的包。Session 是 agent 全部交互历史的仅追加真源 ,采用事件溯源(event sourcing):LLM 消息历史由日志派生 而非独立维护。原始日志之上维护一个 surface 层(产生消息事件的有序投影),用于高效派生和压缩(compaction)。
关键 API 包括:
ctx.sessions.create(id?, { seed?, meta? }?):创建并发布会话;ctx.sessions.flush(session):通过会话捕获的作用域分发一个需等待完成的并行持久性检查点;ctx.sessions.fork(source, boundary?, childSessionId?):fork 会话------选取截至boundary事件序号(含该事件,默认为当前最后一个事件)的前缀,要求所选前缀结束时没有开放轮次,再创建带谱系元数据的子会话;session.append(type, data, opts?):追加持久事件(快照、冻结、校验、同步提交);session.deriveMessages():对每个新的 surface 条目做增量投影,返回完整、带标识且冻结的消息数组。
这个包有意不实现持久化 :持久化是另一个 seam(ctx.sessionPersistence),插件订阅 session/event、在 session/flush 时刷新即可。这个拆分让「日志形态」与「存储介质」彻底解耦------JSONL 与 SQLite 后端是平替的。
2.3.2 core/system-prompt:提示词组装注册表
dsh-system-prompt 是一个组装注册表 :插件可以向它贡献有序段(section)、工具 schema 和具名变量(variable);循环在每个步骤组装一次,把结果渲染为完整的模型提示词。该包拥有静态 harness 身份和全局部署 persona;agent 作用域的 persona 会遮蔽全局默认值。
注册接口按调用上下文的作用域分层:
ctx.systemPrompt.section(section):贡献一个段;complete: true的段在组装 waterfall 之后成为精确的完整提示词(多个 complete 段并存会被拒绝);ctx.systemPrompt.context(context):贡献有序动态上下文,每次符合条件的组装都会求值;ctx.systemPrompt.tools(provider):贡献工具 schema,每次组装用该次组装的上下文求值;ctx.systemPrompt.variable(name, provider):贡献{``{name}}形式的提示词变量。
配置层面有两个关键项:persona(全局部署 persona 模板,渲染为顺序 0 的 deployment:persona 段,支持 {``{...}} 变量插值)与 toolOrder(显式指定面向模型的工具顺序列表,必须恰好包含一个 '<unlisted-tools>' 其余项标记)。工具顺序之所以采用中心列表而非每插件权重,是为了让「模型看到的工具列表」成为一个可审计、可 patch 的单点。
组装本身经过一次 system-prompt/assemble waterfall:监听器可以改写段与工具列表,其返回值具有权威性。
2.3.3 core/tools:工具注册表与执行流水线
dsh-tools 是作用域化的工具注册表和带把关的执行流水线所在。工具插件注册各自的 schema 和执行器;agent loop 依次让每次工具调用经过:
text
tools/pre-execute (可扩展的允许/拒绝门禁)
→ 已注册的单调守卫(guard)
→ tools/execute (供超时/重试/指标插件使用的环绕分发包装层)
→ tools/post-execute (检查/替换结果、附加上下文)
→ 由工具定义持有的 finalizeContent 边界
→ 仅观测的 tools/result 通知
流水线有两个关键性质值得强调:
- 门禁与守卫的单调性 :
tools/pre-execute之后注册的执行守卫,返回理由即拒绝调用、返回undefined则保持原决定;后续 waterfall 监听器无法将守卫的拒绝重新变为允许。 - 呈现模式(mode) :注册表决定工具以何种方式向模型呈现------
native(原生 Function Calling,默认)、code(Code Mode,提供保留的run_code传输与生成的tools:sdk段)或both。单个 agent 可用ctx.tools.presentAs(mode)为自己遮蔽全局默认值。Code Mode 下模型直接调用其他任何工具会在策略运行前被解析为UNKNOWN_TOOL。
注册作用域同样由调用上下文决定:普通插件上下文全局注册;agent.ctx 注册的只对那个 agent 生效并遮蔽同名全局工具;ctx.tools.restrict(filter) 提供 agent 作用域的允许/拒绝掩码(注意:这是实时可见性组合,不是权限边界)。
2.3.4 core/agent:接口与注册表
dsh-agent 定义 Agent 接口、活跃 agent 注册表(ctx.agents)与 agent/* 实时事件词汇。它的设计目标是:每个插件(UI、钩子、编排器)都面向 Agent handle 编程,而不依赖具体循环包------因此循环本身可以替换。
注册表的核心能力:
ctx.agents.register(agent)/get(id)/list()/roots():实时 agent 的增查列举;ctx.agents.currentInitiator()/withInitiator(agent, operation):发起方 agent 作用域 ------AgentLoop在发起方边界内运行每个驱动器的完整生命周期,并发驱动器彼此隔离,子驱动器的 continuation 携带子 agent;ctx.agents.setFactory(factory):注册创建工厂。dsh-agent-loop通过它把自己注册为工厂,于是消费方只需面向ctx.agents.create()/ctx.agents.resume()编程,完全不需要导入循环包。
Agent.ctx 是 agent 的作用域上下文(由 dsh-scope 提供,键 = 该 agent)。通过它注册的工具/段/变量/监听器只对该 agent 生效,并在 agent dispose 时全部撤销------这是「让某个会话拥有不同能力集合」的基础机制。
2.3.5 core/agent-loop:唯一的循环实现
dsh-agent-loop 是 agent 的唯一具体实现插件和循环驱动器 ,其包内部实现满足 Agent 接口,并驱动会话、轮次和步骤的生命周期。这是 harness 中唯一包含具体循环逻辑的包,文档里有一句明确的原则:
其他所有内容要么是抽象服务,要么是针对扩展点的插件:新行为应放入插件,而不是这里。
创建与恢复属于同一个受回滚保护的事务 :构造私有会话、具体 agent 和带作用域的上下文;等待可选 setup;进入会话与 agent 两个注册表;依次宣告 session/created 和 agent/created;发出 agent/session-start;此后才启动驱动器。任何一步失败,失败方都会回滚其私有会话/作用域/驱动器。
所有权模型是「调用方 fiber 与 AgentLoop 提供方共同拥有 agent」:调用方卸载、handle 的 dispose() 或提供方卸载都会汇合到同一个记忆化的完全停稳边界(停止循环 → 等待退出 → 注销 agent → 从存储移除会话 → 撤销作用域)。
2.3.6 core/scope:作用域注册原语
dsh-scope 是支撑上述「按 agent 划分作用域」的底层库,没有 ctx 键。核心 API:
createScope(ctx, key):创建一个带标签的 Cordis 上下文,其底层 fiber 拥有通过该上下文进行的每项注册;- 键可以构成可选的父链(
bindScopeParent),形成两条方向相反的路由规则:- 注册视图沿链向下继承------子作用域看得见祖先各层,近者遮蔽远者;
- 事件放行沿链向上扩展------标签为祖先的监听器能收到子孙键的事件,反向永不成立;
scopeTarget(base, key):为按作用域筛选的事件构造不透明分发载体,让无作用域监听器保持全局可见,标签匹配的监听器才收到事件。
agent loop 为每个存活的 agent 创建一个作用域;agent preset 的常驻挂载则是其各 agent 的父作用域。该机制与键的具体含义无关,底层包无需依赖两者即可使用。
2.3.7 llm/llm:模型词汇与适配器 seam
dsh-llm 是提供方无关的 LLM 词汇与抽象服务:本包定义 agent loop、会话日志和所有插件共同使用的规范词汇(消息、流式 chunk 等)。
LlmRuntime 服务(ctx.llm)是「适配器注册表 + 单一流式调用接口」,可通过 waterfall 拦截:
ctx.llm.registerAdapter(providers, adapter):为给定的提供方路由注册适配器;注册要么全部成功要么全部不生效;句柄的replace(providers)在一次同步操作中替换路由,不出现可观察的空档;ctx.llm.registerConfigurableProviders(entries):声明可通过配置激活 的提供方路由(休眠态)------这正是 Web UI 的 Models 页面背后的机制:settings 文档中的llm-pi-ai:分节一旦提供 provider profile,对应路由即刻注册为存活,分节清空则回落为休眠;ctx.llm.discoverModels(settingsNs, request)/listModels(provider):询问端点公布的模型;ctx.llm.providerRetryPolicy(provider):返回注册时捕获的重试策略。
一个重要的职责划分:dsh-llm 存储有效策略,但不执行重试 ------重试是 dsh-llm-retry 插件的职责。省略提供方配置时使用有界 normal 模式,在首次请求后最多重试五次。
2.4 事件模型:三个领域
事件就是扩展点,而选对事件域是大多数改动的第一个决定。dsh 的事件分属三个领域:
2.4.1 会话事件(持久事实)
会话事件是追加到会话日志并通过 session/event 广播的持久事实 。turn/*、step/*、user/message、assistant/*、tool/* 都属此类。它们的共同特征是:当某个事实必须在重新加载后仍然存在时,就使用会话事件。会话事件是 fork、恢复、transcript(文本记录)、遥测和持久化的共同来源------2.6 节会展开讲它为什么是模型的上下文来源。
2.4.2 agent 域事件(agent/*,实时控制)
agent 事件携带活跃 Agent,覆盖 inbox、步骤、状态、请求、验证与续跑:agent/created、agent/inbox/*、agent/pre-step、agent/request、agent/request-error、agent/turn-stopping 等。它们不落入会话日志 ,是进程内实时扩展点。要观察或拦截进行中的工作时,使用它------拦截器通常修改一个共享的请求或决策对象再委托(waterfall 语义),或作为 terminal checkpoint 直接终止流程(serial 语义)。
2.4.3 能力事件(无需导入循环)
能力事件无需导入循环即可向某个 seam 附加策略和适配器,典型如 fs/*、tools/*、telemetry/*。例如给文件系统加一个审计策略,只需监听 fs/* 事件,不必触碰 agent loop 的任何代码。
三个领域的选择可以浓缩成一句话:
需要跨重启存活 → 会话事件;需要拦截进行中的轮次/步骤 → agent 域事件;需要给某个能力面附加策略或实现 → 能力事件。
docs/event-producer-consumer.zh.md(事件映射)列出了每个事件的生产方与消费方,是排查「谁在发、谁在听」的索引。
2.5 轮次(turn)与步骤(step)流程
2.5.1 两个基本单位
- 一个步骤(step) :一次模型请求加上它调用的工具。
- 一个轮次(turn) :包含零个或多个步骤;它在领取首条输入之前打开,并在不再欠下任何工作时关闭。
「零个步骤」是合法状态:当领取的首条输入被 agent/pre-step 拒绝、或被改写为空时,轮次会关闭而不含任何步骤------但日志仍会记录这次尝试(一个不含步骤的持久轮次),因此「模型收到了什么」永远是可审计的。
2.5.2 完整流程
text
turn/start
claim next-step input plus one queued message
assemble prompt sections + tool schemas
-> agent/pre-step reject | enter(messages)
reject, or a first enter rewritten empty -> close the turn with no step
step/start
append entered messages as user/message
derive model history from the log
agent/request -> llm/stream -> assistant/chunk* -> assistant/message
tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result*
step/end
tools owe another request, or next-step input arrived -> claim -> next step
-> agent/turn-stopping
turn/end
逐段解读:
turn/start:轮次打开。驱动器从 inbox 领取「下一步骤输入 + 一条排队消息」。- 组装 :从插件注册的提示词片段与工具 schema 组装提示词(经
system-prompt/assemblewaterfall)。 agent/pre-step(waterfall):决定模型看到什么。监听器可以改写已领取的消息,也可以直接拒绝它们;首个领取被拒绝或改写为空时,关闭一个不含步骤的轮次。step/start→user/message:被放行的消息作为user/message会话事件追加进日志;模型历史从日志派生。- 模型请求 :
agent/request(waterfall)→llm/stream(waterfall)→ 流式assistant/chunk*事件 → 收尾的assistant/message事件。assistant/message会记录每次成功的提供方调用,包括返回空内容或以max-tokens结束的调用;空内容不进入派生历史,但持久事件仍保留用量与sourceEventSeqs指向。 - 工具执行 :每个
tool/call依次经过tools/pre-execute→tools/execute→tools/post-execute→tool/result。 step/end:若工具要求再来一次模型请求(如工具结果需要模型消化),或有新的下一步骤输入到达,则再次领取、进入下一个步骤。agent/turn-stopping(serial,terminal checkpoint):在本可完成的轮次关闭前运行;它没有next(),监听器直接返回决策。turn/end:轮次关闭,驱动器回到等待。
2.5.3 事件域与分发模式的分布
把流程图中的事件按域和分发模式分类:
| 事件 | 域 | 分发模式 |
|---|---|---|
turn/start、turn/end |
会话(持久) | 经 session/event 广播 |
step/start、step/end |
会话(持久) | 经 session/event 广播 |
user/message、assistant/chunk*、assistant/message |
会话(持久) | 经 session/event 广播 |
tool/call*、tool/result* |
会话(持久) | 经 session/event 广播 |
agent/pre-step |
agent 域(实时) | waterfall (监听器须调用 next() 委托) |
agent/request |
agent 域(实时) | waterfall |
agent/request-error |
agent 域(实时) | waterfall(恢复决策:返回 { kind: 'retry' } 且不调 next()) |
agent/turn-stopping |
agent 域(实时) | serial (无 next()) |
llm/stream |
能力域(实时) | waterfall |
tools/pre-execute / tools/execute / tools/post-execute |
能力域(实时) | waterfall |
一个易错点:waterfall 的监听器必须调用 next() 才能把请求委托下去;忘记调用等价于短路整个下游。serial 事件则相反,它本来就是终点检查点,没有委托概念。
2.5.4 输入通道:inbox
输入通过同一个 inbox 到达驱动器。有些消息会立即唤醒它;注入的上下文 (如 agent.inject() 提交的内容)会留在 inbox 中,直到另一条消息将驱动器唤醒------即注入的上下文总是落到下一次获准的请求中。
2.5.5 错误恢复与压缩
模型请求失败时走 agent/request-error(waterfall):拥有恢复权的监听器返回重试动作,否则保留原始错误。dsh 的上下文压缩(compaction,dsh-compaction-basic)则通过 agent/pre-step 处理上下文压力:任一触发条件满足后,先执行可选的工具结果剪枝,再选择摘要;恢复发生在失败步骤结束之后、失败轮次结束之前。
更完整的时序图见 docs/agent-lifecycle.zh.md,工具流水线细节见 docs/tool-execution-pipeline.zh.md,取消与错误恢复见 docs/subsystems/core.zh.md(the agent handle 一节)。
2.6 会话日志与派生态
2.6.1 会话日志是模型所见上下文的来源
会话日志(2.3.1 节的仅追加 SessionEvent 日志)是模型所见上下文的唯一来源 :deriveMessages() 从中投影出模型历史;原始 assistant/chunk 事件则保证回放和 UI 保真(逐 token 的流式重现)。fork、恢复、transcript、遥测和持久化全部派生自该事件流------不存在第二条「真实的历史」。
2.6.2 模型可见即已记录
dsh 有一条核心运行时不变量:
模型可见即已记录。 抵达模型请求的一切都必须能从日志重建,并由一项运行时不变量断言这一点。
这条不变量的工程含义非常直接:新增一项模型可见输入,就需要新增一个会话事件 ------具体操作是扩展 SessionEventMap(会话事件的类型联合),并从日志渲染该事件。提示词组装的动态上下文(ctx.systemPrompt.context 的贡献)、工具可见性变化、persona 切换......凡是模型在请求中「看到」的东西,都必须以某种事件形式留在日志里。
这一设计带来的收益:
- 可回放:任何一次模型请求的输入都能从日志精确重建,调试与审计不再依赖进程内状态;
- fork 有边界 :
ctx.sessions.fork(source, boundary)的切点就是日志中的一个事件序号,且要求该前缀没有开放轮次; - UI 与模型永远一致:Web GUI 从同一条事件流渲染,用户看到的和模型看到的天然同源。
2.7 能力接缝(seam)
2.7.1 三角色模型
一个 seam(能力接缝) 是一项可替换能力,包含三种角色:
| 角色 | 职责 | 例子 |
|---|---|---|
| Service Definition(服务定义) | 声明接口:键、方法签名、配置 | ctx.llm 的适配器注册表接口 |
| Service Provider(服务提供方) | 实现该接口 | dsh-llm-deepseek、dsh-llm-pi-ai、远程沙箱提供方 |
| Consumer(消费方) | 使用该接口,通常是面向模型的工具 | agent-loop 消费 ctx.llm 发起请求 |
两个容易误解的点:
- 一个包可以合并承担多个角色 ,但单一角色本身不是 seam------只有接口、实现、使用三者齐备,才构成一项可替换能力;
- 添加一项能力意味着把三者一并设计。先定义接口(Definition),再决定默认实现(Provider),最后明确谁来消费(Consumer),缺一不可。
docs/capability-seams.zh.md(能力图)由脚本从源码生成,展示了拥有服务声明的包、已知实现包与直接消费该服务的包之间的关系图。
2.7.2 为什么 seam 能改变整个产品
seam 正是「替换一个提供方就能改变整个产品」的原因。一个典型例子是执行世界 :文件系统与进程提供方共享同一个执行世界,因此把 ctx.fs / ctx.subprocess 指向远程沙箱,就把 Bash、PTY 和 LSP 一并搬了过去,无需为提供方做专用 fork------因为消费方(工具)只面向接口编程,它们不知道也不关心执行发生在本地还是远端。
subagent 提供方 是另一个例子。ctx.subagents 之后千差万别的实现可以共存:
| 提供方 | 行为 |
|---|---|
subagent-spawn-in-process |
启动全新的进程内子 agent |
subagent-fork-in-process |
从父 agent 已完成的历史记录启动进程内子 agent |
subagent-acp |
通过 ACP(Agent Client Protocol)启动进程外子 agent |
subagent-codex |
启动真实的 Codex app-server 子 agent |
subagent-claude-code |
通过官方 Claude Agent SDK 启动真实的 Claude Code 子 agent |
subagent-dsh-sdk |
通过 TypeScript SDK 启动进程外 Harness 子 agent |
它们都注册到同一个 ctx.subagents 键,对 tool-subagent 工具而言完全同构。实验性的 Agent Teams 则是在 ctx.agentTeams 上的私有显式启用协作 seam,在可继续 subagent 之上提供持久 roster、任务板和 mailbox。
2.8 「新行为放哪里」决策表
当你想给 dsh 增加新行为时,第一问永远是「它该落在哪个扩展点」。下表把常见目标映射到机制(改动循环本身时,本表随文档更新):
| 目标 | 机制 |
|---|---|
| 添加模型提供方 | 在 ctx.llm 上注册其适配器 |
| 添加面向模型的能力 | 在 ctx.tools 上注册;其 schema 加入提示词组装 |
| 让某个会话拥有不同的能力集合 | 组装一个 agent preset;其中的服务行需要 isolate realm |
| 添加 shell 执行 | 注册 ctx.shell 后端;本地后端通过 ctx.subprocess spawn 进程 |
| 添加持久化终端执行 | 注册 ctx.terminals 后端和 dsh-tool-terminal |
| 添加用户命令 | 在 ctx.commands 上注册;它无需模型轮次即可分派 |
| 添加后台工作 | 在 ctx.jobs 上注册;job_* 工具负责收集或停止 |
| 添加文件系统访问或策略 | 注册 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.sessions.fork(source, boundary?, childSessionId?) |
| 将注册项限定到单个 agent | 使用该 agent 的 agent.ctx |
使用这张表的几个实践提示:
- 区分「注册」与「监听」。直接能力调用(注册提供方/工具/命令)用服务方法;拦截和策略(改写、审批、审计)优先用事件。
- 注意作用域 。同一注册接口在普通上下文与
agent.ctx下语义不同:后者只影响单个 agent 并遮蔽同名全局注册。 - 每个注册都要想好 disposer。插件卸载时副作用必须能干净撤销;如果 teardown 顺序有要求,把相关工作放在同一个 effect 中。
- 模型可见的东西必须过日志 。如果你新增的机制会让模型看到新内容,先扩展
SessionEventMap(2.6.2 节的不变量)。
分步实操(添加一个包、一个工具、一个 LLM 适配器、一个 Chat 节点、一张设置卡片)见 docs/cookbook/extension-cookbook.zh.md(扩展实操手册),它把功能映射到能力并索引各分步指南。
2.9 本章小结
本章的核心论断只有一句:dsh 没有特权内核,一切能力都是挂载在 Cordis 上下文上的插件,一切替换都发生在配置层。围绕它,本章给出了五张地图:
| 地图 | 回答的问题 |
|---|---|
| 档位/捆绑包叠加顺序(2.2.3) | 我机器上实际跑的是哪棵树?我能在哪一层覆盖? |
| 核心包表(2.3) | 关键服务由哪个包提供、占据哪个 ctx 键? |
| 三事件域(2.4) | 我的改动该用哪种事件? |
| turn/step 流程(2.5) | 一次模型交互中,哪些事件是持久的、哪些是实时的、哪些能短路? |
| 新行为决策表(2.8) | 从目标到扩展点,一步定位。 |
下一章将沿着这些接缝深入具体子系统------工具流水线、模型适配器、会话持久化与压缩------演示如何在这张地图上动手。
附录 A:本章术语速查
| 英文 | 中文 | 备注 |
|---|---|---|
| agent | 智能体 | 本文统一用「智能体」 |
| session | 会话 | 日志与持久化的单位 |
| plugin | 插件 | Cordis Service 的实现 |
| tool | 工具 | 面向模型的可调用能力 |
| turn | 轮次 | 零个或多个步骤 |
| step | 步骤 | 一次模型请求及其工具调用 |
| event | 事件 | 分持久会话事件与实时事件 |
| provider | 提供方 | seam 的实现角色 |
| model | 模型 | LLM 实例 |
| profile | 档位 | Harness home 中的具名组装 |
| bundle | 捆绑包 | 配置项 + 挂载代码的分发格式 |
| seam | 接缝(能力接缝) | 可替换能力的三角色结构 |
| LLM | 大语言模型 | 首次出现标注全称 |
| waterfall | 瀑布式事件 | 监听器须调用 next() 委托 |
| effect | 副作用 | 可逆的注册,随插件卸载撤销 |
| disposer | 资源释放函数 | effect 的撤销端 |
| fork | 分叉 | 从事件边界派生子会话 |
| compaction | 压缩 | 上下文压力下的摘要/剪枝 |
附录 B:相关文档索引
| 主题 | 文档 |
|---|---|
| Cordis 框架入门 | docs/cordis-primer.zh.md |
| Cordis 教程 | docs/cordis-tutorial/ |
| 配置字段目录 | docs/config-catalog.zh.md |
| 事件生产者-消费者映射 | docs/event-producer-consumer.zh.md |
| turn/step 时序图 | docs/agent-lifecycle.zh.md |
| 工具执行流水线 | docs/tool-execution-pipeline.zh.md |
| 能力 seam 图 | docs/capability-seams.zh.md |
| 子系统参考 | docs/subsystems/(core / session / tools / llm-streaming / subagent 等) |
| 扩展实操手册 | docs/cookbook/extension-cookbook.zh.md |
| 组装机制(app-boot) | packages/boot/app-boot/README.zh.md(Profiles 一节) |