@howarewoo/pi-subagents 0.0.0-stage → 0.2.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.
@@ -0,0 +1,673 @@
1
+ import { requireSupportedHost } from "./supported-host.ts";
2
+ import { createProfileViewer, resolvedProfiles, profileToolPolicy } from "./profile-viewer.ts";
3
+ import { Text, truncateToWidth } from "@earendil-works/pi-tui";
4
+ import { EFFORTS, sameSelection } from "./model-policy.ts";
5
+ import { clippedText } from "./outcomes.ts";
6
+ import { readTaskInput } from "./task-input.ts";
7
+ import { resolveWorkspace } from "./workspace.ts";
8
+ import { join } from "node:path";
9
+ import { parameters, coordinationParameters, compatibleParameters } from "./tool-schemas.ts";
10
+ import {
11
+ truncateHead,
12
+ type ExtensionToolContext,
13
+ type ToolDefinition,
14
+ type ToolRenderers,
15
+ type ExtensionAPI,
16
+ type BuildSystemPromptOptions,
17
+ } from "@earendil-works/pi-coding-agent";
18
+ import { resolveScheduling } from "./scheduling.ts";
19
+ import {
20
+ DelegationCoordinator,
21
+ type ChildRef,
22
+ type ChildSnapshot,
23
+ type StartTask,
24
+ } from "./coordinator.ts";
25
+ import { READ_TOOLS, resolveProfile, bundledProfiles, intersectProfileGrants } from "./profiles.ts";
26
+ import { createHub } from "./hub.ts";
27
+ import { createChildWidget } from "./subagent-widget.ts";
28
+ import { taskCard } from "./task-presentation.ts";
29
+ import { safe } from "./thread.ts";
30
+
31
+ export default function delegation(pi: ExtensionAPI) {
32
+ let coordinator: DelegationCoordinator;
33
+ let closing = false;
34
+ let parentPromptOptions: BuildSystemPromptOptions | undefined;
35
+ const availableSkills = () => parentPromptOptions?.skills ?? [];
36
+ pi.on("before_agent_start", (event) => {
37
+ parentPromptOptions = event.systemPromptOptions;
38
+ });
39
+ let renderWidget: (() => void) | undefined;
40
+ let unsubscribeWidget: (() => void) | undefined;
41
+ const ref = (args: string): ChildRef => {
42
+ const [id, generation] = args.trim().split(/\s+/);
43
+ if (!id || !Number.isSafeInteger(Number(generation)))
44
+ throw new Error("Expected <task-id> <generation>");
45
+ return { id, generation: Number(generation) };
46
+ };
47
+ const preview = (value: unknown, transcript?: string) => {
48
+ const snapshots = (Array.isArray(value) ? value : [value]).filter(
49
+ (item): item is ChildSnapshot =>
50
+ !!item &&
51
+ typeof item === "object" &&
52
+ "id" in item &&
53
+ "generation" in item &&
54
+ "status" in item,
55
+ );
56
+ const references = snapshots.map((state) => ({
57
+ id: state.id,
58
+ generation: state.generation,
59
+ status: state.status,
60
+ transcript: state.transcript,
61
+ artifacts: state.artifacts,
62
+ }));
63
+ const text = JSON.stringify(
64
+ value,
65
+ (key, item) => {
66
+ if (
67
+ ["result", "assignment", "activity", "progress", "error"].includes(key) &&
68
+ typeof item === "string"
69
+ )
70
+ return clippedText(item);
71
+ if (key === "value") return clippedText(JSON.stringify(item));
72
+ return item;
73
+ },
74
+ 2,
75
+ );
76
+ const clipped = truncateHead(
77
+ (references.length
78
+ ? `References: ${JSON.stringify(references)}\n`
79
+ : transcript
80
+ ? `Transcript: ${transcript}\n`
81
+ : "") + text,
82
+ );
83
+ return `${clipped.content}${clipped.truncated ? "\n[Preview truncated; inspect full artifact/transcript references or subagent-status for one identity]" : ""}`;
84
+ };
85
+ const compatible = (state: ChildSnapshot) => ({
86
+ ...state,
87
+ taskId: state.id,
88
+ id: `${state.id}@${state.generation}`,
89
+ });
90
+ const show = (value: unknown, transcript?: string, wake = false) =>
91
+ pi.sendMessage(
92
+ { customType: "delegation", content: preview(value, transcript), display: true },
93
+ wake ? { triggerTurn: true, deliverAs: "followUp" } : undefined,
94
+ );
95
+ pi.on("session_start", (_event, ctx) => {
96
+ coordinator = new DelegationCoordinator(
97
+ join(ctx.sessionManager.getSessionDir(), "delegation", ctx.sessionManager.getSessionId()),
98
+ ctx.sessionManager.getSessionId(),
99
+ );
100
+ unsubscribeWidget = coordinator.subscribe((event) => {
101
+ renderWidget?.();
102
+ pi.events.emit("delegation:registry", event);
103
+ });
104
+ if (ctx.mode !== "tui") return;
105
+ ctx.ui.setWidget(
106
+ "delegation",
107
+ (tui, theme) => {
108
+ renderWidget = () => tui.requestRender();
109
+ return createChildWidget(
110
+ () => coordinator.list(),
111
+ theme,
112
+ () => tui.terminal.rows,
113
+ );
114
+ },
115
+ { placement: "aboveEditor" },
116
+ );
117
+ });
118
+ pi.on("session_shutdown", async (_event, ctx) => {
119
+ closing = true;
120
+ renderWidget = undefined;
121
+ if (ctx.mode === "tui") ctx.ui.setWidget("delegation", undefined);
122
+ await coordinator?.shutdown();
123
+ unsubscribeWidget?.();
124
+ });
125
+ pi.on("tool_call", (event) => {
126
+ try {
127
+ const builtinRead =
128
+ READ_TOOLS.includes(event.toolName as (typeof READ_TOOLS)[number]) &&
129
+ pi
130
+ .getAllTools()
131
+ .some(
132
+ (tool) =>
133
+ tool.name === event.toolName && tool.sourceInfo.path === `builtin:${event.toolName}`,
134
+ );
135
+ const tools = pi.getAllTools();
136
+ const ownSource = tools.find((tool) => tool.name === "delegate_task")?.sourceInfo.path;
137
+ const ownCompatibilityCall =
138
+ event.toolName === "subagent" &&
139
+ ownSource !== undefined &&
140
+ tools.some((tool) => tool.name === "subagent" && tool.sourceInfo.path === ownSource);
141
+ coordinator.parentToolStart(
142
+ event.toolCallId,
143
+ event.toolName,
144
+ builtinRead || ownCompatibilityCall,
145
+ );
146
+ } catch (error) {
147
+ return { block: true, reason: String(error) };
148
+ }
149
+ });
150
+ pi.on("tool_execution_end", (event) => coordinator.parentToolEnd(event.toolCallId));
151
+ const assertTools = (names: readonly string[]) => {
152
+ for (const name of names) {
153
+ const tool = pi.getAllTools().find((tool) => tool.name === name);
154
+ if (!tool || tool.sourceInfo.path !== `builtin:${name}`)
155
+ throw new Error(`Delegation requires an unmodified built-in parent tool: ${name}`);
156
+ }
157
+ };
158
+ const codemodeEnabled = () =>
159
+ pi.getActiveTools().includes("codemode") &&
160
+ pi
161
+ .getAllTools()
162
+ .some(
163
+ (tool) => tool.name === "codemode" && tool.exposure === "model-only" && !!tool.reachesTools,
164
+ );
165
+ const taskTool = (
166
+ parent?: ChildRef,
167
+ rootContext?: ExtensionToolContext,
168
+ rootCallerId?: string,
169
+ compatibility = false,
170
+ ): ToolDefinition<typeof parameters> => ({
171
+ name: "delegate_task",
172
+ label: "Delegate task",
173
+ exposure: "model-only",
174
+ promptSnippet: "Delegate coding, review or research to native subagents in supplied workspaces",
175
+ promptGuidelines: [
176
+ "Use delegate_task for authorized native delegation with explicit profiles. When subagent is available, prefer subagent for orchestration task owners and its async completion wake-ups. Supply the existing task workspace cwd; do not substitute inline implementation when a workflow requires a worker.",
177
+ ],
178
+ description:
179
+ "Delegate one assignment or a fully preflighted ordered tasks batch on the selected parent model by default. Optional exact model {provider,id,effort} requires an operator profile models grant and stays exact, suppressing inherited role prewalk; models may only narrow that grant list. No aliases/fallback/coercion. Configured role-default prewalk switches once after first edit/write, not a repository scan. Coding workers are supported through implementation; advisor/probes/speculation/history/executable resource inheritance remain unavailable. Optional cwd selects an existing workspace: root hooks see explicitly resolved builtin paths and a cwd-prefixed shell command; this is not sandboxing or transparent hook-cwd rebinding. Role tools are ceilings intersected with callable parent builtins; inactive tools stay inactive. Snapshots report effective tools. Bounded FIFO capacity; effective implementation writers serialize. Manual !/!! and external edits are uncoordinated: never conflict with child writes. Foreground waits; background delivers one final batch message. Nested foreground only, explicit spawn grants and available capacity required. Limits may only narrow operator policy. Supply exactly one inline assignment or taskFile (explicit non-hidden workspace-relative UTF-8 file, at most 50000 bytes, checked through parent read hooks). Optional resultSchema requires child submit_result; prose is never validated output. Previews are bounded; full result artifacts and transcripts retained after drain.",
180
+ parameters,
181
+ renderCall(args, theme) {
182
+ return {
183
+ render: (width) =>
184
+ [
185
+ theme.fg(
186
+ "toolTitle",
187
+ `Delegate · ${"tasks" in args ? `batch ${args.tasks?.length ?? 0}` : safe(args.profile ?? "profile pending")} · ${args.background ? "background" : "foreground"}`,
188
+ ),
189
+ safe(
190
+ "tasks" in args
191
+ ? "Ordered batch · expand results for each identity"
192
+ : (args.assignment ?? args.taskFile ?? "Receiving assignment…"),
193
+ ).replaceAll("\n", " "),
194
+ ].map((line) => truncateToWidth(line, Math.max(0, width))),
195
+ invalidate() {},
196
+ };
197
+ },
198
+ renderResult(result, options, theme, context) {
199
+ const stored = result.details as ChildSnapshot | { children: ChildSnapshot[] } | undefined;
200
+ const children =
201
+ stored && "children" in stored ? stored.children : stored && "id" in stored ? [stored] : [];
202
+ return {
203
+ render(width) {
204
+ const lines = children.length
205
+ ? children.flatMap((snapshot) => {
206
+ let child = snapshot;
207
+ try {
208
+ child = coordinator?.snapshot(snapshot) ?? snapshot;
209
+ } catch {
210
+ /* delivered snapshots survive runtime replacement */
211
+ }
212
+ return taskCard(child, options.expanded);
213
+ })
214
+ : [
215
+ safe(
216
+ result.content
217
+ .filter((block) => block.type === "text")
218
+ .map((block) => block.text)
219
+ .join("\n"),
220
+ ),
221
+ ];
222
+ if (children.length && !options.expanded)
223
+ lines.push(context.keyHint("app.tools.expand", "to expand"));
224
+ return new Text(lines.map((line) => theme.fg("text", line)).join("\n"), 0, 0).render(
225
+ width,
226
+ );
227
+ },
228
+ invalidate() {},
229
+ };
230
+ },
231
+ async execute(callerId, args, signal, onUpdate, localContext) {
232
+ requireSupportedHost();
233
+ const ctx = rootContext ?? localContext;
234
+ const owner = coordinator;
235
+ if (parent && args.background)
236
+ throw new Error("Nested background delegation is unsupported; use foreground");
237
+ const policy = await resolveScheduling(ctx.cwd, ctx.isProjectTrusted());
238
+ owner.configure(policy);
239
+ const items = "tasks" in args ? args.tasks : [args];
240
+ const inputs: StartTask[] = [];
241
+ for (const task of items) {
242
+ if ((task.assignment === undefined) === (task.taskFile === undefined))
243
+ throw new Error("Supply exactly one of assignment or taskFile");
244
+ const cwd = resolveWorkspace(
245
+ task.cwd ?? (parent ? owner.snapshot(parent)?.cwd : undefined),
246
+ ctx.cwd,
247
+ );
248
+ let { profile, skills } = await resolveProfile(
249
+ task.profile,
250
+ ctx.cwd,
251
+ ctx.isProjectTrusted(),
252
+ undefined,
253
+ availableSkills(),
254
+ );
255
+ if (task.spawn) {
256
+ if (task.spawn.some((role) => !profile.spawn?.includes(role)))
257
+ throw new Error("Request cannot mint spawn grants");
258
+ profile = { ...profile, spawn: task.spawn };
259
+ }
260
+ if (task.coordination) {
261
+ if (task.coordination.some((action) => !profile.coordination?.includes(action)))
262
+ throw new Error("Request cannot mint coordination grants");
263
+ profile = { ...profile, coordination: task.coordination };
264
+ }
265
+ if (task.models) {
266
+ if (
267
+ task.models.some(
268
+ (selection) => !profile.models?.some((grant) => sameSelection(grant, selection)),
269
+ )
270
+ )
271
+ throw new Error("Request cannot mint model grants");
272
+ profile = { ...profile, models: task.models };
273
+ }
274
+ signal?.throwIfAborted();
275
+ let text = task.assignment;
276
+ if (task.taskFile !== undefined) {
277
+ assertTools(["read"]);
278
+ text = await readTaskInput(
279
+ task.taskFile,
280
+ ctx,
281
+ signal,
282
+ parent
283
+ ? async () => {
284
+ await owner.authorizeSpawn(parent, profile, task.model, {
285
+ context: ctx,
286
+ toolMetadata: () => pi.getAllTools(),
287
+ cwd,
288
+ });
289
+ await owner.taskInputRead(parent, signal);
290
+ }
291
+ : undefined,
292
+ cwd,
293
+ );
294
+ }
295
+ inputs.push({
296
+ cwd,
297
+ model: task.model,
298
+ assignment: text!,
299
+ resultSchema: task.resultSchema,
300
+ profile,
301
+ skills,
302
+ context: ctx,
303
+ assertTools,
304
+ toolMetadata: () => pi.getAllTools(),
305
+ codemodeEnabled,
306
+ availableSkills,
307
+ callerId: rootCallerId ?? callerId,
308
+ parent,
309
+ limits: task.limits,
310
+ coordinationTools: (actor) => [coordinationTool(owner, actor)],
311
+ nestedTool: (ref: ChildRef) => taskTool(ref, ctx, rootCallerId ?? callerId),
312
+ });
313
+ }
314
+ signal?.throwIfAborted();
315
+ const children = await owner.admit(inputs, undefined, signal);
316
+ const stop = () =>
317
+ parent && ["pausing", "paused"].includes(owner.snapshot(parent)!.status)
318
+ ? Promise.resolve()
319
+ : owner.stopMany(children);
320
+ const abort = () => {
321
+ void stop();
322
+ };
323
+ signal?.addEventListener("abort", abort, { once: true });
324
+ if (signal?.aborted) await stop();
325
+ const states = () => children.map((child) => owner.snapshot(child)!);
326
+ const result = (snapshots: ChildSnapshot[]) => {
327
+ if (compatibility) snapshots = snapshots.map(compatible);
328
+ const value = "tasks" in args ? snapshots : snapshots[0];
329
+ return {
330
+ content: [
331
+ {
332
+ type: "text" as const,
333
+ text: preview(value, snapshots.length === 1 ? snapshots[0].transcript : undefined),
334
+ },
335
+ ],
336
+ details: "tasks" in args ? { children: snapshots } : snapshots[0],
337
+ isError: snapshots.some(
338
+ (state) => state.status === "failed" || state.status === "cancelled",
339
+ ),
340
+ };
341
+ };
342
+ const completed = Promise.all(children.map((child) => owner.wait(child)));
343
+ if (args.background) {
344
+ signal?.removeEventListener("abort", abort);
345
+ void completed.then((snapshots) => {
346
+ if (compatibility) snapshots = snapshots.map(compatible);
347
+ if (!closing && coordinator === owner)
348
+ show(
349
+ "tasks" in args ? snapshots : snapshots[0],
350
+ snapshots.length === 1 ? snapshots[0].transcript : undefined,
351
+ compatibility,
352
+ );
353
+ });
354
+ return result(states());
355
+ }
356
+ const unsubscribe = owner.subscribe((event) => {
357
+ if (children.some((child) => child.id === event.child.id)) onUpdate?.(result(states()));
358
+ });
359
+ try {
360
+ return result(await completed);
361
+ } finally {
362
+ unsubscribe();
363
+ signal?.removeEventListener("abort", abort);
364
+ }
365
+ },
366
+ });
367
+ const coordinationTool = (
368
+ owner?: DelegationCoordinator,
369
+ actor?: ChildRef,
370
+ wake = false,
371
+ ): ToolDefinition<typeof coordinationParameters> => ({
372
+ name: "delegate_coordinate",
373
+ label: "Coordinate delegation",
374
+ exposure: "model-only",
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.",
377
+ parameters: coordinationParameters,
378
+ async execute(callerId, args, signal, _update, ctx) {
379
+ if (["resume", "followup", "revive"].includes(args.action)) requireSupportedHost();
380
+ const registry = owner ?? coordinator;
381
+ signal?.throwIfAborted();
382
+ let value: unknown;
383
+ if (args.action === "message")
384
+ value = await registry.message(actor, args.target, args.text, args.mode, signal);
385
+ else if (args.action === "inbox") value = await registry.inbox(actor, signal);
386
+ else if (args.action === "wait")
387
+ value = await registry.coordinateWait(
388
+ actor,
389
+ args.target,
390
+ args.timeoutMs,
391
+ signal,
392
+ args.messageId,
393
+ );
394
+ else {
395
+ if (actor) throw new Error("Root-only coordination action");
396
+ if (args.action === "stop") {
397
+ await registry.stop(args.target);
398
+ value = registry.snapshot(args.target);
399
+ } else if (args.action === "park") {
400
+ registry.park(args.target);
401
+ value = registry.agents();
402
+ } else if (args.action === "status")
403
+ value = {
404
+ snapshot: registry.snapshot(args.target),
405
+ generations: registry.generations(args.target.id),
406
+ agents: registry.agents(),
407
+ };
408
+ else if (args.action === "resume") {
409
+ const next = await registry.resume(args.target, args.assignment);
410
+ value = registry.snapshot(next);
411
+ // Each continuation owns exactly one notification for its new window.
412
+ void registry.wait(next).then((state) => {
413
+ if (!closing && coordinator === registry)
414
+ show(wake ? compatible(state) : state, state.transcript, wake);
415
+ });
416
+ } else if (args.action === "followup" || args.action === "revive") {
417
+ const presence = registry
418
+ .agents()
419
+ .find(
420
+ (state) => state.id === args.target.id && state.generation === args.target.generation,
421
+ )?.presence;
422
+ if (presence !== (args.action === "revive" ? "parked" : "idle"))
423
+ throw new Error("Followup requires idle; revive requires parked current identity");
424
+ const state = registry.snapshot(args.target)!;
425
+ const parent = registry.renewalParent(args.target);
426
+ registry.configure(await resolveScheduling(ctx.cwd, ctx.isProjectTrusted()));
427
+ let { profile, skills } = await resolveProfile(
428
+ state.profile,
429
+ ctx.cwd,
430
+ ctx.isProjectTrusted(),
431
+ undefined,
432
+ availableSkills(),
433
+ );
434
+ let ancestor = parent;
435
+ let descendantRole = state.profile;
436
+ while (ancestor) {
437
+ const original = registry.snapshot(ancestor)!;
438
+ const current = (
439
+ await resolveProfile(
440
+ original.profile,
441
+ ctx.cwd,
442
+ ctx.isProjectTrusted(),
443
+ undefined,
444
+ availableSkills(),
445
+ )
446
+ ).profile;
447
+ if (!current.spawn?.includes(descendantRole))
448
+ throw new Error("Current ancestor spawn role denied");
449
+ profile = intersectProfileGrants(profile, current);
450
+ descendantRole = original.profile;
451
+ ancestor =
452
+ original.parentGeneration === undefined
453
+ ? undefined
454
+ : { id: original.parentId, generation: original.parentGeneration };
455
+ }
456
+ signal?.throwIfAborted();
457
+ const next = await registry.renew(
458
+ args.target,
459
+ {
460
+ cwd: state.cwd,
461
+ assignment: args.assignment,
462
+ resultSchema: args.resultSchema,
463
+ profile,
464
+ skills,
465
+ context: ctx,
466
+ assertTools,
467
+ toolMetadata: () => pi.getAllTools(),
468
+ codemodeEnabled,
469
+ availableSkills,
470
+ callerId,
471
+ parent,
472
+ coordinationTools: (ref) => [coordinationTool(registry, ref)],
473
+ nestedTool: (ref) => taskTool(ref, ctx, callerId),
474
+ },
475
+ signal,
476
+ );
477
+ value = registry.snapshot(next);
478
+ }
479
+ }
480
+ return { content: [{ type: "text", text: preview(value) }], details: value };
481
+ },
482
+ });
483
+ pi.registerTool(coordinationTool());
484
+ const taskDefinition = taskTool();
485
+ pi.registerTool(taskDefinition);
486
+ const compatibleTaskDefinition = taskTool(undefined, undefined, undefined, true);
487
+ pi.registerTool({
488
+ name: "subagent",
489
+ label: "Subagent",
490
+ exposure: "model-only",
491
+ promptSnippet:
492
+ "Native coding workers: launch, coordinate and resume work in existing task workspaces",
493
+ promptGuidelines: [
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
+ "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; completed idle/parked work starts a fresh generation.",
497
+ ],
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.",
500
+ parameters: compatibleParameters,
501
+ async execute(callerId, args, signal, onUpdate, ctx) {
502
+ if (!("action" in args) || args.action === "resume" || args.action === "steer")
503
+ requireSupportedHost();
504
+ const owner = coordinator;
505
+ if (!("action" in args)) {
506
+ const items = "tasks" in args ? args.tasks : [args];
507
+ const tasks = items.map((task) => {
508
+ let model = typeof task.model === "object" ? task.model : undefined;
509
+ if (typeof task.model === "string") {
510
+ const slash = task.model.indexOf("/");
511
+ if (slash < 1 || slash === task.model.length - 1)
512
+ throw new Error("Model strings require exact provider/id[:effort]");
513
+ let id = task.model.slice(slash + 1);
514
+ const colon = id.lastIndexOf(":");
515
+ const effort =
516
+ colon >= 0 ? EFFORTS.find((level) => level === id.slice(colon + 1)) : undefined;
517
+ if (effort) id = id.slice(0, colon);
518
+ model = {
519
+ provider: task.model.slice(0, slash),
520
+ id,
521
+ effort: effort ?? ctx.thinkingLevel ?? "off",
522
+ };
523
+ }
524
+ return {
525
+ assignment: task.task,
526
+ profile: task.agent === "worker" ? ("implementation" as const) : task.agent,
527
+ cwd: task.cwd ?? args.cwd,
528
+ model,
529
+ limits: task.limits,
530
+ };
531
+ });
532
+ // Internal reuse: the outer subagent call has its own public validation/hooks;
533
+ // every executed builtin still traverses the original root dispatcher.
534
+ return compatibleTaskDefinition.execute(
535
+ callerId,
536
+ "tasks" in args
537
+ ? { tasks, background: args.async ?? true }
538
+ : { ...tasks[0], background: args.async ?? true },
539
+ signal,
540
+ onUpdate,
541
+ ctx,
542
+ );
543
+ }
544
+ const result = (value: unknown) => ({
545
+ content: [{ type: "text" as const, text: preview(value) }],
546
+ details: value,
547
+ });
548
+ if (!("id" in args))
549
+ return result(
550
+ args.action === "agents"
551
+ ? { agents: [...bundledProfiles(), { name: "worker", profile: "implementation" }] }
552
+ : owner.list().map(compatible),
553
+ );
554
+ const match = /^([0-9a-f-]{36})@([1-9][0-9]*)$/.exec(args.id);
555
+ if (!match || !Number.isSafeInteger(Number(match[2])))
556
+ throw new Error("Use the exact subagent id returned by launch: UUID@generation");
557
+ const target = { id: match[1], generation: Number(match[2]) };
558
+ if (args.action === "status")
559
+ return result({
560
+ snapshot: compatible(owner.snapshot(target)!),
561
+ generations: owner.generations(target.id).map(compatible),
562
+ });
563
+ signal?.throwIfAborted();
564
+ if (args.action === "stop") await owner.stop(target);
565
+ else if (args.action === "steer") await owner.steer(target, args.message);
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
+ const response = await coordinationTool(undefined, undefined, true).execute(
573
+ callerId,
574
+ {
575
+ action:
576
+ owner.snapshot(target)?.status === "paused"
577
+ ? "resume"
578
+ : presence === "parked"
579
+ ? "revive"
580
+ : "followup",
581
+ target,
582
+ assignment: args.message,
583
+ },
584
+ signal,
585
+ undefined,
586
+ ctx,
587
+ );
588
+ 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
+ return result(compatible(next));
594
+ }
595
+ return result(compatible(owner.snapshot(target)!));
596
+ },
597
+ });
598
+ pi.registerCommand("subagent-profiles", {
599
+ description: "Subagent Profiles: inspect resolved workspace profiles (read-only)",
600
+ handler: async (_args, ctx) => {
601
+ const profiles = await resolvedProfiles(
602
+ ctx.cwd,
603
+ ctx.isProjectTrusted(),
604
+ undefined,
605
+ ctx.getSystemPromptOptions().skills ?? [],
606
+ );
607
+ if (ctx.mode !== "tui") {
608
+ show({ toolPolicy: profileToolPolicy, profiles });
609
+ return;
610
+ }
611
+ await ctx.ui.custom<void>(
612
+ (tui, theme, keys, done) =>
613
+ createProfileViewer(
614
+ profiles,
615
+ theme,
616
+ keys,
617
+ () => tui.requestRender(),
618
+ () => done(),
619
+ () => Math.max(1, Math.floor(tui.terminal.rows * 0.9)),
620
+ ),
621
+ { overlay: true, overlayOptions: { width: "90%", maxHeight: "90%", anchor: "center" } },
622
+ );
623
+ },
624
+ });
625
+ pi.registerCommand("subagent-status", {
626
+ description: "List delegation identities, states and final results",
627
+ handler: async (args) =>
628
+ show(args.trim() ? coordinator.snapshot(ref(args)) : coordinator.list()),
629
+ });
630
+ pi.registerCommand("subagent-stop", {
631
+ description: "Stop <task-id> <generation> and drain it",
632
+ handler: async (args) => {
633
+ await coordinator.stop(ref(args));
634
+ },
635
+ });
636
+ pi.registerCommand("subagent-steer", {
637
+ description: "Steer <task-id> <generation> <text>",
638
+ handler: async (args) => {
639
+ const [id, generation, ...text] = args.trim().split(/\s+/);
640
+ await coordinator.steer(ref(`${id} ${generation}`), text.join(" "));
641
+ },
642
+ });
643
+ pi.registerCommand("subagent-hub", {
644
+ description:
645
+ "Subagent Hub: subagent roster, generation history, transcripts and pinned controls (TUI only)",
646
+ handler: async (_args, ctx) => {
647
+ if (ctx.mode !== "tui") return;
648
+ await ctx.ui.custom<void>(
649
+ (tui, theme, keys, done) =>
650
+ createHub(
651
+ coordinator,
652
+ theme,
653
+ keys,
654
+ () => tui.requestRender(),
655
+ () => done(),
656
+ {
657
+ tui,
658
+ cwd: ctx.cwd,
659
+ toolRenderers: {
660
+ delegate_task: {
661
+ renderShell: taskDefinition.renderShell,
662
+ renderCall: taskDefinition.renderCall,
663
+ renderResult: taskDefinition.renderResult,
664
+ } as ToolRenderers,
665
+ },
666
+ },
667
+ () => Math.max(1, Math.floor(tui.terminal.rows * 0.9)),
668
+ ),
669
+ { overlay: true, overlayOptions: { width: "90%", maxHeight: "90%", anchor: "center" } },
670
+ );
671
+ },
672
+ });
673
+ }