ARTICLE · 1006101
DeepSeek Harness 源码详解
DeepSeek Harness 发布之后也经过了一段时间,围绕它的评价一直有点分裂。支持者觉得“一切皆插件”很有野心,Standard、Code、Minimal、Creator 四种运行模式也跳出了常见 Coding Agent 的单一形态;质疑者则认为它包多、配置重、接口还在变化,更像一张提前开源的未来蓝图,离成熟产品尚有距离。
还有一类更务实的声音:插件、事件溯源、Provider、安全分层都不是什么新概念,真正值得看的,是 DeepSeek 有没有把这些旧零件组合出新的工程价值。
本次咱们不靠宣传页,也不凭一次演示站队,直接从源代码里看看它究竟是新瓶装旧酒,还是确实做出了些新东西。
代码结构简介
先认清代码怎样摆放,再讨论它的设计。DeepSeek Harness 是一个以 TypeScript 为主的多包仓库。它没有让 Web、SDK 和编辑器各自拥有一套完整实现,而是把启动入口、运行机制、能力实现和客户端接入拆开,按需组合。

apps/cli 负责把系统启动起来。 Web、无界面运行、SDK 服务和 ACP 服务都从同一个 dsh 启动器进入,再由 Profile 选择产品形态。具体装配逻辑分布在 packages 的 boot、bundle 和 preset 分组里,分别负责启动、宿主能力组合和每个 Agent 的能力组合。它们解决的是“这次把什么装进去”,还没有开始处理用户任务。
packages 放着真正工作的运行时与能力包。 其中 core 分组定义 Agent 接口、默认 Agent Loop、Session、工具注册与提示词组装;llm 分组负责模型接口和适配器;fs、shell、sandbox 等分组负责文件、命令与执行限制;session 分组提供持久化等会话能力。要注意,名字叫 core 不代表它是不可替换的特权内核,默认循环和会话服务同样通过插件装入。
界面与接入代码负责传递意图、呈现结果。 Web 的浏览器界面、服务端宿主和 API 网关分别位于 client、host、api 分组,TypeScript SDK 和编辑器协议接入则由 sdk、acp 分组承担。python/sdk 也没有另写一套 Python Agent Loop,它启动同一个 dsh 运行时,通过 JSON-RPC 与之通信。因此,换客户端不需要把模型与工具循环重写一遍。
vendor/cordis 是把这些包连起来的插件底座。 它管理 Context、服务依赖、事件与资源回收。目录分组说明“能力放在哪里”,Cordis 则决定“运行时谁能使用谁、谁负责释放资源”。仓库中的原生平台组件、构建测试脚本和文档站点承担配套工作,初读主链时可以暂时放在后面。
启动关系可以串成一条线:dsh 读取 Profile,叠加 Bundle 与 Patch,交给 Cordis 挂载宿主插件;创建 Agent 时,再按 Preset 组合它的工具、上下文和行为。这里要分清,Profile 决定整个进程怎样启动,Preset 决定进程里的某个 Agent 怎样工作。
消息进入后,调用关系才真正转起来。Web、SDK 或 ACP 把输入交给 Agent 的 Inbox,Agent Loop 领取消息,组装提示词与工具描述,再调用模型服务。模型要求使用工具时,工具运行时先做参数和权限检查,再调用文件、Shell 等具体能力。过程中的关键事件不断写入 Session,界面和持久化插件消费同一条事件流;工具结果也会进入下一次模型请求,直到任务收束。
因此,读源码时要同时看两种关系:服务调用负责“向谁要能力”,事件负责“把发生的事通知给谁”。工具依赖文件系统的服务契约,界面订阅会话事件,都不必把某个具体实现写死在 Agent Loop 里。这正是后面插件装配、事件日志和安全机制能够各自演进的基础。
先把结论摆在桌面上
读完整套源码后,可以用三个支点概括 DeepSeek Harness。这里的“支点”不是三个并排的功能模块,而是三类贯穿整套运行时的问题。
第一个支点是插件装配,它回答“这个 Agent 此刻具备什么能力”。 模型、工具、Skill、Session、沙箱、存储、调度乃至界面都由插件提供。Profile 和 Preset 先选出本次运行需要的插件,Cordis 再处理依赖、作用域、启动、回滚和清理。换句话说,插件装配决定的是 Agent 的能力边界,以及这些能力由谁提供、何时生效、何时退出。
第二个支点是事件日志,它回答“系统刚才究竟发生了什么”。 用户消息、模型输出、工具调用和结果、Turn 与 Step 的起止、上下文注入等关键变化都会追加到 Session Log。模型下一轮看到的对话只是从日志中投影出来的一张当前视图,原始事实仍然保留,因此恢复、分叉、压缩、回放和审计可以建立在同一份历史上。
第三个支点是安全边界,它回答“哪些文件能访问、哪些命令能执行、哪些网络能连接”。 模型生成了一个工具调用,不等于系统就要照办。调用需要接受可见性与策略检查,按规则决定是否请求用户审批,并在适用的沙箱限制下执行。Policy 管原则,Approval 管这一次是否授权,Sandbox 管操作系统层面最终能够触达哪里。
这三个支点分别管能力从哪里来、运行事实怎样留下、外部访问和操作被限制在什么范围。少了装配,系统会重新长成一个巨型 Agent 类;少了日志,恢复和审计只能靠猜;少了安全边界,插件越多,风险反而越大。

读这张总览时,可以把三种信息分开看。左侧是刚才说的三个设计支点;中间是代码的四层实现,从产品入口、宿主装配一路下沉到 Agent 运行时和能力后端;右侧则是一条消息的流动方向,从用户输入经过模型与工具,进入 Session Log,再投影给界面和 SDK。
一次真实运行会同时经过三个支点。插件系统先决定当前有哪些能力,Agent Loop 使用这些能力推进任务,过程中的关键变化持续写入 Session 日志;一旦工具准备修改外部世界,调用还要经过策略、审批和沙箱。它们不是前后割裂的三个阶段,而是同一次运行中的三套约束。
这也是理解源码的最短路径。先看装配,再追一次请求,最后看日志与安全边界。若从两百多个包逐个翻起,很容易只看到零件,看不到机器。
Harness 和 Agent 不是一回事
Agent 通常指一个会接收输入、调用模型、选择动作的执行体。Harness 则是承载 Agent 的整套装具。它负责准备模型上下文、提供工具、保存状态、限制权限,也负责把同一个执行体暴露给不同客户端。
DeepSeek Harness 在官方介绍里把 “Everything is a Plugin” 放在最显眼的位置。这句话不是一句宣传口号,它直接决定了代码的组织方式。
仓库里确实有 Agent Loop,但它只是协调者。它依赖模型服务、工具运行时、系统提示词和 Session;文件、网络、持久化、沙箱也都有独立的服务定义与实现。换句话说,Agent 不拥有这些能力,只是在当前作用域里消费它们。
总览中间的四层结构也说明了 Harness 和 Agent 的边界。产品入口接收输入并呈现事件,宿主装配选择本次运行需要的插件,Agent 运行时推进模型与工具循环,能力后端才真正接触文件、进程、网络和持久化介质。
这种分层带来一个重要结果:同一套 Agent Loop 可以运行在 CLI、Web 或 SDK 里,也可以把本地文件系统换成 E2B,把 JSONL 持久化换成 SQLite,而不用修改 Loop 主体。
“一切皆插件”真正指向生命周期
如果只把 Cordis 理解成依赖注入容器,会低估它在 Harness 里的位置。普通依赖注入解决“对象从哪里来”,Cordis 还要解决“能力在哪个作用域存在、什么时候激活、何时回收、热替换失败后如何回滚”。
Context 是一块有边界的运行时空间
Cordis 的 Context 可以派生子作用域,也可以隔离插件能看到的服务和事件。每块 Context 都像一间有门的机房,服务可以继承,权限与事件范围也可以收紧。
插件通过 Context 注册服务和事件监听器,并登记需要随插件回收的资源。注册行为不是永久写进一个全局 Map,而是归属于插件自己的 Fiber。Fiber 停用时,监听器、计时器和服务引用会一起清理;激活失败时,已经产生的 Effect 也能回滚。
这套机制解决了插件架构最容易留下的烂摊子。没有生命周期所有权,所谓“动态加载”很快就会变成重复监听、旧服务残留和无法解释的全局状态。
Service 把声明、实现和消费拆开
Harness 中常见的能力路径,以及它与产品装配、插件生命周期的关系,可以合在一张图里理解。

文件系统包声明接口,本地文件包提供实现,沙箱包可以再包装一次,最后文件工具把能力暴露给模型。消费方依赖的是 ctx.fs 这样的服务契约,而不是某个具体类。
Service 的注册、依赖解析、复用和加载都交给 Cordis 管理。这样,调用者面对的是稳定契约,具体实现则可以随着运行环境替换。
产品其实是一张插件图
一条 dsh web 命令不会直接实例化一个巨型应用。CLI 先读取命令行、环境和 Profile,再由 Profile 合成 Bundle、Patch 与 Agent Preset,最后得到本次运行真正需要的插件图。
装配链中的三个配置概念承担不同责任。Profile 决定进程以什么产品形态启动,Bundle 与 Patch 决定宿主装入哪些基础设施和界面能力,Agent Preset 则决定某类 Agent 拥有哪些工具、上下文和行为策略。
例如 Web 产品装入界面和网关能力,Standard Agent 再装入文件、Shell、搜索、规划和子任务等能力。换一个 Preset,同一套宿主就能表现成完全不同的 Agent。
插件化的代价也很明显。仓库里分布着数百个大小不一的能力包,定位一项行为往往要跨 Definition、Provider、Bundle 和消费方。它用更高的阅读成本,换来了更清晰的替换边界和生命周期所有权。
一条用户消息如何穿过系统
理解完装配,下一步应沿一条真实请求阅读。DeepSeek Harness 没有一个包办所有事情的 Agent.run(),真正的主链分散在 Inbox、Agent Loop、LLM Runtime、Tools 和 Session 之间。
Inbox 先决定消息属于哪一轮
Agent 接收消息时,不只有“现在执行”一种语义。Inbox 的四种入口和一轮执行的完整路径如下图所示。

这一区分看似细小,却决定了 Agent 能否被真正地“驾驶”。用户在工具执行期间补充要求,可能希望改变当前任务,也可能只是排队一个后续问题。把两者都粗暴地拼到最后一条消息后面,模型历史和执行时序都会变得不可预测。
Turn 和 Step 是两层不同的边界
Turn 表示一次被唤醒后的完整活动,Step 表示其中的一次模型请求以及它要求的工具执行。外层 Turn 和内部可回环的 Step 把这两种边界明确分开。
Agent Loop 在 Turn 开始时写入边界事件,领取 Inbox 输入,然后循环创建 Step。一个 Step 内部会组装提示词和工具 Schema、发起模型请求、收集流式块、执行工具,再决定是否进入下一 Step。
Turn 和 Step 分开后,系统可以准确表达“这一轮没有进入模型调用”“模型调用成功但工具失败”“用户在两个 Step 之间转向”等状态,而不必从一串消息反推控制流。
模型层固定的是一次调用快照
Harness 的模型接口不是 OpenAI 或 DeepSeek 原始协议的薄封装。通用层定义统一的消息块与 StreamChunk,覆盖文本增量、推理增量、工具调用增量、完整块、用量和结束原因。
每次请求会先解析 Provider、模型和 Adapter 代际,形成 Prepared Call,再开始流式执行。这样即使插件在请求过程中热更新,正在运行的调用仍使用同一份适配器快照。
具体 Adapter 负责把统一消息序列化成供应商请求,建立流式连接,再把各家的事件翻译回 StreamChunk。
统一协议的意义不只是“方便换模型”。它让 Agent Loop、Session 和 UI 不需要理解每家厂商的流事件差异,也让原始响应元数据可以随完成消息保存,供后续重放。
工具可以并发,结果必须有序
模型一次可能请求多个工具。Harness 会根据每次调用的执行计划判断能否并发,而不是简单给整个工具贴一个永久的 parallel: true 标签。安全的调用可以同时运行,但工具结果仍按模型给出的调用顺序提交到 Session。
这个设计同时守住吞吐量和确定性:慢工具不会无意义地阻塞快工具,模型历史却不会因为真实完成时间不同而随机重排。
至此,一次请求的最小闭环就清楚了。它从 Inbox 领取输入开始,经由 Turn 和 Step 推进,在模型流与工具结果之间循环,最后以 assistant/message 和 turn/end 收束。
Session 日志为什么是整个系统的轴心
很多 Agent 框架把会话保存成一个不断覆盖的 messages 数组。这样做简单,却会丢掉控制流和因果关系。发生过哪些原始流事件,工具调用是否完整,压缩替换了哪段历史,进程在何处中断,都难以回答。
DeepSeek Harness 选择了只追加日志。Session 中的每条事件都有连续序号、时间、类型和 JSON 数据。

这里最关键的不是事件数量,而是“事实”和“视图”被明确分开。
Log 保存事实,Surface 生成当前历史
真正进入下一次模型请求的只有用户消息、助手消息和工具结果。它们共同形成 Surface,其余事件留在日志中供审计、UI 和恢复使用。
Surface 支持两种操作:把消息追加到末尾,或者用一条新消息替换一段既有 Surface。替换事件会记录被遮蔽事件的序号,因此压缩后的历史仍能追溯来源。
这种结构解决了一个常见冲突:模型需要一份短而干净的上下文,系统却需要一份完整、可解释的历史。Surface 可以变化,Log 不必删改。
压缩是一笔可审计的历史改写事务
上下文接近模型窗口时,Harness 先裁剪体积巨大、长期价值较低的工具结果,再选择一段边界完整的历史生成摘要。摘要不是覆盖原文件,而是以 Surface Replace 的形式追加。
因此,压缩不会让系统失去“原来发生过什么”。模型下一轮只看到摘要,审计和重放仍可以回到原始事件。
持久化、恢复和分叉因此变得统一
Session 只负责产生规范事件,持久化插件负责把事件安全写入介质。仓库同时提供 JSONL 与 SQLite 实现。
恢复会话时,系统重放日志重建 Surface 和派生状态;分叉会话时,则复制稳定事件前缀并记录父会话来源。UI、SDK 和查询工具消费的也是同一份事件事实,而不是各自维护一套容易漂移的状态。
事件溯源的成本同样真实。事件格式一旦发布就要承担演进和兼容责任,Projection 也必须是确定性的。当前 Session Format 仍未承诺稳定兼容,这套协议还有继续调整的空间。
上下文工程为什么分成四条管线
系统提示词、当前时间、工作区指令和 Skill 最终都会变成模型看到的文字,但源码没有把它们拼成一个随意修改的长字符串。

系统提示词由多个贡献者按顺序和作用域组装,工作区指令则作为具有来源的消息投影。系统因此知道一条规则来自用户级配置、项目目录还是当前运行环境。
Skill 采用懒加载思路。模型先看到名称与目录,真正需要时再读取正文;目录发生变化时,对应缓存也会失效。
这四条管线分开,主要是为了缓存、来源和生命周期。稳定规则不必每轮重算,变化事实不能被错误缓存,目录指令需要知道从哪里继承,Skill 也不应该在每次请求里把全部正文塞进上下文。
工具系统如何把模型意图变成受控执行
工具调用是 Agent 最危险也最容易失控的地方。模型给出一个名字和 JSON 参数,并不意味着系统应该立刻执行。
Harness 中的工具要依次经过定义注册、可见性筛选、参数解析、策略 Hook、审批、真正执行、错误归一、结果展示和 Session 记录。

工具定义包含名称、描述、参数 Schema、执行函数和可选展示逻辑。哪些工具对某个 Agent 可见,由作用域、模式、权限和 Guard 决定;通过筛选后,调用才会进入参数解析、策略、审批和执行流水线。
这意味着 Native Tool 和 Programmatic Tool Calling 虽然入口不同,最终仍应复用同一套治理边界。否则把工具藏进代码运行时,就会绕开审批、日志和错误归一。
Policy、Approval、Sandbox 各管一层
安全设计里最容易犯的错误,是把“有沙箱”当成完整答案。完整的安全链路分成三层:Policy 判断原则上是否允许,Approval 判断当前这一次是否获得授权,Sandbox 则限制操作系统层面最终能触达什么。
Sandbox 定义三种模式:read-only、workspace-write 和 danger-full-access。它们描述的是文件访问与修改的范围,不自动承诺网络隔离或进程可见性。
策略插件把工作区、网络和提升边界解析成具体决策。用户审批是另一项独立服务,负责请求、缓存和撤销授权。
文件工具还有单独的路径围栏。cwd 只是相对路径解析基准,不是安全边界;本地沙箱还会根据操作系统选择真正执行限制的后端。
这套设计坚持一个重要原则:安全后端缺失时不能静默退化成无沙箱执行。需要隔离却无法提供隔离时,正确行为是失败,而不是“尽量运行”。
网络访问也不等于一个普通 fetch()。HTTP Provider 对协议、解析后地址、重定向、响应体大小和读取时间做限制,避免把 Agent 的 Web 工具变成 SSRF 或无限下载入口。
长任务与多端接口如何回到同一模型
当任务从一次对话变成长期运行,系统需要的已经不只是“再调一次工具”。DeepSeek Harness 为不同生命周期提供了不同抽象。

这些抽象并不是五种彼此独立的任务系统。它们最终仍围绕 Agent、Session、工具和事件工作,只是生命周期所有权不同。Subagent 拥有自己的对话上下文,Job 可以脱离当前 Step,Workflow 强调可重复步骤,Schedule 负责时间触发,Goal 则负责跨轮意图。
多端接口也采用同样思路。Web 通过 Typert Gateway 访问控制器,TypeScript 和 Python SDK 使用 JSON-RPC,ACP 把编辑器会话映射到 Harness Session。它们共享的是同一套运行时和事件事实,而不是各写一套 Agent Loop。
这正是事件日志的第二层价值。它不仅用于恢复,也是不同客户端之间的公共事实层。
怎样扩展 Harness,又不破坏它的边界
DeepSeek Harness 最值得借鉴的不是某个工具实现,而是它对能力边界的要求。新增功能时,首先要回答的不是“代码放哪个文件”,而是下面几个问题。
这是新的服务定义,还是已有服务的一种 Provider? 谁拥有它的生命周期,插件卸载时怎样回收? 它是否读写文件、启动进程或访问网络,需要哪一层 Policy、Approval 和 Sandbox? 它会写入 Session 吗,重放和分叉时语义是否仍成立? 它需要进入哪些 Bundle 或 Preset,哪些产品不应该默认加载? 测试应验证协议、投影还是外部世界的真实结果?
仓库的扩展手册也沿这些边界组织。新增工具、新增模型适配器和新增运行模式,都要求开发者先说明服务契约、生命周期与失败方式;运行时还设有独立的约束检查,用来尽早发现插件图和关键状态的异常。
从工程取舍看,这套架构并不简单。

所以,“Everything is a Plugin”并不意味着任何东西都可以随意插拔。真正能插拔的前提,是接口、所有权、事件语义和失败方式都被写成契约。
这套源码最值得带走的四个认识
第一,Agent Loop 应该保持克制。它负责推进 Turn 和 Step,不应该同时拥有模型、文件系统、持久化和安全策略。能力越多,越需要通过明确服务边界组合。
第二,模型历史不等于系统历史。模型只需要当前有用的 Surface,系统则需要完整的事实日志。把两者分开,压缩、恢复、UI 投影和审计才不会互相打架。
第三,工具安全不是一个布尔开关。策略决定原则,审批决定本次授权,沙箱限制最终能力。任何一层都无法单独替代另外两层。
第四,长期运行的关键是所有权。消息属于哪个 Step,后台任务由谁取消,插件卸载时谁清理资源,分叉会话继承哪些事实,这些问题比“模型能不能再调用一次工具”更决定系统是否可靠。
和 Claude Code、Codex、Pi 放在一起看
把它们摆在一起,差别已经不在“能不能读写文件、运行命令、调用工具”。这些早已是 Coding Agent 的标配。真正的分水岭,是谁拥有运行时,扩展可以深入到哪一层,以及系统如何管理状态、权限和外部操作。
Claude Code 更像产品先行的路线。项目指令、Skills、Hooks、MCP、子 Agent、权限和沙箱都围绕开发者工作流展开,重点是让用户今天就能把任务完成。它同样提供丰富的生命周期事件和扩展点,但这些扩展发生在一个边界相对明确的产品框架里。DeepSeek Harness 给开发者的底层自由度更高,相应的装配和理解成本也更高。
Codex 与 Harness 最接近的地方,是都认真对待执行协议、安全边界和多端接入。Codex 用 Rust 核心统一 TUI、自动化入口和 App Server,并通过线程、轮次、事件、审批与沙箱组织一次代码任务。它更像一台围绕软件工程打磨的原生执行引擎;Harness 则更像可以替换内脏、重新组装产品形态的运行时底座。
Pi 走的是“小核心、高可塑性”路线。统一模型接口、Agent Core、会话树、终端界面和 Extension API 都很直接,工具甚至可以被扩展完整替换。Pi 更容易读懂,也更适合快速改造成个人化 Agent;Harness 把相似的可塑性继续向下推进到服务、持久化、沙箱、调度和界面,因此边界更系统,体量也明显更重。
所以,DeepSeek Harness 真正的新意不在某一个零件,而在组合方式。Cordis 插件图负责能力与生命周期,只追加日志保存事件事实,Projection 生成模型当前需要的视图,Policy、Approval、Sandbox 分担不同的安全责任,四种模式再从同一套基础设施上长出来。想要直接使用的 Coding Agent,Claude Code 和 Codex 更省心;想要一个轻巧、好改的核心,Pi 更经济;想搭建多种产品形态并替换底层能力,Harness 的方向更有吸引力。
我的判断是,它并非简单的新瓶装旧酒,因为这套组合确实比常见 Agent SDK 多走了一步;但它也还不是已经定型的下一代标准。它更像一台结构清楚、野心很大、仍需继续打磨的工程样机。
DeepSeek Harness 仍处于早期阶段,包数量、配置复杂度和协议稳定性都需要继续观察。但它已经给出了一种很清晰的方向:
一套成熟的 Agent Harness,不应只是包住模型调用的循环。它应当是一台由插件装配、以事件日志保存事实、用分层安全机制约束外部操作,并能向不同客户端投影同一运行状态的运行时。
这句话基本概括了整个仓库,也解释了为什么它值得被当作运行时源码来读,而不只是另一套 Agent SDK。