pi-subagents 0.24.4 → 0.27.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 +29 -0
- package/README.md +145 -27
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +71 -20
- package/src/agents/agent-management.ts +57 -15
- package/src/agents/agent-serializer.ts +3 -2
- package/src/agents/agents.ts +47 -16
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +171 -0
- package/src/extension/index.ts +7 -2
- package/src/extension/schemas.ts +138 -5
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +185 -10
- package/src/runs/background/async-job-tracker.ts +41 -6
- package/src/runs/background/async-resume.ts +28 -15
- package/src/runs/background/async-status.ts +71 -31
- package/src/runs/background/result-watcher.ts +111 -54
- package/src/runs/background/run-id-resolver.ts +83 -0
- package/src/runs/background/run-status.ts +89 -4
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +648 -42
- package/src/runs/foreground/chain-execution.ts +331 -118
- package/src/runs/foreground/execution.ts +226 -10
- package/src/runs/foreground/subagent-executor.ts +377 -14
- package/src/runs/shared/acceptance-contract.ts +291 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +161 -0
- package/src/runs/shared/acceptance-reports.ts +127 -0
- package/src/runs/shared/acceptance.ts +22 -0
- package/src/runs/shared/chain-outputs.ts +101 -0
- package/src/runs/shared/completion-guard.ts +26 -3
- package/src/runs/shared/dynamic-fanout.ts +293 -0
- package/src/runs/shared/nested-events.ts +819 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +115 -0
- package/src/runs/shared/parallel-utils.ts +31 -1
- package/src/runs/shared/pi-args.ts +73 -5
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
- package/src/runs/shared/workflow-graph.ts +206 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +345 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +268 -43
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
writeInitialProgressFile,
|
|
22
22
|
getStepAgents,
|
|
23
23
|
isParallelStep,
|
|
24
|
+
isDynamicParallelStep,
|
|
24
25
|
resolveStepBehavior,
|
|
25
26
|
suppressProgressForReadOnlyTask,
|
|
26
27
|
taskDisallowsFileUpdates,
|
|
@@ -38,6 +39,7 @@ import { formatControlIntercomMessage, formatControlNoticeMessage, resolveContro
|
|
|
38
39
|
import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
39
40
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
|
|
40
41
|
import {
|
|
42
|
+
attachNestedChildrenToResultChildren,
|
|
41
43
|
buildSubagentResultIntercomPayload,
|
|
42
44
|
deliverSubagentIntercomMessageEvent,
|
|
43
45
|
deliverSubagentResultIntercomEvent,
|
|
@@ -46,8 +48,12 @@ import {
|
|
|
46
48
|
stripDetailsOutputsForIntercomReceipt,
|
|
47
49
|
} from "../../intercom/result-intercom.ts";
|
|
48
50
|
import { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../background/async-resume.ts";
|
|
51
|
+
import { createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
52
|
+
import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
|
|
53
|
+
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
49
54
|
import { inspectSubagentStatus } from "../background/run-status.ts";
|
|
50
55
|
import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
|
|
56
|
+
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
51
57
|
import {
|
|
52
58
|
cleanupWorktrees,
|
|
53
59
|
createWorktrees,
|
|
@@ -59,6 +65,7 @@ import {
|
|
|
59
65
|
} from "../shared/worktree.ts";
|
|
60
66
|
import {
|
|
61
67
|
type AgentProgress,
|
|
68
|
+
type AcceptanceInput,
|
|
62
69
|
type ArtifactConfig,
|
|
63
70
|
type ArtifactPaths,
|
|
64
71
|
type ControlConfig,
|
|
@@ -67,6 +74,8 @@ import {
|
|
|
67
74
|
type ExtensionConfig,
|
|
68
75
|
type IntercomEventBus,
|
|
69
76
|
type MaxOutputConfig,
|
|
77
|
+
type NestedRouteInfo,
|
|
78
|
+
type NestedRunSummary,
|
|
70
79
|
type ResolvedControlConfig,
|
|
71
80
|
type SingleResult,
|
|
72
81
|
type SubagentRunMode,
|
|
@@ -84,6 +93,7 @@ import {
|
|
|
84
93
|
} from "../../shared/types.ts";
|
|
85
94
|
|
|
86
95
|
const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
96
|
+
const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete"]);
|
|
87
97
|
|
|
88
98
|
interface TaskParam {
|
|
89
99
|
agent: string;
|
|
@@ -96,6 +106,7 @@ interface TaskParam {
|
|
|
96
106
|
progress?: boolean;
|
|
97
107
|
model?: string;
|
|
98
108
|
skill?: string | string[] | boolean;
|
|
109
|
+
acceptance?: AcceptanceInput;
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
export interface SubagentParamsLike {
|
|
@@ -127,6 +138,7 @@ export interface SubagentParamsLike {
|
|
|
127
138
|
outputMode?: "inline" | "file-only";
|
|
128
139
|
agentScope?: unknown;
|
|
129
140
|
chainDir?: string;
|
|
141
|
+
acceptance?: AcceptanceInput;
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
interface ExecutorDeps {
|
|
@@ -138,6 +150,7 @@ interface ExecutorDeps {
|
|
|
138
150
|
getSubagentSessionRoot: (parentSessionFile: string | null) => string;
|
|
139
151
|
expandTilde: (p: string) => string;
|
|
140
152
|
discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
|
|
153
|
+
allowMutatingManagementActions?: boolean;
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
interface ExecutionContextData {
|
|
@@ -158,6 +171,7 @@ interface ExecutionContextData {
|
|
|
158
171
|
effectiveAsync: boolean;
|
|
159
172
|
controlConfig: ResolvedControlConfig;
|
|
160
173
|
intercomBridge: IntercomBridgeState;
|
|
174
|
+
nestedRoute?: NestedRouteInfo;
|
|
161
175
|
}
|
|
162
176
|
|
|
163
177
|
function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
|
|
@@ -196,7 +210,23 @@ function formatForegroundActivity(control: SubagentState["foregroundControls"] e
|
|
|
196
210
|
return [`active ${seconds}s ago`, ...facts].join(" | ");
|
|
197
211
|
}
|
|
198
212
|
|
|
213
|
+
function nestedResolutionScopeForExecutor(deps: ExecutorDeps): NestedRunResolutionScope | undefined {
|
|
214
|
+
if (deps.allowMutatingManagementActions !== false) return undefined;
|
|
215
|
+
const route = resolveInheritedNestedRouteFromEnv();
|
|
216
|
+
const address = route ? resolveNestedParentAddressFromEnv() : undefined;
|
|
217
|
+
return {
|
|
218
|
+
routes: route ? [route] : [],
|
|
219
|
+
...(address ? { descendantOf: { parentRunId: address.parentRunId, ...(address.parentStepIndex !== undefined ? { parentStepIndex: address.parentStepIndex } : {}) } } : {}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
199
223
|
function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
|
|
224
|
+
let nestedWarning: string | undefined;
|
|
225
|
+
try {
|
|
226
|
+
updateForegroundNestedProjection(control);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
nestedWarning = `Nested status unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
229
|
+
}
|
|
200
230
|
const activity = formatForegroundActivity(control);
|
|
201
231
|
const lines = [
|
|
202
232
|
`Run: ${control.runId}`,
|
|
@@ -205,6 +235,8 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
|
|
|
205
235
|
control.currentAgent ? `Current: ${control.currentAgent}${control.currentIndex !== undefined ? ` step ${control.currentIndex + 1}` : ""}` : undefined,
|
|
206
236
|
activity ? `Activity: ${activity}` : undefined,
|
|
207
237
|
].filter((line): line is string => Boolean(line));
|
|
238
|
+
lines.push(...formatNestedRunStatusLines(control.nestedChildren, { indent: "", commandHints: true, maxLines: 20 }));
|
|
239
|
+
if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
|
|
208
240
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
|
|
209
241
|
}
|
|
210
242
|
|
|
@@ -252,7 +284,18 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
|
|
|
252
284
|
|
|
253
285
|
type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
|
|
254
286
|
type ForegroundResumeSourceTarget = NonNullable<ReturnType<typeof resolveForegroundResumeTarget>> & { kind: "revive"; source: "foreground" };
|
|
255
|
-
type
|
|
287
|
+
type NestedResumeSourceTarget = {
|
|
288
|
+
kind: "revive";
|
|
289
|
+
source: "nested";
|
|
290
|
+
runId: string;
|
|
291
|
+
state: "complete" | "failed" | "paused";
|
|
292
|
+
agent: string;
|
|
293
|
+
index: number;
|
|
294
|
+
intercomTarget: string;
|
|
295
|
+
cwd?: string;
|
|
296
|
+
sessionFile: string;
|
|
297
|
+
};
|
|
298
|
+
type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
|
|
256
299
|
|
|
257
300
|
function isAsyncRunNotFound(error: unknown): boolean {
|
|
258
301
|
return error instanceof Error && error.message.startsWith("Async run not found.");
|
|
@@ -392,6 +435,119 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
|
|
|
392
435
|
}
|
|
393
436
|
}
|
|
394
437
|
|
|
438
|
+
function nestedRunSessionFile(run: NestedRunSummary): string | undefined {
|
|
439
|
+
return run.sessionFile ?? (run.steps?.length === 1 ? run.steps[0]?.sessionFile : undefined);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function nestedRunAgent(run: NestedRunSummary): string | undefined {
|
|
443
|
+
return run.agent ?? run.agents?.[0] ?? (run.steps?.length === 1 ? run.steps[0]?.agent : undefined);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function pathWithin(base: string, candidate: string): boolean {
|
|
447
|
+
const resolvedBase = path.resolve(base);
|
|
448
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
449
|
+
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: string[]): string {
|
|
453
|
+
const sessionFile = nestedRunSessionFile(run);
|
|
454
|
+
if (!sessionFile) throw new Error(`Nested run '${run.id}' does not have a persisted session file to resume from.`);
|
|
455
|
+
if (path.extname(sessionFile) !== ".jsonl") throw new Error(`Nested run '${run.id}' session file must be a .jsonl file: ${sessionFile}`);
|
|
456
|
+
const resolved = path.resolve(sessionFile);
|
|
457
|
+
if (!path.isAbsolute(sessionFile)) throw new Error(`Nested run '${run.id}' session file must be absolute: ${sessionFile}`);
|
|
458
|
+
if (!fs.existsSync(resolved)) throw new Error(`Nested run '${run.id}' session file does not exist: ${sessionFile}`);
|
|
459
|
+
const stat = fs.lstatSync(resolved);
|
|
460
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Nested run '${run.id}' session file is not a regular file: ${sessionFile}`);
|
|
461
|
+
const realSessionFile = fs.realpathSync(resolved);
|
|
462
|
+
const trustedRoots = trustedSessionRoots
|
|
463
|
+
.filter((root) => fs.existsSync(root))
|
|
464
|
+
.map((root) => fs.realpathSync(root));
|
|
465
|
+
if (!trustedRoots.some((root) => pathWithin(root, realSessionFile))) {
|
|
466
|
+
throw new Error(`Nested run '${run.id}' session file is outside trusted nested session roots: ${sessionFile}`);
|
|
467
|
+
}
|
|
468
|
+
if (!realSessionFile.split(path.sep).includes(run.id)) {
|
|
469
|
+
throw new Error(`Nested run '${run.id}' session file is not under that nested run's session directory: ${sessionFile}`);
|
|
470
|
+
}
|
|
471
|
+
return realSessionFile;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "nested" }, trustedSessionRoots: string[]): NestedResumeSourceTarget {
|
|
475
|
+
const run = match.match.run;
|
|
476
|
+
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.`);
|
|
477
|
+
const agent = nestedRunAgent(run);
|
|
478
|
+
if (!agent) throw new Error(`Could not determine child agent for nested run '${run.id}'.`);
|
|
479
|
+
const state = run.state === "complete" || run.state === "failed" || run.state === "paused" ? run.state : "failed";
|
|
480
|
+
const asyncDir = resolveNestedAsyncDir(match.match.rootRunId, run);
|
|
481
|
+
return {
|
|
482
|
+
kind: "revive",
|
|
483
|
+
source: "nested",
|
|
484
|
+
runId: run.id,
|
|
485
|
+
state,
|
|
486
|
+
agent,
|
|
487
|
+
index: 0,
|
|
488
|
+
intercomTarget: resolveSubagentIntercomTarget(run.id, agent, 0),
|
|
489
|
+
cwd: asyncDir ? path.dirname(asyncDir) : undefined,
|
|
490
|
+
sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function waitForNestedControlResult(target: ResolvedSubagentRunId & { kind: "nested" }, requestId: string, timeoutMs = 1_000) {
|
|
495
|
+
const deadline = Date.now() + timeoutMs;
|
|
496
|
+
while (Date.now() < deadline) {
|
|
497
|
+
const result = readNestedControlResults(target.match.route).find((candidate) => candidate.requestId === requestId && candidate.targetRunId === target.match.run.id);
|
|
498
|
+
if (result) return result;
|
|
499
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
500
|
+
}
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function sendNestedControlRequest(target: ResolvedSubagentRunId & { kind: "nested" }, action: "interrupt" | "resume", message?: string) {
|
|
505
|
+
const requestId = randomUUID();
|
|
506
|
+
writeNestedControlRequest(target.match.route, {
|
|
507
|
+
ts: Date.now(),
|
|
508
|
+
requestId,
|
|
509
|
+
targetRunId: target.match.run.id,
|
|
510
|
+
action,
|
|
511
|
+
...(message ? { message } : {}),
|
|
512
|
+
});
|
|
513
|
+
return waitForNestedControlResult(target, requestId);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nested" }): AgentToolResult<Details> | undefined {
|
|
517
|
+
const run = target.match.run;
|
|
518
|
+
const asyncDir = resolveNestedAsyncDir(target.match.rootRunId, run);
|
|
519
|
+
if (!asyncDir) return undefined;
|
|
520
|
+
const status = readStatus(asyncDir);
|
|
521
|
+
const pid = typeof status?.pid === "number" && status.pid > 0 ? status.pid : run.pid;
|
|
522
|
+
if (!status || status.state !== "running" || typeof pid !== "number" || pid <= 0) return undefined;
|
|
523
|
+
try {
|
|
524
|
+
process.kill(pid, ASYNC_INTERRUPT_SIGNAL);
|
|
525
|
+
return { content: [{ type: "text", text: `Interrupt requested for nested async run ${run.id}.` }], details: { mode: "management", results: [] } };
|
|
526
|
+
} catch (error) {
|
|
527
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
528
|
+
return { content: [{ type: "text", text: `Failed to interrupt nested async run ${run.id}: ${message}` }], isError: true, details: { mode: "management", results: [] } };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "nested" }): Promise<AgentToolResult<Details>> {
|
|
533
|
+
const run = target.match.run;
|
|
534
|
+
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: [] } };
|
|
535
|
+
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: [] } };
|
|
536
|
+
if (run.state === "paused") return { content: [{ type: "text", text: `Nested run ${run.id} is already paused.` }], isError: true, details: { mode: "management", results: [] } };
|
|
537
|
+
const result = await sendNestedControlRequest(target, "interrupt");
|
|
538
|
+
if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } };
|
|
539
|
+
const direct = directNestedAsyncInterrupt(target);
|
|
540
|
+
if (direct) return direct;
|
|
541
|
+
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: [] } };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function resumeLiveNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string }): Promise<AgentToolResult<Details>> {
|
|
545
|
+
const run = input.target.match.run;
|
|
546
|
+
const result = await sendNestedControlRequest(input.target, "resume", input.message);
|
|
547
|
+
if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } };
|
|
548
|
+
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: [] } };
|
|
549
|
+
}
|
|
550
|
+
|
|
395
551
|
async function resumeAsyncRun(input: {
|
|
396
552
|
params: SubagentParamsLike;
|
|
397
553
|
requestCwd: string;
|
|
@@ -408,8 +564,22 @@ async function resumeAsyncRun(input: {
|
|
|
408
564
|
}
|
|
409
565
|
|
|
410
566
|
let target: ResumeSourceTarget;
|
|
567
|
+
const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
|
|
411
568
|
try {
|
|
412
|
-
|
|
569
|
+
const requestedId = input.params.id ?? input.params.runId;
|
|
570
|
+
const resolved = requestedId ? resolveSubagentRunId(requestedId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }) : undefined;
|
|
571
|
+
if (resolved?.kind === "nested") {
|
|
572
|
+
if (resolved.match.run.state === "running" || resolved.match.run.state === "queued") {
|
|
573
|
+
return resumeLiveNestedRun({ target: resolved, message: followUp });
|
|
574
|
+
}
|
|
575
|
+
const trustedSessionRoots = [
|
|
576
|
+
...(input.deps.config.defaultSessionDir ? [path.resolve(input.deps.expandTilde(input.deps.config.defaultSessionDir))] : []),
|
|
577
|
+
...(parentSessionFile ? [input.deps.getSubagentSessionRoot(parentSessionFile)] : []),
|
|
578
|
+
];
|
|
579
|
+
target = resolveNestedResumeTarget(resolved, trustedSessionRoots);
|
|
580
|
+
} else {
|
|
581
|
+
target = resolveResumeTarget(input.params, input.deps.state);
|
|
582
|
+
}
|
|
413
583
|
} catch (error) {
|
|
414
584
|
const message = error instanceof Error ? error.message : String(error);
|
|
415
585
|
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
@@ -445,7 +615,6 @@ async function resumeAsyncRun(input: {
|
|
|
445
615
|
};
|
|
446
616
|
}
|
|
447
617
|
|
|
448
|
-
const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
|
|
449
618
|
input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager);
|
|
450
619
|
const effectiveCwd = target.cwd ?? input.requestCwd;
|
|
451
620
|
const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
|
|
@@ -501,7 +670,7 @@ async function resumeAsyncRun(input: {
|
|
|
501
670
|
|
|
502
671
|
const revivedId = result.details.asyncId ?? runId;
|
|
503
672
|
const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
|
|
504
|
-
const sourceLabel = target.source
|
|
673
|
+
const sourceLabel = target.source;
|
|
505
674
|
const lines = [
|
|
506
675
|
`Revived ${sourceLabel} subagent from ${target.runId}.`,
|
|
507
676
|
`Revived run: ${revivedId}`,
|
|
@@ -538,6 +707,7 @@ async function emitForegroundResultIntercom(input: {
|
|
|
538
707
|
mode: SubagentRunMode;
|
|
539
708
|
results: SingleResult[];
|
|
540
709
|
chainSteps?: number;
|
|
710
|
+
nestedChildren?: NestedRunSummary[];
|
|
541
711
|
}): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
|
|
542
712
|
if (!input.intercomBridge.active || !input.intercomBridge.orchestratorTarget) return null;
|
|
543
713
|
const children = input.results.flatMap((result, index) => result.detached ? [] : [{
|
|
@@ -559,7 +729,7 @@ async function emitForegroundResultIntercom(input: {
|
|
|
559
729
|
runId: input.runId,
|
|
560
730
|
mode: input.mode,
|
|
561
731
|
source: "foreground",
|
|
562
|
-
children,
|
|
732
|
+
children: attachNestedChildrenToResultChildren(input.runId, children, input.nestedChildren),
|
|
563
733
|
...(typeof input.chainSteps === "number" ? { chainSteps: input.chainSteps } : {}),
|
|
564
734
|
});
|
|
565
735
|
const delivered = await deliverSubagentResultIntercomEvent(input.pi.events, payload);
|
|
@@ -573,6 +743,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
|
|
|
573
743
|
runId: string;
|
|
574
744
|
mode: SubagentRunMode;
|
|
575
745
|
details: Details;
|
|
746
|
+
nestedChildren?: NestedRunSummary[];
|
|
576
747
|
}): Promise<{ text: string; details: Details } | null> {
|
|
577
748
|
const payload = await emitForegroundResultIntercom({
|
|
578
749
|
pi: input.pi,
|
|
@@ -581,6 +752,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
|
|
|
581
752
|
mode: input.mode,
|
|
582
753
|
results: input.details.results,
|
|
583
754
|
...(typeof input.details.totalSteps === "number" ? { chainSteps: input.details.totalSteps } : {}),
|
|
755
|
+
...(input.nestedChildren?.length ? { nestedChildren: input.nestedChildren } : {}),
|
|
584
756
|
});
|
|
585
757
|
if (!payload) return null;
|
|
586
758
|
return {
|
|
@@ -589,6 +761,36 @@ async function maybeBuildForegroundIntercomReceipt(input: {
|
|
|
589
761
|
};
|
|
590
762
|
}
|
|
591
763
|
|
|
764
|
+
function validationErrorResult(mode: Details["mode"], text: string): AgentToolResult<Details> {
|
|
765
|
+
return { content: [{ type: "text", text }], isError: true, details: { mode, results: [] } };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function validateAcceptanceForExecution(params: SubagentParamsLike): AgentToolResult<Details> | null {
|
|
769
|
+
const topLevelErrors = validateAcceptanceInput(params.acceptance);
|
|
770
|
+
if (topLevelErrors.length > 0) return validationErrorResult("single", topLevelErrors.join(" "));
|
|
771
|
+
for (const [index, task] of (params.tasks ?? []).entries()) {
|
|
772
|
+
const errors = validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`);
|
|
773
|
+
if (errors.length > 0) return validationErrorResult("parallel", errors.join(" "));
|
|
774
|
+
}
|
|
775
|
+
for (const [stepIndex, step] of (params.chain ?? []).entries()) {
|
|
776
|
+
if (isParallelStep(step)) {
|
|
777
|
+
if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
|
|
778
|
+
for (const [taskIndex, task] of step.parallel.entries()) {
|
|
779
|
+
const errors = validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`);
|
|
780
|
+
if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
|
|
781
|
+
}
|
|
782
|
+
} else if (isDynamicParallelStep(step)) {
|
|
783
|
+
if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on dynamic fanout groups; set acceptance on chain[${stepIndex}].parallel.acceptance for each materialized child.`);
|
|
784
|
+
const errors = validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`);
|
|
785
|
+
if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
|
|
786
|
+
} else {
|
|
787
|
+
const stepErrors = validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`);
|
|
788
|
+
if (stepErrors.length > 0) return validationErrorResult("chain", stepErrors.join(" "));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
|
|
592
794
|
function validateExecutionInput(
|
|
593
795
|
params: SubagentParamsLike,
|
|
594
796
|
agents: AgentConfig[],
|
|
@@ -597,6 +799,9 @@ function validateExecutionInput(
|
|
|
597
799
|
hasSingle: boolean,
|
|
598
800
|
allowClarifyTaskPrompt: boolean,
|
|
599
801
|
): AgentToolResult<Details> | null {
|
|
802
|
+
const acceptanceError = validateAcceptanceForExecution(params);
|
|
803
|
+
if (acceptanceError) return acceptanceError;
|
|
804
|
+
|
|
600
805
|
if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
|
|
601
806
|
return {
|
|
602
807
|
content: [
|
|
@@ -649,6 +854,12 @@ function validateExecutionInput(
|
|
|
649
854
|
details: { mode: "chain" as const, results: [] },
|
|
650
855
|
};
|
|
651
856
|
}
|
|
857
|
+
} else if (isDynamicParallelStep(firstStep)) {
|
|
858
|
+
return {
|
|
859
|
+
content: [{ type: "text", text: "First step in chain cannot be dynamic fanout; expand.from requires a prior structured named output" }],
|
|
860
|
+
isError: true,
|
|
861
|
+
details: { mode: "chain" as const, results: [] },
|
|
862
|
+
};
|
|
652
863
|
} else if (!(firstStep as SequentialStep).task && !params.task && !allowClarifyTaskPrompt) {
|
|
653
864
|
return {
|
|
654
865
|
content: [{ type: "text", text: "First step in chain must have a task" }],
|
|
@@ -810,6 +1021,10 @@ function collectChainSessionFiles(
|
|
|
810
1021
|
}
|
|
811
1022
|
continue;
|
|
812
1023
|
}
|
|
1024
|
+
if (isDynamicParallelStep(step)) {
|
|
1025
|
+
sessionFiles.push(undefined);
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
813
1028
|
sessionFiles.push(sessionFileForIndex(flatIndex));
|
|
814
1029
|
flatIndex++;
|
|
815
1030
|
}
|
|
@@ -828,6 +1043,15 @@ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["
|
|
|
828
1043
|
})),
|
|
829
1044
|
};
|
|
830
1045
|
}
|
|
1046
|
+
if (isDynamicParallelStep(step)) {
|
|
1047
|
+
return {
|
|
1048
|
+
...step,
|
|
1049
|
+
parallel: {
|
|
1050
|
+
...step.parallel,
|
|
1051
|
+
task: wrapForkTask(step.parallel.task ?? "{previous}"),
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
831
1055
|
const sequential = step as SequentialStep;
|
|
832
1056
|
return {
|
|
833
1057
|
...sequential,
|
|
@@ -850,6 +1074,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
850
1074
|
effectiveAsync,
|
|
851
1075
|
controlConfig,
|
|
852
1076
|
intercomBridge,
|
|
1077
|
+
nestedRoute,
|
|
853
1078
|
} = data;
|
|
854
1079
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
855
1080
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
@@ -914,6 +1139,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
914
1139
|
...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
|
|
915
1140
|
...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
|
|
916
1141
|
...(task.progress !== undefined ? { progress: task.progress } : {}),
|
|
1142
|
+
...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
|
|
917
1143
|
}));
|
|
918
1144
|
return executeAsyncChain(id, {
|
|
919
1145
|
chain: [{
|
|
@@ -939,6 +1165,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
939
1165
|
controlConfig,
|
|
940
1166
|
controlIntercomTarget,
|
|
941
1167
|
childIntercomTarget,
|
|
1168
|
+
nestedRoute,
|
|
942
1169
|
});
|
|
943
1170
|
}
|
|
944
1171
|
|
|
@@ -960,12 +1187,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
960
1187
|
sessionRoot,
|
|
961
1188
|
chainSkills,
|
|
962
1189
|
sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex),
|
|
1190
|
+
dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
|
|
963
1191
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
964
1192
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
965
1193
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
966
1194
|
controlConfig,
|
|
967
1195
|
controlIntercomTarget,
|
|
968
1196
|
childIntercomTarget,
|
|
1197
|
+
nestedRoute,
|
|
969
1198
|
});
|
|
970
1199
|
}
|
|
971
1200
|
|
|
@@ -1008,6 +1237,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1008
1237
|
controlConfig,
|
|
1009
1238
|
controlIntercomTarget,
|
|
1010
1239
|
childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
|
|
1240
|
+
nestedRoute,
|
|
1241
|
+
acceptance: params.acceptance,
|
|
1011
1242
|
});
|
|
1012
1243
|
}
|
|
1013
1244
|
|
|
@@ -1060,8 +1291,10 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1060
1291
|
childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
|
|
1061
1292
|
orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
|
|
1062
1293
|
foregroundControl,
|
|
1294
|
+
nestedRoute: foregroundControl?.nestedRoute,
|
|
1063
1295
|
chainSkills,
|
|
1064
1296
|
chainDir: params.chainDir,
|
|
1297
|
+
dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
|
|
1065
1298
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1066
1299
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1067
1300
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
@@ -1097,16 +1330,19 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1097
1330
|
sessionRoot,
|
|
1098
1331
|
chainSkills: chainResult.requestedAsync.chainSkills,
|
|
1099
1332
|
sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForIndex),
|
|
1333
|
+
dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
|
|
1100
1334
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1101
1335
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1102
1336
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1103
1337
|
controlConfig,
|
|
1104
1338
|
controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
|
|
1105
1339
|
childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
|
|
1340
|
+
nestedRoute: data.nestedRoute,
|
|
1106
1341
|
});
|
|
1107
1342
|
}
|
|
1108
1343
|
|
|
1109
1344
|
const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
|
|
1345
|
+
if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
|
|
1110
1346
|
if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
|
|
1111
1347
|
const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
|
|
1112
1348
|
? await maybeBuildForegroundIntercomReceipt({
|
|
@@ -1115,6 +1351,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1115
1351
|
runId,
|
|
1116
1352
|
mode: "chain",
|
|
1117
1353
|
details: chainDetails,
|
|
1354
|
+
...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
|
|
1118
1355
|
})
|
|
1119
1356
|
: null;
|
|
1120
1357
|
if (intercomReceipt) {
|
|
@@ -1311,10 +1548,13 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1311
1548
|
onControlEvent: input.onControlEvent,
|
|
1312
1549
|
intercomSessionName: input.childIntercomTarget?.(task.agent, index),
|
|
1313
1550
|
orchestratorIntercomTarget: input.orchestratorIntercomTarget,
|
|
1551
|
+
nestedRoute: input.foregroundControl?.nestedRoute,
|
|
1314
1552
|
modelOverride: input.modelOverrides[index],
|
|
1315
1553
|
availableModels: input.availableModels,
|
|
1316
1554
|
preferredModelProvider: input.ctx.model?.provider,
|
|
1317
1555
|
skills: effectiveSkills === false ? [] : effectiveSkills,
|
|
1556
|
+
acceptance: task.acceptance,
|
|
1557
|
+
acceptanceContext: { mode: "parallel" },
|
|
1318
1558
|
onUpdate: input.onUpdate
|
|
1319
1559
|
? (progressUpdate) => {
|
|
1320
1560
|
const stepResults = progressUpdate.details?.results || [];
|
|
@@ -1504,6 +1744,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1504
1744
|
...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
|
|
1505
1745
|
...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
|
|
1506
1746
|
...(progress !== undefined ? { progress } : {}),
|
|
1747
|
+
...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
|
|
1507
1748
|
};
|
|
1508
1749
|
});
|
|
1509
1750
|
return executeAsyncChain(id, {
|
|
@@ -1635,12 +1876,14 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1635
1876
|
};
|
|
1636
1877
|
}
|
|
1637
1878
|
|
|
1879
|
+
if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
|
|
1638
1880
|
const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
|
|
1639
1881
|
pi: deps.pi,
|
|
1640
1882
|
intercomBridge: data.intercomBridge,
|
|
1641
1883
|
runId,
|
|
1642
1884
|
mode: "parallel",
|
|
1643
1885
|
details,
|
|
1886
|
+
...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
|
|
1644
1887
|
});
|
|
1645
1888
|
if (intercomReceipt) {
|
|
1646
1889
|
return {
|
|
@@ -1869,11 +2112,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1869
2112
|
onControlEvent,
|
|
1870
2113
|
intercomSessionName: childIntercomTarget,
|
|
1871
2114
|
orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
|
|
2115
|
+
nestedRoute: foregroundControl?.nestedRoute,
|
|
1872
2116
|
index: 0,
|
|
1873
2117
|
modelOverride,
|
|
1874
2118
|
availableModels,
|
|
1875
2119
|
preferredModelProvider: currentProvider,
|
|
1876
2120
|
skills: effectiveSkills,
|
|
2121
|
+
acceptance: params.acceptance,
|
|
2122
|
+
acceptanceContext: { mode: "single" },
|
|
1877
2123
|
});
|
|
1878
2124
|
if (foregroundControl?.currentIndex === 0) {
|
|
1879
2125
|
foregroundControl.interrupt = undefined;
|
|
@@ -1914,12 +2160,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1914
2160
|
rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
|
|
1915
2161
|
|
|
1916
2162
|
if (!r.detached && !r.interrupted) {
|
|
2163
|
+
if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
|
|
1917
2164
|
const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
|
|
1918
2165
|
pi: deps.pi,
|
|
1919
2166
|
intercomBridge: data.intercomBridge,
|
|
1920
2167
|
runId,
|
|
1921
2168
|
mode: "single",
|
|
1922
2169
|
details,
|
|
2170
|
+
...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
|
|
1923
2171
|
});
|
|
1924
2172
|
if (intercomReceipt) {
|
|
1925
2173
|
return {
|
|
@@ -2013,16 +2261,41 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2013
2261
|
};
|
|
2014
2262
|
}
|
|
2015
2263
|
if (params.action === "status") {
|
|
2016
|
-
const
|
|
2017
|
-
if (
|
|
2018
|
-
|
|
2264
|
+
const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
|
|
2265
|
+
if (targetRunId) {
|
|
2266
|
+
try {
|
|
2267
|
+
const nestedScope = nestedResolutionScopeForExecutor(deps);
|
|
2268
|
+
const resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedScope });
|
|
2269
|
+
if (resolved?.kind === "foreground") {
|
|
2270
|
+
const foreground = getForegroundControl(deps.state, resolved.id);
|
|
2271
|
+
if (foreground) return foregroundStatusResult(foreground);
|
|
2272
|
+
}
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2275
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
2276
|
+
}
|
|
2277
|
+
} else {
|
|
2278
|
+
const foreground = getForegroundControl(deps.state, undefined);
|
|
2279
|
+
if (foreground) return foregroundStatusResult(foreground);
|
|
2280
|
+
}
|
|
2281
|
+
return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
|
|
2019
2282
|
}
|
|
2020
2283
|
if (params.action === "resume") {
|
|
2021
2284
|
return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
|
|
2022
2285
|
}
|
|
2023
2286
|
if (params.action === "interrupt") {
|
|
2024
2287
|
const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
|
|
2025
|
-
|
|
2288
|
+
let resolved: ResolvedSubagentRunId | undefined;
|
|
2289
|
+
if (targetRunId) {
|
|
2290
|
+
try {
|
|
2291
|
+
resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2294
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
if (resolved?.kind === "nested") return interruptNestedRun(resolved);
|
|
2298
|
+
const foreground = getForegroundControl(deps.state, resolved?.kind === "foreground" ? resolved.id : targetRunId);
|
|
2026
2299
|
if (foreground?.interrupt) {
|
|
2027
2300
|
const interrupted = foreground.interrupt();
|
|
2028
2301
|
if (interrupted) {
|
|
@@ -2039,7 +2312,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2039
2312
|
details: { mode: "management", results: [] },
|
|
2040
2313
|
};
|
|
2041
2314
|
}
|
|
2042
|
-
const asyncInterruptResult = interruptAsyncRun(deps.state, targetRunId);
|
|
2315
|
+
const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId);
|
|
2043
2316
|
if (asyncInterruptResult) return asyncInterruptResult;
|
|
2044
2317
|
return {
|
|
2045
2318
|
content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
|
|
@@ -2054,6 +2327,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2054
2327
|
details: { mode: "management" as const, results: [] },
|
|
2055
2328
|
};
|
|
2056
2329
|
}
|
|
2330
|
+
if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(params.action)) {
|
|
2331
|
+
return {
|
|
2332
|
+
content: [{ type: "text", text: `Action '${params.action}' is not available from child-safe subagent fanout mode.` }],
|
|
2333
|
+
isError: true,
|
|
2334
|
+
details: { mode: "management" as const, results: [] },
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2057
2337
|
return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd });
|
|
2058
2338
|
}
|
|
2059
2339
|
|
|
@@ -2101,6 +2381,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2101
2381
|
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
|
2102
2382
|
: discoveredAgents;
|
|
2103
2383
|
const runId = randomUUID().slice(0, 8);
|
|
2384
|
+
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
2385
|
+
const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
2386
|
+
const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
|
|
2104
2387
|
const shareEnabled = effectiveParams.share === true;
|
|
2105
2388
|
const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
|
|
2106
2389
|
const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
|
|
@@ -2182,6 +2465,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2182
2465
|
effectiveAsync,
|
|
2183
2466
|
controlConfig,
|
|
2184
2467
|
intercomBridge,
|
|
2468
|
+
nestedRoute,
|
|
2185
2469
|
};
|
|
2186
2470
|
|
|
2187
2471
|
const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
|
|
@@ -2195,6 +2479,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2195
2479
|
currentAgent: undefined,
|
|
2196
2480
|
currentIndex: undefined,
|
|
2197
2481
|
currentActivityState: undefined,
|
|
2482
|
+
nestedRoute,
|
|
2198
2483
|
interrupt: undefined,
|
|
2199
2484
|
};
|
|
2200
2485
|
if (foregroundControl) {
|
|
@@ -2202,14 +2487,92 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2202
2487
|
deps.state.lastForegroundControlId = runId;
|
|
2203
2488
|
}
|
|
2204
2489
|
|
|
2490
|
+
const writeNestedForegroundEvent = (type: "subagent.nested.started" | "subagent.nested.completed", result?: AgentToolResult<Details>): void => {
|
|
2491
|
+
if (!inheritedNestedRoute || !nestedParentAddress) return;
|
|
2492
|
+
const now = Date.now();
|
|
2493
|
+
const details = result?.details;
|
|
2494
|
+
const state = type === "subagent.nested.started"
|
|
2495
|
+
? "running"
|
|
2496
|
+
: result?.isError || details?.results.some((child) => child.exitCode !== 0)
|
|
2497
|
+
? "failed"
|
|
2498
|
+
: details?.results.some((child) => child.interrupted)
|
|
2499
|
+
? "paused"
|
|
2500
|
+
: "complete";
|
|
2501
|
+
const errorText = result?.isError
|
|
2502
|
+
? result.content.find((item) => item.type === "text")?.text
|
|
2503
|
+
: undefined;
|
|
2504
|
+
const agentsForSummary = hasTasks && effectiveParams.tasks
|
|
2505
|
+
? effectiveParams.tasks.map((task) => task.agent)
|
|
2506
|
+
: hasChain && effectiveParams.chain
|
|
2507
|
+
? effectiveParams.chain.flatMap((step) => isParallelStep(step) ? step.parallel.map((task) => task.agent) : [(step as SequentialStep).agent])
|
|
2508
|
+
: effectiveParams.agent ? [effectiveParams.agent] : [];
|
|
2509
|
+
const leafIntercomTarget = intercomBridge.active && agentsForSummary[0]
|
|
2510
|
+
? resolveSubagentIntercomTarget(runId, agentsForSummary[0], 0)
|
|
2511
|
+
: undefined;
|
|
2512
|
+
try {
|
|
2513
|
+
writeNestedEvent(inheritedNestedRoute, {
|
|
2514
|
+
type,
|
|
2515
|
+
ts: now,
|
|
2516
|
+
parentRunId: nestedParentAddress.parentRunId,
|
|
2517
|
+
parentStepIndex: nestedParentAddress.parentStepIndex,
|
|
2518
|
+
child: {
|
|
2519
|
+
id: runId,
|
|
2520
|
+
parentRunId: nestedParentAddress.parentRunId,
|
|
2521
|
+
parentStepIndex: nestedParentAddress.parentStepIndex,
|
|
2522
|
+
depth: nestedParentAddress.depth,
|
|
2523
|
+
path: nestedParentAddress.path,
|
|
2524
|
+
ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
|
|
2525
|
+
leafIntercomTarget,
|
|
2526
|
+
intercomTarget: leafIntercomTarget,
|
|
2527
|
+
ownerState: state === "running" ? "live" : "gone",
|
|
2528
|
+
mode: foregroundMode,
|
|
2529
|
+
state,
|
|
2530
|
+
agent: agentsForSummary[0],
|
|
2531
|
+
agents: agentsForSummary,
|
|
2532
|
+
startedAt: foregroundControl?.startedAt ?? now,
|
|
2533
|
+
...(state !== "running" ? { endedAt: now } : {}),
|
|
2534
|
+
lastUpdate: now,
|
|
2535
|
+
...(errorText ? { error: errorText } : {}),
|
|
2536
|
+
...(details?.results.length ? { steps: details.results.map((child) => ({
|
|
2537
|
+
agent: child.agent,
|
|
2538
|
+
status: child.interrupted ? "paused" : child.exitCode === 0 ? "complete" : "failed",
|
|
2539
|
+
...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
|
|
2540
|
+
...(child.error ? { error: child.error } : {}),
|
|
2541
|
+
})) } : {}),
|
|
2542
|
+
},
|
|
2543
|
+
});
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
console.error("Failed to emit nested foreground status event:", error);
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
let nestedForegroundStarted = false;
|
|
2205
2550
|
try {
|
|
2206
2551
|
const asyncResult = runAsyncPath(execData, deps);
|
|
2207
2552
|
if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
|
|
2208
|
-
if (
|
|
2209
|
-
|
|
2210
|
-
|
|
2553
|
+
if (foregroundControl) {
|
|
2554
|
+
writeNestedForegroundEvent("subagent.nested.started");
|
|
2555
|
+
nestedForegroundStarted = true;
|
|
2556
|
+
}
|
|
2557
|
+
if (hasChain && effectiveParams.chain) {
|
|
2558
|
+
const result = await runChainPath(execData, deps);
|
|
2559
|
+
writeNestedForegroundEvent("subagent.nested.completed", result);
|
|
2560
|
+
return withForkContext(result, effectiveParams.context);
|
|
2561
|
+
}
|
|
2562
|
+
if (hasTasks && effectiveParams.tasks) {
|
|
2563
|
+
const result = await runParallelPath(execData, deps);
|
|
2564
|
+
writeNestedForegroundEvent("subagent.nested.completed", result);
|
|
2565
|
+
return withForkContext(result, effectiveParams.context);
|
|
2566
|
+
}
|
|
2567
|
+
if (hasSingle) {
|
|
2568
|
+
const result = await runSinglePath(execData, deps);
|
|
2569
|
+
writeNestedForegroundEvent("subagent.nested.completed", result);
|
|
2570
|
+
return withForkContext(result, effectiveParams.context);
|
|
2571
|
+
}
|
|
2211
2572
|
} catch (error) {
|
|
2212
|
-
|
|
2573
|
+
const errorResult = toExecutionErrorResult(effectiveParams, error);
|
|
2574
|
+
if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
|
|
2575
|
+
return errorResult;
|
|
2213
2576
|
} finally {
|
|
2214
2577
|
if (foregroundControl) {
|
|
2215
2578
|
clearPendingForegroundControlNotices(deps.state, runId);
|