ai · 2026 · 技术研究笔记
DeepSeek Harness 架构全解:从 Web 请求到 Agent Loop、Trajectory 与本地沙箱
一条用户消息如何穿过浏览器、RPC、Agent、工具和操作系统
沿着一次真实请求拆解 DeepSeek Harness:Cordis 插件装配、Web 与 Typert RPC、Agent Turn/Step、工具管线、Session Event、Trajectory、Shell、Subprocess、权限审批和跨平台沙箱
研究版 · 35 个章节 · 约 39 分钟 · 更新于 2026-08-14
在 DeepSeek Harness 的 Web 页面里输入一句“检查这个项目并运行测试”,浏览器最终可以让本机启动 pnpm test。表面上,这像是一条从网页直达终端的调用;源码里的真实链路却经过配置装配、双侧插件运行时、RPC、Agent 收件箱、模型请求、工具注册表、权限审批、沙箱包装和进程树管理。
只把它称为“一个带 Web UI 的聊天机器人”会漏掉核心设计。DeepSeek Harness 是一个 Agent Harness:Harness 指承载和约束 Agent 的运行时,负责把模型、工具、上下文、会话、权限、存储、用户界面和外部协议装配成一套可运行系统。模型只是其中一个可替换插件。
本文基于 DeepSeek Harness 0.1.0-rc.5 附近的源码结构。项目仍处于 developer preview,包名、配置和交互界面可能继续变化;文中重点是当前架构中的职责划分和调用关系。
先看全景:它不是一层,而是六层
一次 Web 对话可以先压缩成六层:
浏览器层
React UI、Chat、Trajectory、设置、审批交互
│
▼
连接与 API 层
Connection、Typert RPC、API Gateway、API Remotes
│
▼
Agent 编排层
Agent Registry、Inbox、Agent Loop、Turn、Step
│
▼
模型与工具层
System Prompt、LLM Adapter、Tool Registry、Tool Pipeline
│
▼
能力实现层
Shell、Filesystem、Web、LSP、Terminal、Subagent、Workflow
│
▼
操作系统层
Subprocess、Seatbelt、bubblewrap、Landlock、Windows ACL
六层之间不靠一个中央对象直接调用所有模块。大部分能力以 Cordis 插件形式注册服务或事件,调用方只依赖稳定的服务键,例如 ctx.llm、ctx.tools、ctx.sessions 和 ctx.sandbox。
最重要的设计前提:everything is a plugin
Cordis 是 DeepSeek Harness vendored,也就是固定版本复制进仓库维护的插件框架。理解 Cordis,只需要先掌握五个概念。
Plugin 是可挂载和卸载的功能单元。模型适配器、工具注册表、会话日志、Agent Loop,甚至 Web 客户端里的视图都可以由插件贡献。
Context 是服务容器。插件从 ctx.<key> 取得能力,而不是直接实例化具体实现。Shell 消费方依赖 ctx.shell,不需要知道后面是本地 Bash、PowerShell,还是远程 E2B 环境。
Service 是具名能力。例如 ctx.sessions 管理会话,ctx.tools 管理模型工具,ctx.llm 负责模型流式请求。
Event 是扩展点。插件可以监听 Agent 请求、工具执行、文件写入或会话事件,在不修改 Agent Loop 的情况下加入策略和行为。
Effect 是可撤销注册。工具、监听器和服务通过 ctx.effect() 或 ctx.on() 注册,插件卸载时相应资源会按生命周期撤回。它解决的不只是“如何加载插件”,还包括“重新加载时如何释放旧监听器、进程和注册项”。
这套结构带来一个直接结果:DeepSeek Harness 没有一个需要不断增加分支的特权核心。增加模型提供方时注册 LLM adapter;增加工具时注册 Tool Definition;增加沙箱时提供 ctx.sandbox 后端;增加 UI 视图时向客户端 slot 注册视图。
Service Definition、Provider 与 Consumer
项目把可替换能力称为 capability seam,即能力接缝。一个完整接缝包含三种角色:
Service Definition
定义能力接口和数据类型
│
▼
Service Provider
提供本地、远程或平台相关实现
│
▼
Consumer
把能力交给模型、UI 或另一个服务使用
以 Shell 为例:
dsh-shell定义请求、结果和执行器接口。dsh-bash-local或dsh-pwsh-local提供本地执行实现。dsh-tool-bash或dsh-tool-pwsh把 Shell 暴露为模型可调用的工具。dsh-bash-sandbox与dsh-pwsh-sandbox在 Provider 外增加沙箱包装。- 最底层的
dsh-subprocess-local统一管理真实进程树。
如果把 Provider 换成远程沙箱,工具 schema 和 Agent Loop 不需要复制一份。只要新的 Provider 遵守同一接口,消费者仍然调用 ctx.shell。
从目录理解系统规模
源码可以按职责读取,而不必逐包浏览:
apps/
cli/ dsh 命令入口
web/ 浏览器静态壳和 Vite 构建入口
packages/
boot/ 配置、profile 与应用启动
bundle/ base、web-app、headless 等装配层
core/ session、prompt、tools、agent、agent-loop
api/ Typert Gateway 与 Web BFF
client/ 浏览器运行时、Chat、Trajectory、UI 插件
llm/ 模型能力定义和 DeepSeek 等 Provider
shell/ Bash/PowerShell 能力、工具与沙箱消费者
subprocess/ 进程树、输出、终止和 PTY
sandbox/ 沙箱接口及各平台本地实现
fs/ 文件系统能力和写入策略
terminal/ 持久终端会话
session/ 持久化、投影、标题、统计和遥测
interaction/ 审批、命令和提问
subagent/ 子 Agent 与外部委派
skill/ Skill 注册、目录和加载工具
workflow/ 后台工作流
typert/ 类型图、代码生成、加载和运行时注册
vendor/ Cordis 及相关 vendored 依赖
native/ Linux Landlock 原生 runner
examples/ 可运行的组合示例
docs/ 架构、子系统与生成目录
packages/ 中包数量很多,不代表每次运行会加载全部包。真正运行的是 profile 和 bundle 装配出的插件树。
启动第一步:Profile 与 Bundle 决定“这个 dsh 是什么”
Profile 是具名运行组合,例如 web 和 headless。Bundle 是可以叠加的 Cordis 配置层,例如基础能力 bundle 和 Web 应用 bundle。
Web profile 的装配顺序是:
空配置
│
├─ dsh-base bundle
│ 模型、工具、Session、持久化、Shell、沙箱、权限等
│
├─ dsh-web-app bundle
│ Web Server、Connection、API、浏览器插件清单等
│
├─ profile/cordis.patch.yml
│ 当前 profile 的覆盖项
│
├─ home/cordis.patch.yml
│ 用户全局覆盖项
│
└─ --patch overlay
本次启动临时覆盖项
Patch 通过稳定的 id 定位配置行。覆盖一行时会替换该行的完整 config,所以配置的最终结果是确定的。下面的命令可以打印机器上真正启动的插件树:
dsh --profile web --dump-config
从源码运行时,对应命令是:
pnpm dsh --profile web --dump-config
这个机制使“产品形态”也成为配置结果。Headless profile 不需要 Web Server;Web profile 增加浏览器界面;企业部署还可以通过更高层 patch 替换模型、持久化后端或权限策略。
pnpm dsh web 启动了哪些部分
启动链路可以拆成四个阶段:
- CLI 解析
web、profile、端口和 patch 参数。 - Boot 层找到 profile 及其 bundles,合并 Cordis patch。
- Cordis Loader 根据
inject服务依赖挂载插件。需要的服务尚未出现时,插件等待依赖,而不是依靠一份人工维护的固定启动顺序。 - Web Host 绑定本地地址,提供静态资源、Connection 和
/api入口;页面启动后再根据 Host 注入的 boot manifest 加载客户端插件。
一个容易遇到的限制是:apps/web 不是可以单独执行的普通 Vite 应用。它需要 Host 注入 window.__DSH_BOOT__,其中包含客户端模块表和启动信息。因此直接运行 vite 或只执行 pnpm --filter ... dev 会被明确拒绝。正确入口是:
pnpm dsh web
如果需要客户端插件热更新,再同时运行项目提供的 Web 开发脚本。
Web 为什么不能直接执行本地命令
浏览器 JavaScript 受浏览器安全模型限制,不能直接调用 child_process.spawn(),也不能任意读写本机文件。DeepSeek Harness 使用 Host/Client 双运行时:
sequenceDiagram
participant UI as Browser React UI
participant CR as Client Cordis Runtime
participant Conn as Connection
participant API as Typert API Gateway
participant Host as Host Cordis Runtime
UI->>CR: 调用 ctx.remote 的类型化方法
CR->>Conn: RPC call('/api', endpoint, args)
Conn->>API: HTTP/连接帧进入可信 Host endpoint
API->>Host: 查找服务、作用域与公开方法
Host-->>API: JSON 可编码结果
API-->>Conn: 关联 request id 返回
Conn-->>CR: 校验并解码结果
CR-->>UI: 更新客户端状态
这里的 BFF 是 Backend for Frontend,即为某个前端选择和整理后端能力的服务层。dsh-api-remotes 负责选择 Web 客户端能访问的 Host 能力,并处理 Agent/Session 身份解析。它不负责真正传输;Connection 负责请求关联、取消、帧和连接生命周期。
Typert RPC 是项目的类型化远程调用系统。构建阶段生成 Invocation Descriptor,即端点、参数、返回值和编解码规则的描述。客户端调用时把位置参数整理为具名 args;Host 收到后重新读取当前 Cordis 服务,校验参数、解析 Agent 或 Session 作用域,再调用标注为 @Remote 或 @RemoteScope 的方法。
它不是把 Host 上所有 JavaScript 对象自动暴露给浏览器。客户端能力由构建时显式贡献,Host 方法也必须显式登记。这个白名单式结构限制了浏览器能触达的服务面。
用户发送消息后,先进入 Agent Inbox
Web UI 发送消息后,Host 找到对应 Agent,并调用 Agent 的 follow-up 或等价入口。消息先进入 Inbox,即 Agent 的输入队列。
Inbox 不只是排队。它需要处理:
- 新用户消息唤醒空闲 Agent。
- Agent 执行过程中到达的 follow-up。
- Steering,即在执行中调整后续方向的消息。
- Injected Context,即插件加入但不一定立即唤醒 Agent 的上下文。
- 下一步输入的 claim,即由驱动器原子地认领本步骤要处理的输入。
队列变化通过 agent/inbox/* 活事件通知 UI;真正进入模型上下文的消息则写入持久 Session Event。活事件适合描述“现在正在排队或运行”,Session Event 适合描述“之后重放时仍然成立的事实”。
Turn 和 Step 不是同一个概念
DeepSeek Harness 把运行划分为 Turn 与 Step。
Turn 是一次从输入开始、直到当前没有待办工作的完整回合。一次 Turn 可以包含零个或多个 Step。
Step 是一次模型请求以及该请求产生的全部工具执行。模型调用工具后,需要把工具结果送回模型,这会产生下一个 Step,但仍可属于同一个 Turn。
例如:
Turn 3
Step 1: 用户要求检查项目
模型调用 read、bash
Step 2: 模型看到文件和命令结果
模型调用 edit
Step 3: 模型看到编辑结果
模型调用 bash 跑测试
Step 4: 模型读取测试结果并给出最终回答
Turn 也可能没有 Step。agent/pre-step 可以拒绝本次输入,或者把第一次进入的消息改写为空。系统仍记录 turn/start 与 turn/end,从而保留“这次尝试发生过,但没有发起模型请求”的事实。
一次完整 Agent Turn 的真实时序
下面是核心链路:
sequenceDiagram
participant User as 用户/Web
participant Agent
participant Loop as Agent Loop
participant Session
participant Prompt as System Prompt
participant LLM
participant Tools
User->>Agent: followup(message)
Agent->>Loop: Inbox 唤醒驱动器
Loop->>Session: turn/start
Loop->>Agent: claim 输入
Loop->>Loop: agent/pre-step waterfall
Loop->>Session: step/start
Loop->>Session: user/message
Loop->>Prompt: 组装提示词与工具 schema
Loop->>Loop: agent/request waterfall
Loop->>LLM: llm/stream
loop 模型流式输出
LLM-->>Loop: StreamChunk
Loop->>Session: assistant/chunk
end
Loop->>Session: assistant/message
loop 模型请求的工具
Loop->>Session: tool/call
Loop->>Tools: pre-execute → execute → post-execute
Tools-->>Loop: ToolResult
Loop->>Session: tool/result
end
Loop->>Session: step/end
alt 工具结果要求继续请求模型
Loop->>Loop: 开始下一个 Step
else 当前没有待办输入
Loop->>Loop: agent/turn-stopping
Loop->>Session: turn/end
end
其中 agent/pre-step、agent/request、llm/stream 和工具的三个阶段采用 waterfall。Waterfall 是可包裹的中间件链:监听器调用 next() 才会把控制权交给下一层;不调用就会短路后续链路。它可以改写输入、增加策略、替换 Provider,也能直接拒绝请求。
System Prompt 和工具 schema 每一步都会重新组装
模型看到的请求不是启动时固定的一段字符串。每个 Step 开始时,ctx.systemPrompt 根据当前插件注册项组装:
- 身份和工作目录等基础 persona。
- 当前模型与运行环境说明。
- Skill 内容和项目指令。
- 沙箱与权限状态。
- 各工具的 JSON Schema。
- 插件注入的上下文区段。
工具是否可见也可以按 Agent scope 调整。某个 Agent 可以拥有 Bash,另一个只拥有只读 Web Search;同一个会话在权限模式变化后,运行时上下文也会反映新的沙箱状态。
项目坚持一条重要规则:model-visible 等价于 logged。任何进入模型请求的动态内容,都必须能够从 Session Log 重建。否则会出现页面显示一套历史、恢复会话后模型看到另一套历史的问题。
LLM 层只负责流式模型能力
ctx.llm 定义消息词汇、StreamChunk 和 adapter 接口。具体的 DeepSeek Provider 负责:
- 把 Harness 消息转换为模型 API 请求。
- 发送请求并处理取消。
- 把模型返回统一为 StreamChunk。
- 报告文本、reasoning、tool call、结束原因和 token usage。
Agent Loop 不应该出现 if provider === ...。Provider 的差异停留在 adapter 内。原始流块逐条记录为 assistant/chunk,最终组装结果记录为 assistant/message。前者保证流式 UI 和重放精度,后者用于派生下一次模型请求的历史。
工具调用不是 execute() 一步完成
模型产生 tool call 后,Harness 先记录 tool/call,再进入工具管线:
模型给出 tool call
│
▼
tools/pre-execute waterfall
Hook、权限策略、请求改写、审批请求
│
▼
Monotonic Guards
只能拒绝或放行,后面的插件不能推翻已有拒绝
│
▼
tools/execute waterfall
超时、重试、指标等 around middleware
│
▼
ToolDefinition.execute()
工具主体
│
▼
tools/post-execute waterfall
接受、阻止、替换结果、增加上下文
│
▼
finalizeContent()
工具拥有的最终内容约束
│
▼
tools/result 通知
│
▼
Session tool/result
tool/call 在执行前写入日志,所以崩溃或取消后仍能看到调用已开始。最终只产生一个模型可见的 tool/result。工具还可以附带 meta 给 UI 渲染,例如文件编辑工具把 diff 展示数据保存在日志里;恢复会话时可以重建同一张工具卡片。
从模型的 bash 调用到本机进程
当模型选择 bash 工具时,链路继续向下:
flowchart TD
A[模型产生 bash tool call] --> B[dsh-tool-bash 参数校验]
B --> C[tools/pre-execute 权限与 Hook]
C --> D{是否请求越权模式}
D -->|是| E[ctx.approval 用户审批]
D -->|否| F[解析当前 sandbox mode]
E -->|允许一次| F
E -->|拒绝或取消| Z[返回拒绝结果,不执行命令]
F --> G[dsh-bash-sandbox]
G --> H[ctx.sandbox.wrap 生成 runner argv]
H --> I[dsh-subprocess-local spawn]
I --> J[bash -c command]
J --> K[收集 stdout、stderr、退出码]
K --> L[工具结果渲染并写入 Session]
dsh-tool-bash 是 Consumer,负责模型参数、审批字段和结果展示。dsh-bash-sandbox 是受限执行 Provider,先把原始命令交给 ctx.sandbox 包装。dsh-subprocess-local 才真正调用 Node 的进程 API。
本地 Bash 每次调用都会启动新的非 login shell:
bash -c <command>
它不会加载用户的 shell rc 文件,也不保留上次调用的 cd、变量或 shell 函数。工作目录由执行器配置传入。持久交互式会话属于 Terminal/PTY 能力,不应与一次性 Bash 工具混为一谈。
Subprocess 层负责进程树,而不只是一个 PID
命令可能启动管道、子 shell 或后台子进程。只终止父 PID 会留下孤儿进程,所以 dsh-subprocess-local 管理的是进程树:
- POSIX 系统把子进程放入独立 process group,以负 pgid 向组发送信号。
- 正常终止先发送 SIGTERM,超过 grace period 后再发送 SIGKILL。
- Windows 使用
taskkill /PID <pid> /T /F终止进程树。 - 插件卸载时终止并等待仍存活的进程。
- 普通进程输出按 stdout、stderr 分别限制内存尾部大小。
- 超过内存预算的完整输出可以写入权限收紧的临时 spill 文件。
- 环境变量会删除名称包含 KEY、PASSWORD、SECRET、TOKEN 的项以及陈旧的
DSH_*项,再合并调用方明确传入的环境。
这里的输出策略保留 tail,即末尾内容,因为编译器错误、测试摘要和退出原因更常出现在输出尾部。超长输出不会无限占用 Agent 进程内存。
权限模式与沙箱模式是两件事
默认 Web 组合提供三种 preset:
| 模式 | 文件权限 | 审批策略 |
|---|---|---|
read-only | 不允许普通文件写入 | 越界时询问 |
workspace-write | 允许写工作区和临时目录 | 越界时询问 |
danger-full-access | 不加文件沙箱限制 | 不询问 |
Sandbox mode 决定操作系统实际允许进程访问哪些文件。Approval policy 决定请求越过当前范围时是否暂停并询问用户。审批通过一次,不是永久关闭沙箱,而是本次调用使用请求的更宽模式。
权限请求还要求 justification,即模型必须说明为什么需要越权。拒绝、取消、没有可用审批界面或审批服务缺失时,命令都不会执行。
DeepSeek Harness 的本地沙箱是什么
dsh-sandbox-local 根据平台选择 runner,并把策略转换成启动参数:
| 平台 | 首选机制 | 当前限制目标 |
|---|---|---|
| macOS | Seatbelt,通过 sandbox-exec | 文件写入范围 |
| Linux | bubblewrap,失败时尝试 Landlock | 文件写入范围 |
| Windows | Restricted Token + NTFS ACL | 文件写入范围,报告 partial enforcement |
macOS profile 使用“默认允许,再拒绝全部 file-write,最后加入写白名单”的方式。workspace-write 允许工作区、/tmp 和当前用户的 Darwin 临时目录;read-only 只保留运行所需的最小写入路径。
Linux 优先探测 bwrap。bubblewrap 能使用 Linux namespace 和 bind mount 建立受限文件视图。如果 bwrap 不可用,项目还提供 Landlock runner。Landlock 是 Linux 内核的非特权访问控制机制,但旧 ABI 能限制的操作类别较少,因此 Provider 会如实报告 partial enforcement。
Windows 通过受限访问令牌、工作区专用 SID 和 ACL 授权实现。每个会话/工作区对还有独立临时目录和可撤销 ACE,ACE 是 NTFS Access Control Entry,即访问控制列表中的一条授权记录。由于 Windows 进程初始化仍需保留部分 Everyone 权限,以及硬链接可能让不同路径指向同一文件对象,项目明确把它标记为部分强制执行。
如果平台 runner 不可用,Harness 返回 SANDBOX_UNAVAILABLE,不会静默改成无约束执行。这一点很关键:安全机制损坏与用户命令失败是两种不同错误。
这个沙箱当前没有覆盖什么
DeepSeek Harness 当前的本地 profile 重点限制文件写入。它不能等价为完整 VM 或容器隔离:
- 没有在该 profile 中统一限制网络出口。
- 没有依靠沙箱限制 CPU 和内存配额。
- 进程仍运行在宿主内核中。
workspace-write主要回答“能写哪里”,不是“能看见什么进程和网络”。- Windows 与部分 Landlock 环境明确只承诺 partial enforcement。
因此,它与 E2B、Firecracker microVM 或独立 Docker 容器不是同一隔离等级。Harness 的设计价值在于沙箱本身也是 capability seam:部署方可以替换 Provider,把 FS、Shell、Terminal 和 LSP 指向同一个远程执行环境。
Session Event Log 是整套系统的事实来源
每个 Session 是只追加的 event-sourced log。Event sourcing 指不直接保存一份不断覆盖的最终对象,而是记录按顺序发生的事实,再从事实投影出当前状态。
核心事件包括:
turn/start
step/start
user/message
request/header
request/context
assistant/chunk
assistant/message
tool/call
tool/result
step/end
turn/end
其他插件可以通过 TypeScript declaration merging 扩展 SessionEventMap,例如 compaction、todo、hook 和业务状态事件。
日志承担四个职责:
deriveMessages()从日志派生下一次模型请求历史。- Web UI 从日志构建 Chat 和工具卡片。
- Trajectory 从同一事件窗口构建执行账本。
- 持久化、恢复、fork、transcript 和 telemetry 使用同一事实流。
这样不会存在“数据库里一套历史、模型内存里另一套历史、页面里第三套历史”。
Session 如何持久化
内存 Session 负责事件语义,持久化是独立 seam。项目提供协调层以及 JSONL、SQLite 等后端。JSONL 是 JSON Lines,即每行一个 JSON 记录的追加格式;SQLite 则适合索引和结构化查询。
持久化需要处理的不只是 append(event):
- 写入顺序必须与连续 event sequence 对齐。
- 多个快速事件需要 write-behind 合并,以减少同步 I/O。
- checkpoint policy 决定何时要求数据已经持久落盘。
- 崩溃恢复要识别完整和不完整的生命周期区间。
- 重新打开 Session 时要标识 seed history 与本次 live history 的分界。
- 投影缓存可以避免每次打开长会话都从第一个事件重新计算所有 UI 状态。
Session 格式变化和后端 schema 变化分别拥有版本策略,避免把内存事件类型、JSONL 文件和 SQLite 表结构混成一个版本号。
Trajectory 到底是什么
Trajectory 不是隐藏思维链,也不会读取模型内部不可见的推理过程。它是客户端插件 dsh-client-ui-trajectory 提供的 turn-aware event ledger,即按 Turn 和 Step 组织的事件账本。
它从 Session window 和请求检查数据投影出这些记录:
SYSTEM:当前 System Prompt 和工具目录发生的状态变化。USER:用户输入。CONTEXT:由插件注入的模型可见上下文。MESSAGE:Assistant 输出,包括有记录时的 token usage。TOOL:工具调用、参数、结果、错误和耗时。SUBTOOL:Code Mode 等场景中的嵌套工具调用。COMPACTED:上下文压缩事件。
界面把记录按 Turn 分区,用粗线表示 Turn 边界,用 Step 标记表示同一回合中的多次模型请求。选中一条记录后,可以检查 Input、Output、Timing、token usage、工具 schema 和 reasoning 字段中确实被 Provider 记录的内容。
Trajectory 上方还有类似浏览器 Network 面板的时间轴:
- Assistant span 可以拆出 TTFT 与 decoding。TTFT 是 Time To First Token,即从请求开始到第一个输出 token 的时间。
- Tool span 显示调用起点与执行时间。
- 拖动时间范围可以过滤该区间内活跃的记录。
- 滚轮可以缩放时间范围,右键拖动可以平移视口。
- 长会话从尾部打开,向上触发旧页加载。
- DOM 只挂载可见行和少量 overscan,避免数千事件导致页面节点无限增长。
Trajectory 与 Chat 是两个独立投影。它不扫描 Chat 已经渲染出的 React 节点,也不修改 Chat snapshot;两者都从共享 Session 数据构建自己的状态。这能防止一个视图的展示结构变成另一个视图的数据接口。
Trajectory 为什么对调试很重要
普通 Chat 更适合回答“Agent 对用户说了什么”,Trajectory 更适合回答:
- 这个 Turn 为什么发起了四次模型请求?
- 哪一步加入了新的系统提示词或工具 schema?
- 模型在第几步请求了 Bash?
- 工具调用等待审批多久,实际执行多久?
- 首 token 延迟和解码时间各占多少?
- 上下文压缩发生在两个 Turn 之间,还是某个 Turn 内?
- 某次失败来自模型 Provider、工具策略、沙箱 runner,还是用户命令退出码?
它更接近可重放的执行账本,而不是一组临时 debug log。数据来源是 Session Event,因此刷新页面或恢复 Session 后仍能重建。
Chat、Trajectory 和 Waterfall 是三个不同观察角度
Web 客户端通过 conversation view slot 注册多个视图:
- Chat 面向用户对话,合并流块,突出消息与工具卡片。
- Trajectory 面向 Turn/Step、token、耗时和输入输出检查。
- Waterfall 面向按时间排列的执行瀑布,用于观察跨度关系。
这些视图都只是客户端插件贡献。Host Agent Loop 不需要知道页面当前打开的是哪个 Tab。
Compaction 如何加入而不修改 Agent Loop
长会话会超过模型上下文窗口。Compaction 是压缩历史的能力:把较早内容总结成更短表示,并调整后续模型可见 surface。
基础 compaction Provider 监听 agent/pre-step。在发请求前检测上下文压力;必要时先裁剪部分工具结果,再生成 summary。模型 Provider 返回标准 context overflow 时,它还可以在 agent/request-error 阶段恢复。
Compaction 的开始、摘要和结束都写入 Session Event。这样后续模型请求、Trajectory、恢复和审计都能解释“历史为什么变短”,而不是在内存里悄悄替换消息数组。
Subagent 为什么也是一个 capability seam
主 Agent 可以把子任务交给其他 Agent,但“子 Agent”不只代表在本进程新建一个对象。Provider 可以实现为:
- 同一 Harness 内创建 fresh child agent。
- fork 当前 Session 的历史边界。
- 把任务委派给另一个 Agent 产品。
- 在远程环境创建执行单元。
上层 Consumer 只依赖 subagent 接口。父子 Agent 的实时状态通过 Agent 事件协调,需要持久保留的结果写入 Session。Web 侧可以显示子 Agent 活动,但 UI 不是子 Agent 生命周期的所有者。
Skill、自我修改和项目指令如何进入上下文
Skill 是可加载的工作流说明和资源集合。Skill Provider 负责目录、注册和内容加载,模型工具负责发现或加载 Skill。项目级 AGENTS.md、子目录指令和 Skill 内容最终都属于模型可见输入,所以需要经过注入机制并出现在可重建的 Session surface 中。
自我修改能力也遵守插件结构:Agent 检查和挂载自己的插件,不等于 Agent Loop 内写死“修改自己”的分支。这样权限策略、日志和卸载语义仍然适用。
前台 Bash、后台 Job 与持久 Terminal 的区别
三种命令能力经常被混淆:
| 能力 | 生命周期 | 状态 | 适用任务 |
|---|---|---|---|
| 前台 Bash/Pwsh | 单次工具调用 | 不保留 shell 状态 | 编译、测试、一次性命令 |
| Background Job | 跨多个 Step 存活 | 保留进程句柄和输出游标 | 开发服务器、较长任务 |
| Terminal/PTTY | 持久会话 | 保留 cwd、shell 和交互状态 | REPL、交互式 CLI、终端操作 |
后台 Bash 启动后会把 ShellProcess 句柄注册给通用 Jobs runtime。模型通过 job_list、job_read、job_kill 查询和控制,而不是让 Bash 工具私自维护一套后台任务协议。
Terminal 使用 node-pty 创建伪终端。PTY 是 pseudo-terminal,向程序提供类似真实终端的输入输出环境,适合需要 TTY 检测、提示符或持续输入的程序。它仍复用 subprocess 的进程检查和清理能力。
错误如何沿链路返回
把所有失败都显示成“命令失败”会丢失诊断信息。Harness 区分多个责任层:
RPC 参数或端点错误
Typert GatewayError
模型请求错误
agent/request-error 与 Provider 错误
工具参数或策略错误
标准化 ToolResult isError
审批拒绝
命令未执行,返回明确拒绝结果
沙箱 runner 损坏
SANDBOX_UNAVAILABLE 或 runner failure
命令自身失败
stdout/stderr + [exit code: N]
超时或取消
timedOut、aborted、signal 分开分类
沙箱 launcher 启动失败与沙箱内命令返回 127 也不是同一件事。Provider 携带 runner failure signature,Consumer 结合退出信息判断故障归属,防止把安全机制失效当成普通命令错误。
一次“运行测试”的端到端路径
把前面的模块连起来,可以得到完整流程:
- 用户在 Web Composer 输入“运行测试并解释失败”。
- Browser Client 通过类型化 Remote 调用 Host 的 Agent 能力。
- API Gateway 校验 endpoint 和参数,通过 Session/Agent resolver 找到目标 Agent。
- 消息进入 Agent Inbox,并唤醒 Agent Loop。
- Loop 写入
turn/start,claim 输入,通过agent/pre-step。 - Loop 写入
step/start和user/message。 - System Prompt 聚合 persona、项目指令、Skill、权限状态与工具 schema。
agent/request和llm/stream把请求送到 DeepSeek Provider。- 每个模型流块写入
assistant/chunk,浏览器实时显示。 - 模型返回
bash({ command: "pnpm test" })。 - Loop 写入
tool/call。 - Tool Registry 校验 JSON 参数并执行
tools/pre-execute。 - 当前模式是
workspace-write,命令没有申请更高权限,进入 sandbox executor。 - Sandbox Provider 在 macOS 生成 Seatbelt argv,在 Linux 生成 bwrap/Landlock argv,在 Windows 走受限令牌和 ACL。
- Subprocess Runtime 启动受限
bash -c "pnpm test"或 PowerShell,并管理整棵进程树。 - stdout/stderr 被限长收集,超长完整输出按配置 spill 到临时文件。
- Tool 把输出、退出码、超时和沙箱拒绝事实渲染为 ToolResult。
- Loop 写入
tool/result和step/end。 - 因为模型还需要解释测试结果,Loop 在同一 Turn 开启下一个 Step。
deriveMessages()从 Session Log 派生历史,把工具结果送回模型。- 模型返回最终回答;Loop 写入 Assistant 事件并关闭 Turn。
- 持久化层异步写入事件,checkpoint policy 在需要的时点确认落盘。
- Chat 从事件投影用户友好的对话;Trajectory 从同一事件投影 Turn、Step、工具耗时和 token 信息。
浏览器没有直接执行 pnpm test。它只提交用户意图和展示状态;真实命令由 Host 侧 Agent 决定调用,经工具策略和 OS 沙箱后执行。
为什么项目强调“插件,不改 Loop”
Agent Loop 是最容易变成复杂条件树的地方:压缩、审批、超时、Hook、重试、权限、子 Agent 和遥测都可以被塞进去。DeepSeek Harness 要求新增行为优先挂在明确事件或服务接缝上。
例如:
- 请求前压缩:
agent/pre-step。 - 模型错误恢复:
agent/request-error。 - 工具审批:
tools/pre-execute。 - 工具超时:
tools/executearound middleware。 - 文件写入策略:
fs/write-intent。 - Turn 结束前继续工作:
agent/turn-stopping。 - UI 新视图:Client slot contribution。
这样每个插件可以独立装配和卸载,Agent Loop 仍只维护 Turn/Step 的通用状态机。
这套架构的优势
第一,能力替换范围清晰。模型、Shell、存储、沙箱和 UI 都有明确接口。
第二,运行事实可重放。模型历史、Chat 和 Trajectory 来自同一 Session Event Log。
第三,策略不与工具实现绑定。审批、Hook、timeout 和 guard 可以覆盖多个工具家族。
第四,Host 与 Client 有明确分界。浏览器通过显式 Remote 能力访问 Host,而不是把整个运行时序列化过去。
第五,生命周期是架构的一部分。插件卸载时撤回注册、取消调用、终止进程并释放监听器。
第六,平台差异停留在 Provider。macOS、Linux 和 Windows 沙箱不会污染 Agent Loop。
需要正视的代价与限制
插件化不会减少概念数量。阅读一个行为时,经常需要跨 Definition、Provider、Consumer、bundle 配置和客户端投影。生成的类型图与事件目录能减少接口漂移,但提高了构建系统复杂度。
Web 不是独立 SPA,开发时必须由 Host 提供 boot manifest。Typert 的 Host/Client 双 face 还要求严格区分源代码构建面和生成产物构建面。
本地沙箱目前主要是文件写入限制,不是 microVM 级隔离。涉及不可信仓库、依赖安装脚本、网络数据外传或资源耗尽时,需要增加网络策略、容器、远程沙箱或 VM。
Session Event Log 提供可重放事实,但事件词汇和格式属于长期约束。任何新的模型可见输入都必须设计事件、持久化、投影、恢复和兼容行为,不能只在请求发送前临时拼一段文本。
项目仍处于预发布阶段,官方明确不承诺旧磁盘格式兼容。当前代码更适合研究架构、插件开发和受控环境试用,而不是默认视为稳定平台接口。
阅读源码的推荐顺序
如果要给团队讲解,可以按下面顺序阅读:
README.md:确认产品定位和运行入口。docs/architecture.md:建立插件树、Turn/Step 与 Session Log 全景。docs/cordis-primer.md:理解 Context、Service、Event 和 Effect。packages/bundle/base/cordis.patch.yml:查看基础运行时真实装配。packages/bundle/web-app/cordis.patch.yml:查看 Web Host 和客户端 roster。docs/agent-lifecycle.md:沿一条消息理解 Agent Loop。docs/tool-execution-pipeline.md:理解审批、guard 和工具结果。packages/core/session:理解事件为何是事实来源。packages/api/gateway与packages/api/remotes:理解 Web 如何进入 Host。packages/shell、packages/subprocess与packages/sandbox:理解本地命令和安全限制。packages/client/ui-trajectory:理解执行数据如何投影成可观测界面。
阅读时始终问三个问题:这个包是 Definition、Provider 还是 Consumer;它注册了哪个 ctx 服务或事件;它产生的事实是否进入 Session Log。大部分跨包关系都可以由这三个问题定位。
用五分钟向别人讲清楚
可以用下面这段话作为讲解主线:
DeepSeek Harness 是建立在 Cordis 上的插件化 Agent 运行时。Profile 和 Bundle 在启动时把模型、工具、Session、权限、沙箱和 Web UI 装配成插件树。浏览器不会直接执行本机命令,而是通过 Connection 和 Typert RPC 调 Host。用户消息进入 Agent Inbox,Agent Loop 按 Turn 和 Step 发起模型请求;模型产生工具调用后,工具要经过 pre-execute、guard、execute 和 post-execute 管线。Shell 工具再通过 sandbox Provider 包装命令,最后由 subprocess Provider 启动和管理进程树。整个过程以 Session Event 形式只追加记录,Chat 和 Trajectory 都从同一日志投影,因此会话可以恢复、重放和审计。Trajectory 展示的是执行账本,不是模型隐藏思维链。
如果听众继续追问“为什么要这么多层”,答案不是为了增加抽象,而是为了让模型、工具、策略、执行环境和界面能够独立替换,同时让每次 Agent 行为都留下可重建的事实。
最后的判断
DeepSeek Harness 最值得研究的部分不是某个工具数量,也不是 Web 页面本身,而是它把 Agent 运行时拆成三类可组合对象:服务负责直接能力,事件负责拦截和协调,Session Event 负责持久事实。
这三类对象分别回答三个问题:系统现在能做什么;执行过程允许谁介入;任务结束后还能证明发生过什么。Web、Trajectory、沙箱和子 Agent 都建立在这三个问题之上。
当一次 pnpm test 能从浏览器请求一路追踪到 RPC endpoint、Agent Step、Tool Call、Seatbelt 或 bubblewrap argv、进程树和最终 Session Event 时,这套 Harness 才真正从“Agent Demo”变成了可解释、可替换、可约束的运行时。
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:DeepSeek Harness 架构全解:从 Web 请求到 Agent Loop、Trajectory 与本地沙箱
本文链接:https://www.sshipanoo.com/blog/ai/DeepSeek-Harness架构全解/
