🚀 DeepSeek Harness 快速学习指南

一切皆插件的 Agent 运行时

从配置组装到 Agent 运行,系统掌握 DeepSeek Harness 的核心架构、设计思想与调试方法

v0.1.0-rc.8 TypeScript / Node.js Developer Preview

一、全景概览

5
架构层级
9+
Core 子包
6
配置优先级层
7
工具执行阶段

什么是 Agent Harness

LLM 本身只是一个接收消息、生成消息的函数。真正让它成为能完成真实任务的 Agent 的,是模型外面那整套运行系统——系统提示词、工具、工作目录、权限、沙箱、会话、重试、子 Agent、UI,以及把它们组织起来的生命周期。这套运行系统就是 Agent Harness

Agent ≈ Model + Harness

Model 负责推理和生成;Harness 负责上下文、工具、状态、执行环境和控制循环

Harness 需要回答的 8 个问题

📋

上下文组装

本轮应把哪些规则、文件、历史发给模型?

🔀

模型路由

使用哪个 Provider、哪个 Model,什么参数?

🛠️

工具系统

模型能调用什么?输入输出如何校验?

💻

执行环境

Shell、FS、PTY、浏览器在哪里运行?

💾

会话状态

消息、工具调用如何记录、恢复和分叉?

🔒

权限与安全

哪些操作允许、拒绝或需人工批准?

♻️

生命周期

组件重载时,监听器和资源如何清理?

🔍

可观测性

失败发生在哪一层?能否重试或回放?

💡

Harness vs Framework:Agent Framework 更偏向编程框架(流程、节点、状态编排);Agent Harness 更偏向完整运行环境(接住模型调用、工具执行、会话持久化、权限、沙箱、UI 和扩展生态)。DSH 同时提供两者,但命名强调的是"承载多种 Agent 组合的运行底座"。

两条理解主线

理解 DSH 可以沿两条主线展开,它们分别回答不同的问题:

🔧 组装主线

Profile → Bundle → Patch → Loader → Entry → Fiber

回答:这个应用是怎样组装出来的?

  • Profile 决定叠哪些 Bundle
  • Bundle 提供基础 Patch 层
  • Patch 描述 Entry 树的差异
  • Loader 协调 Entry 与 Fiber
  • Fiber 是插件的运行实例

⚡ 运行主线

User → Agent → Session → System Prompt → LLM → Tools → Session

回答:组装完成后一次任务怎样运行?

  • 用户输入进入 Agent Inbox
  • Agent Loop 驱动 Turn/Step
  • Session 记录所有事件事实
  • System Prompt 动态组装提示
  • 工具经流水线执行并回写

五层架构

DSH 可以粗略分成五层,从入口到能力插件逐层深入:

flowchart TB A["入口层
CLI / Web / ACP / Python SDK"] B["组装层
Profile / Bundle / Patch / App Boot"] C["运行内核
Cordis / Loader / Include / HMR"] D["Agent Core
Scope / Session / Agent / Prompt / Tools / Loop"] E["能力插件
LLM / FS / Shell / Sandbox / Approval / Subagent / UI"] A --> B B --> C C --> D D --> E E -.注册 Service/Event 与 Effect.-> C style A fill:#4f46e5,stroke:#4338ca,color:#fff style B fill:#7c3aed,stroke:#6d28d9,color:#fff style C fill:#06b6d4,stroke:#0891b2,color:#fff style D fill:#10b981,stroke:#059669,color:#fff style E fill:#f59e0b,stroke:#d97706,color:#fff
图 1:DSH 五层架构(逻辑分层,非严格单向依赖)
1
入口层
用户与系统交互的入口点
apps/cli, apps/web
2
组装层
Profile、Bundle、Patch 配置组合与启动
packages/boot, packages/bundle
3
运行内核
Cordis 插件系统、Loader、HMR 热更新
vendor/cordis, vendor/loader
4
Agent Core
Session、Agent、Prompt、Tools、Loop 核心抽象
packages/core/*
5
能力插件
具体的模型、工具、沙箱、审批等实现
packages/fs, shell, sandbox...
🎯

Everything is a Plugin:模型适配器、工具注册表、Session、Agent Loop,乃至 Web 应用本身,都是配置树里的插件。插件可以被插入、替换、暂停或卸载;它提供的服务、事件监听器和其他副作用也会随生命周期一起撤销。

二、配置系统

DSH 的配置描述了整个应用的插件树。配置不是一个静态文件,而是由多层 Patch 叠加合成的最终 Entry 列表,再由 Loader 转换为运行中的插件树。

Entry / Patch / Overlay

📦

Entry(条目)

一条可被 Loader 运行的插件配置,有 id、name、config、inject 等字段,是插件树的基本单元。

🔧

Patch(补丁)

对 Entry 列表的一次修改,可以按 id 修改已有 Entry,也可以 insert 新 Entry。字段基本都是可选的。

🎨

Overlay(叠加层)

一组 Patch 的集合,强调来源和优先级——它们覆盖在已有组合之上,是用途的称呼而非独立类型。

Entry 的核心字段

字段作用类比
idEntry 的稳定身份,Patch、更新和诊断都依赖它主键
name要加载的插件模块名或路径模块地址
config传给插件的配置;group 为 true 时解释为子 Entry 列表构造参数
disabled是否阻止该 Entry 及其子节点运行开关
inject为该 Entry 增加 Service 依赖依赖声明
intercept为消费的 Service 附加消费侧配置消费偏好
isolate将指定 Service 放入独立或具名的隔离域命名空间
⚠️

Patch 不做深度合并:Patch 给出 config 时,替换的是整个 config 字段,不会自动深度合并。用户 Patch 必须重述希望保留的配置项。

Profile 与 Bundle

flowchart LR P[Profile
运行配置形态] --> B1[Bundle 1
dsh-base] P --> B2[Bundle 2
dsh-web-app] P --> PP[Profile Patch
cordis.patch.yml] B1 --> E1[Entry 1] B1 --> E2[Entry 2] B2 --> E3[Entry 3] PP --> E4[用户自定义] style P fill:#4f46e5,stroke:#4338ca,color:#fff style B1 fill:#7c3aed,stroke:#6d28d9,color:#fff style B2 fill:#7c3aed,stroke:#6d28d9,color:#fff style PP fill:#06b6d4,stroke:#0891b2,color:#fff
图 2:Profile 由 Bundle 叠加用户 Patch 组成

📁 Profile(配置形态)

~/.dsh/profiles/<name>/

一套可以按名称选择的运行配置

  • package.json:Bundle 清单(dsh.profile.bundles)
  • cordis.yml:Loader 的空根(内容固定为 [])
  • cordis.patch.yml:Profile 自定义 Patch
  • pnpm-workspace.yaml:外置插件解析环境

📦 Bundle(组合包)

@deepseek-ai/dsh-xxx

插件配置的分发单位,可复用的一组 Patch

  • dsh-base:模型、工具、Session、沙箱等公共能力
  • dsh-web-app:增加 Web 应用
  • dsh-headless:增加一次性 Runner
  • 通过 dsh.bundle.patch 声明 Patch 文件
💡

为什么 cordis.yml 是空的?空文件不是漏写了配置,而是有意作为 Loader 实际挂载的 Include 入口、Profile 相对路径的解析基准,以及所有 Patch 的合成起点。真正的插件树由 Bundle 和用户 Patch 从空列表上逐层构造。每次启动都会重写它,避免配置写回固化。

配置层优先级

composeProfile() 按以下顺序合成配置,越靠后的 Patch 优先级越高:

1
Bundle Patch
按 dsh.profile.bundles 顺序
最低
2
Profile Patch
profiles/name/cordis.patch.yml
3
Home Patch
$DSH_HOME/cordis.patch.yml
4
命令行 Overlay
--patch,按参数顺序
5
启动器内部 Patch
Agent 预设目录、Telemetry 强制关闭
最高
🔍

查看最终配置:使用 dsh --profile web --dump-config 查看实际组合结果。--dump-config 不是便利功能,而是理解实际部署状态的重要入口——因为最终配置不等于任一源文件。

启动器内部 Patch 的特殊性

最后两层内部 Patch 不能写在 Bundle 或 Profile 中:

📁

Agent 预设目录 Patch

Preset 的绝对路径随运行环境变化,启动器知道实际安装目录后才写入 agent-presets.roots

🔒

Telemetry 强制关闭 Patch

只有启动器最后追加才能保证:任何前置配置都无法重新开启 Telemetry(DSH_TELEMETRY_DISABLED)

三、Cordis 内核

Cordis 是 DSH 的插件运行内核,源自 Koishi 聊天机器人框架多年的工程经验。它解决的核心问题是:插件如何安全地加载、卸载,以及依赖变化时如何重新协调

Cordis 的两个核心概念

时间可组合性
可逆 Effect:组件离开时,副作用能被完整撤销
🧩
空间可组合性
响应式 Coeffect:依赖出现/消失/替换时自动激活或停用

Context 与 Fiber

flowchart TD subgraph Root [Root Context] R["rootCtx"] end subgraph Plugin [Plugin Context] P["pluginCtx"] end subgraph Scoped [Scoped Context] S["scopedCtx"] end F["Fiber - 插件运行实例"] R -- "ctx.plugin()" --> F F -- "创建" --> P P -- "ctx.extend()" --> S P -. "ctx.fiber 回指" .-> F F -. "fiber.ctx" .-> P style F fill:#f59e0b,stroke:#d97706,color:#fff style R fill:#4f46e5,stroke:#4338ca,color:#fff style P fill:#7c3aed,stroke:#6d28d9,color:#fff style S fill:#06b6d4,stroke:#0891b2,color:#fff
图 3:Context 原型继承链与 Fiber 的互指关系

Context 是 Proxy

插件拿到的 ctx 不是普通对象,而是一个 Proxy。属性读取会先检查真实属性;如果没有,就交给 ReflectService 按服务、Accessor、Inject 和 Isolate 规则解析。因此可以直接写 ctx.tools.register(...) 而不需要手动查找容器。

子 Context 使用原型继承

ctx.extend(meta) 以当前 Context 为原型创建新对象,再把 meta 作为自有属性写入。由此形成原型链:

rootCtx
  ↑ prototype
pluginCtx  (fiber 在这里)
  ↑ prototype
scopedCtx   (继续派生,仍属同一 Fiber)
⚠️

常见误解纠正:不是"每派生一次 Context 就一定创建一个 Fiber"。ctx.plugin() 会创建 Fiber 及其主要子 Context;普通 ctx.extend() 还可以继续派生多个 Context,它们在没有覆盖 fiber 时仍属于同一个 Fiber 生命周期。

Fiber 状态机

状态含义触发条件
PENDING依赖尚未满足服务缺失时
LOADING正在校验配置并运行插件入口依赖全部满足时
ACTIVE插件已激活插件入口执行完成
FAILED配置或插件入口失败加载过程中出错
UNLOADING正在执行清理依赖消失或被禁用
DISPOSED已永久销毁,不再重启Entry 被删除

Fiber 状态变化不是简单直线:

依赖满足:   PENDING → LOADING → ACTIVE
依赖消失:   ACTIVE → UNLOADING → PENDING
再次满足:   PENDING → LOADING → ACTIVE
最终销毁:   任意可销毁状态 → UNLOADING → DISPOSED

Epoch:依赖实现的指纹

epoch 不是简单的执行次数,而是当前依赖实现组合的指纹:只要有一项依赖找不到可用实现,结果就是 INACTIVE;依赖全部满足时,则将各服务提供者的 Fiber uid 拼成一个字符串。它既能表示"依赖是否齐全",也能识别"服务实现是否已经被替换"。

Service / Event / Effect

理解 DSH 的插件模型,需要先认识 Cordis 提供的三种基础机制:

🔌

Service(能力)

提供可直接调用、可替换的服务接口。通过 ctx.provide() 注册,通过 ctx.<key> 消费。

例子:ctx.llm、ctx.tools、ctx.sessions

📡

Event(流程)

提供可供插件监听、拦截或参与处理的流程扩展点。支持 emit、bail、waterfall、parallel、serial 多种模式。

例子:agent/pre-step、tools/pre-execute

♻️

Effect(生命周期)

管理随插件创建、并在插件卸载时撤销的资源。setup 返回 disposer,卸载时逆序执行。

例子:服务注册、监听器、定时器、子Fiber

三者通常会在同一个插件中配合使用:通过 Service 暴露能力,通过 Event 接入流程,再由 Effect 保证相关注册和资源随插件卸载一并撤销

Effect 的核心价值

ctx.effect(setup) 立即运行 setup,并收集它返回或 yield 的 disposer:

ctx.effect(() => {
  const resource = acquire()
  return () => resource.release()
})

这种局部性非常重要:创建资源和释放资源在同一段代码里,而不是分别散落在 start()shutdown() 中。Fiber 卸载时,所有 Effect 按逆序清理。

Events 的分发模式

模式用途DSH 中的例子
emit按顺序同步通知,不汇总结果session/event
bail第一个给出有效结果的监听器终止查找
waterfall环绕中间件,可短路可包装agent/request、tools/execute
parallel并行执行并等待tools/result
serial按顺序执行并等待

Inject / Isolate / Intercept

这三个概念经常混在一起,可以用三个问题区分:

Inject

插件需要哪些服务才能运行?

声明依赖,服务出现/离开/替换时,epoch 改变,驱动插件卸载和重新加载。

🏷️

Isolate

当前 Context 应该使用哪一个同名服务实现?

为某个服务名建立独立解析域,不同域中可有不同实现而不冲突。

⚙️

Intercept

当前插件希望怎样消费选中的服务?

挂消费侧配置到子 Context,供服务解析配置时使用,不替换提供者。

一句话区分

isolate:决定使用哪一个服务实现  |  intercept:决定怎样使用选中的服务

四、Loader 配置树

Cordis Core 管理单个插件实例的生命周期,Loader 则管理配置中的一组插件:它把 EntryOptions[] 变成 Entry 和 Fiber,并在配置发生变化后,让运行中的插件树收敛到新的配置。

Loader 的三重身份

🌳

配置树

Loader extends EntryTree,持有根 EntryGroup 和按 ID 索引的 Entry Store

🔌

Cordis 插件

启动时通过 ctx.plugin(Loader) 挂载,事件监听和内部资源受 Fiber 管理

🛎️

Service

将自身提供为 ctx.loader,其他插件可以用 Inject 等待或使用它

三个运行时对象

对象职责关键字段
EntryTree 一棵配置树的边界,按 ID 查找节点、修改树结构、等待任务结束 ctx, root, store
EntryGroup 持有一组有序的 EntryOptions,批量创建、更新和删除节点 ctx, tree, data
Entry 单个配置条目的运行时表示,导入插件、构造 Context、持有 Fiber options, parent, fiber, subgroup, subtree

Group 与 Include:两种嵌套方式

维度GroupInclude
继承EntryGroupEntryTree
配置来源当前 Entry 内联的 config外部 YAML 或 JSON 文件
创建的结构当前树中的子分组具有独立根 Group 和 Store 的子树
关联字段subgroupsubtree
Store共享父树的 store有独立 store
💡

简单理解:Group 在当前树里分组(子 Entry 仍进入同一个 tree.store);Include 把另一棵配置树挂进来(有独立的根 Group 和 Store)。Include 仍使用已有的 ctx.loader,不会再创建一个 Loader Service。

事务式更新

配置文件变化时,更新单位不是一个 Entry,而是整个 EntryOptions[]EntryGroup.update() 遵循事务语义:

  1. 为缺少 ID 的条目生成 ID,拒绝重复 ID
  2. 并行创建或更新新配置中的所有 Entry,等待全部结果
  3. 只有全部成功后,才删除不存在的旧 Entry,提交新的 group.data
  4. 任意一项失败时,反向移除新增 Entry,按旧配置恢复
  5. 回滚本身也出错时,合并原始错误和回滚错误

这样尽量保证一次配置更新要么形成完整的新树,要么回到更新前的可用树,不让只更新了一半的配置成为新的稳定状态。

五、Agent 核心运行时

packages/core 包含九个子包,它们共同构成了 Agent 的运行核心。Scope 和 Session 是底座,Agent 把它们组合成一个运行主体,Agent Loop 再使用 Prompt、LLM 和 Tools 驱动这个主体完成工作。

flowchart LR SCOPE[Scope
作用域化注册] --> AGENT[Agent
运行主体] SESSION[Session
事件溯源日志] --> AGENT MODEL[Default Model
默认模型选择] --> AGENT AGENT --> LOOP[Agent Loop
Turn/Step 驱动] PROMPT[System Prompt
动态组装提示] --> LOOP TOOLS[Tools
注册与执行流水线] --> LOOP PRESENT[Tool Presentation
Native/Code/Both] --> TOOLS LOOP --> SESSION LOOP --> LLM[LLM Service] style SCOPE fill:#4f46e5,stroke:#4338ca,color:#fff style SESSION fill:#7c3aed,stroke:#6d28d9,color:#fff style AGENT fill:#06b6d4,stroke:#0891b2,color:#fff style LOOP fill:#10b981,stroke:#059669,color:#fff style PROMPT fill:#f59e0b,stroke:#d97706,color:#fff style TOOLS fill:#ef4444,stroke:#dc2626,color:#fff style PRESENT fill:#8b5cf6,stroke:#7c3aed,color:#fff style MODEL fill:#14b8a6,stroke:#0d9488,color:#fff style LLM fill:#64748b,stroke:#475569,color:#fff
图 4:Agent Core 各包关系图

Scope:按 Agent 划分注册可见性

为什么全局 Context 还不够?

一个 DSH 进程可以同时运行多个 Agent。它们可能共享模型 Provider、文件系统后端和持久化服务,但拥有不同的 Persona、工具集合、工具呈现模式、Prompt Section 和策略限制。如果所有注册都进入同一张全局表,一个 Agent 的工具很容易泄漏给另一个 Agent。

createScope(ctx, key) 创建一个由空插件 Fiber 支撑的子 Context,并给它写入 Scope key。通过这个 Context 完成的注册同时获得两种属性:

👁️

可见性

只在对应 Scope 或其规则允许的子 Scope 中可见

🏠

所有权

Scope Fiber 销毁时,这些注册一起撤销

flowchart TD R["Root Context
全局 Tool / Prompt 贡献"] A["Agent Scope Context
(key = sessionId)"] T["Agent-local Tool"] P["Agent-local Persona"] M["Agent-local Presentation Mode"] R --> A A --> T A --> P A --> M style R fill:#4f46e5,stroke:#4338ca,color:#fff style A fill:#06b6d4,stroke:#0891b2,color:#fff style T fill:#10b981,stroke:#059669,color:#fff style P fill:#f59e0b,stroke:#d97706,color:#fff style M fill:#8b5cf6,stroke:#7c3aed,color:#fff
图 5:Scope 层级与可见性
⚠️

Scope 不是权限边界:Scope 面向受信任的同进程插件,解决注册路由和生命周期所有权。它不是沙箱,也不能阻止恶意插件直接访问进程中的其他对象。真正的权限和执行隔离由 Approval、Sandbox、FS Policy 等独立能力负责。

Session:事件日志是真相

Session 的核心原则

Session 是 Agent 交互历史的仅追加事实源。模型历史、UI、Transcript、持久化、Fork 和恢复都从这份日志派生。

flowchart TD A["Session Event Log(事实)
仅追加,永久保留"] B["Session Surface(投影)
模型可见的有序视图"] C["deriveMessages()"] D["LLM Message History"] A --> B B --> C C --> D style A fill:#4f46e5,stroke:#4338ca,color:#fff style B fill:#7c3aed,stroke:#6d28d9,color:#fff style D fill:#10b981,stroke:#059669,color:#fff
图 6:从 Event Log 到模型消息的派生链

Event Log 与 Surface 的分离

这与"直接维护一个 messages 数组"有本质区别。日志里可以保存 turn/start、step/start、assistant/chunk、tool/call、tool/result 等多种事件,其中只有一部分进入模型历史。

原始日志: A B C D E F   (永久保留)

Surface: A [summary] E F   (模型当前看到)

新的替换事件可以遮蔽旧 Surface 节点,而不删除原日志。这既保留了审计和回放事实,也允许压缩旧上下文——Compaction 不再意味着删除历史。

📌

重要不变量:发送给模型的会话内容,必须能够从 Session Log 重建。如果插件要给模型增加持久上下文,不能只在请求发送前临时修改数组;它应产生能够记录来源的 Session Event。否则 Resume、Fork、遥测和重放都会看到不同的世界。

SessionStore ≠ 持久化后端

Core 中的 SessionStore 创建并持有内存 Session,提供 create()get()list()fork()flush() 等能力,但它故意不直接负责磁盘持久化。持久化插件订阅 session/event 并在 session/flush 上建立写入屏障——JSONL、本地数据库或其他存储实现可以替换,而 Session 的事件语义保持不变。

Agent Loop

Turn 与 Step

🔄

Step

一次模型请求,加上该响应产生的工具调用

🎯

Turn

从领取用户工作开始,到没有任何待处理工作为止,可包含零个或多个 Step

💡

为什么 Turn 可能没有 Step?因为输入可以在 agent/pre-step 被拒绝或改写为空。系统仍记录一次打开并关闭的 Turn,用来表达"这批工作被领取和处理过",但不会发起模型请求。

一次 Step 的完整过程

  1. 追加 step/start
  2. 把获准消息追加为 user/message
  3. 从 Session Surface 派生历史
  4. 组装 System Prompt 与 Tool Schema
  5. 解析 Provider、Model 和 Adapter 默认值
  6. 通过 agent/requestllm/stream 请求模型
  7. 持续记录 assistant/chunk
  8. 请求成功后提交完整 assistant/message
  9. 调度模型给出的 Tool Calls
  10. 按模型顺序提交 tool/calltool/result
  11. 追加 step/end
  12. 判断是否还欠下一次请求

为什么既记录 Chunk 又记录 Message

流式 Chunk 用于忠实回放、UI 增量展示和 Usage 统计;完整 Message 是成功 Provider 调用的完成锚点,也是后续模型历史的稳定来源。如果请求失败,不会伪造成功消息。若用户取消时已经看到了非空文本前缀,循环会提交带 interrupted: true 的消息,使后续请求知道用户实际看见过什么。

工具并发

工具是否可并发由工具定义的 isConcurrencySafe(args) 分类:

  • 独占调用形成屏障
  • 声明安全的调用进入有界滚动池
  • 启动前会重新分类,避免定义在排队期间变化
  • 执行主体可以重叠,但策略、持久结果和结果上下文仍按模型顺序提交
⚠️

默认独占(fail-closed):没有声明安全,或分类异常时,默认独占。这个默认值避免未知副作用被乐观并发。

Agent Loop 刻意不负责什么

默认循环只负责"请求模型、执行工具、判断是否继续"。其他行为通过插件接入:

能力接入方式
Compaction(上下文压缩)监听 agent/pre-step 和请求错误
Retry(重试)监听 agent/request-error
Approval/Sandbox/Plan使用工具执行事件和 Guard
Persistence(持久化)监听 session/event 和 session/flush
UI观察 Session 事件与 Agent 状态
Subagent(子 Agent)通过 ctx.subagents、ctx.agents 和 Jobs 组合

工具流水线

一次工具调用不是直接执行 definition.execute(),而是经过一条可插入策略的流水线:

flowchart LR A["按名称解析 Tool"] --> B["tools/pre-execute
前置观察/准备"] B --> C["单调 Guard
审批/权限/只读限制"] C --> D["tools/execute waterfall
沙箱/远端执行器"] D --> E["definition.execute
工具业务实现"] E --> F["校验规范化输出"] F --> G["tools/post-execute
审计/指标/后处理"] G --> H["finalizeContent
生成模型可见内容"] H --> I["tools/result observer
只读观察"] I --> J["Session tool/result"] style A fill:#4f46e5,stroke:#4338ca,color:#fff style C fill:#ef4444,stroke:#dc2626,color:#fff style E fill:#10b981,stroke:#059669,color:#fff style H fill:#f59e0b,stroke:#d97706,color:#fff style J fill:#7c3aed,stroke:#6d28d9,color:#fff
图 7:工具执行流水线的 7 个阶段

各阶段的职责

阶段适合做什么特性
pre-execute记录上下文、准备策略输入、做前置观察观察为主
Guard审批、权限、只读限制等"只能收紧"的决策单调收紧,不可反转
execute waterfall沙箱、远端执行器等替换执行路径可替换实际执行
definition.execute工具自己的业务实现核心逻辑
输出校验保证返回值满足声明的规范形式机器可验证
post-execute审计、指标、后处理可替换结果
finalizeContent生成写入会话并返回模型的最终内容仅工具拥有者
result只读观察最终结果仅观察

工具的三种呈现模式

📋

native

每个工具的原生 Function Calling Schema

💻

code

保留的 run_code 工具和生成的 SDK

🔀

both

原生工具与 Code Mode 同时可用

💡

通告面 = 可调用面:Code Mode 不只是隐藏 Schema。执行器也会拒绝模型绕过 run_code 直接调用其他工具,从而保证"告诉模型能调用的"和"实际能调用的"一致。

六、完整调用链

前面分别拆开了配置、Cordis、Loader 和 Agent Core。现在把它们重新接起来,区分两条链:

  • 组装主线:进程里最终有哪些能力?
  • 运行主线:这些能力如何共同完成一次任务?

组装主线:dsh 如何变成一棵运行中的插件树

flowchart TD A["CLI 入口
apps/cli/src/bin.ts"] --> B["runProfile()"] B --> C["prepareProfile()
加载 Bundle + Profile Patch"] C --> D["composeProfile()
合成所有 Patch 层"] D --> E["boot()
创建根 Context + Loader"] E --> F["mountRootInclude()
创建根 Include Entry"] F --> G["空 cordis.yml + allPatches"] G --> H["applyEntryPatches()
生成最终 Entry 列表"] H --> I["Loader 协调 EntryTree"] I --> J["插件 Fiber 完成激活"] style A fill:#4f46e5,stroke:#4338ca,color:#fff style E fill:#06b6d4,stroke:#0891b2,color:#fff style J fill:#10b981,stroke:#059669,color:#fff
图 8:从 CLI 命令到插件树激活的完整组装链
🎯

启动是一次事务:根树挂载后,boot() 会等待 Loader 的活动收敛,并执行激活审计。如果任何准备或装载步骤抛错,boot() 会销毁已创建的根 Context,避免半启动资源遗留。DSH 的启动不是"把一批模块 import 完就结束",而是一次带所有权、依赖检查和失败清理的运行时事务。

运行主线:一次 Agent 请求如何完成

sequenceDiagram participant U as User / Client participant A as Agent participant S as Session participant L as Agent Loop participant P as System Prompt participant M as LLM participant T as Tool Runtime U->>A: send / followup / steer A->>A: enqueue input A->>L: wake and run L->>S: turn start loop 每个 Step L->>S: step start L->>S: user message L->>P: assemble(agent scope) P-->>L: system prompt + tool schemas L->>S: deriveMessages(surface) L->>M: stream(request) M-->>L: assistant chunks L->>S: assistant chunk (流式) L->>S: assistant message alt Assistant 发起工具调用 L->>T: execute(tool calls) T-->>L: canonical results L->>S: tool result end L->>S: step end end L->>S: turn end A-->>U: idle / final state
图 9:一次 Agent 请求的运行主线时序

这张图省略了插件事件,但保留了三条关键不变量:

  1. 用户、Assistant 和工具的可见结果先成为 Session 事实,再用于后续模型请求
  2. 每个 Step 至多对应一次模型请求,工具调用可能触发下一个 Step
  3. Agent Loop 控制流程,策略插件通过事件和 Service seam 介入,不需要接管整个循环

两条主线在哪里汇合

flowchart TB subgraph Configuration["组装主线"] direction TB P["Profile / Bundle / Patch"] --> LD[Loader] LD --> F[Fibers] F --> SV["Context Services
ctx.agents / ctx.tools / ctx.llm"] end subgraph Request["运行主线"] direction TB U["User Input"] --> A[Agent] A --> AL["Agent Loop"] AL --> SP["Prompt / LLM / Tools"] SP --> SS["Session Events"] end SV --> A SV --> AL SV --> SP style Configuration fill:#eef2ff,stroke:#4f46e5,color:#0f172a style Request fill:#ecfeff,stroke:#06b6d4,color:#0f172a
图 10:组装主线产出服务,运行主线消费服务

组装主线最终产出 ctx.agentsctx.systemPromptctx.tools、模型 Provider 等服务;运行主线消费这些服务完成任务。插件树一旦更新,服务供给随之变化,运行中的任务则通过 Scope、依赖跟踪和 Step 边界感知这些变化。

七、设计原则与取舍

♻️

生命周期优先

资源注册与插件生命周期绑定。监听器、定时器、进程都必须明确归属于某个 Fiber

🔄

依赖是响应式关系

inject 声明的是"有效性依赖",不是一次性检查。服务替换时,消费者自动重新组合

🧩

配置描述结构

Patch 描述差异。Bundle 提供可叠加层,Profile 只表达相对于低层的差异

📜

日志保存事实

Surface 服务模型。Event Log 仅追加,Surface 可替换,Compaction 不删历史

🔒

安全保守默认

fail-closed 思路:工具未声明并发安全则独占;Guard 只能收紧;失败不伪造完成消息

🎯

核心定义机制

插件决定策略。核心定义 Turn/Step 和执行流水线语义,重试/审批/沙箱由插件实现

动态能力的代价

收益相应代价
插件可热插拔、服务可替换控制流更分散,需要追踪 Event 和 Service seam
多层 Patch 高度复用最终配置不等于任一源文件
Prompt 可动态贡献频繁变化会降低模型 KV Cache 命中率
Session 可审计、可投影需要理解 Event Log 与 Surface 两套视图
同进程插件组合高效Scope 不是恶意代码隔离,仍需 Sandbox/Approval
💡

DSH 更像一套为大型 Agent 产品准备的可组合运行时,而不是追求最短示例代码的轻量循环库。

五个常见误解

cordis.yml 是空的,所以没有配置
空文件是根 Include 锚点,真正配置来自多层 Patch
Context 是普通依赖注入容器
它还通过 Proxy、Fiber 和响应式依赖管理能力生命周期
Group 与 Include 都只是嵌套数组
Group 留在同一棵树;Include 创建有独立寻址边界的子树
SessionStore 就是持久化数据库
Core Store 管内存对象,持久化由事件插件实现
Scope 提供权限隔离
Scope 只管可见性和所有权,不是安全边界

八、调试与阅读指南

推荐源码阅读顺序

按源码目录从上到下读,容易先掉进大量具体插件。更高效的路径对应三个逐层深入的问题:

1

应用是怎么被装起来的?

CLI bin.ts → profile-boot.ts → App Boot → Loader

2

组件为什么能安全地出现和消失?

Cordis Context → Reflect → Registry → Fiber → Effect

3

装好以后一次任务怎么跑?

Scope → Session → Agent → System Prompt → Tools → Agent Loop

阅读一个插件时问四个问题

面对任意插件,不必一开始追完全部代码,先回答:

  1. 它通过 inject 依赖哪些 Service?
  2. 它通过 provide、事件或注册表贡献什么?
  3. 它创建的副作用由哪个 Fiber/Scope 所有?
  4. 卸载、失败和依赖消失时如何清理?

这四个问题通常比"它 export 了哪些函数"更接近 DSH 的真实架构。

按现象定位层次

现象优先检查原因
插件完全没出现 Profile、Bundle、Patch、--dump-config 很可能 Entry 根本没有进入最终树
Entry 存在但插件不工作 Loader 状态、模块解析、Fiber inject 可能是装载失败或依赖未满足
改配置后不能恢复默认值 Patch 优先级、对象是否重新克隆 可能把覆盖写入了复用对象
一个 Agent 看到了另一个 Agent 的工具 Scope key 和注册时使用的 Context 注册可能落到了全局 Scope
Resume 后模型上下文不同 Session Event 与 Surface 投影 可能绕过日志临时修改了消息
工具似乎绕过审批/沙箱 pre-execute、Guard、execute waterfall 策略挂载点或执行替换链可能缺失
工具结果顺序异常 并发分类与提交顺序 执行并发不应改变模型顺序
热更新后监听器重复触发 Effect disposer 与 Fiber 所有权 旧副作用可能没有被撤销
UI 有半段回复,模型却不知道 assistant/chunk / interrupted commit 取消路径可能没有提交可见前缀

三类问题的排查路径

⚙️

配置问题

先看最终结果:dsh --profile web --dump-config,再沿 Entry 的 id 反查它来自哪个 Bundle 或用户 Patch。

♻️

生命周期问题

沿所有权反向查:资源是谁创建的 → disposer 是否注册为 Effect → Effect 属于哪个 Fiber → 父子关系是否正确。

📨

请求问题

按 Session 事件切段:turn/start → step/start → assistant → tool → step/end,逐层定位卡在哪个阶段。

📌

避免静态错觉:看到 Service 类型声明 ≠ 运行时一定存在;看到默认 Bundle ≠ 高优先级 Patch 不能替换它;看到注册代码 ≠ 注册在 Fiber 存活期间始终有效;看到 Assistant Chunk ≠ 已完成消息;看到 Scope ≠ 进程级安全隔离。更可靠的思路是同时追踪结构、状态、生命周期、可见范围四个维度。

九、术语速查

术语含义
Harness 把模型变成可运行 Agent 的上下文、工具、状态、策略与控制环境
Context Cordis 暴露 Service、Event 和当前 Fiber 所有权的代理对象(Proxy)
Service 通过 ctx.<key> 消费、可被插件动态提供或替换的能力
Fiber 一个插件实例的运行与资源所有权单元,拥有自己的 Effect 列表和生命周期
Effect 随 Fiber 生命周期自动撤销的副作用(setup + disposer)
Epoch 当前依赖实现组合的指纹,用于识别依赖是否齐全及服务实现是否被替换
Entry Loader 配置树中的一个插件实例描述(id、name、config 等)
Patch 对 Entry 树的插入或字段级覆盖操作
Group 同一 EntryTree 内的结构分组,子 Entry 共享父树的 store
Include 挂载配置文件并形成独立子树边界的 Entry,有独立的根 Group 和 Store
Profile 一种部署形态,由 Bundle 列表和用户 Patch 组成(如 web、headless)
Bundle 可复用的一组 Entry Patch,是插件配置的分发单位
Scope Agent 维度的注册可见性和生命周期域(不是安全边界)
Session Log 仅追加的会话事实源,所有审计、恢复、回放都从这里派生
Surface 当前向模型暴露的 Session 投影,可压缩替换但不删除原日志
Turn Agent 领取一批输入直到本轮停止的过程,可包含零个或多个 Step
Step 一次模型请求及其紧随的工具执行
Guard 只能把执行决策进一步收紧的策略检查(单调收紧)
Capability Seam 一项可替换能力的完整接口:Service Definition + Provider + Consumer