已经是最新一篇文章了!
已经是最后一篇文章了!

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

版本 A · 中文
SCROLL

在 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.llmctx.toolsctx.sessionsctx.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-localdsh-pwsh-local 提供本地执行实现。
  • dsh-tool-bashdsh-tool-pwsh 把 Shell 暴露为模型可调用的工具。
  • dsh-bash-sandboxdsh-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 是具名运行组合,例如 webheadlessBundle 是可以叠加的 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 启动了哪些部分

启动链路可以拆成四个阶段:

  1. CLI 解析 web、profile、端口和 patch 参数。
  2. Boot 层找到 profile 及其 bundles,合并 Cordis patch。
  3. Cordis Loader 根据 inject 服务依赖挂载插件。需要的服务尚未出现时,插件等待依赖,而不是依靠一份人工维护的固定启动顺序。
  4. 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/startturn/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-stepagent/requestllm/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,并把策略转换成启动参数:

平台首选机制当前限制目标
macOSSeatbelt,通过 sandbox-exec文件写入范围
Linuxbubblewrap,失败时尝试 Landlock文件写入范围
WindowsRestricted 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 和业务状态事件。

日志承担四个职责:

  1. deriveMessages() 从日志派生下一次模型请求历史。
  2. Web UI 从日志构建 Chat 和工具卡片。
  3. Trajectory 从同一事件窗口构建执行账本。
  4. 持久化、恢复、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_listjob_readjob_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 结合退出信息判断故障归属,防止把安全机制失效当成普通命令错误。

一次“运行测试”的端到端路径

把前面的模块连起来,可以得到完整流程:

  1. 用户在 Web Composer 输入“运行测试并解释失败”。
  2. Browser Client 通过类型化 Remote 调用 Host 的 Agent 能力。
  3. API Gateway 校验 endpoint 和参数,通过 Session/Agent resolver 找到目标 Agent。
  4. 消息进入 Agent Inbox,并唤醒 Agent Loop。
  5. Loop 写入 turn/start,claim 输入,通过 agent/pre-step
  6. Loop 写入 step/startuser/message
  7. System Prompt 聚合 persona、项目指令、Skill、权限状态与工具 schema。
  8. agent/requestllm/stream 把请求送到 DeepSeek Provider。
  9. 每个模型流块写入 assistant/chunk,浏览器实时显示。
  10. 模型返回 bash({ command: "pnpm test" })
  11. Loop 写入 tool/call
  12. Tool Registry 校验 JSON 参数并执行 tools/pre-execute
  13. 当前模式是 workspace-write,命令没有申请更高权限,进入 sandbox executor。
  14. Sandbox Provider 在 macOS 生成 Seatbelt argv,在 Linux 生成 bwrap/Landlock argv,在 Windows 走受限令牌和 ACL。
  15. Subprocess Runtime 启动受限 bash -c "pnpm test" 或 PowerShell,并管理整棵进程树。
  16. stdout/stderr 被限长收集,超长完整输出按配置 spill 到临时文件。
  17. Tool 把输出、退出码、超时和沙箱拒绝事实渲染为 ToolResult。
  18. Loop 写入 tool/resultstep/end
  19. 因为模型还需要解释测试结果,Loop 在同一 Turn 开启下一个 Step。
  20. deriveMessages() 从 Session Log 派生历史,把工具结果送回模型。
  21. 模型返回最终回答;Loop 写入 Assistant 事件并关闭 Turn。
  22. 持久化层异步写入事件,checkpoint policy 在需要的时点确认落盘。
  23. 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/execute around 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 提供可重放事实,但事件词汇和格式属于长期约束。任何新的模型可见输入都必须设计事件、持久化、投影、恢复和兼容行为,不能只在请求发送前临时拼一段文本。

项目仍处于预发布阶段,官方明确不承诺旧磁盘格式兼容。当前代码更适合研究架构、插件开发和受控环境试用,而不是默认视为稳定平台接口。

阅读源码的推荐顺序

如果要给团队讲解,可以按下面顺序阅读:

  1. README.md:确认产品定位和运行入口。
  2. docs/architecture.md:建立插件树、Turn/Step 与 Session Log 全景。
  3. docs/cordis-primer.md:理解 Context、Service、Event 和 Effect。
  4. packages/bundle/base/cordis.patch.yml:查看基础运行时真实装配。
  5. packages/bundle/web-app/cordis.patch.yml:查看 Web Host 和客户端 roster。
  6. docs/agent-lifecycle.md:沿一条消息理解 Agent Loop。
  7. docs/tool-execution-pipeline.md:理解审批、guard 和工具结果。
  8. packages/core/session:理解事件为何是事实来源。
  9. packages/api/gatewaypackages/api/remotes:理解 Web 如何进入 Host。
  10. packages/shellpackages/subprocesspackages/sandbox:理解本地命令和安全限制。
  11. 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架构全解/