@howarewoo/pi-subagents 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +2 -2
- package/docs/delegation-contract.md +8 -8
- package/docs/verification.md +5 -1
- package/package.json +1 -1
- package/src/child-execution.ts +17 -8
- package/src/coordinator.ts +110 -21
- package/src/delegation-types.ts +7 -0
- package/src/hub.ts +2 -3
- package/src/pi-extended-subagents.ts +5 -19
- package/src/subagent-widget.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0]
|
|
6
|
+
|
|
7
|
+
Requires Node 22.19+ and stable Pi 1.5.2 or newer, unchanged from 0.2.x. Tested host
|
|
8
|
+
remains Pi 1.5.2; newer stable hosts are allowed, not guaranteed compatible.
|
|
9
|
+
|
|
10
|
+
### Breaking — completed-context resume and migration
|
|
11
|
+
|
|
12
|
+
- Resume of successfully completed work now continues the same full SDK conversation and tool history, with the same identity, generation and session count, rather than starting a fresh session. To start fresh work in a new generation, use explicit model-issued `delegate_coordinate` `followup` or `revive` and supply the context it needs; those operations do not replay the old conversation.
|
|
13
|
+
- Only the deadline window resets on resume; request/tool/spawn counters remain cumulative, with at most 64 execution windows per generation. Paused windows retain provisional structured submission state and publish no final outcome artifacts. Completed windows preserve immutable outcome/artifact evidence in `completedWindows`; later completed-window artifacts use `window-N/` without replacing earlier files. Resumed completed structured assignments require a new `submit_result`.
|
|
14
|
+
- Context restoration lasts only for the current parent runtime; saved transcripts are not reload/restart recovery state. Let active tasks drain before updating, then restart Pi. Reload/shutdown aborts and drains children rather than migrating them. Resume the exact parent generation first, then explicitly resume selected children; descendants never resume automatically. Existing authority, grants, authentication, capacity and writer checks still apply.
|
|
15
|
+
|
|
16
|
+
### Changes
|
|
17
|
+
|
|
18
|
+
- Make the queued deadline/continuation lifecycle regression test deterministic with a controlled deadline clock, preserving pre-dispatch expiry and repeated continuation coverage while verifying shutdown disposes an actual retained SDK session. Runtime deadline policy is unchanged.
|
|
19
|
+
|
|
20
|
+
- Correct resume to continue paused or successfully completed conversations with the same identity, generation, session count and full SDK/tool history. Completed runtimes restore from the canonical SessionManager into the same transcript, never a result summary or silent fresh generation. Preserve cumulative budgets and earlier window artifacts/outcomes; revalidate current authority/auth/capacity, reject stopped/failed/retired/unavailable identities, and bound continuations to 64 windows. Explicit followup/revive behavior is unchanged.
|
|
21
|
+
|
|
22
|
+
- Preserve successfully completed descendants across ancestor completion for explicit parent-first same-context resume, without undoing explicit retirement. Validate retained post-handoff model availability on resume rather than requiring the obsolete admission model; keep lineage, grants and writer checks.
|
|
23
|
+
|
|
24
|
+
- Count individual subagent sessions, including retained historical generations, in the widget total using the same roster source as the unfiltered Hub. Distinct tasks sharing a profile and nested sessions each count separately; active widget rows remain bounded and scheduling/lifecycle behavior is unchanged.
|
|
25
|
+
|
|
5
26
|
## [0.2.1]
|
|
6
27
|
|
|
7
28
|
Requires Node 22.19+ and stable Pi 1.5.2 or newer. Tested host remains Pi 1.5.2;
|
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ Pi discovers the native `subagent` tool at startup through its tool description
|
|
|
104
104
|
- `worker` maps to the configured `implementation` profile; `scout`, `review` and `general` are also available.
|
|
105
105
|
- Calls are async by default and wake the parent after drain. Set `async:false` to wait; use `tasks:[{agent,task,cwd?}]` for a batch.
|
|
106
106
|
- Control a task with its returned `id` and `action:"status"`, `"steer"`, `"stop"` or `"resume"`. Steer/resume also require `message`.
|
|
107
|
-
- Resume
|
|
107
|
+
- Resume continues a drained paused **or completed** conversation, preserving the same identity, generation, session count and full SDK conversation/tool history. Completed idle/parked agents restore their canonical session into a new SDK runtime using the same transcript, not a result summary. Missing restoration state, stopped/failed identities and retired descendants reject; there is no fresh-context fallback. Explicit `delegate_coordinate` followup/revive still start fresh work.
|
|
108
108
|
|
|
109
109
|
The coordinator supplies an existing workspace and the complete handoff, including accessible skill/reference paths. The extension does not create or manage worktrees. This is the [supported orchestration interface](docs/delegation-contract.md#native-orchestration-compatibility), not compatibility with every competing workflow script or custom agent definition.
|
|
110
110
|
|
|
@@ -197,7 +197,7 @@ All bundled profiles may delegate to `scout` and `review` by default, within a t
|
|
|
197
197
|
Default task limits are **30 minutes**, **100 model requests** and **100 tool calls**:
|
|
198
198
|
|
|
199
199
|
- A deadline pauses the active subtree after tool/provider drain, releasing capacity and writer leases but retaining sessions and conversations.
|
|
200
|
-
- Use `subagent` resume or root-only `delegate_coordinate` `{action:"resume",target:{id,generation},assignment:"Continue…"}` to continue the same identity and conversation. Only the deadline resets; request/tool/spawn counters remain cumulative.
|
|
200
|
+
- Use `subagent` resume or root-only `delegate_coordinate` `{action:"resume",target:{id,generation},assignment:"Continue…"}` to continue the same identity and conversation. Only the deadline resets; request/tool/spawn counters remain cumulative (at most 64 windows per generation). Completed windows retain earlier result/artifact evidence in `completedWindows`; a resumed completed structured assignment requires a new `submit_result`.
|
|
201
201
|
- Resume the parent first, then explicitly selected children. Descendants never resume automatically.
|
|
202
202
|
- Stop, shutdown and hard request/tool limits cancel and dispose. Unresponsive code must drain before pause or cancellation can release resources.
|
|
203
203
|
|
|
@@ -29,10 +29,10 @@ The model-only `subagent` tool is registered and described through Pi's public `
|
|
|
29
29
|
- Launch: `{agent,task,cwd?,model?,limits?,async?}`. `agent` is required: `general`, `scout`, `review`, `implementation`, or explicit compatibility name `worker` for the configured `implementation` profile. Unknown/custom agent definitions are rejected, not silently mapped. Authority and prompts still resolve through the existing profiles; no ambient Markdown agent loading or role/model fallback is added.
|
|
30
30
|
- Batch: `{tasks:[{agent,task,cwd?,model?,limits?}],cwd?,async?}`. Shared cwd is the default for items lacking one. Existing whole-batch preflight, FIFO limits, ordered outcomes and global writer serialization apply.
|
|
31
31
|
- `async` defaults true; false waits for final results. Accepted calls return immediately with exact `id:"UUID@generation"`, `taskId` (original UUID), generation and normal snapshot fields/artifact references. Control handles never prefix-match or resolve to the latest generation. Native `delegate_task` retains its UUID+generation output and foreground default.
|
|
32
|
-
- Read/control: `{action:"agents"}` or `"list"`; `{action:"status"|"stop",id}`; `{action:"steer"|"resume",id,message}`. Status can inspect a historical generation; mutations refuse stale handles. Resume continues a drained paused assignment in the same
|
|
32
|
+
- Read/control: `{action:"agents"}` or `"list"`; `{action:"status"|"stop",id}`; `{action:"steer"|"resume",id,message}`. Status can inspect a historical generation; mutations refuse stale handles. Resume continues a drained paused or successfully completed (idle/parked) assignment in the same conversation and generation, without increasing session count. Completed runtime graphs are reconstructed from the retained canonical SessionManager, preserving system context and actual tool history in the same transcript; no previous-result-summary or fresh-context fallback is allowed. Stopped, failed, cancelled, retired or unavailable conversations reject without starting new work. Use explicit `delegate_coordinate` followup/revive for fresh work. Native coordination remains available for richer inbox/wait/park behavior.
|
|
33
33
|
- Model overrides accept the native exact selection object or `provider/id[:effort]`. Omitted effort uses the parent's selected effort; overrides still require exact operator grants, availability/auth/scope and supported effort. Bare model IDs and unknown providers are not guessed. Omitting model retains ordinary parent/profile routing.
|
|
34
34
|
|
|
35
|
-
An async compatibility launch sends one window-outcome custom message per single/batch call after actual drain (paused or completed), using public `sendMessage(...,{triggerTurn:true,deliverAs:"followUp"})` to wake the parent. Compatibility resume does the same once for its new execution window
|
|
35
|
+
An async compatibility launch sends one window-outcome custom message per single/batch call after actual drain (paused or completed), using public `sendMessage(...,{triggerTurn:true,deliverAs:"followUp"})` to wake the parent. Compatibility resume does the same once for its new execution window; rejected resumes send no completion notification. This may cause a parent model turn and provider charges; no polling or detached worker survives parent shutdown. The original native tool's notification-only behavior is unchanged. Neither a notification nor a receipt proves the model understood the outcome.
|
|
36
36
|
|
|
37
37
|
The coordinator owns workspace allocation, exclusive writer handoff, Git/PR publication and retirement under the active workflow. Supply a complete task and accessible skill/reference paths: general inherits available skills and restricted reviewed leaf capabilities as specified below; workers never inherit parent instructions/history or context files. No scripts/workflow engine, supervisor RPC, automatic worktree lifecycle, external CLI worker, retained-run migration or blanket competitor API parity is promised.
|
|
38
38
|
|
|
@@ -158,11 +158,11 @@ A parent waiting in its child-local `delegate_task` retains its permit. Nested b
|
|
|
158
158
|
|
|
159
159
|
Deadlines pause rather than cancel. `queued|setup|running → pausing → paused` stops new dispatch, aborts SDK work and waits for actual subtree drain before releasing capacity and writer leases. Snapshots expose `pauseReason` (`deadline` or `ancestor deadline`), `retainedContext`, `window`, cumulative request/tool/spawn counters, and unfinalized structured submission state. Paused is neither success nor failure; no final outcome artifacts are published. A queued assignment can pause before any session/transcript exists.
|
|
160
160
|
|
|
161
|
-
Root-only `delegate_coordinate` `{action:"resume",target:{id,generation},assignment:"Continue…"}` or `subagent` `{action:"resume",id:"UUID@generation",message:"Continue…"}` appends that message to the retained SDK conversation. UUID, generation, assignment, workspace, selected model/prewalk state, role instructions, all prior SDK messages/tool calls/results
|
|
161
|
+
Root-only `delegate_coordinate` `{action:"resume",target:{id,generation},assignment:"Continue…"}` or `subagent` `{action:"resume",id:"UUID@generation",message:"Continue…"}` appends that message to the retained SDK conversation. UUID, generation, assignment, workspace, selected model/prewalk state, role instructions, all prior SDK messages/tool calls/results remain intact. A paused window retains its provisional structured submission state. Completed windows retain immutable result/outcome/artifact references in `completedWindows`; their existing files are never replaced. A resumed completed structured assignment requires a new submission (the previous validated value is evidence, not the new window's result). Later completed-window artifacts are published exclusively under `window-N/` inside the same task directory. Each explicit resume starts a fresh configured deadline window (still bounded by originally admitted and current limits). Cumulative requests/tools/spawns are **not** replenished. Each generation permits at most 64 execution windows, bounding completion evidence and notifications. A pre-session queued pause starts the original assignment followed by the continuation when dispatched. After a completed prewalk handoff, resume validates the retained current model and current/admitted lineage grants, not the obsolete admission model; it neither broadens model grants nor rearms prewalk. No automatic replay, undo, process freezing, restart or cross-parent recovery is provided.
|
|
162
162
|
|
|
163
|
-
An ancestor deadline marks its entire active subtree before draining any member. Resume the exact original parent generation first, then explicitly resume selected children; resuming a parent does not resume descendants. Existing no-permit-lending, writer ancestry and admission rules still apply. Parent completion
|
|
163
|
+
An ancestor deadline marks its entire active subtree before draining any member. Resume the exact original parent generation first, then explicitly resume selected children; resuming a parent does not resume descendants. Existing no-permit-lending, writer ancestry and admission rules still apply. Parent completion retires active/paused children, but preserves successfully completed retained child conversations for later explicit resume once that exact parent generation is running again. Explicit stop/retirement remains effective even across parent resume; failed/cancelled children cannot resume.
|
|
164
164
|
|
|
165
|
-
Resume rechecks operator/trusted-project limits and grants, ancestor spawn/capability ceilings, model availability/authentication, callable tools/provenance and writer/capacity admission. It never broadens original admission. A revoked retained grant rejects rather than
|
|
165
|
+
Resume rechecks operator/trusted-project limits and grants, ancestor spawn/capability ceilings, model availability/authentication, callable tools/provenance and writer/capacity admission. It never broadens original admission. A revoked retained grant rejects rather than silently changing the admitted SDK tool/resource graph; restore the grant or stop and launch a newly authorized identity. Policy/auth/capacity/restoration failure leaves the paused or completed conversation intact with an actionable tool error; there is no fallback model or replacement agent. Concurrent duplicate resumes reject atomically. Stop/shutdown wins a concurrent pending resume and disposes retained sessions.
|
|
166
166
|
|
|
167
167
|
Requests/tools remain hard cancellation guards. The first committed hard-limit reason (`requests` or `tools`) is retained and produces final `status:"cancelled"`, `limitReason`, and explanatory `error`. Stop before setup cancels without SDK setup/transcript. Request guards check **before forwarding** to the parent's public registry. Counts include failed dispatches but not rejected over-limit dispatches, auth preflight or provider-internal retries/subrequests. Tool guards count entered, granted custom-tool executions, including permission-denied forwarded calls; malformed/unknown calls rejected by Pi before execution are not counted. Requests still bound loops of rejected tools.
|
|
168
168
|
|
|
@@ -212,7 +212,7 @@ Operator `coordination` grants selected `message`, `inbox`, `wait` actions; abse
|
|
|
212
212
|
| `status` | Root-only pinned `target`; returns `{snapshot,generations,agents}`. Snapshot/history describe assignments; agents describes current presence. Full detached details, bounded text preview. |
|
|
213
213
|
| `stop` | Root-only pinned current `target`; closes idle/parked presence or aborts/drains active subtree. Completed outcomes/files are unchanged. Stopped identities cannot revive. |
|
|
214
214
|
| `park` | Root-only current disposed idle target; records parked presence. Active parking rejects: stop active work explicitly. No live SDK session, shell, permit or writer lease is retained while idle or parked. |
|
|
215
|
-
| `resume` | Root-only drained paused `target` and nonblank `assignment` continuation. Returns the same identity/window snapshot and sends one notification after that resumed window drains. No schema replacement or descendant auto-resume.
|
|
215
|
+
| `resume` | Root-only drained paused or successfully completed `target` and nonblank `assignment` continuation. Returns the same identity/window snapshot and sends one notification after that resumed window drains. No schema replacement or descendant auto-resume. |
|
|
216
216
|
| `followup` / `revive` | Root-only current target and nonblank `assignment`, optional supported `resultSchema`. Followup requires idle; revive requires parked. Returns the new admitted snapshot, not final success. Use wait/status for completion; no unsolicited duplicate generation-completion message is added. |
|
|
217
217
|
|
|
218
218
|
A message returns `{id,sender,recipient,text,mode,status,error?}`. `accepted` means mailbox admission, `queued` means the public SDK accepted a steer/followUp, `handled` means an SDK input handler consumed it, `delivered` means the exact message entered the recipient transcript or an inbox tool/wait returned it. None means model acknowledgment, understanding or assignment completion. `not-running` means no running recipient (including an undelivered receipt whose recipient ended), and `error` preserves delivery errors. Unknown/foreign/stale identities or receipts reject visibly, not as apparent successful delivery. Inbox messages are not automatically steered; steer/followUp are also retained in the bounded mailbox. Their `delivered` state comes from actual SDK user-message events, not queue acceptance. Message text is literal prefixed data, not tool authority.
|
|
@@ -227,7 +227,7 @@ Followup/revive re-resolve operator/trusted-project policy and selected root mod
|
|
|
227
227
|
|
|
228
228
|
The UUID stays stable and generation increments atomically **only after** synchronous admission checks. Concurrent renewals race on the original reference; exactly one can replace it. A newly owned generation directory `<id>-g<N>` holds its SDK session, `agent/`, transcript and exclusive artifacts. Generation 1 keeps `<id>/`. For these completed-agent fresh generations, no old files are opened for continuation, consulted, replayed, appended or migrated. The new prompt contains only the new explicit assignment plus current role/resources and identity; provide any relevant prior conclusions explicitly. This deliberately avoids retained-conversation replay/tool execution and does not claim a warm resumed Pi session.
|
|
229
229
|
|
|
230
|
-
Descendant renewal requires its **exact original** `parentId+parentGeneration` still active, non-cancelling and authorized. Current ancestor spawn roles, depth/limits, remaining spawn count, immediate nested capacity and writer ancestry all revalidate before generation mutation/setup. Replaced/disposed/stopped ancestors reject; the caller must delegate a new identity through an authorized live parent, never reparent the old one.
|
|
230
|
+
Descendant renewal requires its **exact original** `parentId+parentGeneration` still active, non-cancelling and authorized. Current ancestor spawn roles, depth/limits, remaining spawn count, immediate nested capacity and writer ancestry all revalidate before generation mutation/setup. Replaced/disposed/stopped ancestors reject; the caller must delegate a new identity through an authorized live parent, never reparent the old one. Explicit parent stop closes descendant idle/parked presence too. Successful parent completion does not retire already-successful retained descendant conversations; their explicit resume still requires the exact original active parent generation. Old callbacks, steering, stops, submissions, message delivery and waits capture the originating entry/reference, never retarget by latest UUID. Wait outcomes remain pinned even if a later generation is admitted. Shutdown/reload closes all presence and waiters, drains actual resources and suppresses old-runtime final messages; no cross-parent/process revival exists.
|
|
231
231
|
|
|
232
232
|
## Writer coordination and unsupported shell boundary
|
|
233
233
|
|
|
@@ -256,7 +256,7 @@ Hub names are snapshot-derived assignment headlines plus role, not a naming regi
|
|
|
256
256
|
| Outcome wait | Hub `w` → native event wait with explicit 1000ms bound; native tool supports 1–60000ms outcome/inbox/receipt waits with existing deadlock/refusal rules. |
|
|
257
257
|
| Stop subtree | Hub `x` → pinned target → `y` confirmation → existing stop/drain, retaining capacity/writer until disposal. Queued/setup/running/paused UI targets supported; historical/pausing/draining/disposed UI targets refuse, including transitions during pinned input. `/subagent-stop` and native root stop retain existing semantics. |
|
|
258
258
|
| Park | Hub `p` or native root tool; only disposed idle current identity with valid original parent. No warm SDK retained. |
|
|
259
|
-
| Followup/revive | **Native model-issued `delegate_coordinate`
|
|
259
|
+
| Followup/revive | **Native model-issued `delegate_coordinate` followup/revive only**. Idle/parked assignments use fresh session/generation/current narrowed authority. `subagent` resume continues paused or successfully completed conversations with full restored context and unchanged identity/generation/session count. Both require the exact active original parent for descendants. Hub command context lacks `tools/executeTool`: it cannot safely start a generation; f/r only explain refusal, allocate nothing and dispatch nothing. |
|
|
260
260
|
| Child coordination/spawn/model authority | Coordination/model authority remains operator default-off; spawn defaults to scout/review only. All remain project/request/ancestor subset-only; the UI cannot mint grants or impersonate a child. |
|
|
261
261
|
| Manual !/!!, external editors/terminals, arbitrary extension JS | Uncoordinated; model-tool-only writer boundary, never a shell ban/wrapper/sandbox. |
|
|
262
262
|
| Supplied workspace / production isolation | Explicit path/shell-cwd adaptation through root hooks is supported for existing supplied workspaces. Filesystem sandboxing, managed worktrees, copying/apply and cleanup remain unimplemented; cwd is not a resource grant. |
|
package/docs/verification.md
CHANGED
|
@@ -28,7 +28,7 @@ Startup guidance, examples and compatibility tests describe native Pi delegation
|
|
|
28
28
|
|
|
29
29
|
The approved follow-up adds startup `subagent` discovery/guidance, common launch/control calls, supplied-workspace execution and native completion wake-ups. It does not import another extension or promise full API/configuration parity. Source/test manifest digest: `41d44bc852b2c6b60a61112232992a704d13a0e590abd5ac152a5f7b8f5d470c`.
|
|
30
30
|
|
|
31
|
-
**Controlled SDK/CLI integration:** the default suite passes 145/145 with zero failures, cancellations or skips. Typecheck, SDK exercise, 19-file package dry run and diff checks pass. The actual isolated Pi CLI exposes `subagent` at ordinary startup without an enable call and completes two scouts without activating extra builtins. Real SDK children read/edit/write/run bash in a throwaway Git worktree containing spaces and an apostrophe; root hooks see the actual adapted arguments and retain their real parent cwd. Tests cover root tool/write refusal, failed-cd no-fallback behavior, global writer queue/parent-write exclusion, atomic invalid-cwd admission, search defaults/shorthand refusal, parent-policy retention, task-file cwd, explicitly granted nested workspace inheritance, exact model selection, idle-parent wake once,
|
|
31
|
+
**Controlled SDK/CLI integration:** the default suite passes 145/145 with zero failures, cancellations or skips. Typecheck, SDK exercise, 19-file package dry run and diff checks pass. The actual isolated Pi CLI exposes `subagent` at ordinary startup without an enable call and completes two scouts without activating extra builtins. Real SDK children read/edit/write/run bash in a throwaway Git worktree containing spaces and an apostrophe; root hooks see the actual adapted arguments and retain their real parent cwd. Tests cover root tool/write refusal, failed-cd no-fallback behavior, global writer queue/parent-write exclusion, atomic invalid-cwd admission, search defaults/shorthand refusal, parent-policy retention, task-file cwd, explicitly granted nested workspace inheritance, exact model selection, idle-parent wake once, completed-agent context restoration without fresh generations/sessions, paused resume identity/context retention and stale-control refusal. The first focused attempt used obsolete edit arguments and allowed the controlled parent to replay worker steps; the fixture now uses the current `edits[]` schema and acknowledges parent results. No assertions, runner concurrency or timeouts were weakened.
|
|
32
32
|
|
|
33
33
|
**Unrun:** live-model automatic tool selection and complete multi-task delivery with disposable remote PR/stack creation. This run authorized neither live inference nor a separate smoke repository's external writes. Controlled SDK/CLI results do not prove a model automatically chooses the extension or follows a complete external workflow. No installed-package cutover, active-session reload, retained-run migration or new human visual acceptance was performed. Replacement use requires only one loaded `subagent` provider; Pi's first-registration behavior is not a provider-priority mechanism.
|
|
34
34
|
|
|
@@ -428,3 +428,7 @@ The new redacted ANSI and cursor-separated text captures are retained outside th
|
|
|
428
428
|
Reviewed only this task's changes before final checks. Removed redundant shutdown waits/nested cleanup scaffolding by letting the existing cancellation method own the startup barrier, preserving `finally` disposal. Kept one child, one observable snapshot and one observer subscription; no scheduler, generalized verifier, recovery service or duplicate terminal renderer. Removed an arbitrary finite scripted-response queue in favor of reusing Pi's response factory for each actual request. The final checks cover these changes. For the UI repair, kept rendering stateless (theme recalculated each render), one shared status/activity presentation and native width helpers; no new timers, retained command UI context, scheduler or terminal renderer. The widget subscription is removed before shutdown/disposal so stale children cannot repaint a replacement session.
|
|
429
429
|
|
|
430
430
|
For Slice 0, the scoped pass retained one native color conversion per render and reused the existing row-paint boundary; no color cache, theme classifier, palette configuration or verifier framework was added. Simplified the real-theme regression's per-row comparison before the final full-suite checks.
|
|
431
|
+
|
|
432
|
+
### Same-conversation completed resume correction
|
|
433
|
+
|
|
434
|
+
Real SDK tests in `completed-resume.test.ts` and `compatibility.test.ts` exercise completed idle/parked continuation with original system messages, assistant tool calls and actual tool results, a stable transcript/identity/generation/session count, repeat completions, cumulative exhaustion, immutable earlier artifacts, and per-window structured submissions. Fault-injected unavailable canonical state and revoked policy reject without snapshot/model/event mutation; stop wins pending resume and cancellation drains the writer. Existing deadline-pause/subtree tests continue to exercise retained paused context. Restoration uses the supported `createAgentSession({sessionManager})` API, not direct agent message assignment or result summaries. Parent-lifetime only; no process-restart recovery is claimed.
|
package/package.json
CHANGED
package/src/child-execution.ts
CHANGED
|
@@ -34,6 +34,9 @@ export type ExecutionRecord = {
|
|
|
34
34
|
timer?: ReturnType<typeof setTimeout>;
|
|
35
35
|
run?: Promise<void>;
|
|
36
36
|
session?: AgentSession;
|
|
37
|
+
/** Canonical entry tree survives runtime disposal; never substitute a result summary. */
|
|
38
|
+
sessionManager?: SessionManager;
|
|
39
|
+
resourcePrompt?: string;
|
|
37
40
|
unsubscribe?: () => void;
|
|
38
41
|
paused?: boolean;
|
|
39
42
|
prompt?: (text: string) => Promise<void>;
|
|
@@ -89,11 +92,15 @@ export async function createChildExecution(
|
|
|
89
92
|
? entry.state.id
|
|
90
93
|
: `${entry.state.id}-g${entry.state.generation}`,
|
|
91
94
|
);
|
|
92
|
-
|
|
95
|
+
const restoring = !!entry.sessionManager;
|
|
96
|
+
if (!restoring) await mkdir(taskDir, { mode: 0o700 }); // exclusive initial ownership
|
|
93
97
|
const agentDir = join(taskDir, "agent");
|
|
94
|
-
await mkdir(agentDir, { mode: 0o700 });
|
|
98
|
+
if (!restoring) await mkdir(agentDir, { mode: 0o700 });
|
|
95
99
|
const transcriptDir = join(taskDir, "transcripts");
|
|
96
|
-
await mkdir(transcriptDir, { mode: 0o700 });
|
|
100
|
+
if (!restoring) await mkdir(transcriptDir, { mode: 0o700 });
|
|
101
|
+
// Each completed window publishes exclusively, leaving all earlier artifacts intact.
|
|
102
|
+
const outcomeDir = restoring ? join(taskDir, `window-${entry.state.window}`) : taskDir;
|
|
103
|
+
if (restoring) await mkdir(outcomeDir, { mode: 0o700 });
|
|
97
104
|
const target =
|
|
98
105
|
entry.state.prewalk?.status === "armed"
|
|
99
106
|
? validateModelSelection(input.context, entry.state.prewalk.target)
|
|
@@ -116,14 +123,15 @@ export async function createChildExecution(
|
|
|
116
123
|
target ? [target] : [],
|
|
117
124
|
);
|
|
118
125
|
const resources = childResources(
|
|
119
|
-
|
|
126
|
+
(entry.resourcePrompt ??=
|
|
127
|
+
input.profile.prompt +
|
|
120
128
|
(target
|
|
121
129
|
? "\nPrewalk: investigate and plan using only granted context/tools. After the first completed edit or write the same conversation switches once to the explicitly granted implementation target; all normal budgets still apply."
|
|
122
130
|
: "") +
|
|
123
131
|
`\nYour delegation identity is ${entry.state.id} generation ${entry.state.generation}; parent ${entry.state.parentId}. Coordination, if granted, is limited to family identities explicitly supplied to you.` +
|
|
124
132
|
(entry.validator
|
|
125
133
|
? "\nThis assignment requires a structured result. Call submit_result with the actual value; final prose is not a submission."
|
|
126
|
-
: ""),
|
|
134
|
+
: "")),
|
|
127
135
|
input.skills,
|
|
128
136
|
);
|
|
129
137
|
const loader = new DefaultResourceLoader({
|
|
@@ -316,9 +324,10 @@ export async function createChildExecution(
|
|
|
316
324
|
compaction: { enabled: false },
|
|
317
325
|
retry: { enabled: false },
|
|
318
326
|
}),
|
|
319
|
-
sessionManager: SessionManager.create(selection.cwd, transcriptDir),
|
|
327
|
+
sessionManager: entry.sessionManager ?? SessionManager.create(selection.cwd, transcriptDir),
|
|
320
328
|
});
|
|
321
329
|
entry.session = session;
|
|
330
|
+
entry.sessionManager = session.sessionManager;
|
|
322
331
|
if (session.thinkingLevel !== selection.thinkingLevel)
|
|
323
332
|
throw new Error("SDK did not preserve selected effort");
|
|
324
333
|
if (
|
|
@@ -389,7 +398,7 @@ export async function createChildExecution(
|
|
|
389
398
|
const artifacts: NonNullable<ChildSnapshot["artifacts"]> = {};
|
|
390
399
|
try {
|
|
391
400
|
artifacts.text = await saveOutcomeArtifact(
|
|
392
|
-
|
|
401
|
+
outcomeDir,
|
|
393
402
|
"result.txt",
|
|
394
403
|
last?.content
|
|
395
404
|
.filter((block) => block.type === "text")
|
|
@@ -398,7 +407,7 @@ export async function createChildExecution(
|
|
|
398
407
|
);
|
|
399
408
|
if (entry.state.outcome?.validation === "submitted")
|
|
400
409
|
artifacts.structured = await saveOutcomeArtifact(
|
|
401
|
-
|
|
410
|
+
outcomeDir,
|
|
402
411
|
"structured.json",
|
|
403
412
|
JSON.stringify(entry.state.outcome.value),
|
|
404
413
|
);
|
package/src/coordinator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requireSupportedHost } from "./supported-host.ts";
|
|
2
2
|
import { leafDenial, toolProvenance, type CapabilityDenial } from "./capabilities.ts";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
4
5
|
import {
|
|
5
6
|
SessionManager,
|
|
6
7
|
type AgentSession,
|
|
@@ -110,12 +111,14 @@ export class DelegationCoordinator {
|
|
|
110
111
|
presence: this.presence.get(state.id),
|
|
111
112
|
}));
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
/** Full parent-lifetime session roster, including retained historical generations. */
|
|
115
|
+
sessions(): ChildSnapshot[] {
|
|
114
116
|
return [...this.history.values()]
|
|
115
|
-
.map((record) => record.state)
|
|
116
|
-
.concat(this.list())
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
.map((record) => structuredClone(record.state))
|
|
118
|
+
.concat(this.list());
|
|
119
|
+
}
|
|
120
|
+
generations(id: string): ChildSnapshot[] {
|
|
121
|
+
return this.sessions().filter((state) => state.id === id);
|
|
119
122
|
}
|
|
120
123
|
private listeners = new Set<(event: RegistryEvent) => void>();
|
|
121
124
|
private parentMutations = new Set<string>();
|
|
@@ -224,6 +227,15 @@ export class DelegationCoordinator {
|
|
|
224
227
|
throw new Error("Prewalk target requires an exact model grant");
|
|
225
228
|
validateModelSelection(selection, input.profile.prewalk);
|
|
226
229
|
}
|
|
230
|
+
this.validateAdmission(input, effective, input.model);
|
|
231
|
+
return selection;
|
|
232
|
+
}
|
|
233
|
+
/** Shared writer/lineage checks; resume supplies its retained selection, not admission defaults. */
|
|
234
|
+
private validateAdmission(
|
|
235
|
+
input: StartTask,
|
|
236
|
+
effective: ModelSelection,
|
|
237
|
+
explicit?: ModelSelection,
|
|
238
|
+
): void {
|
|
227
239
|
const writer = this.writer ? this.entries.get(this.writer) : undefined;
|
|
228
240
|
if (
|
|
229
241
|
isWriter(input.profile) &&
|
|
@@ -234,7 +246,7 @@ export class DelegationCoordinator {
|
|
|
234
246
|
const parent = this.entry(input.parent);
|
|
235
247
|
if (parent.cancelled || parent.paused || parent.state.disposed)
|
|
236
248
|
throw new Error("Delegating parent is no longer active");
|
|
237
|
-
this.assertDescendantModels(parent, input.profile, effective,
|
|
249
|
+
this.assertDescendantModels(parent, input.profile, effective, explicit);
|
|
238
250
|
if (!parent.input.profile.spawn?.includes(input.profile.name))
|
|
239
251
|
throw new Error("Ancestor spawn role denied");
|
|
240
252
|
if (parent.state.depth >= Math.min(this.policy.depth, parent.state.limits.depth))
|
|
@@ -248,7 +260,6 @@ export class DelegationCoordinator {
|
|
|
248
260
|
ancestor = this.entries.get(ancestor.state.parentId);
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
|
-
return selection;
|
|
252
263
|
}
|
|
253
264
|
private assertDescendantModels(
|
|
254
265
|
parent: Entry,
|
|
@@ -721,15 +732,56 @@ export class DelegationCoordinator {
|
|
|
721
732
|
async resume(ref: ChildRef, text: string): Promise<ChildRef> {
|
|
722
733
|
requireSupportedHost();
|
|
723
734
|
const entry = this.entry(ref);
|
|
724
|
-
|
|
725
|
-
|
|
735
|
+
const completed = entry.state.status === "settled" && entry.state.disposed;
|
|
736
|
+
const eligible = entry.state.status === "paused" || completed;
|
|
737
|
+
if (
|
|
738
|
+
entry.resuming ||
|
|
739
|
+
!eligible ||
|
|
740
|
+
entry.cancelled ||
|
|
741
|
+
this.closing ||
|
|
742
|
+
this.presence.get(ref.id) === "stopped"
|
|
743
|
+
)
|
|
744
|
+
throw new Error(
|
|
745
|
+
"Resume requires a drained paused or completed conversation; stopped/failed identities and duplicate resume rejected",
|
|
746
|
+
);
|
|
747
|
+
if ((entry.state.window ?? 1) >= MAX_GENERATIONS)
|
|
748
|
+
throw new Error("Resume window limit (64) reached");
|
|
749
|
+
if (
|
|
750
|
+
completed &&
|
|
751
|
+
(!entry.sessionManager ||
|
|
752
|
+
!entry.finalizedMessages?.length ||
|
|
753
|
+
JSON.stringify(entry.sessionManager.buildSessionContext().messages) !==
|
|
754
|
+
JSON.stringify(entry.finalizedMessages))
|
|
755
|
+
)
|
|
756
|
+
throw new Error("Resume unavailable: canonical conversation state was not retained");
|
|
757
|
+
if (completed) {
|
|
758
|
+
// SessionManager.open() can silently initialize missing/empty files. Never use that
|
|
759
|
+
// fallback here: the retained tree AND its original persisted transcript must agree.
|
|
760
|
+
try {
|
|
761
|
+
const persisted = readFileSync(entry.state.transcript!, "utf8")
|
|
762
|
+
.trim()
|
|
763
|
+
.split("\n")
|
|
764
|
+
.map((line) => JSON.parse(line));
|
|
765
|
+
const manager = entry.sessionManager!;
|
|
766
|
+
if (
|
|
767
|
+
JSON.stringify(persisted) !==
|
|
768
|
+
JSON.stringify([manager.getHeader(), ...manager.getEntries()])
|
|
769
|
+
)
|
|
770
|
+
throw new Error("transcript differs from retained canonical entries");
|
|
771
|
+
} catch (error) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`Resume unavailable: original transcript cannot be restored: ${String(error)}`,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
} else if (entry.prompted && !entry.session) {
|
|
777
|
+
throw new Error("Resume unavailable: paused SDK conversation was not retained");
|
|
778
|
+
}
|
|
726
779
|
if (!text.trim()) throw new Error("Nonempty continuation text is required");
|
|
727
780
|
entry.resuming = true;
|
|
728
781
|
try {
|
|
729
782
|
const parent = this.renewalParent(ref);
|
|
730
783
|
const input = entry.input;
|
|
731
784
|
const policy = await resolveScheduling(input.context.cwd, input.context.isProjectTrusted());
|
|
732
|
-
this.configure(policy);
|
|
733
785
|
const limits = { ...entry.state.limits };
|
|
734
786
|
for (const key of Object.keys(limits) as (keyof ExecutionLimits)[])
|
|
735
787
|
limits[key] = Math.min(
|
|
@@ -810,21 +862,52 @@ export class DelegationCoordinator {
|
|
|
810
862
|
throw new Error("Resume denied: delegation depth limit");
|
|
811
863
|
// Reuse the original admission's writer and nested no-lending checks, without
|
|
812
864
|
// admitting another task or charging another spawn.
|
|
813
|
-
this.
|
|
865
|
+
this.validateAdmission(
|
|
866
|
+
input,
|
|
867
|
+
selected,
|
|
868
|
+
entry.state.prewalk?.status === "armed" ? undefined : selected,
|
|
869
|
+
);
|
|
814
870
|
this.renewalParent(ref);
|
|
815
871
|
const available =
|
|
816
872
|
this.active().length < policy.concurrency &&
|
|
817
873
|
(!isWriter(input.profile) || (!this.writer && !this.parentMutations.size));
|
|
818
874
|
if (parent && (this.queue.size || !available))
|
|
819
875
|
throw new Error("Nested capacity unavailable; no permit lending");
|
|
820
|
-
this.
|
|
876
|
+
const previousPolicy = this.policy;
|
|
877
|
+
this.policy = policy;
|
|
878
|
+
try {
|
|
879
|
+
this.assertQueueCapacity([input]);
|
|
880
|
+
} finally {
|
|
881
|
+
this.policy = previousPolicy;
|
|
882
|
+
}
|
|
821
883
|
if (
|
|
822
884
|
entry.cancelled ||
|
|
823
885
|
this.closing ||
|
|
824
|
-
|
|
825
|
-
entry.state.status !== "paused"
|
|
886
|
+
this.presence.get(ref.id) === "stopped" ||
|
|
887
|
+
(completed ? entry.state.status !== "settled" : entry.state.status !== "paused")
|
|
826
888
|
)
|
|
827
889
|
throw new Error("Resume cancelled by stop or shutdown");
|
|
890
|
+
if (completed) {
|
|
891
|
+
const { window, status, result, outcome, artifacts, error, endedAt } = entry.state;
|
|
892
|
+
entry.state = {
|
|
893
|
+
...entry.state,
|
|
894
|
+
completedWindows: [
|
|
895
|
+
...(entry.state.completedWindows ?? []),
|
|
896
|
+
structuredClone({ window, status, result, outcome, artifacts, error, endedAt }),
|
|
897
|
+
],
|
|
898
|
+
disposed: false,
|
|
899
|
+
endedAt: null,
|
|
900
|
+
outcome: entry.validator
|
|
901
|
+
? { validation: "awaiting", attempts: 0, errors: [], finalized: false }
|
|
902
|
+
: undefined,
|
|
903
|
+
artifacts: undefined,
|
|
904
|
+
result: "",
|
|
905
|
+
progress: "",
|
|
906
|
+
};
|
|
907
|
+
entry.selection.model = validateModelSelection(input.context, selected);
|
|
908
|
+
entry.selection.thinkingLevel = selected.effort;
|
|
909
|
+
}
|
|
910
|
+
this.policy = policy;
|
|
828
911
|
entry.done = new Promise<void>((resolve) => {
|
|
829
912
|
entry.finish = resolve;
|
|
830
913
|
});
|
|
@@ -989,24 +1072,27 @@ export class DelegationCoordinator {
|
|
|
989
1072
|
dispose: () => this.dispose(entry),
|
|
990
1073
|
});
|
|
991
1074
|
}
|
|
992
|
-
private closeDescendantPresence(entry: Entry): void {
|
|
1075
|
+
private closeDescendantPresence(entry: Entry, completed = false): void {
|
|
993
1076
|
for (const child of this.entries.values())
|
|
994
1077
|
if (
|
|
995
1078
|
child.state.parentId === entry.state.id &&
|
|
996
1079
|
child.state.parentGeneration === entry.state.generation
|
|
997
1080
|
) {
|
|
998
|
-
|
|
999
|
-
|
|
1081
|
+
// Ancestor completion closes live/paused descendants, but does not retire a
|
|
1082
|
+
// successful retained conversation. Explicit stop remains sticky.
|
|
1083
|
+
if (!(completed && child.state.disposed && child.state.status === "settled"))
|
|
1084
|
+
this.presence.set(child.state.id, "stopped");
|
|
1085
|
+
this.closeDescendantPresence(child, completed);
|
|
1000
1086
|
}
|
|
1001
1087
|
}
|
|
1002
1088
|
private dispose(entry: Entry): void {
|
|
1003
1089
|
if (entry.state.disposed) return;
|
|
1004
1090
|
this.queue.delete(entry);
|
|
1005
1091
|
clearTimeout(entry.timer);
|
|
1006
|
-
entry.finalizedMessages = structuredClone(entry.session
|
|
1092
|
+
if (entry.session) entry.finalizedMessages = structuredClone(entry.session.messages);
|
|
1007
1093
|
entry.unsubscribe?.();
|
|
1008
1094
|
entry.session?.dispose();
|
|
1009
|
-
// Retain immutable evidence, not the
|
|
1095
|
+
// Retain the authoritative SessionManager and immutable evidence, not the SDK runtime graph.
|
|
1010
1096
|
entry.unsubscribe = undefined;
|
|
1011
1097
|
entry.session = undefined;
|
|
1012
1098
|
entry.prompt = undefined;
|
|
@@ -1015,11 +1101,14 @@ export class DelegationCoordinator {
|
|
|
1015
1101
|
entry.run = undefined;
|
|
1016
1102
|
entry.pauseDrain = undefined;
|
|
1017
1103
|
if (this.writer === entry.state.id) this.writer = undefined;
|
|
1018
|
-
this.closeDescendantPresence(
|
|
1104
|
+
this.closeDescendantPresence(
|
|
1105
|
+
entry,
|
|
1106
|
+
entry.state.status === "settled" && !entry.cancelled && !this.closing,
|
|
1107
|
+
);
|
|
1019
1108
|
this.presence.set(entry.state.id, entry.cancelled || this.closing ? "stopped" : "idle");
|
|
1020
1109
|
this.update(entry, {
|
|
1021
1110
|
disposed: true,
|
|
1022
|
-
retainedContext:
|
|
1111
|
+
retainedContext: !!entry.sessionManager && entry.state.status === "settled",
|
|
1023
1112
|
endedAt: Date.now(),
|
|
1024
1113
|
writerLease: false,
|
|
1025
1114
|
outcome: entry.state.outcome ? { ...entry.state.outcome, finalized: true } : undefined,
|
package/src/delegation-types.ts
CHANGED
|
@@ -42,6 +42,13 @@ export type ChildSnapshot = Readonly<
|
|
|
42
42
|
pauseReason?: string;
|
|
43
43
|
retainedContext?: boolean;
|
|
44
44
|
window?: number;
|
|
45
|
+
/** Immutable completion evidence from earlier windows of this same conversation. */
|
|
46
|
+
completedWindows?: ReadonlyArray<
|
|
47
|
+
Pick<
|
|
48
|
+
ChildSnapshot,
|
|
49
|
+
"window" | "status" | "result" | "outcome" | "artifacts" | "error" | "endedAt"
|
|
50
|
+
>
|
|
51
|
+
>;
|
|
45
52
|
status:
|
|
46
53
|
| "queued"
|
|
47
54
|
| "setup"
|
package/src/hub.ts
CHANGED
|
@@ -85,8 +85,7 @@ export function createHub(
|
|
|
85
85
|
const roster = () =>
|
|
86
86
|
taskTree(
|
|
87
87
|
coordinator
|
|
88
|
-
.
|
|
89
|
-
.flatMap((child) => coordinator.generations(child.id))
|
|
88
|
+
.sessions()
|
|
90
89
|
.filter((child) =>
|
|
91
90
|
lineText(
|
|
92
91
|
[
|
|
@@ -208,7 +207,7 @@ export function createHub(
|
|
|
208
207
|
paint(
|
|
209
208
|
interactionTarget
|
|
210
209
|
? "Resize · Esc cancels"
|
|
211
|
-
: "Subagent Hub " + coordinator.
|
|
210
|
+
: "Subagent Hub " + coordinator.sessions().length,
|
|
212
211
|
),
|
|
213
212
|
];
|
|
214
213
|
const inner = Math.max(0, width - 4);
|
|
@@ -107,7 +107,7 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
107
107
|
(tui, theme) => {
|
|
108
108
|
renderWidget = () => tui.requestRender();
|
|
109
109
|
return createChildWidget(
|
|
110
|
-
() => coordinator.
|
|
110
|
+
() => coordinator.sessions(),
|
|
111
111
|
theme,
|
|
112
112
|
() => tui.terminal.rows,
|
|
113
113
|
);
|
|
@@ -373,7 +373,7 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
373
373
|
label: "Coordinate delegation",
|
|
374
374
|
exposure: "model-only",
|
|
375
375
|
description:
|
|
376
|
-
"Live-parent identity-pinned coordination. message targets a running family agent (omit target for root inbox, direct children only); inbox reads own bounded mailbox; wait targets an outcome or own inbox with required 1–60000ms timeout, retaining capacity. No transcript access. Child actions require explicit operator grants. Root additionally has status/stop/park, paused resume (assignment is the appended continuation), and idle followup/parked revive. Resume retains identity and full context, resets only the deadline window, and requires explicit child resumes after parent resume. Followup/revive create a fresh generation/session with current model/auth and narrowed current grants, no old tool replay or historical recovery. Message accepted/queued is NOT model acknowledgment; delivered means transcript/inbox visibility only. 4096-byte messages, 64 mailbox/waiters, 512 messages and 64 generations per live parent.",
|
|
376
|
+
"Live-parent identity-pinned coordination. message targets a running family agent (omit target for root inbox, direct children only); inbox reads own bounded mailbox; wait targets an outcome or own inbox with required 1–60000ms timeout, retaining capacity. No transcript access. Child actions require explicit operator grants. Root additionally has status/stop/park, paused/completed resume (assignment is the appended continuation), and idle followup/parked revive. Resume retains identity and full context, resets only the deadline window, and requires explicit child resumes after parent resume. Followup/revive create a fresh generation/session with current model/auth and narrowed current grants, no old tool replay or historical recovery. Message accepted/queued is NOT model acknowledgment; delivered means transcript/inbox visibility only. 4096-byte messages, 64 mailbox/waiters, 512 messages and 64 generations per live parent.",
|
|
377
377
|
parameters: coordinationParameters,
|
|
378
378
|
async execute(callerId, args, signal, _update, ctx) {
|
|
379
379
|
if (["resume", "followup", "revive"].includes(args.action)) requireSupportedHost();
|
|
@@ -493,10 +493,10 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
493
493
|
promptGuidelines: [
|
|
494
494
|
"Use subagent for native worker delegation requested by the user or an active workflow; no extension-specific user instruction or enable call is needed. Use agent worker for implementation, scout for research, review for review, or general for restricted inherited capabilities.",
|
|
495
495
|
"Pass subagent the approved task handoff and existing workspace cwd. Preserve selected model routing; no model override is needed to inherit the parent. Pass accessible skill/reference paths in the task; general inherits available skills and callable builtins, plus operator-reviewed leaf tools only; parent instructions/history and project trust are not inherited.",
|
|
496
|
-
"subagent launches are async by default and wake this session after actual drain. Continue independent work or yield; do not poll or sleep. Use the exact returned id (UUID@generation) for status, steer, stop or resume. Resume continues a paused conversation with its full context and cumulative budgets;
|
|
496
|
+
"subagent launches are async by default and wake this session after actual drain. Continue independent work or yield; do not poll or sleep. Use the exact returned id (UUID@generation) for status, steer, stop or resume. Resume continues a drained paused or completed conversation with its same identity/generation, full SDK context and cumulative budgets; unavailable context and stopped/failed identities reject. Use explicit delegate_coordinate followup/revive for fresh completed work.",
|
|
497
497
|
],
|
|
498
498
|
description:
|
|
499
|
-
"Native delegation for orchestration and coding task owners. Launch {agent,task,cwd?,model?,async?} or an ordered {tasks:[...],async?,cwd?} batch. worker maps explicitly to the implementation profile; unknown/custom agent names reject. Model strings require exact provider/id[:effort] and operator grants. Actions: agents, list, status/stop with exact returned id, steer/resume with id and message. Uses the same registry, parent permissions, limits and global writer lease as delegate_task. Existing workspaces only: file paths and shell cwd are explicitly adapted through root builtin hooks. No automatic worktree creation/commits/apply/deletion, detached survival, script workflows or blanket competing API parity. Do not load another provider of the subagent tool alongside this extension.",
|
|
499
|
+
"Native delegation for orchestration and coding task owners. Launch {agent,task,cwd?,model?,async?} or an ordered {tasks:[...],async?,cwd?} batch. worker maps explicitly to the implementation profile; unknown/custom agent names reject. Model strings require exact provider/id[:effort] and operator grants. Actions: agents, list, status/stop with exact returned id, steer/resume with id and message. Resume requires a drained paused or completed session and restores its full conversation with the same identity/generation and session count; unavailable context and stopped/failed identities reject. Uses the same registry, parent permissions, limits and global writer lease as delegate_task. Existing workspaces only: file paths and shell cwd are explicitly adapted through root builtin hooks. No automatic worktree creation/commits/apply/deletion, detached survival, script workflows or blanket competing API parity. Do not load another provider of the subagent tool alongside this extension.",
|
|
500
500
|
parameters: compatibleParameters,
|
|
501
501
|
async execute(callerId, args, signal, onUpdate, ctx) {
|
|
502
502
|
if (!("action" in args) || args.action === "resume" || args.action === "steer")
|
|
@@ -564,20 +564,10 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
564
564
|
if (args.action === "stop") await owner.stop(target);
|
|
565
565
|
else if (args.action === "steer") await owner.steer(target, args.message);
|
|
566
566
|
else if ("message" in args) {
|
|
567
|
-
const presence = owner
|
|
568
|
-
.agents()
|
|
569
|
-
.find(
|
|
570
|
-
(state) => state.id === target.id && state.generation === target.generation,
|
|
571
|
-
)?.presence;
|
|
572
567
|
const response = await coordinationTool(undefined, undefined, true).execute(
|
|
573
568
|
callerId,
|
|
574
569
|
{
|
|
575
|
-
action:
|
|
576
|
-
owner.snapshot(target)?.status === "paused"
|
|
577
|
-
? "resume"
|
|
578
|
-
: presence === "parked"
|
|
579
|
-
? "revive"
|
|
580
|
-
: "followup",
|
|
570
|
+
action: "resume",
|
|
581
571
|
target,
|
|
582
572
|
assignment: args.message,
|
|
583
573
|
},
|
|
@@ -586,10 +576,6 @@ export default function delegation(pi: ExtensionAPI) {
|
|
|
586
576
|
ctx,
|
|
587
577
|
);
|
|
588
578
|
const next = response.details as ChildSnapshot;
|
|
589
|
-
if (next.generation !== target.generation)
|
|
590
|
-
void owner.wait(next).then((state) => {
|
|
591
|
-
if (!closing && coordinator === owner) show(compatible(state), state.transcript, true);
|
|
592
|
-
});
|
|
593
579
|
return result(compatible(next));
|
|
594
580
|
}
|
|
595
581
|
return result(compatible(owner.snapshot(target)!));
|
package/src/subagent-widget.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
taskTree,
|
|
16
16
|
} from "./task-presentation.ts";
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/** Full session totals with an event-driven, bounded active-agent tree; details remain in the Hub. */
|
|
19
19
|
export function createChildWidget(
|
|
20
20
|
snapshot: () => ChildSnapshot[],
|
|
21
21
|
theme: Pick<Theme, "fg"> & Partial<Pick<Theme, "bold">>,
|