docs(subagent): correct report acceptance semantics for closing parents

The tool README claimed a "missing, disposed, or closing parent" fails
the call — but acceptance is governed by the parent's registry presence:
`resolveReportParent` only rejects when the durable parent id is absent
from the registry, so a host-owned parent already in disposal but still
registered still accepts (the pinned host-disposing-parent behavior).
The claim misled callers into treating disposal state as a delivery
signal.

Restate the contract in both languages: absence from the registry is the
only `PARENT_UNAVAILABLE` case, and a failed tool call does not prove
non-delivery — a later `tools/post-execute` veto can fail a call whose
report was already accepted, so the durable child transcript remains the
recovery source.

Adds a regression test pinning acceptance into a host-disposing but
still-registered parent, and rejection after disposal settles.
This commit is contained in:
Tianyi Cui
2026-08-02 12:31:54 +08:00
parent 5da2ac5835
commit 3114947324
3 changed files with 19 additions and 2 deletions

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
The optional child-scoped `report` tool is a thin adapter over `ctx.subagents.reportFrom()`. It gives every continuable in-process child a return channel to the Agent that started it. The package registers a continuable-child setup contribution instead of a global tool, so `report` exists only inside those children. Roots, one-shot subagents, remote subagent providers, sibling scopes, and agentless tool execution never present or execute it. Installing this package grants only that child-scoped capability; the parent-to-child direction remains the independent [`@deepseek-ai/dsh-tool-subagent-control`](../tool-subagent-control/README.md), and continuable mode depends on neither package.
A child may call `report` zero or many times in one turn. A successful call neither concludes the turn, settles the Activation, nor prevents later parent follow-ups, and finishing a turn never reports automatically. The tool accepts no recipient: `exec.agent` is the sender's exact live Agent and the authority credential, and the service derives the sole recipient from that child's durable `parentSession`. Success returns the stable `MessageId` of the parent-accepted message, not a read receipt, an inbox-occurrence id, a parent-log acknowledgement, a turn-completion receipt, or a persistence flush. A missing, disposed, or closing parent fails the call with `direct parent is not live; report was not delivered`; the service performs no injection, parent cold resume, or offline mailbox write, so the durable child transcript remains the recovery source.
A child may call `report` zero or many times in one turn. A successful call neither concludes the turn, settles the Activation, nor prevents later parent follow-ups, and finishing a turn never reports automatically. The tool accepts no recipient: `exec.agent` is the sender's exact live Agent and the authority credential, and the service derives the sole recipient from that child's durable `parentSession`. Success returns the stable `MessageId` of the parent-accepted message, not a read receipt, an inbox-occurrence id, a parent-log acknowledgement, a turn-completion receipt, or a persistence flush. A parent absent from the registry fails the call with `direct parent is not live; report was not delivered` — acceptance is governed by registry presence, so a parent already in host-owned disposal but still registered still accepts. The service performs no injection, parent cold resume, or offline mailbox write; the durable child transcript remains the recovery source, and a failed tool call does not prove non-delivery (a later `tools/post-execute` veto can fail a call whose report was already accepted).
`reportDelivery` selects parent scheduling for every accepted report. `quiet` (the default) uses `parent.inject()`, adding model-facing context without starting a parent model request: an idle parent's append completes before the call returns, while a report reaching an admitting or running parent stages for the next safe log position. `wakeup` uses `parent.followup()`, creating exactly one ordinary later parent turn and waking a parked parent driver; it never steers an open turn. This is deployment scheduling policy, so the model-facing schema cannot select or override it per call.

View File

@@ -4,7 +4,7 @@
可选的子级作用域 `report` 工具是 `ctx.subagents.reportFrom()` 之上的轻量适配器。它为每个可继续的进程内子级提供一条返回通道,指向启动该子级的 Agent智能体。本包package注册的是可继续子级设置贡献而不是全局工具因此 `report` 只存在于这些子级内部。根 Agent、一次性 subagent、远程 subagent 提供方、同级作用域以及不关联 Agent 的工具执行都不会提供或执行它。安装本包只授予这项子级作用域功能;父到子方向仍由独立的 [`@deepseek-ai/dsh-tool-subagent-control`](../tool-subagent-control/README.md) 负责,可继续模式不依赖这两个包中的任一个。
子级可以在一个轮次中调用 `report` 零次或多次。调用成功既不会结束轮次或结算 Activation也不会阻止父级后续消息轮次结束也绝不会自动上报。该工具不接受接收方参数`exec.agent` 是发送方准确的实时 Agent也是权限凭据服务根据该子级持久化的 `parentSession` 推导唯一接收方。成功时返回父级已接受消息的稳定 `MessageId`不表示已读回执、inbox 中该次出现的 id、父级日志确认、轮次完成回执或持久化刷盘。父级不存在、已 dispose资源释放或正在关闭时本次调用失败并返回 `direct parent is not live; report was not delivered`;服务不会执行注入、父级冷恢复或离线 mailbox 写入,因此持久化子级 transcript文本记录仍是恢复真源。
子级可以在一个轮次中调用 `report` 零次或多次。调用成功既不会结束轮次或结算 Activation也不会阻止父级后续消息轮次结束也绝不会自动上报。该工具不接受接收方参数`exec.agent` 是发送方准确的实时 Agent也是权限凭据服务根据该子级持久化的 `parentSession` 推导唯一接收方。成功时返回父级已接受消息的稳定 `MessageId`不表示已读回执、inbox 中该次出现的 id、父级日志确认、轮次完成回执或持久化刷盘。接受与否由父级在注册表中的存在性决定:父级不在注册表时,调用失败并返回 `direct parent is not live; report was not delivered`已开始宿主 dispose 但仍在注册表中的父级仍会接受。服务不会执行注入、父级冷恢复或离线 mailbox 写入持久化子级 transcript文本记录仍是恢复真源,且工具调用失败不能证明未送达(后续 `tools/post-execute` 否决可能让报告已被接受的调用以失败结束)
`reportDelivery` 为每条已接受的报告选择父级调度方式。`quiet`(默认值)使用 `parent.inject()`,在不启动父级模型请求的情况下添加面向模型的上下文:父级空闲时,追加操作会在调用返回前完成;报告到达正在准入或运行的父级时,则会暂存到下一个安全日志位置。`wakeup` 使用 `parent.followup()`,准确创建一个普通的后续父级轮次,并唤醒停驻的父级驱动;它绝不会对正在运行的轮次进行 steering中途引导。这是部署调度策略因此面向模型的 schema 不能在单次调用中选择或覆盖该策略。

View File

@@ -350,6 +350,23 @@ describe('dsh-tool-subagent-report', () => {
expect(ctx.agents.list().map(agent => agent.id)).toEqual([parent.id])
})
it('accepts a report into a host-disposing but still-registered parent', async () => {
const { ctx } = await setup()
const parentHandle = await ctx.agents.create({
sessionId: SessionId('disposing-parent'),
agentOptions: { provider: 'mock', model: 'mock' },
})
const { child } = await startChild(ctx, parentHandle.agent)
// Host-owned disposal starts asynchronously; the parent stays registered
// until quiescence, and registry presence — not disposal state — is the
// acceptance gate (pins the README contract).
const disposing = parentHandle.dispose()
const accepted = await callReport(ctx, child, 'during-close')
expect(accepted.isError).toBe(false)
await disposing
expect((await callReport(ctx, child, 'after-close')).isError).toBe(true)
})
it('keeps the namespace plugin shape and validates its default', () => {
expect('default' in tool).toBe(false)
expect(tool.name).toBe('tool-subagent-report')