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.
- package/CHANGELOG.md +26 -5
- package/README.md +19 -11
- package/package.json +4 -8
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +46 -10
- package/src/agents/agent-management.ts +5 -0
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +30 -6
- package/src/agents/skills.ts +25 -23
- package/src/extension/config.ts +16 -0
- package/src/extension/fanout-child.ts +170 -0
- package/src/extension/index.ts +13 -25
- package/src/intercom/intercom-bridge.ts +2 -1
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +107 -7
- package/src/runs/background/async-job-tracker.ts +57 -14
- package/src/runs/background/async-resume.ts +28 -15
- package/src/runs/background/async-status.ts +60 -30
- 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 +79 -3
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +66 -18
- package/src/runs/foreground/chain-execution.ts +6 -0
- package/src/runs/foreground/execution.ts +21 -5
- package/src/runs/foreground/subagent-executor.ts +314 -18
- package/src/runs/shared/completion-guard.ts +23 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -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 +1 -0
- package/src/runs/shared/pi-args.ts +67 -5
- package/src/runs/shared/run-history.ts +12 -7
- package/src/runs/shared/single-output.ts +12 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +25 -5
- package/src/shared/artifacts.ts +2 -2
- package/src/shared/types.ts +95 -0
- package/src/shared/utils.ts +11 -1
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2017
|
-
if (
|
|
2018
|
-
|
|
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
|
-
|
|
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 (
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
-
|
|
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 =
|
|
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,
|