pi-subagents 0.24.3 → 0.25.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +26 -5
  2. package/README.md +19 -11
  3. package/package.json +4 -8
  4. package/prompts/review-loop.md +1 -1
  5. package/skills/pi-subagents/SKILL.md +46 -10
  6. package/src/agents/agent-management.ts +5 -0
  7. package/src/agents/agent-serializer.ts +2 -0
  8. package/src/agents/agents.ts +30 -6
  9. package/src/agents/skills.ts +25 -23
  10. package/src/extension/config.ts +16 -0
  11. package/src/extension/fanout-child.ts +170 -0
  12. package/src/extension/index.ts +13 -25
  13. package/src/intercom/intercom-bridge.ts +2 -1
  14. package/src/intercom/result-intercom.ts +108 -0
  15. package/src/runs/background/async-execution.ts +107 -7
  16. package/src/runs/background/async-job-tracker.ts +57 -14
  17. package/src/runs/background/async-resume.ts +28 -15
  18. package/src/runs/background/async-status.ts +60 -30
  19. package/src/runs/background/result-watcher.ts +111 -54
  20. package/src/runs/background/run-id-resolver.ts +83 -0
  21. package/src/runs/background/run-status.ts +79 -3
  22. package/src/runs/background/stale-run-reconciler.ts +46 -1
  23. package/src/runs/background/subagent-runner.ts +66 -18
  24. package/src/runs/foreground/chain-execution.ts +6 -0
  25. package/src/runs/foreground/execution.ts +21 -5
  26. package/src/runs/foreground/subagent-executor.ts +314 -18
  27. package/src/runs/shared/completion-guard.ts +23 -1
  28. package/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
  29. package/src/runs/shared/nested-events.ts +819 -0
  30. package/src/runs/shared/nested-path.ts +52 -0
  31. package/src/runs/shared/nested-render.ts +115 -0
  32. package/src/runs/shared/parallel-utils.ts +1 -0
  33. package/src/runs/shared/pi-args.ts +67 -5
  34. package/src/runs/shared/run-history.ts +12 -7
  35. package/src/runs/shared/single-output.ts +12 -2
  36. package/src/runs/shared/subagent-prompt-runtime.ts +25 -5
  37. package/src/shared/artifacts.ts +2 -2
  38. package/src/shared/types.ts +95 -0
  39. package/src/shared/utils.ts +11 -1
  40. package/src/tui/render.ts +254 -153
@@ -35,9 +35,10 @@ import { createForkContextResolver } from "../../shared/fork-context.ts";
35
35
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
36
36
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
37
37
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
38
- import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
38
+ import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
39
39
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
40
40
  import {
41
+ attachNestedChildrenToResultChildren,
41
42
  buildSubagentResultIntercomPayload,
42
43
  deliverSubagentIntercomMessageEvent,
43
44
  deliverSubagentResultIntercomEvent,
@@ -46,6 +47,9 @@ import {
46
47
  stripDetailsOutputsForIntercomReceipt,
47
48
  } from "../../intercom/result-intercom.ts";
48
49
  import { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../background/async-resume.ts";
50
+ import { createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
51
+ import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
52
+ import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
49
53
  import { inspectSubagentStatus } from "../background/run-status.ts";
50
54
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
51
55
  import {
@@ -67,6 +71,8 @@ import {
67
71
  type ExtensionConfig,
68
72
  type IntercomEventBus,
69
73
  type MaxOutputConfig,
74
+ type NestedRouteInfo,
75
+ type NestedRunSummary,
70
76
  type ResolvedControlConfig,
71
77
  type SingleResult,
72
78
  type SubagentRunMode,
@@ -84,6 +90,7 @@ import {
84
90
  } from "../../shared/types.ts";
85
91
 
86
92
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
93
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete"]);
87
94
 
88
95
  interface TaskParam {
89
96
  agent: string;
@@ -138,6 +145,7 @@ interface ExecutorDeps {
138
145
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
139
146
  expandTilde: (p: string) => string;
140
147
  discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
148
+ allowMutatingManagementActions?: boolean;
141
149
  }
142
150
 
143
151
  interface ExecutionContextData {
@@ -158,6 +166,7 @@ interface ExecutionContextData {
158
166
  effectiveAsync: boolean;
159
167
  controlConfig: ResolvedControlConfig;
160
168
  intercomBridge: IntercomBridgeState;
169
+ nestedRoute?: NestedRouteInfo;
161
170
  }
162
171
 
163
172
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -196,7 +205,23 @@ function formatForegroundActivity(control: SubagentState["foregroundControls"] e
196
205
  return [`active ${seconds}s ago`, ...facts].join(" | ");
197
206
  }
198
207
 
208
+ function nestedResolutionScopeForExecutor(deps: ExecutorDeps): NestedRunResolutionScope | undefined {
209
+ if (deps.allowMutatingManagementActions !== false) return undefined;
210
+ const route = resolveInheritedNestedRouteFromEnv();
211
+ const address = route ? resolveNestedParentAddressFromEnv() : undefined;
212
+ return {
213
+ routes: route ? [route] : [],
214
+ ...(address ? { descendantOf: { parentRunId: address.parentRunId, ...(address.parentStepIndex !== undefined ? { parentStepIndex: address.parentStepIndex } : {}) } } : {}),
215
+ };
216
+ }
217
+
199
218
  function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
219
+ let nestedWarning: string | undefined;
220
+ try {
221
+ updateForegroundNestedProjection(control);
222
+ } catch (error) {
223
+ nestedWarning = `Nested status unavailable: ${error instanceof Error ? error.message : String(error)}`;
224
+ }
200
225
  const activity = formatForegroundActivity(control);
201
226
  const lines = [
202
227
  `Run: ${control.runId}`,
@@ -205,6 +230,8 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
205
230
  control.currentAgent ? `Current: ${control.currentAgent}${control.currentIndex !== undefined ? ` step ${control.currentIndex + 1}` : ""}` : undefined,
206
231
  activity ? `Activity: ${activity}` : undefined,
207
232
  ].filter((line): line is string => Boolean(line));
233
+ lines.push(...formatNestedRunStatusLines(control.nestedChildren, { indent: "", commandHints: true, maxLines: 20 }));
234
+ if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
208
235
  return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
209
236
  }
210
237
 
@@ -252,7 +279,18 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
252
279
 
253
280
  type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
254
281
  type ForegroundResumeSourceTarget = NonNullable<ReturnType<typeof resolveForegroundResumeTarget>> & { kind: "revive"; source: "foreground" };
255
- type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget;
282
+ type NestedResumeSourceTarget = {
283
+ kind: "revive";
284
+ source: "nested";
285
+ runId: string;
286
+ state: "complete" | "failed" | "paused";
287
+ agent: string;
288
+ index: number;
289
+ intercomTarget: string;
290
+ cwd?: string;
291
+ sessionFile: string;
292
+ };
293
+ type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
256
294
 
257
295
  function isAsyncRunNotFound(error: unknown): boolean {
258
296
  return error instanceof Error && error.message.startsWith("Async run not found.");
@@ -392,6 +430,119 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
392
430
  }
393
431
  }
394
432
 
433
+ function nestedRunSessionFile(run: NestedRunSummary): string | undefined {
434
+ return run.sessionFile ?? (run.steps?.length === 1 ? run.steps[0]?.sessionFile : undefined);
435
+ }
436
+
437
+ function nestedRunAgent(run: NestedRunSummary): string | undefined {
438
+ return run.agent ?? run.agents?.[0] ?? (run.steps?.length === 1 ? run.steps[0]?.agent : undefined);
439
+ }
440
+
441
+ function pathWithin(base: string, candidate: string): boolean {
442
+ const resolvedBase = path.resolve(base);
443
+ const resolvedCandidate = path.resolve(candidate);
444
+ return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
445
+ }
446
+
447
+ function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: string[]): string {
448
+ const sessionFile = nestedRunSessionFile(run);
449
+ if (!sessionFile) throw new Error(`Nested run '${run.id}' does not have a persisted session file to resume from.`);
450
+ if (path.extname(sessionFile) !== ".jsonl") throw new Error(`Nested run '${run.id}' session file must be a .jsonl file: ${sessionFile}`);
451
+ const resolved = path.resolve(sessionFile);
452
+ if (!path.isAbsolute(sessionFile)) throw new Error(`Nested run '${run.id}' session file must be absolute: ${sessionFile}`);
453
+ if (!fs.existsSync(resolved)) throw new Error(`Nested run '${run.id}' session file does not exist: ${sessionFile}`);
454
+ const stat = fs.lstatSync(resolved);
455
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Nested run '${run.id}' session file is not a regular file: ${sessionFile}`);
456
+ const realSessionFile = fs.realpathSync(resolved);
457
+ const trustedRoots = trustedSessionRoots
458
+ .filter((root) => fs.existsSync(root))
459
+ .map((root) => fs.realpathSync(root));
460
+ if (!trustedRoots.some((root) => pathWithin(root, realSessionFile))) {
461
+ throw new Error(`Nested run '${run.id}' session file is outside trusted nested session roots: ${sessionFile}`);
462
+ }
463
+ if (!realSessionFile.split(path.sep).includes(run.id)) {
464
+ throw new Error(`Nested run '${run.id}' session file is not under that nested run's session directory: ${sessionFile}`);
465
+ }
466
+ return realSessionFile;
467
+ }
468
+
469
+ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "nested" }, trustedSessionRoots: string[]): NestedResumeSourceTarget {
470
+ const run = match.match.run;
471
+ if (run.state === "running" || run.state === "queued") throw new Error(`Nested run '${run.id}' is live; route the follow-up to the owner process instead.`);
472
+ const agent = nestedRunAgent(run);
473
+ if (!agent) throw new Error(`Could not determine child agent for nested run '${run.id}'.`);
474
+ const state = run.state === "complete" || run.state === "failed" || run.state === "paused" ? run.state : "failed";
475
+ const asyncDir = resolveNestedAsyncDir(match.match.rootRunId, run);
476
+ return {
477
+ kind: "revive",
478
+ source: "nested",
479
+ runId: run.id,
480
+ state,
481
+ agent,
482
+ index: 0,
483
+ intercomTarget: resolveSubagentIntercomTarget(run.id, agent, 0),
484
+ cwd: asyncDir ? path.dirname(asyncDir) : undefined,
485
+ sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
486
+ };
487
+ }
488
+
489
+ async function waitForNestedControlResult(target: ResolvedSubagentRunId & { kind: "nested" }, requestId: string, timeoutMs = 1_000) {
490
+ const deadline = Date.now() + timeoutMs;
491
+ while (Date.now() < deadline) {
492
+ const result = readNestedControlResults(target.match.route).find((candidate) => candidate.requestId === requestId && candidate.targetRunId === target.match.run.id);
493
+ if (result) return result;
494
+ await new Promise((resolve) => setTimeout(resolve, 50));
495
+ }
496
+ return undefined;
497
+ }
498
+
499
+ async function sendNestedControlRequest(target: ResolvedSubagentRunId & { kind: "nested" }, action: "interrupt" | "resume", message?: string) {
500
+ const requestId = randomUUID();
501
+ writeNestedControlRequest(target.match.route, {
502
+ ts: Date.now(),
503
+ requestId,
504
+ targetRunId: target.match.run.id,
505
+ action,
506
+ ...(message ? { message } : {}),
507
+ });
508
+ return waitForNestedControlResult(target, requestId);
509
+ }
510
+
511
+ function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nested" }): AgentToolResult<Details> | undefined {
512
+ const run = target.match.run;
513
+ const asyncDir = resolveNestedAsyncDir(target.match.rootRunId, run);
514
+ if (!asyncDir) return undefined;
515
+ const status = readStatus(asyncDir);
516
+ const pid = typeof status?.pid === "number" && status.pid > 0 ? status.pid : run.pid;
517
+ if (!status || status.state !== "running" || typeof pid !== "number" || pid <= 0) return undefined;
518
+ try {
519
+ process.kill(pid, ASYNC_INTERRUPT_SIGNAL);
520
+ return { content: [{ type: "text", text: `Interrupt requested for nested async run ${run.id}.` }], details: { mode: "management", results: [] } };
521
+ } catch (error) {
522
+ const message = error instanceof Error ? error.message : String(error);
523
+ return { content: [{ type: "text", text: `Failed to interrupt nested async run ${run.id}: ${message}` }], isError: true, details: { mode: "management", results: [] } };
524
+ }
525
+ }
526
+
527
+ async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "nested" }): Promise<AgentToolResult<Details>> {
528
+ const run = target.match.run;
529
+ if (run.state === "complete") return { content: [{ type: "text", text: `Nested run ${run.id} is already complete and cannot be interrupted.` }], isError: true, details: { mode: "management", results: [] } };
530
+ if (run.state === "failed") return { content: [{ type: "text", text: `Nested run ${run.id} has failed and cannot be interrupted.` }], isError: true, details: { mode: "management", results: [] } };
531
+ if (run.state === "paused") return { content: [{ type: "text", text: `Nested run ${run.id} is already paused.` }], isError: true, details: { mode: "management", results: [] } };
532
+ const result = await sendNestedControlRequest(target, "interrupt");
533
+ if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } };
534
+ const direct = directNestedAsyncInterrupt(target);
535
+ if (direct) return direct;
536
+ return { content: [{ type: "text", text: `Nested run ${run.id} owner is not reachable and no safe direct async interrupt fallback is available.` }], isError: true, details: { mode: "management", results: [] } };
537
+ }
538
+
539
+ async function resumeLiveNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string }): Promise<AgentToolResult<Details>> {
540
+ const run = input.target.match.run;
541
+ const result = await sendNestedControlRequest(input.target, "resume", input.message);
542
+ if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } };
543
+ return { content: [{ type: "text", text: `Nested run ${run.id} appears live but its owner route is not reachable. Wait for completion, then retry action='resume'.` }], isError: true, details: { mode: "management", results: [] } };
544
+ }
545
+
395
546
  async function resumeAsyncRun(input: {
396
547
  params: SubagentParamsLike;
397
548
  requestCwd: string;
@@ -408,8 +559,22 @@ async function resumeAsyncRun(input: {
408
559
  }
409
560
 
410
561
  let target: ResumeSourceTarget;
562
+ const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
411
563
  try {
412
- target = resolveResumeTarget(input.params, input.deps.state);
564
+ const requestedId = input.params.id ?? input.params.runId;
565
+ const resolved = requestedId ? resolveSubagentRunId(requestedId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }) : undefined;
566
+ if (resolved?.kind === "nested") {
567
+ if (resolved.match.run.state === "running" || resolved.match.run.state === "queued") {
568
+ return resumeLiveNestedRun({ target: resolved, message: followUp });
569
+ }
570
+ const trustedSessionRoots = [
571
+ ...(input.deps.config.defaultSessionDir ? [path.resolve(input.deps.expandTilde(input.deps.config.defaultSessionDir))] : []),
572
+ ...(parentSessionFile ? [input.deps.getSubagentSessionRoot(parentSessionFile)] : []),
573
+ ];
574
+ target = resolveNestedResumeTarget(resolved, trustedSessionRoots);
575
+ } else {
576
+ target = resolveResumeTarget(input.params, input.deps.state);
577
+ }
413
578
  } catch (error) {
414
579
  const message = error instanceof Error ? error.message : String(error);
415
580
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
@@ -445,7 +610,6 @@ async function resumeAsyncRun(input: {
445
610
  };
446
611
  }
447
612
 
448
- const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
449
613
  input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager);
450
614
  const effectiveCwd = target.cwd ?? input.requestCwd;
451
615
  const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
@@ -501,7 +665,7 @@ async function resumeAsyncRun(input: {
501
665
 
502
666
  const revivedId = result.details.asyncId ?? runId;
503
667
  const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
504
- const sourceLabel = target.source === "foreground" ? "foreground" : "async";
668
+ const sourceLabel = target.source;
505
669
  const lines = [
506
670
  `Revived ${sourceLabel} subagent from ${target.runId}.`,
507
671
  `Revived run: ${revivedId}`,
@@ -538,6 +702,7 @@ async function emitForegroundResultIntercom(input: {
538
702
  mode: SubagentRunMode;
539
703
  results: SingleResult[];
540
704
  chainSteps?: number;
705
+ nestedChildren?: NestedRunSummary[];
541
706
  }): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
542
707
  if (!input.intercomBridge.active || !input.intercomBridge.orchestratorTarget) return null;
543
708
  const children = input.results.flatMap((result, index) => result.detached ? [] : [{
@@ -559,7 +724,7 @@ async function emitForegroundResultIntercom(input: {
559
724
  runId: input.runId,
560
725
  mode: input.mode,
561
726
  source: "foreground",
562
- children,
727
+ children: attachNestedChildrenToResultChildren(input.runId, children, input.nestedChildren),
563
728
  ...(typeof input.chainSteps === "number" ? { chainSteps: input.chainSteps } : {}),
564
729
  });
565
730
  const delivered = await deliverSubagentResultIntercomEvent(input.pi.events, payload);
@@ -573,6 +738,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
573
738
  runId: string;
574
739
  mode: SubagentRunMode;
575
740
  details: Details;
741
+ nestedChildren?: NestedRunSummary[];
576
742
  }): Promise<{ text: string; details: Details } | null> {
577
743
  const payload = await emitForegroundResultIntercom({
578
744
  pi: input.pi,
@@ -581,6 +747,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
581
747
  mode: input.mode,
582
748
  results: input.details.results,
583
749
  ...(typeof input.details.totalSteps === "number" ? { chainSteps: input.details.totalSteps } : {}),
750
+ ...(input.nestedChildren?.length ? { nestedChildren: input.nestedChildren } : {}),
584
751
  });
585
752
  if (!payload) return null;
586
753
  return {
@@ -850,6 +1017,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
850
1017
  effectiveAsync,
851
1018
  controlConfig,
852
1019
  intercomBridge,
1020
+ nestedRoute,
853
1021
  } = data;
854
1022
  const hasChain = (params.chain?.length ?? 0) > 0;
855
1023
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -939,6 +1107,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
939
1107
  controlConfig,
940
1108
  controlIntercomTarget,
941
1109
  childIntercomTarget,
1110
+ nestedRoute,
942
1111
  });
943
1112
  }
944
1113
 
@@ -966,6 +1135,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
966
1135
  controlConfig,
967
1136
  controlIntercomTarget,
968
1137
  childIntercomTarget,
1138
+ nestedRoute,
969
1139
  });
970
1140
  }
971
1141
 
@@ -979,7 +1149,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
979
1149
  };
980
1150
  }
981
1151
  const rawOutput = params.output !== undefined ? params.output : a.output;
982
- const effectiveOutput: string | false | undefined = rawOutput === true ? a.output : (rawOutput as string | false | undefined);
1152
+ const effectiveOutput = normalizeSingleOutputOverride(rawOutput, a.output);
983
1153
  const effectiveOutputMode = params.outputMode ?? "inline";
984
1154
  const normalizedSkills = normalizeSkillInput(params.skill);
985
1155
  const skills = normalizedSkills === false ? [] : normalizedSkills;
@@ -1008,6 +1178,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1008
1178
  controlConfig,
1009
1179
  controlIntercomTarget,
1010
1180
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
1181
+ nestedRoute,
1011
1182
  });
1012
1183
  }
1013
1184
 
@@ -1060,6 +1231,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1060
1231
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
1061
1232
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1062
1233
  foregroundControl,
1234
+ nestedRoute: foregroundControl?.nestedRoute,
1063
1235
  chainSkills,
1064
1236
  chainDir: params.chainDir,
1065
1237
  maxSubagentDepth: currentMaxSubagentDepth,
@@ -1103,10 +1275,12 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1103
1275
  controlConfig,
1104
1276
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1105
1277
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
1278
+ nestedRoute: data.nestedRoute,
1106
1279
  });
1107
1280
  }
1108
1281
 
1109
1282
  const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
1283
+ if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1110
1284
  if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1111
1285
  const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
1112
1286
  ? await maybeBuildForegroundIntercomReceipt({
@@ -1115,6 +1289,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1115
1289
  runId,
1116
1290
  mode: "chain",
1117
1291
  details: chainDetails,
1292
+ ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
1118
1293
  })
1119
1294
  : null;
1120
1295
  if (intercomReceipt) {
@@ -1311,6 +1486,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1311
1486
  onControlEvent: input.onControlEvent,
1312
1487
  intercomSessionName: input.childIntercomTarget?.(task.agent, index),
1313
1488
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
1489
+ nestedRoute: input.foregroundControl?.nestedRoute,
1314
1490
  modelOverride: input.modelOverrides[index],
1315
1491
  availableModels: input.availableModels,
1316
1492
  preferredModelProvider: input.ctx.model?.provider,
@@ -1635,12 +1811,14 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1635
1811
  };
1636
1812
  }
1637
1813
 
1814
+ if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1638
1815
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
1639
1816
  pi: deps.pi,
1640
1817
  intercomBridge: data.intercomBridge,
1641
1818
  runId,
1642
1819
  mode: "parallel",
1643
1820
  details,
1821
+ ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
1644
1822
  });
1645
1823
  if (intercomReceipt) {
1646
1824
  return {
@@ -1716,7 +1894,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1716
1894
  );
1717
1895
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
1718
1896
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
1719
- let effectiveOutput: string | false | undefined = rawOutput === true ? agentConfig.output : (rawOutput as string | false | undefined);
1897
+ let effectiveOutput = normalizeSingleOutputOverride(rawOutput, agentConfig.output);
1720
1898
  const effectiveOutputMode = params.outputMode ?? "inline";
1721
1899
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
1722
1900
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
@@ -1750,7 +1928,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1750
1928
  task = result.templates[0]!;
1751
1929
  const override = result.behaviorOverrides[0];
1752
1930
  if (override?.model) modelOverride = override.model;
1753
- if (override?.output !== undefined) effectiveOutput = override.output;
1931
+ if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
1754
1932
  if (override?.skills !== undefined) skillOverride = override.skills;
1755
1933
 
1756
1934
  if (result.runInBackground) {
@@ -1869,6 +2047,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1869
2047
  onControlEvent,
1870
2048
  intercomSessionName: childIntercomTarget,
1871
2049
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2050
+ nestedRoute: foregroundControl?.nestedRoute,
1872
2051
  index: 0,
1873
2052
  modelOverride,
1874
2053
  availableModels,
@@ -1914,12 +2093,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1914
2093
  rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
1915
2094
 
1916
2095
  if (!r.detached && !r.interrupted) {
2096
+ if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1917
2097
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
1918
2098
  pi: deps.pi,
1919
2099
  intercomBridge: data.intercomBridge,
1920
2100
  runId,
1921
2101
  mode: "single",
1922
2102
  details,
2103
+ ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
1923
2104
  });
1924
2105
  if (intercomReceipt) {
1925
2106
  return {
@@ -2013,16 +2194,41 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2013
2194
  };
2014
2195
  }
2015
2196
  if (params.action === "status") {
2016
- const foreground = getForegroundControl(deps.state, paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId);
2017
- if (foreground) return foregroundStatusResult(foreground);
2018
- return inspectSubagentStatus(paramsWithResolvedCwd);
2197
+ const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
2198
+ if (targetRunId) {
2199
+ try {
2200
+ const nestedScope = nestedResolutionScopeForExecutor(deps);
2201
+ const resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedScope });
2202
+ if (resolved?.kind === "foreground") {
2203
+ const foreground = getForegroundControl(deps.state, resolved.id);
2204
+ if (foreground) return foregroundStatusResult(foreground);
2205
+ }
2206
+ } catch (error) {
2207
+ const message = error instanceof Error ? error.message : String(error);
2208
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
2209
+ }
2210
+ } else {
2211
+ const foreground = getForegroundControl(deps.state, undefined);
2212
+ if (foreground) return foregroundStatusResult(foreground);
2213
+ }
2214
+ return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
2019
2215
  }
2020
2216
  if (params.action === "resume") {
2021
2217
  return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
2022
2218
  }
2023
2219
  if (params.action === "interrupt") {
2024
2220
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
2025
- const foreground = getForegroundControl(deps.state, targetRunId);
2221
+ let resolved: ResolvedSubagentRunId | undefined;
2222
+ if (targetRunId) {
2223
+ try {
2224
+ resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
2225
+ } catch (error) {
2226
+ const message = error instanceof Error ? error.message : String(error);
2227
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
2228
+ }
2229
+ }
2230
+ if (resolved?.kind === "nested") return interruptNestedRun(resolved);
2231
+ const foreground = getForegroundControl(deps.state, resolved?.kind === "foreground" ? resolved.id : targetRunId);
2026
2232
  if (foreground?.interrupt) {
2027
2233
  const interrupted = foreground.interrupt();
2028
2234
  if (interrupted) {
@@ -2039,7 +2245,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2039
2245
  details: { mode: "management", results: [] },
2040
2246
  };
2041
2247
  }
2042
- const asyncInterruptResult = interruptAsyncRun(deps.state, targetRunId);
2248
+ const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId);
2043
2249
  if (asyncInterruptResult) return asyncInterruptResult;
2044
2250
  return {
2045
2251
  content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
@@ -2054,6 +2260,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2054
2260
  details: { mode: "management" as const, results: [] },
2055
2261
  };
2056
2262
  }
2263
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(params.action)) {
2264
+ return {
2265
+ content: [{ type: "text", text: `Action '${params.action}' is not available from child-safe subagent fanout mode.` }],
2266
+ isError: true,
2267
+ details: { mode: "management" as const, results: [] },
2268
+ };
2269
+ }
2057
2270
  return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd });
2058
2271
  }
2059
2272
 
@@ -2101,6 +2314,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2101
2314
  ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
2102
2315
  : discoveredAgents;
2103
2316
  const runId = randomUUID().slice(0, 8);
2317
+ const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
2318
+ const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
2319
+ const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
2104
2320
  const shareEnabled = effectiveParams.share === true;
2105
2321
  const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
2106
2322
  const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
@@ -2182,6 +2398,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2182
2398
  effectiveAsync,
2183
2399
  controlConfig,
2184
2400
  intercomBridge,
2401
+ nestedRoute,
2185
2402
  };
2186
2403
 
2187
2404
  const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
@@ -2195,6 +2412,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2195
2412
  currentAgent: undefined,
2196
2413
  currentIndex: undefined,
2197
2414
  currentActivityState: undefined,
2415
+ nestedRoute,
2198
2416
  interrupt: undefined,
2199
2417
  };
2200
2418
  if (foregroundControl) {
@@ -2202,14 +2420,92 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2202
2420
  deps.state.lastForegroundControlId = runId;
2203
2421
  }
2204
2422
 
2423
+ const writeNestedForegroundEvent = (type: "subagent.nested.started" | "subagent.nested.completed", result?: AgentToolResult<Details>): void => {
2424
+ if (!inheritedNestedRoute || !nestedParentAddress) return;
2425
+ const now = Date.now();
2426
+ const details = result?.details;
2427
+ const state = type === "subagent.nested.started"
2428
+ ? "running"
2429
+ : result?.isError || details?.results.some((child) => child.exitCode !== 0)
2430
+ ? "failed"
2431
+ : details?.results.some((child) => child.interrupted)
2432
+ ? "paused"
2433
+ : "complete";
2434
+ const errorText = result?.isError
2435
+ ? result.content.find((item) => item.type === "text")?.text
2436
+ : undefined;
2437
+ const agentsForSummary = hasTasks && effectiveParams.tasks
2438
+ ? effectiveParams.tasks.map((task) => task.agent)
2439
+ : hasChain && effectiveParams.chain
2440
+ ? effectiveParams.chain.flatMap((step) => isParallelStep(step) ? step.parallel.map((task) => task.agent) : [(step as SequentialStep).agent])
2441
+ : effectiveParams.agent ? [effectiveParams.agent] : [];
2442
+ const leafIntercomTarget = intercomBridge.active && agentsForSummary[0]
2443
+ ? resolveSubagentIntercomTarget(runId, agentsForSummary[0], 0)
2444
+ : undefined;
2445
+ try {
2446
+ writeNestedEvent(inheritedNestedRoute, {
2447
+ type,
2448
+ ts: now,
2449
+ parentRunId: nestedParentAddress.parentRunId,
2450
+ parentStepIndex: nestedParentAddress.parentStepIndex,
2451
+ child: {
2452
+ id: runId,
2453
+ parentRunId: nestedParentAddress.parentRunId,
2454
+ parentStepIndex: nestedParentAddress.parentStepIndex,
2455
+ depth: nestedParentAddress.depth,
2456
+ path: nestedParentAddress.path,
2457
+ ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
2458
+ leafIntercomTarget,
2459
+ intercomTarget: leafIntercomTarget,
2460
+ ownerState: state === "running" ? "live" : "gone",
2461
+ mode: foregroundMode,
2462
+ state,
2463
+ agent: agentsForSummary[0],
2464
+ agents: agentsForSummary,
2465
+ startedAt: foregroundControl?.startedAt ?? now,
2466
+ ...(state !== "running" ? { endedAt: now } : {}),
2467
+ lastUpdate: now,
2468
+ ...(errorText ? { error: errorText } : {}),
2469
+ ...(details?.results.length ? { steps: details.results.map((child) => ({
2470
+ agent: child.agent,
2471
+ status: child.interrupted ? "paused" : child.exitCode === 0 ? "complete" : "failed",
2472
+ ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
2473
+ ...(child.error ? { error: child.error } : {}),
2474
+ })) } : {}),
2475
+ },
2476
+ });
2477
+ } catch (error) {
2478
+ console.error("Failed to emit nested foreground status event:", error);
2479
+ }
2480
+ };
2481
+
2482
+ let nestedForegroundStarted = false;
2205
2483
  try {
2206
2484
  const asyncResult = runAsyncPath(execData, deps);
2207
2485
  if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
2208
- if (hasChain && effectiveParams.chain) return withForkContext(await runChainPath(execData, deps), effectiveParams.context);
2209
- if (hasTasks && effectiveParams.tasks) return withForkContext(await runParallelPath(execData, deps), effectiveParams.context);
2210
- if (hasSingle) return withForkContext(await runSinglePath(execData, deps), effectiveParams.context);
2486
+ if (foregroundControl) {
2487
+ writeNestedForegroundEvent("subagent.nested.started");
2488
+ nestedForegroundStarted = true;
2489
+ }
2490
+ if (hasChain && effectiveParams.chain) {
2491
+ const result = await runChainPath(execData, deps);
2492
+ writeNestedForegroundEvent("subagent.nested.completed", result);
2493
+ return withForkContext(result, effectiveParams.context);
2494
+ }
2495
+ if (hasTasks && effectiveParams.tasks) {
2496
+ const result = await runParallelPath(execData, deps);
2497
+ writeNestedForegroundEvent("subagent.nested.completed", result);
2498
+ return withForkContext(result, effectiveParams.context);
2499
+ }
2500
+ if (hasSingle) {
2501
+ const result = await runSinglePath(execData, deps);
2502
+ writeNestedForegroundEvent("subagent.nested.completed", result);
2503
+ return withForkContext(result, effectiveParams.context);
2504
+ }
2211
2505
  } catch (error) {
2212
- return toExecutionErrorResult(effectiveParams, error);
2506
+ const errorResult = toExecutionErrorResult(effectiveParams, error);
2507
+ if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
2508
+ return errorResult;
2213
2509
  } finally {
2214
2510
  if (foregroundControl) {
2215
2511
  clearPendingForegroundControlNotices(deps.state, runId);
@@ -54,11 +54,24 @@ const GENERAL_IMPLEMENTATION_PATTERNS = [
54
54
  /\b(?:update|add|remove|replace|delete|create)\s+(?:the\s+)?(?:file|files|code|source|implementation|test|tests|component|function|module|class|method|logic|import|imports|readme|docs?|changelog|package\.json|config|manifest|extension|prompt|command)\b/i,
55
55
  ];
56
56
 
57
+ const READ_ONLY_BUILTIN_TOOLS = new Set([
58
+ "read",
59
+ "grep",
60
+ "find",
61
+ "ls",
62
+ "web_search",
63
+ "fetch_content",
64
+ "get_search_content",
65
+ "intercom",
66
+ "contact_supervisor",
67
+ ]);
57
68
 
58
69
  interface CompletionMutationGuardInput {
59
70
  agent: string;
60
71
  task: string;
61
72
  messages: Message[];
73
+ tools?: string[];
74
+ mcpDirectTools?: string[];
62
75
  }
63
76
 
64
77
  interface CompletionMutationGuardResult {
@@ -83,6 +96,13 @@ function stripScopedNoEditConstraints(task: string): string {
83
96
  return stripped;
84
97
  }
85
98
 
99
+ function declaresOnlyReadOnlyTools(tools: string[] | undefined, mcpDirectTools: string[] | undefined): boolean {
100
+ return tools !== undefined
101
+ && tools.length > 0
102
+ && (mcpDirectTools?.length ?? 0) === 0
103
+ && tools.every((tool) => READ_ONLY_BUILTIN_TOOLS.has(tool));
104
+ }
105
+
86
106
  export function expectsImplementationMutation(agent: string, task: string): boolean {
87
107
  const taskText = stripFrameworkInstructions(task);
88
108
  const taskTextWithoutScopedConstraints = stripScopedNoEditConstraints(taskText);
@@ -115,7 +135,9 @@ export function hasMutationToolCall(messages: Message[]): boolean {
115
135
  }
116
136
 
117
137
  export function evaluateCompletionMutationGuard(input: CompletionMutationGuardInput): CompletionMutationGuardResult {
118
- const expectedMutation = expectsImplementationMutation(input.agent, input.task);
138
+ const expectedMutation = declaresOnlyReadOnlyTools(input.tools, input.mcpDirectTools)
139
+ ? false
140
+ : expectsImplementationMutation(input.agent, input.task);
119
141
  const attemptedMutation = hasMutationToolCall(input.messages);
120
142
  return {
121
143
  expectedMutation,