就此AI就此AI

DeepSeek Harness 架构深度解析

2026-08-14 · 就此AI

本文基于 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 概括为五个思想:

  1. 插件是一个实现 Service 的对象。可以是带可选 injectapply(ctx) 的函数,也可以是 Service 子类;Cordis 负责把它的生命周期挂载进当前上下文。
  2. 上下文是服务的仓库。一个服务在上下文中占有一个稳定的键 ctx.<key>(如 ctx.toolsctx.llmctx.sessions),其他插件通过键查找服务,而不是 import 具体实现——这就是"面向接缝"的解耦。
  3. 通过 inject 声明服务依赖。插件声明需要的服务,Cordis 等这些服务就绪后才加载该插件;加载顺序由服务依赖表达,而不是手工排启动顺序。
  4. 类型化事件通信。服务通过 TypeScript declaration merging 声明事件名,然后按 emit / waterfall / parallel / serial 四种模式派发(见下文)。
  5. 注册是可逆的效果。提示词片段、工具 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.ymlbash-sandbox/tool-bashdisabled: !!js process.platform === 'win32'pwsh-sandbox/tool-pwsh 带反向表达式——同一份补丁文件,在 Windows 上恰好挂载一套 shell 栈,在 POSIX 上挂载另一套。

三、Profile 与 Bundle:运行时如何组装

一个运行中的 dsh 是一棵插件树,由启动时按有序层次组合而成(详见 packages/boot/app-boot/README.mddocs/architecture.md)。

3.1 概念

  • Profile(配置档案):一个命名的组合,存放在 Harness home 目录。它列出要叠加的 bundles、安装的外部插件,以及用户自己的 cordis.patch.ymlwebheadless 作为模板随仓库提供。
  • Bundle(分发包):Cordis 配置行 + 其挂载代码的分发格式。Bundle 的配置行可以被上层的 profile 补丁覆盖,保证"什么都能补丁"。

每个包在自己的 package.jsondsh 字段声明身份: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 默认 ~/.dshpackages/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-fstool-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/messageassistant/*tool/*持久会话事件;其余是三个领域的活体扩展点。
  • agent/pre-stepagent/requestllm/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.tsReactLoopAgent 是默认驱动,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 可覆盖);buildRequestrequest/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)

接缝是一个可替换的能力,有三种角色:

  1. 服务定义(Service Definition):声明接口;
  2. 服务提供者(Service Provider):实现它;
  3. 消费者(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/writerequest/headerrequest/contextsession/end-seed 日志型事件(不产生 LLM 消息)

SessionEventMap12 个事件变体。消息来源语义分两层:MessageSourcekind = 谁产生这条消息)+ ContextFormform = 何种信息:instructions / catalog / snapshot / notice / relay / recall)——语义而非视觉。

每个 SessionEventtype 上的判别联合(switch 自动收窄),带 seq(=log.length,自 0 连续单调)、timedata,以及条件字段 surfaceOp/sourceEventSeqs/ignorableignorable 标记纯信息事件供旧读者安全跳过;缺失则要求新事件必须"拒绝而非静默丢弃"。data 必须是 lossless JSON(appendisJsonValue 运行时校验,拒绝 BigInt/函数/循环引用/稀疏数组),保证日志可逐字持久化。

8.2 表面(Surface)机制

SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result'三种会产出 LLM 消息的事件子集。它们必须携带 surfaceOp 说明如何进入有序"表面":

  • SurfaceOp = 'append' | { op: 'replace', start, end }——append 是普通尾部追加;replace 是压缩用于把一段表面节点"遮蔽/替换"成一个总结节点。
  • 消息型事件还可带 sourceEventSeqs 声明它派生的源事件(replace 必须列出所有被遮蔽节点)。

SurfaceManagerpackages/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 类与请求快照

Sessionpackages/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/startturn/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(目标事件+有界窗口)、readSurfacelistEvents
  • 过滤:会话级(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-telemetrySessionTelemetryRecord 携带原始 StreamChunk + 时间戳,走 session-telemetry/record(waterfall,可 redact)后非阻塞送往后端;session-telemetry-otel 是 OpenTelemetry 后端。语义是"捕获侧",batching/retry 归上报 SDK。
  • session-titlefallbackSessionTitle(首个合格人类 prompt 前 N 词,确定性)+ normalizeSessionTitle(清理终端转义、空白归一、按 Unicode 码点截断不劈代理对)+ session/title log-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 走每个单元的 applyapply 对无关事件必须返回同一 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.kinduser/message 表面事件进入日志;
  • pre-step 消息前置:在 agent/pre-step{prepend:true} 把新消息插到消息列表(time-contexttmux-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-sandboxpwsh-sandbox(见"Shell 执行"章节)。容器、microVM、远程执行是"整个能力接缝"的兄弟实现,不是 ctx.sandbox 的 provider。

十一、插件生态:扩展运行时本身

dsh 的"一切皆插件"不只是架构口号——它还允许运行时自我修改

11.1 动态 Cordis(模型自改运行时)

packages/extensions 实现了"模型可以用工具定义并运行新插件"的能力:

  • host-runnerctx.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/identitygetOrCreateAnonymousUserId(随机 UUIDv4,持久化于 $DSH_HOME/.anonymous-user-id 单行;用于 OTel user.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/sdkdeepseek-harness-sdkDeepSeekHarness.run 拉起 dsh 子进程,经 stdio JSON-RPC 通信,默认注入 DSH_CORDIS_CONFIG
  • python/sdk-runtimedeepseek-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 三段)+ ProjectionSnapshotasOfSeq 一致切面)+ 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 处理长上下文的三件套,配合非常精巧:

  1. token-meterpackages/llm/token-meter):ctx.tokenMeter.measure() 返回 TokenMeasurement(logRevision + baseline + surfaceDeltaTokens + totalTokens + surfaceTokens + nodes)——通过重放日志得到"当前 surface(模型可见面)压力"快照。
  2. compactionpackages/compaction/*):CompactionEngine + CompactionTrigger(触发条件)+ 可选 toolResultPruner(先裁剪工具结果再选摘要)。触发点有两个:agent/pre-step 的压力检查(压力触发),与 agent/request-error 的上下文溢出恢复(CONTEXT_WINDOW_EXCEEDED,绕过正常阈值做一次有收益的均衡缩减)。默认后端 BasicCompactionEngine 的阈值:thresholdRatio=0.8(contextWindow 的 80% 触发)、retainRatio=0.16(保留近期尾)、maxTokens=8192compactionRetries=1maxOverflowRetries=1,支持 per-model modelPolicies。范围选择 selectCompactableRange 从头向后锚定、保留定价的近期尾、不拆散 assistant tool-call/result 对。压缩事务:append compaction/start(日志锁防并发)→ tokenMeter.measure 快照 → summarize(唯一可覆盖 hook)→ 稳定性检查 → append compaction/summary + append user/messagesurfaceOp:{op:'replace',start,end},总结必须比遮蔽内容更小,否则失败)→ append compaction/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 人类命令)。
  3. spillpackages/spill/*):SaveTextSpill / SpillOwner / SpillRef(品牌化 SpillLocator)——超大文本(如巨型工具输出)落盘(spill-local),上下文里只放定位符,spill-policy 挂在 tools/code-dispatch-logtools/post-execute 瀑布上决定何时 spill。

关键不变量:request/header + request/context 是 log-only 快照,使"一次模型请求"成为日志的纯函数——运行时不变量断言这一点,也是"模型可见即已记录"的落地方式。

十四、审批、权限预设与安全模型

  • approvalpackages/interaction/user-approval):ApprovalOutcome闭集 fail-closed——唯一 grant 是 allowed-once(一次授权一次有效);ApprovalPolicy 只有 ask / nevernever 是确定性拒绝,不是"从不问")。approval/request 是 waterfall,acpapiproxy 监听;审批记录以 log-only 方式进审计日志。
  • permission-presetspackages/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)。

十五、设置、存储、工作区与不变量

  • settingspackages/settings/settings + settings-file):SettingsNamespace 按 schemastery schema 注册(register<T>(ns, schema, {base?, applies?, validate?})),resolve 层序 defaults → base → composition → user 深合并;SettingsScope update/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)。
  • storagepackages/storage/*):StorageBackend(kv facet)+ StorageForms + DomainSpec/Domainstorage-domain,domain/changed 事件);storage-json / storage-sqlite 两个后端。feedback、attachment 等都挂在 storage-domain 侧车。
  • workspacepackages/workspace/workspace):workspace registry,以 canonical path 标识,attach/detach。
  • invariantspackages/runtime-diagnostics/invariants):运行时不变量注册表——每个包带 ./invariant companion 文件,INVARIANT 归因错误;这是 dsh "文档与代码强一致"工程文化的运行时版本。

十六、工具系统与执行管道

工具是 agent 与世界的接口。packages/core/toolsToolRuntimectx.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 保真可回放"哲学)。

第一方工具用 defineToolpackages/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) 把"继承表面"与"自己层"分开:

  1. 继承表面 = 全局层 + 每个祖先层,按最近祖先优先 shadow 更远者;
  2. restrict() 施加的 allow/deny 掩码只过滤继承表面,绝不锁自己层注册的工具;掩码在链上取交集
  3. 自己层的注册最后并入(shadow 继承同名,不受过滤约束);
  4. 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-clientpackages/mcp/mcp-client):MCP 客户端(无 server 端),一个插件实例连一个 server。connection.ts 是连接主管(重连循环:初始 500ms 指数退避至 30s 封顶、maxAttempts 10 次、一次 outage 共享预算);tools.tstools/list(drain 分页)发现工具并构造 ToolDefinition 注册进 ctx.tools命名契约 mcp__<serverName>__<rawName>(归一化到 ≤64 字符 [A-Za-z0-9_-],有损时拼 12 位 SHA-256 防冲突,wire 上永远用 raw name);传输支持 stdio 与 streamable-http。HMR 热切换(改配置触发断开重连,serverName 不变则工具名不变)。
  • acppackages/acp/acp):ACP(Agent Client Protocol)服务器(automation-only)——把 dsh agent 暴露为 ACP JSON-RPC stdio 对话端,一个 ACP 会话映射为一个 dsh agent;用 @agentclientprotocol/sdkAgentSideConnection + ndjson,实现 initialize/authenticate/newSession/prompt/cancel;经 session/eventagent/inbox/claimedapproval/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_DENIEDworkspace-write 重 resolve 捕获 TOCTOU 并验证 canonical 路径在 writableRoots(policy) 内)、fs-observation-policy 实现"读先于写/编辑、版本守卫"语义(fs/write-intentfs/edit-intent 单槽决策 waterfall + fs/observed)。工具面:tool-fs(read/write/edit/read_image,FsSandboxControllerctx.fs.sandboxModesandbox_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-bashtool-bash-persistenttool-pwshctx.shellEnv
  • packages/subprocess/*SubprocessSpawnSpec全显式零默认)、SubprocessOutputReader.readFrom(fromByte)(偏移式非消费 reader,滑出窗口 lossy + spill 恢复)、credential scrubscrubbedParentEnv 剔除 /KEY|PASSWORD|SECRET|TOKEN/iDSH_*)。本地实现用分离进程树:POSIX detached: true 自成进程组、kill(-pid) 全组信号 + Linux /proc/<pid>/stat 轮询组存活;Windows taskkill /T /F 递归终结。唯一终止动词 terminate()SIGTERM → graceMs → SIGKILLspawnTerminalnode-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-bashBashTerminalBackend:注入受控环境(PS1='dsh> 'PROMPT_COMMAND=OSC 133;D、TERM=dumb),pollReadiness 每 50ms inspectForeground() 按序落定 stdin_read / inferred_idle(3s 静默)/ timeout(30s);BoundedTextBuffer 4MB/1 万行尾保留滚动;sandbox fence:会话开着时若把 sandbox mode 改换则拒绝(防打开 PTY 期间策略偷偷变宽)。工具面六个:terminal_open/send/read/signal/close/listterminal_send 后台走 ctx.jobs)。tool-bash 在结果追加锚定末尾的 [exit code: N] / [killed by signal: X],由 packages/shell/shell/src/render.tsparseExitStatus 逆解析,拆成卡片的正文 + exit-status pill(card:'terminal')。

16.9 代码执行与 LSP

  • packages/code-runtime/*CodeRuntime 接缝(ctx.codeRuntime),well-known 语言 typescriptpython只有 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-stdioctx.subprocess.spawn() 的 raw pipe 做 JSON-RPC 传输(可服务本地或远程实现),应答 server 的 workspace/configuration、拒绝 workspace/applyEditinitialize 握手 + 瞬态 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.fsrealpath -mz 求远程 canonical 路径、原子暂存写、createIfAbsentln -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:sqliteDatabaseSync,wal 模式,owner-only 0o600user_version 物理布局版本)→ 域层(storage-domain:zod schema 验证 + domain/changed 事件,每域单条写链:先 await 后端持久化再突变内存再 emit)。消费方只用数据表单,不直接触后端。
  • packages/attachment/*:不可变二进制附件(图像为主)。attachment-localSHA-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.subagentsSubagentRuntime),而不是单一 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——由 SubagentContinuationManagerpackages/subagent/subagent/src/continuation.ts,1483 行)通过私有 activation-owner scope 创建 AgentHandle;每个子代一个持久 Session、至多一个进程内 Activation。
  • 隔离核心applyChildCompositionchild-agent.ts)给每个子代全新的扁平 scopecomposeFrom(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,超 maxDepthSubagentDepthError
  • 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.jobsJobRegistry 抽象 + LocalJobRegistry 进程内实现):

  • 生命周期running | stopping | completed | killed | failedstart(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_killwait(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.messageFeedbackstorage-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.mddocs/event-producer-consumer.mdscripts/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:e2etest: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-modectx.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/goalctx.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-goalget_goal/create_goal/update_goal,create/update 需 direct human authorityblocked 需 ≥3 轮(blockedAfterConsecutiveRounds 默认 3)。命令面 /goal

21.3 Workflow(工作流编排)

packages/workflow/workflowctx.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-item nullWorkflowRun.result 永不 reject
  • 观察事件workflow/start / phase / log / agent-start / agent-end / end 是 observe-only,供 UI 展示阶段进度。工具面:workflow 工具(tool:workflow section order 115,明示"仅当用户显式要 workflow 或大型多 agent 编排时用")+ tool-ralph(Ralph 循环)。

21.4 Todo(待办)

packages/todo/tool-todotodo_write 工具,整表替换——每调用 append 一条 todo/write 快照事件,重放 last-write-wins;todos 投影单元(turn/start 清零)。策略 allowParallelInProgress(默认单活跃);校验 trim 非空、唯一内容。

21.5 Commands(人类斜杠命令)

packages/interaction/commandsctx.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-questionsctx.userQuestions):provider-neutral 词汇——AskUserQuestionItem{id,question,detail?,header?,options?,multiSelect?,intent?};单 UI provider 注册,跨包可 batch 多问(每个稳定 id 路由回答案);intent(如 kind:'plan-review')打标签改变呈现不改协议,approve 显式命名批准选项而非靠顺序。
  • user-approvalctx.approval):ApprovalOutcome 闭集 fail-closed——唯一 grant 是 allowed-once,missing/throw/rogue 都归一为 unavailableApprovalPolicy('ask'|'never')never 在 dispatch 之前被服务自身决定(rejected,不审任何 answerer);signal.abortedcancelledlog-only 审计:每次 request() 要求 open turn,append approval/asked + approval/decided 审计对(都是 log-only 非模型可见;模型从 approval:policy section(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) 先 append permission/preset(记录用户意图),再各自走 canonical 写入 sandbox/modeapproval/policy

21.7 Schedule(定时提醒)

packages/schedule/schedulesession-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 词汇的唯一源头MessageContentBlockStreamChunkToolSchema 等被 dsh-toolsdsh-system-promptdsh-session 等反向引用),并实现 LlmRuntimectx.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-deltaargumentsDelta 保持原始 JSON 字符串)| block-end(携带完整装配块)| usage | finish。契约:usage 必须在 finish 之前且其后无任何 chunk;tool 参数全程是原始 JSON 字符串;适配器可 throw,但 LlmRuntime.stream() 将其归一为 finish{error|aborted}

BlockAssemblerassembler.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 注册表同模式):SkillProviderlist()/get() 接口)+ SkillCandidate(含 rank/locator)+ SkillSummary + SkillDefinition(含 content)+ SkillInvocationPolicymodelInvocable/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 要求 namedescription,可选 whenToUsemetadata;激活策略读精确 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 正交):

  1. 首个完整快照时注入初始 <available_skills> 目录;
  2. 后续每步计算目录摘要(sha256),变化则 agent.inject() 全量替换(目录只含 name + XML 转义 description,不含正文/路径/source);
  3. skill({name}) 工具:校验 kebab-case → 查目录 → isModelInvocable 门禁 → ctx.skills.get() 重读 → 返回 renderSkillContent<skill_content>/<skill_resources>/<skill_instructions>,含 XML 转义防 framing 注入);
  4. /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):

  • webserverpackages/host/webserver):纯 node:http 服务器插件(WebServerctx.webServer),只做连接与路由、不服务任何文件;config 只有 host127.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。
  • connectionpackages/client/connection):RPC 载体——请求关联、信任边界、取消、响应信封、/api HTTP 桥,注册 /api/events.mux/api/events.host 两条升级(WebSocket)路由。
  • gatewaypackages/api/gateway + packages/api/remotes):Typert 网关,ctx.typertGateway 认领两段 Remote 端点、解析对象/上下文、调用活体 Cordis 服务、校验请求与返回值;packages/host/apiproxy 处理没有 Remote 描述符的遗留端点。
  • remotes:业务服务的 Remote 声明与贡献。

关于 SSE 的澄清:agent 事件流不是 SSE,而是两条 WebSocket 下行流;SSE 只有两个真实用途——(a) 进程内载体apiproxysseResponse 把 Mux/Host 帧流包成 text/event-stream(Electron 走 fetch/IPC 桥时用);(b) HMR 开发通道packages/client/hmrGET /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 参数 → agentId wire 字段),运行时经 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。

双流协议ConnectionControllerpackages/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.tsMuxFrame = session/subscribed | session/event | approval/requested | approval/resolved | question/requested | question/resolved | session/queue | session/jobs | session/projection | stream/errorHostFrame = 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-layoutAppFrame 以 CSS Grid 三栏渲染子 slot sidebar | conversation | details | shell.overlay,各 ui-* 插件按 key 注册,会话切换靠 sessions.open(id) 而非 URL 导航。状态管理用 Zustand vanillacreateStore + subscribeWithSelector + shallow)+ Immer(produce)+ dev 深冻结,localStorage 持久化手写(替代 persist 中间件);React 侧 web-react/src/bind.tsuseSyncExternalStoreWithSelector 把任意 {getSnapshot, subscribe} 源绑成 hook——无 Redux、无单一全局 store

标准 kit 与 slotSessionStandardProps(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:hostbuild:lib:client同一个源码按 DSH_BUILD_FACE(host|client)双面构建,Host 面与 Client 面永不进入同一个 ts.Program——这是 API 安全性的构建期保证。

二十五、总结:dsh 架构的十个特征

  1. 一切皆插件,无特权核心。模型适配器、工具注册表、会话日志、agent loop 本身都是 Cordis 插件;扩展 dsh 就是"在旁边挂一个插件",所有注册都是可逆 effect。dsh-agent-loop 可替换,UI/hook/tool 插件只依赖 dsh-agent
  2. 事件溯源是唯一事实源。会话是一条只追加的 SessionEvent 日志;模型历史、UI 投影、遥测、标题、目标、计划、todo、压缩全部从这条日志重放派生。"模型可见即已记录"是运行时不变量——每次模型请求都是日志的纯函数。
  3. 作用域(scope)是并发安全的地基。注册视图向下继承、事件准入向上流动;每个 agent 以自身为 key 获得独立注册面,销毁即回滚。多 agent(含子代理森林)在同一进程内安全共存。
  4. 接缝(seam)让"换 provider 换世界"。Service Definition / Provider / Consumer 三角色分离:换掉 ctx.fs/ctx.subprocess 的 provider,bash、PTY、LSP 一起搬进 E2B 云沙箱;子代理 provider 从进程内 spawn 到 Claude Code 到另一个 dsh 实例。
  5. fail-closed 与 fail-loud 是安全语言。审批闭集(唯一 grant=allowed-once)、沙箱无后端即 SANDBOX_UNAVAILABLE、能力不支持即 UNSUPPORTED_CAPABILITY、waterfall 短路即决策——静默降级在 dsh 里不合法。
  6. 重放即恢复。fork 取已完成 turn 前缀、resume 靠日志、压缩靠 replace 节点、崩溃恢复补 interrupted marker、HMR 靠不可变调用绑定——没有"第二状态机",一切状态都可从日志重建。
  7. 模型自修改运行时。agent 可以用 cordis_* 工具检查、定义、运行、卸载自己的插件(node:vm 求值 + 白名单 ctx 门面)——官方明示这"视为与 bash 同等信任",不是安全边界。
  8. 双面构建的强类型 RPC。Typert 从 TS 类型图生成 Host/Client 契约,Host 面与 Client 面永不进入同一 ts.Program@Remote 一元方法走 /api,流式协议另立通道。
  9. 工程文化把"文档即代码"做到极致。module-graph、事件矩阵、Cordis API 目录、config catalog 全部由脚本生成并 CI 保鲜门控;每个包带 ./invariant 伴生文件;测试覆盖快照录制重放、llm-mock、agent-loop-testkit、Web 压力/性能。
  10. 生态分层完整。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.mddocs/cordis-primer.mddocs/subsystems/docs/cookbook/ 是最佳入口;本文所有文件引用均可在仓库内以相对路径找到。


本文由 DeepSeek Harness 分析而成:代码分析由 DeepSeek Harness(dsh)驱动、基于 DeepSeek v4 Flash 模型完成——分析过程本身就跑在本文所描述的这套架构之上。

查看更多文章 →

正在加载完整版…