本文基于
deepseek-harness仓库0.1.0-rc.5版本源码与官方文档编写。文中所有路径均为仓库内相对路径(如packages/core/agent-loop),不涉及任何绝对路径。
一、概述:DeepSeek Harness 是什么
DeepSeek Harness(命令行命令 dsh,npm 包 @deepseek-ai/dsh)是 DeepSeek AI 开源的 Agent 运行时(agent harness),采用 MIT 许可证。它不是一个聊天机器人前端,而是一个可以承载各种 Agent 形态的运行时框架:Web UI、无头模式、插件系统、子代理、任务队列、沙箱执行……全部以插件形式组装。
核心哲学一句话概括:一切皆插件(everything is a plugin)。包括模型适配器、工具注册表、会话日志、甚至 agent 主循环本身,都是插件。整个产品没有"特权核心"需要打补丁——扩展 dsh 的方式就是在它旁边挂载一个插件。
这个设计建立在 Cordis 插件框架之上。Cordis 的设计思想来自论文《A Programming Paradigm for Spatiotemporal Composability》。dsh 将 Cordis 源码 vendored 到仓库的 vendor/ 目录,并基于它构建了整套体系。
当前处于 developer preview 阶段,版本迭代极快,官方明确警告"会有破坏性变更"(THERE WILL BE COMPATIBILITY-BREAKING CHANGES)。分析时版本为 0.1.0-rc.5。
1.1 仓库规模
| 维度 | 数值 |
|---|---|
| 顶层代码组(packages 下的分组目录) | 37 个 |
| 实际 npm 包数量 | 约 150 个(packages/<组>/<包> 双层结构) |
| 运行要求 | Node.js ^22.19.0 || >=24.0.0 |
| Web UI 默认端口 | http://127.0.0.1:3080 |
| 许可证 | MIT |
1.2 快速上手
# 方式一:从 npm 直接运行(需要 Node.js)
npx @deepseek-ai/dsh web
# 方式二:从源码构建
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
dsh 命令入口位于 apps/cli/src/bin.ts,支持三种调用模式:profile(默认,按 Profile 启动,如 dsh web)、plugin(插件管理)、dump-config(导出当前生效的配置树,用于调试)。
二、设计哲学:Cordis 的五个核心思想
dsh 官方文档(docs/cordis-primer.md)把 Cordis 概括为五个思想:
- 插件是一个实现 Service 的对象。可以是带可选
inject和apply(ctx)的函数,也可以是Service子类;Cordis 负责把它的生命周期挂载进当前上下文。 - 上下文是服务的仓库。一个服务在上下文中占有一个稳定的键
ctx.<key>(如ctx.tools、ctx.llm、ctx.sessions),其他插件通过键查找服务,而不是 import 具体实现——这就是"面向接缝"的解耦。 - 通过
inject声明服务依赖。插件声明需要的服务,Cordis 等这些服务就绪后才加载该插件;加载顺序由服务依赖表达,而不是手工排启动顺序。 - 类型化事件通信。服务通过 TypeScript declaration merging 声明事件名,然后按
emit/waterfall/parallel/serial四种模式派发(见下文)。 - 注册是可逆的效果。提示词片段、工具 schema、适配器、provider、监听器都通过
ctx.effect()/ctx.on()安装,插件卸载时按确定性顺序回滚。
2.1 四种事件派发模式
| 模式 | 是否等待 | 派发顺序 | 有返回值? | 用途 |
|---|---|---|---|---|
emit |
否 | 按注册顺序观察 | 否 | 观察/通知 |
waterfall |
否 | 按注册顺序观察 | 是 | 环绕中间件(可拦截/改写/短路) |
parallel |
是 | 全部并行 | 否 | 扇出 |
serial |
是 | 按注册顺序 | 是 | 有序接力 |
waterfall 是 Cordis 最核心的模式:监听器收到 (...args, next),调用 next() 把(可能被包装的)结果传给下一个服务;不调用 next() 直接返回即短路。策略类监听器"短路即决策",注释类监听器必须委托。
2.2 加载器配置(!!js 表达式)
@deepseek-ai/cordis-plugin-include 把配置里的 !!js 解析为表达式节点。加载器在插件激活后(针对该插件上下文)插值 config,在每次挂载决策时(针对加载器上下文)插值 disabled 字段。典型例子:packages/bundle/base/cordis.patch.yml 中 bash-sandbox/tool-bash 带 disabled: !!js process.platform === 'win32',pwsh-sandbox/tool-pwsh 带反向表达式——同一份补丁文件,在 Windows 上恰好挂载一套 shell 栈,在 POSIX 上挂载另一套。
三、Profile 与 Bundle:运行时如何组装
一个运行中的 dsh 是一棵插件树,由启动时按有序层次组合而成(详见 packages/boot/app-boot/README.md 与 docs/architecture.md)。
3.1 概念
- Profile(配置档案):一个命名的组合,存放在 Harness home 目录。它列出要叠加的 bundles、安装的外部插件,以及用户自己的
cordis.patch.yml。web和headless作为模板随仓库提供。 - Bundle(分发包):Cordis 配置行 + 其挂载代码的分发格式。Bundle 的配置行可以被上层的 profile 补丁覆盖,保证"什么都能补丁"。
每个包在自己的 package.json 的 dsh 字段声明身份:dsh.profile 列出 profile 的 bundles,dsh.bundle 指向 bundle 的补丁文件。
3.2 分层结构
bundle 层(按 profile 列出的顺序叠加)
→ dsh-base(基础层:模型适配器、工具、持久化、沙箱、审批策略、设置、凭据、遥测)
→ dsh-web-app(Web UI)或 dsh-headless(无服务器的一次性运行器)
→ profile 的 cordis.patch.yml
→ home 层的 cordis.patch.yml
→ 命令行 --patch 覆盖
补丁按行 id 定位:整个替换一行的 config,或插入新行。注意没有深合并——覆盖某行必须重述它要保留的每个字段。
调试命令:dsh --profile web --dump-config 打印本机实际启动的配置树,其中任何一行都可以用你自己的补丁替换。
3.3 三个内置 Bundle
| Bundle | 位置 | 作用 |
|---|---|---|
dsh-base |
packages/bundle/base |
每个 profile 的第一层:模型适配器(含 dormant 的 Codex/Claude Code provider)、默认模型选择、工具集、持久化、策略、设置/凭据、遥测、宿主级子代理 provider |
dsh-web-app |
packages/bundle/web-app |
追加浏览器应用(Web UI) |
dsh-headless |
packages/bundle/headless |
无服务器的单次运行器 |
dsh-base 本身没有运行时 API——profile 组合器通过 dsh.bundle.patch 清单字段解析补丁,从不通过代码。
3.4 启动与配置分层
CLI 入口 apps/cli/src/bin.ts:launcher 只解析自己的 flag(--profile 主模式、web 别名、plugin 插件管理、--dump-config/--dump-default-config 导出配置树),其余交给 app(packages/boot/cmdline 是 launcher 交给 app 的命令行快照)。
配置来源分层(packages/boot/app-boot):继承的 env > $DSH_HOME/.env > 调用目录 .env,产出不可变环境快照(packages/util/launch-environment);bootstrap-only 名称(PATH/HOME/DSH_/XDG_ 等)禁止从 .env 设置(fail-loud)。
注册顺序:bundle 层(按序)→ profile 自身 cordis.patch.yml → home 级补丁 → --patch overlay → telemetry → agent-presets。模块解析用双锚点(安装优先)+ healProfilesModuleFallback(扁平 symlink 闭包共用同一 cordis 实例)。Harness home 默认 ~/.dsh(packages/util/home-paths,可用 $DSH_HOME 覆盖)。
3.5 进程模型
dsh 主体是单进程:主进程内嵌 web servlet 集合(packages/host/*:webserver=node:http + WebSocket、apiproxy=RPC 网关 + browser-trust 栅栏、frontend-static、plugin-inventory=Loader 状态投影、directory-picker)。动态插件在 cordis-dynamic group 下的 host-half fiber 启动(packages/extensions/cordis-host-runner)。SIGTERM(0)/SIGINT(130) → createProcessShutdown → 根 fiber dispose;日志走 ctx.logger。
四、核心包与 ctx 键
docs/architecture.md 给出了一张核心包表(packages/<组>/<包> 双层结构,包名前缀 @deepseek-ai/dsh-):
| 包 | 职责 | ctx 键 |
|---|---|---|
core/session |
只追加的 SessionEvent 日志与内存存储 |
ctx.sessions |
core/system-prompt |
提示词片段与工具 schema 组装 | ctx.systemPrompt |
core/tools |
作用域化的工具注册表与受守卫的执行管道 | ctx.tools |
core/agent |
Agent 接口、活体注册表、agent/* 事件 |
ctx.agents |
core/agent-loop |
实现该接口的默认驱动 | ctx.agentLoop |
core/scope |
按 agent 隔离的注册原语 | 库,无键 |
llm/llm |
消息与流词汇 + 适配器接缝 | ctx.llm |
4.1 全量包族总览
packages/README.md 维护了 37 个组(packages/<组>/<包>,npm scope @deepseek-ai/dsh-*)的职责与发布期望表,全部 ~150 个包。按发布期望分三类:Product(稳定 API)、POC(概念验证,如 e2b)、Support(支持设施,如 test-support/util)。下面按组列出职责:
| 组 | 职责 |
|---|---|
core/ |
产品 API 脊柱:sessions、prompts、tools、agent 服务与具体主循环(agent、agent-loop、agent-default-model、agent-tool-presentation、scope、session、system-prompt、tools) |
api/ |
Remote BFF 组装 + Typert RPC 网关(gateway、remotes) |
typert/ |
类型图生成、产物加载、运行时注册表(generator、loader、protocol、registry) |
llm/ |
LLM 能力族:抽象服务 + provider 适配器(llm、llm-deepseek、llm-pi-ai、llm-retry、token-meter) |
session/ |
持久会话数据平面:持久化接缝 + JSONL/SQLite 后端、投影接缝、日志标题、会话报告 |
session-query/ |
会话检索族:逻辑语料、有界读取、谱系、事件关系、语义过滤、SQLite 全文搜索 |
subagent/ |
子代理能力族:provider 注册表契约 + 面向模型的委托工具 + 6 个 provider |
jobs/ |
通用后台任务运行时 + job_* 控制工具 |
workflow/ |
工作流接缝、worker-thread 引擎、workflow/ralph 工具 |
goal/ |
同会话目标持久化与生命周期(goal、goal-round-driver、tool-goal、command-goal) |
plan/ |
计划协作状态(plan-mode) |
todo/ |
todo_write 工具 |
fs/ |
文件系统能力族:接缝、本地实现、模型面文件工具、bash 支撑的发现工具 |
shell/ |
Bash 能力族:执行器接缝、本地实现、模型面工具(bash-local/sandbox、pwsh-local/sandbox、tool-bash*) |
terminal/ |
持久 PTY 能力族(terminal、terminal-bash、tool-terminal) |
subprocess/ |
子进程能力族:Service Definition + 本地进程树 provider |
sandbox/ |
进程限制接缝;bwrap/Landlock/Seatbelt 后端(sandbox、sandbox-local、sandbox-policy、sandbox-windows-acl) |
code-runtime/ |
代码执行能力族:Service Definition + worker-thread provider + Code Mode Consumer |
e2b/ |
E2B providers(POC):e2b、fs-e2b、subprocess-e2b |
web/ |
Web 能力族:接缝、search/fetch provider 实现、模型面 web 工具 |
lsp/ |
LSP 能力族:接缝、通用 stdio provider、lsp 工具 |
skill/ |
技能能力族:provider 注册表、本地 provider、模型面 catalog/loader(skill、skill-filesystem、skill-badge、tool-skill) |
compaction/ |
压缩能力族:Service Definition + basic provider + 命令消费者 |
spill/ |
溢出能力族:存储接缝、本地实现、工具结果溢出策略 |
context/ |
模型可见请求上下文:workspace 指令、时间上下文、tmux 上下文、跨会话引用 |
attachment/ |
持久附件身份、校验、本地内容寻址存储 |
preset/ |
按会话从 preset cordis.yml 组合 agent(agent-presets、persona) |
guard/ |
循环卫生守卫:重复调用提醒 + tools/execute 截止线强制(timeout-policy、repeat-tool-reminder) |
interaction/ |
人类协作面:approval/interaction 接缝、权限预设、命令、ask-user 工具 |
bundle/ |
可安装的 dsh --profile 补丁层(base、web-app、headless) |
extensions/ |
agent 运行时自修改:live 插件/服务检查 + 模型写插件挂载/卸载(cordis-host-runner、cordis-client-runner、tool-cordis、ui-cordis) |
hooks/ |
钩子桥 + 共享 Claude Code / Codex 线协议库 |
settings/ |
用户设置接缝 + 文件后端 |
credentials/ |
凭据引用接缝 + env-over-.env provider |
storage/ |
非会话存储枢纽 + 后端 + 域形式(storage-domain、storage-json、storage-sqlite) |
workspace/ |
工作区实体 |
sdk/ |
进程外运行时 SDK:JSON-RPC 协议、TypeScript client、server 插件 |
acp/ |
仅自动化 Agent Client Protocol server |
boot/ |
共享 app-bin 启动胶水(app-boot、cmdline) |
host/ |
Web-GUI 宿主半端:API 网关 + HTTP 路由服务(webserver、apiproxy、frontend-static、plugin-inventory、directory-picker) |
client/ |
Web-GUI 浏览器半端:shell、wire、object services、slots、ui-* 插件(约 30 个子包) |
examples/ |
演示 bundles(Support) |
test-support/ |
测试基础设施:testkits、invariants、replay、Loader smokes(Support) |
util/ |
零依赖低层工具:Branded<B>、Harness home/路径、timeout、retention(Support) |
依赖铁律:扩展插件只依赖 Service Definition,绝不依赖具体 provider(dsh-agent-loop 可替换;UI/hook/tool 插件只用 dsh-agent)。能力在独立演进时把 Service Definition / Service Provider / Consumer 三角色拆开——这就是"接缝"。依赖图由 docs/module-graph.md 生成并在 CI 中保鲜门控。
五、事件模型:三大领域
事件是 dsh 的扩展点。官方把事件分成三个领域,选对领域是大多数改动的第一步:
- 会话事件(Session events):追加进日志的持久事实,通过
session/event广播。用于"这个事实必须在重载后存活"的场景。 - Agent 事件(
agent/*):携带活体Agent:inbox、step、status、request、validation、continuation。用于观察或拦截正在飞行的工作。 - 能力事件(Capability events):把策略和适配器挂到接缝上(
fs/*、tools/*、telemetry/*),不 import 主循环。
5.1 事件全景(节选)
docs/event-producer-consumer.md(由 scripts/gen-doc-graphs.ts 生成)列出了全部 harness 事件的派发方与监听方。核心事件包括:
| 事件 | 模式 | 派发方 | 主要监听方 |
|---|---|---|---|
agent/created / agent/disposed |
emit | core/agent |
agent-presets、goal-round-driver、schedule、subagent |
agent/status |
emit | core/agent-loop |
agent、apiproxy、compaction-basic、schedule |
agent/pre-step |
waterfall | core/agent-loop |
agent-instructions、compaction-basic、plan-mode、hooks-*、tool-skill 等 14 个 |
agent/request |
waterfall | core/agent-loop |
agent |
agent/request-error |
waterfall | core/agent-loop |
compaction-basic、llm-retry |
agent/turn-stopping |
serial | core/agent-loop |
hooks-claude-code、hooks-codex |
llm/stream |
waterfall | llm/llm |
agent-loop、session-checkpoint-policy、session-title 等 |
system-prompt/assemble |
waterfall | core/system-prompt |
agent、agent-presets、system-prompt |
tools/pre-execute / tools/execute / tools/post-execute |
waterfall | core/tools |
hooks-*、timeout-policy、spill-policy 等 |
tools/result |
emit | core/tools |
agent-instructions、subagent-in-process-driver |
session/event |
emit | core/session |
约 20 个监听方 |
session/flush |
parallel | core/session |
session-persistence、session-telemetry |
approval/request |
waterfall | interaction/user-approval |
acp、apiproxy |
subagent/start / subagent/end |
emit | subagent/subagent |
hooks-*、server |
workflow/start / workflow/phase / workflow/end |
emit | workflow/workflow |
tool-workflow |
fs/write-intent / fs/edit-intent |
waterfall | tool-fs、tool-str-replace-editor |
fs-observation-policy |
六、Turn / Step 生命周期(Agent 主循环)
dsh 把交互建模成 step 与 turn 两层:
- step(步骤):一次模型请求 + 它调用的所有工具。
- turn(回合):零个或多个 step。在第一个输入被认领前开启,在"不再欠任何东西"时关闭。
官方时序(docs/agent-lifecycle.md)核心流程:
turn/start
→ 认领 next-step 输入 + 一条排队消息
→ 组装提示词片段 + 工具 schema
→ agent/pre-step(waterfall):监听者可改写消息或整体拒绝
→ step/start
→ 把进入的消息追加为 user/message
→ 从日志推导模型历史(deriveMessages)
→ agent/request(waterfall)→ llm/stream(waterfall)→ assistant/chunk* → assistant/message
→ tool/call* → tools/pre-execute → tools/execute → tools/post-execute → tool/result*
→ step/end
→ 若工具要求再来一次,或 next-step 输入到达 → 认领 → 下一个 step
→ agent/turn-stopping(serial,回合结束前最后一个检查点)
turn/end
要点:
turn/*、step/*、user/message、assistant/*、tool/*是持久会话事件;其余是三个领域的活体扩展点。agent/pre-step、agent/request、llm/stream、三个tools/*事件是 waterfall,监听者必须调用next()委托;agent/turn-stopping是 serial,没有next()。- 输入通过唯一 inbox 到达驱动。部分消息立即唤醒它;注入的上下文在 inbox 中等待,直到另一条消息到来。
- 被拒绝或空认领的第一个 claim 仍会关闭一个"没花 step"的持久 turn——日志会记录这次尝试。
- 每个 step 读取插件注册的提示词片段与工具 schema。
agent/pre-step的返回是权威的;agent/request-error让 compaction-basic、llm-retry 等可以决定"重试"还是"保留原始错误"。
6.1 "模型可见即已记录"不变量
dsh 有一个运行时不变量(invariant):凡是到达模型请求的内容,都必须能从会话日志重建("Model-visible means logged")。因此新的模型可见输入必须新增 SessionEventMap 事件并从日志渲染。这个设计让 fork、resume、转录、遥测、持久化全都从同一条事件流派生。
6.2 ReactLoopAgent:默认驱动的实现细节
packages/core/agent-loop/src/agent.ts 的 ReactLoopAgent 是默认驱动,phase = idle | maintenance | running{turn,step}。每次 step 的请求构造链:
preStep
→ systemPrompt.assemble(收集 tools/sections/contexts/variables,跑 system-prompt/assemble waterfall,renderPrompt 严格插值 {{var}})
→ agent/pre-step(waterfall:监听者可改写消息或整体拒绝)
→ buildRequest(agent/request waterfall + llm.prepareCall)
→ llm.stream → assistant/chunk* → assistant/message
→ 工具调用 → executeToolCalls
工具调度(tool-calls.ts):按 executionMode 判定 exclusive(barrier 串行)或 parallel(有界池 maxParallelToolCalls);派发可以重叠,但结果/上下文按模型序提交;中止时写合成 error 结果,保证日志回放完整。max-tokens sticky:如果模型因 max_tokens 截断结束,loop 会带着未完成意图继续下一 step(保持同一 step 语义续跑)。
默认模型:ctx.agentDefaultModel.currentSelection()(settings 可覆盖);buildRequest 从 request/header 恢复精确模型默认,或由 agent/request waterfall 决定。
system prompt 组装:PromptLayer 每 scope 一层(全局层 + 每 agent 层,作用域化注册的直接应用);orderTools 应用 toolOrder 配置 + 字典序 + clone;renderPrompt 对 {{var}} 做严格插值(缺变量 fail-loud 而非静默)。
工具呈现:schema(tools)→ 执行(pre/execute/post-execute waterfall)→ UI 渲染意图(presentation.ts 的 generic/terminal/diff/search/read/web 卡片,presentationMeta 随日志持久化);packages/core/agent-tool-presentation 控制模型所见的形式(native/code/both)。
七、能力接缝(Capability Seams)
接缝是一个可替换的能力,有三种角色:
- 服务定义(Service Definition):声明接口;
- 服务提供者(Service Provider):实现它;
- 消费者(Consumer):使用它,通常是面向模型的工具。
一个包可以身兼数职,但单独一个角色不构成接缝;增加一项能力意味着设计全部三个角色。接缝是"换一个 provider 就改变整个产品"的原因:文件系统与子进程 provider 共享同一个执行世界,把它们指向远程沙箱,Bash、PTY、LSP 就一起搬过去了;子代理 provider 在同一接口后面差异极大——从全新子代理到另一个产品中的委托回合。
(接缝图见 docs/capability-seams.md;各子系统文档在 docs/subsystems/。)
八、会话日志:事件溯源模型
整个 dsh 的会话是一条只追加(append-only)的事件日志,它是 agent 交互历史的唯一事实源。LLM 可见的 Message[] 从不下发存储,而是通过"表面折叠(surface fold)"从日志中派生——这就是"会话日志是模型所看上下文的源头"的完整含义。
8.1 SessionEventMap:事件词汇表
packages/core/session/src/types.ts 定义 SessionEventMap——合并可扩展的并集映射,第三方插件可以向它追加新事件类型(compaction 加了 compaction/*,hook 加了 hook/*)。核心事件:
| 事件 | 含义 |
|---|---|
turn/start / turn/end{reason} / step/start / step/end |
循环边界 |
user/message |
user 角色消息——人类提示、agent.inject() 的合成上下文(文件变更通知、AGENTS.md、skill 内容、定时任务通知)等,source 区分来源 |
assistant/chunk |
原始流 token(逐 token 重放保真) |
assistant/message |
一步内拼装好的 assistant 消息(含 usage) |
tool/call{callId,name,arguments} / tool/result{message,error?,meta?} |
工具调用与结果,callId 配对 |
steering/message |
人类/外部驱动的转向消息(steering) |
todo/write、request/header、request/context、session/end-seed |
日志型事件(不产生 LLM 消息) |
SessionEventMap 共 12 个事件变体。消息来源语义分两层:MessageSource(kind = 谁产生这条消息)+ ContextForm(form = 何种信息:instructions / catalog / snapshot / notice / relay / recall)——语义而非视觉。
每个 SessionEvent 是 type 上的判别联合(switch 自动收窄),带 seq(=log.length,自 0 连续单调)、time、data,以及条件字段 surfaceOp/sourceEventSeqs/ignorable。ignorable 标记纯信息事件供旧读者安全跳过;缺失则要求新事件必须"拒绝而非静默丢弃"。data 必须是 lossless JSON(append 用 isJsonValue 运行时校验,拒绝 BigInt/函数/循环引用/稀疏数组),保证日志可逐字持久化。
8.2 表面(Surface)机制
SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result' 是三种会产出 LLM 消息的事件子集。它们必须携带 surfaceOp 说明如何进入有序"表面":
SurfaceOp = 'append' | { op: 'replace', start, end }——append是普通尾部追加;replace是压缩用于把一段表面节点"遮蔽/替换"成一个总结节点。- 消息型事件还可带
sourceEventSeqs声明它派生的源事件(replace 必须列出所有被遮蔽节点)。
SurfaceManager(packages/core/session/src/surface.ts)用折叠维护"当前表面节点 seq 的有序数组",重放时校验:seq 连续、replace 的 start/end 必须是现存节点、sourceEventSeqs 必须覆盖所有被遮蔽节点且引用更早事件、tool/result 替换只能改写 content。deriveEventMessage 定义逐节点投影规则:user/message → UserMessage;assistant/message → message(空 content 跳过——它只承载 max-token 的 usage);tool/result → message;其余 → null。"模型历史由投影派生"就是这个函数:外部重构器用同一函数折叠日志前缀可精确重建任意请求。
8.3 Session 类与请求快照
Session(packages/core/session/src/index.ts)持有:log(只追加)、surfaceManager(增量表面)、header(与事件日志分开的存储元数据:version/id/createdAt/cwd/parentSession/seedLength/origin/delegationDepth/agentPreset)、firstLiveSeq、派生消息缓存。append() 校验 → 深度冻结 → 表面校验 → push → 同步通知 store 观察者(持久化插件 buffered 处理),热路径不阻塞 I/O。
请求快照:loop 在 step 内组装 EpochHeader{config, system, tools},session.append('request/header', {header, reason:'initial'|'resume'|'change'}) 与可选 request/context。这使"一次模型请求"成为日志的纯函数——运行时不变量断言这一点,也是"模型可见即已记录"的落地方式:新增模型可见输入必须扩展 SessionEventMap 并从日志渲染。
8.4 持久化(双后端 + 崩溃恢复)
packages/session/session-persistence 定义 ctx.sessionPersistence 接缝(locate/create/append/load/inspect/prepare/readFrom/list/listSnapshots),PersistenceCoordinator 承载写批处理:首个 pending 事件开启固定批窗口,后续加入不重置;session/flush 取消等待并排空到安静。后端两个:
session-persistence-jsonl:每个会话一个.jsonl文件,支持 zstd 编码与原始产物读取;session-persistence-sqlite:按 seq 可寻址读取后缀。
崩溃恢复:重载到开放 turn/start 无 turn/end 的日志时不截断,补一个合成 turn/end{reason:{kind:'interrupted'}} 保持平衡;对 live 会话则等待权威内存快照落盘并拒绝 open turn。session-checkpoint-policy 在构造下游模型流之前 ctx.sessions.flush()——保证"已记录的请求前缀已落盘"才派发到 adapter。
8.5 会话查询(session-query)
ctx.sessionQuery 是 live 优先的统一查询引擎:
- 精确读取:
readSession(整日志+校验重放)、readEvent(目标事件+有界窗口)、readSurface、listEvents; - 过滤:会话级(id/cwd/created-at/parent/availability)与事件级(seq/time/type/surface/text)谓词,
text过滤编译成大小写不敏感、空白灵活、防注入的正则; - 全文搜索:
searchSessions/searchEvents(SQLite 索引后端,cursor 分代); - 追踪:
traceSession(谱系祖先/后代,检测环)、traceEvent(positional replace 链 + cited source); - 标题折叠:
readTitle等。
tool-session-query 把它暴露为工具;session-log-export 导出日志。
8.6 遥测与标题
session-telemetry:SessionTelemetryRecord携带原始StreamChunk+ 时间戳,走session-telemetry/record(waterfall,可 redact)后非阻塞送往后端;session-telemetry-otel是 OpenTelemetry 后端。语义是"捕获侧",batching/retry 归上报 SDK。session-title:fallbackSessionTitle(首个合格人类 prompt 前 N 词,确定性)+normalizeSessionTitle(清理终端转义、空白归一、按 Unicode 码点截断不劈代理对)+session/titlelog-only 事件(pinned停止自动生成)。LLM provider 三选一:session-title-llm/-first-prompt-llm/-all-prompts-llm。
8.7 投影单元(session-projection)
ctx.sessionProjections 注册表把每个"域投影单元"{key, schema, init, apply, view, stateVersion} 驱动地作用于已提交事件(为 UI 提供整值投影):框架只订阅一次 session/event,每个事件 eager 走每个单元的 apply;apply 对无关事件必须返回同一 state 引用(Object.is 门控 change feed);承载状态的日志事件必须带完整 post-change 状态(last-wins);init/apply/view 必须同步。stateVersion 是投影缓存的失效锚点。
8.8 上下文提供者(packages/context):"上下文树"的真相
不存在单一的"上下文树"。LLM 上下文 = system prompt + 按注入顺序加入 step 消息列表的多个 user/message 事件。packages/context/ 不是上下文容器,而是一组上下文提供者插件,通过两条途径进入会话:
- 生命周期注入:
agent.inject(createUserMessage(...))(hooks、user-approval、plan-mode 等都这么做)→ 成为带source.kind的user/message表面事件进入日志; - pre-step 消息前置:在
agent/pre-step用{prepend:true}把新消息插到消息列表(time-context、tmux-context即此路径)。
四个内置提供者:
| 提供者 | 注入内容 |
|---|---|
agent-instructions |
加载 AGENTS.md/workspace 指令:workspaceBaselineIdentity 在首个请求前进入 durable 上下文;fs 工具触碰文件时把该文件的嵌套/变更/移除指令注入 inbox(source.kind='agent-instructions') |
time-context |
从 step 消息推导浏览器时区(request-zone.ts),格式化时钟快照,按 refreshIntervalMs 去重注入 |
tmux-context |
注入 tmux 会话/窗口/窗格信息 |
session-reference |
跨会话引用:解析 @session-id 提及,用 sessionQuery.readSurface 快照另一会话的 user/assistant 对话(排除工具/推理/被遮蔽节点),head/tail 截断裁剪到 maxReferenceBytes 预算,包进 <referenced-sessions> 不可信只读背景(maxReferences 限制数量) |
九、作用域化注册(Scope)
packages/core/scope 是一个库原语(不是 Cordis 服务),提供"一个注册上下文同时表达 per-agent 可见性与共享生命周期所有权"的词汇表:
ScopeKey是不透明对象标识。dsh 自带的主循环用活体Agent对象本身作为 key,但原语从不检查对象内容。Scoped<T>是编译期品牌:scopeTarget(base, key)返回的路由接收器要求 scope 过滤事件声明把它作为this类型,真实事件主体仍是显式参数。Scope把注册上下文与两条拆除路径配对:rawDispose保留 Cordis 精确 disposer(用于有序复合 effect),dispose()是公开的共享静默边界(并发调用等待同一完成)。ScopedLayers<L>拥有急切的全局层与惰性创建的精确 scope 层:读不创建层,merge()物化"插入序全局命名条目 + scope 影子"。注册只用一个上下文同时获得可见性和 Cordis effect 所有权;scope 层只在整层为空时才被回收。
关键价值:给某个 agent 注册的提示词片段、工具、事件监听,随该 agent 的销毁自动回滚,不会泄漏到其他 agent。实现上(packages/core/scope/src/index.ts):createScope 派生子 fiber 并打 kScope tag;注册视图向下继承(ScopedLayers:子 scope 看得到全局层与祖先层注册),事件准入向上流动(scopeTarget 路由载体 + filter:子 scope 的事件只投递给该 agent 的监听者)。每个 Agent 以自身对象为 key,实现"每 agent 独立注册 + 全局观察"。这是 dsh 多 agent 并发安全的地基——也是 system prompt 按 scope 分层(PromptLayer per scope)、工具注册按 scope 过滤(ToolRestriction)的共同底座。
十、进程沙箱(Sandbox)
沙箱是 dsh 安全模型的执行面。packages/sandbox/sandbox 定义抽象接缝 ctx.sandbox:把"同世界"子进程的 argv 包装成带文件效应策略的执行,而不把消费者耦合到特定平台的 runner。
10.1 三种模式与执行完整性
type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
type SandboxEnforcement = 'full' | 'partial'
read-only:拒绝写入(POSIX runner 额外授予 shell 需要的/dev/null;Windows ACL runner 不授予显式可写根)。workspace-write:允许在工作区根与后端承诺的临时区下写入。danger-full-access:绕过限制——只有前两种模式能发给 provider;danger-full-access消费者直接 spawn 原始 argv,不调用ctx.sandbox。- Enforcement 是"报告的事实":
full表示后端管住了模式承诺的每个文件效应;partial表示旧内核 ABI 或 Windows ACL 边界只能管住一部分——要求绝对边界的消费者必须拒绝或显式区分。
10.2 每次调用携带完整策略
策略按每次能力调用解析并携带(SandboxExecutionPolicy:mode + workspaceRoot + sessionId),而不是固定在 provider 上:bash 在 read-only 下运行的同时,受限子代理可能需要在 workspace-write 下写自己的状态目录;一次被批准的升级重试就是一次带更宽策略的新调用。普通工具调用的 workspaceRoot 来自调用会话不可变的 cwd;部署配置是 agentless 调用的回退。根路径先按文件系统语义规范化再做词法归一,所以包含 symlink/.. 的 cwd 也能正确识别真实目录。
ctx.sandboxPolicy.resolve() 的优先级:批准的显式 mode > 会话最后一条 sandbox/mode 事件 > 部署默认值。
10.3 包装 argv 与失败分类
confine(argv, policy) 返回 ConfinedArgv(替换 argv + enforcement 事实 + 两类 stderr 分类器):
denialSignatures:本后端拒绝文件效应时产生的方言(bwrap 只读绑定下的 EROFS 文本、Landlock 的 EACCES、Seatbelt 的 EPERM)——消费者按本后端的方言匹配,而不是跨后端联合。runnerFailureRules:识别 runner 在真正执行命令前就失败/拒绝的证据(退出码门 + 剩余 stderr 行内大小写不敏感的致命签名,先按整行精确相等移除良性信息行)。
失败闭合(fail-closed)是硬约束:confine 要么返回强制执行的 argv,要么抛 SandboxUnavailableError(code SANDBOX_UNAVAILABLE)。对受限策略而言,静默的无限制透传永远不合法。
10.4 后端矩阵
packages/sandbox/sandbox-local 提供多平台 runner:
| 平台 | 技术 |
|---|---|
| Linux | bwrap(bubblewrap)/ Landlock |
| macOS | Seatbelt |
| Windows | ACL 受限令牌 runner(sandbox-windows-acl,每个会话/工作区对给随机私有临时目录与 SID) |
消费者是 bash-sandbox 与 pwsh-sandbox(见"Shell 执行"章节)。容器、microVM、远程执行是"整个能力接缝"的兄弟实现,不是 ctx.sandbox 的 provider。
十一、插件生态:扩展运行时本身
dsh 的"一切皆插件"不只是架构口号——它还允许运行时自我修改。
11.1 动态 Cordis(模型自改运行时)
packages/extensions 实现了"模型可以用工具定义并运行新插件"的能力:
- host-runner(
ctx.dynamicCordisRunner):在node:vm新 realm 中求值 host 半端插件——全局是 tag 过的写透 console、harness注册 helper、btoa/atob编码原语,以及对被故意保留的 Node API 的"可调用陷阱":文件系统→ctx.fs、网络→ctx.web、进程→ctx.bash、定时器→Cordis timers。guard.ts提供 SANDBOX CONTEXT FAÇADE 白名单门面(声明服务的服务 + 生命周期安全动词,框架内部值被拒);官方明确这不是安全边界,视为与 bash 同等信任。进程内存执行,不落盘、不跨重启、没有 marketplace。带浏览器半端的包是"可回答的往返":发cordis/request-run事件暂停,等人批准/拒绝,或调用方 AbortSignal 取消。 - client-runner(浏览器半端):闭包求值 + loader 挂载,配合
ui-cordis的全帧面板与插件定义卡片。 - 工具面:
tool-cordis提供cordis_inspect/cordis_define/cordis_run/cordis_stop/cordis_undefine五个工具,让 agent 在运行时检查、定义、运行、停止、卸载插件。 - 集成走 Cordis IPC:
ctx.remote.$on。
11.2 Agent Presets
packages/preset/agent-presets:一个 preset 是含 agent.cordis.yml 的目录,standalone 挂载一次(ctx.agentPresets),agent 通过 bindScopeParent 加入;预设在 apps/cli/config/agent-presets/。persona 子包允许 preset 改变 agent 身份。注意与 packages/interaction/permission-presets(权限预设)区分。
11.3 凭据管理(Credentials)
packages/credentials 的核心原则:配置只带引用,不带秘密。
resolve:每次操作重新解析(支持热更新);describe:暴露 configured/source/writable 状态,永不暴露值。credentials-local的四层统一优先级:继承的进程环境(writable:false,启动环境最优先)>$DSH_HOME/.credentials.yaml(可写;0700目录 +0600文件,原子写 + 跨进程写锁,写前 chmod 校验,权限是唯一防线)> 调用 cwd 的.env(project-env,不可写)>$DSH_HOME/.env(user-env)。无加密、无 OS keychain;官方承认同 UID 的模型进程可读——harness 的承诺是"从不把解析出的文档路径交给模型,也不把值加载进进程环境"。
11.4 守卫(Guard)与身份
packages/guard:两个循环行为守卫——timeout-policy(工具声明timeoutMs,协作式截止线)与repeat-tool-reminder(同参数连发时提醒)。packages/identity:getOrCreateAnonymousUserId(随机 UUIDv4,持久化于$DSH_HOME/.anonymous-user-id单行;用于 OTeluser.id、/feedback确认、DeepSeek 请求头x-deepseek-harness-user-id),不是账户模型;永不从 hostname/网络/git remote 推导,删除文件即重铸新身份(刻意不恢复)。
11.5 原生沙箱启动器(native/landlock-run)
- C11(不是 Rust)、约 300 行,直接调用 Landlock UAPI,musl 静态链接,采用 self-restrict-then-exec(先自限制再执行目标),fail-closed。
- JS 包装导出
launcherPath/probe/grantArgs;由packages/sandbox/sandbox-local消费、bash-sandbox使用,构建脚本scripts/build.ts。
11.6 Python SDK 与外部驱动
python/sdk:deepseek-harness-sdk,DeepSeekHarness.run拉起 dsh 子进程,经 stdio JSON-RPC 通信,默认注入DSH_CORDIS_CONFIG。python/sdk-runtime:deepseek-harness-runtime-bin,用hatch_build.py把运行时打成单文件可执行分发给 Python 侧。
11.7 SDK(TypeScript 侧)
packages/sdk 三件套:
protocol/:换行分隔的 JSON-RPC wire(initialize/session/prompt/shutdown+session.event/status/subagent.*);client/:DeepSeekHarness/HarnessClient客户端;server/:stdio JSON-RPC 插件(stdout 即协议)——供第三方自动化、subagent-dsh-sdk后端使用。
11.8 示例场景(examples/)
| 示例 | 演示 |
|---|---|
examples/mcp-memory |
接入第三方 MCP server |
examples/acp-agent |
ACP 自动化 server |
examples/web-cordis |
模型自改 Cordis(动态插件) |
examples/jsonrpc-agent |
SDK unbundled、无 UI 的编码 agent |
examples/web-schedule |
session-local Schedule overlay |
examples/headless-agent |
replay / 真实模型的 headless 编码 agent |
十二、会话投影、查询与遥测(速览)
会话日志是"唯一的持久事实源",其他一切读取都是它的投影(projection)。第 8 节已详述机制,这里给出一览:
| 子系统 | 一句话 |
|---|---|
session-projection |
ProjectionDefinition(纯同步 init/apply/view 三段)+ ProjectionSnapshot(asOfSeq 一致切面)+ change feed |
session-query |
live-preferred 精确读取 + lineage/event trace + 过滤与全文检索分页(SQLite 索引后端) |
session-telemetry / -otel |
SessionTelemetryRecord(ledger/ops)经 session-telemetry/record(waterfall,可 redact)非阻塞上报;带 sharing disclosure |
session-title |
last-wins 标题快照 + sources seqs;三个 LLM provider 竞争"唯一标题提供者" |
session-reference |
跨 session 快照引用(@session-id 提及解析)与 prepared message context |
session-log-export |
日志导出 |
十三、Token 计量、上下文压缩与溢出(token-meter / compaction / spill)
这是 dsh 处理长上下文的三件套,配合非常精巧:
- token-meter(
packages/llm/token-meter):ctx.tokenMeter.measure()返回TokenMeasurement(logRevision + baseline + surfaceDeltaTokens + totalTokens + surfaceTokens + nodes)——通过重放日志得到"当前 surface(模型可见面)压力"快照。 - compaction(
packages/compaction/*):CompactionEngine+CompactionTrigger(触发条件)+ 可选toolResultPruner(先裁剪工具结果再选摘要)。触发点有两个:agent/pre-step的压力检查(压力触发),与agent/request-error的上下文溢出恢复(CONTEXT_WINDOW_EXCEEDED,绕过正常阈值做一次有收益的均衡缩减)。默认后端BasicCompactionEngine的阈值:thresholdRatio=0.8(contextWindow 的 80% 触发)、retainRatio=0.16(保留近期尾)、maxTokens=8192、compactionRetries=1、maxOverflowRetries=1,支持 per-modelmodelPolicies。范围选择selectCompactableRange从头向后锚定、保留定价的近期尾、不拆散 assistant tool-call/result 对。压缩事务:appendcompaction/start(日志锁防并发)→tokenMeter.measure快照 →summarize(唯一可覆盖 hook)→ 稳定性检查 → appendcompaction/summary+ appenduser/message(surfaceOp:{op:'replace',start,end},总结必须比遮蔽内容更小,否则失败)→ appendcompaction/end。总结帧(summarizer.ts)要求固定 Markdown 结构(Primary Request and Intent / Key Technical Concepts / Files and Code / Errors and Fixes / Pending Jobs / Current Work / Next Step / Critical Context),提示词放在重放对话之后作为最后一条 user 消息——aux 调用成为上次请求的前缀,KV cache 友好。compaction 事件是 log-only,历史可完整回放;只有 pruning/summarization 推进了 surface 替换代(generation)才开新的重试 turn。另有command-compact(/compact人类命令)。 - spill(
packages/spill/*):SaveTextSpill/SpillOwner/SpillRef(品牌化SpillLocator)——超大文本(如巨型工具输出)落盘(spill-local),上下文里只放定位符,spill-policy挂在tools/code-dispatch-log与tools/post-execute瀑布上决定何时 spill。
关键不变量:request/header + request/context 是 log-only 快照,使"一次模型请求"成为日志的纯函数——运行时不变量断言这一点,也是"模型可见即已记录"的落地方式。
十四、审批、权限预设与安全模型
- approval(
packages/interaction/user-approval):ApprovalOutcome是闭集 fail-closed——唯一 grant 是 allowed-once(一次授权一次有效);ApprovalPolicy只有ask/never(never是确定性拒绝,不是"从不问")。approval/request是 waterfall,acp与apiproxy监听;审批记录以 log-only 方式进审计日志。 - permission-presets(
packages/interaction/permission-presets):把 sandbox 模式 + approval 策略打成命名预设的组合器,本身不执行——它写穿各旋钮(sandbox mode、approval policy),custom派生生态;执行仍由 sandbox/enforcement 与 approval 各持有人负责。plan mode 只是软引导,不是安全边界。 - guard:monotonic(注册后不可被降权的最终拒绝策略)+ filesystem version/stale guard。
- 安全三层总览:凭据引用(配置无秘密)→ 进程文件沙箱(bwrap/Landlock/Seatbelt/Windows ACL + fail-closed)→ loop 行为守卫(timeout-policy、repeat-tool-reminder)。
十五、设置、存储、工作区与不变量
- settings(
packages/settings/settings+settings-file):SettingsNamespace按 schemastery schema 注册(register<T>(ns, schema, {base?, applies?, validate?})),resolve 层序 defaults → base → composition → user 深合并;SettingsScopeupdate/replace/mutate、secret redaction。UI 不是通用 form 渲染器:packages/client/schema-form把 schema 信封重水合成节点树(rehydrateSchema/validateDraft/setPath),各编辑器手写控件并算最小差分{op:'set'|'unset', path}提交。持久化:settings-file落<dshHome>/settings.yaml——withFileLock+ 原子写(0600)、YAML 叶级 diff 补丁保留注释、单排他操作链、chokidar watch 外部编辑 reconcile、写带expectedRevision(冲突抛SettingsConflictError)。 - storage(
packages/storage/*):StorageBackend(kv facet)+StorageForms+DomainSpec/Domain(storage-domain,domain/changed 事件);storage-json/storage-sqlite两个后端。feedback、attachment 等都挂在 storage-domain 侧车。 - workspace(
packages/workspace/workspace):workspace registry,以 canonical path 标识,attach/detach。 - invariants(
packages/runtime-diagnostics/invariants):运行时不变量注册表——每个包带./invariantcompanion 文件,INVARIANT归因错误;这是 dsh "文档与代码强一致"工程文化的运行时版本。
十六、工具系统与执行管道
工具是 agent 与世界的接口。packages/core/tools(ToolRuntime,ctx.tools,核心文件 packages/core/tools/src/index.ts 约 1900 行)是整个系统的中枢:按 agent 作用域(scope)解析的工具注册中心 + 一次调用要穿过完整五段瀑布的执行管道,同时决定"工具如何呈现给模型"(native 函数调用 / Code Mode / both),并自动把可见工具 schema 注入系统提示词。
16.1 ToolDefinition:一个注册工具
ToolDefinition = ToolSchema(模型可见字段)+ ToolOutputDefinition(规范输出契约)+ execute + 可选元数据:
ToolSchema(模型可见面):name/description/parameters——只有这些会进入模型请求。ctx.tools.schemas()用显式白名单(schemaOf())投影并深克隆,output/execute/finalizeContent/timeoutMs/isConcurrencySafe/presentCall/presentResult永远不允许泄漏给模型。output: ToolOutputDefinition:强制要求声明。schema(对每个成功规范值做 JSON Schema 校验)+render(args, value)(纯投影为 Native/模型内容)+presentationMeta?(纯可回放展示投影,只对顶层调用计算)。这规约了"规范(canonical)无损 JSON 值"与"呈现给模型的内容"的分离。execute(args, exec):只返回规范无损 JSON 值;异步工作必须观察/转发exec.signal(取消信号)并在自己的工作达到静默后才 settle——注册表通过环绕派发信号替换保留调用方取消,但不能硬杀同进程代码。finalizeContent?:同步的最后一步变换,在每次规范化的结果(包括绕过tools/post-execute的管道失败)上恰好调用一次,紧接无损物化之前;必须是全函数且不能抛。timeoutMs?:协作式超时预算,永不发给模型(由timeout-policy作为tools/execute包装强制执行)。isConcurrencySafe?:纯同步分类器,只有显式true才加入并行组;未声明、异常、非 true 返回值都是"独占"。入选的执行不得修改父持有状态。presentCall/presentResult:可选 UI 呈现器,纯且无副作用——UI 在实时流式期间与日志回放时都会调用它们("UI 保真可回放"哲学)。
第一方工具用 defineTool(packages/core/tools/src/schema.ts):验证并收窄参数、从 output.schema 推断 body 返回类型、为两个输出投影器定型。
16.2 统一 JSON Schema DSL
ValueSchemaSpec 支持 string / number / integer / boolean / null / array / object / 仅作者可用的 json / 恰一的 oneOf;标量 enum/const 必须与节点类型匹配;显式 object 节点总是声明 additionalProperties: true | false。
16.3 作用域解析:view()——"继承表面 + 自己层"
注册按"调用 context 作用域"分层:普通插件 ctx 注册进全局层;agent.ctx 注册进该 agent 单独层并可 shadow 同名全局工具;层内重名抛错;run_code 名称无条件保留(不可注册/遮蔽)。view(scope) 把"继承表面"与"自己层"分开:
- 继承表面 = 全局层 + 每个祖先层,按最近祖先优先 shadow 更远者;
restrict()施加的 allow/deny 掩码只过滤继承表面,绝不锁自己层注册的工具;掩码在链上取交集;- 自己层的注册最后并入(shadow 继承同名,不受过滤约束);
- Code Mode 的
run_code传输在能力过滤之后追加,且只对modeFor(scope) !== 'native'的作用域可见。
一个 resolver 喂三个消费者:get(name, scope)(查找)、schemas(scope)(投射给模型)、dispatch(执行)。
16.4 执行管道:五段瀑布(硬性契约)
对 agent-loop 的并行调度器暴露 TOOL_RUNTIME_SCHEDULER(prepare/dispatch/finalize/finish 四阶段,前有 createExecution):
createExecution
→ 生成不透明 token、解析 rootCallId、快照参数并 deepFreeze(undefined/BigInt/循环/sparse/-0 拒绝为参数错误)
→ prepare:tools/pre-execute 瀑布(allow/deny/ask)→ ask 走 approval(无审批服务即降级拒绝)
→ guardReason():从全局层到作用域链的单调 guard(拒绝是终态,后面监听器不能翻回允许)
→ dispatch:tools/execute around-wrapper 瀑布(只能替换 signal),fuseToolSignals 熔合后调 tool.execute
→ 返回值按 output.schema 校验(违规 → INVALID_TOOL_OUTPUT)→ 深冻结 → render 内容
→ finalize:tools/post-execute 瀑布(可替换 content 或 value 二选一、附加 context;block 转无值失败)
→ 应用定义 finalizeContent
→ finish:materializeFinalResult(无损 JSON 快照 + 深冻结)→ tools/result(observe-only)
配套机制:deferContext()(把 context 推迟到最终结果送还循环)、concludeTurn()(标记本回合完成);取消是协作式、静止式——body 前取消 → ABORTED_BEFORE_DISPATCH,body 后只能把成功改 ABORTED,已启动工作仍被 drain(不 abandon promise)。
16.5 呈现模式与 Code Mode
presentAs(mode) 让 agent 预设为自己选 native/code/both,shadow 部署默认。Code Mode 下模型直接调用(无 parent)只能命名 run_code(否则拒为 UNKNOWN_TOOL);run_code 在进程内 worker-thread 跑 TypeScript 程序(code-runtime-worker-thread:全新 Worker、stripTypeScriptTypes 位置保持剥类型、bindings 经 message port structured-clone、资源预算 computeMs=CPU 忙碌时间/maxWallMs/maxOldGenerationSizeMb/maxOutputBytes,失败分类正交 exception/timeout/abort/worker-exit/invalid-output/output-limit),程序把 tools 当全局数组调用——即 Code Mode 的 SDK。嵌套子分派经 parent token 关联外层执行,经 tools/code-dispatch-log 记日志。
16.6 MCP 与 ACP
- mcp-client(
packages/mcp/mcp-client):MCP 客户端(无 server 端),一个插件实例连一个 server。connection.ts是连接主管(重连循环:初始 500ms 指数退避至 30s 封顶、maxAttempts10 次、一次 outage 共享预算);tools.ts从tools/list(drain 分页)发现工具并构造ToolDefinition注册进ctx.tools,命名契约mcp__<serverName>__<rawName>(归一化到 ≤64 字符[A-Za-z0-9_-],有损时拼 12 位 SHA-256 防冲突,wire 上永远用 raw name);传输支持 stdio 与 streamable-http。HMR 热切换(改配置触发断开重连,serverName不变则工具名不变)。 - acp(
packages/acp/acp):ACP(Agent Client Protocol)服务器(automation-only)——把 dsh agent 暴露为 ACP JSON-RPC stdio 对话端,一个 ACP 会话映射为一个 dsh agent;用@agentclientprotocol/sdk的AgentSideConnection+ ndjson,实现initialize/authenticate/newSession/prompt/cancel;经session/event、agent/inbox/claimed、approval/request桥接,审批只给一次性 allow-once/reject-once。消费方如subagent-acp。
16.7 文件系统(ctx.fs)
packages/fs/*:fs 定义接缝(FsTargetKey 不透明、FsVersion freshness 令牌、FsWriteIntent、13 种 FsErrorCode)、fs-local 本地实现(realpath 派生目标身份——符号链接别名共享同一 stale guard;每 targetKey FIFO 写链串行化;原子写发布:同目录私有 staging + 0o700/0o600 + 独占打开 + fsync;LF 归一化 diff 基底;二进制/非法 UTF-8 拒绝;applyLiteralEdit 0 次 → FS_EDIT_NOT_FOUND、多匹配 → FS_AMBIGUOUS_EDIT)、fs-sandbox 给写/编辑加每调用策略围栏(读放行:read-only 拒变 FS_SANDBOX_DENIED、workspace-write 重 resolve 捕获 TOCTOU 并验证 canonical 路径在 writableRoots(policy) 内)、fs-observation-policy 实现"读先于写/编辑、版本守卫"语义(fs/write-intent、fs/edit-intent 单槽决策 waterfall + fs/observed)。工具面:tool-fs(read/write/edit/read_image,FsSandboxController 据 ctx.fs.sandboxMode 附 sandbox_permissions 提权字段)、tool-fs-search(glob/grep,走 @vscode/ripgrep 二进制 + ctx.subprocess.spawn(),非 ctx.fs 非 ctx.shell)、tool-str-replace-editor(Claude Code 风格)。
16.8 Shell、子进程与终端
packages/shell/*:bash-local/bash-sandbox/pwsh-local/pwsh-sandbox四个执行器(sandbox 版把 argv 交给ctx.sandbox包装);tool-bash、tool-bash-persistent、tool-pwsh;ctx.shellEnv。packages/subprocess/*:SubprocessSpawnSpec(全显式零默认)、SubprocessOutputReader.readFrom(fromByte)(偏移式非消费 reader,滑出窗口 lossy + spill 恢复)、credential scrub(scrubbedParentEnv剔除/KEY|PASSWORD|SECRET|TOKEN/i与DSH_*)。本地实现用分离进程树:POSIXdetached: true自成进程组、kill(-pid)全组信号 + Linux/proc/<pid>/stat轮询组存活;Windowstaskkill /T /F递归终结。唯一终止动词terminate():SIGTERM → graceMs → SIGKILL。spawnTerminal用 node-pty;Linux 前台检测解析/proc/<pid>/stat的 tpgid 与/proc/<pid>/task/<tid>/syscall(macOS 退化为ps)。packages/terminal/*:持久、按 Agent 隔离的 PTY 会话。注意:GUI 没有 xterm、没有实时终端面板、没有 PTY ioctl 传输——交互终端只对 agent 工具开放(链路tool-terminal(terminal_*) → ctx.terminals → BashTerminalBackend → subprocess.spawnTerminal → node-pty),输出是有界文本。terminal-bash的BashTerminalBackend:注入受控环境(PS1='dsh> '、PROMPT_COMMAND=OSC 133;D、TERM=dumb),pollReadiness每 50msinspectForeground()按序落定stdin_read/inferred_idle(3s 静默)/timeout(30s);BoundedTextBuffer4MB/1 万行尾保留滚动;sandbox fence:会话开着时若把 sandbox mode 改换则拒绝(防打开 PTY 期间策略偷偷变宽)。工具面六个:terminal_open/send/read/signal/close/list(terminal_send后台走ctx.jobs)。tool-bash在结果追加锚定末尾的[exit code: N]/[killed by signal: X],由packages/shell/shell/src/render.ts的parseExitStatus逆解析,拆成卡片的正文 + exit-status pill(card:'terminal')。
16.9 代码执行与 LSP
packages/code-runtime/*:CodeRuntime接缝(ctx.codeRuntime),well-known 语言typescript与python(只有 typescript 有已发布后端);isolation声明 substrate(worker-thread/process/container,诊断标签非安全声明)。见 16.5 Code Mode。packages/lsp/*:LSP 语义导航接缝——恰好 4 个闭操作goToDefinition | findReferences | goToImplementation | hover(零基 UTF-16 坐标,LspQueryResult闭结果联合,无通用 JSON-RPC 逃生口);lsp-stdio用ctx.subprocess.spawn()的 raw pipe 做 JSON-RPC 传输(可服务本地或远程实现),应答 server 的workspace/configuration、拒绝workspace/applyEdit,initialize握手 + 瞬态 open→query→close 串行化 + 优雅 shutdown 超时后 SIGTERM→SIGKILL;tool-lsp注册lsp工具(1 基坐标换算、sessionCwd强制)。
16.10 E2B:整个执行世界搬上云
packages/e2b/* 把整个 fs + subprocess 执行世界搬到 E2B 云沙箱,作为本地 provider 的替代实现(二者挂同一 ctx key,Cordis 同服务只实现一次)——bash-local、terminal-bash、lsp-stdio 无需 fork 即作用于沙箱内,这是"接缝 = 可移植执行世界"的活例证。e2b 持共享沙箱句柄(Sandbox.create({secure:true, lifecycle:{onTimeout:'kill'}}),私有运行根 cwd/.dsh-e2b chmod 700);fs-e2b 实现 ctx.fs(realpath -mz 求远程 canonical 路径、原子暂存写、createIfAbsent 用 ln -T、per-targetKey withLock);subprocess-e2b 实现 ctx.subprocess(远端写 wrapper bash 编码 spec、setsid --wait 独立进程组、env -i 执行、stdout/stderr 经 tee + base64 逐行回传 + EOF 帧、宿主轮询 pid/exit-code、远端 spill;terminal.ts 建 PTY、BootstrapOutputFilter 随机 marker 探就绪、按 sid 属组所有 PGID 做 TERM→KILL 双重升格清理)。
16.11 Web 访问、存储与附件
packages/web/*:web 接缝(search/fetch 两操作、provider availability、WebError);web-fetch-http;三个搜索 provider(web-search-deepseek/web-search-exa/web-search-perplexity挂同一ctx.web接缝);tool-web。packages/storage/*:三层——hub(ctx.storage命名后端注册 + 数据表单挂载)→ 后端(storage-json每 unit 一个文件、原子整文件重发布;storage-sqlite用内置node:sqlite的DatabaseSync,wal 模式,owner-only0o600,user_version物理布局版本)→ 域层(storage-domain:zod schema 验证 +domain/changed事件,每域单条写链:先 await 后端持久化再突变内存再 emit)。消费方只用数据表单,不直接触后端。packages/attachment/*:不可变二进制附件(图像为主)。attachment-local:SHA-256 内容寻址(attachmentId永不含路径或 bearer URL)、owner 私有存储根DSH_HOME/attachments/v1、硬链接原子发布、目录链逐级 fsync 崩溃持久、读回重校验 digest 与头字段(防ATTACHMENT_CORRUPT);sharp栅格校验与头部探测;默认 5 MiB/图、20 张/100 MiB 每消息、4000 万像素。
十七、子代理系统(Subagents)
dsh 有一个关键架构决策:子代理是"若干能力接缝之一",而不是 agent 主循环的一部分——所以它的类型放在独立包(packages/subagent/subagent)而非 core。它与 bash 这类接缝的不同点在于:多种 provider 实现可以在同一上下文共存,按名字注册到 ctx.subagents(SubagentRuntime),而不是单一 executor:
| Provider | 说明 |
|---|---|
subagent-spawn-in-process |
全新子代理(默认) |
subagent-fork-in-process |
fork 父会话 |
subagent-acp |
通过 ACP 协议委托给外部产品 |
subagent-claude-code |
委托给 Claude Code |
subagent-codex |
委托给 Codex |
subagent-dsh-sdk |
委托给另一个 dsh 实例(经 SDK JSON-RPC) |
17.1 创建与隔离
- 一次性(one-shot):
ctx.subagents.start(name, request)——先做 capability 校验(assertCapabilities,不支持就抛UNSUPPORTED_CAPABILITY,不静默降级),再snapshotSubagentDescriptor生成持久 descriptor,然后调provider.start(resolved)。 - 可持续(continuable):
startContinuable——由SubagentContinuationManager(packages/subagent/subagent/src/continuation.ts,1483 行)通过私有 activation-owner scope 创建AgentHandle;每个子代一个持久 Session、至多一个进程内 Activation。 - 隔离核心:
applyChildComposition(child-agent.ts)给每个子代全新的扁平 scope(composeFrom(childCtx, parent.ctx)),不继承父注册表;子代的 persona/toolFilter 只在子 scope 生效(最近 scope 胜出)。并注入固定代理范围声明SUBAGENT_DELEGATION_CONTEXT:"你的权限范围在你启动时被固定,无法在此会话内扩大"。 - fork 的特殊种子:
completedTurnPrefix(parent)取父日志"最后turn/end之前的平衡已完成前缀"作为 seed——fork 子代"看到父已完成 turn 但不看到进行中 turn",不继承工具/服务/权威,只继承对话前缀。
17.2 深度预算与委托策略
delegationDepth持久化在SessionHeader(monotone 下界,冷恢复不能降低),resolveChildDepth = parent + 1,超maxDepth抛SubagentDepthError。captureDelegatedPolicyOverrides捕捉父 sandbox 覆盖并把子代 approval policy 钉死为'never'——子代只能在固定 sandbox 范围内行动,任何审批请求被确定性拒绝;覆盖以source: 'delegation'事件追加到子代日志(冷恢复时回放,不重新捕捉)。- followup/interrupt 权威:
authorizeLineage要求发送者是精确存活的直接父(parentSession匹配);interrupt 支持 user(parentSessionId)或精确祖先 Agent 两种权威。
17.3 通信与结果回收
- 进程内直接调用 Agent 的
followup/inject/steer,没有自己的流协议;一次性 run 经SubagentRun.result拿终态(不因孩子失败而 reject——模型/传输失败 resolve 为stopReason:'error')。 - inbox 是唯一队列:running Activation → 入队;waiting → 唤醒同一 Activation;无 Activation → 冷恢复。
- report 反向通信:
reportFrom(child, ...)——子代是权威凭据,调用方不能指名接收者;'subagent-report'source,可quiet(inject,不开父 turn)或wakeup(followup,开父 turn)。 - settlement 通知:
notifySettlement在孩子所有权释放前,用'subagent-settled'给父发"孩子结束/为何结束+最终内容";父 idle 时用followup开普通 turn,否则steer汇入(多个孩子一起 settle 只花一步)。 ChildLock按 childId 串行化同子代的投递/释放/销毁临界区。
17.4 生命周期状态机
Activation 是"重建 child Agent 驻留的 epoch"(running | waiting | settled),由 Agent quiescence + ownedChildren 集合推导,而非第二状态机。dispose 是内存化事务:先 top-down 传播取消,再 child-first 释放 handle;finishDisposal 依次 child 释放 → quiesce → best-effort 最终 flush → capture → dispose handle → notifySettlement → releaseOwnership → settle。drain() 处理宿主 teardown。可发现性:listChildren/listDescendants 全程不加载 Agent,经投影单元三级阶梯解析(watermark 缓存 / projection checkpoint / 一次 persistence.inspect)。
17.5 与 workflow 的关系
workflow 包本身不依赖 subagent 的 npm 包,但 workflow-worker-thread/src/host.ts 利用 subagent seam 驱动每个 agent() 孩子(this.subagents.start(provider, ...),默认 spawn),并把脚本工作放到 worker 线程。workflow 是子代理的编排消费者——subagent 是"员工层",workflow 是"调度编排层"。
十八、任务队列(Jobs)与调度(Schedule)
18.1 Jobs:后台任务
ctx.jobs(JobRegistry 抽象 + LocalJobRegistry 进程内实现):
- 生命周期:
running | stopping | completed | killed | failed;start(spec)做 preflight(kind/label 非空、outputLimitBytes 正整数、servesOwner、activeTaskCount < maxConcurrentJobsPerOwner(默认 10))。 - settle 是 first-wins:只记录第一个终态,release waiters 后再通知 listeners——completion 最后宣布(因为 reporter 可能同步开一个 model turn)。
- 没有自动重试:job 一旦 settle 即终态,producer 自己决定是否重建。
- 无跨进程持久化:
LocalJobRegistry全部 in-memory,崩溃后 job 不恢复(可持续子代本身由 sessionPersistence 持久化,可经send_message冷恢复)。 - owner 隔离靠授权而非保密:
assertAccess按 session id(job.owner.id !== caller?.id则拒绝),unowned job 对任何 caller 开放。 - 工具面:
tool-jobs提供job_output/job_list/job_kill(wait(timeoutMs)用dsh-timeout的 deadline 实现有界等待,超时区分于调用方取消)。Producer 目前有 bash 后台任务与 subagent(一次性子代作为kind:'subagent'的 job 跑)。
18.2 Schedule:定时提醒
packages/schedule 是独立的时间调度子系统,不依赖 jobs——jobs 是"后台长任务集合/监控/取消",schedule 是"定时产生 prompt"的调度器,两者正交:schedule 到点触发 agent turn,jobs 承载跑很长的后台工作。细节见 21.7。
十九、钩子桥(Hooks)与反馈(Feedback)
19.1 Hooks:让 Claude Code / Codex 的 hooks.json 直接跑
packages/hooks 不是通用生命周期钩子,而是方言中立协议库 + 两个方言桥:让未修改的 Claude Code / Codex hooks.json 能跑在 harness 的拦截扩展点上。
| hooks.json 事件 | 桥接到的 Cordis 事件 |
|---|---|
SessionStart |
agent/session-start(脱钩跑,结果注入 additionalContext) |
UserPromptSubmit |
agent/pre-step(可 deny → reject 或 prepend 上下文) |
PreToolUse |
tools/pre-execute(可 deny / ask) |
PostToolUse |
tools/post-execute(block 或追加上下文) |
Stop |
agent/turn-stopping(deny 时 agent.steer 强制续跑) |
SubagentStart / SubagentStop |
subagent/start / subagent/end |
每个点对匹配 matcher 的命令逐个 runHook(经 ctx.shell,带回退超时、退出码、stdout JSON/stderr 解码),写持久 hook/invoked + hook/result 事件对,最后 merge 取最严结果。文档明确指出:真正做定制应写 typed 原生插件(直接 ctx.on('agent/pre-step'...)),桥只是兼容层。
19.2 Feedback:生命周期绑定的反馈
ctx.messageFeedback 是 storage-domain sidecar 服务——它从不清扫、不建立、不 resume Agent 或 Session,只 inspect 持久化会话历史。关键约束:
- 每条反馈目标必须是已定稿的 append-origin assistant 消息(
isAppendSurfaceEvent+deriveEventMessage验证 messageId 确实存在); put/delete走乐观锁版本控制(ifVersion必须精确匹配当前 UUID,否则返回version-conflict);- 每 session 一个侧车行,写操作按 session 级
enqueue串行化;ensureTargetDurable先把目标日志前缀推到持久化屏障再写侧车。
command-feedback 注册 /feedback <text> 命令,追加 feedback/record 事件(log-only,不进模型上下文)。
二十、工程文化:文档与代码强一致
dsh 仓库有非常强的"文档/目录/代码三向一致"工程文化(scripts/ 与根 package.json):
- 生成式文档:
docs/module-graph.md(由scripts/gen-module-graph.ts从各包 peerDependencies 生成)、docs/agent-lifecycle.md与docs/event-producer-consumer.md(scripts/gen-doc-graphs.ts)、docs/config-catalog.md(配置目录)、各子系统页的 Cordis API 区(scripts/gen-cordis-catalog.ts)——这些文档禁止手改,且 CI 里有verify-cordis-catalog等验证保证它们与源码字节级一致。 - 校验脚本群:
verify-md-links/verify-md-refs/verify-md-wrap/verify-package-paths/verify-package-invariants/verify-package-readme-model-experience/verify-config-source-ownership/verify-public-repository-links/verify-mermaid/doc-typecheck——发布前所有文档引用、包路径、README 的"模型体验"声明都会被自动核对。 - 双面构建:
tsc -b(类型)+tsdown --env.DSH_BUILD_FACE host|client——同一个源码按 host(Node)与 client(浏览器)两个面分别构建,tsconfig.host.json/tsconfig.client.json分离,保证"宿主半端/浏览器半端"各自只携带合法依赖。 - 测试体系:vitest 多配置——
test(单元)、test:e2e、test:snapshot(快照,DSH_SNAPSHOT=record/refresh录制/刷新)、test:web(构建后 Web 集成)、test:web:perf(性能)、test:web:stress(压力);test-support包提供acp-snapshot(确定性 input.json + stdout tee + 稳定化器的 keyless 快照)、agent-loop-testkit(主循环测试工具)、llm-mock-server(mock 模型服务器)、llm-replay(录制重放)。 - 门禁编排:
scripts/run-gates.ts把 CI 拆成check-all/ci-primary/ci-linux-primary/ci-static/ci-snapshot/ci-artifacts/ci-consumers/ Windows 阻塞/观测门等,按平台(Linux/Windows+wine)编排。 - 代码质量:oxlint(
scripts/run-oxlint.ts)、jscpd 重复代码检查、knip(未使用导出)、publint(发布健康)。
二十一、规划、目标与工作流(plan / goal / workflow)
21.1 Plan mode(规划模式)
packages/plan/plan-mode(ctx.planMode):plan/mode{active} 是 log-only 整值替换事件,foldPlanMode 纯重放恢复;激活时把 plan:policy section(order 50)加入每个请求的 system prompt。设置是 pending 语义:turn 未开时立即 append commit;turn 开着时存入 pendingIntents(WeakMap),pending 到下一次被接受的 in-turn agent/pre-step(listener 先跑下游 next(),accept 后才 append,只能 prepend)。pre-step 还把选中状态作为 notice 追加到消息批尾部(仅当 last header 描述相反状态才叙述,避免冗余)。/plan [off|message] 命令 + exit_plan_mode 工具(常驻,plan mode 外执行失败;plan mode 内要求 # 开头的完整计划,经 user-questions 的 plan-review intent 审阅,批准返回 {approved:true} 并记 silent pending exit)。plan mode 只是软引导,不是安全边界。
21.2 Goal(同会话目标)
packages/goal/goal(ctx.goals):事件溯源目标域——全部状态来自可重放的 goal/change 事件(完整快照或 clear tombstone)。GoalRef{id,revision} 带 CAS(每次 mutation 校对 revision);GoalPhase('active'|'paused'|'blocked'|'complete');roundsStarted 计数(每次 admitted 用户消息推进)。激活(armed/disarmed)是 process-local、非持久化——resume 会 rearm。
goal-round-driver 在 agent 空闲且 goal active+armed 时自动排队下一个 continuation round:安装 agent/pre-step 竞赛围栏——只有"自己预留的消息 + 精确 live revision + round 连续"才允许进入 step,否则 reject/stale 并 block(round-limit/queue-failed/prompt-rejected);预算耗尽自动 block。工具面 tool-goal:get_goal/create_goal/update_goal,create/update 需 direct human authority,blocked 需 ≥3 轮(blockedAfterConsecutiveRounds 默认 3)。命令面 /goal。
21.3 Workflow(工作流编排)
packages/workflow/workflow(ctx.workflowEngine):与 subagent 一样是可选项,每 context 只允许一个 engine 实现(插件配置替换,非命名注册)。
- 编排脚本格式:纯 JS 体(非 TS,无
export const meta——meta 是参数),允许 top-level await,以return <value>结束(JSON 可序列化)。脚本暴露 hooks:agent(prompt, opts?)、pipeline(items, ...stages)(无阶段间屏障)、parallel(thunks)(屏障)、phase(title)(纯进度分组)、log(message)、args。没有 fs/net/timer/Node API——脚本只做协调,干活的是 agent。 - worker-thread 引擎(
workflow-worker-thread):每次 run 一个 worker,脚本 vm 上下文在其中;agent()支持label/phase/schema/provider/model(其余如 effort/isolation/agentType 响亮拒绝);经 child RPC 走 subagent/spawn seam 起真正的子 agent——workflow 是子代理的编排消费者。 - 故障纪律:hook 误用、未知
agent()选项、触发 cap 等 →WorkflowError(fatal),parallel()/pipeline()re-throw fatal(typo 的 option 必须响亮杀死脚本);只有子任务失败才映射为 per-itemnull。WorkflowRun.result永不 reject。 - 观察事件:
workflow/start/phase/log/agent-start/agent-end/end是 observe-only,供 UI 展示阶段进度。工具面:workflow工具(tool:workflowsection order 115,明示"仅当用户显式要 workflow 或大型多 agent 编排时用")+tool-ralph(Ralph 循环)。
21.4 Todo(待办)
packages/todo/tool-todo:todo_write 工具,整表替换——每调用 append 一条 todo/write 快照事件,重放 last-write-wins;todos 投影单元(turn/start 清零)。策略 allowParallelInProgress(默认单活跃);校验 trim 非空、唯一内容。
21.5 Commands(人类斜杠命令)
packages/interaction/commands(ctx.commands):CommandLayer 分层(global vs agent-scoped 子 ctx 阴影);parseCommand 纯语法解析("admission miss" 不记录);execute 解析命中后 mint commandId(instance-token+seq 防重放重复)→ append command/run(log-only)→ 调 handler(withAbort 绑定 UI signal)→ command/done。人类命令(/compact、/feedback、/plan、/goal 等)不经模型 turn 直接派发。
21.6 用户提问与审批(user-questions / user-approval / permission-presets)
- user-questions(
ctx.userQuestions):provider-neutral 词汇——AskUserQuestionItem{id,question,detail?,header?,options?,multiSelect?,intent?};单 UI provider 注册,跨包可 batch 多问(每个稳定 id 路由回答案);intent(如kind:'plan-review')打标签改变呈现不改协议,approve显式命名批准选项而非靠顺序。 - user-approval(
ctx.approval):ApprovalOutcome闭集 fail-closed——唯一 grant 是allowed-once,missing/throw/rogue 都归一为unavailable;ApprovalPolicy('ask'|'never')中never在 dispatch 之前被服务自身决定(rejected,不审任何 answerer);signal.aborted→cancelled。log-only 审计:每次request()要求 open turn,appendapproval/asked+approval/decided审计对(都是 log-only 非模型可见;模型从approval:policysection(order 115)与approval/policy事件得知策略)。 - permission-presets:把两个独立旋钮(sandbox mode + approval policy)捆成具名预设——默认
workspace-write(workspace-write+ask) /danger-full-access(danger-full-access+never);set(session, name)先 appendpermission/preset(记录用户意图),再各自走 canonical 写入sandbox/mode与approval/policy。
21.7 Schedule(定时提醒)
packages/schedule/schedule:session-local 定时提醒,到期回到原 live Session 作为普通对话 turn(无外部通知/冷会话调度器)。AfterScheduleRecord(delay)/ AtScheduleRecord(绝对时间,带时区校验,拒绝夏令时 gap)/ EveryScheduleRecord(固定间隔 ≥300s);时间统一规范化为 RFC 3339 UTC。追赶行为:floor((now-target)/interval) 直接跳到最新一次 occurrence,不枚举不持久化跳过区间。投递 best-effort at-least-once。工具:schedule_create/list/delete。
二十二、LLM 抽象层(packages/llm)
packages/llm/llm 是消息/流/Token 词汇的唯一源头(Message、ContentBlock、StreamChunk、ToolSchema 等被 dsh-tools、dsh-system-prompt、dsh-session 等反向引用),并实现 LlmRuntime(ctx.llm)适配器注册表与流式调用入口 llm/stream Waterfall。它运行时只依赖 @deepseek-ai/schemastery——被几乎每个包依赖,所以必须保持零重量。
22.1 LlmAdapter 接口
抽象基类,唯一必选方法是 stream(options): AsyncIterable<StreamChunk>。可选钩子:providerInfo(路由显示元数据)、providerRetryPolicy(每路由重试策略)、listModels()(advisory 模型目录,不是请求白名单)、resolveModel(provider, model, signal)(精确路由的权威元数据:contextWindow/defaultMaxTokens/reasoning)。通过 ctx.llm.registerAdapter(providers, adapter) 注册,返回 AdapterRegistrationHandle(disposer + 同实例原子 replace());重复 provider 全有/全无失败,注册随 fiber 销毁。PreparedLlmCall 把"调用配置 + 适配器注册 + 模型元数据"绑定为一次不可变调用,保证 HMR 不混配。
22.2 StreamChunk:流协议
StreamChunk 是闭包判别联合:block-start(index+blockType)| text-delta | reasoning-delta | tool-call-delta(argumentsDelta 保持原始 JSON 字符串)| block-end(携带完整装配块)| usage | finish。契约:usage 必须在 finish 之前且其后无任何 chunk;tool 参数全程是原始 JSON 字符串;适配器可 throw,但 LlmRuntime.stream() 将其归一为 finish{error|aborted}。
BlockAssembler(assembler.ts)把 chunk 组装成消息:partials: Map<index, PartialBlock> 累积 delta;finish=max-tokens 时丢弃 tool-call 块(无法安全执行的工具不提交);对只发 delta 的协议容忍;对已闭合 index 的迟到 delta 直接忽略(防记忆膨胀)。agent loop 把原始 chunk 同时喂 assembler 与日志——重放保真由此而来。
22.3 强制归因:AppIdentity
attributionHeaders() 只映射标准 User-Agent 头(product/version (+url),RFC 9110),不支持 OpenRouter 专属头、无法被省略/抑制;每个 provider 请求必须携带(有 wire 级测试强制)。这正是 dsh "可问责遥测"文化的体现。
22.4 适配器与重试
- llm-deepseek:直接 fetch + SSE 打 DeepSeek 的 OpenAI-compatible chat-completions;transport-only,bearer token 每请求解析;流空闲 watchdog(默认 5 分钟);上下文溢出统一映射
CONTEXT_WINDOW_EXCEEDED、配额QUOTA_EXCEEDED、空完成EMPTY_RESPONSE(可重试)。 - llm-pi-ai:库支持的 Pi AI 适配器,功能最全(模型目录、endpoint 探测、跨模型 replay 转换)。
- llm-retry:挂在 agent loop 的
agent/request-error恢复扩展点上:从failure.code是否命中retryableCodes决定重试;延迟优先providerRetryAfterMs,否则指数退避min(initial*2^retry, maxDelay) * jitter。每次计划的重试先 append 一条可重放的llm/retry事件(含 retryId/延迟/failure)再开始可取消等待;retryId依 turn/step/provider/policyKey 保持不变以便持久化延续。适配器禁用库内重试——重试只发生在 agent 层。
22.5 Token 计量(token-meter)
ctx.tokenMeter.measure(session, requestHeader?) 把会话日志作为纯折叠流:用 provider 真实用量做锚(仅当最新成功调用的请求 envelope 匹配且其总量不低于启发式定价时复用,否则整段启发式重定价),返回不可变 TokenMeasurement。启发式:每 4 字符/token,块开销 4,role 开销 4;surface-projection 提供 O(1) 增量折叠(配合 compaction 的 ShadowPriceClaim 影子价格协议)。
二十三、技能系统(packages/skill)
23.1 分层注册表(ctx.skills)
SkillRegistry 是分层注册表(host + per-scope,与 tools 注册表同模式):SkillProvider(list()/get() 接口)+ SkillCandidate(含 rank/locator)+ SkillSummary + SkillDefinition(含 content)+ SkillInvocationPolicy(modelInvocable/userInvocable 两个独立布尔)。读取时合并 global + 查看 scope 链,最近层同名师胜出,rank 只解决同层冲突。get() 不缓存全文。
23.2 SKILL.md 约定与发现优先级
skill-filesystem 从 project/custom/user roots 扫描目录束(<name>/SKILL.md)与扁平 Markdown(<name>.md),不支持嵌套递归 **/SKILL.md。YAML frontmatter 要求 name、description,可选 whenToUse、metadata;激活策略读精确 key disable-model-invocation/user-invocable。
本地优先级(rank 越小越优先):project-dsh(100, <projectRoot>/.dsh/skills) → project-agents(200, <root>/.agents/skills) → custom(300) → user-dsh(400, <dshHome>/skills) → user-agents(500) → bundled(600)。project root 是最近含 .git 的祖先。bundled root 标记 trustedHost 跳过 fs 服务直读。
23.3 模型面注入与调用
tool-skill 的注入点不是 system prompt section,而是 agent/pre-step 的 user-role <system-reminder>(与 ctx.systemPrompt 正交):
- 首个完整快照时注入初始
<available_skills>目录; - 后续每步计算目录摘要(sha256),变化则
agent.inject()全量替换(目录只含 name + XML 转义 description,不含正文/路径/source); skill({name})工具:校验 kebab-case → 查目录 →isModelInvocable门禁 →ctx.skills.get()重读 → 返回renderSkillContent(<skill_content>/<skill_resources>/<skill_instructions>,含 XML 转义防 framing 注入);/name用户手势:pre-step 扫描 user 消息中的词边界 token,命中 user-invocable 技能时注入正文(注册在目录监听器之前,使注入位于目录之后、最靠近模型答案)。
二十四、Web UI 与 API 网关
24.1 分层:remotes → gateway → connection → webserver
API 层组织为 remotes → gateway → connection → webserver 四层(docs/api-gateway.md):
- webserver(
packages/host/webserver):纯node:http服务器插件(WebServer,ctx.webServer),只做连接与路由、不服务任何文件;config 只有host(127.0.0.1/0.0.0.0)与port(0=OS 分配),没有 TLS/auth/origin——0.0.0.0即裸露。路由匹配:exact 表 → 最长 prefix 表 → fallback 席;tapIndex(transform)注册 index.html 变换(client-modules借此注入 boot manifest)。frontend-static认领 fallback 席以 SPA 语义服务 dist:非 GET/HEAD 405、越出 dist root 403、miss 回退 index.html 200、未知扩展 octet-stream。 - connection(
packages/client/connection):RPC 载体——请求关联、信任边界、取消、响应信封、/apiHTTP 桥,注册/api/events.mux与/api/events.host两条升级(WebSocket)路由。 - gateway(
packages/api/gateway+packages/api/remotes):Typert 网关,ctx.typertGateway认领两段 Remote 端点、解析对象/上下文、调用活体 Cordis 服务、校验请求与返回值;packages/host/apiproxy处理没有 Remote 描述符的遗留端点。 - remotes:业务服务的 Remote 声明与贡献。
关于 SSE 的澄清:agent 事件流不是 SSE,而是两条 WebSocket 下行流;SSE 只有两个真实用途——(a) 进程内载体:apiproxy 的 sseResponse 把 Mux/Host 帧流包成 text/event-stream(Electron 走 fetch/IPC 桥时用);(b) HMR 开发通道:packages/client/hmr 的 GET /plugins/events(帧类型 {type:'graph'} / {type:'rebuilt',id,rev})。
24.2 Typert:类型安全的 RPC 生成
packages/typert/*(protocol/generator/loader/registry)实现了从 TypeScript 类型图生成 Host/Client 契约的严格管线:
- 业务服务用
@Remote/@RemoteScope装饰器声明暴露给 Client 的方法(未标记的方法不进入生成类型、不可经ctx.remote调用);复杂 Host 对象经TypertLookupMap声明线身份(如agent参数 →agentIdwire 字段),运行时经ctx.typert.lookups解析回 Host 对象。 - 生成物(
typert.host.js/typert.remote-client.js+.d.ts)写入各业务包自己的lib/;Client 端ctx.remote.<namespace>是具体的普通函数(不是 Proxy),带 declaration map——编辑器可以从 Client 调用直接跳到 Host 源码实现。 - 严格分析:Remote 必须是 public 非 static 实例方法、非泛型、参数必须是必填简单标识符(不允许解构/默认值/rest/可选参数)。
- 开发回退 SRC:源码模式(
node --import tsx/esm)下装饰器 initializer 仍记录方法名与模式,Gateway 用更弱的临时描述符分发(不读 TS 类型);Client 端永远拒绝无严格 codec 的 SRC 描述符。 - 调用路径:
connection.rpc.call('/api', '<namespace>/<method>', {args}, signal)→POST /api/<namespace>/<method>。
边界:Remote 只处理一元方法(一请求一响应);会话事件流、分页、增量 reduce、投影、实体子流需要独立的数据协议(复用 Connection 但绝不以 Remote 方法伪装)。
24.3 浏览器半端(packages/client)
Web-GUI 浏览器半端约 30 个子包:connection(wire)、runtime(对象服务)、slots(UI 槽)、web-react(React 绑定)、web(shell)、ui-* 插件(ui-agent-preset、ui-attachment、ui-commands、ui-conversation、ui-deliverables、ui-goal、ui-input-trigger、ui-jobs、ui-layout、ui-message-feedback、ui-model-selection、ui-permission-presets、ui-plan、ui-settings-*、ui-sidebar、ui-skill、ui-subagent、ui-tool、ui-trajectory、ui-user-questions、ui-workflow-run、ui-workspace 等)。每个 UI 插件是一个"ConversationNode":注册 ConversationNodeDefinition + keyed renderer(docs/cookbook/adding-a-conversation-node.md)。技术栈 React 18 + use-sync-external-store;apps/web 是 Vite 构建入口(dsh-web-frontend),产物 dist/ 由 dsh web 伺服。
启动链(packages/client/web/src/boot.tsx AppWebEntry.run()):parse window.__DSH_BOOT__ → ClientModuleSystem → 注册 AppShell + ModulesClient → 渲染 loading 页 → 并发 prefetch + ctx.plugin(Loader)(内联契约注入,防浏览器裸 dynamic import fallback)→ 等 prefetch → loader.await() + assertEntriesActive() sweep。唯一 ctx 级 renderSlot 是 ctx.slots.renderSlot('root', {})。
快照驱动渲染:SessionFace = ISession & ObservableSnapshot<ConversationSnapshot>;useSession = bindSnapshotSelector(session)。每个 Session 持原始事件窗口 + ConversationNodeAssembler——mux 帧 session/event 只在窗口内 append,notifier(微任务/RAF 批)重建 buildSnapshot();历史 GET 只是 bootstrap 快照回填(首次 sessions.history + 分页/缺口重拉),非轮询,实时全靠 WS push。
双流协议:ConnectionController(packages/client/connection/src/client/connection.ts)——一个 generation = 两条 WS 流 + 严格 host.describe + onOpen 握手 + 指数退避重连;HTTP-up fetch 做 unary/respond,/api/events.mux + /api/events.host 两条只读 WS 下行(client 上行消息被 1008 "downlink only" 拒绝)。帧类型权威在 packages/host/apiproxy/src/api/events.ts:MuxFrame = session/subscribed | session/event | approval/requested | approval/resolved | question/requested | question/resolved | session/queue | session/jobs | session/projection | stream/error;HostFrame = host/session-added | session-removed | session-status | agent-error | workspace-* | remote-event | stream/error。runtime apply 把帧送进 SessionRuntime/WorkspaceRuntime 物化到 SnapshotStore,React 组件经 useSession/useSessions/useProjection 订阅;实时全靠 WS push,历史 GET 只是回填(首次 sessions.history + 分页/缺口重拉,非轮询)。
布局与状态管理:没有 URL 路由表——唯一 ctx 级 renderSlot('root', {}),ui-layout 的 AppFrame 以 CSS Grid 三栏渲染子 slot sidebar | conversation | details | shell.overlay,各 ui-* 插件按 key 注册,会话切换靠 sessions.open(id) 而非 URL 导航。状态管理用 Zustand vanilla(createStore + subscribeWithSelector + shallow)+ Immer(produce)+ dev 深冻结,localStorage 持久化手写(替代 persist 中间件);React 侧 web-react/src/bind.ts 用 useSyncExternalStoreWithSelector 把任意 {getSnapshot, subscribe} 源绑成 hook——无 Redux、无单一全局 store。
标准 kit 与 slot:SessionStandardProps(useSession/sessionId/useProjection)经 SessionProvideChannel 合成,插件 sessions.provide({hooks, props, resolve}) 贡献成员(ui-conversation 提供 useInput/inputActions);三层分家:ui-slots 纯核心(SlotCore)+ runtime SlotRegistry + web-react createSlotRenderer。工具调用 UI 走 tool-call 节点 → tool.call.toolview keyed 槽。
24.4 HMR 与双面构建
- Host 从源码跑(tsx)时用 SRC 回退;
pnpm run dev:web只 watch 带dsh.client声明的 Client 插件并重写lib/client.js。 - 只改 Remote 方法实现体不重新生成 Typert 文件;改了装饰器/导出/参数/返回值等必须按序跑
build:lib:host→build:lib:client。同一个源码按DSH_BUILD_FACE(host|client)双面构建,Host 面与 Client 面永不进入同一个ts.Program——这是 API 安全性的构建期保证。
二十五、总结:dsh 架构的十个特征
- 一切皆插件,无特权核心。模型适配器、工具注册表、会话日志、agent loop 本身都是 Cordis 插件;扩展 dsh 就是"在旁边挂一个插件",所有注册都是可逆 effect。
dsh-agent-loop可替换,UI/hook/tool 插件只依赖dsh-agent。 - 事件溯源是唯一事实源。会话是一条只追加的
SessionEvent日志;模型历史、UI 投影、遥测、标题、目标、计划、todo、压缩全部从这条日志重放派生。"模型可见即已记录"是运行时不变量——每次模型请求都是日志的纯函数。 - 作用域(scope)是并发安全的地基。注册视图向下继承、事件准入向上流动;每个 agent 以自身为 key 获得独立注册面,销毁即回滚。多 agent(含子代理森林)在同一进程内安全共存。
- 接缝(seam)让"换 provider 换世界"。Service Definition / Provider / Consumer 三角色分离:换掉
ctx.fs/ctx.subprocess的 provider,bash、PTY、LSP 一起搬进 E2B 云沙箱;子代理 provider 从进程内 spawn 到 Claude Code 到另一个 dsh 实例。 - fail-closed 与 fail-loud 是安全语言。审批闭集(唯一 grant=allowed-once)、沙箱无后端即
SANDBOX_UNAVAILABLE、能力不支持即UNSUPPORTED_CAPABILITY、waterfall 短路即决策——静默降级在 dsh 里不合法。 - 重放即恢复。fork 取已完成 turn 前缀、resume 靠日志、压缩靠 replace 节点、崩溃恢复补
interruptedmarker、HMR 靠不可变调用绑定——没有"第二状态机",一切状态都可从日志重建。 - 模型自修改运行时。agent 可以用
cordis_*工具检查、定义、运行、卸载自己的插件(node:vm 求值 + 白名单 ctx 门面)——官方明示这"视为与 bash 同等信任",不是安全边界。 - 双面构建的强类型 RPC。Typert 从 TS 类型图生成 Host/Client 契约,Host 面与 Client 面永不进入同一
ts.Program;@Remote一元方法走/api,流式协议另立通道。 - 工程文化把"文档即代码"做到极致。module-graph、事件矩阵、Cordis API 目录、config catalog 全部由脚本生成并 CI 保鲜门控;每个包带
./invariant伴生文件;测试覆盖快照录制重放、llm-mock、agent-loop-testkit、Web 压力/性能。 - 生态分层完整。TypeScript SDK(JSON-RPC)、Python SDK(打包单文件 runtime)、ACP server、MCP client、Web UI(React + slots)、headless 模式——同一个核心,无数种驱动方式。
DeepSeek Harness 目前仍是 developer preview(0.1.0-rc.5),架构在快速迭代,官方明确警告会有破坏性变更。但它的核心设计——以事件溯源为底、以作用域保并发、以接缝做替换、以 fail-closed 立边界——已经展示了一个"可编程、可自修改、可移植执行世界"的 agent 运行时应该长什么样。对于想深入 agent 框架设计的读者,docs/architecture.md、docs/cordis-primer.md、docs/subsystems/ 与 docs/cookbook/ 是最佳入口;本文所有文件引用均可在仓库内以相对路径找到。
本文由 DeepSeek Harness 分析而成:代码分析由 DeepSeek Harness(dsh)驱动、基于 DeepSeek v4 Flash 模型完成——分析过程本身就跑在本文所描述的这套架构之上。