refactor(tasks): declare-then-execute — ctx.tasks.start() replaces register()
start({ kind, label, owner, run }) preflights everything that can fail
(the attachSurface fence, validation, the owner-cleanup attach) BEFORE
invoking the producer's run() starter, then commits atomically —
'work started but never got a collectable id' is now structurally
impossible instead of a producer try/catch rollback obligation (the
P1 review fix, rebuilt on #185's declare/execute split). Producers
lose their catch-wraps; the leak tests now pin the stronger property
that a failed preflight never spawns anything. TaskRegistration splits
into TaskStart (identity + run) and TaskHooks (cancel/done/readOutput);
docs, type-equiv manifest, catalogs, and both RFCs move with it.
This commit is contained in:
22 files changed
+281
-240
No files matched your search
@@ -16,7 +16,7 @@ Each `dsh-tool-subagent` instance may expose `run_in_background?: boolean`, gate
|
||||
|
||||
A foreground call keeps the synchronous semantics: it waits for `run.result`, returns final text on `completed`, maps non-clean terminal stop reasons to an errored tool result, and disposes the run in `finally`.
|
||||
|
||||
A background call validates that a parent agent exists, checks an already-aborted tool signal before starting, starts the provider run through `ctx.subagents`, registers the run with `ctx.tasks`, and returns `started background subagent task <task_id>`. After the id is returned the tool-call signal is NOT connected to `run.cancel()` — the parent step may finish while the child continues; cancellation belongs to `task_kill` and the owner-cleanup path. A registration that throws (the no-control-surface fence) does not orphan the child: the producer cancels the run, awaits its `done` (which settles only after `run.dispose()`), and rethrows — the model never learns an id for work that is not actually tracked. `ctx.tasks.register()` supplies the runtime guarantees this feature needs and this RFC does not implement: kind-prefixed branded task ids, owner-scoped access (the parent agent is the owner; another session's agent cannot read or kill the task), the loud no-control-surface failure, completion-notice injection, and the generic prompt guidance.
|
||||
A background call validates that a parent agent exists, checks an already-aborted tool signal, and hands the delegation to `ctx.tasks.start()` — the runtime preflights the control-surface fence and the owner cleanup BEFORE its `run()` starter creates the child through `ctx.subagents`, so a child that started without a collectable id is structurally impossible — then returns `started background subagent task <task_id>`. After the id is returned the tool-call signal is NOT connected to `run.cancel()` — the parent step may finish while the child continues; cancellation belongs to `task_kill` and the owner-cleanup path. `ctx.tasks.start()` supplies the runtime guarantees this feature needs and this RFC does not implement: kind-prefixed branded task ids, owner-scoped access (the parent agent is the owner; another session's agent cannot read or kill the task), the loud no-control-surface failure, completion-notice injection, and the generic prompt guidance.
|
||||
|
||||
The registration maps the seam vocabulary onto the runtime's:
|
||||
|
||||
@@ -53,10 +53,10 @@ The child session is already the trace for internal reasoning, tool calls, and i
|
||||
|
||||
## Testing
|
||||
|
||||
Unit coverage pins the stop-reason → outcome mapping (`runOutcome`, including unknown merge-extensible reasons), `settleRun`'s dispose-before-report on both result paths, the detached-signal contract (a pre-aborted signal refuses to start; a returned id is never wired to the tool signal), background settlement collected through the real `task_output`/`task_kill` tools, the no-orphan rollback when `register()` throws, per-instance schema gating (`enableRunInBackground: false` omits the parameter and the background wording), and the loud failure when the tasks runtime is absent. Snapshot coverage pins the changed `subagent`/`subagent_fork` schemas through the pinned-header fixture; recording a live background-delegation transcript requires a `DEEPSEEK_API_KEY` re-record and remains named follow-up work.
|
||||
Unit coverage pins the stop-reason → outcome mapping (`runOutcome`, including unknown merge-extensible reasons), `settleRun`'s dispose-before-report on both result paths, the detached-signal contract (a pre-aborted signal refuses to start; a returned id is never wired to the tool signal), background settlement collected through the real `task_output`/`task_kill` tools, the structural no-orphan guarantee (a failed `tasks.start` preflight never invokes the provider), per-instance schema gating (`enableRunInBackground: false` omits the parameter and the background wording), and the loud failure when the tasks runtime is absent. Snapshot coverage pins the changed `subagent`/`subagent_fork` schemas through the pinned-header fixture; recording a live background-delegation transcript requires a `DEEPSEEK_API_KEY` re-record and remains named follow-up work.
|
||||
|
||||
## Consequences
|
||||
|
||||
Slow delegation no longer holds the parent step open: the model fans out background children, keeps working, and collects with the same three control tools it already uses for bash — no new habit, no schema clones, and `dsh-tool-subagent`'s per-provider shape survived unchanged. The feature's usability depends on the tasks pair being loaded; the runtime's `register()` fence turns a missing control surface into a loud, actionable error rather than a silent dead end, and the `dsh-agent-core` bundle ships the pair so every stock deployment has it.
|
||||
Slow delegation no longer holds the parent step open: the model fans out background children, keeps working, and collects with the same three control tools it already uses for bash — no new habit, no schema clones, and `dsh-tool-subagent`'s per-provider shape survived unchanged. The feature's usability depends on the tasks pair being loaded; the runtime's `start()` preflight fence turns a missing control surface into a loud, actionable error (raised before any child exists) rather than a silent dead end, and the `dsh-agent-core` bundle ships the pair so every stock deployment has it.
|
||||
|
||||
The prompt guidance reduces abandoned tasks but cannot force a model to collect every background result. Runtime cleanup through the awaited owner-disposal path is the hard stop; a future planner or guard could enforce "no final answer with relevant running tasks" more strongly if the prompt proves insufficient. A background child outliving its starting tool call means a misbehaving child consumes tokens until collected, killed, or owner-disposed; `task_list` keeps it visible, and setting `enableRunInBackground: false` per instance keeps a deployment's delegation strictly synchronous.
|
||||
Reference in New Issue
Block a user