mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(subagent): preserve startup cleanup failures
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md
|
||||
2026-08-10-fork-children-stay-one-shot.md: f799f9f7affe7dfecbbe12d0988cf6f9b936d43e
|
||||
2026-08-10-fork-children-stay-one-shot.zh.md: 5c55e96296ed6678ac82afc0e40150d443f74c96
|
||||
2026-08-10-fork-children-stay-one-shot.md: 030bed3c1b516a54afd5f97b00000ed7ceaf64d8
|
||||
2026-08-10-fork-children-stay-one-shot.zh.md: 4b033a70c3674315c491e85be67a1d357aeac51b
|
||||
|
||||
@@ -12,7 +12,7 @@ The child-scoped `report` return channel is now the largest such addition, and s
|
||||
|
||||
## Decision
|
||||
|
||||
Every shipped composition binds the fork delegation tool to `backgroundMode: one-shot`: [the base bundle](../../../../packages/bundle/base/cordis.patch.yml), [the ACP example](../../../../examples/acp-agent/cordis.yml), and [the headless example](../../../../examples/headless-agent/cordis.yml). All three have access to the generic Task runtime: the base bundle leaves `run_in_background` available, while the two examples set `enableRunInBackground: false` as an explicit foreground-only composition choice.
|
||||
Every shipped composition binds the fork delegation tool to `backgroundMode: one-shot`: [the base bundle](../../../../packages/bundle/base/cordis.patch.yml), [the ACP example](../../../../examples/acp-agent/cordis.yml), and [the headless example](../../../../examples/headless-agent/cordis.yml). The base bundle leaves `run_in_background` available, because it mounts a task service; the two examples set `enableRunInBackground: false`, because they mount none and a one-shot background start would otherwise fail at call time on a missing `tasks` service.
|
||||
|
||||
One-shot children — foreground and background alike — are created through `SubagentService.start()`, which never enters the continuable activation-setup registry, so neither `report` nor its prompt section is installed. A forked one-shot child's system prompt and tool schemas therefore equal its parent's, apart from the `persona` and `toolFilter` deltas a deployment opts into per delegation tool.
|
||||
|
||||
@@ -38,7 +38,7 @@ The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reus
|
||||
|
||||
## Consequences
|
||||
|
||||
- No shipped composition creates a continuable forked child. The base bundle may return a one-shot Task id for `subagent_fork`, while the ACP and headless examples return the result to the caller's turn; `send_message` addresses only spawned children.
|
||||
- No shipped composition creates a continuable forked child; `subagent_fork` returns a result to its caller's turn, and `send_message` addresses only spawned children.
|
||||
- A forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona` or `toolFilter` on the fork delegation tool, so the token cost of seeding buys provider-side reuse again.
|
||||
- The fork provider's continuable path has no production caller and no assembled-composition coverage. It keeps its package-level tests, and the seam still accepts it, so a bundle or `--patch` overlay can reintroduce it with no code change and no warning.
|
||||
- `subagent_fork`'s model-visible schema changes: the continuable background wording is replaced by the one-shot task wording in the base bundle, and disappears entirely from the two examples. The affected keyless snapshot tool-schema sidecars are re-recorded in the same change.
|
||||
|
||||
@@ -12,7 +12,7 @@ fork 与 spawn 的唯一区别是 child 的 Session 会以 parent 已完成轮
|
||||
|
||||
## 决策
|
||||
|
||||
所有随附组合都把 fork 委派工具绑定为 `backgroundMode: one-shot`:[base 组合包](../../../../packages/bundle/base/cordis.patch.yml)、[ACP 示例](../../../../examples/acp-agent/cordis.yml)与[headless 示例](../../../../examples/headless-agent/cordis.yml)。三者都能使用通用 Task 运行时:base 组合包保留 `run_in_background`,两个示例则把 `enableRunInBackground: false` 作为显式的仅前台组装选择。
|
||||
所有随附组合都把 fork 委派工具绑定为 `backgroundMode: one-shot`:[base 组合包](../../../../packages/bundle/base/cordis.patch.yml)、[ACP 示例](../../../../examples/acp-agent/cordis.yml)与[headless 示例](../../../../examples/headless-agent/cordis.yml)。base 组合包保留 `run_in_background`,因为它挂载了 task 服务;两个示例设置 `enableRunInBackground: false`,因为它们都不挂载 task 服务,否则一次 one-shot 后台启动会在调用时因缺少 `tasks` 服务而失败。
|
||||
|
||||
one-shot child——前台与后台皆然——经由 `SubagentService.start()` 创建,该路径从不进入可继续的 activation setup 注册表,因此 `report` 与它的提示词 section 都不会被安装。于是一个 fork 出的 one-shot child 的系统提示词与工具 schema 与其 parent 相同,只差部署逐个委派工具主动选择的 `persona` 与 `toolFilter` 增量。
|
||||
|
||||
@@ -38,7 +38,7 @@ one-shot child——前台与后台皆然——经由 `SubagentService.start()`
|
||||
|
||||
## 后果
|
||||
|
||||
- 没有任何随附组合会创建可继续的 fork child。base 组合包中的 `subagent_fork` 可以返回 one-shot Task id,而 ACP 与 headless 示例会把结果返回给调用方的轮次;`send_message` 只寻址 spawn 出的 child。
|
||||
- 没有任何随附组合会创建可继续的 fork child;`subagent_fork` 把结果返回给调用方的轮次,而 `send_message` 只寻址 spawn 出的 child。
|
||||
- 除非部署在 fork 委派工具上配置了 `persona` 或 `toolFilter`,fork child 的请求前缀与其 parent 逐字节相同,因此初始内容的 token 成本重新换来了提供方侧的复用。
|
||||
- fork 提供方的可继续路径没有生产调用方,也没有整体组装层面的覆盖。它保留自己的包内测试,seam 也仍然接受它,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地把它重新引入。
|
||||
- `subagent_fork` 面向模型的 schema 发生变化:base 组合包中可继续的后台措辞被 one-shot 的 task 措辞取代,在两个示例中则完全消失。受影响的无密钥快照工具 schema 伴随文件在同一次改动中重新记录。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md
|
||||
README.md: 556cde7da4467659d02709c43325e60fa7de6a74
|
||||
README.zh.md: bda00d7be4467e739604eeba0020850cb1a99d50
|
||||
README.md: f0f6e39820920e0b266802585b74a953a068828a
|
||||
README.zh.md: 52baee6c1158bb38505fd0cf454a820853e0cf89
|
||||
|
||||
@@ -82,7 +82,7 @@ Independent of the parent request cache. Reuse depends only on Claude Code's own
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a bounded completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
|
||||
Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ Claude Code 子任务会在一个全新的 SDK query 中接收独立文本任务
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达有界完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
|
||||
通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
|
||||
|
||||
#### 对 token 的影响
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md
|
||||
README.md: 6a882c128ebd372ee148a4315a237d4eacf6d42b
|
||||
README.zh.md: beb5411f6a449efc7a45c171d217f98657805740
|
||||
README.md: 570a2346b121a3d100600285f225a0906c38aeec
|
||||
README.zh.md: 9fded2ceb35b276f0c698c937720ea13e13c6acc
|
||||
|
||||
@@ -76,7 +76,7 @@ Independent of the parent request cache. Reuse depends only on Codex's own provi
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a bounded completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
|
||||
Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Codex 子任务会在一个全新的临时线程中,以单个轮次接收这
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达有界完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Codex 的过程说明、推理(reasoning)、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
|
||||
通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Codex 的过程说明、推理(reasoning)、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
|
||||
|
||||
#### 对 token 的影响
|
||||
|
||||
|
||||
@@ -113,7 +113,9 @@ async function settleStart(start: Promise<SubagentRun>, signal: AbortSignal): Pr
|
||||
try {
|
||||
return await settleRun(await start)
|
||||
} catch (error: unknown) {
|
||||
return signal.aborted
|
||||
// Product providers aggregate startup and rollback failures. Cancellation
|
||||
// must not turn a failed cleanup into a cleanly killed Task.
|
||||
return signal.aborted && !(error instanceof AggregateError)
|
||||
? { status: 'killed' }
|
||||
: { status: 'failed', detail: String(error) }
|
||||
}
|
||||
|
||||
@@ -939,6 +939,48 @@ describe('dsh-tool-subagent background mode', () => {
|
||||
expect(text(output)).toBe('(no new output)\n[status: killed]')
|
||||
})
|
||||
|
||||
it('reports startup rollback failure after cancellation as a failed task', async () => {
|
||||
const ctx = await backgroundSetup({ provider: 'mock' })
|
||||
const parent = ownerAgent(ctx, 'sess-parent')
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'broken-start-rollback',
|
||||
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
|
||||
inheritsParentContext: false,
|
||||
start: request => new Promise((_resolve, reject) => {
|
||||
request.signal.addEventListener('abort', () => {
|
||||
reject(new AggregateError(
|
||||
[new Error('startup aborted'), new Error('cleanup failed')],
|
||||
'startup failed and cleanup also failed',
|
||||
))
|
||||
}, { once: true })
|
||||
}),
|
||||
})
|
||||
tool.apply(ctx, { provider: 'broken-start-rollback', toolName: 'subagent_broken_rollback' })
|
||||
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('broken-rollback-start'),
|
||||
name: 'subagent_broken_rollback',
|
||||
arguments: { description: 'broken rollback', prompt: 'p', run_in_background: true },
|
||||
agent: parent,
|
||||
})
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('broken-rollback-kill'),
|
||||
name: 'task_kill',
|
||||
arguments: { task_id: 'subagent-1' },
|
||||
agent: parent,
|
||||
})
|
||||
const output = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('broken-rollback-output'),
|
||||
name: 'task_output',
|
||||
arguments: { task_id: 'subagent-1', wait: true },
|
||||
agent: parent,
|
||||
})
|
||||
expect(text(output)).toContain('[status: failed, AggregateError: startup failed and cleanup also failed]')
|
||||
})
|
||||
|
||||
it('forwards task_kill reasons through the run signal (and defaults one when absent)', async () => {
|
||||
// Use a provider that remains live until its signal is aborted.
|
||||
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } })
|
||||
|
||||
Reference in New Issue
Block a user