图鉴
subagent 家族、Ralph、goal / plan / preset、skill、沙箱与信任、credentials 分层。
图鉴式的一页:每个子系统统一四问——这是什么 → 解决什么问题 → seam 怎么切 → 源码坐标。目的是让你在需要的时候查得到,而不是逼你现在读完。
读法建议:先扫「seam 怎么切」那一栏。看一个项目怎么切它的能力边界,比看它实现了什么功能,信息量大得多。
5.1 subagent 家族:把别人家的 agent 当子进程
11 个包,是全仓第三大的分组。ctx.subagents 上可以同时挂多个具名 provider——它们的差异之大,正是这个 seam 的价值所在:
| Provider | 子代理是什么 |
|---|---|
subagent-spawn-in-process | 进程内起一个全新的孩子 |
subagent-fork-in-process | 进程内从父代理已完成的历史分叉一个孩子 |
subagent-acp | 通过 ACP 起一个进程外的孩子 |
subagent-dsh-sdk | 通过 TypeScript SDK 起一个进程外的 harness 孩子 |
subagent-codex | 起一个真实的 Codex app-server 孩子 |
subagent-claude-code | 通过官方 Claude Agent SDK 起一个真实的 Claude Code 孩子 |
「进程内新起一个孩子」和「驱动一个别家产品的完整 agent」,在这个接口后面是同一件事。architecture.md 拿这个当 seam 价值的例证:「subagent provider 的差异之大不亚于此,从一个全新的子代理,到另一个产品里的一次被委派的 turn。」
Codex 和 Claude Code 这两个包是独立的可选 bundle,不进 dsh-base。所以 @deepseek-ai/dsh 的默认生产依赖闭包里,既没有这两个 provider,也没有 Claude Agent SDK,也没有 Codex 的 wrapper 和平台负载。装了才有,删了就只撤走它自己那一份私有运行时闭包。
5.2 Ralph:一个「没有改 loop」的编排策略
ralph({ objective, maxRounds? }) 给一串全新的子代理一个不可变的目标,一轮一轮跑下去。每一轮起一个新孩子,孩子拿到的只有:不可变目标、当前轮次与上限、一句「共享工作区才是权威」的指令,以及上一轮的结构化交接报告。父对话和之前孩子的会话都不会被种进去。
README 第一段就把话说死了:它「以一个普通插件的形式,在 ctx.workflowEngine 和 ctx.subagents 之上演示了一种专门化的编排策略:agent-loop 里没有加任何 Ralph 模式或 fresh-agent 循环,同会话的 goal 域也保持独立。」
这是对 M2 那条断言的又一次检验——一个足够特殊的编排策略,能不能不碰主循环就实现出来?能,那这个架构就是真的。
几个设计细节值得单看:
- provider 由部署配置携带,不由脚本挑。配置里的
subagentProvider通过WorkflowStartRequest传下去,「固定脚本无法检视或改变路由,而普通的、由模型编写的workflow工具也不会因此多出一个 provider 选择器」。 - 无效报告直接失败,不截断。「无效、缺失或超长的报告会让 workflow 失败,而不是被截断、或被误当成额度耗尽。」
- 完成状态是「工人自己说的」。渲染出来的完成与阻塞标签「明确写明这是一个 worker 报告的结果,而不是独立认证」。
5.3 goal / plan / preset:三种「状态」,三种切法
active / paused / blocked / complete 相位和一个轮次上限。它是状态,不是调度器,也不是另一段对话;session log 仍然是它的真相来源。最有意思的是 goal activation:这个「允许再跑一轮」的许可是进程本地的,刻意不进持久重放——所以 resume 和 fork 之后,必须有一次人授权的恢复动作,才会重新开始自动工作。 packages/goal/
这句话本身就是一次克制的示范:并不是所有东西都该做成 seam。决策记录的分类是
simplification——它是把一个更泛化的设计收窄回来的结果。
packages/plan/plan-mode/
agent.cordis.yml 的目录。把它挂在某个 agent 的 scope context 下,这个 session 就有了自己的工具和 prompt section,而其它活着的 session 各保各的——一个进程里可以同时跑几个组合完全不同的 agent。护栏也写死了:一个 preset 如果指名了一行会发布进程全局服务的插件,会在挂载时被拒绝,而不是允许它和下一个 session 撞车。 packages/preset/
skill)+ 本地文件系统发现(skill-filesystem)+ 内置徽章技能(skill-badge)+ 面向模型的目录与加载器(tool-skill)。README 里点了它的定位:「这个能力留在核心控制主干之外,可以用本地、内嵌或远程的 provider,而不改变面向模型的契约。」——同样的技能概念,Claude Code 是产品内建,这里是一个可换后端的 seam。 packages/skill/
5.4 沙箱与信任:一条画得很清楚的线
这里最值得学的是分层的方式:
sandbox(定义 + 共享的提权词汇)、sandbox-local(本地平台限制后端)、sandbox-policy(按 session 解析持久策略)——「策略」和「机制」被拆成了两个包。- 跨家族的那一步:
fs-sandbox让ctx.fs的写入也被同一份策略围栏。architecture.md 给了这么做的理由:「文件系统和子进程 provider 共享同一个执行世界,所以把它们指向一个远程沙箱,就把 Bash、PTY、LSP 一起搬过去了,不需要 fork 任何 provider。」 - Windows 上不是降级放行,而是走
sandbox-windows-acl的受限令牌 runner,「权限面和 POSIX 上完全一样」。
5.5 credentials:配置里放引用,不放密钥
三个包,切法很干净:credentials(引用与记录的 seam)、credentials-local(环境变量与本地文件 provider)、authorization(那些必须问人才能拿到凭据的流程,比如 OAuth)。
核心规则一句话:「配置携带的是引用,不是密钥值。」consumer 在自己的操作边界上解析这些引用。而两个 seam 的关系被规定得很死——「一个 authorization 流程写出一条凭据记录,并以它为键;这两个 seam 在记录这里相遇,别处不相遇。」
层次上,托管凭据住在 $DSH_HOME/.credentials.yaml;留在 .env 里的凭据仍然是一个优先级更低的兜底。这个「不强制迁移、但明确排序」的处理方式,比一刀切要现实得多。
5.6 还有一批,按需查
dsh-shell 是 Service Definition,bash-local / bash-sandbox 是 provider,tool-bash 是 Consumer;pwsh 一侧完整对称。base bundle 按平台用 disabled: !!js process.platform === 'win32' 各挂一套。
packages/shell/
tool-fs、tool-fs-search、tool-str-replace-editor)。fs-observation-policy 是个容易被忽略的角色:它让「模型有没有真的读过这个文件」变成可查询的事实。
packages/fs/