Implement the continuable background subagents RFC: a durable child session with a series of Task-backed activations, each disposing its run before the Task settles. - dsh-subagent: rename SubagentRun.sendMessage to strict steer, drop run-level resume, add SubagentProvider.resume dispatch via SubagentService.resume, the continuation start field, and the versioned model-hidden subagent/descriptor session event. - dsh-subagent-inprocess/-spawn/-fork: publish the control-allocated child id, append the descriptor inside the initial turn, implement cold resume from the child's own transcript under the live parent scope, and strict running-only steer. - dsh-subagent-control (new): SubagentControlService owning stable child ids, descriptor snapshot/fold/authorization, Task-backed activation with settle-then-dispose ordering, the process-local active-run association, and steer-or-resume sendMessage routing. - dsh-tool-subagent: background route branches on the provider's resume capability (continuable via the control service; one-shot task for ACP), returning both child and task ids. - dsh-tool-subagent-control (new): the globally named send_message tool rendering steered/started routes. Keyless coverage spans Task ownership and disposal ordering, running delivery, cold follow-up, descriptor rejection and rollback, known-id reconstruction, kill during lookup, admission races, and a new subagent-continuable ACP snapshot scenario.
38 lines
4.3 KiB
Markdown
38 lines
4.3 KiB
Markdown
# @deepseek-ai/dsh-subagent-control
|
|
|
|
The continuable-subagent control service (`ctx.subagentControl`): the one orchestration path that binds a durable child session to a series of disposable Task-backed activations. Model tools and human-facing adapters call the same contract; the low-level `ctx.subagents` seam stays collection-, Task-, and persistence-agnostic.
|
|
|
|
## Activation lifecycle
|
|
|
|
A continuable background subagent is a durable child session with a series of Task-backed activations. `startContinuable()` allocates the stable child session id before Task creation, snapshots the descriptor inputs (a non-JSON input throws with no Task), and registers the initial activation's Task; the provider publishes exactly that child id and appends the versioned `subagent/descriptor` event inside the child's first turn. Every activation — initial or resumed — creates a fresh Task whose settlement awaits the child result, disposes the run, and only then records the `TaskOutcome`: a terminal Task leaves the durable child session but no live child Agent.
|
|
|
|
`sendMessage(parent, childId, message)` owns steer-or-resume routing. A running activation receives live delivery through the run's strict `steer` capability and returns the existing Task id (`steered`); an absent activation starts a fresh Task that loads the persisted child, authorizes the recorded `parentSession` as the direct parent, folds the descriptor, and dispatches `SubagentService.resume()` (`started`). Failure throws and means the message was not delivered: losing a strict-steering race with Task settlement never falls through to cold resume within the same call, and a live registry Agent outside the activation association is an ownership conflict rather than an adoption target.
|
|
|
|
Cancellation targets the whole activation. `task_kill` or owner disposal aborts the Task-owned signal; before publication the provider rejects only after its creation transaction rolled back to quiescence, afterwards the signal cancels the published run, and settlement records `killed` only once the activation is quiescent. Human input shares this path: an adapter submits child input through `sendMessage()` under the loaded parent, so parent and human messages that joined one turn share its result and cancellation outcome, and `TaskService.start()`'s control-surface requirement applies (load `@deepseek-ai/dsh-tool-tasks` or attach a surface).
|
|
|
|
The activation association is process-local routing state, installed before any persistence or provider await and removed after run disposal and Task terminal publication. It is not a durable catalog: restart recovers the child session, not in-flight Tasks or their notifications.
|
|
|
|
## Model Experience
|
|
|
|
### Task completion and output
|
|
|
|
#### What the model sees
|
|
|
|
None directly, as this package registers no tool and no prompt text; the model observes continuable children through `@deepseek-ai/dsh-tool-subagent`'s background acknowledgement, `@deepseek-ai/dsh-tool-subagent-control`'s `send_message` results, and the generic task surface, whose outputs this service produces.
|
|
|
|
#### Token effect
|
|
|
|
None beyond the consuming tools' own results.
|
|
|
|
#### KV Cache effect
|
|
|
|
None; this service appends nothing to any model-visible sequence.
|
|
|
|
## Known Limitations and Deferred Work
|
|
|
|
- **Concurrent stopped-child admission is not atomic across awaits** — the synchronous association install admits one activation per child in this process, but a caller bypassing the control service can still race it; the Agent registry's same-id collision is the final backstop, and the losing Task fails with its message not delivered.
|
|
- **The association coordinates only one runtime** — concurrent resume from multiple processes needs a persistence-level lease or compare-and-set, which no backend offers yet.
|
|
- **Task records are process-local** — restart recovers the durable child session, not an interrupted Task, its result, or its completion notice; durable Task recovery is a separate concern.
|
|
- **Human interaction requires the exact live parent Agent** — Task access is fenced by the owner session and owner disposal cancels its Tasks; standalone child conversations belong to the interactive-side-sessions proposal, not this Task-owned lifecycle.
|
|
- **ACP children remain one-shot** — `AcpProvider.resume` and per-child continuation advertisement are deferred until the remote-session descriptor contract is resolved.
|