DeepSeek Harness:Cordis 插件树与 Agent 主链路

DeepSeek Harness:Cordis 插件树与 Agent 主链路

很多 Agent 框架的架构图看起来差不多:输入进模型,模型选工具,结果回到模型,循环直到结束。Harness(dsh)的差异在于两件事同时成立------运行能力由 Cordis 插件树在启动时组装 (agent-loop 本身也是插件,没有需要打补丁的内核),运行事实写入 Session 仅追加事件账本 (模型 history 来自 deriveMessages() 对 surface 的投影,不是内存里的 messages[])。

本文沿这条主链做 源码级走读 :Profile/Bundle 如何拼树 → 核心包与 Session 如何成为 ground truth → followup 如何驱动 Inbox、kick/turn/step → 每 step 的 Context 如何组装 → 工具如何经 ctx.tools 落盘 → 打断、SubAgent 与扩展点如何挂接。贯穿例子只有一句:用户 followup("请阅读 README.md 并用一句话总结"),模型先调 Read,再文字回答------后文用这一条线把事件因果串起来。

五部分:架构总览 → Cordis 插件底座 → 核心包、Session 与调度前准备 → Agent 执行机制与 Context 工程 → 打断、SubAgent、扩展与调试。官方文档与源码锚点见文末。


第一部分 · 架构总览与设计原则

1.1 架构总览

Harness 可按 七个主题 理解:Cordis、Profile 与组合包、核心包、三域事件、轮次流程、会话日志、能力 seam。CLI / Web / SDK 等 入口 bin 与 UI 驱动 ctx.agents、订阅 session/event,不单独占一层编号。

主题 职责
① Cordis 插件向共享 ctx 贡献服务、类型化事件、可逆副作用;无特权内核
② Profile 与组合包 启动时按 bundle → profile patch → home patch → --patch 叠加,拼出有效插件树
③ 核心包 ctx 键(sessions / systemPrompt / tools / llm / agents / agentLoop)+ dsh-scope
④ 三域事件 Session (持久 log)、Agentagent/* 实时)、Capabilityfs/*tools/* ...)
⑤ 轮次流程 inbox → claim → pre-step → step → llm/stream → tools → turn/end
⑥ 会话日志 append-only ground truth;surface + deriveMessages() 供模型 history
⑦ 能力 seam Definition + Provider + Consumer;换 Provider 即换实现
css 复制代码
flowchart TB
 subgraph boot["② Profile 与组合包"]
 Bund["bundle 顺序叠加"]
 Patch["cordis.patch.yml / --patch"]
 Boot["app-boot → Loader mount"]
 Bund --> Patch --> Boot
 end

 subgraph cordis["① Cordis"]
 CTX["Context Proxy"]
 Fiber["Fiber 生命周期"]
 Ref["Reflect / Registry"]
 Ev["Events · waterfall / serial"]
 end

 subgraph core["③ 核心包"]
 Sess["ctx.sessions"]
 SP["ctx.systemPrompt"]
 Tools["ctx.tools"]
 LLM["ctx.llm"]
 Agt["ctx.agents"]
 Loop["ctx.agentLoop"]
 Loop --> Sess & SP & Tools & LLM & Agt
 SP --> Tools
 end

 subgraph seams["⑦ 能力 seam"]
 Cap["fs / shell / subagent / persistence / compaction ..."]
 end

 subgraph runtime["⑤ 轮次流程 · ⑥ 会话日志"]
 IB["Inbox → kick/turn/step"]
 Log["Session.append"]
 Der["surface → deriveMessages"]
 IB --> Log --> Der
 end

 subgraph ext["④ 三域事件(扩展点)"]
 SE["Session 事件 · 持久"]
 AE["agent/* · 实时"]
 CE["能力事件 · seam 策略"]
 end

 subgraph io["入口与表现(扩展归属表)"]
 Entry["dsh CLI · Web BFF · headless"]
 UI["Web Client · SDK · ACP"]
 end

 Boot --> cordis
 cordis --> core
 Tools --> Cap
 Loop --> runtime
 ext -.-> runtime
 ext -.-> core
 ext -.-> seams
 Entry --> boot
 UI --> Agt
 UI --> Log

数据与控制主轴

bash 复制代码
用户输入 → ctx.agents.followup/steer/inject
       → Inbox 持久化排队 → agent-loop kick/turn/step
       → Session.append(user/assistant/tool 等)
       → deriveMessages + systemPrompt.assemble → buildRequest
       → ctx.llm.stream + ctx.tools.execute
       → 再 append → 持久化 / UI 订阅 session/event

1.2 五条核心设计点

(1)Everything is a plugin --- 无特权内核

agent-loop、Session、tools、LLM 适配器与 Read/Shell 工具 同级 ,都是 cordis.yml 条目。换模型 = 换 llm 适配器插件;换持久化 = 换 session-persistence 插件;不必改 agent.ts

(2)模型可见 ⟺ 已记录 --- Session 是唯一 context 源

进 LLM 的 systemtoolsmessages 必须能从 Session log 重建。写:session.append(..., { surfaceOp });读模型 history:deriveMessages() 只读 surface,不是整份 log。

(3)依赖声明加载 --- Cordis inject

插件 inject: ['tools','sessions',...],Cordis 在依赖 ACTIVE 后才 LOADING。agent-loop 声明 agentssessionsllmtoolssystemPrompt 五键齐备 才进入 LOADING;tools 另 inject systemPrompt 。cordis.yml 条目顺序不决定 加载顺序------Fiber 依赖图决定。缺服务则 PENDING(等待,非报错)。

(4)扩展走事件与服务,优先不改 loop

三域事件 是大多数扩展的第一个决定:持久事实走 Session 事件 ;观察/拦截进行中 driver 走 agent/* ;seam 策略走 能力事件tools/*fs/* ...)。

意图 挂载点
改本 step 是否进模型 agent/pre-step(waterfall)
改 provider/model agent/request
审批/包装工具 tools/pre-executetools/execute
改 prompt/tools 列表 ctx.systemPrompt + system-prompt/assemble
新持久事实 扩展 SessionEventMap + append

packages/core/agent-loop 是最后手段。

(5)Capability seam 三件套 --- 可替换能力必须拆全

能力 seam 中的每一项(Shell、FS、Web、SubAgent...),在 Harness 里不是「一个工具包打天下」,而是 Service Definition + Service Provider + Consumer 三角色齐备,才构成完整的 capability seam 。缺一角就不是 seam,只是半成品插件。替换实现时 只换 Provider(例如 bash 本机执行 ↔ 沙箱执行),Definition 契约与 Consumer(模型看到的工具)保持不变。下文用 Shell 例子展开三者的分工与调用关系。


1.3 Capability seam 详解(设计点 5 展开)

设计点 (5) 只说了「三件套齐备才是 seam」。本节把 Definition / Provider / Consumer 分别是什么、如何协作 讲清楚;周边能力插件也建立在这一模式之上。

1.3.1 三件套是什么

角色 做什么 典型形态
Service Definition 定契约、占 ctx 键 --- 声明服务(如 ctx.shell)、Request/Result 类型、resolve() 如何补全参数 Cordis Service 子类,如 dsh-shell
Service Provider 给实现 --- 在运行时挂载具体后端(本机 bash、沙箱、HTTP fetch...) 独立插件包,可多个或互斥
Consumer 接到模型或产品 --- 通常是 ctx.tools.register 的工具;内部 inject Definition 并调用服务 dsh-tool-bash
复制代码
Consumer(模型看到的 bash 工具)
 ↓ inject + 调用
Service Definition(ctx.shell 上的统一 API)
 ↓ 运行时绑定
Service Provider(本机 or 沙箱执行)

单独一个 Provider 或单独一个 Tool 都不是 seam 。seam = Definition + 至少一个 Provider + 至少一个 Consumer 构成的 完整能力

1.3.2 核心例子:Shell(bash 执行)

bash 执行 seam 为规范范例:

角色 ctx / 产物
Definition dsh-shell ctx.shell --- ShellExecRequest / ShellExecSpecresolve(request)
Provider dsh-bash-local 本机进程树执行
Provider dsh-bash-sandbox 沙箱/隔离环境执行(profile 可切换)
Consumer dsh-tool-bash 模型可见的 bash 工具;execute 里调 ctx.shell

模型发起一次 bash 调用时

rust 复制代码
sequenceDiagram
 participant M as 模型
 participant Loop as agent-loop
 participant TB as dsh-tool-bash<br/>(Consumer)
 participant SH as ctx.shell<br/>(Definition)
 participant PR as dsh-bash-local<br/>(Provider)

 M->>Loop: assistant/message 含 tool-call bash
 Loop->>TB: ctx.tools.execute(name=bash, args)
 TB->>SH: resolve(Request) → Spec
 TB->>SH: 执行 API
 SH->>PR: spawn、收 stdout
 PR-->>SH: CollectedOutput
 SH-->>TB: 结果
 TB-->>Loop: tool result blocks
 Loop->>Loop: session.append tool/result

各层 只管自己的话

  • Consumer --- 模型参数 → ShellExecRequest;不管本机还是沙箱。
  • Definition --- Request/Spec、超时、workdir、abort;不管 JSON Schema。
  • Provider --- 按 Spec 真跑命令;不管 Session 与 turn/step。

1.3.3 换 Provider 为何不用改 Tool

profile 里把 dsh-bash-local 换成 dsh-bash-sandbox

  • Consumer dsh-tool-bash 不变 --- 模型仍见同一 bash 工具
  • Definition ctx.shell 不变 --- 契约不变
  • Provider 替换 --- 命令实际执行环境变

替换边界在 Provider ,而不是在 agent-loop 或 Tool 里写 if (sandbox)。Consumer 经 ctx.tools 注册,Provider 经能力 seam 挂载------二者通过 Definition 解耦。

1.3.4 不完整拆分的后果

做法 问题
在 Tool 里直接 child_process.spawn 换沙箱要改 Tool;PTY、jobs 等无法复用
只有 Provider、没有 Definition 无统一 Request/Spec,词汇无法共享
只有 Definition + Provider、没有 Consumer 能力存在,模型不可见

FS、Web(search/fetch)、SubAgent(Task 工具)、LLM(ctx.llm + 适配器)均用同一三角色模式;差异主要在 Provider 能否 多个并存(SubAgent、Web search 可以;bash executor 通常单一 active)。

1.3.5 与设计点(4)如何配合

设计点 解决什么
(5)Capability seam 能力 由谁实现、如何整包替换
(4)扩展走事件 同一次调用链上 如何插策略 (如 tools/pre-execute 审批 bash)

Consumer 仍调 Definition → Provider;审批挂在 tools 流水线,不打破三角色边界。


第二部分 · Cordis 插件底座

Harness 的业务语义在 dsh-* 包;Cordis 只负责「插件怎么活」 ------依赖顺序、服务注册、事件分发、可逆副作用。源码 vendored 在 vendor/cordis/src/;Loader / Include / Group / HMR 在 vendor/*本身也是插件,不是「内核外的加载器」。

2.1 源码模块架构:谁协作、谁不负责业务

Cordis core 文件极少,但职责边界清晰:

arduino 复制代码
vendor/cordis/src/
 context.ts Context 类 + Proxy 入口;extend / isolate / intercept
 reflect.ts 服务 store;Proxy handler;provide / get 沿 Fiber 链解析
 fiber.ts 单插件生命周期、inject 检查、effect/disposer、config 校验
 registry.ts Plugin 形状归一化;ctx.plugin / ctx.inject
 events.ts emit / waterfall / parallel / serial / bail
 service.ts Service 基类;构造时 reflect.provide;intercept config 合并
 logger.ts 日志服务
 utils.ts symbols、DisposableList、callable 包装
 index.ts 公共导出

实现层协作关系(读源码时的 mental model):

css 复制代码
flowchart TB
 subgraph boot["启动"]
 YML["cordis.yml 条目"] --> LDR["Loader 插件"]
 LDR --> IMP["dynamic import(name)"]
 IMP --> REG["ctx.registry.plugin(plugin, config)"]
 end

 subgraph core["Cordis core"]
 REG --> FIB["Fiber(PENDING→LOADING→ACTIVE)"]
 FIB --> RUN["Service 构造 或 apply(ctx)"]
 RUN --> PROV["reflect.provide('tools', instance)"]
 RUN --> EFF["ctx.effect / ctx.on → disposer"]
 CTX["Context Proxy"] --> REF["ReflectService"]
 CTX --> EVT["EventsService"]
 REF --> PROV
 end

 subgraph harness["Harness 插件(举例)"]
 PROV --> T["ctx.tools = ToolRuntime"]
 PROV --> A["ctx.agents = AgentRegistry"]
 PROV --> S["ctx.sessions = SessionStore"]
 EFF --> TR["dsh-tool-fs: tools.register(Read/Write)"]
 end

 FIB -->|"inject 依赖齐"| RUN
 T --> AL["agent-loop inject tools 后 ACTIVE"]
模块 不负责什么
Reflect 不管 yml、不管 npm import、不管 Agent turn
Fiber 不管 Session 事件、不管 LLM 协议
Registry 不解析 profile patch;那是 Loader
Events 不持久化;Harness 的 session 事件在 dsh-session

Cordis 回答:插件何时加载、服务挂在哪、如何查找、如何卸载;不回答「一句 followup 怎么跑」------那是 agent-loop 的事。

2.2 从 cordis.yml 到 ctx:完整链路

2.2.1 Profile 如何变成有效插件树

运行中的 dsh 不是读单个 yml,而是 多层叠加

arduino 复制代码
dsh --profile headless "任务"
  → app-boot 解析 profile
  → 按序叠加:bundle 组合包 → profile 的 cordis.patch.yml → home patch → --patch
 → 得到有效 Entry 树(每条:id / name / config / disabled / inject)
 → 根 Context 创建 → Loader 插件 mount → Loader 逐条 import + ctx.plugin
 → 依赖自发满足 → ctx 上出现 sessions / tools / agents / llm / agentLoop ...
 → 具体 bin(CLI / webserver)再 inject 所需服务并启动 I/O

验证本机实际树(不 boot 全应用也可看 patch 结果):

css 复制代码
pnpm dsh --profile web --dump-config

examples/headless-agent/cordis.yml 片段(每条 name 对应一个 npm 包,Loader 会 import 并 mount):

yaml 复制代码
- id: llm-deepseek
  name: '@deepseek-ai/dsh-llm-deepseek'
- id: bash
  name: '@deepseek-ai/dsh-bash-local'
- id: agent-spine
  name: '@deepseek-ai/dsh-agent-spine-demo'
  config:
    agents: [{ id: main, provider: deepseek-official, model: deepseek-v4-flash, ... }]
- id: tool-fs
  name: '@deepseek-ai/dsh-tool-fs'

2.2.2 单条条目:import → Fiber → provide

Loader 对每条 Entry 的核心路径(vendor/loader/src/config/entry.ts):

scala 复制代码
Entry.init()
 1. import(options.name) # 动态加载 @deepseek-ai/dsh-tools 等模块
 2. unwrapExports(module) # 取 default / 命名导出插件对象
 3. ctx.registry.plugin(plugin, config)
 → new Fiber(parentCtx, config, inject, runtime)
 → 子 ctx = parent.extend({ fiber })
 → _checkImpl:inject 的服务是否已有 ACTIVE 提供方
 → 依赖齐 → LOADING
 4. LOADING 阶段
 · class extends Service → new ToolRuntime(ctx, config)
 → super(ctx, 'tools') → reflect.provide('tools', this)
 · 或 function apply(ctx) { ctx.inject(['tools'], child => { ... register ... }) }
 5. ACTIVE → fiber.store 快照;依赖方 Fiber 被唤醒

两个具体形态(Harness 里极常见):

形态 谁提供 ctx 键 谁消费 / 注册工具
Service 插件 dsh-toolsexport default class ToolRuntime extends Servicectx.tools dsh-tool-fsinject: ['tools','fs']applyctx.tools.register(...)
纯 apply 插件 同上,由别的包 provide dsh-bash-local provide ctx.shelldsh-tool-bash inject shell + tools
rust 复制代码
sequenceDiagram
 participant Y as cordis.yml
 participant L as Loader
 participant M as dsh-tools 模块
 participant R as Registry/Fiber
 participant Ref as Reflect.store
 participant TF as dsh-tool-fs
 participant AL as dsh-agent-loop

 Y->>L: Entry id=tools, name=dsh-tools
 L->>M: import
 M->>R: plugin(ToolRuntime, config)
 R->>R: inject systemPrompt 满足 → LOADING
 R->>Ref: provide('tools', ToolRuntime)
 Note over Ref: ctx.tools 可读

 Y->>L: Entry id=tool-fs, name=dsh-tool-fs
 L->>TF: import + plugin(apply)
 TF->>TF: inject tools, fs → LOADING
 TF->>Ref: tools.register(Read/Write/Edit)

 Y->>L: Entry agent-loop
 L->>AL: plugin(AgentLoop)
 AL->>AL: inject agents,sessions,llm,tools,systemPrompt
 Note over AL: 五者 ACTIVE 后 agentLoop 才 LOADING

inject 顺序不是手写启动脚本agent-loop 声明 static inject = ['agents','sessions','llm','tools','systemPrompt'],缺任一服务则其 Fiber 保持 PENDING,直到对应插件 ACTIVE。

2.3 ctx 里有什么

ctxProxy 服务容器 + 事件总线 + 当前 Fiber 句柄 。读 ctx.xxx 不是读普通对象属性,而是 Reflect 沿 Fiber 父链查 store

2.3.1 Cordis 内置(根 ctx)

键 / API 来源
reflectregistryeventslogger 根 Context 构造时创建
loader Loader 插件 provide('loader', ...)
plugin / inject / effect / on / emit / waterfall mixin 到 ctx 的便捷 API
fiber 当前插件实例;根 Fiber 代表「应用根」

2.3.2 核心包:六个 ctx 键(必记)

这六个服务 全是插件 ,由 cordis.yml 挂载;agent-loop inject 五键齐备后才 ACTIVE。dsh-scope 无 ctx 键,提供按 agent 划分的作用域注册(agent.ctx)。

ctx 键 提供方包(典型) 职责
sessions dsh-session 仅追加 SessionEvent log;append / deriveMessages / fork / resume
systemPrompt dsh-system-prompt section、变量、每 step assemble tools schema + prompt
tools dsh-tools 作用域化 register / view / execute 流水线
llm dsh-llm + 适配器插件 适配器 seam、stream、prepareCall
agents dsh-agent Agent 注册表、create / resume / withInitiator
agentLoop dsh-agent-loop 默认 driver ReactLoopAgent;config 里可声明 agents[]
rust 复制代码
flowchart TB
 Loop["ctx.agentLoop<br/>dsh-agent-loop"] --> Sess["ctx.sessions"]
 Loop --> SP["ctx.systemPrompt"]
 Loop --> Tools["ctx.tools"]
 Loop --> LLM["ctx.llm"]
 Loop --> Agt["ctx.agents"]
 Agt --> Sess
 SP --> Tools

Consumer 插件如何挂到 tools 上(设计点 5 的 Consumer 侧):

less 复制代码
dsh-tool-fs(Consumer)
 inject: ['tools', 'fs', 'systemPrompt']
 apply(ctx) {
 ctx.tools.register(defineTool({ name: 'Read', execute: ... }))
 ctx.tools.register(defineTool({ name: 'Write', ... }))
 }

模型 永远看不见 ctx 对象本身;它只见 systemPrompt 组装出的 schema + tools.execute 的返回值。ctx.tools运行时注册表,不是 prompt 字符串。

2.3.3 能力 seam 与其它常见 ctx 键

ctx 键 说明
shell 能力 Definition dsh-shell;Provider 如 dsh-bash-local
fs 能力 Definition dsh-fs-local + policy 插件
subagents 能力 dsh-subagent + spawn/fork Provider
sessionPersistence Session 周边 JSONL/SQLite;resume 用
jobs 后台 Task 工具、continuable subagent
commands 交互 用户命令,无需模型轮次
codeRuntime 工具模式 tools.mode: code 时的 SDK 渲染

2.3.4 TypeScript 类型 vs 运行时

typescript 复制代码
declare module '@deepseek-ai/cordis' {
 interface Context {
 tools: ToolRuntime
 agents: AgentRegistry
 // ...
 }
}

运行时 只有插件 provide 之后才有实现;未 mount dsh-tools 时读 ctx.tools 会抛「未 inject / inactive」。类型图方便 IDE;--dump-config 才是本机 ground truth

2.4 Fiber 生命周期与 inject

复制代码
PENDING ──依赖齐──► LOADING ──成功──► ACTIVE
 ▲ │
 │ └──失败──► FAILED
 └── 某 inject 服务 dispose ──► 回 PENDING / UNLOADING

ACTIVE ──dispose/HMR──► UNLOADING ──► DISPOSED
  • PENDINGinject 列表里至少一个服务尚无 ACTIVE 提供方(或 Service.check() 为 false)。
  • LOADING :跑 apply 或 Service 构造函数;此阶段注册的 effect 在 unload 时逆序 disposer。
  • ACTIVEfiber.store 有效;其他插件可读该服务。

插件永远 PENDING 时:查是否漏 mount 提供方(常见:忘了 dsh-tools 或 llm 适配器)。

2.5 读 ctx.tools 时发生什么

scss 复制代码
loopCtx.tools
 → Proxy get('tools')
 → waterfall('internal/get')
 → 从当前 Fiber 沿 parent 查 store['tools']( respect isolate 标签)
 → 命中 dsh-tools Fiber ACTIVE 时的 ToolRuntime 实例

Reflect 核心逻辑(vendor/cordis/src/reflect.ts):

rust 复制代码
 return ctx.events.waterfall('internal/get', ctx, prop, error, () => {
 const key = target[symbols.isolate][prop]
 let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber
 while (true) {
 const impl = fiber.store?.[prop]
 if (impl) return getTraceable(ctx, impl.value)
 ...
 fiber = fiber.parent.fiber
 }
 })

2.6 三种 ctx:别把 loopCtx 和 agent.ctx 混了

Agent 路径上会出现三个 Context;agents / tools 的注册位置取决于你在哪一个 ctx 上调用:

ctx 谁持有 典型用途
loopCtx AgentLoop 插件 fiber driver 构造、llm.stream、全局 listener
ownerCtx 调用 agents.create 的 caller fiber lifecycle effect、ownership、取消
agent.ctx 每个 Agent 实例 extend({ agent: this })仅该 agent 的工具 / section
ini 复制代码
AgentLoop 构造 ReactLoopAgent 时:
 scope = createScope(loopCtx, agent)
 agent.ctx = scope.ctx.extend({ agent })

在 agent.ctx 上 tools.register → 只对该 Agent 可见
在根 ctx 上 register → 全局继承(agent.ctx 解析时沿父链找到)

Preset、restrict、subagent 的「谁能看见哪些工具」都靠 注册 ctx + dsh-scope 层 ,不在 agent-loop 里写 if (preset === ...)

2.7 Events 与 effect

模式 Harness 用法
waterfall agent/pre-stepagent/requesttools/execute --- 须 next() 委托
emit agent/statussession/event
serial agent/turn-stopping(无 next)

effecttools.register、事件监听器必须可逆 disposer;Fiber unload / HMR 时逆序执行,否则泄漏。

2.8 组合插件(Loader 家族)

插件 作用
Loader 维护 Entry 树;import、ctx.registry.plugin!!js config 在 internal/config 插值
Include 嵌套 yml + patch 层(profile 叠加的基础)
Group 整组 mount/unmount;配合 isolate 做 scope
HMR 文件 / yml 变更 → Fiber dispose → 重新 import → remount

Profile 叠加顺序:bundle → profile patch → home patch → --patch。yml 变更按 entry id diff,稳定 id 才能增量 HMR;缺 id 则任意编辑都可能 remount 全树。


第三部分 · 核心包、Session 与调度前准备

kick / turn / step 开始之前,Harness 要先完成:Profile 拼好插件树核心包 ACTIVESession 账本建立Agent 绑定并发布。本节讲清 ground truth、事件如何分层、多 Agent 如何共存------没有单独的「调度内核」,只有多个独立 driver 与委派关系。


3.1 调度前:核心包就绪与 Agent 从哪来

3.1.1 六插件依赖(核心包 inject 图)

Agent 跑起来之前,agent-loop 的 Fiber 必须等到五样 inject 全部 ACTIVE。static injectagent-loopagents / sessions / llm / tools / systemPrompttoolssystemPrompt

就绪顺序(典型) 含义
sessions + agents + llm + tools + systemPrompt 核心包服务 mount 完成
agentLoop LOADING setFactory(this),开始接受 create/resume
agents.create / config 启动 进入发布事务
agent/session-start 第一个可安全做启动注入的扩展点
followup → kick 触发 driver 循环

要点agentLoop 是插件,但 ReactLoopAgent 不能通过 yml 换类 ------可替换边界是整包 AgentFactory;日常改行为走事件,不改 driver 源码。

3.1.2 三条创建入口

入口 谁调用 典型场景
ctx.agents.create / resume UI、ACP、subagent、测试 指定 sessionId、可选 setup(agentCtx)
agentLoop.config.agents[] cordis.yml 配置 headless 预创建 main;Web profile 常为空,客户端按需 create
ctx.agentLoop.create(...) 示例 / 快捷 API 内部 prepare → 立即 publish,随 AgentLoop fiber dispose

应用与 UI 只面向 ctx.agents ,不 new ReactLoopAgent

3.1.3 发布事务:create / resume 共用骨架

无论哪条入口,最终都走 setupAndPublishdsh-agent-loop):在 Agent announce 之前 ,Session 与 Agent 都不进全局注册表

rust 复制代码
sequenceDiagram
 participant Caller
 participant AL as AgentLoop
 participant SS as SessionStore
 participant AR as AgentRegistry
 participant Drv as ReactLoopAgent

 Caller->>AL: create / resume
 AL->>SS: prepare 或 persistence.prepare
 Note over SS: Session 对象存在,未 enter
 AL->>Drv: new ReactLoopAgent(loopCtx, id, options, session)
 Note over Drv: Inbox 从 session.events 重放<br/>Phase 从最后 turn/start 恢复
 AL->>AL: await setup(agentCtx)
 Note over AL: 未发布:可在 agentCtx 注册 per-agent 工具/section
 AL->>SS: enter(session)
 AL->>AR: enter(agent, owner)
 AL->>SS: announce → session/created
 AL->>AR: announce → agent/created
 AL->>Drv: agent/session-start { source }
 AL-->>Caller: AgentHandle(配置启动无 handle)

任一步失败 → rollback:registry detach、scope dispose,不留半创建对象。


3.2 Session:append-only 账本

设计点(2)「模型可见 ⟺ 已记录」 的落地载体是 Session,不是 Agent 内存里的 messages[]

原则 含义
事件溯源 append,不改旧事件;一切可 replay
log 是权威 surface、deriveMessages、requestHeader 都是 解释
持久化是插件 dsh-session 管内存接受与 session/event;JSONL/SQLite 订阅写盘
Lossless JSON payload 必须可无损快照(snapshotJsonValue

服务入口:ctx.sessionsdsh-session · SessionStore)。


3.3 四种视图:别混读

读 Session 时最容易混的是四种「视图」------它们都从同一份 log 来,但消费者不同:

perl 复制代码
 ┌──────────────────────────────────────┐
 │ Session.events(完整 append-only log) │
 └──────────────────┬───────────────────┘
 │
 ┌──────────────────────────┼──────────────────────────┐
 ▼ ▼ ▼
 Session.surface deriveMessages() requestHeader()
 nodes: seq[] → Message[] → EpochHeader
 │ │ │
 │ └─ 每 step buildRequest │
 │ │
 compaction 改这里 路由/epoch 快照
 (log 不动) (与 messages 分离)
视图 是什么 谁消费
events 全部已提交事件(冻结快照) 持久化、UI 全量 transcript、replay、Inbox 重放
surface.nodes 当前 模型可见 事件的 seq 有序列表 deriveMessages()、compaction 选段
deriveMessages() 把 surface 投影成 Message[] agent-loop buildRequest
requestHeader() fold 最新 request/header provider/model 变更检测、resume 提案

人类 transcript vs 模型 history :UI 应读 append-origin 事件(读者已见历史不能被 surface replace 抹掉);模型只读 deriveMessages(compaction 后 history 变短,log 里原文仍在)。


3.4 事件分层:trace、模型可见、Agent 实时

3.4.1 执行 trace(进 log,通常不进 surface)

记录 怎么跑的 ,重建模型对话时不必全部进 messages

事件 作用
turn/start · turn/end 轮次边界;turn/end 带结束原因
step/start · step/end 步骤边界
assistant/chunk 流式 token 录像(UI/replay;不上 surface
tool/call 工具调用审计(参数、callId;不上 surface
request/header · request/context 本 epoch 路由、system/tools 快照元数据
llm/retry 同 step 内 LLM 重试
compaction/* 压缩 bracket(部分仅 log)
agent/inbox/spliced Inbox 队列变更(持久化排队
插件扩展 todo/writehook/*fs/observed...

3.4.2 模型可见 message(必须带 surfaceOp

只有三种 type 可上 surface:

事件 surfaceOp 投影结果
user/message appendreplace { start, end } user 消息
assistant/message 同上 assistant(含 tool-call block)
tool/result 同上 tool-result

附加字段:

  • sourceEventSeqs --- 溯源(如 assistant/message ← 多个 assistant/chunktool/resulttool/call
  • replace --- 只改 surface 索引不删 log 里旧 seq(compaction 核心)

3.4.3 Agent 实时事件(不在 Session log)

事件 作用
agent/status Phase:idle / running
agent/inbox/inserted · discarded · claimed Inbox 内存变更通知(UI)
agent/created · agent/disposed 注册表生命周期

进程 crash 后 Phase 与 Inbox 内存丢失Session log 从磁盘 reload ,新 driver resume 后 Inbox 从 agent/inbox/spliced 重放。


3.5 Surface 机制与 deriveMessages

3.5.1 append 与 replace

bash 复制代码
surfaceOp: append
 → surface.nodes 尾部 +1 seq

surfaceOp: replace { start, end }
 → 从 nodes 去掉 [start..end] 覆盖的 seq
 → 换成 1 个新 seq(log 里被 replace 掉的旧事件仍在)

compactiondsh-compaction-basic 等)在 agent/pre-step 触发:生成 summary 事件,对旧 message 段做 surface replace从不改写历史 append 记录

3.5.2 走读示例:summary 如何 replace 一段 history

假设某 Session 的 surface.nodes(括号内为事件 type 简写):

ini 复制代码
seq: [10] [11] [12] [13] [14] [15] [16] [17]
 u₀ a₀ t₀ a₁ u₁ a₂ t₁ u₂
 └──────── 待压缩区(老) ────────┘ └──── retain 尾部 ────┘
  • log(events) :seq 10--17 的 append 记录 永远保留(审计、UI 全量 transcript、replay)。
  • token meter 测得 surface 总 token 超过阈值(默认约为 context window 的 80%)。
  • selectCompactableRange :从尾部往前保留约 16% window 的节点 → 压缩区为 seq 10--13;并 回退 cut 直到 tool call / tool result 配对平衡(不能把 assistant+tool_call 和 tool/result 拆开)。

压缩 commit 时 append(简化):

css 复制代码
compaction/start
compaction/summary { shadowedSeqs: [10,11,12,13], ... }
user/message { content: "<compacted-summary>...</compacted-summary>" }
 surfaceOp: replace { start: 10, end: 13 }
compaction/end

replace 之后 surface.nodes

css 复制代码
[18] [14] [15] [16] [17]
checkpoint_u u₁ a₂ t₁ u₂

deriveMessages() 此时约等于:[checkpoint_u, u₁, a₂, t₁, u₂] --- 模型 看不到 u₀/a₀/t₀/a₁ 原文,但 log 里 seq 10--13 仍在。

对比 阶段 A(tool result pruner) :对 单条 过长的 tool/result单点 replace(head + marker + tail),不调 LLM;overflow 路径上常先于 summary 执行。

读者 读到什么
模型(deriveMessages) replace 后的短 history
UI 全量 transcript append-origin 事件,含被 replace 遮蔽前的原文
磁盘 log 全部 seq,含 compaction bracket 与旧 message

3.5.3 deriveMessages 做什么

scss 复制代码
deriveMessages()
 → 只遍历 surface.nodes 当前 seq 列表
 → 每条 eligible 事件 → deriveEventMessage → Message 或 skip
 → replaceGeneration 变化时可能全量重建(增量优化对调用方透明)

agent-loop 在 user/message append 之后deriveMessages() 拼 LLM 请求------因此本 step 刚写入的用户话 一定在 messages 里

3.5.4 append 内部流水线(简化)

bash 复制代码
session.append(type, data, { surfaceOp?, sourceEventSeqs? })
 1. 校验 JSON、type 是否在 SessionEventMap、surface 规则
 2. 分配 monotonic seq,写入 events 数组
 3. 若有 surfaceOp → 更新 surface.nodes
 4. emit session/event(UI、BFF、persistence 并行订阅)

Store 级 API:

API 作用
sessions.create(id?, { meta, seed? }) 新建 live Session
sessions.prepare / enter / announce agent-loop 发布事务
sessions.fork(source, boundary?, childId?) 从已完成 turn 前缀 fork 子 Session(不自动创建 Agent)
sessions.flush(session) 等 persistence listener checkpoint

3.6 Agent 与 Session 如何绑定

3.6.1 一个 id,两个注册表

ini 复制代码
 SessionId(进程内唯一)
 │
 ┌─────────┴─────────┐
 ▼ ▼
 SessionStore AgentRegistry
 events / surface driver / Phase / Inbox 投影
 │ │
 └─────────┬─────────┘
 ▼
 ReactLoopAgent
 id === session.id
 session: 构造注入的同一对象

硬约束

  • agent.id === session.id === SessionId
  • 同一 id 同时只能有一个 live Session + 一个 live Agent
  • Session 存事实Agent 存行为(何时 kick、claim inbox、append 什么)

3.6.2 构造时绑定

ReactLoopAgent 构造(简化):

kotlin 复制代码
constructor(loopCtx, id, options, session) {
 this.inbox = new Inbox(session, notifications) // 从 session.events 重放 inbox
 const lastTurn = session.events.findLast(e => e.type === 'turn/start')?.data.turn ?? 0
 this.phase = { kind: 'idle', lastTurn }
 this.scope = createScope(loopCtx, this)
 this.ctx = this.scope.ctx.extend({ agent: this })
}
绑定 说明
agent.session 全程向 同一 Session append
Inbox(session) 每次 splice agent/inbox/spliced,再改内存队列
Phase.lastTurn 从已有 log 恢复;resume 后不是「唤醒旧 Agent 对象」
agent.ctx per-agent 注册域(工具、section 只服务此 agent)

3.6.3 create vs resume

create resume
Session 来源 prepare(id, { meta, seed? }) 空 log 或带 seed sessionPersistence.prepare(id) 冷加载 + 可选 crash repair
Agent 实例 总是新 driver 总是新 driver(Phase → idle)
session-start.source 'startup' 'resume'
需要 persistence (无盘则无法 resume)

resume 语义:新 Agent 实例 + 旧 Session log,不是恢复 crash 前的内存 Phase。

3.6.4 Header 与 seed 边界

SessionHeader (创建时一次):{ id, version, cwd?, parentSession?, seedLength?, ... }

  • fork 子 SessionparentSession + seedLength 标记继承前缀
  • session/end-seed:标记 constructor seed 结束;此前 seq 来自 fork/resume/replay,此后才是本进程 live append

request/header (每 epoch):{ config, system?, tools?, adapterDefaults? } + reason: initial | resume | change

  • messages 分离:改 provider/model 不必重写 history

3.6.5 setup 与调度前扩展

setup(agentCtx) (create/resume 选项):

  • Agent 已构造、未 announce
  • 可在 agentCtx 上注册 仅该 agent 的工具 / system-prompt section
  • 不可 followup(driver 尚未发布)

发布后第一个扩展点agent/session-start --- 可 inject 首条上下文、触发启动逻辑。


3.7 多 Agent 场景

Harness 没有「多 Agent 中央调度器」------没有全局 turn 队列或内核线程。

ini 复制代码
每个 Agent = 一个 ReactLoopAgent(独立 kick → while(turn()))
每个 Agent = 一个 SessionId = 一份 Session log
AgentRegistry = 进程内 id → Agent live 表

3.7.1 多种「多 Agent」来源

场景 怎么来的 调度关系
配置多根 agent agentLoop.agents[] 各 create 一次 彼此独立,各跑各的 loop
Web 多会话 客户端多次 agents.create 不同 sessionId 同上
subagent 委派 Task / subagent 工具 → agents.create 子 id 树状 runtime owner;见下
fork sessions.fork 得 child Session → 再 create child agent Session 谱系在 header;≠ runtime owner
Jobs 后台 jobs.start + one-shot 子 run 父 step 不阻塞

3.7.2 三种关系不要混

关系 记录位置 含义
live 注册 AgentRegistry agents.get(id) 是否存在
runtime owner agents.enter(agent, owner) 谁创建了此 agent(subagent 父链)
持久谱系 SessionHeader.parentSession fork/resume 用的 Session 父子

根 agent:owner = undefinedagents.roots())。

3.7.3 并发模型:多 driver、非共享循环

不同 Agent = 不同 async driver Promise(OS 线程上并发 async,不是同一个 for 循环):

arduino 复制代码
父 driver: ... step → executeToolCalls → [await 子工具?] → ...
子 driver: kick → turn → step → ... (完全独立 Session log)
委派模式 父 driver 是否等待 典型
前台 one-shot subagent child.whenIdle() 父 step 卡在 tool execute
后台 one-shot(Jobs) 不等 execute 立刻返回 jobId
continuable 几乎不等 子 agent 自主 FIFO;父通过 send_message / 读子 Session 跟进

同一 agent 内 不会两个 kick 真并行:wakeDriver 在 non-idle 时 latch。

withInitiator :每次 kick()withInitiator(this, ...) 内运行,使 ctx.agents.requireInitiator() 指向当前 driver------subagent 并发时各自隔离 initiator 链。

3.7.4 fork 与 subagent 对比(调度前视角)

fork spawn subagent
Session 新 id,继承 prefix + 新 live append 新 id, 或 policy 定 seed
Agent 需另行 create 工具内 agents.create
模型 context 继承父 Session 已完成 turn 前缀 通常 fresh agent(fork 工具则继承)
典型用途 分支探索、checkpoint 实验 并行任务、专家委派

3.7.5 ContinuationManager:continuable 子 agent 谁协调

one-shot subagent(父 step 里 await child.whenIdle())不经过 ContinuationManager。continuable 模式才需要它------ctx.subagents 背后的 SubagentContinuationManagerpackages/subagent/subagent/src/continuation.ts)。

为什么需要单独一层

可继续子 agent 同时要满足:

需求 负责方
持久 Session(跨重启) Session + persistence
进程内 live driver agent-loop
FIFO 轮次队列 Agent Inbox(每个 agent 唯一)
何时物化 / 唤醒 / 冷恢复 ContinuationManager
父何时能 dispose ownedChildren 所有权图
子结束后如何告知父 notifySettlement (不能靠外部 subagent/end listener------那时 child handle 可能已 dispose)

三个生命周期(不要混):

bash 复制代码
Continuable Session(持久,childId 稳定,log 里有 subagent/descriptor)
 └─ Activation(进程内,同一 childId 同一时刻 ≤1)
 ├─ AgentHandle + 子 driver
 ├─ inbox → agent-loop 执行 turn
 └─ ownedChildren → 尚未 settle 的子孙 Activation id
启动与后续消息

startContinuable (工具返回 childId + messageId 时):

markdown 复制代码
1. 预留 childId,写 subagent/descriptor 到 seed
2. provider.prepareContinuable() → seed(仅 detached 数据)
3. materialize → agents.create 或(冷路径)agents.resume
4. submitMaterialized(initial prompt) → child.followup()
5. return { childId, messageId } // inbox **接受**即成功,不保证 turn 已开始

followup(parent, childId, content) (父后续发消息)在 per-child 锁 内:

情况 行为
无 Activation coldResume → 读 descriptor + log → materialize → submit
Activation 正在 disposal 等 teardown 结束 → 重试 / coldResume
有 live Activation submitAdmitted → child.followup() + wake

鉴权:parent 必须是 live 对象,且 parent.id === child.session.header.parentSession(持久直接父)。

三态 residency(推导,非独立状态机)
ini 复制代码
stateOf(activation):
 running ← Agent.status === 'running' 或 accepted 非空(followup 已接受但未 claim 的窗口)
 waiting ← idle 且 ownedChildren 非空(自己 quiet,子孙还在)
 settled ← idle 且 ownedChildren 空 → 可 dispose handle
lua 复制代码
stateDiagram-v2
 direction LR
 [*] --> running: materialize + submit
 running --> waiting: whenIdle 且 ownedChildren 非空
 running --> settled: whenIdle 且 ownedChildren 空
 waiting --> running: followup 唤醒
 waiting --> settled: 所有子 Activation dispose
 settled --> [*]: dispose handle + notifySettlement

ownedChildren :子 startContinuable 时,acquireOwnership(父, childId) 把 childId 记入 直接父 的 set。父在 waiting (子孙未 settle)时 不能 dispose,避免树半途解散。

结算:child-first dispose + 通知父

watchSettlement 每个 Activation 一个 async 循环:whenIdle → 若 stateOf === 'settled'dispose (先递归 dispose 子孙,再 handle.dispose())。

notifySettlementreleaseOwnership 之前 调用(避免父 watcher 误判已无子 agent):

父状态 投递方式
父正在 teardown inject(不唤醒,避免 dispose 前多跑一轮 LLM)
父 idle followup(普通新 turn,带 settlement 摘要)

announced === true 的子(调用方曾拿到过 messageId)无条件 通知,不要求子调过 report

continuable 模式下,父 driver 几乎不等 子跑完;父子通过 Manager + inbox + settlement 消息 异步协作,而不是共享一个 kick 循环。子 agent 自身的 turn/step 机制与根 agent 相同。


3.8 能力 seam 周边与表现(入口/UI)

能力 seam 周边 :FS、Shell、SubAgent、持久化等按 Capability seam 三角色组织(Shell 详例见上文)。它们在 Agent 创建前 mount 到 ctx;Consumer 在根或 agent.ctxtools.register,决定 哪些 agent 看见哪些工具

入口与表现 :CLI / Web BFF boot 插件树 ;Web Client、SDK、ACP 不嵌入 agent-loop --- 驱动 ctx.agents、订阅 session/event 渲染 transcript,订阅 agent/status / inbox 事件更新 UI。持久化插件同样 listen session/event,SessionStore 本身不打开文件。

Agent 开始处理用户输入前,通常应满足:

sql 复制代码
□ cordis.yml 核心包 + 能力 seam Consumer 已 ACTIVE
□ Session 已 prepare/enter/announce(或 resume 加载)
□ Agent 已 enter registry,session-start 已处理
□ 用户 followup → Inbox splice 持久化

第四部分 · Agent 执行机制与 Context 工程(贯穿例子)

Session 账本与 Agent 绑定就绪后,本节从 followup 触发 driver 起,讲清 Phase / Inbox 如何协调调度kick / turn 如何循环 ,以及 每 step 的 Context 如何组装 ------全部沿例子:followup("请阅读 README.md 并用一句话总结") → 模型调 Read → 文字总结


4.1 总览:一条 followup 穿过哪些层

css 复制代码
flowchart TB
 subgraph input["输入层"]
 FU["followup / steer / inject"]
 IB["Inbox(next-turn / next-step)"]
 FU --> IB
 end

 subgraph driver["Driver 层(内存)"]
 WD["wakeDriver"]
 K["kick: while(turn())"]
 PH["Phase: idle ↔ running"]
 WD --> K --> PH
 end

 subgraph turnloop["Turn / Step 层"]
 PS["preStep: claim + assemble + pre-step"]
 ST["step: derive + buildRequest + stream + tools"]
 PS --> ST
 end

 subgraph ground["Ground truth(Session log)"]
 SPL["agent/inbox/spliced"]
 TR["turn/* step/* user/message ..."]
 IB --> SPL
 PS --> TR
 ST --> TR
 end

 IB --> WD
 K --> turnloop
arduino 复制代码
followup
 → Inbox.splice('next-turn') + agent/inbox/spliced(持久排队)
 → wakeDriver(仅 idle 时新开 kick)
 → kick: while(turn())
 turn: preStep → step/start + user/message → step() → step/end
 → turn/end → idle(或 inbox 仍有活 → 同一 kick 开下一 turn)

4.2 Phase:driver 内存状态机

Phase 不是 Session------二者分工如下:

Phase Session
是什么 ReactLoopAgent内存状态机 append-only 事件账本
会不会落盘 (crash 丢失) (persistence)
典型数据 idle / running / maintenanceturnstepAbortController turn/startuser/messagetool/result...
给谁用 协调「是否在跑、第几轮第几步、能否 cancel」 重建 history、UI transcript、resume

Phase 三种形态(agent.ts):

css 复制代码
type Phase =
 | { kind: 'idle'; lastTurn: number }
 | { kind: 'maintenance'; abort; lastTurn; wakeRequested }
 | { kind: 'running'; abort; turn; step; wakeRequested }
Phase 对外 agent/status 含义
idle idle 无 driver 在跑;可 wakeDriver 开新 kick
running running kick → turn → step 循环占用;持有 abort 供 cancel
maintenance idle(外观) runMaintenance 独占 driver;inbox 可排队,结束后再 wake

resume 后 :Phase 从 { idle, lastTurn } 起步(lastTurn 从 log 里最后一个 turn/start 推断);不是恢复 crash 前的 running。

setPhase 在状态变化时 emit agent/status,UI 据此显示「思考中 / 空闲」。

4.2.1 maintenance 与 wakeRequested latch

wakeRequested 是 running / maintenance Phase 上的 「待会再开 driver」 标志,不是 Inbox 队列的一部分。

wakeDriver()phase !== idle 时通常直接 return,但有两种情况会 只 latch、不重复开 kick

场景 wakeDriver 行为
running + 普通 followup/steer return;不设 latch --- 当前 kick 会在 turn 边界自己 claim
maintenance 期间 又来带 wake 的消息 phase.wakeRequested = true
cancel 尚未收敛到 idle 时又来带 wake 的消息(wakeAfterAbort phase.wakeRequested = true
dispose teardownreason.kind === 'disposed' 不 latch --- 避免 dispose 期间再开 turn

maintenancerunMaintenance):

scss 复制代码
仅 idle 可进入 → setPhase(maintenance)(对外 status 仍显示 idle)
 → 跑独占 job(signal 可 abort)
 → finally: setPhase(idle)
 → 若 maintenance.wakeRequested && inbox.hasPending → wakeDriver()

maintenance 期间用户仍可 followup (消息进 Inbox 并持久化),但不会并行跑 turn;job 结束后若 latch 了 wake,再 一次性 开 kick。

kick 结束时补偿finally):

scss 复制代码
setPhase({ idle, lastTurn: turn })
if (wakeRequested && inbox.hasPending) wakeDriver() // cancel / maintenance 后的延迟唤醒

连开多 turn 时turn() 在 inbox 仍有活时会 换新 AbortControllerwakeRequested = false (旧 latch 作废),由 同一 kick 继续下一 turn,不经 idle。


4.3 Inbox:持久排队与 claim 规则

Inbox(packages/core/agent/src/inbox.ts)是 Agent 上 唯一的 FIFO 输入队列 ,但分 两条车道

队列 写入 API 何时 claim
next-turn followup() 新 turn 的第一个 preStep('next-turn')1 条
next-step steer()inject();工具 additionalContexts 每个 step 的 preStep('next-step')全部

4.3.1 splice:先落 Session,再改内存

每次改队列都走 inbox.splice

markdown 复制代码
1. append agent/inbox/spliced(target, deleteCount, insertMessages)
2. 更新内存 next-turn / next-step
3. emit agent/inbox/inserted | discarded | claimed

构造 Inbox 时session.events 重放所有 agent/inbox/spliced------resume 后排队不丢。

4.3.2 claim:step 边界领走输入

arduino 复制代码
claim(target, turn) {
 const claimed = 清空并取出全部 next-step
 if (target === 'next-turn') claimed.push(取出 next-turn 的 1 条)
 for (m of claimed) notifications.claimed(m, turn)
 return claimed
}

同 turn 内 :第一个 step 用 target='next-turn'(可领到 followup);后续 step 用 target='next-step'不会 再碰 next-turn 里排队的下一条 followup)。

4.3.3 followup / steer / inject 对比

API 队列 是否 wake running 时何时处理
followup next-turn 下一 turn 的第一个 preStep
steer next-step 本 turn 下一 step 的 preStep
inject next-step 同上,但不 kick idle driver

4.3.4 running 时又来 followup:只入队,不新 kick

scss 复制代码
wakeDriver() {
 if (phase.kind !== 'idle') return // running:Live drivers claim queued work themselves
 setPhase(running); withInitiator(this, () => kick())
}

时间线(agent 正在 step1 跑工具时用户又 followup):

arduino 复制代码
T1 followup → next-turn += 消息;wakeDriver → 非 idle → return(不新 kick)
T2 当前 step 结束 → step/end
T3 同 turn 若还有 next-step → step2(仍不碰 next-turn)
T4 turn/end
T5 inbox.hasPending → turn() return true → 同一 kick 开 turn+1(不经 idle)
T6 preStep('next-turn') → claim 到 T1 的消息 → user/message → 模型

同一 kick 可连开多 turn (inbox 不空就不回 idle);只有 turn() 返回 false、kick 结束,才变 idle------此时新 followup 才需要再次 wakeDriver

4.3.5 cancel:对 Phase、Inbox 与 turn 收尾

scss 复制代码
cancel(cause, options = {}) {
 if (!options.keepInbox) {
 inbox.clear() // 默认:清空两队列
 if (phase.kind !== 'idle') phase.wakeRequested = false // 取消待唤醒 latch
 }
 if (phase.kind !== 'idle') phase.abort.abort(cause)
}
选项 / 效果 行为
默认 (无 keepInbox inbox.clear() → 所有排队消息 discarded (写 agent/inbox/spliced + emit discarded);不会再进模型
keepInbox: true 队列保留;用于如 interrupt_agent「停当前轮、保留排队」
phase.abort.abort(cause) running 中 turn/step 在下一处 signal.throwIfAborted() 停止
Session log 已 append 的 turn/step/message 不回滚 ;当前 turn 以 turn/end { reason: aborted } 收尾

cancel 时间线(running 中用户点停止)

scss 复制代码
T0 cancel(cause)
 → inbox 清空(默认)或保留(keepInbox)
 → abort.signal 触发
T1 当前 stream / tool execute / preStep 检测到 aborted → 抛错
T2 turn() catch → turnEnds = { kind: 'aborted', reason: cause }
T3 append turn/end(aborted)
T4 kick catch 吞掉 rejection(错误已通过 agent/error 等上报)
T5 kick finally → setPhase(idle)
 → 默认无 wakeRequested、inbox 空 → 结束

cancel 尚未 idle 时又发 followup/steersend 里的 wakingAfterAbort):

c 复制代码
const wakingAfterAbort = wakeup && phase.kind !== 'idle' && phase.abort.signal.aborted
const resolvedTarget = wakingAfterAbort ? 'next-turn' : target // 强制进下一轮,不进已死的 step
inbox.splice(resolvedTarget, ...)
if (wakeup) wakeDriver(wakingAfterAbort) // latch wakeRequested,等 kick 进 idle 后再开

要点:已 abort 的 turn 不会再 claim 新消息 ;新任务必须等 driver 边界收敛到 idle(或 latch 后在 kick.finally 补偿唤醒)后,从 新 turn 开始。


4.4 wakeDriver 与 kick

Agent 的 driver (驱动器)不是单个函数,而是 两层分工wakeDriver 负责「要不要开工、Phase 怎么切」;kick 负责「开工后连续干多少 turn、何时收工」。用户 followup 从不直接调 turn() ,路径固定为:

scss 复制代码
followup / steer(wake)
 → wakeDriver() // 同步:守门 + 可能 setPhase(running)
 → kick() // 异步:while (turn()) 直到 inbox 空
 → turn() → step() ...

4.4.1 概念对照

wakeDriver kick
是什么 启动器(synchronous gate) 工作循环(一次 activity 的 async 主入口)
谁调用 followup / steer(带 wake)、kick.finally(补偿唤醒)、runMaintenance.finally wakeDriver(idle 时)
粒度 一次「唤醒决策」 一次唤醒到 idle 的整段会话
Phase idle → running (或 latch wakeRequested 持有 running,结束时 → idle
是否 await (fire-and-forget 安排 kick) (内部 await turn() 循环)
是否领 Inbox (领消息在 preStepclaim
是否调 LLM (调模型在 step

与相邻层级一起记(由外到内):

arduino 复制代码
wakeDriver → kick → turn → step
 一次唤醒 一次 activity 一轮对话 一次模型请求+工具

4.4.2 wakeDriver:守门与开工

作用 :在 idle (或 maintenance 结束)时,把 Agent 从「可接收输入」切到 running ,并 异步启动一次 kick ;在 已有 activity 时决定是 忽略重复唤醒 还是 latch 待会再跑

scss 复制代码
private wakeDriver(wakeAfterAbort = false): void {
 if (phase.kind !== 'idle') {
 // running:当前 kick 自己会 claim,直接 return
 // maintenance / wakeAfterAbort:只设 wakeRequested,不叠第二个 kick
 if (...) phase.wakeRequested = true
 return
 }
 activityDone = new Promise(...) // 绑定本次 kick 生命周期
 setPhase({ kind: 'running', abort, turn: lastTurn, step: 0, ... })
 withInitiator(this, () => kick()).then(resolve activityDone)
}

wakeDriver 负责的事

  • 检查 Phase:同一时刻最多一个 kick(running 时不重复开)
  • 创建本轮 AbortController (供 cancel 使用)
  • 设置 activityDone --- whenIdle() / 父 subagent await child.whenIdle() 等的是 kick 整段结束
  • withInitiator(this, ...) 内启动 kick --- 工具/subagent 可通过 requireInitiator() 知道 谁发起的这轮 driver

wakeDriver 不负责的事 :不 claim inbox、不 append Session、不 assemble prompt、不调模型------这些都在 turn / step / preStep 里。

4.4.3 kick:一次 activity 的主循环

作用 :从 running 开始,循环 turn() 直到 inbox 没有待办(turn() 返回 false),然后在 finally 统一回到 idle------无论正常结束、LLM 报错还是 cancel。

csharp 复制代码
private async kick() {
 try { while (await this.turn()) {} }
 catch { /* turn/step 错误已在边界上报;此处 containment */ }
 finally {
 if (phase.kind === 'running') {
 setPhase({ idle, lastTurn: turn })
 if (wakeRequested && inbox.hasPending) wakeDriver() // 补偿唤醒
 }
 }
}

kick 负责的三件事

  1. 调度 turn --- turn() 返回 true(inbox 仍有 followup 等)→ 同一 kick 内 开下一 turn,不必先回 idle 再 wake
  2. 错误边界 --- turn / step 抛错或 abort 在 catch 收敛,避免未处理的 rejection;turn/end 仍带 aborted / error reason
  3. idle 收尾 --- finally唯一 把 Phase 从 running 切回 idle 并 emit agent/status: idle;必要时触发 补偿 wakeDriver

kick 不负责的事 :不解析用户消息内容、不决定 tool schema------只 驱动 turn 循环 直到队列排空。

4.4.4 协作流程图

rust 复制代码
flowchart TD
 START["followup / steer(wake)"] --> SPLICE["Inbox.splice + inbox/spliced"]
 SPLICE --> WD{"wakeDriver<br/>(启动器)"}
 WD -->|"phase !== idle"| LATCH["maintenance/aborted: wakeRequested latch<br/>running: 直接 return"]
 WD -->|"phase === idle"| RUN["setPhase(running)<br/>activityDone = kick Promise<br/>withInitiator → kick()"]
 RUN --> KICK["kick(工作循环)"]
 KICK --> LOOP{"while await turn()"}
 LOOP -->|"true: inbox 仍有活"| LOOP
 LOOP -->|"false"| FIN["finally: setPhase(idle)<br/>wakeRequested && hasPending → wakeDriver again"]

4.4.5 生命周期简图

arduino 复制代码
idle
 │ followup + wakeDriver(同步 setPhase running,异步 kick)
 ▼
running ── kick 开始 ─────────────────────────────┐
 │ turn 1 → turn 2 → ... → turn N │
 │ (turn() false 时退出 while) │
 ▼ │
idle ◄── kick finally(emit status idle)─────────┘
 │
 └─ wakeRequested && hasPending → wakeDriver → 又一次 kick

与 maintenance / cancel 的衔接 :maintenance 期间 wakeDriver 只 latch;cancel 默认清 inbox 并 abort 当前 kick,由 kick.finally 收工到 idle;abort 期间新来的 followup 走 wakingAfterAbort latch,idle 后再补偿 kick。


4.5 turn:轮次内的 step 循环

rust 复制代码
flowchart TD
 TS["append turn/start; turn++"] --> INIT["target = next-turn"]
 INIT --> PS["preStep(target)"]
 PS --> REJ{"reject?"}
 REJ -->|是| TE["turn/end blocked"]
 REJ -->|否| EMPTY{"step0 且 messages 空?"}
 EMPTY -->|是| TE2["turn/end completed(无 model call)"]
 EMPTY -->|否| SS["append step/start"]
 SS --> UM["append user/message × N"]
 UM --> ST["step(assembly)"]
 ST --> SE["append step/end"]
 SE --> ENDCHK{"turnEnds && nextStep 空?"}
 ENDCHK -->|是| SERIAL["agent/turn-stopping"]
 ENDCHK -->|否| MORE{"turnEnds && nextStep 空?"}
 MORE -->|是| BREAK["break 内层 loop"]
 MORE -->|否| NS["target = next-step → 下一 step"]
 NS --> PS
 BREAK --> TEND["append turn/end"]
 TEND --> PEND{"inbox.hasPending?"}
 PEND -->|是| NEWAB["新 AbortController; step=0; return true"]
 PEND -->|否| RET["return false → kick 结束"]

turn 与 step 语义

  • Step = 一次模型请求 + 它触发的工具执行(可能 0 个 tool-call)。
  • Turn = 零个或多个 step;在领取首条输入前打开,在不再欠工作时关闭。

例子 turn1

step target claim 到 模型行为
1 next-turn 用户「请阅读 README...」 Read
2 next-step 常为空 读 tool result,文字总结

step1 返回 null(还有 tool 后续工作)→ turn 继续;step2 返回 { completed }nextStep 空 → turn/end


4.6 Context 工程:每 step 三块如何拼

设计点(2) 在执行层的体现:进 LLM 的三块都必须能从 Session 重建 ;且 每个 step 重新 assemble,不是启动时拼一次。

4.6.1 三块与来源

写入 request 主要来源
system request.system renderPrompt(assembly) --- sections + variables
tools request.tools assembly.tools --- 本 agent scope 可见 schema
messages request.messages session.deriveMessages() --- surface 投影
arduino 复制代码
const system = renderPrompt(assembly)
const msgs = this.session.deriveMessages() // user/message append 之后
await buildRequest(turn, step, assembly.tools, system, msgs, signal)

4.6.2 preStep 组装流水线(Context 核心)

每个 step 在 preStep 按固定顺序发生:

scss 复制代码
① inbox.claim(target) → 本 step enter 批次(用户话 / steer / 工具 additionalContexts)
② systemPrompt.assemble(agent) → sections + contexts + tools + variables
③ runtimeContext.project(...) → 动态快照(可选)→ 额外 UserMessage
④ agent/pre-step waterfall → 准入 reject | enter(可改写 messages)
⑤ step/start + user/message append(decision.messages)
⑥ step() → derive + buildRequest → LLM
kotlin 复制代码
 const claimed = this.inbox.claim(target, position.turn)
 const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
 const sections = renderContextSections(assembly)
 const context = this.runtimeContext.project(joinContextSections(sections), sections)
 const decision = await this.dispatch.waterfall('agent/pre-step', { messages: claimed, ... },
 () => ({ kind: 'enter', messages: context ? [...claimed, context] : claimed }))

例子 step1 enter 批次 (可能):[用户句, runtime 工作区快照?, time-context 时间块?]

4.6.3 systemPrompt.assemble 合并什么

ctx.systemPrompt.assemble({ agent, signal })PromptAssembly

字段 内容
sections persona、部署说明、工具 guidance...(scope shadowing)
contexts 命名上下文块(可进 runtime 快照)
tools 当前 step 可见 tool schemas(toolOrder 排序)
variables {{cwd}}{{model}} 等插值

最后走 system-prompt/assemble waterfall,插件可改 assembly。

工具 schema 五层漏斗 (Harness 不替模型选工具,只决定 schema 列表):

c 复制代码
Profile 挂载哪些 tool 插件
 → register 在根 ctx 还是 agent.ctx
 → tools.view(scope) + restrict
 → wireSchemas / mode(native | code | both)
 → toolOrder + assemble waterfall

4.6.4 动态注入两条路径

路径 机制 进 history 方式
A:runtime 快照 assembly.contextsRuntimeContextProjection.project 内容变化才生成 user/messagesource: plugin/system-prompt
B:pre-step 插件 agent/pre-stepenter.messages time-context 每 step 追加时间

compactioncompaction-basic @ pre-step):token 超阈值 → surface replacederiveMessages() 自然变短;loop 无特殊分支。

4.6.5 buildRequest:请求锚点

buildRequest 意图链:

  1. seedConfig --- 首次用 AgentOptions;之后 requestProposal(session.requestHeader())
  2. agent/request waterfall --- 改 provider/model/采样(不能改 messages
  3. llm.prepareCall --- 适配器、stream、retry
  4. append request/headerrequest/context(epoch 变化时)
  5. deepFreeze(GenerateOptions) --- 交给 llm.stream

request/header 与 messages 分离:改路由不必重写 history;compaction 设计也尽量保留 prefix 以利于 provider cache。

4.6.6 Context 扩展点地图

想改什么 扩展点 影响
系统提示词段落 ctx.systemPrompt.section() request.system
工具列表 tool provider + scope request.tools
每 step 动态文本 contexts 或 pre-step user/message → messages
拦截本 step 输入 agent/pre-step enter 批次
改模型路由 agent/request config
压缩 history compaction @ pre-step deriveMessages()
工作区 AGENTS.md agent-instructions baseline + fs 触发

4.7 step 内:stream、tool-call 与工具流水线

4.6preStep 拼好 Context 并 append user/messagestep(assembly) 负责 一次模型往返 + 该次返回的全部 tool-call 执行 。工具能力在 ctx.tools(ToolRuntime) ,不在 loop 里硬编码------loop 只做 调度、写 Session、接 inbox

4.7.1 step() 在 turn 中的位置

bash 复制代码
preStep → step/start → user/message append
 → step(assembly)
 ├─ buildRequest + llm.stream(可能 retry 循环)
 ├─ assistant/chunk* → assistant/message
 ├─ 无 tool-call → 返回 { completed }
 └─ executeToolCalls → tool/call + tool/result → 返回 null | { completed }
 → step/end

step() 内部有 while (true) 包裹 LLM 调用:同一步内若 agent/request-error 决定 retry ,会 不 append 新 assistant/message 直接再 stream 一次。

4.7.2 LLM stream:chunk 与 assistant/message

arduino 复制代码
 for await (const chunk of stream) {
 this.session.append('assistant/chunk', { turn, step, chunk })
 assembler.push(chunk)
 }
 this.session.append('assistant/message', { message, ... },
 { surfaceOp: 'append', sourceEventSeqs: chunkSeqs })
事件 surface 作用
assistant/chunk 通常 流式 UI、crash 中间态 replay;sourceEventSeqs 链到最终 message
assistant/message append canonical 助手回复;含 text / reasoning / tool-call blocks

stream 结束后:

  • max-tokens → step 返回 { kind: 'max-tokens' }(turn 结束 reason 可能 sticky)
  • 无 tool-call block{ kind: 'completed' } → turn 可能在 step 边界结束
  • 有 tool-call → 进入 executeToolCalls

4.7.3 step 返回值与 turn 是否续步

javascript 复制代码
 const toolCalls = message.content.filter(block => block.type === 'tool-call')
 if (toolCalls.length === 0) return { kind: 'completed' }
 const { concluded } = await executeToolCalls(...)
 return concluded ? { kind: 'completed' } : null
step() 返回 含义 turn 外层
{ completed } 无 tool-call,或工具声明 concludesTurn nextStep 空 → 可 turn/end
null 有 tool-call 且 turn 被工具结束 同一 turn step+1(常无新 user/message)
{ max-tokens } 输出截断 turn 可能结束

4.7.4 两条线:Loop 调度 vs ToolRuntime 流水线

Harness 故意把 多 call 调度单 call 策略/执行 拆开:

vbscript 复制代码
┌─ agent-loop(tool-calls.ts)────────────────────────────┐
│ 模型 tool-call 顺序、并行池、barrier │
│ Session:tool/call、tool/result append │
│ additionalContexts → inbox next-step │
└───────────────────────────┬────────────────────────────┘
 │ 每个 call
 ▼
┌─ dsh-tools(ToolRuntime)───────────────────────────────┐
│ prepare → dispatch(execute) → post-execute → finalize │
│ tools/pre-execute、guard、approval、工具定义回调 │
└─────────────────────────────────────────────────────────┘
路径 写 Session turn/step? 走 ToolRuntime 流水线?
executeToolCalls(Agent loop) --- tool/call + tool/result
手动 ctx.tools.execute 否(除非插件自 append) --- 无 turn 边界

设计点(1)的体现:Read / bash / Task 等是 Consumer 注册的工具;loop 只调用 ctx.tools 的调度接口,不 spawn、不直接读文件。

4.7.5 executeToolCalls:规划、分组与并行

文件:packages/core/agent-loop/src/tool-calls.ts

对每个 tool-call block 规划 PlannedCall

scss 复制代码
callId ← block.id(模型权威 id;tool/result 必须对齐)
name ← block.name
arguments← JSON.parse;失败保留 raw
agent ← ctx.agents.requireInitiator()(须在 kick 的 withInitiator 内)
signal ← 与 step 共享的 AbortSignal

分组调度

csharp 复制代码
while 还有未处理 call:
 mode = executionMode(第一个未处理) // exclusive | parallel
 group = parallel ? 连续 parallel-safe 段(受 barrier 截断): [当前一个]
 await runGroup(group, mode)
模式 行为
exclusive(默认) 一次一个 call;形成 barrier
parallel 仅当工具 isConcurrencySafe(args) === true;有界池(maxParallelToolCalls

并行池规则:工具 body 可重叠执行 ,但 tool/result 写入 Session 的顺序 = 模型 tool-call 顺序;池中若下一个变 exclusive → 先 drain 再开新 barrier。

4.7.6 ToolRuntime 单 call 生命周期

Loop 通过 TOOL_RUNTIME_SCHEDULER 驱动,不直接散落调用 tools.execute

sql 复制代码
1. appendToolCall
 session.append('tool/call', { turn, step, callId, name, arguments })
 → log-only,不进 surface(模型已在 assistant/message 见过 call)

2. prepare(exec)
 解析/快照 arguments
 tools/pre-execute waterfall → allow | deny | ask
 guard(单调,pre-execute 之后)
 → dispatch | post-result | final-result(参数错、deny、abort 等可跳过 body)

3. dispatch(exec) → ToolDefinition.execute(args, exec)

4. finalize / finish → post-execute、finalizeContent

5. appendToolResult
 session.append('tool/result', { message }, { surfaceOp: 'append', sourceEventSeqs: [callSeq] })

Cordis 事件(实时,不写 Session)

事件 mode 典型用途
tools/pre-execute waterfall 审批、策略 deny/ask
tools/post-execute waterfall 改 result content、附加 meta
tools/result emit UI 监听完成

deny / 无 approval 的 ask :仍写 synthetic tool/result(模型可见错误文本),保证 surface 配对完整。

code 模式 :模型直接调非 run_code 的工具 → prepare 阶段即拒绝(collapse 规则)。

4.7.7 Session 与 surface:为何 tool/call 与 tool/result 分开

事件 surface deriveMessages
assistant/message(含 tool_calls) ✅ assistant
tool/call ❌ --- 审计、UI pending、sourceEventSeqs 关联
tool/result ✅ tool-result(callId 对齐

下一轮 deriveMessages() 自然序列:

sql 复制代码
... user → assistant(含 tool_calls)→ tool-result → ...

callId 必须来自模型 block.id;post-execute 可改 content,不可改 id。

4.7.8 additionalContexts:工具结果之外的下一步输入

工具或 tools/post-execute 可返回 additionalContexts: UserMessage[]

Loop 在 该 call 的 tool/result 已 commit 之后,按模型顺序:

ini 复制代码
context => inbox.splice('next-step', inbox.nextStep.length, 0, [context])

同一 step 内所有 tool/result 写完后 ,这些 context 才进 inbox;下一 step 的 preStep → claim('next-step') 把它们当作 enter 批次 append 为 user/message 。设计意图:tool/result 与注入 context 在 log 里 相邻,不插在两次 tool 结果中间。

4.7.9 cancel 与 skipped tool-call

step 的 signal abort 时:

  1. 已启动的 call:drain → commit 已有 result 或 abort 替换
  2. 未启动 的 call:仍写 tool/call + synthetic tool/result (如 TOOL_ABORTED_BEFORE_DISPATCH),保证 replay 配对平衡
  3. 已 commit 的 additionalContexts 仍可进 next-step

surface 上不会出现「有 call 无 result」的悬空态。

4.7.10 无 Agent 的对比实验(greet demo)

不经过 turn/step 也可测 ToolRuntime 流水线------但没有 Session turn 边界

php 复制代码
// 最小插件:inject tools,register greet,直接 execute
void ctx.tools.execute({
 callId: CallId('demo-1'), name: 'greet',
 arguments: { name: 'Harness' }, signal: abortSignal,
})

可见 tools/pre-executetools/result,但 turn/start 。完整 Agent 路径必须走 executeToolCalls,才会把 tool/call、tool/result 与 turn/step 对齐写入 log。

4.7.11 贯穿例子:step1 调 Read

rust 复制代码
sequenceDiagram
 participant S as step()
 participant L as llm.stream
 participant E as executeToolCalls
 participant T as ToolRuntime(Read)
 participant Log as Session

 S->>L: deriveMessages + buildRequest
 L-->>S: assistant/message + tool-call Read
 S->>Log: assistant/message (surface)
 S->>E: toolCalls[Read]
 E->>Log: tool/call
 E->>T: pre-execute → execute(README path)
 T-->>E: content blocks
 E->>Log: tool/result (surface)
 E-->>S: concluded=false → return null
 Note over S: turn step2:derive 含 tool result,无新 user/message
  • step1 返回 null → turn 内 step2
  • step2 deriveMessages() ≈ user + assistant(tool-call) + tool(result) → 模型文字总结 → { completed }turn/end

工具扩展挂点 (不改 loop):tools/pre-execute(审批 bash)、tools/post-execute(包装 result)、Capability seam 的 Consumer(新工具 register)。


4.8 贯穿例子:事件因果收束

bash 复制代码
followup("请阅读 README...")
 agent/inbox/spliced, agent/status → running
 turn/start(1)
 preStep(next-turn): claim → assemble(persona+tools) → pre-step enter
 step/start(1,1) → user/message
 request/header, request/context
 assistant/chunk* → assistant/message(Read tool-call)
 tool/call → tools.execute → tool/result(README)
 step/end(1,1)
 preStep(next-step): claim 常 ∅
 step/start(1,2) → deriveMessages 含 tool result
 assistant/message(总结)
 step/end(1,2)
 turn/end(1) → agent/status → idle
事件 为何存在
agent/inbox/spliced durable 排队;resume 可重放
user/message + surfaceOp 设计点(2);进 derive
assistant/chunk vs assistant/message 流式 UI vs canonical + surface
tool/call 不进 surface 模型已在 assistant message 见过 call
tool/result + surfaceOp step2 derive 必需
step2 无 user/message 续步靠 surface history,非 bug

调试时可按上表预测下一 event type,再对照 JSONL/SQLite 导出与运行时断点。


第五部分 · 打断、SubAgent、扩展与调试

正常路径是 followup → kick → turn/step → LLM/tools 。本节补 运行中如何改道、如何委派子 agent、插件应挂哪、如何查 log------仍不改 agent-loop 内核,走 documented extension points。


5.1 followup、steer、inject:三种输入语义

三者都经 send(message, target, wakeup) 。从 调用方与用户意图 看:

API 典型调用方 用户语义
followup Web 发送、SDK prompt() 「新一条任务 / 新一轮对话」
steer UI 中途改方向、ContinuationManager 结算通知(idle 父) 「别那样做,下一步按我说的来」
inject 插件、agent-instructions、Manager 结算(父 teardown 中) 「先记着,等下次 step 带上」

Web / SDK 不直接碰 ReactLoopAgent,而是 ctx.agents.get(sessionId)?.followup(...)

5.1.1 running 中 steer 的典型时间线

lua 复制代码
T0 用户 steer「改读 package.json」
 → next-step += 消息;agent/inbox/inserted
 → wakeDriver → running → return(不新 kick)
T1 step1 跑完(可能已按旧意图调了 Read)
T2 preStep('next-step') claim steer 消息
T3 step2 user/message 含 package.json 意图 → 模型改调 Read

steer 不打断当前 stream/tool ------只影响 下一 step 边界 起的 enter 批次。若要硬停当前轮,用 cancel

5.1.2 inject 的典型用途

场景 做法
插件塞动态上下文 agent.inject(userMessage) --- 排队,等已有 driver 跑到 step 边界或等下次 followup wake
插件塞模型可见上下文 agent.inject() → 下一次获准请求的 enter 批次
ContinuationManager 父 teardown inject 结算通知 --- 不唤醒,避免 dispose 前多跑一轮 LLM

idle + inject :消息进 next-step,但 不 wake --- 直到用户再 followup/steer 或外部 wakeDriver 条件满足。

5.1.3 与工具 additionalContexts 的关系

工具 execute 返回的 additionalContexts 也进 next-step ,与 steer/inject 同一 claim 队列 ------下一 step 的 preStep('next-step') 一次性领走(steer 文本 + 工具注入 context 可同批 enter)。


5.2 cancel:打断当前 activity

cancel 作用于 Phase.abort,不是 Inbox 的「删历史」:

scss 复制代码
cancel(cause, { keepInbox?: boolean })
选项 Inbox Phase 典型场景
默认 clear() 清空排队 abort.abort(cause) 用户点停止;UI 取消当前轮
keepInbox: true 保留 abort interrupt_agent:停 turn,保留已排队 followup

效果链abort → stream/tool/preStep 处 throwIfAborted → turn/end { reason: aborted } → kick finally → idle

cancel 后又发 followupwakingAfterAbort 强制 next-turn + latch wakeRequested,等 idle 后新 kick --- 不会把消息并进已死的 step。

与 steer 对比

steer cancel + followup
当前 step 跑完 中断
新意图何时进模型 下一 step 边界 新 turn(abort 收敛后)
排队消息 保留(除非 clear) 默认 clear

5.3 SubAgent:模型委派与三条热路径

SubAgent 是 Capability seamctx.subagents Definition + spawn/fork Provider + dsh-tool-subagent Consumer(模型见 Task / subagent 工具)。

父 Session log 形态不变 ------仍是普通 tool/call + tool/result;差异在 工具 execute 内部是否阻塞父 driver

5.3.1 从父 step 到子 agent

rust 复制代码
sequenceDiagram
 participant P as 父 step
 participant T as tool-subagent
 participant SA as ctx.subagents
 participant C as 子 Agent driver

 P->>P: assistant/message 含 tool-call Task
 P->>T: executeToolCalls → execute
 T->>SA: start | startContinuable | jobs.start
 SA->>C: agents.create + followup(prompt)
 alt 前台 one-shot
 C->>C: kick → turn → ...
 C-->>T: whenIdle → output
 T-->>P: tool/result(父 step 继续)
 else continuable
 T-->>P: 立刻返回 childId(父几乎不等)
 C->>C: 自主多 turn
 end

5.3.2 三条热路径(tool-subagent 路由)

路径 条件 API 父 step 是否阻塞 工具返回
A 前台 one-shot run_in_background: false subagents.startawait whenIdle() 子 agent 最终输出
B 后台 one-shot one-shot + run_in_background: true jobs.start → 异步 subagents.start jobId
C continuable backgroundMode: continuable(默认后台) startContinuable (inbox 接受即返回) childId + messageId

spawn vs fork(Provider 差异,不是 tool 分支):

Provider 子 Session context 典型用途
spawn 新 log,policy 定 seed 并行专家、fresh child
fork 继承父 已完成 turn 前缀 分支探索、checkpoint 实验

5.3.3 continuable 与 ContinuationManager

路径 C 的后续 followup(parent, childId, msg) 、冷恢复、ownedChildren 结算与 notifySettlementSubagentContinuationManager 协调。工具侧结论:父 Session log 仍是普通 tool/call + tool/result;父 driver 不共享 子 loop。

5.3.4 concludesTurn:工具即最终答案

工具 execute 内可调 exec.concludeTurn() → result 带 concludesTurn: truestep() 返回 { completed } 即使刚跑完 tool --- turn 不再开下一步

用于:goal 汇报、subagent 前台 one-shot 返回、某些「工具输出就是给用户看的终态」场景。与 路径 A 阻塞等待 配合:父 step 结束,turn 可关。


5.4 扩展点:三个事件域

Harness 扩展的 第一个决定 :改的是 持久 Session进行中 Agent ,还是 某能力 seam

持久? 典型前缀 何时用
Session 事件 --- append 进 log user/messagecompaction/*、插件扩展 SessionEventMap 事实必须在 reload 后仍在
Agent 事件 --- 实时 agent/pre-stepagent/requestagent/status 观察/拦截 当前 driver、inbox、请求
能力事件 视插件 tools/*fs/*llm/stream 策略挂在 seam 上,避免 import 循环

Waterfall 须 next() (设计点 4):agent/pre-stepagent/requesttools/pre-executetools/post-execute 等。 Serial 无 nextagent/turn-stopping --- turn 即将结束前的串行钩子。

Turn/step 在 Session log 中的事件顺序见第四部分 turn/step 流程与贯穿例子。

5.4.1 常见扩展:改什么挂哪

目标 机制 影响面
系统提示词 / persona ctx.systemPrompt.section() request.system
工具 schema 列表 tool provider + preset / agent.ctx register request.tools
本 step 是否进模型 agent/pre-step enter 批次 reject/改写
模型路由 / 采样 agent/request config(不能改 messages
工具审批 / deny tools/pre-execute 是否 dispatch body
包装 tool result tools/post-execute result content / meta
压缩 history compaction @ pre-step surface → derive 变短
新持久事实 扩展 SessionEventMap + append 全链路 replay
模型可见动态上下文 inject 或 pre-step / runtime 快照 user/message
换 bash/FS 实现 换 Capability Provider 不改 Consumer 工具名
用户斜杠命令 ctx.commands model turn
UI 渲染 订阅 session/eventagent/*;工具 presentCall/presentResult 表现层

不要改 loop:新行为应落在上表某一格。

5.4.2 Agent 事件速查(调试/UI 常订阅)

事件 何时
agent/status idle ↔ running
agent/inbox/inserted · discarded · claimed Inbox 变更
agent/error step/turn 边界失败
agent/session-start create/resume 发布后
agent/turn-stopping turn 将结束前
subagent/start · subagent/end 父 scope 内观测子 run(非 Session)

5.5 调试:从插件树到 Session log

5.5.1 配置与 spine 是否就绪

css 复制代码
pnpm dsh --profile web --dump-config # 本机有效 cordis.yml(patch 叠加后)
pnpm dsh --profile headless --dump-config

对照检查:是否有 sessiontoolsllm 适配器、agent-loopagent;Consumer 工具(dsh-tool-fs 等)是否 mount。

5.5.2 运行时断点

断点位置 观察什么
agent.ts preStep claim 批次、assemble、pre-step 决策
agent.ts stepderiveMessages 前后 surface 投影是否含刚 append 的 user/tool result
agent.ts buildRequest request/header、frozen GenerateOptions
tool-calls.ts executeToolCalls 并行组、tool/call·result 顺序
inbox.ts splice / claim next-turn vs next-step 消费

读 log 方法 :按贯穿例子的事件因果表预测下一 type,再对照 JSONL/SQLite 导出;UI transcript 读 append-origin,模型 history 看 surface/derive。

5.5.3 持久化与 resume

Backend 典型路径 用途
JSONL profile 配置 dsh-session-persistence-jsonl 人类可读逐行事件
SQLite dsh-session-persistence-sqlite 结构化查询、chunk 打包

ctx.sessions.flush(session) --- checkpoint 完成后再依赖磁盘。resume:新 driver + 旧 log,Inbox 从 agent/inbox/spliced 重放。

5.5.4 无 API Key 的分层实验

层级 做法 能验证什么
仅 ToolRuntime greet demo --- ctx.tools.execute pre-execute、execute、post-execute
完整 Agent pnpm dsh --profile headless "..."(需 DEEPSEEK_API_KEY turn/step、Session 全链
keyless snapshot 仓库 pnpm run test:snapshot 组装应用 transcript replay

5.5.5 常见问题定位

现象 先查
插件永远 PENDING --dump-config 缺 inject 提供方;Fiber 缺 dsh-tools / llm
followup queued 不处理 Phase 是否 running;是否等 下一 turn claim
steer 没生效 是否等到 下一 step;当前 step 是否已 commit assistant
tool result 不进下一步 tool/result 是否 surfaceOp;step2 是否 derive 到
子 agent 父一直 running 是否前台 one-shot 在 await whenIdle()(路径 A)
history 突然变短 compaction replace;log 全量仍在

结语

Harness 的 mental model 可概括为一条主轴:

arduino 复制代码
Profile 拼 Cordis 插件树
  → 核心包 provide 服务
  → agent-loop:Inbox → turn/step → append Session → derive + assemble → LLM + tools
  → 能力 seam 与 UI 订阅同一 log

架构 回答「组件在哪一层、为何这样拆」;执行机制 回答「一句 followup 如何穿过 agent.ts」;Session 事件回答「运行时 ground truth 是什么」。三者对齐,即掌握 Harness 的主干。


参考与延伸阅读

官方文档

文档 内容
docs/architecture.zh.md 架构总览:Cordis、Profile/组合包、核心包、三域事件、轮次流程、会话日志、能力 seam
docs/cordis-primer.zh.md Cordis 插件模型、Fiber、inject、waterfall
docs/subsystems/shell.zh.md bash 执行 capability seam 规范范例
docs/agent-lifecycle.md Agent 生命周期时序
docs/tool-execution-pipeline.md 工具执行流水线
docs/event-producer-consumer.md 事件生产方与消费方映射

源码锚点

路径 内容
vendor/cordis/src/ Cordis 核心(Context、Fiber、Reflect、Events)
vendor/loader/src/config/entry.ts Loader 单条 Entry 加载路径
packages/core/agent-loop/src/agent.ts ReactLoopAgent:Phase、Inbox、kick/turn/step、preStep
packages/core/agent-loop/src/tool-calls.ts executeToolCalls 并行调度
packages/core/agent/src/inbox.ts Inbox splice / claim
packages/subagent/subagent/src/continuation.ts SubagentContinuationManager
packages/boot/app-boot/ Profile 与 boot 组装

本文结构索引

部分 主题
第一部分 架构总览、五条设计点、Capability seam
第二部分 Cordis 底座:Loader、ctx、Fiber、Events
第三部分 核心包、Session 四视图、surface、多 Agent、ContinuationManager
第四部分 Phase/Inbox、kick/turn/step、Context 工程、工具流水线
第五部分 followup/steer/inject、cancel、SubAgent、扩展点、调试
相关推荐
程序猿DD1 小时前
OctaFuse Gateway 2.7.0:按星期计价、用户级模型折扣、百炼ASR模型支持优化
后端·agent
呆萌很2 小时前
Sigmoid 与 Tanh 激活函数(S 型饱和激活函数)
人工智能·深度学习·机器学习
leeyi2 小时前
MultiAgent Host 源码 + ADK prebuilt 三种预制模式(第92篇-E78)
人工智能·aigc·agent
前沿在线2 小时前
WRC2026丨当机器开始理解人的意图,人机交互走向更多场景
人工智能·ai·大模型
动物园猫2 小时前
红外无人机目标检测数据集:4,500+张图像 | 目标检测
人工智能·目标检测·无人机
达子6662 小时前
AI训练师图解_6.1_让AI理解人类语言_自然语言处理
人工智能·自然语言处理
Linguwen2 小时前
中山GEO分享会回顾文
人工智能·chatgpt
武子康2 小时前
多 Agent 不是多开几个终端:Pi 的 Sub-agent 取舍
人工智能·llm·agent
俊哥V2 小时前
每日 AI 研究简报 · 2026-08-22
人工智能·ai