feat(subagent): deliver continuable child settlement to parents

A continuable child that stopped without reporting — an error, a token
ceiling, cancellation, teardown — left its parent nothing to act on.
The continuation manager now delivers an unconditional settlement
notice to the durable direct parent before releasing ownership, folding
consumed work (foldConsumedWork supersedes findLastMessageTurnEnd) so a
claimed-but-unrun prompt reads as aborted rather than completed, waking
an idle parent, steering a busy one, and never waking a closing tree.
This commit is contained in:
Hypatia May
2026-08-11 12:31:49 +08:00
parent 76cf6cbd0b
commit 85dd22fd4f
102 changed files with 2139 additions and 345 deletions
@@ -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/feature/2026-07-21-log-backed-session-titles.md
2026-07-21-log-backed-session-titles.md: 47025ba7e72ea6746d6f4428a81d6d754a8dcd03
2026-07-21-log-backed-session-titles.zh.md: d7e12875354ca1fc66f0035020c7ffe2ddaa9354
2026-07-21-log-backed-session-titles.md: 46128a3e53d6617c9fa7a0c27d1ac01acfd1d9c1
2026-07-21-log-backed-session-titles.zh.md: 2cdf5c22d6b2a28b27a68472619ced4b7dedd106
@@ -44,7 +44,7 @@ Automatic provider failures are nonfatal warnings and retain the latest title. E
A fork inherits seed title events unchanged, like the rest of its source log — a pinned (user-sourced) title stays pinned in the child until an explicit refresh. The first-message provider does not automatically retitle a fork. The all-messages provider may append a child-owned revision after a later child prompt, using inherited and new eligible messages.
`ctx.sessionQuery.readTitle()` folds one live-preferred or persisted log without loading titles during `listSessions()`. The TUI uses the latest title as its header subtitle and sets the terminal window title to `<session title> — <configured product title>` after terminal-safe rendering. The Web host folds the same log state into a validated mux control frame after each attached-session subscription baseline and immediately after forwarding a live raw title event. The browser retains only newer title event seqs even when the control frame precedes list or session-instance creation; sidebar labels, search, breadcrumbs, and the browser title then react to the projected revision. `session.list` remains metadata-only, so a cold persisted session uses the cwd basename or id until opening or resuming it attaches the log. The browser title uses `<session title> — <existing HTML title>` only for a selected titled session and otherwise preserves the product title. Consumers reporting agent completion use the core `findLastMessageTurnEnd()` fold, so a later between-turn title record cannot replace the preceding message-triggered outcome.
`ctx.sessionQuery.readTitle()` folds one live-preferred or persisted log without loading titles during `listSessions()`. The TUI uses the latest title as its header subtitle and sets the terminal window title to `<session title> — <configured product title>` after terminal-safe rendering. The Web host folds the same log state into a validated mux control frame after each attached-session subscription baseline and immediately after forwarding a live raw title event. The browser retains only newer title event seqs even when the control frame precedes list or session-instance creation; sidebar labels, search, breadcrumbs, and the browser title then react to the projected revision. `session.list` remains metadata-only, so a cold persisted session uses the cwd basename or id until opening or resuming it attaches the log. The browser title uses `<session title> — <existing HTML title>` only for a selected titled session and otherwise preserves the product title. Consumers reporting agent completion use the core `foldConsumedWork()` fold, so a later between-turn title record cannot replace the preceding message-triggered outcome.
## Alternatives considered
@@ -44,7 +44,7 @@ Status: implemented
与源日志的其他部分相同,fork 会原样继承作为种子的标题事件——被钉住(user 来源)的标题在子会话中保持钉住,直到显式 refresh。首消息提供方不会自动为 fork 重新生成标题。全部消息提供方可以在子会话出现后续提示词后追加一项归子会话所有的修订,并使用继承的合格消息和新增的合格消息。
`ctx.sessionQuery.readTitle()` 会折叠一份实时优先或已持久化的日志,而不会在 `listSessions()` 期间加载标题。TUI 使用最新标题作为其标题栏副标题,并在完成终端安全渲染后,将终端窗口标题设置为 `<session title> — <configured product title>`。Web host 会在每个已附加会话的订阅基线之后,以及转发实时原始标题事件后立即,将同一份日志状态折叠为经过校验的 mux 控制帧。即使控制帧先于列表或会话实例创建抵达,浏览器也只保留标题事件 seq 较新的版本;侧边栏标签、搜索、面包屑和浏览器标题会随投影后的修订更新。`session.list` 仍只包含元数据,因此尚未打开的持久化会话会继续以 cwd 基名或 id 作为回退,直至打开或恢复会话时附加其日志。浏览器仅在选中已有标题的会话时将标题设置为 `<session title> — <existing HTML title>`,否则保留产品标题。报告 agent 完成情况的消费方使用核心的 `findLastMessageTurnEnd()` 折叠逻辑,因此后续的轮次间标题记录无法取代此前由消息触发的结果。
`ctx.sessionQuery.readTitle()` 会折叠一份实时优先或已持久化的日志,而不会在 `listSessions()` 期间加载标题。TUI 使用最新标题作为其标题栏副标题,并在完成终端安全渲染后,将终端窗口标题设置为 `<session title> — <configured product title>`。Web host 会在每个已附加会话的订阅基线之后,以及转发实时原始标题事件后立即,将同一份日志状态折叠为经过校验的 mux 控制帧。即使控制帧先于列表或会话实例创建抵达,浏览器也只保留标题事件 seq 较新的版本;侧边栏标签、搜索、面包屑和浏览器标题会随投影后的修订更新。`session.list` 仍只包含元数据,因此尚未打开的持久化会话会继续以 cwd 基名或 id 作为回退,直至打开或恢复会话时附加其日志。浏览器仅在选中已有标题的会话时将标题设置为 `<session title> — <existing HTML title>`,否则保留产品标题。报告 agent 完成情况的消费方使用核心的 `foldConsumedWork()` 折叠逻辑,因此后续的轮次间标题记录无法取代此前由消息触发的结果。
## 考虑过的替代方案
@@ -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/feature/2026-08-06-continuable-child-report-obligation.md
2026-08-06-continuable-child-report-obligation.md: ba52c72a94350d637699f778342093e71f2ed442
2026-08-06-continuable-child-report-obligation.zh.md: 5c4178e0e095dac5b1e50ad92ef75d0138940439
2026-08-06-continuable-child-report-obligation.md: f152ec1b8c353f094f2ba70785112eb1e165c510
2026-08-06-continuable-child-report-obligation.zh.md: 4ec17e4642ffac385e6ce5464f41a3f4b3bebdbf
@@ -29,7 +29,7 @@ The section is registered on the child's own scope, the same mechanism [child co
Nothing rejects a child that never reports. No runtime path inspects whether a report was sent, and `report` still accepts zero or many calls per turn. The change is model-facing wording plus a scheduling default; the service authority, acknowledgement, and recovery contracts are unchanged.
That boundary is deliberate: prompt text can only reach a child that is still running its own loop. A child stopped by an error, a token ceiling, cancellation, or teardown never gets the chance to comply. Covering those cases needs an account the runtime keeps itself, which this package cannot supply and does not claim to.
That boundary is deliberate: prompt text can only reach a child that is still running its own loop. A child stopped by an error, a token ceiling, cancellation, or teardown never gets the chance to comply, which is why the runtime keeps its own account of settlement rather than trusting this instruction ([manager-owned settlement delivery](2026-08-06-manager-owned-subagent-settlement-delivery.md)).
### Snapshot coverage
@@ -57,4 +57,4 @@ The assembled ACP `subagent-report` scenario now exercises the shipped default:
Waking by default amplifies model work in deep trees. The deployment owns that through `reportDelivery`, and the amplification is bounded by one turn per accepted report.
A child can still finish without reporting, and this change cannot detect it. Closing that case needs the runtime's own settlement account, which this package does not own.
A child can still finish without reporting, and this change cannot detect it. Only the runtime's own [settlement account](2026-08-06-manager-owned-subagent-settlement-delivery.md) closes that case.
@@ -29,7 +29,7 @@ Status: implemented
没有任何东西会拒绝一个从不上报的 child。没有任何运行时路径会检查是否发送过报告,`report` 仍接受一个轮次中调用零次或多次。本次改动是面向模型的措辞加上一个调度默认值;服务权限、确认与恢复契约都保持不变。
这条边界是刻意划定的:提示词文本只能到达仍在运行自身循环的 child。被错误、token 上限、取消或拆卸终止的 child 根本没有机会遵守。覆盖这些情形需要由运行时自己保存的记账,而本包无法提供,也不声称提供
这条边界是刻意划定的:提示词文本只能到达仍在运行自身循环的 child。被错误、token 上限、取消或拆卸终止的 child 根本没有机会遵守,因此运行时自己记录结算这件事,而不是信任这条指令(见[由管理器负责的结算投递](2026-08-06-manager-owned-subagent-settlement-delivery.md)
### 快照覆盖
@@ -57,4 +57,4 @@ Status: implemented
默认唤醒会在深层树中放大模型工作量。部署通过 `reportDelivery` 掌握该取舍,且放大幅度以每条被接受报告一个轮次为界。
child 仍可能不上报就结束,本次改动无法检测这一点。补上这一情形需要运行时自己的结算记账,而本包并不拥有它
child 仍可能不上报就结束,本次改动无法检测这一点。只有运行时自己的[结算记账](2026-08-06-manager-owned-subagent-settlement-delivery.md)才能补上这一情形
@@ -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/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md
2026-08-06-manager-owned-subagent-settlement-delivery.md: e06fe2b4ca7dd9a975f70524979de09d13cdcf79
2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 77e8b5e4bae9b05e08bf2c4a4bf997b688cd1157
@@ -0,0 +1,103 @@
# Agent Note: Settlement delivery belongs to the continuation manager
Status: implemented
English | [中文](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)
## Problem
Continuable background delegation was the one asynchronous operation a model could start but could not reach the end of. Every other shape has a retrieval primitive or a return value: a background bash command and a one-shot background subagent both settle through a Task that `task_output(wait: true)` can block on, a workflow and a foreground subagent return their result to the caller. A continuable background child returned only its durable id, and nothing existed that a parent could wait on or would be handed.
[The report obligation](2026-08-06-continuable-child-report-obligation.md) closed the cooperative half of that gap by instructing the child to report before it finishes. Instruction cannot close the rest. A child stopped by a token ceiling, a model failure, cancellation, or teardown never reaches the point where it could comply — not rarely, but never — and those are precisely the endings a waiting parent most needs to hear about. The observable downstream symptoms were parents busy-polling `list_agents`, re-sending messages to children that had already settled, and deployments abandoning `subagent` for `workflow` because a workflow at least returns something.
The signal already existed. `subagent/end` has carried `stopReason` and `lastAssistantMessage` since continuable Activations shipped. What was missing was any consumer that turned it into context the parent's model could see.
## Decision
The continuation manager delivers the account itself, from inside the disposal transaction that ends the Activation.
When a resident Activation settles, `notifySettlement()` resolves the child's durable direct parent and sends it one user-role message: the epoch's outcome as a sentence the parent can act on, then the child's final assistant content, or a statement that it produced none. Delivery is unconditional for every child whose id a caller actually received. It does not consult whether the child reported, and it keeps no bookkeeping that could make the promise conditional — that unconditionality is what lets `tool-subagent` tell the model "you are told when it finishes, so never poll or wait on it" and have that be true. A materialization rolled back before its first accepted message stays silent, because the caller was told that child was not established.
### Provenance
The notice carries `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`. It is deliberately not the existing `subagent-report` kind. A report is content the child chose; this is the runtime stating what became of the child. Merging them would credit the child with words it never wrote, and would make a durable log unable to distinguish "the child said it was done" from "the harness observed that it stopped". The `notice` form also gives a UI the collapsed one-line presentation this message wants, where `relay` would present it as correspondence.
### Two ordering rules, and why the manager owns them
An external `ctx.on('subagent/end')` listener looks more decoupled and is wrong. `SubagentRunEndInfo` names no parent, the child handle is already disposed when the edge fires so the parent cannot be recovered from it, and the ownership release that wakes the parent's own settlement watcher has already run. The manager holds the parent reference throughout disposal, so none of those obstacles exist for it.
**The send happens before `releaseOwnership`.** At that point the parent still counts this child, so `stateOf(parent)` is `waiting` and the parent is structurally unable to be judged settled. Delivering after the release instead races a watcher that resumes one microtask later, finds itself childless and quiet, and disposes an Agent whose `cancel()` clears the very inbox the notice is sitting in. The failure mode is a silently missing message with no error anywhere.
**A resident parent receives it through `admitWaking`.** Registering the message id before the synchronous send is what keeps the window between `followup()` and the microtask that admits it from being read as quiescence. This is not belt-and-braces over the first rule: `Agent.status` folds context maintenance into `idle`, and a waking send behind maintenance only arms a deferred wake, so a parent compacting its context is judged quiet by both `status` and the owned-child set the moment the release lands.
Both rules are pinned by tests that fail when the ordering is reversed or the accounting removed.
### Scheduling
An idle parent gets one ordinary later turn. A busy parent is steered into its nearest step boundary, because `Inbox.claim()` takes the whole next-step batch at one boundary: four children settling together then cost one step rather than four turns. Steering rather than injecting is deliberate — the wake is a no-op while the driver is running, and it closes the window where a driver retires between the status read and the send, which would strand the notice unclaimed until something unrelated woke the parent. This is a correctness rule, not a deployment preference, so it is not a `Config` field.
One `running` parent is not steerable: one whose turn is already cancelled but has not yet exited. `Agent.send()` redirects waking input submitted after cancellation to the next turn, latches the wake, and replays it once the cancelled driver converges — except for a disposal cancellation, which never latches and belongs to the teardown rule below. The notice therefore still opens its own turn without waiting for unrelated input; the cost is a redirected turn boundary, not the message.
**A parent whose own teardown began gets no wake.** Waking is not a queue operation: `Agent.followup()` on a quiescent Agent starts a turn, and `cancel()` on an idle Agent is a documented no-op that does not arm against a later one. Every teardown path therefore ends with a live, cancelled, still-registered parent — `drainContinuableDescendants()` is called by the ACP bridge between cancelling its session agents and disposing them — so an unguarded notice starts a real model request on an Agent about to be destroyed, once per tree layer, because each layer's own notice then wakes the layer above it. `notifySettlement()` asks the same question `assertAdmitting()` asks (is this lineage's continuable admission closed?) and injects instead. Injection is not a durable mailbox — Accepted risks records what the parent's own disposal then does to it — but it is the only send that reaches a parent still reading its inbox without arming a turn on one that is not, and nothing is lost that the wake would have delivered: the turn a wake started was itself disposed mid-flight.
Delivery never blocks or fails teardown. A rejected send is logged and dropped, because retaining a child to retry a notice would pin its whole ancestry in `waiting` forever, and a parent that has left the registry is an ordinary outcome rather than an error.
### The epoch's own log is the whole account
`epochStopReason()` reads the epoch's outcome from its own log, because teardown succeeding says nothing about whether the model errored, hit its ceiling, or was stopped. Reading turns alone got that wrong twice, in the same shape both times: a turn stopped before its first step leaves a `turn/end` indistinguishable from the balanced no-op turns a rejection or an emptied claim produces, so the filter that skipped those also skipped real endings and answered with the previous turn's clean completion. The durability checkpoint (`dsh-session-checkpoint-policy`, in every shipped profile) and prompt assembly both run at that boundary and both propagate, and `Inbox.claim()` has already taken the messages by then — so the parent was told a child finished while the delivery it was waiting on had been swallowed. Under a promise that says "you are told when it finishes, so never poll", that is the one failure a parent cannot detect and will not retry.
The missing fact was never the turn's; it was the inbox's. `Inbox` logs every mutation with `removedCount` and marks a cancellation `outcome: 'canceled'`, which separates a turn claiming its input from work being dropped unrun. `foldConsumedWork()` in `dsh-agent` folds both vocabularies into one answer: the latest turn that accounts for consumed work — stepped, or claimed-then-failed, stopped, or rejected — and whether accepted work was cancelled after it with no turn opening over it. A `blocked` end over claimed input is an account too: the pre-step rejection that produced it — a hook deny, a policy plugin — discarded the messages the turn claimed, so the notice says the child declined rather than finished. Only a `blocked` turn that claimed nothing stays invisible.
Deriving it from the log rather than from live state is what makes it whole. An earlier version sampled the manager's own Activation immediately before cancelling, which could only ever see cancellations this manager was about to perform: an ancestor's `interrupt()`, or an unloading plugin cancelling an agent it tracks, left the sample false and the notice still saying `finished`. It also left the accepted-but-never-claimed case pinned to nothing a test could distinguish from its absence. One fold over the log covers every issuer, and both halves fail their own tests when removed.
Precedence is the consumer's: a recorded failure or ceiling wins over a cancellation, because stopping a child that had already failed does not turn its failure into a cancellation. `dsh-agent` owns the fold because it owns the inbox marker the answer depends on, and both consumers already depend on it — the continuable epoch here, and the one-shot `readResult()`, which had the same hole.
Both matter past the notice: `subagent/end` carries `stopReason` to the jsonrpc UI and the Claude hook bridge, which reported a torn-down mid-turn child as `completed`.
### Snapshot coverage
Three assembled ACP scenarios cover the notice: a child that never reports, a child that reports first, and a child driven through several follow-up turns. All three needed an explicit fence. The notice arrives once the child's teardown finishes, which races whatever the parent is already doing, so each scenario holds the child behind the parent's spawn turn and then waits for the parent turn the notice opens (`waitForTurnStart` at that turn, then `waitForTurnEnd`) before the script continues. Waiting for a turn the run is not fenced to produce is not coverage: it is a timeout when the notice lands in the turn already running instead.
`subagent-continuable` is the one that pins a failure. Its child's last turn dies on the forced durability checkpoint without entering a step, so that transcript is where the stop-reason rule above is visible end to end: the notice says the child *failed*, carries the earlier `SECOND_OK` as its last content rather than as a result, and the parent's own acknowledgement turn reaches the ACP client.
A keyless headless Loader snapshot covers the user-visible path end to end. Its replay parent starts one continuable child with `run_in_background: true`, never calls `list_agents`, `send_message`, or Task tools, consumes the manager-authored `subagent-settled` notice, and produces its final answer. The child never calls `report`, so the transcript cannot pass through the cooperative report path. A test-only Loader fence holds the parent's post-spawn request until the real manager notice enters its inbox, removing platform scheduling from the transcript without synthesizing the notice.
`subagent-report` needed one more concession. With the shipped waking report default, that scenario has two independent parent wakes — the report and the settlement — and whether the second extends the first's turn or opens its own is a genuine coin flip that measured 50/50 across runs. No authored transcript can hold both orders. Its overlay therefore pins `reportDelivery: quiet`, leaving settlement as the only wake, and a snapshot-only pre-step fence holds the child until the parent's spawn turn ends so that wake opens one deterministic turn claiming both messages. The waking report default keeps its coverage in the report package's own tests.
The refusal and interruption wordings are pinned verbatim in unit tests rather than in a replayed transcript: producing them needs a rejecting policy plugin or a cancellation fenced at a step boundary, which the keyless assemblies do not otherwise carry, and the assembled scenarios already pin the notice pathway itself end to end.
## Alternatives considered
**Give continuable children a Task.** A Task is a one-shot contract: one producer, one settlement, one result. An Activation runs many turns, outlives any single one, and can be resumed after it ends. Wrapping it in a Task recreates exactly the lifetime mismatch continuable children were introduced to remove, and would make one turn look terminal.
**Attach an external `subagent/end` listener.** Rejected on three counts above — no parent in the payload, a disposed child handle, and an ordering the listener cannot influence. A listener would also have to be strictly synchronous to beat the release, and nothing at that seam enforces it, so the correct version would be correct only by accident.
**Deliver only when the child did not report.** This was the first design. It needs per-Activation bookkeeping, still misses the child that reported progress and then died before its result, and — decisively — makes the parent-facing promise conditional. "Usually you are told" is not a contract a tool description can state, and a model that cannot rely on the notice will poll anyway.
**Make delivery configurable.** A deployment switch would return the model-facing text to "usually", which is the failure this change exists to remove. Protocol constants and safety invariants stay fixed; this is one of them.
**Change `subagent/end` to carry the parent, and let a plugin deliver.** That widens a published payload for one in-package consumer, keeps every ordering hazard, and makes the return channel an optional plugin again. Extending the package-private `ActivationObserver` with `terminal(failure)` keeps one computation of the terminal facts and no public surface change.
**Always use `followup`.** Simpler and uniform, but a fan-out of children settling together would cost one parent turn each. The step-boundary batch already exists; using it is free.
## Consequences
- A continuable child's parent receives one message per settled Activation. Fan-out deployments therefore add parent turns; steering keeps a simultaneous batch to one step.
- `tool-subagent` promises the notice in its schema because the return channel is service behavior, not an optional plugin.
- `Activation` carries `parentSession` and `announced`. The first exists because the child handle is disposed before delivery; the second is what keeps a rolled-back materialization silent.
- `foldConsumedWork()` replaces `dsh-session`'s `findLastMessageTurnEnd()` and moves to `dsh-agent`, which owns the inbox marker it reads; the one-shot in-process path folds the same answer and does not classify a cut-short one-shot child as `completed`.
- Unit coverage pins the unconditional contract, each terminal reason, idle and busy scheduling, the batch, the maintenance regression, the pre-release ordering, a parent that is gone, and a rejected send that must not fail teardown.
- Three ACP scenarios use an explicit settlement fence, and `subagent-report` has a config overlay that pins quiet report delivery.
- A keyless headless Loader snapshot pins background start → manager-authored settlement notice → final parent answer with no polling or child `report` call.
### Accepted risks
The notice is delivered, not confirmed. There is no durable mailbox, receipt, or retry: a parent that is not live loses it, and the child's Session remains the only durable record. Closing that needs an offline mailbox protocol with its own addressing, authorization, and replay rules.
A notice injected during teardown is not read by a model when that parent is disposed next, which every teardown caller does: the disposal cancel clears the unclaimed message and the log keeps the insert/cancel pair as the record. Making teardown delivery readable after resume requires either the offline mailbox above or a change to disposal of durable pending work. Disposal discards every unclaimed inbox item, including user input, so changing that behavior is a core-agent decision rather than a settlement-delivery detail. After resume, the parent can discover the child but does not receive the outcome: `list_agents` reports existence and live-or-stored status only — `SubagentListEntry.activity` says so — and recovering the ending requires asking the child through `send_message`.
Stop-reason attribution is a best effort over the log's existing splice vocabulary, biased against overstating success. `Inbox.remove()` and teardown's `clear()` write identical cancellation splices, so removing a message whose content survives elsewhere — `workspace-context` vacuuming a pending instruction refresh, or settlement's own cancel clearing one left pending — can read as work dropped unrun and report a finished child as stopped. Separating them requires a richer removal vocabulary in `dsh-agent`; without it, the misread is narrow and errs toward the parent double-checking a finished child, never toward trusting an unfinished one.
Turn amplification is real for deep or wide trees, and it is not configurable by design. The step-boundary batch bounds it for simultaneous settlement but not for children that settle apart.
Two independent waking sources cannot be ordered in an authored transcript. The assembled coverage pins each separately rather than their interleaving.
@@ -0,0 +1,103 @@
# Agent Note: Settlement delivery belongs to the continuation manager
Status: implemented
[English](2026-08-06-manager-owned-subagent-settlement-delivery.md) | 中文
## Problem
可继续后台委派是模型唯一一种能够发起、却无法抵达终点的异步操作。其他每一种形态都有取回原语或返回值:后台 bash 命令与一次性后台 subagent 都通过 Task 结算,`task_output(wait: true)` 可以阻塞等待;workflow 与前台 subagent 会把结果返回给调用方。可继续后台 child 只返回它持久化的 id,而父级既没有可等待的对象,也不会被交付任何东西。
[报告义务](2026-08-06-continuable-child-report-obligation.md)通过要求 child 在结束前上报,补上了这一缺口中协作的那一半。指令无法补上其余部分。被 token 上限、模型失败、取消或拆卸终止的 child 永远走不到能够遵守的那一步——不是很少,而是从不——而这些恰恰是等待中的父级最需要被告知的结束方式。可观察到的下游症状包括:父级忙轮询 `list_agents`、向已经结算的 child 反复发送消息,以及部署放弃 `subagent` 转用 `workflow`,因为 workflow 至少会返回点什么。
信号本身早就存在。自可继续 Activation 发布以来,`subagent/end` 就一直携带 `stopReason``lastAssistantMessage`。缺的是把它变成父级模型能看到的上下文的那个消费者。
## Decision
继续执行管理器自己投递这份记账,就在结束 Activation 的那笔 dispose 事务内部完成。
当驻留 Activation 结算时,`notifySettlement()` 解析该 child 持久化的直接父级,并向它发送一条用户角色消息:先是父级可据以行动的一句结果说明,然后是 child 的最终 assistant 内容,或一句说明它没有产出内容。对每个调用方真正拿到过 id 的 child,投递都是无条件的。它不查询 child 是否上报过,也不保留任何可能让这项承诺变成有条件的记账——正是这种无条件性,才让 `tool-subagent` 能够告诉模型「它结束时你会被告知,因此绝不要轮询或等待它」并且这句话为真。在第一条消息被接受之前就回滚的物化保持静默,因为调用方已被告知该 child 未建立。
### 来源信息
该通知携带 `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`,刻意不复用既有的 `subagent-report` kind。上报是 child 选择的内容;这条消息则是运行时在陈述这个 child 后来怎样了。把两者合并会把 child 从未写过的话算到它头上,也会让持久化日志无法区分「child 说它做完了」和「harness 观察到它停下了」。`notice` 形态还为 UI 提供了这条消息想要的折叠单行呈现,而 `relay` 会把它呈现为往来信件。
### 两条顺序规则,以及为什么归管理器所有
外部 `ctx.on('subagent/end')` listener 看起来更解耦,但它是错的。`SubagentRunEndInfo` 不指名父级;该边触发时 child handle 已被 dispose,因此无法从中恢复父级;而唤醒父级自身结算 watcher 的所有权释放也已经执行过了。管理器在整个 dispose 过程中都持有父级引用,因此这些障碍对它都不存在。
**发送发生在 `releaseOwnership` 之前。** 此刻父级仍然计入这个 child,因此 `stateOf(parent)``waiting`,父级在结构上不可能被判定为已结算。改在释放之后投递,则会与一个在下一个 microtask 恢复的 watcher 竞争:它会发现自己没有 child 且处于静止,于是 dispose 一个 Agent,而该 Agent 的 `cancel()` 会清空正装着这条通知的那个 inbox。失效表现是一条静默丢失的消息,任何地方都不会报错。
**驻留父级通过 `admitWaking` 接收它。** 在同步发送之前登记消息 id,正是让 `followup()` 与承认它的那个 microtask 之间的窗口不被读作静止的原因。这不是对第一条规则的多余保险:`Agent.status` 会把上下文维护折叠成 `idle`,而维护期间的唤醒发送只会预置一次延后唤醒,因此正在压缩上下文的父级,在所有权释放落地的那一刻会同时被 `status` 与已拥有 child 集合判定为静止。
两条规则都有测试固定:把顺序反转或去掉记账,测试就会失败。
### 调度
空闲父级得到一个普通的后续轮次。繁忙父级则被 steer 到其最近的 step 边界,因为 `Inbox.claim()` 会在一个边界上整批取走 next-step:四个 child 同时结算时因此只消耗一个 step,而不是四个轮次。采用 steer 而非 inject 是刻意的——驱动运行期间该唤醒是空操作,同时它关闭了「驱动在状态读取与发送之间退出」的那个窗口;否则通知会滞留无人认领,直到别的事件唤醒父级。这是正确性规则而非部署偏好,因此不做成 `Config` 字段。
有一种 `running` 父级是无法 steer 的:轮次已被 cancel 但尚未退出的那种。`Agent.send()` 会把取消之后提交的唤醒输入改投到下一个轮次、闩存这次唤醒,并在被取消的驱动收敛后重放它——只有 disposal 取消从不闩存,那属于下面的拆卸规则。因此通知仍会开启自己的轮次,无需等待无关输入;代价是一次被改投的轮次边界,而不是消息本身。
**自身已开始拆卸的父级不会被唤醒。** 唤醒不是入队操作:对静息 Agent 调用 `Agent.followup()` 会开启一个轮次,而对空闲 Agent 调用 `cancel()` 是文档明确的空操作,不会对之后的轮次设防。因此每条拆卸路径最终都面对一个在线、已取消、仍在注册表中的父级——ACP 桥接层正是在取消其 session agent 与 dispose 它们之间调用 `drainContinuableDescendants()`——于是一条无防护的通知会在一个即将被销毁的 Agent 上发起真实模型请求,而且每层树各一次,因为每层自己的通知又会唤醒它上面那层。`notifySettlement()` 会问 `assertAdmitting()` 问的同一个问题(这条谱系的可继续准入是否已关闭?),并改为 inject。inject 不是持久 mailbox——父级自身的 dispose 会对它做什么,记在「已接受的风险」里——但它是唯一能送达仍在读取自身 inbox 的父级、又不会在不该被唤醒的父级上预置一个轮次的发送方式;而唤醒本可送达的东西一样没有丢失:唤醒开启的那个轮次本身就会在半途被 dispose。
投递绝不会阻塞或使拆卸失败。发送被拒会被记录并丢弃,因为为重试一条通知而保留 child,会把它的整条祖先链永久钉在 `waiting` 上;而父级已离开注册表属于普通结果,不是错误。
### epoch 自己的日志就是全部交代
`epochStopReason()` 从 epoch 自己的日志读取结局,因为拆卸成功与否,对「模型是否报错、是否撞到上限、是否被停下」什么也没说明。只读轮次这件事已经错了两次,而两次的形状相同:在第一个 step 之前被停下的轮次,其 `turn/end` 与「拒绝」或「被清空的认领」产生的平衡空转轮次长得一模一样,于是那道用来跳过后者的过滤,也把真实的结局一起跳过了,转而用上一个轮次的干净收尾作答。持久化检查点(`dsh-session-checkpoint-policy`,存在于每个随附 profile 中)与提示词组装都运行在这个边界上、且都会向外传播,而此时 `Inbox.claim()` 已经把消息取走了——于是父级被告知 child 已完成,而它正在等待的那条投递已被吞掉。在「你会在它完成时被告知,所以永远不要轮询」这一承诺之下,这恰恰是父级无法察觉、也不会重试的那一种失败。
缺失的事实从来不属于轮次,而属于 inbox。`Inbox` 会把每次改动连同 `removedCount` 一起记入日志,并给取消标记 `outcome: 'canceled'`,这就把「某个轮次认领了它的输入」与「工作被丢弃且从未运行」区分开来。`dsh-agent` 中的 `foldConsumedWork()` 把两套词汇折叠成一个答案:能为已消费工作作出交代的最新轮次——进入过 step 的,或认领后失败、被停下或被拒绝的——以及此后是否有已接受的工作被取消、而没有任何轮次为它开启过。认领过输入、以 `blocked` 结束的轮次同样是一份交代:产生它的 pre-step 拒绝——hook deny、策略插件——把该轮次认领的消息一并丢弃了,因此通知会说 child 拒绝了任务,而不是完成了任务。只有没认领任何输入的 `blocked` 轮次保持不可见。
从日志而不是从活动状态推导,才让它完整。早先的版本会在 cancel 之前立刻采样管理器自己的 Activation,而那样只能看到本管理器即将执行的取消:来自祖先的 `interrupt()`,或某个正在卸载的插件取消它所跟踪的 Agent,都会让该采样为假,通知照旧说 `finished`。它也让「已接受但从未被认领」这一情形没有任何测试能把它与「该判据不存在」区分开。一次对日志的折叠覆盖了所有发起方,而两个半边在被移除时都会让各自的测试失败。
优先级归消费方:已记录的失败或上限优先于取消,因为停下一个已经失败的 child,不会把它的失败变成一次取消。`dsh-agent` 拥有这个 fold,是因为答案所依赖的那个 inbox 标记归它所有,而两个消费方本来就依赖它——这里的可继续 epoch,以及一次性的 `readResult()`(它有同一个漏洞)。
两者的影响都超出通知本身:`subagent/end` 会把 `stopReason` 送到 jsonrpc UI 与 Claude hook 桥接层,而它们此前把被拆卸的、正在跑轮次的 child 报成 `completed`
### 快照覆盖
三个整体组装的 ACP 场景覆盖该通知:一个从不上报的 child、一个先上报的 child,以及一个被多轮 follow-up 驱动的 child。三者都需要显式栅栏。通知在 child 拆卸完成后才到达,会与父级当时正在做的事竞争,因此每个场景都会把 child 保持到父级启动轮次结束,再等待该通知开启的那个父级轮次(先 `waitForTurnStart` 到该轮次,再 `waitForTurnEnd`),然后脚本才继续。等待一个运行并未被栅栏保证会产生的轮次不算覆盖:一旦通知落进已经在跑的那个轮次,它就是一次超时。
`subagent-continuable` 是其中固定失败结局的那个。它的 child 最后一个轮次在被强制的持久化检查点上死亡,且未进入任何 step,因此该 transcript 正是上面那条终止原因规则的端到端可见之处:通知说该 child **失败**,把此前的 `SECOND_OK` 作为它最后产出的内容而非结果携带,而父级自己的确认轮次会到达 ACP 客户端。
另有一个无密钥的 headless Loader 快照端到端覆盖用户可见路径。其重放父级通过 `run_in_background: true` 启动一个可继续 child,从不调用 `list_agents``send_message` 或 Task 工具,消费管理器写入的 `subagent-settled` 通知,并给出最终答案。child 从不调用 `report`,因此该 transcript 不可能经由协作式上报路径通过。一个仅用于测试的 Loader 栅栏会把父级启动后的请求保持到真实管理器通知进入其 inbox 为止,从 transcript 中排除平台调度差异,但不会伪造该通知。
`subagent-report` 还需要多做一步让步。在随附的唤醒上报默认值下,该场景有两个互相独立的父级唤醒——上报与结算——而第二个究竟是延长第一个的轮次还是另开一个轮次,是一枚真正的硬币,多次运行实测约为五五开。任何手写 transcript 都无法同时容纳两种顺序。因此它的 overlay 固定 `reportDelivery: quiet`,使结算成为唯一唤醒;另一个仅用于快照的 pre-step 栅栏会把 child 保持到父级启动轮次结束,使这次唤醒开启一个确定轮次并同时认领两条消息。唤醒上报默认值的覆盖则保留在 report 包自身的测试中。
拒绝与中断两种措辞在单元测试中逐字钉死,而不进入重放 transcript:触发它们需要一个会拒绝的策略插件、或一次在 step 边界被栅栏卡住的取消,而无密钥组装本身并不携带这些;通知通路本身已由整体组装场景端到端钉住。
## Alternatives considered
**给可继续 child 引入 Task。** Task 是一次性契约:一个生产者、一次结算、一个结果。Activation 会执行许多轮次、比其中任何一轮活得更久,并且可以在结束后被恢复。用 Task 包装它,恰好重建了可继续 child 当初为消除而引入的生命周期错配,还会让某一个轮次看起来是终局。
**挂一个外部 `subagent/end` listener。** 因上文三点被否决——payload 里没有父级、child handle 已被 dispose,以及 listener 无法影响的顺序。listener 还必须严格同步才能抢在释放之前,而该 seam 上没有任何东西强制这一点,因此正确的版本只能靠碰巧正确。
**仅在 child 没有上报时投递。** 这是最初的设计。它需要按 Activation 记账,仍会漏掉「报了进度、随后在给出结果前死掉」的 child,而且最关键的是:它让面向父级的承诺变成有条件的。「通常你会被告知」不是工具描述能陈述的契约,而无法依赖该通知的模型无论如何都会去轮询。
**把投递做成可配置。** 部署开关会把面向模型的文本重新变回「通常」,而这正是本次改动要消除的失效。协议常量与安全不变量保持固定;这就是其中之一。
**修改 `subagent/end` 让它携带父级,由插件负责投递。** 那会为一个包内消费者拓宽已发布的 payload,保留全部顺序风险,并让返回通道重新变成可选插件。以 `terminal(failure)` 扩展包私有的 `ActivationObserver`,则只保留一处终止事实的计算,且不改动任何公开面。
**始终使用 `followup`。** 更简单也更统一,但一批同时结算的 child 会各自消耗一个父级轮次。step 边界的批量语义本来就存在,用它是免费的。
## Consequences
- 可继续 child 的父级会为每个已结算 Activation 收到一条消息。因此,做扇出的部署会增加父级轮次;steer 会把同时结算的一批压缩到一个 step。
- `tool-subagent` 在其 schema 中承诺该通知,因为返回通道是服务行为,不是可选插件。
- `Activation` 携带 `parentSession``announced`。前者存在是因为 child handle 在投递前已被 dispose;后者让被回滚的物化保持静默。
- `foldConsumedWork()` 取代 `dsh-session``findLastMessageTurnEnd()`,并迁移到 `dsh-agent`——它拥有该 fold 所读取的 inbox 标记;一次性 in-process 路径折叠同一个答案,不会把被中途切断的一次性 child 归类为 `completed`
- 单元覆盖固定了无条件约定、每种终止原因、空闲与繁忙两种调度、批量语义、维护期回归、释放前顺序、父级已消失,以及一次不得让拆卸失败的发送被拒。
- 三个 ACP 场景使用显式的结算栅栏,`subagent-report` 带有固定静默上报投递的配置 overlay。
- 一个无密钥的 headless Loader 快照固定了「后台启动 → 管理器写入的结算通知 → 父级最终答案」路径,其中没有轮询,也没有 child `report` 调用。
### 已接受的风险
通知只是被投递,而不是被确认。没有持久化 mailbox、回执或重试:不在线的父级会丢失它,child 的 Session 仍是唯一的持久记录。要补上这一点,需要一套带有自身寻址、授权与重放规则的离线 mailbox 协议。
当父级紧接着被 dispose 时(每个拆卸调用方都会这么做),在拆卸期间被 inject 的通知不会被模型读到:dispose 的 cancel 会清除这条未被认领的消息,而日志保留 insert/cancel 这一对作为记录。要让拆卸期投递在 resume 之后仍可读,要么需要上面那套离线 mailbox,要么需要改变 dispose 对持久待处理工作的处理方式。dispose 会丢弃每一条未被认领的 inbox 项,用户输入也不例外,因此改变该行为是一个 core-agent 决策,而不是结算投递的细节。resume 后的父级可以发现 child,但不会收到结局:`list_agents` 只报告存在性与「在线/仅存储」状态——`SubagentListEntry.activity` 就是这么写的——要取回结局,必须通过 `send_message` 去问那个 child。
终止原因的归因是对日志既有 splice 词汇的尽力而为,偏向永不高估成功。`Inbox.remove()` 与拆卸的 `clear()` 写出的取消 splice 完全相同,因此删除一条内容仍保留在别处的消息——`workspace-context` 清理待处理的 instructions 刷新、或结算自身的 cancel 清掉一条仍在挂起的这类消息——可能被读作「工作被丢弃且从未运行」,把已完成的 child 报成被停下。区分二者需要 `dsh-agent` 提供更丰富的删除词汇;在该词汇可用前,这项误读的范围很窄,且错的方向是让父级复查一个已完成的 child,而永远不是信任一个未完成的 child。
对于深或宽的树,轮次放大是真实存在的,而且按设计不可配置。step 边界的批量语义只能约束同时结算的情形,无法约束分散结算的 child。
两个互相独立的唤醒源无法在手写 transcript 中排序。整体组装覆盖分别固定它们,而不固定它们的交错。
+2 -2
View File
@@ -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 docs/event-producer-consumer.md
event-producer-consumer.md: 33e3f8e67291d9f3b50d9a52fc3104e2e218d799
event-producer-consumer.zh.md: 2f036ba0cad1d86952424c4d4969795a3862cfd7
event-producer-consumer.md: d5a61bf384ee38b5d69ccef6283a661c9c4ac36f
event-producer-consumer.zh.md: af1ca18d728320940af40b5b89f9ff842f9077f7
+8 -8
View File
@@ -30,17 +30,17 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:75`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:97`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:106`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:145`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:156`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:146`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:157`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
+4 -4
View File
@@ -32,10 +32,10 @@
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:75`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:97`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:106`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
+2 -2
View File
@@ -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 docs/subsystems/core.md
core.md: ad00c4da7d77b0e1ab4728173b202ebc17fb56a0
core.zh.md: 9c606023c85369643e7148f829526b1f75ea3631
core.md: bea9b29f66f3189e47540f4d51f908e3d139c0ce
core.zh.md: 7f0a91d9a6b6e67530d99de3ac7339cb42658e1f
+1 -1
View File
@@ -718,7 +718,7 @@ list(): Agent[]
roots(): Agent[]
```
Source: [`packages/core/agent/src/index.ts:255`](../../packages/core/agent/src/index.ts)
Source: [`packages/core/agent/src/index.ts:256`](../../packages/core/agent/src/index.ts)
<a id="agent-events"></a>
+1 -1
View File
@@ -726,7 +726,7 @@ list(): Agent[]
roots(): Agent[]
```
Source: [`packages/core/agent/src/index.ts:255`](../../packages/core/agent/src/index.ts)
Source: [`packages/core/agent/src/index.ts:256`](../../packages/core/agent/src/index.ts)
<a id="agent-events"></a>
+2 -2
View File
@@ -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 docs/subsystems/session.md
session.md: 990b249cde9f02343f2c668aee5d7c000837df56
session.zh.md: 39e8ff1e8831fd75c8929c93e263622bb5aa6ea4
session.md: e81f284f54057aa92c5c8cc76c2200751e51f4d7
session.zh.md: 2d9eb45c4addf7c8df2370f9a0f3b4b57b4f05a1
+5 -5
View File
@@ -744,7 +744,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId):
Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)
Source: [`packages/core/session/src/index.ts:813`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:792`](../../packages/core/session/src/index.ts)
<a id="session-events"></a>
@@ -773,7 +773,7 @@ Creation announcement during session publication. A synchronous throw vetoes and
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:75`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:54`](../../packages/core/session/src/index.ts)
<a id="sessiondisposed--emit"></a>
@@ -796,7 +796,7 @@ Emitted once when an announced session leaves the store, including publication r
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:85`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:64`](../../packages/core/session/src/index.ts)
<a id="sessionevent--emit"></a>
@@ -821,7 +821,7 @@ Post-commit, fire-and-forget append feed. The listener snapshot resolves before
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:97`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:76`](../../packages/core/session/src/index.ts)
<a id="sessionflush--parallel"></a>
@@ -843,5 +843,5 @@ Awaited parallel durability checkpoint: every listener runs and the caller await
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:106`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:85`](../../packages/core/session/src/index.ts)
<!-- END GENERATED cordis-surface -->
+5 -5
View File
@@ -748,7 +748,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId):
Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)
Source: [`packages/core/session/src/index.ts:813`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:792`](../../packages/core/session/src/index.ts)
<a id="session-events"></a>
@@ -777,7 +777,7 @@ Creation announcement during session publication. A synchronous throw vetoes and
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:75`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:54`](../../packages/core/session/src/index.ts)
<a id="sessiondisposed--emit"></a>
@@ -800,7 +800,7 @@ Emitted once when an announced session leaves the store, including publication r
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:85`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:64`](../../packages/core/session/src/index.ts)
<a id="sessionevent--emit"></a>
@@ -825,7 +825,7 @@ Post-commit, fire-and-forget append feed. The listener snapshot resolves before
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:97`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:76`](../../packages/core/session/src/index.ts)
<a id="sessionflush--parallel"></a>
@@ -847,5 +847,5 @@ Awaited parallel durability checkpoint: every listener runs and the caller await
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:106`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:85`](../../packages/core/session/src/index.ts)
<!-- END GENERATED cordis-surface -->
+2 -2
View File
@@ -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 docs/subsystems/subagent.md
subagent.md: 42359215b2e21028e13734c2a4cd2488f86a07af
subagent.zh.md: 0d1d4c42fa774867679ff248bfc99b0024b04257
subagent.md: fbfe83010fc297b6c10baf0631e8cdfd4d05854c
subagent.zh.md: fc2fc85e35eaee1467921dc9f94b3d2a40292bbb
+26 -5
View File
@@ -209,6 +209,27 @@ interface SubagentReportMessageSource {
type SubagentReportDelivery = 'quiet' | 'wakeup'
```
Reporting is the child's own choice, so the manager keeps a separate account of its own: when a resident Activation settles, it delivers one notice to the child's durable direct parent describing how that epoch ended and carrying its final assistant content. That delivery is unconditional for every child whose id a caller received, happens before the ownership release that would let the parent be judged settled, and reaches a resident parent through the same waking-admission accounting as a report. A parent whose own lineage is already tearing down receives it without a wake, because waking a quiescent Agent starts a turn rather than queueing work. Its provenance is a distinct kind so a transcript never presents a runtime account as something the child wrote.
```ts type-equiv
/**
* Durable attribution for the runtime's own account of a continuable child
* settling. Deliberately a different kind from
* {@link SubagentReportMessageSource}: a report is content the child chose,
* while this message is the manager stating what became of the child, and a
* transcript that merged them would credit the child with words it never wrote.
*/
interface SubagentSettledMessageSource {
readonly kind: 'subagent-settled'
/** A runtime account shown without expanding the row (`notice` context form). */
readonly form: 'notice'
/** One-line account of how the child ended. */
readonly summary: string
/** Session id of the child that settled. */
readonly senderSessionId: SessionId
}
```
```ts type-equiv
/** Options for one continuable child's report to its direct parent. */
interface SubagentReportOptions {
@@ -619,7 +640,7 @@ async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md) · [MessageId](llm-streaming.md) · [SessionId](core.md)
Source: [`packages/subagent/subagent/src/index.ts:170`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:171`](../../packages/subagent/subagent/src/index.ts)
<a id="subagent-events"></a>
@@ -645,7 +666,7 @@ A published child settled. Scope-filtered dispatch uses the same delegating pare
Types: [Scoped](scope.md)
Source: [`packages/subagent/subagent/src/index.ts:165`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:166`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentprovider-added--emit"></a>
@@ -662,7 +683,7 @@ A provider became resolvable in the registry.
'subagent/provider-added'(provider: SubagentProvider): void
```
Source: [`packages/subagent/subagent/src/index.ts:139`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:140`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentprovider-removed--emit"></a>
@@ -679,7 +700,7 @@ A provider left the registry. Accepted runs remain holder-owned.
'subagent/provider-removed'(name: string): void
```
Source: [`packages/subagent/subagent/src/index.ts:145`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:146`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentstart--emit"></a>
@@ -703,5 +724,5 @@ A provider established a published child. For in-process providers, `ctx.agents.
Types: [Scoped](scope.md)
Source: [`packages/subagent/subagent/src/index.ts:156`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:157`](../../packages/subagent/subagent/src/index.ts)
<!-- END GENERATED cordis-surface -->
+26 -5
View File
@@ -209,6 +209,27 @@ interface SubagentReportMessageSource {
type SubagentReportDelivery = 'quiet' | 'wakeup'
```
上报是 child 自己的选择,因此管理器还保有一份属于自己的记账:当驻留 Activation 结算时,它会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 内容。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与上报相同的唤醒准入记账到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个静息 Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。
```ts type-equiv
/**
* Durable attribution for the runtime's own account of a continuable child
* settling. Deliberately a different kind from
* {@link SubagentReportMessageSource}: a report is content the child chose,
* while this message is the manager stating what became of the child, and a
* transcript that merged them would credit the child with words it never wrote.
*/
interface SubagentSettledMessageSource {
readonly kind: 'subagent-settled'
/** A runtime account shown without expanding the row (`notice` context form). */
readonly form: 'notice'
/** One-line account of how the child ended. */
readonly summary: string
/** Session id of the child that settled. */
readonly senderSessionId: SessionId
}
```
```ts type-equiv
/** Options for one continuable child's report to its direct parent. */
interface SubagentReportOptions {
@@ -621,7 +642,7 @@ async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
Types: [Agent](core.md) · [ContentBlock](llm-streaming.md) · [MessageId](llm-streaming.md) · [SessionId](core.md)
Source: [`packages/subagent/subagent/src/index.ts:170`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:171`](../../packages/subagent/subagent/src/index.ts)
<a id="subagent-events"></a>
@@ -647,7 +668,7 @@ A published child settled. Scope-filtered dispatch uses the same delegating pare
Types: [Scoped](scope.md)
Source: [`packages/subagent/subagent/src/index.ts:165`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:166`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentprovider-added--emit"></a>
@@ -664,7 +685,7 @@ A provider became resolvable in the registry.
'subagent/provider-added'(provider: SubagentProvider): void
```
Source: [`packages/subagent/subagent/src/index.ts:139`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:140`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentprovider-removed--emit"></a>
@@ -681,7 +702,7 @@ A provider left the registry. Accepted runs remain holder-owned.
'subagent/provider-removed'(name: string): void
```
Source: [`packages/subagent/subagent/src/index.ts:145`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:146`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentstart--emit"></a>
@@ -705,5 +726,5 @@ A provider established a published child. For in-process providers, `ctx.agents.
Types: [Scoped](scope.md)
Source: [`packages/subagent/subagent/src/index.ts:156`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:157`](../../packages/subagent/subagent/src/index.ts)
<!-- END GENERATED cordis-surface -->
@@ -0,0 +1,51 @@
# Keyless counterpart to subagent-report-quiet.cordis.yml: replace the live
# adapter with replay, keep report delivery quiet, and fence the child behind
# the end of its parent's spawn turn so settlement opens the next turn.
- id: base
name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- id: acp-agent
name: '@deepseek-ai/dsh-acp-demo'
config:
provider: deepseek-official
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
persistenceCompression: none
workspaceContext:
maxBytes: 65536
persona: |
You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
Verify your work by running the code or tests. Keep answers brief and factual.
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
config:
runnerCommand:
- bash
- -c
- while [ "$1" != "--" ]; do shift; done; shift; exec "$@"
- passthrough-runner
runnerFailureSignatures:
- 'passthrough-runner: profile rejected'
- id: tool-subagent-report
name: '@deepseek-ai/dsh-tool-subagent-report'
config:
reportDelivery: quiet
- insert:
- id: llm-replay
name: '@deepseek-ai/dsh-llm-replay'
config:
providers:
- id: deepseek-official
name: DeepSeek
models:
- id: deepseek-v4-flash
- id: deepseek-v4-pro
- id: report-fence
name: './tests/fixtures/subagent-report-fence.ts'
@@ -0,0 +1,17 @@
# Snapshot-only overlay pinning quiet report delivery. The shipped default wakes
# the parent on every accepted report, and the runtime's settlement notice wakes
# it again when the child's Activation ends; two independent wakes have no single
# authored order. Quiet delivery leaves settlement as the only wake, while the
# fixture below holds the child until the parent's spawn turn has closed.
- id: base
name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
- id: tool-subagent-report
name: '@deepseek-ai/dsh-tool-subagent-report'
config:
reportDelivery: quiet
- id: report-fence
name: './tests/fixtures/subagent-report-fence.ts'
+18 -3
View File
@@ -44,6 +44,9 @@ const CHILD_QUESTION_CONFIG = fileURLToPath(new URL('../child-question.cordis.ym
const SESSION_SANDBOX_ROOT_CONFIG = fileURLToPath(new URL('../session-sandbox-root.cordis.yml', import.meta.url))
const RETRY_CONFIG = fileURLToPath(new URL('../retry.cordis.yml', import.meta.url))
const SESSION_TITLE_CONFIG = fileURLToPath(new URL('../session-title.cordis.yml', import.meta.url))
const SUBAGENT_REPORT_QUIET_CONFIG = fileURLToPath(
new URL('../subagent-report-quiet.cordis.yml', import.meta.url),
)
const SUBAGENT_DURABILITY_FAILURE_CONFIG = fileURLToPath(
new URL('../subagent-durability-failure.cordis.yml', import.meta.url),
)
@@ -353,7 +356,13 @@ const SCENARIOS: Scenario[] = [
// turns on that same child (the parent is never woken with their output),
// send_message to an unknown subagent id fails without delivering, and the
// child's retained handle is disposed child-first at teardown despite a
// failed final durability confirmation.
// failed final durability confirmation. That failed confirmation is also what
// the settlement notice must report: the child's last turn claimed the third
// message and then died on its durability checkpoint without entering a step,
// so the notice opening the parent's second turn says the child FAILED and the
// parent must not read the earlier answer as final. The scenario's fixture
// fences the child behind the parent's spawn turn so that notice can only
// arrive at an idle parent.
{
name: 'subagent-continuable',
hasModelTurn: true,
@@ -367,6 +376,8 @@ const SCENARIOS: Scenario[] = [
// continuable background child's log carries that override as a
// `sandbox/mode` `source: 'delegation'` event, so the child's runtime
// context states the inherited policy instead of the deployment default.
// The input also waits for the manager-owned settlement turn, keeping that
// delivery from racing transcript harvest.
{
name: 'subagent-continuable-inheritance',
hasModelTurn: true,
@@ -387,12 +398,16 @@ const SCENARIOS: Scenario[] = [
configPath: SUBAGENT_DURABILITY_FAILURE_CONFIG,
},
// Authored child-to-parent transcript: the child calls its scope-local
// `report`, the default waking delivery gives the parked parent one ordinary
// turn on that report, and a later prompt still reads it back from the log.
// `report`, and the runtime's unconditional settlement notice then wakes the
// parked parent into one ordinary turn that claims both. The overlay pins
// quiet report delivery because two independent wakes have no orderable
// transcript; the shipped waking default is covered by package tests.
{
name: 'subagent-report',
hasModelTurn: true,
recorded: false,
overridden: false,
configPath: SUBAGENT_REPORT_QUIET_CONFIG,
pinsChildToolSchemas: [1],
pinsChildSystemPrompts: [1],
},
@@ -31,6 +31,8 @@ const FAILED_CHECKPOINT_TURN = 3
/** Fail the child checkpoint and stabilize the authored follow-up failure ordering. */
export function apply(ctx: Context): void {
const followupsAccepted = Promise.withResolvers<undefined>()
const parentTurnClosed = Promise.withResolvers<undefined>()
let parentClosed = false
const publishedFailure = process.env.DSH_SUBAGENT_PUBLISHED_FAILURE === '1'
const persistence = ctx.sessionPersistence
const load = persistence.load.bind(persistence)
@@ -62,8 +64,22 @@ export function apply(ctx: Context): void {
agents.create = create
persistence.load = load
followupsAccepted.resolve(undefined)
parentTurnClosed.resolve(undefined)
}, 'subagent snapshot ordering')
// The manager's settlement notice races whatever the parent is doing when the
// child's Activation ends, and this transcript pins it as the parent's own
// later turn. Hold the child's steps until the parent's spawn turn closes, so
// the notice can only arrive at an idle parent. The parent's turn never awaits
// child model work — its own fences need inbox acceptance only — so the child
// cannot deadlock it.
ctx.on('session/event', (session, event) => {
if (session.header.parentSession !== undefined || event.type !== 'turn/end') return
if (event.data.turn !== 1) return
parentClosed = true
parentTurnClosed.resolve(undefined)
})
// Remap the placeholder child id in a follow-up to the live child. The child
// id the model "knows" is authored into the transcript, while the running
// child is minted with a random id, so without this the follow-ups would
@@ -91,7 +107,12 @@ export function apply(ctx: Context): void {
if (accepted >= 3) followupsAccepted.resolve(undefined)
})
ctx.on('agent/pre-step', async ({ agent }, next) => {
if (agent.session.header.parentSession !== undefined) await followupsAccepted.promise
if (agent.session.header.parentSession === undefined) return next()
await followupsAccepted.promise
// The published-failure variant's child never reaches a step (its follow-up
// throws), and its parent turn awaits that child, so only the continuable
// scenario takes the settlement fence.
if (!publishedFailure && !parentClosed) await parentTurnClosed.promise
return next()
})
@@ -0,0 +1,41 @@
/**
* Loader fixture that holds the report child until its parent's spawn turn ends.
* @module subagent-report-fence
*/
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-agent-loop'
/** Fixture plugin name. */
export const name = 'subagent-report-fence'
/**
* Keep replay scheduling from folding settlement into the parent's first turn.
* @param ctx - assembled ACP-agent context.
*/
export function apply(ctx: Context): void {
const childReady = Promise.withResolvers<undefined>()
const parentStopped = Promise.withResolvers<undefined>()
let hasStopped = false
ctx.effect(() => {
const disposeSession = ctx.root.on('session/event', (session, event) => {
if (session.header.parentSession !== undefined || event.type !== 'turn/end' || event.data.turn !== 1) return
hasStopped = true
parentStopped.resolve(undefined)
})
const disposeStep = ctx.root.on('agent/pre-step', async ({ agent, turn, step }, next) => {
if (agent.session.header.parentSession !== undefined) {
childReady.resolve(undefined)
if (!hasStopped) await parentStopped.promise
} else if (turn === 1 && step === 2) {
await childReady.promise
}
return next()
})
return () => {
disposeStep()
disposeSession()
}
}, 'subagent-report-fence.listeners')
}
@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357538308,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
{"type":"step/start","seq":5,"time":1786357538310,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730458555,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"ebe0cfa0-a909-47e0-8294-28ad84a8fe77"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357538310,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"1b650647-a278-4be1-a76e-bf09540bff75"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357538310,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"a74dc391-2558-432c-b665-603b52eba070"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357538310,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730458555,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730458555,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357538469,"data":{"version":2,"mode":"one-shot","provider":"spawn"}}
{"type":"step/start","seq":5,"time":1786357538470,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730458703,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2ac2cc54-9bce-4cfa-a569-a64f51bc30a7"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357538471,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"c74c4556-1622-4d2e-9745-c5abd087fa83"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357538471,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"c2882641-5206-4d05-9b81-03898bc2943e"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357538471,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730458703,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730458703,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -4,7 +4,7 @@
{"type":"agent/inbox/spliced","seq":2,"time":1785821417919,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1783957884486,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785498801761,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: try a no-op temporary Cordis Plugin named snapshot-marker; use run_code to inspect the live temporary Plugins through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; stop dyn-1; then reply with exactly ADVANCED_ACP_OK."}],"source":{"kind":"user"},"role":"user","id":"6e45782a-31be-4ba7-8c4a-7411a2027e36"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730458430,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"06416873-c855-452d-8996-ea5cf45223d1"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730458430,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"b6de532e-08ff-42b0-abae-1895dc463500"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730458430,"data":{"title":"Run this advanced flow exactly","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785498801765,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730458431,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -132,13 +132,13 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record<string, JsonValue>;
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work. */
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work. */
subagent: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */
prompt: string;
/** Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message. */
/** Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message. */
run_in_background?: boolean;
} & Record<string, JsonValue>;
/** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. */
@@ -309,7 +309,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -323,7 +323,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -252,7 +252,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -266,7 +266,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -115,13 +115,13 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record<string, JsonValue>;
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work. */
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work. */
subagent: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */
prompt: string;
/** Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message. */
/** Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message. */
run_in_background?: boolean;
} & Record<string, JsonValue>;
/** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. */
@@ -268,7 +268,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -282,7 +282,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -231,7 +231,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -245,7 +245,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -231,7 +231,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -245,7 +245,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -231,7 +231,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -245,7 +245,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -435,7 +435,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -449,7 +449,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -294,7 +294,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -308,7 +308,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -14,6 +14,13 @@
"op": "waitForSubagentTurnEnd",
"child": 1,
"minimumTurn": 1
},
{
"op": "waitForTurnStart",
"minimumTurn": 2
},
{
"op": "waitForTurnEnd"
}
]
}
@@ -27,3 +27,16 @@
{"type":"assistant/message","seq":25,"time":1786333735904,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4057a08e-b50e-45e7-beb0-c74485f2b7d6"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[20,21,22,23,24],"surfaceOp":"append"}
{"type":"step/end","seq":26,"time":1786333735904,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":27,"time":1786333735904,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"agent/inbox/spliced","seq":28,"time":1786374357751,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"e0bd4902-daba-4e23-bfcb-9e102fdd203d"}]}}
{"type":"turn/start","seq":29,"time":1786374357751,"data":{"turn":2}}
{"type":"agent/inbox/spliced","seq":30,"time":1786374357751,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":31,"time":1786374357758,"data":{"turn":2,"step":1}}
{"type":"user/message","seq":32,"time":1786374357758,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"e0bd4902-daba-4e23-bfcb-9e102fdd203d"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":33,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":34,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
{"type":"assistant/chunk","seq":35,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
{"type":"assistant/chunk","seq":36,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":37,"time":1786374357764,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":38,"time":1786374357764,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"bbe5ef7b-2a3a-47f4-8475-60945b31a373"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[33,34,35,36,37],"surfaceOp":"append"}
{"type":"step/end","seq":39,"time":1786374357765,"data":{"turn":2,"step":1}}
{"type":"turn/end","seq":40,"time":1786374357765,"data":{"turn":2,"reason":{"kind":"completed"}}}
@@ -2,3 +2,4 @@
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
@@ -247,7 +247,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -261,7 +261,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -10,6 +10,17 @@
"op": "prompt",
"text": "Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."
},
{ "op": "waitForSubagentTurnEnd", "child": 1, "minimumTurn": 3 }
{
"op": "waitForSubagentTurnEnd",
"child": 1,
"minimumTurn": 3
},
{
"op": "waitForTurnStart",
"minimumTurn": 2
},
{
"op": "waitForTurnEnd"
}
]
}
@@ -1,10 +1,10 @@
{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1789000000000,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"agent/inbox/spliced","seq":0,"time":1785730451297,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"125665d3-8c03-4190-b4f9-c27d61d245f4"}]}}
{"type":"agent/inbox/spliced","seq":0,"time":1785730451297,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"579d3d6d-a57e-4d55-9b48-05832a79d9f8"}]}}
{"type":"turn/start","seq":1,"time":1785821408972,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785821408972,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1785730451327,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785730451327,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"125665d3-8c03-4190-b4f9-c27d61d245f4"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730451328,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"533e9513-a329-4a36-9a8d-ddaf544b57c3"},"surfaceOp":"append"}
{"type":"user/message","seq":4,"time":1785730451327,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Call send_message twice in a row, both with the subagent id from step 1: first with message 'Now reply with exactly SECOND_OK.', then with message 'Now reply with exactly THIRD_OK.'. 3. Call send_message with subagent_id exactly '22222222-2222-4222-8222-222222222222' (a subagent that does not exist) and message 'Please continue.', and observe that it fails. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"579d3d6d-a57e-4d55-9b48-05832a79d9f8"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730451328,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"c4f5f7ed-1c11-4f31-923f-3142c79f0c2c"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730451328,"data":{"title":"Follow these steps exactly, then","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785730451329,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730451329,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -13,9 +13,9 @@
{"type":"assistant/chunk","seq":11,"time":1785544945188,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}}}
{"type":"assistant/chunk","seq":12,"time":1785544945188,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":13,"time":1785730451338,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":14,"time":1785730451338,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4a2c2f8b-66be-4860-9bbf-b83feb56009e"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"assistant/message","seq":14,"time":1785730451338,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"680da987-6d29-4141-b83d-af57b050c712"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"tool/call","seq":15,"time":1785730451338,"data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}
{"type":"tool/result","seq":16,"time":1785730451348,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"b54a0d61-4233-40f1-ab3a-3eb58e1b0c61"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"tool/result","seq":16,"time":1785730451348,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"7825edb2-080e-49c1-ba74-ad69d16bf566"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"step/end","seq":17,"time":1785730451348,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":18,"time":1785730451360,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":19,"time":1789000000019,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -23,9 +23,9 @@
{"type":"assistant/chunk","seq":21,"time":1785544945212,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}}}
{"type":"assistant/chunk","seq":22,"time":1785544945212,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":23,"time":1785730451364,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":24,"time":1785730451364,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"398dea92-a100-4b2f-a9e1-72629def132d"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"assistant/message","seq":24,"time":1785730451364,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ef6eadc7-165e-4705-b865-3889f0af0f36"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"tool/call","seq":25,"time":1785730451365,"data":{"turn":1,"step":2,"callId":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}
{"type":"tool/result","seq":26,"time":1785730451377,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message queued as the next turn for subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"45b388fd-6a48-4a02-9f3b-1d642e797c71"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
{"type":"tool/result","seq":26,"time":1785730451377,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message queued as the next turn for subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"ac1214a5-1d91-4fab-8f96-833baca114f8"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
{"type":"step/end","seq":27,"time":1785730451377,"data":{"turn":1,"step":2}}
{"type":"step/start","seq":28,"time":1785730451390,"data":{"turn":1,"step":3}}
{"type":"assistant/chunk","seq":29,"time":1789000000029,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -33,9 +33,9 @@
{"type":"assistant/chunk","seq":31,"time":1785544945241,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}}}
{"type":"assistant/chunk","seq":32,"time":1785544945242,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":33,"time":1785730451394,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":34,"time":1785730451394,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f56cec19-e761-4c77-9237-07d12d334275"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
{"type":"assistant/message","seq":34,"time":1785730451394,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"33939813-0792-4ac5-8864-ec62a4ddff8e"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
{"type":"tool/call","seq":35,"time":1785730451395,"data":{"turn":1,"step":3,"callId":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"33333333-3333-4333-8333-333333333333\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}
{"type":"tool/result","seq":36,"time":1785730451406,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message queued as the next turn for subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"193df31b-de9a-4723-b2b1-c7c96a8eaee4"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
{"type":"tool/result","seq":36,"time":1785730451406,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message queued as the next turn for subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"a6f64c64-f50c-47cd-a6b3-a3b57d3dc83d"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
{"type":"step/end","seq":37,"time":1785730451406,"data":{"turn":1,"step":3}}
{"type":"step/start","seq":38,"time":1785730451419,"data":{"turn":1,"step":4}}
{"type":"assistant/chunk","seq":39,"time":1789000000039,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -43,9 +43,9 @@
{"type":"assistant/chunk","seq":41,"time":1785544945273,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}}}
{"type":"assistant/chunk","seq":42,"time":1785544945273,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":43,"time":1785730451424,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":44,"time":1785730451424,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cbf54e23-fb96-45cc-b629-b9cb48fc9876"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}
{"type":"assistant/message","seq":44,"time":1785730451424,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"95eab91d-b103-4033-8e2e-c9c93b1b0211"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}
{"type":"tool/call","seq":45,"time":1785730451425,"data":{"turn":1,"step":4,"callId":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}
{"type":"tool/result","seq":46,"time":1785730451437,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"d5d961af-c171-40b6-87a1-33d2c14740a7"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[45],"surfaceOp":"append"}
{"type":"tool/result","seq":46,"time":1785730451437,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"8a095e4b-3059-420d-856f-1cbd20b6a2e2"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[45],"surfaceOp":"append"}
{"type":"step/end","seq":47,"time":1785730451437,"data":{"turn":1,"step":4}}
{"type":"step/start","seq":48,"time":1785730451450,"data":{"turn":1,"step":5}}
{"type":"assistant/chunk","seq":49,"time":1785394678779,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
@@ -53,6 +53,19 @@
{"type":"assistant/chunk","seq":51,"time":1785544945303,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":52,"time":1785544945303,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":53,"time":1785730451453,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":54,"time":1785730451453,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"fbea6b64-84cf-4411-abec-48d26b3801da"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}
{"type":"assistant/message","seq":54,"time":1785730451453,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"eb51ecb3-3347-4216-ad4e-c2130c43ecfc"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}
{"type":"step/end","seq":55,"time":1785730451454,"data":{"turn":1,"step":5}}
{"type":"turn/end","seq":56,"time":1785730451454,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"agent/inbox/spliced","seq":57,"time":1786011346660,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 failed before it finished.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"2cf0afd2-ee6e-4a3f-a35a-2fd4d5b665ca"}]}}
{"type":"turn/start","seq":58,"time":1786011346660,"data":{"turn":2}}
{"type":"agent/inbox/spliced","seq":59,"time":1786011346660,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":60,"time":1786012466689,"data":{"turn":2,"step":1}}
{"type":"user/message","seq":61,"time":1786012466689,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 failed before it finished.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"2cf0afd2-ee6e-4a3f-a35a-2fd4d5b665ca"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":62,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":63,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
{"type":"assistant/chunk","seq":64,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
{"type":"assistant/chunk","seq":65,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":66,"time":1786012466693,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":67,"time":1786333283620,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"758adcee-9284-4889-86a2-0181a278a754"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[62,63,64,65,66],"surfaceOp":"append"}
{"type":"step/end","seq":68,"time":1786333283620,"data":{"turn":2,"step":1}}
{"type":"turn/end","seq":69,"time":1786333283620,"data":{"turn":2,"reason":{"kind":"completed"}}}
@@ -2,3 +2,4 @@
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
@@ -247,7 +247,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -261,7 +261,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357533600,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth one"}}
{"type":"step/start","seq":5,"time":1786357533602,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730456014,"data":{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"},"role":"user","id":"a8129357-1bde-4cbd-90b4-6b8ad51d52e1"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357533602,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"722c8476-461f-4a02-904a-f1084b973c95"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357533602,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"78a8aa2f-27d5-45d1-8b06-7b2627fc76ae"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357533602,"data":{"title":"Call subagent once. Ask that","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730456014,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730456014,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357533628,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Start depth two"}}
{"type":"step/start","seq":5,"time":1786357533630,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730456041,"data":{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"},"role":"user","id":"d4dc5a16-e542-4dd9-8e82-e6b7829cfc4b"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357533630,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"fc837f97-bbf9-4c44-9eea-3a8f52c7a77d"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357533630,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"615d0b40-18da-47be-a235-7422989344f5"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357533630,"data":{"title":"Attempt one subagent call beyond","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730456041,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730456042,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -14,6 +14,13 @@
"op": "waitForFile",
"path": ".dsh-snapshot-subagent-settled"
},
{
"op": "waitForTurnStart",
"minimumTurn": 2
},
{
"op": "waitForTurnEnd"
},
{
"op": "prompt",
"text": "Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."
@@ -1,10 +1,10 @@
{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1789000000000,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"agent/inbox/spliced","seq":0,"time":1785730454756,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"c2febfff-792d-4457-a944-933ff0de0570"}]}}
{"type":"agent/inbox/spliced","seq":0,"time":1785730454756,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"356b3b62-c8b8-4d2a-84d7-7df1b6e4811e"}]}}
{"type":"turn/start","seq":1,"time":1785821412725,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785821412725,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1785730454783,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785730454783,"data":{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"c2febfff-792d-4457-a944-933ff0de0570"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730454783,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"c9d1f853-56bc-4082-ae08-00d4bcbb04a6"},"surfaceOp":"append"}
{"type":"user/message","seq":4,"time":1785730454783,"data":{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"356b3b62-c8b8-4d2a-84d7-7df1b6e4811e"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730454783,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"9be42fb0-f0d0-4ab9-a232-fb753f7db482"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730454783,"data":{"title":"Call the subagent tool once","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785730454784,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730454784,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -13,9 +13,9 @@
{"type":"assistant/chunk","seq":11,"time":1785531795632,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}}}
{"type":"assistant/chunk","seq":12,"time":1785531795632,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":13,"time":1785730454793,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":14,"time":1785730454793,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4dda49d6-5d02-456f-ba95-68699662793d"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"assistant/message","seq":14,"time":1785730454793,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"c8802574-4e43-4ee7-8648-5a132935b5dc"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"tool/call","seq":15,"time":1785730454793,"data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}
{"type":"tool/result","seq":16,"time":1785730454804,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"7f1fbe79-98b7-410a-af4d-c40d52a366dc"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"tool/result","seq":16,"time":1785730454804,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"c83395ad-93c6-4899-9ae1-8d29f92d4dde"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"step/end","seq":17,"time":1785730454804,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":18,"time":1785730454814,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":19,"time":1789000000017,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
@@ -23,39 +23,42 @@
{"type":"assistant/chunk","seq":21,"time":1785531795656,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}}}
{"type":"assistant/chunk","seq":22,"time":1785531795656,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":23,"time":1785730454820,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":24,"time":1785730454820,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"2ee459fa-21f9-48c6-a42e-3c38eca1e4c9"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"assistant/message","seq":24,"time":1785730454820,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"1fefe87b-4c3c-49b0-860c-8097193f9567"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"step/end","seq":25,"time":1785730454821,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":26,"time":1785730454821,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"agent/inbox/spliced","seq":27,"time":1785730454857,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"3bcac2a3-e0db-465b-94b4-4e2761236475"}]}}
{"type":"turn/start","seq":28,"time":1785821412840,"data":{"turn":2}}
{"type":"agent/inbox/spliced","seq":29,"time":1785821412840,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":30,"time":1785730454863,"data":{"turn":2,"step":1}}
{"type":"user/message","seq":31,"time":1785730454863,"data":{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"3bcac2a3-e0db-465b-94b4-4e2761236475"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":32,"time":1785531795686,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":33,"time":1785536135065,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_list","name":"list_agents","argumentsDelta":"{\"scope\":\"descendants\"}"}}}
{"type":"assistant/chunk","seq":34,"time":1785730454867,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{\"scope\":\"descendants\"}"}}}}
{"type":"assistant/chunk","seq":35,"time":1785730454867,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":36,"time":1785730454867,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":37,"time":1785730454867,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{\"scope\":\"descendants\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"199c7794-d782-4957-b7ed-69d094b9c0ef"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"}
{"type":"tool/call","seq":38,"time":1785730454867,"data":{"turn":2,"step":1,"callId":"call_list","name":"list_agents","arguments":"{\"scope\":\"descendants\"}"}}
{"type":"tool/result","seq":39,"time":1785730454893,"data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"33333333-3333-4333-8333-333333333333 [complete] parent=11111111-1111-4111-8111-111111111111 depth=1 — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"61ceb650-3e32-413e-9d7e-b6ec8a351858"}},"sourceEventSeqs":[38],"surfaceOp":"append"}
{"type":"step/end","seq":40,"time":1785730454893,"data":{"turn":2,"step":1}}
{"type":"step/start","seq":41,"time":1785730454903,"data":{"turn":2,"step":2}}
{"type":"assistant/chunk","seq":42,"time":1785531795715,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":43,"time":1785536135106,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_interrupt","name":"interrupt_agent","argumentsDelta":"{\"agent_id\":\"33333333-3333-4333-8333-333333333333\"}"}}}
{"type":"assistant/chunk","seq":44,"time":1785730454907,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_interrupt","name":"interrupt_agent","arguments":"{\"agent_id\":\"33333333-3333-4333-8333-333333333333\"}"}}}}
{"type":"assistant/chunk","seq":45,"time":1785730454907,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":46,"time":1785730454907,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":47,"time":1785730454907,"data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_interrupt","name":"interrupt_agent","arguments":"{\"agent_id\":\"33333333-3333-4333-8333-333333333333\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"17c1a7c5-84f5-493e-adb5-6a65219e6ad6"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[42,43,44,45,46],"surfaceOp":"append"}
{"type":"tool/call","seq":48,"time":1785730454907,"data":{"turn":2,"step":2,"callId":"call_interrupt","name":"interrupt_agent","arguments":"{\"agent_id\":\"33333333-3333-4333-8333-333333333333\"}"}}
{"type":"tool/result","seq":49,"time":1785730454917,"data":{"turn":2,"step":2,"message":{"source":{"kind":"tool","callId":"call_interrupt"},"content":[{"type":"tool-result","toolCallId":"call_interrupt","content":[{"type":"text","text":"interrupt requested for agent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"bf93cbe0-1946-4aef-a4ce-431790026c6f"}},"sourceEventSeqs":[48],"surfaceOp":"append"}
{"type":"step/end","seq":50,"time":1785730454917,"data":{"turn":2,"step":2}}
{"type":"step/start","seq":51,"time":1785730454927,"data":{"turn":2,"step":3}}
{"type":"assistant/chunk","seq":52,"time":1785531795715,"data":{"turn":2,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":53,"time":1785536135106,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
{"type":"assistant/chunk","seq":54,"time":1785730454907,"data":{"turn":2,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":55,"time":1785730454907,"data":{"turn":2,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
{"type":"assistant/chunk","seq":56,"time":1785730454907,"data":{"turn":2,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":57,"time":1785730454907,"data":{"turn":2,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e4c04cd6-9c23-4c99-a5f7-bac6fc141e95"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[52,53,54,55,56],"surfaceOp":"append"}
{"type":"step/end","seq":58,"time":1785730454938,"data":{"turn":2,"step":3}}
{"type":"turn/end","seq":59,"time":1785730454938,"data":{"turn":2,"reason":{"kind":"completed"}}}
{"type":"agent/inbox/spliced","seq":27,"time":1786011499211,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"9275a12c-bf9a-48e2-b33b-4fc484e936cb"}]}}
{"type":"turn/start","seq":28,"time":1786011499211,"data":{"turn":2}}
{"type":"agent/inbox/spliced","seq":29,"time":1786011499211,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":30,"time":1786012470343,"data":{"turn":2,"step":1}}
{"type":"user/message","seq":31,"time":1786012470343,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"9275a12c-bf9a-48e2-b33b-4fc484e936cb"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":32,"time":1786012470346,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":33,"time":1786011499224,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
{"type":"assistant/chunk","seq":34,"time":1786011499224,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
{"type":"assistant/chunk","seq":35,"time":1786011499224,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":36,"time":1786011499224,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":37,"time":1786012470346,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4620e8c0-dd13-4a2f-87dc-f4b66aa51219"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"}
{"type":"step/end","seq":38,"time":1786012470346,"data":{"turn":2,"step":1}}
{"type":"turn/end","seq":39,"time":1786012470346,"data":{"turn":2,"reason":{"kind":"completed"}}}
{"type":"agent/inbox/spliced","seq":40,"time":1786012470360,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"7a2a86d0-80a3-4db5-822f-2d3fcbc16e11"}]}}
{"type":"turn/start","seq":41,"time":1786011499224,"data":{"turn":3}}
{"type":"agent/inbox/spliced","seq":42,"time":1786011499224,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":43,"time":1786011499234,"data":{"turn":3,"step":1}}
{"type":"user/message","seq":44,"time":1786011499234,"data":{"content":[{"type":"text","text":"Call list_agents once with scope set to descendants and observe the subagent you started. Then call interrupt_agent once with agent_id set to 33333333-3333-4333-8333-333333333333. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"7a2a86d0-80a3-4db5-822f-2d3fcbc16e11"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":45,"time":1786011499238,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":46,"time":1786011499238,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_list","name":"list_agents","argumentsDelta":"{}"}}}
{"type":"assistant/chunk","seq":47,"time":1785531795715,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}}}
{"type":"assistant/chunk","seq":48,"time":1785536135106,"data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":49,"time":1785730454907,"data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":50,"time":1786011499238,"data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"d3402e92-2f7e-4cd5-9537-ae9beedeecab"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"}
{"type":"tool/call","seq":51,"time":1786011499238,"data":{"turn":3,"step":1,"callId":"call_list","name":"list_agents","arguments":"{}"}}
{"type":"tool/result","seq":52,"time":1786011499267,"data":{"turn":3,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"33333333-3333-4333-8333-333333333333 [complete] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"8ae233de-8fde-48d7-a9d0-0d9a480a00d0"}},"sourceEventSeqs":[51],"surfaceOp":"append"}
{"type":"step/end","seq":53,"time":1785730454908,"data":{"turn":3,"step":1}}
{"type":"step/start","seq":54,"time":1786011499275,"data":{"turn":3,"step":2}}
{"type":"assistant/chunk","seq":55,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":56,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
{"type":"assistant/chunk","seq":57,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":58,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
{"type":"assistant/chunk","seq":59,"time":1786011499279,"data":{"turn":3,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":60,"time":1786011499279,"data":{"turn":3,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3ac0f29f-72ae-44fb-9414-974470095618"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[55,56,57,58,59],"surfaceOp":"append"}
{"type":"step/end","seq":61,"time":1786011499279,"data":{"turn":3,"step":2}}
{"type":"turn/end","seq":62,"time":1786011499279,"data":{"turn":3,"reason":{"kind":"completed"}}}
@@ -2,5 +2,6 @@
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"STARTED"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}
@@ -247,7 +247,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -261,7 +261,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357524752,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Reply ALPHA only"}}
{"type":"step/start","seq":5,"time":1786357524755,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730450187,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"73ce401a-faaf-408a-879e-7485380d537d"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357524755,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"36cca213-d6e9-491d-9442-87057994f573"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357524755,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"8b8f6910-a5fc-4839-837f-9b9335252807"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357524755,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730450187,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730450188,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -27,7 +27,7 @@
{"type":"subagent/descriptor","seq":41,"time":1786357524800,"data":{"version":2,"mode":"one-shot","provider":"fork","label":"Recall project codeword"}}
{"type":"step/start","seq":42,"time":1786357524803,"data":{"turn":2,"step":1}}
{"type":"user/message","seq":43,"time":1786357524803,"data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"},"role":"user","id":"86e9f144-764f-460d-b72b-262cffe43d77"},"surfaceOp":"append"}
{"type":"user/message","seq":44,"time":1786358036899,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"86f2fe9d-3154-41c2-815e-e1c1e18ed895"},"surfaceOp":"append"}
{"type":"user/message","seq":44,"time":1786358036899,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"ccb25ca6-93e6-40e0-8713-ad065065d540"},"surfaceOp":"append"}
{"type":"request/header","seq":45,"time":1786358036900,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}}
{"type":"assistant/chunk","seq":46,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
{"type":"reasoning-chunks","seq0":47,"time0":1783352148077,"data":{"turn":2,"step":1,"index":0,"dt":[0,0,29,0,0,0,35,0,0,0,0,26,29,31,0,30,0,0,27,1,27,0,1,0,0,31,1,0,0,1790157964],"texts":["The"," user"," is"," asking"," me"," to"," recall"," the"," project"," cod","ew","ord"," that"," was"," mentioned"," earlier"," in"," the"," conversation","."," I"," was"," told"," to"," remember"," it",":"," SA","FF","RON","."]}}
@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357521754,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return ALPHA only"}}
{"type":"step/start","seq":5,"time":1786357521756,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730447828,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"a287f842-f6f2-4a17-ab4c-820e41f498d5"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357521756,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"c253c429-6ca2-4ac4-a192-ea1383e90a1e"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357521756,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"e35564a9-35f0-4a5c-8754-e5502bc6a35b"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357521756,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730447828,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730447828,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -6,7 +6,7 @@
{"type":"subagent/descriptor","seq":4,"time":1786357521799,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Return BETA only"}}
{"type":"step/start","seq":5,"time":1786357521801,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":1785730447881,"data":{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"},"role":"user","id":"53f6419d-8ddc-4eee-8803-5b68411336f9"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357521802,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"488666ba-7318-4b83-bc3f-d7700024b196"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":1786357521802,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"56f96521-b5b9-4e1f-8ca4-38ac7c212d99"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":1786357521802,"data":{"title":"Reply with exactly the word","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":1785730447881,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":1785730447881,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -1,10 +1,10 @@
{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1789000000000,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"agent/inbox/spliced","seq":0,"time":1785730453561,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Report a finding', and prompt 'Call the report tool once with output exactly CHILD_REPORT_OK, then stop.'. 2. Reply with the single word STARTED. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"e45e4b6d-dae7-42d9-9edb-d24385664025"}]}}
{"type":"agent/inbox/spliced","seq":0,"time":1785730453561,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Report a finding', and prompt 'Call the report tool once with output exactly CHILD_REPORT_OK, then stop.'. 2. Reply with the single word STARTED. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"b765ae32-73e2-4625-81ba-01095f8c83d0"}]}}
{"type":"turn/start","seq":1,"time":1785821411429,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785821411429,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1785730453591,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":4,"time":1785730453591,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Report a finding', and prompt 'Call the report tool once with output exactly CHILD_REPORT_OK, then stop.'. 2. Reply with the single word STARTED. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"e45e4b6d-dae7-42d9-9edb-d24385664025"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730453592,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"9319509b-82df-4848-8f3a-7654804c9c45"},"surfaceOp":"append"}
{"type":"user/message","seq":4,"time":1785730453591,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Report a finding', and prompt 'Call the report tool once with output exactly CHILD_REPORT_OK, then stop.'. 2. Reply with the single word STARTED. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"b765ae32-73e2-4625-81ba-01095f8c83d0"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785730453592,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"1f4f5888-2068-4df0-904f-12ffb4aa3321"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":1785730453592,"data":{"title":"Follow these steps exactly, then","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":1785730453592,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":8,"time":1785730453593,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -13,9 +13,9 @@
{"type":"assistant/chunk","seq":11,"time":1785501592851,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}}}}
{"type":"assistant/chunk","seq":12,"time":1785501592851,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":13,"time":1785730453601,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":14,"time":1785730453602,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"dcd1402a-35c7-4748-bfeb-8734e1018732"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"assistant/message","seq":14,"time":1785730453602,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"97b897d5-0d01-4a6c-ad0c-4776c61c9c68"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"tool/call","seq":15,"time":1785730453602,"data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}}
{"type":"tool/result","seq":16,"time":1785730453613,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"b752b617-5282-40b6-bfe0-0535fb069504"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"tool/result","seq":16,"time":1785730453613,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"91fb94ce-cf3e-47ed-ab20-8f46cf4aec55"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
{"type":"step/end","seq":17,"time":1785730453613,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":18,"time":1785730453623,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":19,"time":1789000000018,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
@@ -23,32 +23,35 @@
{"type":"assistant/chunk","seq":21,"time":1785501592877,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}}}
{"type":"assistant/chunk","seq":22,"time":1785501592877,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":23,"time":1785730453628,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":24,"time":1785730453628,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"d5bf8946-4dd4-464e-88de-c7d6191c7671"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"assistant/message","seq":24,"time":1785730453628,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"b1b1cf78-11a8-4440-b9f9-2096d15e7884"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"step/end","seq":25,"time":1785730453628,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":26,"time":1785730453629,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"agent/inbox/spliced","seq":27,"time":1786008565468,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"2ba9ef7b-dac7-4f39-8035-35f0bf88d0e7"}]}}
{"type":"turn/start","seq":28,"time":1786008565468,"data":{"turn":2}}
{"type":"agent/inbox/spliced","seq":29,"time":1786008565468,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":30,"time":1786008565474,"data":{"turn":2,"step":1}}
{"type":"user/message","seq":31,"time":1786008565474,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"2ba9ef7b-dac7-4f39-8035-35f0bf88d0e7"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":32,"time":1786008565479,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":33,"time":1786008565479,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"REPORT_RECEIVED"}}}
{"type":"assistant/chunk","seq":34,"time":1786008565479,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"REPORT_RECEIVED"}}}}
{"type":"assistant/chunk","seq":35,"time":1786008565479,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":36,"time":1786008565479,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":37,"time":1786008565479,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"REPORT_RECEIVED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0fe8d69e-6640-4bd5-b72b-70f2cf013cde"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"}
{"type":"step/end","seq":38,"time":1786008565479,"data":{"turn":2,"step":1}}
{"type":"turn/end","seq":39,"time":1786008565479,"data":{"turn":2,"reason":{"kind":"completed"}}}
{"type":"agent/inbox/spliced","seq":40,"time":1786008565505,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"50d55072-bbf2-4e30-8948-1b837d8db9ee"}]}}
{"type":"turn/start","seq":41,"time":1786008565505,"data":{"turn":3}}
{"type":"agent/inbox/spliced","seq":42,"time":1786008565505,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":43,"time":1786008565511,"data":{"turn":3,"step":1}}
{"type":"user/message","seq":44,"time":1786008565511,"data":{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"50d55072-bbf2-4e30-8948-1b837d8db9ee"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":45,"time":1786008565516,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":46,"time":1786008565516,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_REPORT_OK"}}}
{"type":"assistant/chunk","seq":47,"time":1786008565516,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_REPORT_OK"}}}}
{"type":"assistant/chunk","seq":48,"time":1786008565516,"data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":49,"time":1786008565516,"data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":50,"time":1786008565516,"data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"de2ca55e-a346-4f25-911d-a33316b9866b"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"}
{"type":"step/end","seq":51,"time":1786008565516,"data":{"turn":3,"step":1}}
{"type":"turn/end","seq":52,"time":1786008565516,"data":{"turn":3,"reason":{"kind":"completed"}}}
{"type":"agent/inbox/spliced","seq":27,"time":1786008565468,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"ec024a7a-5506-4ebf-a9d8-82ce01dc88b4"}]}}
{"type":"agent/inbox/spliced","seq":28,"time":1786012334062,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"Reported."}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"08101cc1-abde-49ca-9745-1d075a3911b5"}]}}
{"type":"turn/start","seq":29,"time":1786012334062,"data":{"turn":2}}
{"type":"agent/inbox/spliced","seq":30,"time":1786012334062,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
{"type":"agent/inbox/spliced","seq":31,"time":1786012334062,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":32,"time":1786376144953,"data":{"turn":2,"step":1}}
{"type":"user/message","seq":33,"time":1786376144953,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"ec024a7a-5506-4ebf-a9d8-82ce01dc88b4"},"surfaceOp":"append"}
{"type":"user/message","seq":34,"time":1786012334068,"data":{"content":[{"type":"text","text":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"Reported."}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent 33333333-3333-4333-8333-333333333333 finished and will do no further work unless you send it more.","senderSessionId":"33333333-3333-4333-8333-333333333333"},"role":"user","id":"08101cc1-abde-49ca-9745-1d075a3911b5"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":35,"time":1786376144959,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":36,"time":1786012334073,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
{"type":"assistant/chunk","seq":37,"time":1786012334073,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
{"type":"assistant/chunk","seq":38,"time":1786012334073,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":39,"time":1786012334073,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":40,"time":1786376144959,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"c6fecd6e-033f-4be8-98ee-cc0733b18c83"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[35,36,37,38,39],"surfaceOp":"append"}
{"type":"step/end","seq":41,"time":1786376144960,"data":{"turn":2,"step":1}}
{"type":"turn/end","seq":42,"time":1786376144960,"data":{"turn":2,"reason":{"kind":"completed"}}}
{"type":"agent/inbox/spliced","seq":43,"time":1786376145165,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"6c0b0e51-4ad9-4c4b-bbbc-508973862b77"}]}}
{"type":"turn/start","seq":44,"time":1786012334073,"data":{"turn":3}}
{"type":"agent/inbox/spliced","seq":45,"time":1786012334073,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":46,"time":1786012334082,"data":{"turn":3,"step":1}}
{"type":"user/message","seq":47,"time":1786012334083,"data":{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"6c0b0e51-4ad9-4c4b-bbbc-508973862b77"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":48,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":49,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_REPORT_OK"}}}
{"type":"assistant/chunk","seq":50,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_REPORT_OK"}}}}
{"type":"assistant/chunk","seq":51,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":52,"time":1786012334087,"data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":53,"time":1786012334087,"data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"98d44266-6695-482b-910c-0e1e570fe7a6"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[48,49,50,51,52],"surfaceOp":"append"}
{"type":"step/end","seq":54,"time":1786012334087,"data":{"turn":3,"step":1}}
{"type":"turn/end","seq":55,"time":1786012334087,"data":{"turn":3,"reason":{"kind":"completed"}}}
@@ -2,6 +2,6 @@
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"STARTED"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"REPORT_RECEIVED"}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"CHILD_REPORT_OK"}}}}
{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}
@@ -247,7 +247,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -261,7 +261,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -231,7 +231,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -245,7 +245,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -231,7 +231,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: you receive only its subagent id, never its result, and it works on its own. Use this for work whose result you do not need returned by this call; `send_message` sends it more work.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a background subagent that keeps its conversation: this call returns only its subagent id, and the subagent works on its own from there. You are told when it finishes, so never poll or wait on it; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -245,7 +245,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call never returns its result; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return only its subagent id. This call does not wait for it; you are told when it finishes. Send it more work with send_message."
}
},
"required": [
@@ -0,0 +1,21 @@
# Keyless assembled-app coverage for continuable child settlement delivery. The
# replay child deliberately never calls report; the parent can reach its final
# answer only if the continuation manager places the child's closing message in
# the parent turn without list_agents, send_message, or a Task collector.
- id: base
name: '@deepseek-ai/cordis-plugin-include'
config:
path: ./cordis.yml
patches:
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- insert:
- id: llm-replay
name: '@deepseek-ai/dsh-llm-replay'
# Prevent platform scheduling from choosing a streamed-chunk interleave. The
# fence releases only after the real manager notice enters the parent inbox.
- id: settlement-fence
name: './tests/fixtures/subagent-settlement-fence.ts'
@@ -0,0 +1,43 @@
/**
* Loader fixture that holds the parent's second step until settlement delivery.
* @module subagent-settlement-fence
*/
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-agent-loop'
import type {} from '@deepseek-ai/dsh-subagent'
/** Fixture plugin name. */
export const name = 'subagent-settlement-fence'
/**
* Fence the parent's post-spawn request behind admission of the manager notice.
*
* This pins content order, not step placement: the held pre-step runs after its
* own `Inbox.claim()`, so the notice lands after step 2's claim and is claimed at
* step 3 because the child's settlement pipeline is strictly longer than the
* parent's claim path, not because a barrier forces it.
* @param ctx - assembled headless-agent context.
*/
export function apply(ctx: Context): void {
const delivered = Promise.withResolvers<undefined>()
let hasDelivered = false
ctx.effect(() => {
const disposeInbox = ctx.root.on('agent/inbox/inserted', ({ agent, message }) => {
if (agent.session.header.parentSession !== undefined || message.source.kind !== 'subagent-settled') return
hasDelivered = true
delivered.resolve(undefined)
})
const disposeStep = ctx.root.on('agent/pre-step', async ({ agent, turn, step }, next) => {
if (agent.session.header.parentSession === undefined && turn === 1 && step === 2 && !hasDelivered) {
await delivered.promise
}
return next()
})
return () => {
disposeStep()
disposeInbox()
}
}, 'subagent-settlement-fence.listeners')
}
@@ -45,6 +45,8 @@ const credentialsConfigPath = fileURLToPath(new URL('../credentials.cordis.snaps
const invalidCredentialScenarioDir = join(snapshotsDir, 'invalid-credential')
const ralphScenarioDir = join(snapshotsDir, 'ralph-loop')
const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', import.meta.url))
const settlementScenarioDir = join(snapshotsDir, 'subagent-settlement')
const settlementConfigPath = fileURLToPath(new URL('../subagent-settlement.cordis.snapshot.yml', import.meta.url))
const startupFailureConfigPath = fileURLToPath(new URL('./fixtures/startup-activation-error/cordis.yml', import.meta.url))
const startupFailureExpected = join(snapshotsDir, 'startup-activation-error', 'stderr.expected.txt')
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
@@ -777,6 +779,77 @@ describe('headless stream-json snapshots', () => {
expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('delivers a continuable child result without parent polling', async () => {
const parentReplay = join(settlementScenarioDir, 'parent.replay.jsonl')
const parentOverride = join(settlementScenarioDir, 'parent.override.json')
const childReplay = join(settlementScenarioDir, 'child.replay.jsonl')
const childExpected = join(settlementScenarioDir, 'child.expected.jsonl')
const streamExpected = join(settlementScenarioDir, 'stream-json.expected.jsonl')
const task = 'Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, task_output, or task_list.'
let runCwd = ''
const result = await runLoaderSmoke({
label: 'continuable settlement headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-subagent-settlement-',
binScript,
libBinScript: binScript,
configPath: settlementConfigPath,
binArgs: [settlementConfigPath, task],
tsconfigPath,
env: {
// The override fully supplies the parent script; the child fixture
// remains separate so replay binds it to the fresh child Session.
DSH_SNAPSHOT_FILE: parentReplay,
DSH_SNAPSHOT_OVERRIDE: parentOverride,
DSH_SNAPSHOT_CHILD_FILES: childReplay,
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
},
prepare: (cwd) => { runCwd = cwd },
inspect: async (cwd) => {
const logs = await persistedLogs(cwd)
expect(logs).toHaveLength(2)
const parent = logs.find(log => typeof log.header.parentSession !== 'string')
const child = logs.find(log => typeof log.header.parentSession === 'string')
if (parent === undefined || child === undefined) throw new Error('missing persisted parent or child log')
const parentRecords = parseJsonl(parent.content)
const calls = parentRecords.filter(record => record.type === 'tool/call')
expect(calls.map(record => (record.data as JsonObject | undefined)?.name)).toEqual(['subagent'])
const callArguments = (calls[0]?.data as JsonObject | undefined)?.arguments
if (typeof callArguments !== 'string') throw new Error('subagent call did not persist its arguments')
expect(JSON.parse(callArguments)).toMatchObject({ run_in_background: true })
const notices = parentRecords.flatMap((record) => {
if (record.type !== 'agent/inbox/spliced') return []
const inserted = (record.data as JsonObject | undefined)?.inserted
if (!Array.isArray(inserted)) return []
return (inserted as JsonObject[]).filter((message) => {
const source = message.source as JsonObject | undefined
return source?.kind === 'subagent-settled'
})
})
expect(notices).toHaveLength(1)
expect(JSON.stringify(notices[0])).toContain('CHILD_RESULT')
const context = contextFromLogs([parent.content, child.content])
const normalizedChild = scrubRequestHeaders(normalizeSessionLog(child.content, context))
if (refreshing) await writeFile(childExpected, normalizedChild)
expect(normalizedChild).toBe(await readFile(childExpected, 'utf8'))
expect(normalizedChild).toContain('CHILD_RESULT')
expect(normalizedChild).not.toContain('"name":"report"')
},
})
expect(result.stderr).toBe('')
const records = parseJsonl(result.stdout)
expect(records.at(-1)).toMatchObject({
type: 'result',
output: 'PARENT_RECEIVED_CHILD_RESULT',
})
const normalized = normalizeHeadlessStream(result.stdout, runCwd)
if (refreshing) await writeFile(streamExpected, normalized)
expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('replays persistent PTY tools through the one-shot app', async () => {
const input = JSON.parse(await readFile(join(ptyScenarioDir, 'input.json'), 'utf8')) as {
steps?: { op?: unknown; text?: unknown }[]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{sessionId}}","origin":"subagent","delegationDepth":1}
{"type":"subagent/descriptor","seq":0,"time":0,"data":{"version":2,"mode":"continuable","provider":"spawn","label":"Return child result","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}}
{"type":"session/end-seed","seq":1,"time":0,"data":{}}
{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call report."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
{"type":"turn/start","seq":3,"time":0,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":4,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":5,"time":0,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":6,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call report."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","seq":7,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":8,"time":0,"data":{"title":"Reply with exactly CHILD_RESULT and","messageSeqs":[6],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":9,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":10,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_RESULT"}}}
{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_RESULT"}}}}
{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":16,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"}
{"type":"step/end","seq":17,"time":0,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":18,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -0,0 +1,6 @@
{"type":"session","version":0,"id":"subagent-settlement-child","createdAt":2,"delegationDepth":1}
{"type":"assistant/chunk","seq":0,"time":1,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":1,"time":2,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CHILD_RESULT"}}}
{"type":"assistant/chunk","seq":2,"time":3,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_RESULT"}}}}
{"type":"assistant/chunk","seq":3,"time":4,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":4,"time":5,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
@@ -0,0 +1,32 @@
[
{
"kind": "chunks",
"chunks": [
{ "type": "block-start", "index": 0, "blockType": "tool-call" },
{ "type": "tool-call-delta", "index": 0, "id": "start-child", "name": "subagent", "argumentsDelta": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\",\"run_in_background\":true}" },
{ "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "start-child", "name": "subagent", "arguments": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\",\"run_in_background\":true}" } },
{ "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } },
{ "type": "finish", "reason": { "kind": "tool-calls" } }
]
},
{
"kind": "chunks",
"chunks": [
{ "type": "block-start", "index": 0, "blockType": "text" },
{ "type": "text-delta", "index": 0, "text": "STARTED" },
{ "type": "block-end", "index": 0, "block": { "type": "text", "text": "STARTED" } },
{ "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } },
{ "type": "finish", "reason": { "kind": "stop" } }
]
},
{
"kind": "chunks",
"chunks": [
{ "type": "block-start", "index": 0, "blockType": "text" },
{ "type": "text-delta", "index": 0, "text": "PARENT_RECEIVED_CHILD_RESULT" },
{ "type": "block-end", "index": 0, "block": { "type": "text", "text": "PARENT_RECEIVED_CHILD_RESULT" } },
{ "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } },
{ "type": "finish", "reason": { "kind": "stop" } }
]
}
]
@@ -0,0 +1 @@
{"type":"session","version":0,"id":"subagent-settlement-parent","createdAt":1,"delegationDepth":0}
@@ -0,0 +1,38 @@
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":0,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, task_output, or task_list."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":1,"time":0,"data":{"turn":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, task_output, or task_list."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":5,"time":0,"data":{"title":"Start one continuable background subagen","messageSeqs":[4],"source":{"kind":"fallback"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":6,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":7,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"start-child","name":"subagent","argumentsDelta":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\",\"run_in_background\":true}"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\",\"run_in_background\":true}"}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\",\"run_in_background\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":14,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\",\"run_in_background\":true}"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"start-child"},"content":[{"type":"tool-result","toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":17,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":18,"time":0,"data":{"turn":1,"step":2}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"STARTED"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":25,"time":0,"data":{"turn":1,"step":2}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":26,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":27,"time":0,"data":{"turn":1,"step":3}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":28,"time":0,"data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":0,"text":"PARENT_RECEIVED_CHILD_RESULT"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_RECEIVED_CHILD_RESULT"}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":34,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_RECEIVED_CHILD_RESULT"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":35,"time":0,"data":{"turn":1,"step":3}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":36,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}
{"type":"result","sessionId":"{{sessionId}}","output":"PARENT_RECEIVED_CHILD_RESULT","usage":{"inputTokens":30,"outputTokens":15}}
+2
View File
@@ -38,6 +38,7 @@
"headless-agent/tests/fixtures/semantic-checkpoint-agent.ts",
"headless-agent/tests/fixtures/subagent-diagnostic-agent.ts",
"headless-agent/tests/fixtures/subagent-inheritance-agent.ts",
"headless-agent/tests/fixtures/subagent-settlement-fence.ts",
"headless-agent/tests/fixtures/workspace-context-resume-agent.ts",
"headless-agent/tests/fixtures/goal-domain/seed-goal.ts",
"headless-agent/tests/fixtures/time-context-driver.ts",
@@ -50,6 +51,7 @@
"acp-agent/tests/fixtures/parent-sandbox-override.ts",
"acp-agent/tests/fixtures/partial-landlock-sandbox.ts",
"acp-agent/tests/fixtures/subagent-durability-failure.ts",
"acp-agent/tests/fixtures/subagent-report-fence.ts",
"acp-agent/tests/fixtures/subagent-settlement-marker.ts",
"acp-agent/tests/fixtures/workspace-context-compaction.ts",
"acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts",
+2 -2
View File
@@ -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/core/agent/README.md
README.md: 0fb65b94a3b311aa9f0df09d39dd937cba4cc7b4
README.zh.md: 44f67483343a98c280317793ece544bd0b984596
README.md: 61294df5cca1b03f9f158678f2992a6e4fbaaffd
README.zh.md: 5576a07330926cfd8fd5bd42c8a0351a375f035c
+2
View File
@@ -58,6 +58,8 @@ Inbox live notifications are deliberately per-message and minimal: `agent/inbox/
Turn and step boundaries and the model token stream are durable `session/event` facts rather than mirrored `agent/*` notifications. Consumers read `turn/*`, `step/*`, and `assistant/chunk` from the session feed; tool policy and outcome observation belong to the complete pipeline documented by [`dsh-tools`](../tools/README.md).
`foldConsumedWork(events)` reads that feed back for the one question the turn sequence cannot answer alone: what became of the work a log consumed. It returns the latest `turn/end` that accounts for consumed work — a turn that entered a model step, or one that claimed inbox input and then failed, was stopped, or was rejected before reaching one — plus whether accepted work was later cancelled out of the inbox unrun. Both facts come from the log, so a cancellation reads the same whichever owner issued it. A no-step turn that took nothing, or emptied its claim and completed, describes no work and is skipped; a `blocked` end over claimed input is an account, because rejection discarded that input.
### Agent interface (`types.ts`)
The handle every plugin programs against:
+2
View File
@@ -58,6 +58,8 @@ inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserte
轮次和步骤边界以及模型 token 流是持久 `session/event` 事实,而不是镜像的 `agent/*` 通知。消费方从会话事件流读取 `turn/*``step/*``assistant/chunk`;工具策略与结果观测属于 [`dsh-tools`](../tools/README.md) 记录的完整流水线。
`foldConsumedWork(events)` 把这条事件流读回来,回答仅凭轮次序列无法回答的那个问题:一份日志消费掉的工作最终怎样了。它返回能够为已消费工作作出交代的最新 `turn/end`——即进入过模型 step 的轮次,或者认领了 inbox 输入、但在进入 step 之前失败、被停下或被拒绝的轮次——并额外给出「已接受的工作此后是否被从 inbox 中取消且从未运行」。两项事实都来自日志,因此无论由哪个所有者发起取消,读出来都一样。没有取走任何输入、或认领批次被改写清空后正常结束的无 step 轮次不描述工作,会被跳过;认领过输入、以 `blocked` 结束的轮次则是一份交代,因为拒绝把这些输入一并丢弃了。
### Agent 接口(`types.ts`
每个插件面向的 handle
+108
View File
@@ -0,0 +1,108 @@
/**
* How one agent log accounts for the work it consumed.
*
* The turn and step vocabulary alone cannot answer this. A turn that stops
* before its first step leaves a `turn/end` shaped exactly like the balanced
* no-op turns a rejection or an empty claim produces, so reading turns in
* isolation either credits cut-short work as finished or convicts every no-op.
* The missing fact is the inbox's own record: {@link Inbox} logs each mutation
* with `removedCount` and marks a cancellation `outcome: 'canceled'`, which
* separates a turn claiming its input from work being dropped unrun.
*
* @module @deepseek-ai/dsh-agent/consumed-work
*/
import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session'
/** How one agent log accounts for the work it consumed. */
export interface ConsumedWork {
/**
* The latest closed turn that accounts for consumed work: one that entered a
* model step, or one that claimed inbox input and then failed, was stopped,
* or was rejected. Absent when no turn closed over any work.
*/
readonly end?: SessionEvent<'turn/end'>
/**
* Whether accepted work was cancelled out of the inbox, unrun, after that
* turn. This is the only account of input a cancellation took before any turn
* could open over it — no `turn/end` describes it.
*/
readonly droppedUnrun: boolean
}
/**
* Whether a turn that consumed input but never reached a step ends in a way
* that accounts for that input. Only a `completed` end does not: it had
* nothing left to run once its claim was rewritten away. A `blocked` end is
* that input's ending too — the pre-step rejection that produced it discarded
* the claimed messages, so the work it took will never run.
* @param reason - the turn's recorded ending.
* @returns whether the ending accounts for the input the turn took.
*/
function accountsForClaim(reason: TurnEndReason): boolean {
switch (reason.kind) {
case 'completed':
return false
case 'blocked':
case 'aborted':
case 'interrupted':
case 'error':
return true
/* v8 ignore next 4 -- unreachable: the one unnamed built-in, `max-tokens`, requires a step,
* so its turn short-circuits as stepped before this call, and `TurnEndReasonMap` is
* merge-extensible, so a backend-added variant cannot be listed; an unnameable ending over
* consumed input must not read as success. */
default:
return true
}
}
/**
* Fold one agent log, or an owned suffix of one, into its account of consumed
* work. Single pass, and every input is the log itself: no caller has to sample
* live state before cancelling, so a cancellation issued by anyone — the owner's
* teardown, an ancestor's interrupt, an unloading plugin — reads the same.
* @param events - the log, or an owned suffix, to fold.
* @returns the accounting turn when one closed, and whether work was dropped unrun after it.
*/
export function foldConsumedWork(events: readonly SessionEvent[]): ConsumedWork {
const stepped = new Set<number>()
const claimed = new Set<number>()
let open: number | undefined
let end: SessionEvent<'turn/end'> | undefined
let droppedUnrun = false
for (const event of events) {
switch (event.type) {
case 'turn/start':
open = event.data.turn
break
case 'step/start':
stepped.add(event.data.turn)
break
case 'agent/inbox/spliced': {
const { removedCount, outcome, inserted } = event.data
if (removedCount === undefined) break
// A replacement keeps the work pending under a new identity, so only a
// cancellation that leaves nothing behind drops it.
if (outcome === 'canceled') droppedUnrun ||= inserted.length === 0
// Claims are the loop's own step-boundary reads, always inside a turn.
else if (open !== undefined) claimed.add(open)
break
}
case 'turn/end': {
const { turn, reason } = event.data
open = undefined
if (stepped.delete(turn) || (claimed.delete(turn) && accountsForClaim(reason))) {
end = event
// Anything dropped before this turn closed is what its own ending
// reports; only a later drop is still unaccounted for.
droppedUnrun = false
}
break
}
default:
break
}
}
return { ...end === undefined ? {} : { end }, droppedUnrun }
}
+1
View File
@@ -18,6 +18,7 @@ import type { Agent, AgentOptions } from './runtime-types.ts'
export * from './runtime-types.ts'
export * from './types.ts'
export * from './inbox.ts'
export * from './consumed-work.ts'
export * from './model-selection.ts'
export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from './dispatch.ts'
export type { AgentEventDispatch, AgentSubjectEvent } from './dispatch.ts'
@@ -0,0 +1,160 @@
import { describe, expect, it } from 'vitest'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { TurnEndReason } from '@deepseek-ai/dsh-session'
import { foldConsumedWork } from '@deepseek-ai/dsh-agent'
/** One pending message, as the inbox records it. */
function message(text: string) {
return createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } })
}
/** Log an accepted message the way `Inbox.append()` does. */
function accept(session: Session, text: string): void {
session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [message(text)] })
}
/** Log the step-boundary read of one pending message, as `Inbox.claim()` does. */
function claim(session: Session): void {
session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, removedCount: 1, inserted: [] })
}
/** Log a cancellation of one pending message, as `Inbox.clear()` does. */
function cancelPending(session: Session): void {
session.append('agent/inbox/spliced', {
target: 'next-turn', start: 0, removedCount: 1, inserted: [], outcome: 'canceled',
})
}
/** Run one whole turn that reached a model step. */
function steppedTurn(session: Session, turn: number, reason: TurnEndReason): void {
session.append('turn/start', { turn })
claim(session)
session.append('step/start', { turn, step: 1 })
session.append('step/end', { turn, step: 1 })
session.append('turn/end', { turn, reason })
}
describe('foldConsumedWork', () => {
it('reports nothing for a log that consumed no work', () => {
const session = Session.create(SessionId('empty'))
accept(session, 'queued')
expect(foldConsumedWork(session.events)).toEqual({ droppedUnrun: false })
})
it('reports the latest turn that entered a model step', () => {
const session = Session.create(SessionId('stepped'))
steppedTurn(session, 1, { kind: 'completed' })
steppedTurn(session, 2, { kind: 'max-tokens' })
expect(foldConsumedWork(session.events).end?.data)
.toEqual({ turn: 2, reason: { kind: 'max-tokens' } })
})
it('reports a turn that claimed its input and then failed before any step', () => {
const session = Session.create(SessionId('failed-claim'))
steppedTurn(session, 1, { kind: 'completed' })
// The step boundary runs the durability checkpoint and prompt assembly, so a
// turn can take its input and then fail without entering a step.
session.append('turn/start', { turn: 2 })
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'error', error: { message: 'ENOSPC', code: 'UNKNOWN' } } })
expect(foldConsumedWork(session.events).end?.data.turn).toBe(2)
})
it('reports a turn that claimed its input and was then stopped before any step', () => {
const session = Session.create(SessionId('stopped-claim'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('turn/start', { turn: 2 })
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'aborted', reason: { kind: 'user' } } })
expect(foldConsumedWork(session.events).end?.data.turn).toBe(2)
})
it('ignores a turn stopped, failed, or rejected without taking any input', () => {
const session = Session.create(SessionId('no-claim'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('turn/start', { turn: 2 })
session.append('turn/end', { turn: 2, reason: { kind: 'aborted', reason: { kind: 'parent' } } })
session.append('turn/start', { turn: 3 })
session.append('turn/end', { turn: 3, reason: { kind: 'error', error: { message: 'x', code: 'UNKNOWN' } } })
session.append('turn/start', { turn: 4 })
session.append('turn/end', { turn: 4, reason: { kind: 'blocked' } })
// None of these turns describes work: they opened, found nothing of their own, and closed.
expect(foldConsumedWork(session.events).end?.data.turn).toBe(1)
})
it('reports a turn whose claimed input a pre-step rejection discarded', () => {
const session = Session.create(SessionId('rejected-claim'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('turn/start', { turn: 2 })
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'blocked' } })
// Rejection does not retain the claimed messages, so the `blocked` end is
// the only account of input that will never run.
expect(foldConsumedWork(session.events).end?.data.turn).toBe(2)
})
it('ignores a claim its own turn emptied', () => {
const session = Session.create(SessionId('emptied-claim'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('turn/start', { turn: 2 })
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
// An emptied claim ran nothing and dropped nothing: a listener rewrote the
// batch away, which is not this log's account of the work.
expect(foldConsumedWork(session.events).end?.data.turn).toBe(1)
})
it('credits a claim with no open turn to no turn at all', () => {
const session = Session.create(SessionId('mid-turn-suffix'))
steppedTurn(session, 1, { kind: 'completed' })
// An owned suffix can begin inside a turn whose start it does not contain,
// so a claim may appear with no turn to attribute it to.
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'aborted', reason: { kind: 'user' } } })
expect(foldConsumedWork(session.events).end?.data.turn).toBe(1)
})
it('reports work cancelled out of the inbox after the last accounting turn', () => {
const session = Session.create(SessionId('dropped'))
steppedTurn(session, 1, { kind: 'completed' })
accept(session, 'never runs')
cancelPending(session)
// No turn opened over it, so only the cancellation says the work was cut short.
expect(foldConsumedWork(session.events)).toEqual({
end: session.events.find(event => event.type === 'turn/end'),
droppedUnrun: true,
})
})
it('keeps a replacement pending rather than counting it as dropped', () => {
const session = Session.create(SessionId('replaced'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('agent/inbox/spliced', {
target: 'next-turn', start: 0, removedCount: 1, inserted: [message('rewritten')], outcome: 'canceled',
})
expect(foldConsumedWork(session.events).droppedUnrun).toBe(false)
})
it('lets a later accounting turn absorb an earlier drop', () => {
const session = Session.create(SessionId('absorbed'))
steppedTurn(session, 1, { kind: 'completed' })
cancelPending(session)
steppedTurn(session, 2, { kind: 'completed' })
expect(foldConsumedWork(session.events)).toEqual({
end: session.events.findLast(event => event.type === 'turn/end'),
droppedUnrun: false,
})
})
})
-21
View File
@@ -34,27 +34,6 @@ export { deriveEventMessage, foldSurface, isAppendSurfaceEvent, isReplacementSur
export { canonicalHeader, foldRequestHeader, headerEquals } from './request-header.ts'
export { KNOWN_SESSION_EVENT_TYPES } from './known-event-types.ts'
/**
* Find the latest closed turn that entered at least one model step, ignoring
* balanced no-step turns produced by rejection, empty input, or cancellation.
* @param events - session events, or an owned suffix, to inspect.
* @returns the latest matching turn end, or `undefined`.
*/
export function findLastMessageTurnEnd(
events: readonly SessionEvent[],
): SessionEvent<'turn/end'> | undefined {
const steppedTurns = new Set<number>()
let latest: SessionEvent<'turn/end'> | undefined
for (const event of events) {
if (event.type === 'step/start') {
steppedTurns.add(event.data.turn)
continue
}
if (event.type === 'turn/end' && steppedTurns.delete(event.data.turn)) latest = event
}
return latest
}
declare module '@deepseek-ai/cordis' {
interface Context {
sessions: SessionStore
@@ -7,28 +7,11 @@ import SessionStore, {
Session,
SessionEvent,
SessionId,
findLastMessageTurnEnd,
snapshotSessionEvent,
} from '@deepseek-ai/dsh-session'
import type { CreateSessionOptions, SessionEventType, SessionHeader, SessionSurface, TodoItem } from '@deepseek-ai/dsh-session'
describe('Session', () => {
it('finds the latest closed turn that entered a model step', () => {
const session = Session.create(SessionId('last-message-turn'))
session.append('turn/start', { turn: 1 })
session.append('turn/end', { turn: 1, reason: { kind: 'blocked' } })
expect(findLastMessageTurnEnd(session.events)).toBeUndefined()
session.append('turn/start', { turn: 2 })
session.append('step/start', { turn: 2, step: 1 })
session.append('step/end', { turn: 2, step: 1 })
session.append('turn/end', { turn: 2, reason: { kind: 'max-tokens' } })
expect(findLastMessageTurnEnd(session.events)?.data)
.toEqual({ turn: 2, reason: { kind: 'max-tokens' } })
})
it('exposes one stable readonly surface view', () => {
const session = Session.create(SessionId('surface-view'))
const surface = session.surface
@@ -13,8 +13,9 @@
import { randomUUID } from 'node:crypto'
import type { Context } from '@deepseek-ai/cordis'
import { foldConsumedWork } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
import { findLastMessageTurnEnd, SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
import { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
import { createUserMessage, type ContentBlock } from '@deepseek-ai/dsh-llm'
import {
appendDelegatedPolicyOverrides,
@@ -52,6 +53,10 @@ function toStopReason(reason: TurnEndReason | undefined): SubagentStopReason {
return 'max-tokens'
case 'aborted':
return 'aborted'
// A pre-step rejection discarded the claimed prompt: the task was
// declined, and the caller must not read the run as done.
case 'blocked':
return 'refusal'
case 'error':
case 'interrupted':
default:
@@ -207,7 +212,11 @@ function readResult(
structured?: { captured?: { value: unknown } | undefined },
): SubagentResult {
const own = child.session.events.slice(boundary)
const lastEnd = findLastMessageTurnEnd(own)
// `droppedUnrun` is deliberately unread: a one-shot prompt is claimed by its
// awaited first turn almost immediately, and the owner's own teardown is the
// `cancelled` flag below. A cancellation with no accounting turn resolves
// `error` through `toStopReason(undefined)`, which never overstates success.
const lastEnd = foldConsumedWork(own).end
// The seam's canonical selection rule; a partial answer survives cancel and truncation.
const output: ContentBlock[] = finalAssistantOutput(own) ?? []
const recorded = toStopReason(lastEnd?.data.reason)
@@ -82,6 +82,20 @@ describe('startInProcessRun', () => {
await run.dispose()
})
it('reports a prompt a pre-step rejection discarded as refusal, not completion', async () => {
const { ctx, parent } = await setup([])
// A UserPromptSubmit deny or a policy plugin: the child claims its prompt,
// the rejection discards it, and the turn closes `blocked` with no step.
ctx.on('agent/pre-step', async ({ agent: subject }, next) => {
if (subject === parent) return next()
return { kind: 'reject' as const }
})
const run = await startInProcessRun(request(parent), {})
await expect(run.result).resolves.toMatchObject({ stopReason: 'refusal' })
await run.dispose()
})
it('does not add a final durability checkpoint to a foreground run', async () => {
const { ctx, parent } = await setup([textResponse('driver answer')])
let flushes = 0
+2 -2
View File
@@ -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/README.md
README.md: 28f649ef54bbf88feda24a9ce197c2c366f8349b
README.zh.md: 595c5e5e7fffc367f2e3fd8142b779dc22fb3b79
README.md: 6373f09e44a5bb87679d2afe67321f49c7174fef
README.zh.md: a657e6371677caa97f0884d3f741a95b3cc6c437
+23 -1
View File
@@ -76,6 +76,14 @@ The manager derives three internal residency conditions from Agent quiescence an
The manager reserves the child identity, resolves the durable descriptor, calls `ctx.agents.create()` (or `ctx.agents.resume()` for cold resume) through a private activation-owner scope, installs the returned `AgentHandle` in the Activation, establishes any continuable-parent ownership, and then submits the prompt. Cold resume never dispatches through a provider because the persisted Session already holds the initial prefix and the folded descriptor is the whole reconstruction input.
### Settlement delivery
When a resident Activation settles, the manager tells the child's durable direct parent, in the parent's own turn stream, that the child produced everything it is going to. Delivery is unconditional for every child whose id a caller actually received: it does not consider whether the child called `report`, because the endings that most need an account — a token ceiling, a model failure, cancellation, teardown — are exactly the ones where the child never got to choose. A materialization rolled back before its first accepted message stays silent, since that caller was told the child was not established. The message carries the epoch's stop reason, its final assistant content when it produced any, and durable provenance `{ kind: 'subagent-settled', form: 'notice', senderSessionId: <child-id> }` — a different source kind from a child-authored `subagent-report`, so a transcript never credits the child with words the runtime wrote.
Two ordering rules make the delivery reliable rather than lucky, and both are why this belongs to the manager instead of an external `subagent/end` listener. First, the send happens **before** the child's ownership release, while the parent still counts the child and is therefore structurally unable to be judged settled. Second, a parent that is itself a resident Activation receives the message through the same waking-admission accounting as a report, so the window between the synchronous send and the microtask that admits it is not mistaken for quiescence — `Agent.status` folds context maintenance into `idle`, and a waking send behind maintenance only arms a deferred wake. Without either rule the parent can be disposed with the notice still in an inbox that `cancel()` clears, which loses it silently.
An idle parent receives the notice as one ordinary later turn. A busy parent is steered into its nearest step boundary instead, so several children settling together cost one step rather than one turn each; steering rather than injecting also means a driver that retires between the status read and the send still claims the message. A parent whose own lineage is already draining receives the notice by injection, with no wake at all: `Agent.followup()` on a quiescent parent starts a turn and `cancel()` does not arm against a later one, so waking during teardown would spend a model request on an Agent its host is about to dispose — once per tree layer, since each layer's notice then wakes the layer above it. The injected message reaches a parent that is still reading its inbox, and the log records the account either way, but it does not outlive that parent's own disposal: `AgentHandle.dispose()` is a `keepInbox: false` cancel, which durably cancels an unclaimed notice. A resumed parent therefore has no pending notice to read: `list_agents` tells it which children exist and whether each is live or stored, while the outcome itself stays in the child's own Session, which a `send_message` reaches by resuming that child. A parent that has left the registry is not an error: the notice is dropped and the child's own Session remains the durable record. Delivery never blocks or fails teardown — a rejected send is logged, because retaining a child to retry a notice would pin its whole ancestry in `waiting` forever.
A continuation-managed parent Activation records each child Session id in an `ownedChildren` set before the child can run and disposes only after every owned child Activation completes `AgentHandle` disposal (child-first). Teardown propagates Agent cancellation top-down before awaiting slow descendants, while handle release remains child-first. Top-level and other non-continuation Agents have no Activation and stay outside this waiting graph. Final settlement awaits a best-effort `ctx.sessions.flush(child.session)` before handle disposal. A listener rejection is logged without failing the Activation because listener participation does not identify a persistence backend; the persisted state may therefore be missing or stale on resume.
## Lifecycle events
@@ -100,11 +108,25 @@ Continuable Activations await a best-effort final session flush without treating
## Model Experience
### Settlement notice
#### What the model sees
One user-role parent message opening with the outcome — `Background subagent <child-id> finished and will do no further work unless you send it more.`, or the matching line for a child that was stopped, ran out of room, declined, or failed — followed by `Its closing message:` and the child's final assistant content, or `It left no closing message.` when it produced none. This is the service's only direct parent-side contribution; delegation schemas, parent continuation and discovery, and the child-scoped `report` belong to `dsh-tool-subagent`, `dsh-tool-subagent-control`, and `dsh-tool-subagent-report`.
#### Token effect
One notice per settled Activation in the parent's request, sized by the child's final message. A child that both reports and settles costs the parent both.
#### KV Cache effect
Append-only in the parent: the notice follows its reusable request prefix. Reaching an idle parent starts one independent model request; reaching a busy one does not.
### Child delegation-scope statement
#### What the model sees
Every in-process child's runtime-context snapshot carries the `subagent:delegation` statement below, after the sandbox-policy and approval-policy sentences; parent-side rendering stays with `dsh-tool-subagent` (delegation schemas), `dsh-tool-subagent-control` (continuation and discovery), and `dsh-tool-subagent-report` (the child-scoped `report`).
Every in-process child's runtime-context snapshot carries the `subagent:delegation` statement below, after the sandbox-policy and approval-policy sentences.
##### The delegation-scope statement
+23 -1
View File
@@ -76,6 +76,14 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
管理器预留子 agent 身份、解析持久化描述符,通过私有的 activation-owner 作用域调用 `ctx.agents.create()`(冷恢复时为 `ctx.agents.resume()`),把返回的 `AgentHandle` 安装到 Activation 中,建立任何可继续父级所有权,然后提交提示词。冷恢复绝不通过提供方分发,因为持久化 Session 已持有初始前缀,折叠后的描述符即是全部重建输入。
### 结算投递
当一个驻留 Activation 结算时,管理器会在父级自身的轮次流中告知该子级持久化的直接父级:这个子级已经产出它将产出的全部内容。对每个调用方真正拿到过 id 的子级,这条投递都是无条件的:它不考虑该子级是否调用过 `report`,因为最需要这条投递的结束方式——token 上限、模型失败、取消、拆卸——恰恰是子级根本没有机会选择的那些。在第一条消息被接受之前就回滚的物化保持静默,因为那位调用方已被告知该子级未建立。消息会携带该 epoch 的终止原因、它产出过的最终 assistant 内容,以及持久化来源 `{ kind: 'subagent-settled', form: 'notice', senderSessionId: <child-id> }`——与子级自撰的 `subagent-report` 是不同的来源 kind,因此 transcript(文本记录)绝不会把运行时写下的话算到子级头上。
有两条顺序规则让这条投递可靠而非侥幸,它们也正是这件事属于管理器而非外部 `subagent/end` listener 的原因。第一,发送发生在子级所有权释放**之前**,此时父级仍然计入该子级,因此在结构上不可能被判定为已结算。第二,本身就是驻留 Activation 的父级会通过与 report 相同的唤醒准入记账接收该消息,因此同步发送与承认它的那个 microtask 之间的窗口不会被误判为静止——`Agent.status` 会把上下文维护折叠成 `idle`,而维护期间的唤醒发送只会预置一次延后唤醒。缺少其中任一条规则,父级都可能在通知仍留在 inbox 时被 dispose,而 `cancel()` 会清空该 inbox,于是通知被静默丢失。
空闲父级会以一个普通的后续轮次收到该通知。繁忙父级则被 steer 到其最近的 step 边界,因此同时结算的多个子级只消耗一个 step,而不是各自一个轮次;采用 steer 而非 inject 还意味着:即便驱动在状态读取与发送之间退出,该消息仍会被认领。若父级自身所在的谱系已在 draining,则该通知改为 inject 送达,完全不唤醒:对静息父级调用 `Agent.followup()` 会开启一个轮次,而 `cancel()` 不会对之后的轮次设防,因此在拆卸期间唤醒,会在宿主即将 dispose 的 Agent 上白花一次模型请求——而且每层树各一次,因为每层自己的通知又会唤醒它上面那层。被 inject 的消息会送达仍在读取自身 inbox 的父级,而无论如何日志都会记录这份记账;但它不会比该父级自身的 dispose 活得更久:`AgentHandle.dispose()` 是一次 `keepInbox: false` 的 cancel,会持久地取消尚未被认领的通知。因此 resume 后的父级没有待处理通知可读:`list_agents` 只告诉它有哪些子级、各自是在线还是仅存于存储;结局本身留在子级自己的 Session 里,一次 `send_message` 会通过 resume 该子级把它取回。已离开注册表的父级不算错误:通知被丢弃,子级自身的 Session 仍是持久记录。投递绝不会阻塞或使拆卸失败——发送被拒只会记录日志,因为为了重试一条通知而保留子级,会把它的整条祖先链永久钉在 `waiting` 上。
受继续执行管理的父级 Activation 会在子 agent 能够运行之前,把每个子 agent 的 Session id 记录到 `ownedChildren` 集合中,并且只有在每个所拥有的子 agent Activation 完成 `AgentHandle` dispose 之后才会 dispose(子先于父)。拆卸会先自顶向下传播 Agent 取消,再等待缓慢的后代,而 handle 释放仍保持 child-first。顶层及其他非继续执行的 Agent 没有 Activation,处于该等待图之外。最终结算会在 dispose handle 前等待 best-effort 的 `ctx.sessions.flush(child.session)`。listener rejection 会被记录,但不会使 Activation 失败,因为 listener 是否参与无法标识持久化后端;因此,恢复时持久化状态可能缺失或陈旧。
## 生命周期事件
@@ -100,11 +108,25 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
## 模型体验
### 结算通知
#### 模型看到的内容
一条用户角色的父级消息,开头是结果本身——`Background subagent <child-id> finished and will do no further work unless you send it more.`,或子级被停止、耗尽额度、拒绝任务或失败时的对应句子——随后是 `Its closing message:` 与子级的最终 assistant 内容;若子级没有产出内容,则是 `It left no closing message.`。这是本服务面向父级的唯一直接贡献;委派 schema、父级延续与发现以及子级作用域的 `report` 分别归 `dsh-tool-subagent``dsh-tool-subagent-control``dsh-tool-subagent-report` 所有。
#### Token 影响
父级请求中,每个已结算的 Activation 一条通知,长度取决于子级的最终消息。既上报又结算的子级会让父级同时支付两份。
#### KV Cache 影响
在父级中仅追加:通知位于其可复用请求前缀之后。到达空闲父级会启动一次独立的模型请求,到达繁忙父级则不会。
### 子级委派范围声明
#### 模型看到的内容
每个进程内子 agent 的运行时上下文快照都携带下方的 `subagent:delegation` 声明,位于沙箱策略与审批策略语句之后;父级侧的渲染仍归 `dsh-tool-subagent`(委派 schema)、`dsh-tool-subagent-control`(延续与发现)和 `dsh-tool-subagent-report`(子级作用域的 `report`)所有
每个进程内子 agent 的运行时上下文快照都携带下方的 `subagent:delegation` 声明,位于沙箱策略与审批策略语句之后。
##### 委派范围声明
+204 -26
View File
@@ -1,7 +1,8 @@
/**
* Internal continuable-subagent manager: stable child ids, descriptor
* persistence, activation admission, the live ownership graph, cold resume,
* and child-first disposal behind `ctx.subagents`.
* child-first disposal, and settlement delivery to the parent, behind
* `ctx.subagents`.
*
* A continuable child has one durable Session and at most one process-local
* {@link Activation} — one residency epoch for a reconstructed child Agent. An
@@ -11,6 +12,12 @@
* residency while the Agent loop owns all turn ordering and execution. No
* continuable path creates a Task or an intermediate result-bearing wrapper.
*
* Because residency is this manager's alone to end, telling the parent that a
* child settled is its job too. An external `subagent/end` listener cannot do
* it correctly: that payload names no parent, the child handle is already
* disposed by then, and the release that wakes the parent's own settlement
* watcher has already run. See {@link SubagentContinuationManager.notifySettlement}.
*
* @module @deepseek-ai/dsh-subagent
*/
@@ -23,7 +30,7 @@ import type {
AgentSetupCommit,
CreateAgentOptions,
} from '@deepseek-ai/dsh-agent'
import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'
import { boundContextSummary, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
@@ -42,8 +49,8 @@ import {
import type { DelegatedPolicyOverrides } from './child-agent.ts'
import { assertSubagentMaxDepth } from './depth.ts'
import { seedDescriptorTurn } from './descriptor-seed.ts'
import type { ContinuableCreateRequest, ContinuableCreateSpec, SubagentStartRequest } from './types.ts'
import type { ActivationObserver } from './lifecycle.ts'
import type { ContinuableCreateRequest, ContinuableCreateSpec, SubagentResult, SubagentStartRequest } from './types.ts'
import type { ActivationObserver, ActivationTerminal } from './lifecycle.ts'
import { SubagentError } from './error.ts'
import type SubagentActivationSetupRegistry from './activation-setup-registry.ts'
@@ -65,10 +72,28 @@ export interface SubagentReportMessageSource {
readonly senderSessionId: SessionId
}
/**
* Durable attribution for the runtime's own account of a continuable child
* settling. Deliberately a different kind from
* {@link SubagentReportMessageSource}: a report is content the child chose,
* while this message is the manager stating what became of the child, and a
* transcript that merged them would credit the child with words it never wrote.
*/
export interface SubagentSettledMessageSource {
readonly kind: 'subagent-settled'
/** A runtime account shown without expanding the row (`notice` context form). */
readonly form: 'notice'
/** One-line account of how the child ended. */
readonly summary: string
/** Session id of the child that settled. */
readonly senderSessionId: SessionId
}
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
coordinator: CoordinatorMessageSource
'subagent-report': SubagentReportMessageSource
'subagent-settled': SubagentSettledMessageSource
}
}
@@ -166,6 +191,13 @@ interface ContinuationHost {
interface Activation {
/** The durable child this Activation is an epoch of. */
readonly childId: SessionId
/**
* The durable direct parent, stored because settlement delivery must resolve
* that parent after the child handle is gone. {@link ancestry} cannot answer
* it: a `WeakSet` is not enumerable, and the child's own header is only
* reachable through a handle disposal has already released.
*/
readonly parentSession: SessionId
/** The provider name recorded in the durable descriptor. */
readonly provider: string
/** The retained live Agent handle, disposed exactly once at settlement. */
@@ -197,6 +229,12 @@ interface Activation {
* microtask that admits it, so settlement must not treat that gap as quiet.
*/
readonly accepted: Set<MessageId>
/**
* Whether any delivery to this child was ever accepted. A materialization
* rolled back before its first acceptance is a child the caller was told does
* not exist, so its teardown owes the parent no settlement account.
*/
announced: boolean
/** Renewed whenever a settlement watcher must re-observe quiescence. */
poke: PromiseWithResolvers<void>
}
@@ -243,6 +281,36 @@ function disposalOf(activation: Activation): Promise<void> | undefined {
return activation.disposal
}
/**
* One line telling a parent that a background child is finished and why, in
* the parent's own task vocabulary.
* @param childId - the durable child the parent knows by id.
* @param stopReason - how the child's last ordinary turn ended.
* @returns the model-facing opening line of the settlement notice.
*/
function settlementSummary(childId: SessionId, stopReason: SubagentResult['stopReason']): string {
const subject = `Background subagent ${childId}`
switch (stopReason) {
case 'completed':
return `${subject} finished and will do no further work unless you send it more.`
case 'aborted':
return `${subject} was stopped before it finished.`
case 'max-tokens':
return `${subject} ran out of room before it finished.`
// A pre-step rejection — a hook deny, a policy plugin — discarded input
// the child had claimed, so the parent must not treat the task as done.
case 'refusal':
return `${subject} declined the task.`
case 'error':
return `${subject} failed before it finished.`
/* v8 ignore next 4 -- `SubagentResult['stopReason']` is merge-extensible, so this arm
* needs a backend that adds a variant; an unnameable ending is reported as unfinished
* rather than silently as success. */
default:
return `${subject} ended abnormally (${String(stopReason)}) before it finished.`
}
}
/** Whether one settlement attempt opened the disposal transaction. */
type SettlementAttempt =
| { readonly settling: false }
@@ -576,19 +644,36 @@ export class SubagentContinuationManager {
senderSessionId: activation.childId,
},
})
const parentActivation = this.activations.get(parent.id)
if (delivery === 'wakeup'
&& parentActivation !== undefined
&& parentActivation.handle.agent === parent) {
this.admitWaking(parentActivation, message.id, () => {
this.sendReport(parent, message, delivery)
})
if (delivery === 'wakeup') {
this.sendWaking(parent, message, () => { this.sendReport(parent, message, delivery) })
} else {
this.sendReport(parent, message, delivery)
}
return message.id
}
/**
* Perform one waking send to a parent, accounted against that parent's own
* Activation when it has one. Registering the id before the send is what
* keeps a continuation-managed parent from being judged quiescent in the
* window between `followup()` and the microtask that admits it.
* @param parent - the exact live parent receiving the waking message.
* @param message - the message whose id is accounted.
* @param send - the synchronous waking send to perform.
*/
private sendWaking(
parent: Agent,
message: ReturnType<typeof createUserMessage>,
send: () => void,
): void {
const parentActivation = this.activations.get(parent.id)
if (parentActivation !== undefined && parentActivation.handle.agent === parent) {
this.admitWaking(parentActivation, message.id, send)
} else {
send()
}
}
/** Send one report while translating only the parent's own rejection. */
private sendReport(
parent: Agent,
@@ -745,23 +830,32 @@ export class SubagentContinuationManager {
return lineage
}
/** Reject new admission once the manager or this exact parent tree began draining. */
private assertAdmitting(agent: Agent): void {
if (this.draining) {
throw new SubagentError(
'continuable subagents are draining; the operation was not admitted',
'DRAINING',
)
}
/**
* The teardown that closed continuable admission for this agent's lineage.
* `'manager'` is the whole manager draining; an Agent is the exact scoped root
* whose forest is closing.
* @param agent - the agent whose lineage is tested.
* @returns the closing teardown, or `undefined` while admission is open.
*/
private closingTeardownFor(agent: Agent): Agent | 'manager' | undefined {
if (this.draining) return 'manager'
const lineage = this.liveLineage(agent)
for (const [root, members] of this.closingScopes) {
if (members.has(agent) || lineage.includes(root)) {
throw new SubagentError(
`continuable subagents below parent "${root.id}" are draining; the operation was not admitted`,
'DRAINING',
)
}
if (members.has(agent) || lineage.includes(root)) return root
}
return undefined
}
/** Reject new admission once the manager or this exact parent tree began draining. */
private assertAdmitting(agent: Agent): void {
const closing = this.closingTeardownFor(agent)
if (closing === undefined) return
throw new SubagentError(
closing === 'manager'
? 'continuable subagents are draining; the operation was not admitted'
: `continuable subagents below parent "${closing.id}" are draining; the operation was not admitted`,
'DRAINING',
)
}
/**
@@ -930,6 +1024,10 @@ export class SubagentContinuationManager {
const activation: Activation = {
childId,
// The durable lineage, not merely the caller: creation stamps this same
// agent into the child's header, and cold resume authorized it against
// the persisted header before materializing.
parentSession: parent.id,
provider,
handle,
ancestry: new WeakSet([handle.agent, ...parentLineage]),
@@ -937,6 +1035,7 @@ export class SubagentContinuationManager {
observer,
disposal: undefined,
accepted: new Set(),
announced: false,
poke: Promise.withResolvers<void>(),
}
// After transfer, any failure must dispose the created handle, remove the
@@ -1038,9 +1137,13 @@ export class SubagentContinuationManager {
// establish it before the message can enter the child's inbox.
this.acquireOwnership(parent, activation.childId)
const message = createUserMessage({ content, source })
return this.admitWaking(activation, message.id, () => {
const accepted = this.admitWaking(activation, message.id, () => {
activation.handle.agent.followup(message)
})
// Past this point the caller has an id for this child, so its eventual
// settlement is something the parent is owed an account of.
activation.announced = true
return accepted
}
/**
@@ -1261,6 +1364,12 @@ export class SubagentContinuationManager {
// makes a racing delivery wait for release rather than cold-resume into the
// still-registered agent.
this.activations.delete(childId)
// BEFORE releasing ownership, while the parent still counts this child and
// therefore cannot be judged settled. Delivering after the release would
// race a parent watcher that resumes one microtask later, finds itself
// childless and quiet, and disposes an Agent whose `cancel()` clears the
// inbox this notice is sitting in.
this.notifySettlement(activation, activation.observer.terminal(failure))
// Release ownership even on failure: a retained failed child would pin its
// ancestors in `waiting` forever.
this.releaseOwnership(childId)
@@ -1270,6 +1379,75 @@ export class SubagentContinuationManager {
if (failure !== undefined) throw failure
}
/**
* Tell the durable direct parent that this child produced everything it is
* going to. Unconditional for every child the caller received an id for: it
* does not consider whether the child reported, because the cases that most
* need it — a token ceiling, a model failure, cancellation, teardown — are
* exactly the ones where the child never got to choose. A materialization
* rolled back before its first acceptance stays silent, since the caller was
* told that child was not established. A parent that is no longer live is not
* an error; the child's own Session remains the durable record either way.
* A parent whose own lineage is already closing receives the notice without a
* wake, because teardown is not a reason to start a turn.
*
* Never blocks disposal. A delivery failure is logged and dropped, because
* retaining a child to retry a notice would pin its whole ancestry in
* `waiting` forever.
* @param activation - the settling Activation, still owned by its parent.
* @param terminal - how this epoch ended, as the terminal edge will report it.
*/
private notifySettlement(activation: Activation, terminal: ActivationTerminal): void {
if (!activation.announced) return
try {
const parent = this.ctx.agents.get(activation.parentSession)
if (parent === undefined) return
const summary = settlementSummary(activation.childId, terminal.stopReason)
const message = createUserMessage({
content: [
{ type: 'text' as const, text: summary },
...terminal.output === undefined
? [{ type: 'text' as const, text: 'It left no closing message.' }]
: [{ type: 'text' as const, text: 'Its closing message:' }, ...terminal.output],
],
source: {
kind: 'subagent-settled' as const,
form: 'notice' as const,
summary: boundContextSummary(summary),
senderSessionId: activation.childId,
},
})
// A parent whose own teardown already began must not be woken. Waking is
// not a queue operation: `followup()` on a quiescent Agent starts a turn,
// and `cancel()` does not arm against a later one, so a notice arriving
// during teardown would spend a model request on an Agent its host is
// about to dispose — once per tree layer, since each layer's own notice
// then wakes the layer above it. Injecting delivers to a parent still
// reading its inbox and records the account in the log either way; it
// does NOT survive that parent's own disposal, whose `keepInbox: false`
// cancel durably clears whatever it never claimed.
if (this.closingTeardownFor(parent) !== undefined) {
parent.inject(message)
return
}
// An idle parent has nothing else to look at, so it gets one ordinary
// turn. A busy parent is steered instead of woken: `Inbox.claim()` takes
// the whole next-step batch at one boundary, so several children settling
// together cost one step rather than one turn each. Steering rather than
// injecting closes the window where a driver retires between this status
// read and the send, which would strand the notice unclaimed.
this.sendWaking(parent, message, () => {
if (parent.status === 'idle') parent.followup(message)
else parent.steer(message)
})
} catch (error: unknown) {
this.ctx.logger.warn(
`subagent "${activation.childId}" settlement notice was not delivered to its parent: `
+ errorChain(error),
)
}
}
/**
* Request a best-effort final session flush after the child is quiescent.
* Listener failure is logged because flush participation cannot identify a
+1
View File
@@ -119,6 +119,7 @@ export type {
SubagentReportDelivery,
SubagentReportMessageSource,
SubagentReportOptions,
SubagentSettledMessageSource,
} from './continuation.ts'
export type { ContinuableSetupContribution } from './activation-setup-registry.ts'
export type { SubagentDescendantListEntry, SubagentListEntry } from './list-children.ts'
+53 -16
View File
@@ -18,12 +18,23 @@ import { randomUUID } from 'node:crypto'
import type { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { findLastMessageTurnEnd } from '@deepseek-ai/dsh-session'
import { foldConsumedWork } from '@deepseek-ai/dsh-agent'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import { finalAssistantOutput } from './assistant-output.ts'
import { SubagentRunId } from './types.ts'
import type { SubagentResult, SubagentRun, SubagentRunEndInfo, SubagentRunInfo } from './types.ts'
/**
* How one Activation's residency epoch ended, as both the terminal lifecycle
* edge and the manager's own parent delivery report it.
*/
export interface ActivationTerminal {
/** Why this epoch's last ordinary turn ended, or `error` when teardown failed. */
readonly stopReason: SubagentResult['stopReason']
/** The epoch's final assistant content, absent when it produced none or failed. */
readonly output?: ContentBlock[]
}
/**
* Lifecycle observer for one Activation's residency epoch, so continuable
* children emit the same start/end pair as one-shot runs. Package-private: the
@@ -43,6 +54,15 @@ export interface ActivationObserver {
* @param child - the quiescent child agent about to be released.
*/
capture(child: Agent): void
/**
* Resolve the terminal facts {@link settle} will publish, without publishing
* them. The manager's parent delivery must run before the ownership release
* that lets the parent settle, which is earlier than the terminal edge; both
* therefore read one computation instead of restating the failure rule.
* @param failure - the teardown or durability failure, or `undefined` on success.
* @returns this epoch's stop reason and final assistant content.
*/
terminal(failure: unknown): ActivationTerminal
/**
* Publish the terminal edge exactly once, pairing this epoch's {@link start},
* after the disposal outcome is known. Called only for a resident epoch: a
@@ -165,9 +185,12 @@ export function createActivationObserver(
let boundary = 0
// Assigned by `capture()`, which the disposal path always runs before
// `settle()`; a resident epoch therefore always has its facts by then.
let captured: { stopReason: SubagentResult['stopReason']; output?: ContentBlock[] } = {
stopReason: 'completed',
}
let captured: ActivationTerminal = { stopReason: 'completed' }
// Teardown failure overrides the epoch's own outcome and withholds its
// output: an answer this harness could not durably release is not a result.
const terminal = (failure: unknown): ActivationTerminal => failure === undefined
? captured
: { stopReason: 'error' }
return {
start: (child: Agent): void => {
boundary = child.session.events.length
@@ -181,11 +204,12 @@ export function createActivationObserver(
...output === undefined ? {} : { output },
}
},
terminal,
settle: (failure: unknown): void => {
const output = failure === undefined ? captured.output : undefined
const { stopReason, output } = terminal(failure)
emit('subagent/end', {
...identity,
stopReason: failure === undefined ? captured.stopReason : 'error',
stopReason,
...output === undefined ? {} : { lastAssistantMessage: output },
}, parent)
},
@@ -193,18 +217,24 @@ export function createActivationObserver(
}
/**
* Why this child's last ordinary turn ended, for the terminal lifecycle edge.
* The child's own `turn/end` is authoritative: teardown succeeding says nothing
* about whether the model errored, hit its token ceiling, or was cancelled, so
* deriving the reason from disposal would report failed work as completed.
* Why this child's epoch ended, for the terminal lifecycle edge and the
* manager's own parent delivery. The child's own log is authoritative:
* teardown succeeding says nothing about whether the model errored, hit its
* token ceiling, or was cancelled, so deriving the reason from disposal would
* report failed work as completed.
*
* {@link foldConsumedWork} supplies both halves the raw turn sequence cannot:
* which turn accounts for the work this epoch consumed, and whether accepted
* work was cancelled after it without any turn opening over it. A recorded
* failure still wins over a cancellation — stopping a child that had already
* failed does not turn its failure into a cancellation.
* @param events - this epoch's own event suffix.
* @returns its terminal stop reason; `completed` when no ordinary turn closed.
* @returns its terminal stop reason; `completed` only for an epoch that both
* closed cleanly and had nothing left to run.
*/
function epochStopReason(events: readonly SessionEvent[]): SubagentResult['stopReason'] {
const reason = findLastMessageTurnEnd(events)?.data.reason
// No ordinary turn closed, so nothing failed either.
if (reason === undefined) return 'completed'
switch (reason.kind) {
const { end, droppedUnrun } = foldConsumedWork(events)
switch (end?.data.reason.kind) {
case 'max-tokens':
return 'max-tokens'
case 'aborted':
@@ -212,8 +242,15 @@ function epochStopReason(events: readonly SessionEvent[]): SubagentResult['stopR
return 'aborted'
case 'error':
return 'error'
// A pre-step rejection — a hook deny, a policy plugin — discarded input
// this epoch had claimed: the work was declined, not done.
case 'blocked':
return 'refusal'
// A clean ending and no accounting turn at all share one rule: the epoch
// finished what it was given unless a cancelled queue says otherwise.
case undefined:
case 'completed':
return 'completed'
return droppedUnrun ? 'aborted' : 'completed'
/* v8 ignore next 3 -- `TurnEndReason` is merge-extensible, so this arm needs a
* backend that adds a variant; treating an unnameable reason as success would
* report failed work as completed. */
@@ -15,7 +15,7 @@ import type { GenerateOptions, MessageId, StreamChunk } from '@deepseek-ai/dsh-l
import { CallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import InvariantService from '@deepseek-ai/dsh-invariants'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import SubagentService, {
SubagentError,
SUBAGENT_DESCRIPTOR_VERSION,
@@ -142,6 +142,18 @@ async function waitNoActivation(ctx: Context, childId: SessionId): Promise<void>
}, { timeout: 5_000 })
}
/**
* Keep the top-level test parent out of a scripted model corpus. Every child
* settlement wakes its parent, so a suite that scripts only child responses
* would otherwise spend them on the parent's own turns.
*/
function parkParent(ctx: Context, parent: Agent): void {
ctx.on('agent/pre-step', async ({ agent: subject }, next) => {
if (subject !== parent) return next()
return { kind: 'reject' as const }
})
}
/** Observe calls at the Agent cancellation boundary without a production event. */
function observeCancel(agent: Agent, callback: () => void): void {
const cancel = agent.cancel.bind(agent)
@@ -531,7 +543,10 @@ describe('SubagentService.followup residency routing', () => {
await waitNoActivation(ctx, grandchild.childId)
await waitNoActivation(ctx, started.childId)
const loaded = await ctx.sessionPersistence.load(started.childId)
expect(userTexts(loaded.events)).toEqual(['child task', 'while waiting'])
// This child is itself a parent, so its grandchild's settlement notice is
// an ordinary later user message in its log.
expect(userTexts(loaded.events).slice(0, 2)).toEqual(['child task', 'while waiting'])
expect(userTexts(loaded.events).slice(2).join('\n')).toContain('finished and will do no further work')
})
it('rejects a parent that is not the durable direct parent', async () => {
@@ -1182,6 +1197,7 @@ describe('continuable review regressions', () => {
it('reports this epoch\'s own output, captured while the child was still live', async () => {
const { ctx, parent } = await setup([textResponse('first answer'), textResponse('second answer')])
parkParent(ctx, parent)
const ends: SubagentRunEndInfo[] = []
ctx.on('subagent/end', (info) => { ends.push(info) })
@@ -1254,9 +1270,10 @@ describe('continuable review regressions', () => {
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(ends).toHaveLength(1) })
// Reading the whole session would resurrect 'first answer' here.
// Reading the whole session would resurrect 'first answer' here. The
// rejection discarded the claimed follow-up, so the epoch reads as refused.
expect(ends[0]!.lastAssistantMessage).toBeUndefined()
expect(ends[0]!.stopReason).toBe('completed')
expect(ends[0]!.stopReason).toBe('refusal')
})
it('reports handle-disposal failure on the terminal edge', async () => {
@@ -1439,11 +1456,13 @@ describe('continuable review regressions', () => {
await waitNoActivation(ctx, started.childId)
})
it('reports completed when no ordinary turn closed', async () => {
it('reports a prompt a pre-step rejection discarded as refusal', async () => {
const { ctx, parent } = await setup([])
parkParent(ctx, parent)
const ends: SubagentRunEndInfo[] = []
ctx.on('subagent/end', (info) => { ends.push(info) })
// Block admission so the child's only turn never opens.
// A UserPromptSubmit deny or a policy plugin: the child claims its prompt,
// the rejection discards it, and no step ever runs.
ctx.on('agent/pre-step', async ({ agent: subject }, next) => {
if (subject === parent) return next()
return { kind: 'reject' }
@@ -1452,8 +1471,10 @@ describe('continuable review regressions', () => {
const started = await ctx.subagents.startContinuable(startSpec(parent))
await waitNoActivation(ctx, started.childId)
// The parent would otherwise believe a vetoed delivery was done and never
// resend it — the one failure the settlement promise says cannot happen.
await vi.waitFor(() => { expect(ends).toHaveLength(1) })
expect(ends[0]!.stopReason).toBe('completed')
expect(ends[0]!.stopReason).toBe('refusal')
})
it('retains the Activation while an accepted message is still in the inbox', async () => {
@@ -1482,15 +1503,538 @@ describe('continuable review regressions', () => {
expect(ctx.agents.get(started.childId)).toBe(child)
releaseFirst.resolve(undefined)
await waitNoActivation(ctx, started.childId)
expect(adapter.requests).toHaveLength(2)
// Two child turns; the third request is the parent's own turn on the
// settlement notice.
expect(adapter.requests.filter(request => request.sessionId === started.childId)).toHaveLength(2)
const loaded = await ctx.sessionPersistence.load(started.childId)
expect(hasUserText(loaded.events, 'queued')).toBe(true)
})
})
/** Every settlement notice this agent received, in order, as flat text. */
function settlementNotices(agent: Agent): { sender: string; text: string; summary: string }[] {
const logged = agent.session.events.flatMap(event => event.type === 'user/message' ? [event.data] : [])
return [...logged, ...agent.inbox.nextStep, ...agent.inbox.nextTurn].flatMap((message) => {
if (message.source.kind !== 'subagent-settled') return []
return [{
sender: message.source.senderSessionId,
summary: message.source.summary,
text: message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n'),
}]
})
}
describe('continuable settlement delivery', () => {
it('tells the parent what the child finished with, without being asked', async () => {
const { ctx, parent } = await setup([textResponse('the answer'), textResponse('parent ack')])
const started = await ctx.subagents.startContinuable(startSpec(parent))
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
const notice = settlementNotices(parent)[0]!
expect(notice.sender).toBe(started.childId)
expect(notice.text).toBe(
`Background subagent ${started.childId} finished and will do no further work unless you send it more.`
+ '\nIts closing message:\nthe answer',
)
// The collapsed row states the outcome without the child's content.
expect(notice.summary).toBe(
`Background subagent ${started.childId} finished and will do no further work unless you send it more.`,
)
})
it('delivers even when the child already reported for itself', async () => {
const { ctx, parent } = await setup([textResponse('the answer'), textResponse('parent ack')])
const started = await ctx.subagents.startContinuable(startSpec(parent))
const child = await vi.waitFor(() => {
const live = ctx.agents.get(started.childId)
expect(live).toBeDefined()
return live!
})
await ctx.subagents.reportFrom(child, message('an explicit report'), {
delivery: 'quiet',
signal: testSignal,
})
await waitNoActivation(ctx, started.childId)
// The contract is unconditional precisely so the parent-side tool
// description can promise it; bookkeeping "did it report?" would make the
// promise conditional on a channel this manager does not own.
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
})
it('delivers the terminal reason when the child never had a chance to report', async () => {
const { ctx, parent } = await setup([maxTokensResponse('half an ans'), textResponse('parent ack')])
const started = await ctx.subagents.startContinuable(startSpec(parent))
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
expect(settlementNotices(parent)[0]!.text).toBe(
`Background subagent ${started.childId} ran out of room before it finished.`
+ '\nIts closing message:\nhalf an ans',
)
})
it('tells the parent a policy-rejected delivery was declined, not finished', async () => {
const { ctx, parent } = await setup([textResponse('parent ack')])
// A pre-step rejection on the child — a UserPromptSubmit deny, a policy
// plugin — discards the claimed prompt without running it.
ctx.on('agent/pre-step', async ({ agent: subject }, next) => {
if (subject === parent) return next()
return { kind: 'reject' }
})
const started = await ctx.subagents.startContinuable(startSpec(parent))
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
expect(settlementNotices(parent)[0]!.text).toBe(
`Background subagent ${started.childId} declined the task.`
+ '\nIt left no closing message.',
)
})
it('reports a turn that failed before reaching its first step', async () => {
const releaseFirst = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([
{ chunks: textResponse('the answer'), gate: releaseFirst.promise },
{ chunks: textResponse('parent ack') },
])
const { ctx, parent } = await setupWith(adapter)
// The shipped durability checkpoint (`dsh-session-checkpoint-policy`) is
// fail-closed at the step boundary, so a rejected write ends the turn after
// it claimed its messages and before it entered a step.
ctx.on('agent/pre-step', async ({ agent: subject, turn }, next) => {
if (subject.session.header.parentSession === undefined || turn < 2) return next()
throw new Error('ENOSPC: no space left on device')
})
const started = await ctx.subagents.startContinuable(startSpec(parent))
await followup(ctx, parent, started.childId, message('second task'))
releaseFirst.resolve(undefined)
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
// The parent must not be told the child finished: the delivery it is still
// waiting on was claimed out of the inbox and then swallowed by the failure.
const child = await ctx.sessionPersistence.load(started.childId)
expect(hasUserText(child.events, 'second task')).toBe(false)
expect(settlementNotices(parent)[0]!.text).toBe(
`Background subagent ${started.childId} failed before it finished.`
+ '\nIts closing message:\nthe answer',
)
})
it('reports accepted work cut short before its first step as stopped', async () => {
const releaseFirst = Promise.withResolvers<undefined>()
const releaseCheckpoint = Promise.withResolvers<undefined>()
const atCheckpoint = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([{ chunks: textResponse('the answer'), gate: releaseFirst.promise }])
const { ctx, parent } = await setupWith(adapter)
// A step-boundary participant — the shipped durability checkpoint, a hook,
// prompt assembly — holding the child's second turn open before its first
// step, which is where teardown cancellation then catches it.
ctx.on('agent/pre-step', async ({ agent: subject, turn }, next) => {
if (subject.session.header.parentSession === undefined || turn < 2) return next()
atCheckpoint.resolve(undefined)
await releaseCheckpoint.promise
return next()
})
const started = await ctx.subagents.startContinuable(startSpec(parent))
// Queued while turn 1 still runs, so turn 2 opens and claims it without a
// second model call: the Activation is mid-turn when the drain cancels it.
await followup(ctx, parent, started.childId, message('second task'))
releaseFirst.resolve(undefined)
await atCheckpoint.promise
const drained = drainManager(ctx)
releaseCheckpoint.resolve(undefined)
await drained
// Turn 2 leaves a balanced no-step `aborted` end, so the log alone would
// answer with turn 1's clean completion and tell the parent its still-unrun
// task had finished.
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
expect(settlementNotices(parent)[0]!.text).toBe(
`Background subagent ${started.childId} was stopped before it finished.`
+ '\nIts closing message:\nthe answer',
)
})
it('reports a child stopped before it ever reached the model as stopped', async () => {
const releaseCheckpoint = Promise.withResolvers<undefined>()
const atCheckpoint = Promise.withResolvers<undefined>()
const { ctx, parent } = await setupWith(new GatedAdapter([]))
ctx.on('agent/pre-step', async ({ agent: subject }, next) => {
if (subject.session.header.parentSession === undefined) return next()
atCheckpoint.resolve(undefined)
await releaseCheckpoint.promise
return next()
})
const started = await ctx.subagents.startContinuable(startSpec(parent))
await atCheckpoint.promise
const drained = drainManager(ctx)
releaseCheckpoint.resolve(undefined)
await drained
// This epoch closed no stepped turn at all, which on its own reads as "had
// nothing to report"; only the interruption distinguishes it from a child
// that genuinely finished with no output.
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
expect(settlementNotices(parent)[0]!.text).toBe(
`Background subagent ${started.childId} was stopped before it finished.`
+ '\nIt left no closing message.',
)
})
it('reports a child an ancestor interrupted before its first step as stopped', async () => {
const atCheckpoint = Promise.withResolvers<undefined>()
const releaseCheckpoint = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([{ chunks: textResponse('parent ack') }])
const { ctx, parent } = await setupWith(adapter)
ctx.on('agent/pre-step', async ({ agent: subject }, next) => {
if (subject.session.header.parentSession === undefined) return next()
atCheckpoint.resolve(undefined)
await releaseCheckpoint.promise
return next()
})
const started = await ctx.subagents.startContinuable(startSpec(parent))
await atCheckpoint.promise
// The shipped interrupt path: nothing about it runs inside this manager, so
// no pre-cancel sample could see it — the child's own log has to say so.
ctx.subagents.interrupt(started.childId, { kind: 'ancestor', agent: parent })
releaseCheckpoint.resolve(undefined)
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
expect(settlementNotices(parent)[0]!.text).toBe(
`Background subagent ${started.childId} was stopped before it finished.`
+ '\nIt left no closing message.',
)
})
it('reports accepted work cancelled before any turn could open as stopped', async () => {
const releaseChild = Promise.withResolvers<undefined>()
const releaseGrandchild = Promise.withResolvers<undefined>()
const releaseMaintenance = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([
{ chunks: textResponse('the answer'), gate: releaseChild.promise },
{ chunks: textResponse('grandchild'), gate: releaseGrandchild.promise },
])
const { ctx, parent } = await setupWith(adapter)
const started = await ctx.subagents.startContinuable(startSpec(parent))
const child = await vi.waitFor(() => {
const live = ctx.agents.get(started.childId)
expect(live).toBeDefined()
return live!
})
// A descendant keeps the child resident once its own turn closes, so the
// maintenance phase below is reachable without racing settlement.
const grandchild = await ctx.subagents.startContinuable(startSpec(child))
await vi.waitFor(() => { expect(ctx.agents.get(grandchild.childId)).toBeDefined() })
releaseChild.resolve(undefined)
await vi.waitFor(() => { expect(child.status).toBe('idle') })
// Context maintenance folds into `idle` and defers waking work, so this
// delivery is accepted with no turn to claim it.
const maintaining = child.runMaintenance(async () => { await releaseMaintenance.promise })
await followup(ctx, parent, started.childId, message('never runs'))
const drained = drainManager(ctx)
releaseMaintenance.resolve(undefined)
releaseGrandchild.resolve(undefined)
await maintaining
await drained
// Turn 1 closed cleanly and no later turn opened, so the cancelled queue is
// the only record that this epoch was cut short.
expect(hasUserText(child.session.events, 'never runs')).toBe(false)
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
expect(settlementNotices(parent)[0]!.text).toBe(
`Background subagent ${started.childId} was stopped before it finished.`
+ '\nIts closing message:\nthe answer',
)
})
it('withholds an outcome the harness could not durably release', async () => {
const { ctx, parent } = await setup([textResponse('the answer'), textResponse('parent ack')])
const started = await ctx.subagents.startContinuable(startSpec(parent))
const manager = (ctx.subagents as unknown as {
continuations: { activations: Map<SessionId, { handle: { dispose(): Promise<void> } }> }
}).continuations
const activation = await vi.waitFor(() => {
const live = manager.activations.get(started.childId)
expect(live).toBeDefined()
return live!
})
const dispose = activation.handle.dispose.bind(activation.handle)
activation.handle.dispose = async () => {
await dispose()
throw new Error('scope unwind failed')
}
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) })
expect(settlementNotices(parent)[0]!.text).toBe(
`Background subagent ${started.childId} failed before it finished.\nIt left no closing message.`,
)
})
it('gives an idle parent one ordinary turn on the notice', async () => {
const { ctx, parent, adapter } = await setup([textResponse('the answer'), textResponse('parent ack')])
const turnStarts: number[] = []
ctx.on('session/event', (session, event) => {
if (session.id === parent.id && event.type === 'turn/start') turnStarts.push(event.data.turn)
})
const started = await ctx.subagents.startContinuable(startSpec(parent))
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => {
expect(adapter.requests.filter(request => request.sessionId === parent.id)).toHaveLength(1)
})
expect(turnStarts).toEqual([1])
})
it('batches simultaneous notices into one step of a busy parent', async () => {
const releaseChildren = Promise.withResolvers<undefined>()
const releaseParent = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([
{ chunks: textResponse('parent works'), gate: releaseParent.promise },
{ chunks: textResponse('first child'), gate: releaseChildren.promise },
{ chunks: textResponse('second child'), gate: releaseChildren.promise },
{ chunks: textResponse('parent reacts') },
])
const { ctx, parent } = await setupWith(adapter)
// Open a parent turn first, so both notices arrive while it is running.
parent.followup(createUserMessage({ content: message('start working'), source: { kind: 'user' } }))
await vi.waitFor(() => { expect(parent.status).toBe('running') })
const first = await ctx.subagents.startContinuable(startSpec(parent))
const second = await ctx.subagents.startContinuable(startSpec(parent))
releaseChildren.resolve(undefined)
await waitNoActivation(ctx, first.childId)
await waitNoActivation(ctx, second.childId)
// Both notices are waiting for the same step boundary, not two turns.
expect(parent.inbox.nextStep).toHaveLength(2)
expect(parent.inbox.nextTurn).toHaveLength(0)
const turnStarts: number[] = []
ctx.on('session/event', (session, event) => {
if (session.id === parent.id && event.type === 'turn/start') turnStarts.push(event.data.turn)
})
releaseParent.resolve(undefined)
await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(2) })
expect(turnStarts).toEqual([])
// Both children released together, so which settles first is not ordered.
expect(new Set(settlementNotices(parent).map(entry => entry.sender)))
.toEqual(new Set([first.childId, second.childId]))
})
it('holds a maintaining parent live until it can read the notice', async () => {
const releaseFirst = Promise.withResolvers<undefined>()
const releaseSecond = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([
{ chunks: textResponse('outer') },
{ chunks: textResponse('first inner'), gate: releaseFirst.promise },
{ chunks: textResponse('second inner'), gate: releaseSecond.promise },
{ chunks: textResponse('outer reacts') },
{ chunks: textResponse('root reacts') },
])
const { ctx, parent } = await setupWith(adapter)
const outer = await ctx.subagents.startContinuable(startSpec(parent))
const middle = await vi.waitFor(() => {
const live = ctx.agents.get(outer.childId)
expect(live).toBeDefined()
return live!
})
const first = await ctx.subagents.startContinuable(startSpec(middle))
const second = await ctx.subagents.startContinuable(startSpec(middle))
await vi.waitFor(() => { expect(middle.status).toBe('idle') })
// `Agent.status` folds maintenance into `idle`, and a waking send behind it
// only arms a deferred wake. The first child's release moves the middle
// Activation's settlement watcher onto its quiescence race; the second one
// then arrives at exactly the point where an unaccounted delivery would be
// judged quiet, settled, and cancelled — clearing the inbox it sits in.
const maintaining = Promise.withResolvers<undefined>()
const maintenance = middle.runMaintenance(async () => { await maintaining.promise })
releaseFirst.resolve(undefined)
await waitNoActivation(ctx, first.childId)
releaseSecond.resolve(undefined)
await waitNoActivation(ctx, second.childId)
expect(ctx.agents.get(outer.childId)).toBe(middle)
maintaining.resolve(undefined)
await maintenance
await vi.waitFor(() => { expect(settlementNotices(middle)).toHaveLength(2) })
expect(settlementNotices(middle).map(entry => entry.sender))
.toEqual([first.childId, second.childId])
await waitNoActivation(ctx, outer.childId)
})
it('delivers before releasing the ownership that lets the parent settle', async () => {
const releaseChild = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([
{ chunks: textResponse('outer') },
{ chunks: textResponse('inner'), gate: releaseChild.promise },
{ chunks: textResponse('outer reacts') },
])
const { ctx, parent } = await setupWith(adapter)
const outer = await ctx.subagents.startContinuable(startSpec(parent))
const middle = await vi.waitFor(() => {
const live = ctx.agents.get(outer.childId)
expect(live).toBeDefined()
return live!
})
const inner = await ctx.subagents.startContinuable(startSpec(middle))
await vi.waitFor(() => { expect(middle.status).toBe('idle') })
const manager = (ctx.subagents as unknown as {
continuations: { activations: Map<SessionId, { ownedChildren: Set<SessionId> }> }
}).continuations
let ownedAtDelivery: SessionId[] | undefined
ctx.on('agent/inbox/inserted', ({ agent, message }) => {
if (agent !== middle || message.source.kind !== 'subagent-settled') return
ownedAtDelivery = [...manager.activations.get(middle.id)!.ownedChildren]
})
releaseChild.resolve(undefined)
await waitNoActivation(ctx, inner.childId)
// Still owned at delivery: the parent is structurally unable to settle in
// the window the notice crosses, rather than winning a race against it.
expect(ownedAtDelivery).toEqual([inner.childId])
await waitNoActivation(ctx, outer.childId)
})
it('does not wake a parent whose own teardown already began', async () => {
const hold = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([{ chunks: textResponse('interrupted'), gate: hold.promise }])
const { ctx, parent } = await setupWith(adapter)
const started = await ctx.subagents.startContinuable(startSpec(parent))
await vi.waitFor(() => { expect(ctx.agents.get(started.childId)).toBeDefined() })
const drained = drainManager(ctx)
hold.resolve(undefined)
await drained
// Delivered and durably logged, but no turn: waking a parent the host is
// about to dispose spends a model request nothing reads. What happens to the
// message when that parent is disposed next is pinned by the test below.
expect(settlementNotices(parent)).toHaveLength(1)
expect(settlementNotices(parent)[0]!.text).toBe(
`Background subagent ${started.childId} was stopped before it finished.`
+ '\nIt left no closing message.',
)
expect(parent.session.events.some(event => event.type === 'agent/inbox/spliced')).toBe(true)
expect(parent.session.events.some(event => event.type === 'turn/start')).toBe(false)
expect(parent.status).toBe('idle')
})
it('does not wake a parent below a scoped teardown root', async () => {
const hold = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([{ chunks: textResponse('interrupted'), gate: hold.promise }])
const { ctx, parent } = await setupWith(adapter)
const started = await ctx.subagents.startContinuable(startSpec(parent))
await vi.waitFor(() => { expect(ctx.agents.get(started.childId)).toBeDefined() })
const drained = ctx.subagents.drainContinuableDescendants([parent])
hold.resolve(undefined)
await drained
expect(settlementNotices(parent)).toHaveLength(1)
expect(parent.session.events.some(event => event.type === 'turn/start')).toBe(false)
})
it('records but cannot deliver a teardown notice once the parent is disposed too', async () => {
const hold = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([{ chunks: textResponse('interrupted'), gate: hold.promise }])
const { ctx } = await setupWith(adapter)
const parentId = SessionId('closing-parent')
const host = await ctx.agents.create({
sessionId: parentId,
agentOptions: { provider: 'mock', model: 'mock' },
})
const started = await ctx.subagents.startContinuable(startSpec(host.agent))
await vi.waitFor(() => { expect(ctx.agents.get(started.childId)).toBeDefined() })
const drained = ctx.subagents.drainContinuableDescendants([host.agent])
hold.resolve(undefined)
await drained
expect(settlementNotices(host.agent)).toHaveLength(1)
// Disposal is a `keepInbox: false` cancel, so it durably cancels the notice
// it never claimed. Teardown delivery therefore reaches a parent that is
// still resident — a resumed one reads the log, not a pending message — and
// no wording anywhere may promise otherwise.
await host.dispose()
const resumed = await ctx.agents.resume({
resumeSessionId: parentId,
agentOptions: { provider: 'mock', model: 'mock' },
})
expect(settlementNotices(resumed.agent)).toEqual([])
await resumed.dispose()
// The account is still in the durable log: delivered, then cancelled unread.
const persisted = await ctx.sessionPersistence.load(parentId)
expect(persisted.events.flatMap(event => event.type === 'agent/inbox/spliced'
? [{ inserted: event.data.inserted.length, removed: event.data.removedCount ?? 0 }]
: [])).toEqual([{ inserted: 1, removed: 0 }, { inserted: 0, removed: 1 }])
})
it('drops the notice without disturbing teardown when the parent is gone', async () => {
const releaseChild = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([{ chunks: textResponse('answer'), gate: releaseChild.promise }])
const { ctx } = await setupWith(adapter)
const warnings: string[] = []
ctx.logger.warn = (text: string) => { warnings.push(text) }
const host = await ctx.agents.create({
sessionId: SessionId('disposable-parent'),
agentOptions: { provider: 'mock', model: 'mock' },
})
const started = await ctx.subagents.startContinuable(startSpec(host.agent))
const ends: SubagentRunEndInfo[] = []
ctx.on('subagent/end', (info) => { ends.push(info) })
releaseChild.resolve(undefined)
await host.dispose()
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(ends).toHaveLength(1) })
expect(warnings).toEqual([])
})
it('logs a rejected notice instead of failing the child\'s teardown', async () => {
const { ctx, parent } = await setup([textResponse('the answer')])
const warnings: string[] = []
ctx.logger.warn = (text: string) => { warnings.push(text) }
vi.spyOn(parent, 'followup').mockImplementation(() => {
throw new Error('parent closed during delivery')
})
const ends: SubagentRunEndInfo[] = []
ctx.on('subagent/end', (info) => { ends.push(info) })
const started = await ctx.subagents.startContinuable(startSpec(parent))
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(ends).toHaveLength(1) })
expect(ends[0]!.stopReason).toBe('completed')
expect(warnings.some(warning => warning.includes('settlement notice was not delivered'))).toBe(true)
})
it('stays silent about a child the caller was told does not exist', async () => {
const { ctx, parent } = await setup([])
const drains: Promise<void>[] = []
ctx.on('subagent/start', () => { drains.push(drainManager(ctx)) })
await expect(ctx.subagents.startContinuable(startSpec(parent)))
.rejects.toMatchObject({ code: 'DRAINING' })
await Promise.all(drains)
expect(settlementNotices(parent)).toEqual([])
})
})
describe('continuable lifecycle observation', () => {
it('emits one paired start/end per residency epoch', async () => {
const { ctx, parent } = await setup([textResponse('first'), textResponse('second')])
parkParent(ctx, parent)
const starts: SubagentRunInfo[] = []
const ends: SubagentRunEndInfo[] = []
ctx.on('subagent/start', (info) => { starts.push(info) })
@@ -1510,6 +2054,8 @@ describe('continuable lifecycle observation', () => {
expect(starts.map(info => info.provider)).toEqual(['spawn', 'spawn'])
// Each end pairs its own start's runId.
expect(ends.map(info => info.runId)).toEqual(starts.map(info => info.runId))
// Both epochs ran their own scripted response; neither exhausted the corpus.
expect(ends.map(info => info.stopReason)).toEqual(['completed', 'completed'])
})
})
@@ -16,6 +16,7 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as tool from '../src/list-agents.ts'
import { parkParent } from './park-parent.ts'
/** One scripted response that may wait on a caller-released gate before streaming. */
interface GatedEntry {
@@ -63,6 +64,7 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
await ctx.plugin(tool)
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
parkParent(ctx, parent)
return { ctx, parent, adapter }
}
@@ -0,0 +1,22 @@
/**
* Shared suite helper: keep this package's stand-in parent out of a scripted
* model corpus.
* @module park-parent
*/
import type { Context } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
/**
* Reject every step of the stand-in parent. Each child settlement wakes its
* parent, and these suites size their scripts for child turns only; the tests
* assert on delivery rather than on the parent's own turn.
* @param ctx - the booted test context.
* @param parent - the stand-in parent whose turns must not reach the model.
*/
export function parkParent(ctx: Context, parent: { id: SessionId }): void {
ctx.on('agent/pre-step', async ({ agent: subject }, next) => {
if (subject.id !== parent.id) return next()
return { kind: 'reject' as const }
})
}
@@ -15,6 +15,7 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as tool from '../src/index.ts'
import { parkParent } from './park-parent.ts'
/** One scripted response that may wait on a caller-released gate before streaming. */
interface GatedEntry {
@@ -62,6 +63,7 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
await ctx.plugin(tool)
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
parkParent(ctx, parent)
return { ctx, parent, adapter }
}
@@ -549,6 +549,9 @@ describe('dsh-tool-subagent-report result independence', () => {
expect(ctx.agents.get(started.childId) === undefined).toBe(true)
}, { timeout: 5_000 })
// The parent does learn the child settled — that account is the
// continuation service's, carried under its own `subagent-settled` source.
// Nothing turns the child's final answer into a report it did not send.
expect(reports(parent)).toEqual([])
expect(userTexts((await ctx.sessionPersistence.load(started.childId)).events)).toEqual(['child task'])
expect(ctx.get('tasks')).toBeUndefined()
@@ -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/tool-subagent/README.md
README.md: ac3ec0563cce9128608ca31860b034a103dc1a3a
README.zh.md: d64831d7cf64800ad3307ce6cb7f294500a0a6f0
README.md: a46300cc5dffcbc420ed9525d8f081c670771294
README.zh.md: 985d33beb2ac7de60527b457c4786a0257232611
+5 -5
View File
@@ -10,7 +10,7 @@ Each plugin instance binds one `provider` to one `toolName`; the model receives
A foreground call passes the execution signal through startup and execution, awaits `run.result`, and always awaits `run.dispose()` before returning. Only `completed` returns the canonical `{ kind: 'foreground', runId, output: JsonValue[] }`, rendered as the same final text; abort, refusal, token limit, and other failures become errored tool results whose message appends the child's preserved partial text (the `SubagentResult.output` selection) after the stop-reason headline, so a truncated answer is never reported as success yet never silently lost. If result collection and disposal both reject, the errored result preserves both diagnostics.
With `run_in_background: true`, `backgroundMode` selects the route. `one-shot` registers a plain parent-owned Task and returns canonical `{ kind: 'background', taskId }`, rendered as `started background subagent task <id>`, even when the provider supports continuable children; generic task tools own its later status, collection, cancellation, and notices. `continuable` requires a provider with the `prepareContinuable` capability, calls `ctx.subagents.startContinuable()`, and returns `{ kind: 'continuable', subagentId }`, rendered as `started subagent <childId>`. The continuable route resolves at inbox acceptance: the child owns its own turns from there, so this call neither waits for nor collects a result, and the child does not report back — its transcript by that id is the source of its output, and the optional global `send_message` tool sends it more work. Starting continuable work does not require `send_message` to be loaded. See the [background subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), and the [merged-service Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
With `run_in_background: true`, `backgroundMode` selects the route. `one-shot` registers a plain parent-owned Task and returns canonical `{ kind: 'background', taskId }`, rendered as `started background subagent task <id>`, even when the provider supports continuable children; generic task tools own its later status, collection, cancellation, and notices. `continuable` requires a provider with the `prepareContinuable` capability, calls `ctx.subagents.startContinuable()`, and returns `{ kind: 'continuable', subagentId }`, rendered as `started subagent <childId>`. The continuable route resolves at inbox acceptance: the child owns its own turns from there, so this call neither waits for nor collects a result. The child's transcript by that id remains the source of its detailed output, and the optional global `send_message` tool sends it more work. The parent is not left guessing when to look, though: the continuation service delivers one settlement notice to it whenever a continuable child's Activation ends, which is why the schema tells the model it will be told and must not poll. Starting continuable work does not require `send_message` to be loaded. See the [background subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), and the [merged-service Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
`toolFilter` changes the child's global tool layer but is not a parent-derived authority ceiling. See the [agent-scope security non-goal](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals).
@@ -37,7 +37,7 @@ Foreground and background calls are exclusive. Children may share the parent's w
#### What the model sees
The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) under this instance's configured name while its provider exists. Provider context inheritance changes the tool and prompt descriptions; enabled background mode adds `run_in_background`, and continuable mode describes starting a background subagent that keeps its conversation and returns its subagent id, while one-shot mode describes a background task id collected with `task_output` and stopped with `task_kill`.
The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) under this instance's configured name while its provider exists. Provider context inheritance changes the tool and prompt descriptions; enabled background mode adds `run_in_background`, and continuable mode describes starting a background subagent that keeps its conversation, returns its subagent id, and reports its own completion — so the model is told never to poll or wait on it — while one-shot mode describes a background task id collected with `task_output` and stopped with `task_kill`.
#### Token effect
@@ -65,11 +65,11 @@ Append-only; newly visible content follows the reusable request prefix and does
#### What the model sees
Start returns exactly `started subagent <childId>` in configured continuable mode, or `started background subagent task <id>` in configured one-shot mode. In one-shot mode the generic task surface provides later status, final output, cancellation responses, and notices. In continuable mode the child does not report back; an independently loaded `send_message` tool delivers follow-ups, and the child's transcript by its id is the source of its output.
Start returns exactly `started subagent <childId>` in configured continuable mode, or `started background subagent task <id>` in configured one-shot mode. In one-shot mode the generic task surface provides later status, final output, cancellation responses, and notices. In continuable mode this tool returns no result of its own; the child's settlement reaches the parent as a [service-owned notice](../subagent/README.md#settlement-notice), an independently loaded `send_message` tool delivers follow-ups, and the child's transcript by its id is the source of its detailed output.
#### Token effect
The acknowledgement is retained; a one-shot final output enters parent history only when collected or injected, while a continuable child's output never returns through this tool.
The acknowledgement is retained; a one-shot final output enters parent history only when collected or injected, while a continuable child's output never returns through this tool — its settlement notice arrives independently of any tool result.
#### KV Cache effect
@@ -77,6 +77,6 @@ Append-only; newly visible content follows the reusable request prefix and does
## Known Limitations and Deferred Work
- **Background runs expose no result through this tool** — a one-shot task's final output is collected through the generic task surface, and a continuable child's output stays in its own session, read by its subagent id.
- **Background runs expose no result through this tool** — a one-shot task's final output is collected through the generic task surface, and a continuable child's output stays in its own session, read by its subagent id. The settlement notice states how that child ended and carries its closing message, but it is not this call's return value and cannot be awaited here.
- **Duplicate names across waiting instances are detected late** (`TODO(subagent-dup-toolname)`) — preventing provider-registration rollback requires a registry of intended names.
- **Child policy is fixed per instance** — another model, persona, tool filter, or depth cap requires another distinctly named tool.
+5 -5
View File
@@ -10,7 +10,7 @@
前台调用会让执行信号贯穿启动和执行,等待 `run.result`,并且在返回前总会等待 `run.dispose()`。只有 `completed` 会返回规范值 `{ kind: 'foreground', runId, output: JsonValue[] }`,并渲染为相同的最终文本;中止、拒绝、token 上限和其他失败都会变成出错的工具结果,其消息在终止原因标题之后附带子代理保留下来的部分文本(即 `SubagentResult.output` 的选取结果)——被截断的回答不会被报告为成功,也绝不会被悄悄丢弃。如果结果收集与 dispose(资源释放)都 reject,出错的结果会保留两项诊断信息。
设置 `run_in_background: true` 后,`backgroundMode` 会选择路由。`one-shot` 会注册一个归父级所有的普通 Task,并返回规范值 `{ kind: 'background', taskId }`,渲染为 `started background subagent task <id>`,即使提供方支持可继续子 agent 也不例外;通用 Task 工具负责其后续状态、收集、取消和通知。`continuable` 要求提供方具备 `prepareContinuable` 能力,调用 `ctx.subagents.startContinuable()`,并返回 `{ kind: 'continuable', subagentId }`,渲染为 `started subagent <childId>`。可继续路由在 inbox 接受时结算:子 agent 自此拥有自己的轮次,因此该调用既不等待也不收集结果,而且子 agent 不会回报——通过该 id 查看其 transcript(文本记录)是其输出来源,可选的全局 `send_message` 工具则向其发送更多工作。启动可继续工作不要求加载 `send_message`。见 [后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续的 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)和[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)。
设置 `run_in_background: true` 后,`backgroundMode` 会选择路由。`one-shot` 会注册一个归父级所有的普通 Task,并返回规范值 `{ kind: 'background', taskId }`,渲染为 `started background subagent task <id>`,即使提供方支持可继续子 agent 也不例外;通用 Task 工具负责其后续状态、收集、取消和通知。`continuable` 要求提供方具备 `prepareContinuable` 能力,调用 `ctx.subagents.startContinuable()`,并返回 `{ kind: 'continuable', subagentId }`,渲染为 `started subagent <childId>`。可继续路由在 inbox 接受时结算:子 agent 自此拥有自己的轮次,因此该调用既不等待也不收集结果通过该 id 查看其 transcript(文本记录)是其详细输出来源,可选的全局 `send_message` 工具则向其发送更多工作。不过父级无需猜测何时查看:每当可继续子 agent 的 Activation 结束,继续执行服务都会向父级投递一条结算通知——正因如此,schema 才告诉模型它会被通知,且不得轮询。启动可继续工作不要求加载 `send_message`。见 [后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续的 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)和[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)。
`toolFilter` 会改变子 agent 的全局工具层,但不是从父级派生的权限上限。见 [agent 作用域的安全非目标](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals)。
@@ -37,7 +37,7 @@
#### 模型看到的内容
当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent)。提供方是否继承上下文会改变工具描述和提示词描述;启用后台模式会添加 `run_in_background`,可继续模式描述为启动一个保留其对话返回子 agent id 的后台子 agent而一次性模式描述为返回一个用 `task_output` 收集、用 `task_kill` 停止的后台任务 id。
当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent)。提供方是否继承上下文会改变工具描述和提示词描述;启用后台模式会添加 `run_in_background`,可继续模式描述为启动一个保留其对话返回子 agent id 并会自行报告完成的后台子 agent——因此模型被告知绝不要轮询或等待它——而一次性模式描述为返回一个用 `task_output` 收集、用 `task_kill` 停止的后台任务 id。
#### Token 影响
@@ -65,11 +65,11 @@
#### 模型看到的内容
在配置的可继续模式下,启动时返回内容恰为 `started subagent <childId>`;在配置的一次性模式下,则返回 `started background subagent task <id>`。一次性模式下,通用 Task 接口提供后续状态、最终输出、取消响应和通知。可继续模式下,子 agent 不会回报;独立加载的 `send_message` 工具会投递后续消息,而通过其 id 查看子 agent 的 transcript 即是其输出来源。
在配置的可继续模式下,启动时返回内容恰为 `started subagent <childId>`;在配置的一次性模式下,则返回 `started background subagent task <id>`。一次性模式下,通用 Task 接口提供后续状态、最终输出、取消响应和通知。可继续模式下,本工具不返回自己的结果;子 agent 的结算会以[服务负责的通知](../subagent/README.md#settlement-notice)到达父级,独立加载的 `send_message` 工具会投递后续消息,而通过其 id 查看子 agent 的 transcript 即是其详细输出来源。
#### Token 影响
确认消息会被保留;一次性最终输出只在收集或注入时进入父级历史,而可继续子 agent 的输出绝不会通过本工具返回。
确认消息会被保留;一次性最终输出只在收集或注入时进入父级历史,而可继续子 agent 的输出绝不会通过本工具返回——其结算通知独立于任何工具结果到达
#### KV Cache 影响
@@ -77,6 +77,6 @@
## 已知限制与暂缓事项
- **后台运行不通过本工具公开结果**:一次性任务的最终输出通过通用 Task 接口收集,可继续子 agent 的输出留在其自身会话中,按其 subagent id 读取。
- **后台运行不通过本工具公开结果**:一次性任务的最终输出通过通用 Task 接口收集,可继续子 agent 的输出留在其自身会话中,按其 subagent id 读取。结算通知会说明该子 agent 如何结束并携带其收尾消息,但它不是本次调用的返回值,也无法在此等待。
- **等待中实例的重复名称发现较晚**(`TODO(subagent-dup-toolname)`):若要阻止提供方注册回滚,需要一份预期名称注册表。
- **每个实例的子 agent 策略固定**:其他模型、persona、工具过滤器或深度上限都需要另一个名称不同的工具。

Some files were not shown because too many files have changed in this diff Show More