diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml new file mode 100644 index 0000000000..ef424471d8 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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: 030bed3c1b516a54afd5f97b00000ed7ceaf64d8 +2026-08-10-fork-children-stay-one-shot.zh.md: 4b033a70c3674315c491e85be67a1d357aeac51b diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md new file mode 100644 index 0000000000..030bed3c1b --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md @@ -0,0 +1,49 @@ +# Agent Note: Forked children stay one-shot + +Status: implemented + +English | [中文](2026-08-10-fork-children-stay-one-shot.zh.md) + +## Problem + +Fork's only difference from spawn is that the child Session is seeded with the parent's completed-turn prefix ([subagent-fork](../../../../packages/subagent/subagent-fork/README.md)). That seed costs real tokens — the inherited history is re-sent in every child request — and its one concrete payoff is provider-side prefix reuse: under the same provider and model, a child request whose leading bytes are identical to the parent's re-prefills none of the shared span. Anything a child scope adds *ahead* of the inherited history spends that payoff, because reuse stops at the first differing byte. + +The child-scoped `report` return channel is now the largest such addition, and since [the report obligation](../feature/2026-08-06-continuable-child-report-obligation.md) it is two deltas rather than one: the `report` tool schema and the `tool:report` system-prompt section. Both live in the request head — the system block and the tool block precede every message — so a continuable forked child invalidates reuse before the first inherited turn and re-prefills the whole transcript it was forked to reuse. That composition pays fork's duplication cost and collects none of its benefit, while the parent still holds a reusable prefix the child could have shared. + +## 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). 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. + +`spawn` keeps `backgroundMode: continuable`. Continuable children and the report obligation ship unchanged for the provider whose child starts with no inherited prefix to protect, so this decision costs the report channel nothing. + +### The restriction is composition, not code + +`ForkProvider.prepareContinuable` stays implemented and `ctx.subagents.startContinuable()` still accepts `fork`; only the shipped `cordis.yml` rows changed. `tool-subagent` knows both the provider's `inheritsParentContext` and its own `backgroundMode` at mount, so a load-time rejection of the pair was available and is deliberately not added: the pair is not wrong in general. It is wrong only while a child-scope delta precedes inherited history, and the package that creates that delta — [`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.md) — is separately installable and, by its own design, invisible to `tool-subagent`. A deployment that omits the report package can run continuable forked children with the prefix intact. Encoding one roster's consequence as a delegation-tool invariant would make the tool assert something it cannot observe. + +The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reuse)` marker on `prepareContinuable` itself, the one method the shipped compositions do not call, and tracked as issue #2124: continuable fork reopens when a child's system prompt and tool schemas can match its parent's byte for byte. + +## Alternatives considered + +**Reject `inheritsParentContext` + `continuable` at mount.** A loud load-time failure would prevent silent reintroduction, which is what the configuration change cannot do. Rejected because the delegation tool cannot see the report package and the combination is legitimate without it; the invariant would be false for a deployment that never installs a child-scope delta, and `tool-subagent` would be asserting a fact owned by the roster. + +**Stop mounting the fork provider at all.** This was the broader form of the restriction. Rejected because foreground fork *is* the prefix-reusing case and is untouched by the report channel, so a full ban gives up the capability without buying anything the one-shot binding does not already buy — and would leave no shipped composition exercising session seeding. + +**Ship continuable forked children and accept the loss.** Rejected because the loss is total rather than marginal: reuse breaks ahead of the inherited history, so the child pays full prefill on a transcript it duplicated for the sole purpose of not paying it. A deployment that wants a long-lived child with no inherited context already has `spawn`. + +**Make `report` visible to every Agent.** A global registration would restore byte-identical prefixes by giving parent and child the same schema and section. Rejected because roots, one-shot children, remote children, and agentless callers would advertise a tool with no derivable recipient, and execution-time rejection would make schema visibility disagree with authority — the scope-local decision the [report tool Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.md) already settled. + +**Install the child-scope deltas after the inherited history.** Rejected as unrepresentable: the system prompt and the tool schemas are request-head structures in every provider's wire format, so no ordering within them can place a child-only addition behind the message list. + +## Consequences + +- 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. +- The report obligation's reach narrows to spawned children in shipped deployments. Its default `wakeup` scheduling, authority model, and coverage are unchanged. + +### Accepted risks + +The constraint lives in three configuration files and a code comment, not in a gate. A future bundle row or profile patch can set `backgroundMode: continuable` on a fork tool and silently reintroduce the prefix loss; nothing fails loud. That is the accepted cost of not encoding one roster's consequence into `tool-subagent`. diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md new file mode 100644 index 0000000000..4b033a70c3 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md @@ -0,0 +1,49 @@ +# Agent Note: fork 出的 child 保持 one-shot + +Status: implemented + +[English](2026-08-10-fork-children-stay-one-shot.md) | 中文 + +## 问题 + +fork 与 spawn 的唯一区别是 child 的 Session 会以 parent 已完成轮次的前缀作为初始内容(见 [subagent-fork](../../../../packages/subagent/subagent-fork/README.md))。这份初始内容有实打实的 token 成本——继承的历史会在 child 的每次请求中重新发送——而它唯一确定的回报是提供方侧的前缀复用:在提供方与模型相同的前提下,起始字节与 parent 逐字节相同的 child 请求,无需为这段共享区间重新预填充。任何由 child 作用域添加在继承历史*之前*的内容都会消耗掉这份回报,因为复用在第一个不同字节处即告停止。 + +作用域局部的 `report` 返回通道现在是此类添加中最大的一项,而自[report 义务](../feature/2026-08-06-continuable-child-report-obligation.md)起它是两项而非一项增量:`report` 工具 schema,以及 `tool:report` 系统提示词 section。两者都位于请求头部——系统块与工具块先于所有消息——因此一个可继续的 fork child 会在第一条继承轮次之前就使复用失效,并重新预填充它当初 fork 就是为了复用的整份 transcript(文本记录)。这种组合付出了 fork 的复制成本却收不到它的收益,而 parent 手上仍握着一份 child 本可共享的可复用前缀。 + +## 决策 + +所有随附组合都把 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` 增量。 + +`spawn` 保持 `backgroundMode: continuable`。对于 child 起步时本就没有继承前缀需要保护的那个提供方,可继续 child 与 report 义务随附行为不变,因此本决策没有让 report 通道付出任何代价。 + +### 该限制在于组合,不在于代码 + +`ForkProvider.prepareContinuable` 仍然实现完好,`ctx.subagents.startContinuable()` 也仍接受 `fork`;改动的只有随附的 `cordis.yml` 行。`tool-subagent` 在挂载时同时知道提供方的 `inheritsParentContext` 与自身的 `backgroundMode`,因此一个加载期拒绝该组合的检查是可行的,而这里刻意不加:该组合并非普遍错误。它只在某个 child 作用域增量位于继承历史之前时才是错的,而产生该增量的包——[`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.md)——是独立安装的,并且按其自身设计对 `tool-subagent` 不可见。一个不安装 report 包的部署可以在前缀完好的前提下运行可继续的 fork child。把某一份插件清单的后果写成委派工具的不变量,会让该工具断言它无法观察到的事实。 + +重新开放的条件记录为 `prepareContinuable` 方法上的 `TODO(fork-continuable-prefix-reuse)` 标记——随附组合不调用这个方法——并由 issue #2124 跟踪:当 child 的系统提示词与工具 schema 能与其 parent 逐字节一致时,可继续 fork 即可重新开放。 + +## 备选方案 + +**在挂载时拒绝 `inheritsParentContext` 与 `continuable` 的组合。** 一次响亮的加载期失败可以阻止悄然的重新引入,而配置改动做不到这一点。否决的原因是委派工具看不到 report 包,且在没有它时该组合是合法的;对于从不安装任何 child 作用域增量的部署,这个不变量是假的,而 `tool-subagent` 会去断言一件由插件清单拥有的事实。 + +**干脆不挂载 fork 提供方。** 这是该限制更彻底的形式。否决的原因是前台 fork *正是*复用前缀的那种情形,且不受 report 通道影响,因此全面禁用会在不换来任何 one-shot 绑定尚未换来的东西的同时放弃该能力——并且随附组合将没有任何一个演练 session 初始内容。 + +**照常随附可继续的 fork child 并接受这份损失。** 否决的原因是这份损失是全额而非边际的:复用在继承历史之前就已中断,于是 child 为一份自己复制过来、目的恰恰是不必付费的 transcript 付了全额预填充。想要一个没有继承上下文的长期 child 的部署,本来就有 `spawn`。 + +**让 `report` 对每个 Agent 可见。** 全局注册会通过让 parent 与 child 拥有相同的 schema 与 section 来恢复逐字节相同的前缀。否决的原因是根 agent、one-shot child、远端 child 与无 agent 调用方都会宣告一件推导不出收件方的工具,而执行期拒绝会让 schema 可见性与权限彼此矛盾——这正是[report 工具 Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.md)已经定下的作用域局部决策。 + +**把 child 作用域增量安装到继承历史之后。** 否决的原因是它无法表达:在每个提供方的协议格式中,系统提示词与工具 schema 都是请求头部结构,因此它们内部的任何排序都无法把仅属于 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 伴随文件在同一次改动中重新记录。 +- 在随附部署中,report 义务的覆盖范围收窄到 spawn 出的 child。它的 `wakeup` 默认调度、权限模型与覆盖均保持不变。 + +### 已接受的风险 + +该限制存在于三个配置文件与一处代码注释中,而不在门禁里。未来某个组合包行或 profile 补丁可以在 fork 工具上设置 `backgroundMode: continuable`,从而悄然重新引入前缀损失;没有任何东西会失败得很响亮。这就是不把某一份插件清单的后果写入 `tool-subagent` 所接受的代价。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-list-agents-residency-vocabulary.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-06-list-agents-residency-vocabulary.i18n.yaml new file mode 100644 index 0000000000..5f01146dbe --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-06-list-agents-residency-vocabulary.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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/bug-fix/2026-08-06-list-agents-residency-vocabulary.md +2026-08-06-list-agents-residency-vocabulary.md: 01fff958921465909f5cd300dc355fdac6ec0772 +2026-08-06-list-agents-residency-vocabulary.zh.md: d68b9857de23ccb0b68f0bfafba153fde63303a7 diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-list-agents-residency-vocabulary.md b/.agents/notes/implemented/bug-fix/2026-08-06-list-agents-residency-vocabulary.md new file mode 100644 index 0000000000..01fff95892 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-06-list-agents-residency-vocabulary.md @@ -0,0 +1,40 @@ +# Agent Note: `list_agents` uses `ready` for resumable children + +Status: implemented + +English | [中文](2026-08-06-list-agents-residency-vocabulary.zh.md) + +## Problem + +`list_agents` projected a continuable child's process residency as `running | idle | complete`. `complete` reads as a terminal unit of work with a result somewhere, but the underlying fact says only that no Activation is resident: the conversation is intact, `send_message` can continue it, and nothing about the child's outcome is being claimed. A model that reads `complete` reasonably looks for a result to collect or sends replacement work to a conversation it believes has ended. + +The word is especially misleading alongside [manager-owned settlement delivery](../feature/2026-08-06-manager-owned-subagent-settlement-delivery.md). Completion reaches the parent as a notice; listing exists to recall durable conversations, not to poll for that notice. + +## Decision + +The model-facing projection reports `running | idle | ready`: + +- **`running`** means the resident Agent has an active driver. +- **`idle`** means the Agent is resident between turns and may be waiting on agents it started. +- **`ready`** means only the durable conversation remains. `send_message` starts the next turn on the same conversation; the status is resumable rather than terminal and does not mean a result is waiting to be collected. + +The tool description states those distinctions and directs the model away from polling: it says the parent is told when a child finishes and that listing is for recalling which children it started. `send_message` remains the authoritative delivery check because either snapshot may race another process or a later message. + +The service layer is unchanged. `SubagentListEntry.activity` retains `'running' | 'inactive'`, which accurately describes corpus residency for consumers such as a UI. The model-facing adapter maps `inactive` to `ready` because that word communicates the action available to the model without inventing an outcome. + +## Alternatives considered + +**Keep `complete` and qualify it in the description.** A description saying that `complete` does not mean complete fights the rendered status on every read. The line the model scans must carry the correct distinction itself. + +**Use `active | dormant`.** This removes the useful distinction between a resident Agent that is between turns and a storage-only conversation, and makes the storage-only state sound unavailable. `ready` states the useful fact: the same conversation accepts another turn. + +**Drop the status entirely.** Residency remains useful when a parent decides whether to send more work. Removing it trades one misleading status for no signal. + +**Rename the service activity values.** `running | inactive` is correct at the service layer and has non-model consumers. Renaming it would churn a general contract to fix one adapter's presentation; the [durable catalog note](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.md) continues to own that service vocabulary. + +## Consequences + +- The rendered line uses ` [running] —