Deepseek harness的 subAgent机制(chatgpt)

可以。把你之前的聊天记录和我刚刚重新核对的 DeepSeek Harness 2026-08-19 当前 master 源码 合起来看,我觉得之前我们对 ctx.subagents 的理解方向是对的,但还停留在:

SubAgent = Service + 多个 Provider

这个层级。

你之前的记录已经确认了 DSH 可以把 Claude Code、Codex 等放到 ctx.subagents 后面,父 Agent 不需要绑定某个具体实现。

但当前源码已经进一步演化成了:

SubAgent = Provider Registry + Child Session + Activation Manager + Agent Inbox + Control Channel + Report Channel + Lifecycle Events

换句话说,它已经不只是"调用一个子 Agent 的工具",而是一套小型的 Multi-Agent Runtime。 官方目前明确把 ctx.subagents 定义为可同时注册多个 provider 的 named registry,而且同一个 service 还负责 continuable child 的编排。(GitHub)


一、先建立正确心智模型:DSH 的 SubAgent 到底是什么

当前源码结构可以简化成这样:

text 复制代码
                         Parent Agent
                              │
                              │ model tool call
                              ▼
                    dsh-tool-subagent
                    "模型可见的工具层"
                              │
                              ▼
                ┌─────────────────────────┐
                │      ctx.subagents      │
                │    SubagentRuntime      │
                │                         │
                │  1. Provider Registry   │
                │  2. ContinuationManager │
                │  3. Identity / Catalog  │
                │  4. Lifecycle Events    │
                └────────────┬────────────┘
                             │
          ┌──────────────────┼─────────────────────┐
          │                  │                     │
          ▼                  ▼                     ▼

      In-process         External Agent         Full Runtime
      Provider           Provider               Provider

      spawn              ACP                    dsh-sdk
      fork               Claude Code
                         Codex

          │                  │                     │
          ▼                  ▼                     ▼

     Child Agent         Child Process        Child DSH
     Child Session       Own Context          Own Runtime

官方现在明确列了 6 类 provider:

Provider 子 Agent 在哪里跑 父历史 当前 shipped continuation
spawn 同进程 DSH Agent 不继承 支持
fork 同进程 DSH Agent 继承已完成历史 能力实现了,但 shipped one-shot
acp 外部 ACP Agent 不继承 one-shot
claude-code Claude Code + Agent SDK 不继承 one-shot
codex Codex app-server 不继承 one-shot
dsh-sdk 新 DSH Runtime 进程 不继承 one-shot

这里最重要的不是 Provider 数量,而是:

Parent → SubAgent 的语义,与 SubAgent 究竟怎么运行,已经被拆开了。

ctx.subagents 是协议,Provider 是运输方式。(GitHub)


二、现在 DSH 其实有两套完全不同的 SubAgent 语义

这是理解当前源码最关键的一点。

以前我们很容易觉得:

text 复制代码
SubAgent
=
spawn(prompt)
→ 等它跑完
→ 拿 result

当前 DSH 已经明确分成:

text 复制代码
A. One-shot delegation
B. Continuable child conversation

它们甚至不是同一种运行对象


A. One-shot:更像"把一个工作包外包出去"

典型场景:

"你去研究一下 authentication 模块,然后告诉我结论。"

父 Agent 不打算和它长期聊天。

整个调用链是:

text 复制代码
Parent LLM
   │
   │ tool:
   │ subagent({
   │   description: "研究认证模块",
   │   prompt: "...",
   │ })
   ▼

dsh-tool-subagent
   │
   │ 构造 SubagentStartRequest
   ▼

ctx.subagents.start("spawn", request)
   │
   ├─ 找到 provider
   ├─ capability check
   ├─ depth check
   └─ provider.start(...)
          │
          ▼
       创建 child
          │
          ▼
    publish SubagentRun
          │
          ├─ emit subagent/start
          │
          ▼
       child agent loop
          │
          ▼
     SubagentResult
          │
          ├─ output
          ├─ structured?
          └─ stopReason
          │
          ▼
     emit subagent/end
          │
          ▼
       dispose
          │
          ▼
 Parent 获得最终结果

SubagentRun 很值得注意。

它是:

ts 复制代码
interface SubagentRun {
    id
    localAgent?
    result: Promise<SubagentResult>
    dispose()
}

而不是一个长期存在的 Agent handle。

官方甚至明确强调:

one-shot run has no steering and no resume

也就是:

text 复制代码
创建
→ 执行
→ result
→ dispose

结束了就是结束了。(GitHub)


三、dsh-tool-subagentctx.subagents 为什么还要分两层?

这个设计很漂亮。

模型看到的是:

text 复制代码
subagent(...)

但这并不是 SubAgent Runtime 本身。

实际上:

text 复制代码
       Model
         │
         ▼
┌────────────────────┐
│ dsh-tool-subagent  │
│ Consumer / Adapter │
└─────────┬──────────┘
          │
          ▼
┌────────────────────┐
│   ctx.subagents    │
│   Runtime Service  │
└─────────┬──────────┘
          │
          ▼
       Provider

tool-subagent 负责的是:

text 复制代码
模型参数
→ Runtime 参数

例如模型可能产生:

json 复制代码
{
  "description": "检查鉴权代码",
  "prompt": "分析 src/auth 下可能存在的安全问题",
  "run_in_background": true
}

而 tool plugin 自己还有部署配置:

text 复制代码
provider = spawn
maxDepth = 3
persona = ...
toolFilter = ...
backgroundMode = continuable

于是最终组合成一个真正的 SubagentStartRequest。当前源码也明确规定:foreground 始终收集运行结果;background 到底变成 one-shot Job 还是 continuable child,由这个 tool plugin 的 backgroundMode 决定。(GitHub)

这意味着:

模型决定"我要委派",Harness 决定"怎么委派"。

这个分层很值得学。


四、DSH 还做了一个很重要的 Capability Negotiation

假设父 Agent 请求:

text 复制代码
spawn child:

persona = "你是安全专家"
toolFilter = 禁止 Bash
maxDepth = 2
outputSchema = SecurityReport

这几个并不是所有 Provider 都能做到。

所以 Provider 会声明:

ts 复制代码
SubagentCapabilities {
    outputSchema
    depthLimit
    toolFilter
    persona
}

例如本地 spawn/fork 四个都是 true

但 Claude Code/Codex 这种外部 Provider 都是 false。

于是:

text 复制代码
Parent Request
      │
      ▼
ctx.subagents
      │
      ├─ provider supports? ── YES → start
      │
      └─ NO
          ↓
   UNSUPPORTED_CAPABILITY

不是:

text 复制代码
"你要求 toolFilter,
 Claude Code 不支持,
 算了我偷偷忽略。"

官方叫:

fail loud, no silent degradation

这是一个非常好的 Multi-Agent 工程原则。(GitHub)

因为否则你以为:

text 复制代码
这个金融子 Agent 没有 Bash 权限

实际上 provider 根本没执行这个限制,就会产生很危险的权限幻觉。


五、真正有意思的是第二种:Continuable SubAgent

这部分我认为比 one-shot 更值得研究。

例如父 Agent 说:

text 复制代码
你去持续研究 DeepSeek Harness 的 SubAgent。
先研究架构。

Child 做了一轮。

过一会父 Agent 又说:

text 复制代码
再重点看一下 Claude Code Provider。

再过一会:

text 复制代码
刚才结论不对,继续核对 fork。

如果是 one-shot:

text 复制代码
Agent A
  ↓ finish

Agent B
  ↓ finish

Agent C

三个独立 Agent。

而 continuable 是:

text 复制代码
               Child Session #123
                       │
             ┌─────────┼─────────┐
             │         │         │
            Turn1     Turn2     Turn3
             │         │         │
          架构研究   Claude    fork校验

一直是同一个 child。


六、DSH 为此引入了一个非常关键的区分

不是:

text 复制代码
Child Agent = 一个运行中的 Agent 对象

而是:

text 复制代码
        Durable Child Session
                 │
                 │ 可以存在很久
                 ▼
        ┌───────────────────┐
        │   Child #abc123   │
        └─────────┬─────────┘
                  │
                  │ 需要运行时
                  ▼
             Activation
                  │
                  ▼
             AgentHandle
                  │
                  ▼
               Agent

官方定义得非常清楚:

text 复制代码
persisted Session
      ↓
optional live Activation
      ↓
retained AgentHandle
      ↓
Agent inbox

Session 是长期身份。

Activation 是当前驻留在进程里的"活体"。

(GitHub)

这个设计我认为非常漂亮。


七、为什么 Session 和 Activation 一定要拆?

假设:

text 复制代码
09:00
父 Agent 创建研究 Agent R

R Session ID = child-123

R 完成第一轮工作:

text 复制代码
09:05
Activation #1
结束

为了节省资源:

text 复制代码
Agent 对象释放
内存释放

但:

text 复制代码
Session child-123
仍然存在

10:00 父 Agent 又发:

继续研究 memory 部分。

这时候:

text 复制代码
child-123
   │
   │ 当前没有 Activation
   ▼
Cold Resume
   │
   ▼
Activation #2
   │
   ▼
resume child session

于是它继续原来的上下文。

所以它实际上实现了:

Agent Identity ≠ Agent Residency

这个抽象对真正的大规模 Multi-Agent 系统非常关键。

如果一个用户有:

text 复制代码
100 个长期子 Agent

你显然不希望:

text 复制代码
100 个 Agent 对象
永远活在内存里。

而 DSH 是:

text 复制代码
100 Durable Sessions

但可能只有

3 个 Live Activations

需要的时候 cold resume。官方 continuation manager 正是负责 activation admission、cold resume、ownership 和 child-first disposal。(GitHub)


八、startContinuable() 到底发生了什么?

这段源码思想很值得拆开。

父 Agent:

text 复制代码
ctx.subagents.startContinuable(...)

并不是直接:

text 复制代码
provider.spawnAgent()

而是:

text 复制代码
① 先预留稳定 childId

childId = session-abc

然后:

text 复制代码
② 写下 subagent descriptor

provider = spawn
mode = continuable
parent = parent-session
...

然后才问 Provider:

text 复制代码
③ prepareContinuable()

注意这个方法名字非常关键。

不是:

text 复制代码
provider.startContinuable()

而只是:

text 复制代码
provider.prepareContinuable()

之后真正的 Agent:

text 复制代码
④ ContinuationManager
   → ctx.agents.create()

接着:

text 复制代码
⑤ 创建 Activation
⑥ 建 ownership
⑦ 把 initial prompt 放入 Child Inbox

最后:

text 复制代码
⑧ inbox 接受消息后

return {
   childId,
   messageId
}

此时甚至不等 Child 的 turn 开始。

官方明确规定 startContinuable() 在 inbox acceptance 后就返回,不等待模型执行,也不等待该消息进入 Session Log。(GitHub)

这实际上就是:

text 复制代码
Create Durable Worker
+
Asynchronous Message Send

而不是 RPC:

text 复制代码
request
→ wait
→ response

九、这意味着 Continuable SubAgent 更像 Actor

如果让我用经典计算机架构解释:

One-shot 很像:

text 复制代码
RPC

而 Continuable 很像:

text 复制代码
Actor

因为每个 Child 有:

text 复制代码
Identity
+
Mailbox
+
State
+
Lifecycle

特别是 DSH 明确规定:

Agent Inbox 是唯一的消息 FIFO。

没有另造一个:

text 复制代码
SubAgentQueue

于是:

text 复制代码
Parent
   │
   ├── message A
   ├── message B
   └── message C
          │
          ▼

      Agent Inbox

      ┌───────┐
      │   A   │
      ├───────┤
      │   B   │
      ├───────┤
      │   C   │
      └───────┘

Child 自己一个 turn 一个 turn 消费。

这避免了:

text 复制代码
Agent scheduler
+
SubAgent scheduler

两套状态机互相打架。(GitHub)


十、send_message 其实只是往 Child Inbox 塞消息

现在模型侧还有:

text 复制代码
send_message
interrupt_agent
list_agents

例如:

text 复制代码
send_message(
    childId,
    "重点再看看 session 恢复逻辑"
)

背后是:

text 复制代码
tool
 ↓
ctx.subagents.followup()
 ↓
authorization
 ↓
找到 child
 ↓
┌───────────────────────────┐
│ child 当前状态?           │
├───────────────────────────┤
│ running → 同 Activation    │
│ waiting → 唤醒 Activation │
│ cold    → Cold Resume      │
└───────────────────────────┘
 ↓
Agent.followup()
 ↓
Inbox

所以 parent 根本不需要知道:

text 复制代码
child 现在还活着吗?

这是 Runtime 的事情。

(GitHub)


十一、有一个很细但很重要的语义:send_message 不是 steering

假设 Child 正在:

text 复制代码
Turn 5:
分析数据库代码......

这时候 Parent:

text 复制代码
send_message:
"别研究数据库了,改看网络层。"

DSH 不会把消息直接插入正在执行的 Turn 5。

而是:

text 复制代码
当前 Turn 5
     ↓
继续完成/被中断
     ↓
Inbox
     ↓
新的 Turn 6
"别研究数据库了......"

官方明确说:

follow-up cannot redirect a turn already underway.

所以:

text 复制代码
send_message ≠ 实时修改 Thought

它是:

text 复制代码
next-turn message

(GitHub)


十二、那真想停掉当前方向怎么办?interrupt_agent

现在 DSH 又专门加了:

text 复制代码
interrupt_agent

其内部是:

text 复制代码
ctx.subagents.interrupt(...)
       │
       ▼
Agent.cancel(
    cause,
    { keepInbox: true }
)

注意:

text 复制代码
interrupt
≠
kill child

它只是:

text 复制代码
停止当前 Turn

不会:

text 复制代码
销毁 Session
清空 Inbox
杀掉 descendants
删除 Activation 身份

所以可以:

text 复制代码
Child:
正在做 AAAAAA......

Parent:
interrupt

         ↓

当前 turn cancel

Parent:
send_message("方向错了,改查 B")

         ↓

Child 新 turn:
开始做 B

这已经很接近真正的长期 Worker Agent 了。(GitHub)


十三、Parent 和 Child 之间默认并不是"共享脑子"

这是 DSH 一个非常明确的原则。

Child 做了:

text 复制代码
读取 20 个文件
执行 8 个 Bash
思考 10k tokens
查网页
失败三次
修改代码

这些东西不会自动塞回 Parent Context

否则 SubAgent 最大的价值:

context isolation

就没了。

现在是:

text 复制代码
             Child Session

     Files / Tools / Thinking
     Search / Intermediate
     Messages / Failures
              │
              X
              │ 不自动复制
              ▼
           Parent

例如官方 report prompt 甚至明确告诉 Child:

父 Agent 和你共享 workspace,但不会自动收到你的 transcript、tool output 或 reasoning。

所以 Child 必须显式:

text 复制代码
report({
  output:
  "我检查了 auth.ts 和 token.ts,
   发现刷新 token 存在竞态,
   关键代码在..."
})

(GitHub)

这其实是一个非常好的 Multi-Agent communication 原则:

共享世界,不共享全部认知。


十四、report 又和 one-shot 的 result 不一样

这是一个很容易混淆的地方。

One-shot:

text 复制代码
Child
 ↓
finish
 ↓
SubagentResult
 ↓
Parent

而 Continuable:

text 复制代码
Child
 │
 ├── report("发现 A")
 │
 │
 ├── 继续工作
 │
 ├── report("又发现 B")
 │
 │
 ├── Parent send_message(...)
 │
 │
 └── 继续工作

report()

text 复制代码
不是 finish
不是 return
不是 shutdown

官方甚至允许:

text 复制代码
一个 Turn 0 次 report
一个 Turn 1 次 report
一个 Turn N 次 report

只是 prompt 建议最终应该 report 一次自包含结果。(GitHub)

所以它实际上更像:

text 复制代码
Actor → Actor Message

十五、同时 Runtime 自己还有 Settlement Notice

为什么既有:

text 复制代码
report

又有:

text 复制代码
settlement notice

因为二者语义不同。

report 是:

text 复制代码
Child 说:
"我发现 X。"

而 settlement 是:

text 复制代码
Runtime 说:
"Child #123 这一轮执行结束了,
 stopReason = completed,
 最终 assistant output = ..."

官方刻意使用不同 provenance,防止把 Runtime 的话伪装成 Child 自己说的话。(GitHub)

这是 Session Log / provenance 思想再次出现。


十六、spawn 和 fork 到底有什么区别?

它们几乎共用所有执行机制。

真正的差别只有:

text 复制代码
Child Session 初始 Seed

spawn

text 复制代码
Parent history

A
B
C
D

       spawn
         │
         ▼

Child

Task

也就是说:

text 复制代码
zero parent conversation

它有自己的 session、system prompt,复用同一套 DSH Agent factory,但父对话完全不带过去。(GitHub)

fork

则是:

text 复制代码
Parent

Turn 1 ✓
Turn 2 ✓
Turn 3 ✓

Turn 4
   assistant tool_call: subagent_fork(...)
   ← 当前还没结束

Child 拿到:

text 复制代码
Turn 1
Turn 2
Turn 3
+
新 Task

不会拿:

text 复制代码
Turn 4

原因就是我们之前研究 Session Log 时讲过的:

text 复制代码
assistant tool_call

还没有:

text 复制代码
tool/result
turn/end

这个 Session prefix 是不平衡的,不能直接 replay。

所以源码:

ts 复制代码
const lastEnd =
    events.findLast(e => e.type === 'turn/end')

return events.slice(0, lastEnd.seq + 1)

只拷贝到最后一个完整 turn/end。(GitHub)

这个地方把:

Session Event Sourcing + SubAgent Fork

真正串起来了。


十七、但 fork 并不是"继承父 Agent 一切"

这是非常容易误解的。

inheritsParentContext = true

这里的 Context 指:

conversation history

不是:

text 复制代码
父 Agent 权限
父 Agent tool restriction
父 Agent Service scope
父 Agent authority

事实上 spawn 和 fork 的 Child 都创建:

fresh flat scope

所以:

text 复制代码
Parent
 ├─ 禁止 Tool A
 ├─ 特殊权限 B
 └─ 特殊临时 Service C

并不意味着 fork Child 自动继承这些。

官方专门强调 inheritsParentContext 只描述 conversation seeding,不代表 tool、service 或 authority inheritance。(GitHub)

这是一个非常成熟的安全设计:

继承知识和继承权限必须分开。


十八、为什么当前 shipped 的 fork 被固定成 one-shot?

这个地方很有 DeepSeek Harness 味道。

理论上 fork Provider 实现了:

text 复制代码
prepareContinuable()

所以技术上能做:

text 复制代码
continuable fork

但当前官方 cordis.yml 明确让:

text 复制代码
spawn → continuable

fork → one-shot

原因不是功能做不到,而是 KV Cache

fork 的价值在于:

text 复制代码
Parent history

可以作为 byte-identical prefix 直接被 Child 重用。

但 continuable Child 需要额外加入:

text 复制代码
report tool schema
+
report system prompt section

而这些内容出现在 inherited history 前面。

于是:

text 复制代码
Parent prefix

AAAA BBBB CCCC

变成:

text 复制代码
AAAA
[report prompt]
[report tool schema]
BBBB CCCC

父 prefix cache 被破坏。

所以官方当前明确选择:

fork 保持 one-shot,优先保证 prefix reuse。

(GitHub)

我觉得这个设计取舍特别值得研究:

SubAgent 架构不仅在考虑功能,还在考虑模型 Prefix Cache 的物理成本。


十九、再来看最有意思的:Claude Code Provider 到底怎么实现

我们之前说:

text 复制代码
DSH
 ↓
ctx.subagents
 ↓
Claude Code

这个结论没错。

但容易产生一个误解:

DSH 是不是调用了 Claude Code 里面原生的 Agent SubAgent Tool?

不是。

当前 dsh-subagent-claude-code 做的是:

text 复制代码
DSH Parent
    │
    ▼
dsh-tool-subagent
    │
    ▼
ctx.subagents
    │
    ▼
ClaudeCodeProvider
    │
    ▼
@anthropic-ai/claude-agent-sdk
    │
    │ query()
    ▼
native `claude`
CLI process
    │
    ▼
完整 Claude Code Agent

官方在 8 月 4 日的实现说明中明确写了,它使用官方 Claude Agent SDK,并通过 query() 启动真实 Claude Code,每次给它一个 self-contained text task。每次调用都是一个fresh product process + non-resumable conversation 。(GitHub)

而 Anthropic 自己也把 Agent SDK 的 query() 定义为适合 one-shot / stateless / unidirectional interaction;需要有状态 follow-up 时应使用其他 client 形态。(GitHub)

所以:

text 复制代码
DSH Claude Code Provider

更准确理解成:

把整个 Claude Code 当成一个外部 Worker Runtime。

而不是:

"调用 Claude Code 内部的某个 native subagent。"


二十、这和 Claude Code 自己的 SubAgent 是两个层级

Claude Code 原生 SubAgent 是:

text 复制代码
Claude Code
   │
   ▼
Agent Tool
   │
   ├─ Explore
   ├─ Plan
   ├─ general-purpose
   └─ custom agents

每个 subagent 有自己的 context window、system prompt、tools 和权限,只把结果带回主上下文。(Claude Platform Docs)

而 DSH 是:

text 复制代码
                DSH
                 │
          ctx.subagents
                 │
        ┌────────┼────────┐
        ▼        ▼        ▼
      spawn    Codex    Claude Code
                           │
                           ▼
                  整个 Claude Code
                           │
                           ▼
                   它内部甚至还能
                   再 spawn SubAgent

所以理论上会出现:

text 复制代码
DSH Parent
   ↓
Claude Code Provider
   ↓
Claude Code Agent
   ↓
Claude native SubAgent

两层 delegation

这也解释了为什么 DSH 对 Claude Code Provider 配置:

text 复制代码
maxDepth = provider-managed

因为一旦进入 Claude Code 世界,内部递归策略属于 Claude Code,而不是 DSH。(GitHub)


二十一、Claude Code Provider 为什么不支持 persona/toolFilter/outputSchema?

因为 DeepSeek 很克制地定义了边界。

DSH 只给 Claude Code:

text 复制代码
task
cwd
cancel signal
environment

其他东西:

text 复制代码
model
system prompt
tools
permissions
authentication
Claude settings

继续由:

text 复制代码
native Claude Code

管理。

不是 DSH 强行覆盖它。

所以 Claude Provider 宣布:

text 复制代码
outputSchema = false
depthLimit = false
toolFilter = false
persona = false

而父 Agent 如果要求这些能力,DSH 直接 fail loud。(GitHub)

我认为这个边界划分是对的。

否则就会产生:

text 复制代码
DSH Permission System
        ×
Claude Permission System
        ×
Claude settings

三套 authority 互相冲突。


二十二、Codex / ACP / DSH-SDK 也是同一个抽象

所以你现在再看 ctx.subagents,就会发现它其实统一了差异极大的系统:

text 复制代码
spawn
=
同进程 Child Agent

fork
=
同进程 + Parent Session Seed

ACP
=
协议级外部 Agent

Claude Code
=
外部完整 Coding Agent 产品

Codex
=
外部完整 Coding Agent 产品

DSH-SDK
=
另一套完整 DeepSeek Harness Runtime

例如 DSH SDK Provider 会真的启动一个新的 DSH runtime,它有自己的 cordis.yml、模型、工具、session 和完整 plugin tree;父 Agent 只拿最终 assistant 结果,子 runtime 的 token 和完整 transcript 都不进入 Parent Context。(GitHub)

这已经相当接近:

Agent Virtualization Layer


二十三、Session Log 在这里再次成为底座

Local SubAgent 并不是:

text 复制代码
new Agent()

完事。

而是有真正的:

text 复制代码
Child Session

Child header 会记录:

text 复制代码
parentSession = Parent Session ID

所以形成:

text 复制代码
Session P
   │
   ├── Child A
   │     │
   │     ├── Child A1
   │     └── Child A2
   │
   └── Child B

而不是临时的调用树。

listChildren() / listDescendants() 甚至可以直接从 durable Session store 和 projection 重建这棵树,不用把 Child Agent resume 到内存里 。(GitHub)

这里又出现了我们之前研究 Session 时那个核心思想:

text 复制代码
Runtime Object
≠
Persistent Truth

二十四、所以 SubAgent 体系真正有四种 ID

理解源码时建议把这四个概念分清:

text 复制代码
childId / SessionId
=
"这个长期 Child 是谁"

runId
=
"这次运行 epoch 是谁"

messageId
=
"塞进它 Inbox 的这条消息是谁"

jobId
=
"如果 one-shot 在后台跑,
 Job Runtime 里的后台任务是谁"

例如同一个 continuable Child:

text 复制代码
SessionId = child-123

上午:

text 复制代码
Activation #1
runId = run-A

释放。

下午 cold resume:

text 复制代码
Activation #2
runId = run-B

但是:

text 复制代码
childId 仍然是 child-123

官方也明确规定,每个 Activation epoch 都有自己的 subagent/start/end lifecycle pair,cold resume 会生成新的 run epoch,但 durable child identity 不变。(GitHub)


二十五、把整个实现压缩成一张源码心智图

我建议你以后直接拿这张图去读源码:

text 复制代码
                         Parent Model
                              │
                       subagent tool call
                              │
                              ▼
               ┌────────────────────────┐
               │   dsh-tool-subagent    │
               │                        │
               │ Model-facing Consumer  │
               └────────────┬───────────┘
                            │
                     provider="spawn"
                            │
                            ▼
        ┌─────────────────────────────────────┐
        │          ctx.subagents              │
        │         SubagentRuntime             │
        │                                     │
        │ ┌─────────────┐ ┌─────────────────┐ │
        │ │ Provider    │ │ Continuation    │ │
        │ │ Registry    │ │ Manager         │ │
        │ └──────┬──────┘ └────────┬────────┘ │
        └────────┼─────────────────┼───────────┘
                 │                 │
        One-shot │                 │ Continuable
                 │                 │
       ┌─────────▼────────┐    ┌───▼──────────────┐
       │ SubagentRun      │    │ Durable Session  │
       │                  │    │                  │
       │ result           │    │ childId          │
       │ dispose          │    │ descriptor       │
       └─────────┬────────┘    └───┬──────────────┘
                 │                 │
                 │             Activation
                 │                 │
                 │             AgentHandle
                 │                 │
                 │              Inbox FIFO
                 │                 │
                 ▼                 ▼
       ┌──────────────────────────────────────┐
       │              Provider                │
       │                                      │
       │ spawn / fork / ACP / Codex / Claude │
       │ Code / DSH SDK                       │
       └──────────────────┬───────────────────┘
                          │
                          ▼
                     Child Work
                          │
             ┌────────────┴─────────────┐
             │                          │
       one-shot result            explicit report
             │                          │
             └────────────┬─────────────┘
                          ▼
                     Parent Inbox

二十六、我认为 DeepSeek SubAgent 最值得学的其实不是"多 Agent"

看完源码后,我会把值得借鉴的优先级重新排一下。

第一是 Durable Agent Identity ≠ Live Agent Instance

这比普通 SubAgent Framework 高一个层次:

text 复制代码
Session
  ↕ cold resume
Activation
  ↕
AgentHandle

它为长期 Agent、资源回收、恢复、Agent Tree 打好了基础。(GitHub)

第二是 Provider 只解决"怎么创建",Runtime 才拥有生命周期。

尤其 continuable 模式下:

text 复制代码
Provider
只 prepareContinuable()

之后:

text 复制代码
identity
session
activation
inbox
resume
authority
dispose

全归统一 Runtime。

这是非常重要的 ownership 设计。(GitHub)

第三是显式 Agent Communication,而不是共享 Transcript。

text 复制代码
Parent → Child
send_message

Child → Parent
report

Runtime → Parent
settlement

三种通道语义完全不同。

不是把两个 Agent 的 message history 混成一锅。(GitHub)

第四才是多 Provider。

text 复制代码
spawn / fork / Claude / Codex / ACP / DSH

虽然很酷,但从架构价值上反而不是最深的。


二十七、如果套到语音助手,我认为可以直接得到一个很有价值的架构

例如以后不是:

text 复制代码
Main Agent
 ├─ TravelAgent function
 ├─ HealthAgent function
 └─ FinanceAgent function

而是:

text 复制代码
                    User Agent
                        │
                 ctx.subagents
                        │
        ┌───────────────┼────────────────┐
        ▼               ▼                ▼
   Travel Agent     Finance Agent    Research Agent

 Session T          Session F         Session R
     │                  │                 │
 Activation          cold              Activation
     │                                    │
     ▼                                    ▼
长期维护旅行规划                       持续后台调研

其中:

text 复制代码
Session = Agent 长期记忆/身份
Activation = 当前是否在工作
Inbox = 用户/MainAgent 给它的新任务
Report = 它主动返回重要发现
Session Log = 它自己的完整经历

这其实和我们之前讨论的:

text 复制代码
Service
Event
Session Log

真正闭环了。

而且你之前用 Claude Code Hooks 来校验 Event 的理解也仍然成立:Claude Hook 更像产品暴露出来的生命周期扩展点,而 DSH Event 是 Runtime 内部统一机制。


最后给你一句我现在对 DSH SubAgent 的定义

之前我会说:

SubAgent = 一个可以替换 Provider 的 Agent delegation Service。

看完当前源码后,我会改成:

DeepSeek Harness SubAgent 是一套以 Durable Session 为身份、以 Activation 为运行实例、以 Inbox 为通信队列、以 Provider 为执行后端、以 Event/Session Log 为生命周期事实的轻量 Agent Runtime。

而最有意思的一点是:

text 复制代码
One-shot SubAgent
≈ RPC / Task

Continuable SubAgent
≈ Actor / Durable Worker

Claude Code / Codex Provider
≈ 外部 Agent Runtime Adapter

这三个模式,被 ctx.subagents 统一成了同一个能力面 。我认为这才是 DeepSeek 这套 SubAgent 设计真正厉害的地方。(GitHub)

如果下一步继续往源码最深处走,我建议直接拆 startContinuable()ContinuationManagerAgent.followup() → Inbox → Session Log → cold resume 这一条链。我可以下一轮按函数级源码调用顺序,把每个核心 TypeScript 类、函数、关键字段以及对象生命周期逐段展开,这部分会把"为什么需要 Activation、为什么不直接 resume Session、父子 Agent 到底怎么管理 Scope"彻底讲透。

继续往下钻之后,这条链已经比较完整了。现在我认为 DeepSeek Harness 的 continuable SubAgent 最核心的实现并不是"SubAgent"本身,而是把 Agent + Session + Inbox + Activation 四个东西拆开了。

先给结论:

Session 决定"这个子 Agent 是谁、经历过什么";Agent 是执行这个 Session 的机器;Inbox 是待执行输入的持久队列;Activation 则表示"这个 Session 当前是否被装载成一个活 Agent"。

这四层一拆开,cold resume 就自然成立了。


1. 先把完整调用链画出来

父 Agent 第一次创建一个 continuable child:

text 复制代码
Parent Agent
    │
    │ subagent(run_in_background=true)
    ▼
tool-subagent
    │
    ▼
ctx.subagents.startContinuable()
    │
    ├─ ① reserve childId
    ├─ ② snapshot descriptor
    ├─ ③ capture parent policy
    ├─ ④ provider.prepareContinuable()
    ├─ ⑤ seed child Session
    │
    ▼
ContinuationManager.materialize()
    │
    ├─ ctx.agents.create()
    │      │
    │      ├─ Session
    │      ├─ Agent
    │      ├─ Scope
    │      └─ Inbox
    │
    ├─ create Activation
    ├─ acquireOwnership()
    ├─ watchSettlement()
    │
    ▼
submitAdmitted()
    │
    ▼
Agent.followup()
    │
    ▼
Inbox
    │
    │ agent/inbox/spliced
    ▼
Session Log
    │
    ▼
Agent Loop
    │
    ├─ turn/start
    ├─ inbox.claim()
    ├─ user/message
    ├─ LLM
    ├─ tools...
    └─ turn/end

父 Agent 以后再发消息:

text 复制代码
ctx.subagents.followup(childId)
             │
             ▼
     Activation 还在吗?
        │             │
       YES            NO
        │             │
        ▼             ▼
 Agent.followup()   coldResume()
                      │
                      ▼
             persistence.inspect()
                      │
             fold descriptor
                      │
                      ▼
              ctx.agents.resume()
                      │
                      ▼
                new Activation
                      │
                      ▼
               Agent.followup()

当前源码对此写得非常明确:startContinuable() 只等到 Inbox acceptance ,不会等 turn 开始,更不会等消息进入 user/message。(GitHub)

这个细节非常重要。


2. startContinuable() 第一件事甚至不是创建 Agent

源码:

ts 复制代码
const childId = SessionId(randomUUID())

先创建:

text 复制代码
Durable Child Identity

然后才考虑 Agent。

这和传统写法是反过来的。

传统很容易:

text 复制代码
Agent agent = new Agent()
childId = agent.id

DSH 的思路更像:

text 复制代码
childId
   ↓
Session Identity
   ↓
需要时 materialize Agent

所以:

text 复制代码
Child Identity
≠
Agent Object

startContinuable() 随后把模型、provider、persona、toolFilter 等信息快照到 descriptor;特别值得注意的是,这些值在第一个 await 之前 就确定了。(GitHub)

原因其实很讲究。

假设:

text 复制代码
T0 Parent model = DeepSeek-V4

startContinuable()
      │
      │ await...
      │
T1 Parent 被切成另一个模型

Child 应该继承哪一个?

DSH 的答案:

text 复制代码
T0 时刻

因为:

delegation boundary 是调用发生的那个时刻。

而不是等 Child 真创建出来以后再读取 Parent 当前状态。(GitHub)


3. 第二步:写一个非常重要的 subagent/descriptor

它本质上就是 Child 的:

Durable Creation Manifest

大致类似:

text 复制代码
subagent/descriptor

version = 2
mode = continuable
provider = spawn
label = "研究 Harness"
agentProvider = deepseek
agentModel = xxx
persona = Research Agent
toolFilter = ...

这个东西:

text 复制代码
不会进入 LLM history

但是:

text 复制代码
会永久留在 Child Session Log

并且 survives compaction。(GitHub)

这里非常漂亮。

因为将来 cold resume 时根本不需要:

text 复制代码
"还记得这个 Agent 当时是怎么创建的吗?"

Session 自己就知道。

可以理解成:

text 复制代码
Session Log

┌──────────────────────────┐
│ subagent/descriptor      │
│                          │
│ mode = continuable       │
│ model = X                │
│ persona = Y              │
│ toolFilter = Z           │
└──────────────────────────┘
            │
            ▼
       Cold Resume

也就是:

Agent 的构成信息本身也 Event Source 化了。


4. Provider 在 continuable 模式下其实权力很小

这是我这轮研究里觉得很值得借鉴的一点。

以前容易理解成:

text 复制代码
Subagent Provider
    ↓
创建 Agent
运行 Agent
暂停 Agent
Resume Agent

实际上 continuable 模式不是这样。

Provider 只有:

text 复制代码
prepareContinuable()

也就是说:

text 复制代码
Provider
只告诉 Runtime:

"第一次创建这个 Child 时,
 需要这些额外 seed 数据。"

例如:

spawn

text 复制代码
seed = none

fork

text 复制代码
seed =
Parent 已完成的 Session history

然后 Provider 基本退出舞台。

后面:

text 复制代码
Agent 创建
Session
Activation
Resume
Inbox
Lifecycle
Dispose

全部由:

text 复制代码
ContinuationManager

拥有。官方明确说明 continuable provider 只贡献 detached creation data;cold resume 完全不经过 provider。(GitHub)

这实际上是一个很好的 ownership 原则:

text 复制代码
Provider
=
Factory Hint

Runtime
=
Lifecycle Owner

而不是:

text 复制代码
Provider
=
Mini Agent Framework

5. 真正创建 Child 的地方:materialize()

这里是核心。

text 复制代码
startContinuable
      │
      ▼
materialize()
      │
      ▼
materializeTracked()

如果第一次创建:

ts 复制代码
ownerCtx.agents.create(...)

如果 cold resume:

ts 复制代码
ownerCtx.agents.resume(...)

源码就是:

text 复制代码
create !== undefined
        │
        ├── YES → ctx.agents.create()
        │
        └── NO  → ctx.agents.resume()

所以你可以发现:

ContinuationManager 根本没有自己实现 Agent Loop。

它依赖:

text 复制代码
ctx.agents

来创建或恢复普通 Agent。

这又符合我们之前讲的 Service:

text 复制代码
SubAgent Runtime
       │
       ▼
ctx.agents
       │
       ▼
AgentLoop implementation

SubAgent 是一个 orchestrator,而不是第二套 Agent Loop。


6. ctx.agents.create() 最终返回的不是 Agent,而是 AgentHandle

这是另一个关键抽象:

ts 复制代码
interface AgentHandle {
    agent: Agent
    dispose(): Promise<void>
}

(GitHub)

也就是:

text 复制代码
Agent
=
"我可以跟它交互"

AgentHandle
=
"我拥有它的生命周期"

这两个能力故意分开。

普通插件:

text 复制代码
ctx.agents.get(id)

只能得到:

text 复制代码
Agent

但负责创建它的人:

text 复制代码
ctx.agents.create()

拿到:

text 复制代码
AgentHandle

所以只有 owner 才有:

text 复制代码
dispose()

这非常像资源系统中的:

text 复制代码
reference
vs
ownership handle

ContinuationManager 会把这个 Handle 收进 Activation:

text 复制代码
Activation
 ├─ childId
 ├─ parentSession
 ├─ provider
 ├─ handle
 ├─ ancestry
 ├─ ownedChildren
 ├─ accepted
 ├─ disposal
 └─ observer

7. 所以 Activation 到底是什么?

现在可以给它一个非常准确的定义:

Activation = 一个 Durable Child Session 当前在这个进程中的 Residency Epoch。

例如:

text 复制代码
Child Session S123

生命周期可能是:

text 复制代码
                Durable Session S123

─────────────────────────────────────────▶ time

       Activation A1
       ┌───────────────┐
       │ Agent object  │
       │ AgentHandle   │
       └───────────────┘

                        cold

                                Activation A2
                                ┌──────────────┐
                                │ Agent object │
                                │ AgentHandle  │
                                └──────────────┘

所以:

text 复制代码
Session S123

可能活几个月。

但:

text 复制代码
Activation A1

可能只活 30 秒。

A1 dispose 后:

text 复制代码
Agent 对象没了

并不意味着:

text 复制代码
Child Agent 身份没了

因为 Session 还在。

这就是:

Durable identity / ephemeral residency separation。

官方源码的定义几乎就是这个意思:一个 continuable child 始终只有一个 durable Session,同时最多存在一个 process-local Activation。


8. 为什么还需要 accepted: Set<MessageId>

这个字段一开始看非常奇怪:

ts 复制代码
accepted: Set<MessageId>

但它实际上解决一个很真实的异步竞态。

考虑:

text 复制代码
Agent 当前 status = idle

Parent:

text 复制代码
send_message("继续研究")

发生:

text 复制代码
Agent.followup()
     │
     ▼
Inbox 已插入消息

但是 JavaScript 调度上,可能存在一个非常短的时间窗口:

text 复制代码
Inbox 已有消息

但

Agent driver 尚未真正开始运行

如果只判断:

ts 复制代码
agent.status === idle

SettlementWatcher 会说:

text 复制代码
"哦,它闲了,可以 dispose。"

结果刚收进去的消息还没运行,Agent 就被销毁。

所以 ContinuationManager 在真正调用:

ts 复制代码
Agent.followup()

之前先:

ts 复制代码
activation.accepted.add(messageId)

等 Inbox 发出:

text 复制代码
agent/inbox/claimed

或者:

text 复制代码
agent/inbox/discarded

再删除这个 ID。

因此判断:

text 复制代码
running

不是:

ts 复制代码
agent.status === "running"

而是:

text 复制代码
agent.status === running
OR
accepted.size > 0

源码正是:

text 复制代码
running:
Agent running
OR
accepted waking work

waiting:
Agent idle
BUT owns children

settled:
Agent idle
AND no owned children

(GitHub)

这个设计很细,但非常值得学。


9. Parent 消息究竟怎么进入 Child?

关键函数:

text 复制代码
ContinuationManager.submit()

先:

text 复制代码
acquireOwnership(parent, childId)

然后:

ts 复制代码
const message = createUserMessage(...)

再:

ts 复制代码
activation.handle.agent.followup(message)

到这里:

text 复制代码
SubAgent 世界

基本结束。

之后进入:

text 复制代码
普通 Agent 世界

因为:

text 复制代码
Child Agent

和普通 Agent 调的是完全一样的:

text 复制代码
followup()

10. Agent.followup() 实际只有两行逻辑

核心:

ts 复制代码
followup(input) {
    this.send(input, 'next-turn', true)
}

也就是:

text 复制代码
target = next-turn
wakeup = true

(GitHub)

另外两个方法也很有意思:

text 复制代码
followup
→ next-turn + wake

steer
→ next-step + wake

inject
→ next-step + no wake

所以三者语义其实很干净:

API 什么时候消费 会不会唤醒 Agent
followup 下一 Turn
steer 下一 Step
inject 下一 Step

(GitHub)

因此我们上一轮说:

text 复制代码
send_message ≠ steering

现在从源码上完全坐实。


11. 然后消息进入 Inbox

send()

ts 复制代码
this.inbox.splice(
    resolvedTarget,
    Infinity,
    0,
    [message]
)

然后:

ts 复制代码
if (wakeup)
    this.wakeDriver()

(GitHub)

所以:

text 复制代码
send_message
  ↓
SubagentRuntime.followup
  ↓
Agent.followup
  ↓
Inbox.nextTurn

12. 但这里最漂亮的是:Inbox 不是普通内存数组

源码:

ts 复制代码
class Inbox {
  state = {
    "next-turn": [],
    "next-step": []
  }
}

看起来很普通。

但是它构造的时候:

ts 复制代码
for (const event of session.events) {
    if (event.type === 'agent/inbox/spliced')
        apply(event)
}

(GitHub)

也就是说:

text 复制代码
Inbox
=
Session Log 的 Projection

这和之前讲:

text 复制代码
Surface
=
Session Log Projection

完全同一个思想。


13. 插入 Inbox 时,先写 Session Event

真正 mutate:

ts 复制代码
this.session.append(
    'agent/inbox/spliced',
    splice
)

然后才:

ts 复制代码
inbox.splice(...)

(GitHub)

所以 Parent 发送:

再重点研究 cold resume。

Session 实际上先出现:

text 复制代码
seq 500
agent/inbox/spliced

target = next-turn
inserted = [
   message #M123
   "再重点研究 cold resume"
]

此时:

text 复制代码
这条消息还没给 LLM 看。

注意这个区别。


14. 这说明 DSH 其实有两个"消息存在状态"

状态 A:已接收但尚未消费

text 复制代码
agent/inbox/spliced

代表:

Runtime 已经承诺接受这个消息。

但它还不是:

text 复制代码
model history

状态 B:已经进入 Agent Turn

Agent Loop:

text 复制代码
turn/start
  ↓
inbox.claim()
  ↓
agent/inbox/spliced
删除 pending
  ↓
user/message
  ↓
LLM

Agent 在 preStep() 调:

ts 复制代码
this.inbox.claim(...)

真正准备执行时,随后才:

ts 复制代码
session.append(
  'user/message',
  message,
  { surfaceOp: 'append' }
)

(GitHub)

所以完整 timeline:

text 复制代码
Parent send_message
       │
       ▼
agent/inbox/spliced
       │
       │ "accepted"
       │
       ▼
等待......
       │
       ▼
turn/start
       │
       ▼
agent/inbox/spliced
(remove from pending)
       │
       ▼
agent/inbox/claimed
       │
       ▼
user/message
       │
       ▼
LLM

15. 这比我之前理解的 Session Log 又更进了一步

我们之前一直说:

Model-visible means logged。

现在你会发现 DSH 实际已经更进一步:

text 复制代码
Model-visible
→ logged

AND

Runtime-promised input
→ 也 logged

也就是说 Session Log 不只是:

text 复制代码
模型经历

还开始保存:

text 复制代码
Agent runtime 的待办状态。

这已经有一点:

text 复制代码
WAL + Event Sourcing

的味道。


16. 这也是为什么 crash 后理论上可以恢复 Inbox

假设:

text 复制代码
09:00:00

Parent:
"再研究一下 Claude Provider。"

       ↓

agent/inbox/spliced

此时进程 crash。

LLM 甚至还没收到消息。

重启:

text 复制代码
ctx.agents.resume()
      │
      ▼
load Session
      │
      ▼
new Agent(...)
      │
      ▼
new Inbox(session)
      │
      ▼
replay agent/inbox/spliced

于是:

text 复制代码
pending next-turn

可以重新构建出来。Inbox 构造函数就是通过回放 Session 后缀里的 agent/inbox/spliced 重建两个 pending list。(GitHub)

这就是我觉得 DSH Session Log 设计非常强的原因。

它开始承担:

text 复制代码
Conversation State
+
Runtime State

而不只是 Chat History。


17. 现在来看 cold resume,整个逻辑就很自然了

Parent:

text 复制代码
send_message(child-123)

ContinuationManager:

ts 复制代码
const activation =
    this.activations.get(childId)

如果没有:

text 复制代码
activation === undefined

直接:

text 复制代码
coldResume()

(GitHub)


18. coldResume() 第一步不是 Resume,而是 inspect()

ts 复制代码
persistence.inspect(childId)

(GitHub)

注意:

text 复制代码
inspect

和:

text 复制代码
resume

也是分开的。

先用廉价的持久化信息确认:

text 复制代码
这个 Session 存不存在?
是谁的 Child?
能不能 Resume?
descriptor 是什么?

不需要立刻创建 Agent。

这是:

text 复制代码
cold data

阶段。


19. 然后先做权限检查

读取:

text 复制代码
loaded.meta.parentSession

然后:

ts 复制代码
authorizeLineage(
    parent,
    childId,
    loaded.meta.parentSession
)

(GitHub)

权限非常严格:

text 复制代码
只有 Durable Direct Parent

可以继续这个 Child。

不是:

text 复制代码
祖先 Agent
任意同 Session Agent
workflow
team
host

目前都不行。

源码明确检查:

text 复制代码
ctx.agents.get(parent.id) === parent

并且:

text 复制代码
child.parentSession === parent.id

否则:

text 复制代码
UNAUTHORIZED

这里又体现了一个原则:

知道 Child ID ≠ 拥有 Child。


20. 然后从 Session 自己恢复 descriptor

ts 复制代码
foldSubagentDescriptor(...)

而不是找:

text 复制代码
原 Provider

(GitHub)

所以:

text 复制代码
第一次创建:

spawn Provider
     │
     ▼
prepareContinuable()

但 cold resume:

text 复制代码
NO spawn Provider

而是:

text 复制代码
Child Session
   │
   ├─ descriptor
   ├─ model
   ├─ persona
   ├─ toolFilter
   └─ policies
        │
        ▼
ctx.agents.resume()

官方甚至明确指出:

原始 provider 即使已经 unregister,continuable child 仍然可以 cold-resume;provider name 只是 descriptor provenance,不是恢复能力。(GitHub)

这个解耦很彻底。


21. ctx.agents.resume() 到底做什么?

它最终进入 AgentLoop:

text 复制代码
ctx.agents.resume()
       │
       ▼
AgentFactory.resume()
       │
       ▼
sessionPersistence.prepare(id)
       │
       ▼
load Session
       │
       ▼
new ReactLoopAgent(...)
       │
       ├─ Inbox(session)
       ├─ RuntimeContextProjection
       └─ scope
       │
       ▼
setup
       │
       ▼
register Session
register Agent
       │
       ▼
agent/session-start
       │
       ▼
return AgentHandle

官方的 AgentLoop 文档明确说 resume 会加载 persisted Session,在同一个 SessionId 下重新注册 Agent,并继续原来的 turn numbering 和 derived history。(GitHub)

因此:

text 复制代码
Cold Resume

本质上不是:

text 复制代码
恢复某个 JavaScript Agent 对象。

而是:

用同一份 Session Event Log 构建一个新的 Agent runtime instance。


22. 所以"记忆"到底在哪里?

不是:

text 复制代码
Activation

不是:

text 复制代码
Agent object

甚至不是:

text 复制代码
Provider

真正的状态锚点是:

text 复制代码
Session Log

可以画成:

text 复制代码
              Durable State

              Session Log
                  │
        ┌─────────┼──────────┐
        │         │          │
        ▼         ▼          ▼
   Conversation  Inbox    Descriptor
      history    state      policy
        │         │          │
        └─────────┼──────────┘
                  │
                  ▼
              materialize
                  │
                  ▼
              Activation
                  │
                  ▼
               Agent

所以:

Agent 是 Session 的执行视图。

这句话我觉得可以作为理解 DSH Agent Runtime 的又一个核心心智模型。


23. Child 的 Scope 又是怎么恢复的?

这个问题非常关键。

因为恢复 Session 只解决:

text 复制代码
历史

但是 Agent 还要知道:

text 复制代码
Tools
Prompt
Persona
Permission
Model
Sandbox

DSH 的解决办法不是:

text 复制代码
Parent scope 整体 clone

而是分层处理。


24. 第一层:继承 Parent 当前的 Agent Preset Composition

创建 Child 时:

ts 复制代码
childCtx
   .agentPresets
   ?.composeFrom(childCtx, parent.ctx)

(GitHub)

这很有意思。

不是:

text 复制代码
重新读取 preset.yaml

而是:

join Parent 正在实际运行的那一代 composition。

官方说明原因也非常合理:

如果:

text 复制代码
Parent 在 9:00 加载 Preset V1

10:00:

text 复制代码
preset 文件已经更新为 V2

Parent 此刻仍运行:

text 复制代码
V1

Child 应该看到:

text 复制代码
V1

而不是重新解析得到 V2。

否则:

text 复制代码
Parent history

是在 Tool Set V1 下形成的,

Child 却突然:

text 复制代码
Tool Set V2

上下文就不一致了。(GitHub)


25. 第二层:Child 自己再 shadow

Parent Composition:

text 复制代码
Standard Agent
├─ files
├─ bash
├─ web
├─ skills
└─ ...

Child:

text 复制代码
composeFrom(parent)
       │
       ▼
Standard composition
       │
       ├─ child persona
       ├─ child toolFilter
       └─ subagent delegation context

源码:

ts 复制代码
if (persona)
    systemPrompt.section(...)

if (toolFilter)
    tools.restrict(...)

(GitHub)

因此实际上是:

text 复制代码
Base Parent Capability
         ∩
Child Restriction

而不是 Child 自己随便扩大权限。


26. 这里还有一个非常重要的安全设计:approval 强制 never

Child 的系统上下文甚至明确告诉模型:

text 复制代码
你是 delegated subagent,
permission scope 在创建时已经固定,
不能在 session 内自行扩大;
需要额外授权的操作会被拒绝。

(GitHub)

然后源码:

ts 复制代码
approvalPolicy =
    approval service exists
      ? 'never'
      : undefined

也就是:

text 复制代码
Parent:
遇到敏感行为
可以 Ask User

Child:
不允许自己 Ask User

(GitHub)

这个设计我非常认同。

否则可能出现:

text 复制代码
Main Agent
委派后台 Agent
     │
     ▼
后台 Agent 突然弹授权
     │
     ▼
用户不知道自己在授权谁

所以 DSH 的原则更像:

SubAgent 不拥有扩大 authority 的能力。

需要更大权限时:

text 复制代码
report to Parent

Parent 决定怎么办。


27. 更漂亮的是:这些权限也写入 Child 自己的 Session Log

Delegation 时:

text 复制代码
captureDelegatedPolicyOverrides(parent)

然后:

text 复制代码
appendDelegatedPolicyOverrides(
    childSession,
    policies
)

(GitHub)

于是:

text 复制代码
Child Session
    │
    ├─ sandbox/mode
    │   source=delegation
    │
    └─ approval/policy
        source=delegation

这样 cold resume:

text 复制代码
不重新问 Parent:
"你现在是什么 permission?"

而是:

text 复制代码
replay Child 自己的 policy log

所以:

text 复制代码
Parent 今天 permission=A

Child 创建
→ 固定 A

Parent 明天 permission=B

Child cold resume
→ 仍然按照 Child durable policy

官方 8 月 10 日的实现说明明确强调:cold resume 不重新捕获 parent policy;Child log 是 policy owner 。(GitHub)

这是很值得你们记忆/权限系统借鉴的。


28. 再来看 Parent → Child → Grandchild

假设:

text 复制代码
P
│
└── A
    │
    └── B

A 创建 B 时:

ts 复制代码
A.activation.ownedChildren.add(B)

所以如果:

text 复制代码
A 自己的 LLM Turn 已经结束

但是:

text 复制代码
B 还在运行

A 的状态不是:

text 复制代码
settled

而是:

text 复制代码
waiting

因为:

ts 复制代码
agent idle
AND
ownedChildren > 0

(GitHub)

于是:

text 复制代码
A Activation

不能被释放。

这是非常合理的:

text 复制代码
A
├─ 自己不工作了
└─ 但它还有未完成的孩子

它仍然是整个生命周期树的一部分。


29. SettlementWatcher 就是一台非常小的状态机

逻辑:

text 复制代码
                 ┌──────────────┐
                 │   running    │
                 │              │
                 │ Agent active │
                 │ OR inbox work│
                 └──────┬───────┘
                        │ idle
                        ▼
               ownedChildren?
                │            │
              YES            NO
                │            │
                ▼            ▼
          ┌──────────┐  ┌──────────┐
          │ waiting  │  │ settled  │
          └────┬─────┘  └────┬─────┘
               │ child done   │
               └──────────────┘
                              │
                              ▼
                           dispose

Watcher 不断:

ts 复制代码
await Promise.race([
   agent.whenIdle(),
   activation.poke
])

然后重新判断。

为什么用:

text 复制代码
re-observe

而不是一次:

text 复制代码
whenIdle → dispose

因为 waiting 状态随时可能:

text 复制代码
Parent send_message

于是:

text 复制代码
waiting → running

同一个 Activation 又活起来。


30. 什么时候才真的销毁 Activation?

条件:

text 复制代码
Agent idle
AND
accepted.size == 0
AND
ownedChildren.size == 0

才:

text 复制代码
dispose()

dispose 顺序也很讲究。

首先:

text 复制代码
top-down cancellation
text 复制代码
Parent
 ↓ cancel
Child
 ↓ cancel
Grandchild

但真正 release:

text 复制代码
child-first
text 复制代码
Grandchild dispose
       ↓
Child dispose
       ↓
Parent dispose

源码明确如此设计。

这是典型资源树析构顺序。

否则 Parent Scope 先没了:

text 复制代码
Child

可能仍在访问 Parent composition / runtime dependencies。


31. 一个特别细的地方:settlement notice 必须在 releaseOwnership() 之前

源码顺序:

text 复制代码
delete Activation
       ↓
notifySettlement()
       ↓
releaseOwnership()
       ↓
subagent/end

为什么?

假设:

text 复制代码
Child C

结束。

Parent P 本身也已经:

text 复制代码
idle

如果先:

text 复制代码
releaseOwnership(C)

那么 Parent:

text 复制代码
ownedChildren == 0

SettlementWatcher 很可能马上判断:

text 复制代码
Parent settled

然后把 Parent dispose。

这时候再:

text 复制代码
notify Parent:
"C 已经完成......"

消息可能刚进 Parent Inbox:

text 复制代码
Parent 就被 cancel + dispose

所以必须:

text 复制代码
先把 settlement notice 交给 Parent

让 Parent 变成:

text 复制代码
accepted work

然后:

text 复制代码
再 release ownership

这样 Parent 就不会错误 settle。官方正是为了解决这个竞态,在 manager 的 disposal transaction 内投递 settlement。(GitHub)

这个细节说明这套东西是真的做过大量并发边界推敲的。


32. 现在完整走一个实例

假设 Parent:

帮我深入研究 DeepSeek Harness 的 Session 和 SubAgent 联系。

T1 创建

text 复制代码
Parent P
  │
  ▼
startContinuable

生成:

text 复制代码
childId = R123

Child Session:

text 复制代码
Session R123

seq0 subagent/descriptor
seq1 sandbox/policy
seq2 approval/policy

然后:

text 复制代码
Activation A1

创建。


T2 初始 Prompt 入队

text 复制代码
seq3
agent/inbox/spliced

insert:
"研究 Session 和 SubAgent 联系"

此刻:

text 复制代码
startContinuable()

已经可以返回:

text 复制代码
{
  childId: R123,
  messageId: M1
}

Parent 可以继续做自己的事情。(GitHub)


T3 Agent 真正开始

text 复制代码
seq4 turn/start

seq5 agent/inbox/spliced
     remove M1

seq6 user/message
     "研究 Session 和 SubAgent 联系"

seq7 step/start
...
seqN assistant/message
seqN+1 turn/end

Agent Loop 本身就是这么把 claimed inbox messages 转成 user/message 并继续模型循环的。(GitHub)


T4 Child 创建另一个 Child C

text 复制代码
R123.ownedChildren = { C456 }

R123 自己做完:

text 复制代码
status=idle

但:

text 复制代码
ownedChildren.size=1

所以:

text 复制代码
R123 = waiting

不会 dispose。


T5 C456 完成

text 复制代码
notify R123
releaseOwnership(C456)

于是 R123 收到新的 waking message。

可能再次:

text 复制代码
waiting → running

整理 Child 结果。


T6 R123 最终闲下来

text 复制代码
idle
accepted = 0
ownedChildren = 0

于是:

text 复制代码
settled

然后:

text 复制代码
Activation A1 dispose

但:

text 复制代码
Session R123

仍存在于 persistence。


T7 两小时后 Parent 又说

再重点研究下 Inbox。

Parent:

text 复制代码
send_message(R123)

ContinuationManager:

text 复制代码
activations.get(R123)
→ undefined

于是:

text 复制代码
coldResume()

T8 恢复

text 复制代码
persistence.inspect(R123)
       ↓
authorize parent P
       ↓
fold descriptor
       ↓
ctx.agents.resume(R123)
       ↓
persistence.prepare(R123)
       ↓
new Session projection
       ↓
new Agent
       ↓
new Inbox
       ↓
new Activation A2

然后:

text 复制代码
"再重点研究 Inbox"

进入 next-turn

这时候 LLM 看到的历史就是:

text 复制代码
之前 R123 的完整有效 Session History
+
新的 user message

而不是:

text 复制代码
一个全新的 Agent

从认知上,它还是"同一个研究 Agent"。


33. 所以这套架构实际上很像 Actor + Event Sourcing + Virtual Memory

我现在觉得可以用三个经典系统概念理解。

Actor Model

text 复制代码
ChildId
+
Inbox
+
Private State
+
Messages

Event Sourcing

text 复制代码
Session Log
=
durable truth

Virtual Memory / Process Residency

text 复制代码
Session
=
persistent process image

Activation
=
resident process

cold resume
=
page/process reload

所以 DeepSeek Harness 的 continuable SubAgent 已经不像一般 Multi-Agent Framework 的:

text 复制代码
Agent A calls Agent B

而更像:

一个非常轻量的 Agent Process Model。


34. 对你的语音助手设计,我认为这里有三个特别值得拿走

第一,Agent 身份与 Agent 实例一定要分离。

未来例如:

text 复制代码
旅行规划 Agent
投资研究 Agent
健康管理 Agent

不要理解成三个:

text 复制代码
长期驻留 Agent 对象

而应该:

text 复制代码
Agent Identity
        │
        ▼
Durable Agent Memory / Session
        │
        ▼
需要时 Activation

这样资源成本才可控。

第二,Agent 的待办消息也应该是 durable memory。

DSH 的 Inbox 不是 Redis 临时 Queue,而是:

text 复制代码
Event Log
    ↓
Inbox Projection

这对手机/语音助手尤其重要:

text 复制代码
设备切换
进程被杀
端云切换
模型服务重启

都不应该导致:

text 复制代码
"刚才已经交给 Agent 的任务"

悄悄丢失。

第三,权限必须在 delegation boundary 固化,而不是 resume 时重新继承。

我尤其建议记住这个原则:

text 复制代码
Parent 当前权限
      │
      │ delegation
      ▼
snapshot
      │
      ▼
Child Durable Policy
      │
      ├── activation 1
      ├── activation 2
      └── activation 3

而不是:

text 复制代码
每次 Child resume
→ 重新复制 Parent 当前权限

后者会导致长期 Agent 的权限语义漂移。


35. 到这里可以重新定义四个核心对象

以后读 DSH SubAgent 源码,我建议就按这四句话记:

Session

text 复制代码
"我是谁,我经历过什么,我被赋予了什么。"

Inbox

text 复制代码
"别人已经交给我、但我还没有处理什么。"

Agent

text 复制代码
"谁负责真正消费 Inbox、调用 LLM/Tool,并继续写 Session。"

Activation

text 复制代码
"这个 durable Agent 此刻有没有装载进内存,以及谁拥有它的生命周期。"

合起来就是:

text 复制代码
                       Durable
             ┌────────────────────────┐
             │        Session         │
             │                        │
             │ History                │
             │ Descriptor             │
             │ Policy                 │
             │ Inbox Events           │
             └───────────┬────────────┘
                         │
                    materialize
                         │
                         ▼
             ┌────────────────────────┐
             │      Activation        │
             │                        │
             │ AgentHandle            │
             │ accepted               │
             │ ownedChildren          │
             │ disposal               │
             └───────────┬────────────┘
                         │
                         ▼
             ┌────────────────────────┐
             │         Agent          │
             │                        │
             │ Inbox                  │
             │ Agent Loop             │
             │ LLM / Tools            │
             └────────────────────────┘

这张图,我认为已经比单纯看 SubAgent Provider 更接近 DeepSeek Harness 当前 Multi-Agent 架构的本质了。

而且它又把我们前面研究的三个核心概念串起来了:

text 复制代码
Service
   │
   │ ctx.subagents / ctx.agents
   ▼

Event
   │
   │ agent/status
   │ inbox/claimed
   │ subagent/start/end
   ▼

Session Log
   │
   │ descriptor
   │ policy
   │ inbox
   │ conversation
   ▼

Durable Agent Runtime

下一层最值得继续拆的已经不是 startContinuable() 了,而是两个更深的问题:

① Child 到底继承 Parent 哪些 Service / Tool / Prompt / Memory,Cordis Scope 是怎么做到"共享 composition,但局部 shadow"的;

spawn/fork/continuable 和 Session Surface / Compaction 怎么互动,尤其是 fork 为什么能共享历史但又不能简单共享 Parent 当前 Context。

这两个问题会直接把 Cordis Scope + Session + SubAgent 三块真正连起来,而且对你们以后设计"主 Agent + 垂域 Agent + 共享记忆/私有记忆"的架构参考价值很高。(GitHub)

相关推荐
精彩AI说1 天前
ChatGPT生成内容总是不按要求怎么办?格式、字数和指令失效的6个解决方法
chatgpt·prompt·ai写作·提示词·使用技巧·chatgpt教程
番茄不是西红柿kk1 天前
什么是Token?
人工智能·ai·chatgpt·agent·token·codex·deepseek
AI导出鸭1 天前
怎么让Claude做表格?AI导出鸭苹果版将Claude输出的Markdown表格或结构化列表智能解析为二维数据,一键导出Excel/Word标准表格。
人工智能·chatgpt·word·excel·ai导出鸭
yuhulkjv3351 天前
告别“复制-粘贴-崩格式”:ChatGPT鸿蒙版导出word格式的底层技术重构
ai·chatgpt·word·harmonyos·ai导出鸭
燕云少君2 天前
你只是跟AI聊聊天,它已经准备好刷你的卡了
人工智能·chatgpt·openai
yingyuecom2 天前
Seedance 2.5正式发布:映悦AI迎来“更长、更可控、更极致”的视频生成时代
人工智能·gpt·chatgpt·prompt·aigc
DS随心转APP2 天前
生成word文档的ChatGPT格式乱码终结者:AI导出鸭横向测评与工程化架构解析 摘要
人工智能·ai·chatgpt·word·deepseek·ai导出鸭