pi-subagents 0.23.0 → 0.23.1
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 +17 -0
- package/README.md +4 -3
- package/agents/reviewer.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +18 -1
- package/src/extension/index.ts +12 -6
- package/src/extension/schemas.ts +1 -1
- package/src/intercom/intercom-bridge.ts +4 -1
- package/src/intercom/result-intercom.ts +8 -3
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-resume.ts +57 -31
- package/src/runs/background/result-watcher.ts +3 -1
- package/src/runs/background/run-status.ts +22 -19
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +21 -7
- package/src/runs/foreground/chain-execution.ts +55 -21
- package/src/runs/foreground/execution.ts +6 -3
- package/src/runs/foreground/subagent-executor.ts +152 -20
- package/src/runs/shared/single-output.ts +21 -6
- package/src/shared/settings.ts +19 -0
- package/src/shared/types.ts +18 -0
- package/src/slash/slash-commands.ts +1 -0
- package/src/tui/render.ts +5 -3
|
@@ -7,7 +7,7 @@ import type { Message } from "@mariozechner/pi-ai";
|
|
|
7
7
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
8
8
|
import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
9
9
|
import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
|
|
10
|
-
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput } from "../shared/single-output.ts";
|
|
10
|
+
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
11
11
|
import {
|
|
12
12
|
type ActivityState,
|
|
13
13
|
type ArtifactConfig,
|
|
@@ -100,6 +100,7 @@ interface StepResult {
|
|
|
100
100
|
error?: string;
|
|
101
101
|
success: boolean;
|
|
102
102
|
skipped?: boolean;
|
|
103
|
+
sessionFile?: string;
|
|
103
104
|
intercomTarget?: string;
|
|
104
105
|
model?: string;
|
|
105
106
|
attemptedModels?: string[];
|
|
@@ -575,6 +576,7 @@ async function runSingleStep(
|
|
|
575
576
|
modelAttempts?: ModelAttempt[];
|
|
576
577
|
artifactPaths?: ArtifactPaths;
|
|
577
578
|
interrupted?: boolean;
|
|
579
|
+
sessionFile?: string;
|
|
578
580
|
intercomTarget?: string;
|
|
579
581
|
completionGuardTriggered?: boolean;
|
|
580
582
|
}> {
|
|
@@ -582,7 +584,6 @@ async function runSingleStep(
|
|
|
582
584
|
const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
|
|
583
585
|
const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
|
|
584
586
|
const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
|
|
585
|
-
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
586
587
|
|
|
587
588
|
let artifactPaths: ArtifactPaths | undefined;
|
|
588
589
|
if (ctx.artifactsDir && ctx.artifactConfig?.enabled !== false) {
|
|
@@ -604,10 +605,12 @@ async function runSingleStep(
|
|
|
604
605
|
const attemptNotes: string[] = [];
|
|
605
606
|
const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
|
|
606
607
|
let finalResult: RunPiStreamingResult | undefined;
|
|
608
|
+
let finalOutputSnapshot: SingleOutputSnapshot | undefined;
|
|
607
609
|
let completionGuardTriggeredFinal = false;
|
|
608
610
|
|
|
609
611
|
for (let index = 0; index < candidates.length; index++) {
|
|
610
612
|
const candidate = candidates[index];
|
|
613
|
+
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
611
614
|
const { args, env, tempDir } = buildPiArgs({
|
|
612
615
|
baseArgs: ["--mode", "json", "-p"],
|
|
613
616
|
task,
|
|
@@ -659,7 +662,9 @@ async function runSingleStep(
|
|
|
659
662
|
? 1
|
|
660
663
|
: hiddenError?.hasError
|
|
661
664
|
? (hiddenError.exitCode ?? 1)
|
|
662
|
-
: run.exitCode
|
|
665
|
+
: run.error && run.exitCode === 0
|
|
666
|
+
? 1
|
|
667
|
+
: run.exitCode;
|
|
663
668
|
const error = completionGuardError
|
|
664
669
|
?? (hiddenError?.hasError
|
|
665
670
|
? hiddenError.details
|
|
@@ -676,6 +681,7 @@ async function runSingleStep(
|
|
|
676
681
|
modelAttempts.push(attempt);
|
|
677
682
|
if (candidate) attemptedModels.push(candidate);
|
|
678
683
|
completionGuardTriggeredFinal = completionGuardTriggered;
|
|
684
|
+
finalOutputSnapshot = outputSnapshot;
|
|
679
685
|
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
|
|
680
686
|
if (attempt.success || completionGuardTriggered) break;
|
|
681
687
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
@@ -684,7 +690,7 @@ async function runSingleStep(
|
|
|
684
690
|
|
|
685
691
|
const rawOutput = finalResult?.finalOutput ?? "";
|
|
686
692
|
const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
|
|
687
|
-
? resolveSingleOutput(step.outputPath, rawOutput,
|
|
693
|
+
? resolveSingleOutput(step.outputPath, rawOutput, finalOutputSnapshot)
|
|
688
694
|
: { fullOutput: rawOutput };
|
|
689
695
|
const output = resolvedOutput.fullOutput;
|
|
690
696
|
const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
|
|
@@ -731,6 +737,7 @@ async function runSingleStep(
|
|
|
731
737
|
output: outputForSummary,
|
|
732
738
|
exitCode: finalResult?.exitCode ?? 1,
|
|
733
739
|
error: finalResult?.error,
|
|
740
|
+
sessionFile: step.sessionFile,
|
|
734
741
|
intercomTarget: ctx.childIntercomTarget,
|
|
735
742
|
model: finalResult?.model,
|
|
736
743
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
@@ -780,7 +787,7 @@ function markParallelGroupSetupFailure(input: {
|
|
|
780
787
|
input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
|
|
781
788
|
input.statusPayload.steps[flatTaskIndex].durationMs = 0;
|
|
782
789
|
input.statusPayload.steps[flatTaskIndex].exitCode = 1;
|
|
783
|
-
input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false });
|
|
790
|
+
input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, sessionFile: input.group.parallel[taskIndex].sessionFile });
|
|
784
791
|
}
|
|
785
792
|
input.statusPayload.currentStep = input.groupStartFlatIndex;
|
|
786
793
|
input.statusPayload.lastUpdate = input.failedAt;
|
|
@@ -916,6 +923,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
916
923
|
steps: flatSteps.map((step) => ({
|
|
917
924
|
agent: step.agent,
|
|
918
925
|
status: "pending",
|
|
926
|
+
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
919
927
|
skills: step.skills,
|
|
920
928
|
model: step.model,
|
|
921
929
|
attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
|
|
@@ -1409,6 +1417,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1409
1417
|
error: pr.error,
|
|
1410
1418
|
success: pr.exitCode === 0,
|
|
1411
1419
|
skipped: pr.skipped,
|
|
1420
|
+
sessionFile: pr.sessionFile,
|
|
1412
1421
|
intercomTarget: pr.intercomTarget,
|
|
1413
1422
|
model: pr.model,
|
|
1414
1423
|
attemptedModels: pr.attemptedModels,
|
|
@@ -1492,6 +1501,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1492
1501
|
output: singleResult.output,
|
|
1493
1502
|
error: singleResult.error,
|
|
1494
1503
|
success: singleResult.exitCode === 0,
|
|
1504
|
+
sessionFile: singleResult.sessionFile,
|
|
1495
1505
|
intercomTarget: singleResult.intercomTarget,
|
|
1496
1506
|
model: singleResult.model,
|
|
1497
1507
|
attemptedModels: singleResult.attemptedModels,
|
|
@@ -1580,9 +1590,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1580
1590
|
}
|
|
1581
1591
|
}
|
|
1582
1592
|
|
|
1593
|
+
const resultMode = config.resultMode ?? statusPayload.mode;
|
|
1583
1594
|
const agentName = flatSteps.length === 1
|
|
1584
1595
|
? flatSteps[0].agent
|
|
1585
|
-
:
|
|
1596
|
+
: resultMode === "parallel"
|
|
1597
|
+
? `parallel:${flatSteps.map((s) => s.agent).join("+")}`
|
|
1598
|
+
: `chain:${flatSteps.map((s) => s.agent).join("->")}`;
|
|
1586
1599
|
let sessionFile: string | undefined;
|
|
1587
1600
|
let shareUrl: string | undefined;
|
|
1588
1601
|
let gistUrl: string | undefined;
|
|
@@ -1667,7 +1680,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1667
1680
|
writeAtomicJson(resultPath, {
|
|
1668
1681
|
id,
|
|
1669
1682
|
agent: agentName,
|
|
1670
|
-
mode:
|
|
1683
|
+
mode: resultMode,
|
|
1671
1684
|
success: !interrupted && results.every((r) => r.success),
|
|
1672
1685
|
state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
|
|
1673
1686
|
summary: interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
|
|
@@ -1677,6 +1690,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1677
1690
|
error: r.error,
|
|
1678
1691
|
success: r.success,
|
|
1679
1692
|
skipped: r.skipped || undefined,
|
|
1693
|
+
sessionFile: r.sessionFile,
|
|
1680
1694
|
intercomTarget: r.intercomTarget,
|
|
1681
1695
|
model: r.model,
|
|
1682
1696
|
attemptedModels: r.attemptedModels,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
buildChainInstructions,
|
|
19
19
|
writeInitialProgressFile,
|
|
20
20
|
createParallelDirs,
|
|
21
|
+
suppressProgressForReadOnlyTask,
|
|
21
22
|
aggregateParallelOutputs,
|
|
22
23
|
isParallelStep,
|
|
23
24
|
type StepOverrides,
|
|
@@ -178,8 +179,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
178
179
|
} as SingleResult;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
const behavior = input.parallelBehaviors[taskIndex]!;
|
|
182
182
|
const taskTemplate = input.parallelTemplates[taskIndex] ?? "{previous}";
|
|
183
|
+
const behavior = suppressProgressForReadOnlyTask(input.parallelBehaviors[taskIndex]!, taskTemplate, input.originalTask);
|
|
183
184
|
const templateHasPrevious = taskTemplate.includes("{previous}");
|
|
184
185
|
const { prefix, suffix } = buildChainInstructions(
|
|
185
186
|
behavior,
|
|
@@ -537,7 +538,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
537
538
|
|
|
538
539
|
try {
|
|
539
540
|
const agentNames = step.parallel.map((task) => task.agent);
|
|
540
|
-
const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
|
|
541
|
+
const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
|
|
542
|
+
.map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
|
|
541
543
|
for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
|
|
542
544
|
const behavior = parallelBehaviors[taskIndex]!;
|
|
543
545
|
const outputPath = typeof behavior.output === "string"
|
|
@@ -616,6 +618,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
616
618
|
}),
|
|
617
619
|
};
|
|
618
620
|
}
|
|
621
|
+
const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
|
|
622
|
+
const detached = detachedIndexInStep >= 0 ? parallelResults[detachedIndexInStep] : undefined;
|
|
623
|
+
if (detached) {
|
|
624
|
+
return {
|
|
625
|
+
content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
626
|
+
details: buildChainExecutionDetails({
|
|
627
|
+
results,
|
|
628
|
+
includeProgress,
|
|
629
|
+
allProgress,
|
|
630
|
+
allArtifactPaths,
|
|
631
|
+
artifactsDir,
|
|
632
|
+
chainAgents,
|
|
633
|
+
totalSteps,
|
|
634
|
+
currentStepIndex: stepIndex,
|
|
635
|
+
}),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
619
638
|
|
|
620
639
|
const failures = parallelResults
|
|
621
640
|
.map((result, originalIndex) => ({ ...result, originalIndex }))
|
|
@@ -695,7 +714,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
695
714
|
? tuiOverride.skills
|
|
696
715
|
: normalizeSkillInput(seqStep.skill),
|
|
697
716
|
};
|
|
698
|
-
const behavior = resolveStepBehavior(agentConfig, stepOverride, chainSkills);
|
|
717
|
+
const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfig, stepOverride, chainSkills), stepTemplate, originalTask);
|
|
699
718
|
|
|
700
719
|
const isFirstProgress = behavior.progress && !progressCreated;
|
|
701
720
|
if (isFirstProgress) {
|
|
@@ -822,24 +841,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
822
841
|
if (r.progress) allProgress.push(r.progress);
|
|
823
842
|
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
824
843
|
|
|
825
|
-
if (behavior.output && r.exitCode === 0) {
|
|
826
|
-
try {
|
|
827
|
-
const expectedPath = path.isAbsolute(behavior.output)
|
|
828
|
-
? behavior.output
|
|
829
|
-
: path.join(chainDir, behavior.output);
|
|
830
|
-
if (!fs.existsSync(expectedPath)) {
|
|
831
|
-
const dirFiles = fs.readdirSync(chainDir);
|
|
832
|
-
const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
|
|
833
|
-
const warning = mdFiles.length > 0
|
|
834
|
-
? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
|
|
835
|
-
: `Agent did not create expected output file: ${behavior.output}`;
|
|
836
|
-
r.error = r.error ? `${r.error}\n${warning}` : warning;
|
|
837
|
-
}
|
|
838
|
-
} catch {
|
|
839
|
-
// Ignore validation errors - this is just a diagnostic
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
844
|
if (r.interrupted) {
|
|
844
845
|
return {
|
|
845
846
|
content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
|
|
@@ -855,6 +856,21 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
855
856
|
}),
|
|
856
857
|
};
|
|
857
858
|
}
|
|
859
|
+
if (r.detached) {
|
|
860
|
+
return {
|
|
861
|
+
content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${r.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
862
|
+
details: buildChainExecutionDetails({
|
|
863
|
+
results,
|
|
864
|
+
includeProgress,
|
|
865
|
+
allProgress,
|
|
866
|
+
allArtifactPaths,
|
|
867
|
+
artifactsDir,
|
|
868
|
+
chainAgents,
|
|
869
|
+
totalSteps,
|
|
870
|
+
currentStepIndex: stepIndex,
|
|
871
|
+
}),
|
|
872
|
+
};
|
|
873
|
+
}
|
|
858
874
|
|
|
859
875
|
if (r.exitCode !== 0) {
|
|
860
876
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
@@ -877,6 +893,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
877
893
|
};
|
|
878
894
|
}
|
|
879
895
|
|
|
896
|
+
if (behavior.output) {
|
|
897
|
+
try {
|
|
898
|
+
const expectedPath = path.isAbsolute(behavior.output)
|
|
899
|
+
? behavior.output
|
|
900
|
+
: path.join(chainDir, behavior.output);
|
|
901
|
+
if (!fs.existsSync(expectedPath)) {
|
|
902
|
+
const dirFiles = fs.readdirSync(chainDir);
|
|
903
|
+
const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
|
|
904
|
+
const warning = mdFiles.length > 0
|
|
905
|
+
? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
|
|
906
|
+
: `Agent did not create expected output file: ${behavior.output}`;
|
|
907
|
+
r.error = r.error ? `${r.error}\n${warning}` : warning;
|
|
908
|
+
}
|
|
909
|
+
} catch {
|
|
910
|
+
// Ignore validation errors; this diagnostic should not mask successful chain output.
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
880
914
|
prev = getSingleResultOutput(r);
|
|
881
915
|
}
|
|
882
916
|
}
|
|
@@ -421,7 +421,7 @@ async function runSingleAttempt(
|
|
|
421
421
|
const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
|
|
422
422
|
? evt.args as Record<string, unknown>
|
|
423
423
|
: {};
|
|
424
|
-
if (options.allowIntercomDetach && evt.toolName === "intercom") {
|
|
424
|
+
if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
|
|
425
425
|
intercomStarted = true;
|
|
426
426
|
}
|
|
427
427
|
progress.toolCount++;
|
|
@@ -633,7 +633,10 @@ async function runSingleAttempt(
|
|
|
633
633
|
return result;
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
if (exitCode === 0
|
|
636
|
+
if (result.error && result.exitCode === 0) {
|
|
637
|
+
result.exitCode = 1;
|
|
638
|
+
}
|
|
639
|
+
if (result.exitCode === 0 && !result.error) {
|
|
637
640
|
const errInfo = detectSubagentError(result.messages);
|
|
638
641
|
if (errInfo.hasError) {
|
|
639
642
|
result.exitCode = errInfo.exitCode ?? 1;
|
|
@@ -746,7 +749,6 @@ export async function runSync(
|
|
|
746
749
|
|
|
747
750
|
const shareEnabled = options.share === true;
|
|
748
751
|
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
749
|
-
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
750
752
|
const skillNames = options.skills ?? agent.skills ?? [];
|
|
751
753
|
const skillCwd = options.cwd ?? runtimeCwd;
|
|
752
754
|
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
|
|
@@ -797,6 +799,7 @@ export async function runSync(
|
|
|
797
799
|
for (let i = 0; i < modelsToTry.length; i++) {
|
|
798
800
|
const candidate = modelsToTry[i];
|
|
799
801
|
if (candidate) attemptedModels.push(candidate);
|
|
802
|
+
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
800
803
|
const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
|
|
801
804
|
sessionEnabled,
|
|
802
805
|
systemPrompt,
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
getStepAgents,
|
|
23
23
|
isParallelStep,
|
|
24
24
|
resolveStepBehavior,
|
|
25
|
+
suppressProgressForReadOnlyTask,
|
|
26
|
+
taskDisallowsFileUpdates,
|
|
25
27
|
type ChainStep,
|
|
26
28
|
type ResolvedStepBehavior,
|
|
27
29
|
type SequentialStep,
|
|
@@ -206,6 +208,115 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
|
|
|
206
208
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; results: SingleResult[] }): void {
|
|
212
|
+
state.foregroundRuns ??= new Map();
|
|
213
|
+
state.foregroundRuns.set(input.runId, {
|
|
214
|
+
runId: input.runId,
|
|
215
|
+
mode: input.mode,
|
|
216
|
+
cwd: input.cwd,
|
|
217
|
+
updatedAt: Date.now(),
|
|
218
|
+
children: input.results.map((result, index) => ({
|
|
219
|
+
agent: result.agent,
|
|
220
|
+
index,
|
|
221
|
+
status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
|
|
222
|
+
...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
|
|
223
|
+
})),
|
|
224
|
+
});
|
|
225
|
+
while (state.foregroundRuns.size > 50) {
|
|
226
|
+
const oldest = [...state.foregroundRuns.values()].sort((left, right) => left.updatedAt - right.updatedAt)[0];
|
|
227
|
+
if (!oldest) break;
|
|
228
|
+
state.foregroundRuns.delete(oldest.runId);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; intercomTarget: string; cwd: string; sessionFile: string } | undefined {
|
|
233
|
+
const requested = (params.id ?? params.runId)?.trim();
|
|
234
|
+
if (!requested || !state.foregroundRuns?.size) return undefined;
|
|
235
|
+
const direct = state.foregroundRuns.get(requested);
|
|
236
|
+
const matches = direct ? [direct] : [...state.foregroundRuns.values()].filter((run) => run.runId.startsWith(requested));
|
|
237
|
+
if (matches.length === 0) return undefined;
|
|
238
|
+
if (matches.length > 1) throw new Error(`Ambiguous foreground run id prefix '${requested}' matched: ${matches.map((run) => run.runId).join(", ")}. Provide a longer id.`);
|
|
239
|
+
const run = matches[0]!;
|
|
240
|
+
if (run.children.length > 1 && params.index === undefined) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Provide index to choose one.`);
|
|
241
|
+
const index = params.index ?? 0;
|
|
242
|
+
if (!Number.isInteger(index)) throw new Error(`Foreground run '${run.runId}' index must be an integer.`);
|
|
243
|
+
if (index < 0 || index >= run.children.length) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Index ${index} is out of range.`);
|
|
244
|
+
const child = run.children[index]!;
|
|
245
|
+
if (child.status === "detached") throw new Error(`Foreground run '${run.runId}' child ${index} is detached for intercom coordination and cannot be revived safely from the remembered foreground state. Reply to the supervisor request first; after the child exits, start a fresh follow-up if needed.`);
|
|
246
|
+
if (!child.sessionFile) throw new Error(`Foreground run '${run.runId}' child ${index} does not have a persisted session file to resume from.`);
|
|
247
|
+
if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
|
|
248
|
+
const sessionFile = path.resolve(child.sessionFile);
|
|
249
|
+
if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
|
|
250
|
+
return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, intercomTarget: resolveSubagentIntercomTarget(run.runId, child.agent, index), cwd: run.cwd, sessionFile };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
|
|
254
|
+
type ForegroundResumeSourceTarget = NonNullable<ReturnType<typeof resolveForegroundResumeTarget>> & { kind: "revive"; source: "foreground" };
|
|
255
|
+
type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget;
|
|
256
|
+
|
|
257
|
+
function isAsyncRunNotFound(error: unknown): boolean {
|
|
258
|
+
return error instanceof Error && error.message.startsWith("Async run not found.");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isResumeAmbiguity(error: unknown): boolean {
|
|
262
|
+
return error instanceof Error && /Ambiguous .*run id prefix/.test(error.message);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function resumeTargetExact(target: { runId: string } | undefined, requested: string): boolean {
|
|
266
|
+
return target?.runId === requested;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function escapeRegExp(value: string): string {
|
|
270
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isExactResumeError(error: unknown, source: "async" | "foreground", requested: string): boolean {
|
|
274
|
+
if (!(error instanceof Error) || !requested) return false;
|
|
275
|
+
return new RegExp(`\\b${source} run '${escapeRegExp(requested)}'`, "i").test(error.message);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState): ResumeSourceTarget {
|
|
279
|
+
const requested = (params.id ?? params.runId)?.trim() ?? "";
|
|
280
|
+
let foregroundTarget: ForegroundResumeSourceTarget | undefined;
|
|
281
|
+
let foregroundError: unknown;
|
|
282
|
+
let asyncTarget: AsyncResumeSourceTarget | undefined;
|
|
283
|
+
let asyncError: unknown;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const target = resolveForegroundResumeTarget(params, state);
|
|
287
|
+
if (target) foregroundTarget = { kind: "revive", source: "foreground", ...target };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
foregroundError = error;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params) };
|
|
293
|
+
} catch (error) {
|
|
294
|
+
asyncError = error;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (foregroundTarget && asyncTarget) {
|
|
298
|
+
const foregroundExact = resumeTargetExact(foregroundTarget, requested);
|
|
299
|
+
const asyncExact = resumeTargetExact(asyncTarget, requested);
|
|
300
|
+
if (foregroundExact && !asyncExact) return foregroundTarget;
|
|
301
|
+
if (asyncExact && !foregroundExact) return asyncTarget;
|
|
302
|
+
throw new Error(`Resume id '${requested}' is ambiguous between foreground run '${foregroundTarget.runId}' and async run '${asyncTarget.runId}'. Provide a full run id.`);
|
|
303
|
+
}
|
|
304
|
+
if (foregroundTarget) {
|
|
305
|
+
if (isExactResumeError(asyncError, "async", requested)) throw asyncError;
|
|
306
|
+
if (isResumeAmbiguity(asyncError) && !resumeTargetExact(foregroundTarget, requested)) throw asyncError;
|
|
307
|
+
return foregroundTarget;
|
|
308
|
+
}
|
|
309
|
+
if (asyncTarget) {
|
|
310
|
+
if (isExactResumeError(foregroundError, "foreground", requested)) throw foregroundError;
|
|
311
|
+
if (isResumeAmbiguity(foregroundError) && !resumeTargetExact(asyncTarget, requested)) throw foregroundError;
|
|
312
|
+
return asyncTarget;
|
|
313
|
+
}
|
|
314
|
+
if (foregroundError && !isAsyncRunNotFound(asyncError)) throw foregroundError;
|
|
315
|
+
if (foregroundError) throw foregroundError;
|
|
316
|
+
if (asyncError) throw asyncError;
|
|
317
|
+
throw new Error("Run not found. Provide id or runId.");
|
|
318
|
+
}
|
|
319
|
+
|
|
209
320
|
function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
|
|
210
321
|
if (runId) {
|
|
211
322
|
const direct = state.asyncJobs.get(runId);
|
|
@@ -296,9 +407,9 @@ async function resumeAsyncRun(input: {
|
|
|
296
407
|
};
|
|
297
408
|
}
|
|
298
409
|
|
|
299
|
-
let target:
|
|
410
|
+
let target: ResumeSourceTarget;
|
|
300
411
|
try {
|
|
301
|
-
target =
|
|
412
|
+
target = resolveResumeTarget(input.params, input.deps.state);
|
|
302
413
|
} catch (error) {
|
|
303
414
|
const message = error instanceof Error ? error.message : String(error);
|
|
304
415
|
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
@@ -352,7 +463,7 @@ async function resumeAsyncRun(input: {
|
|
|
352
463
|
const agentConfig = agents.find((agent) => agent.name === target.agent);
|
|
353
464
|
if (!agentConfig) {
|
|
354
465
|
return {
|
|
355
|
-
content: [{ type: "text", text: `Unknown agent for
|
|
466
|
+
content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
|
|
356
467
|
isError: true,
|
|
357
468
|
details: { mode: "management", results: [] },
|
|
358
469
|
};
|
|
@@ -390,8 +501,9 @@ async function resumeAsyncRun(input: {
|
|
|
390
501
|
|
|
391
502
|
const revivedId = result.details.asyncId ?? runId;
|
|
392
503
|
const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
|
|
504
|
+
const sourceLabel = target.source === "foreground" ? "foreground" : "async";
|
|
393
505
|
const lines = [
|
|
394
|
-
`Revived
|
|
506
|
+
`Revived ${sourceLabel} subagent from ${target.runId}.`,
|
|
395
507
|
`Revived run: ${revivedId}`,
|
|
396
508
|
`Agent: ${target.agent}`,
|
|
397
509
|
`Session: ${target.sessionFile}`,
|
|
@@ -836,6 +948,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
836
948
|
const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
|
|
837
949
|
return executeAsyncChain(id, {
|
|
838
950
|
chain,
|
|
951
|
+
task: params.task,
|
|
839
952
|
agents,
|
|
840
953
|
ctx: asyncCtx,
|
|
841
954
|
availableModels,
|
|
@@ -972,6 +1085,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
972
1085
|
const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
|
|
973
1086
|
return executeAsyncChain(id, {
|
|
974
1087
|
chain: asyncChain,
|
|
1088
|
+
task: params.task,
|
|
975
1089
|
agents,
|
|
976
1090
|
ctx: asyncCtx,
|
|
977
1091
|
availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
|
|
@@ -992,8 +1106,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
992
1106
|
});
|
|
993
1107
|
}
|
|
994
1108
|
|
|
995
|
-
const chainDetails = chainResult.details ? compactForegroundDetails(chainResult.details) : undefined;
|
|
996
|
-
|
|
1109
|
+
const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
|
|
1110
|
+
if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
|
|
1111
|
+
const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
|
|
997
1112
|
? await maybeBuildForegroundIntercomReceipt({
|
|
998
1113
|
pi: deps.pi,
|
|
999
1114
|
intercomBridge: data.intercomBridge,
|
|
@@ -1010,7 +1125,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1010
1125
|
};
|
|
1011
1126
|
}
|
|
1012
1127
|
|
|
1013
|
-
return chainResult;
|
|
1128
|
+
return chainDetails ? { ...chainResult, details: chainDetails } : chainResult;
|
|
1014
1129
|
}
|
|
1015
1130
|
|
|
1016
1131
|
interface ForegroundParallelRunInput {
|
|
@@ -1376,17 +1491,21 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1376
1491
|
currentSessionId: deps.state.currentSessionId!,
|
|
1377
1492
|
currentModelProvider: ctx.model?.provider,
|
|
1378
1493
|
};
|
|
1379
|
-
const parallelTasks = tasks.map((t, i) =>
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1494
|
+
const parallelTasks = tasks.map((t, i) => {
|
|
1495
|
+
const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
|
|
1496
|
+
const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
|
|
1497
|
+
return {
|
|
1498
|
+
agent: t.agent,
|
|
1499
|
+
task: taskText,
|
|
1500
|
+
cwd: t.cwd,
|
|
1501
|
+
...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
|
|
1502
|
+
...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
|
|
1503
|
+
...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
|
|
1504
|
+
...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
|
|
1505
|
+
...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
|
|
1506
|
+
...(progress !== undefined ? { progress } : {}),
|
|
1507
|
+
};
|
|
1508
|
+
});
|
|
1390
1509
|
return executeAsyncChain(id, {
|
|
1391
1510
|
chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
|
|
1392
1511
|
resultMode: "parallel",
|
|
@@ -1411,7 +1530,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1411
1530
|
}
|
|
1412
1531
|
}
|
|
1413
1532
|
|
|
1414
|
-
const behaviors = agentConfigs.map((config, index) => resolveStepBehavior(config, behaviorOverrides[index]!));
|
|
1533
|
+
const behaviors = agentConfigs.map((config, index) => suppressProgressForReadOnlyTask(resolveStepBehavior(config, behaviorOverrides[index]!), taskTexts[index]));
|
|
1415
1534
|
const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
|
|
1416
1535
|
const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
1417
1536
|
const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
@@ -1495,16 +1614,26 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1495
1614
|
const interrupted = results.find((result) => result.interrupted);
|
|
1496
1615
|
const details = compactForegroundDetails({
|
|
1497
1616
|
mode: "parallel",
|
|
1617
|
+
runId,
|
|
1498
1618
|
results,
|
|
1499
1619
|
progress: params.includeProgress ? allProgress : undefined,
|
|
1500
1620
|
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1501
1621
|
});
|
|
1622
|
+
rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
|
|
1502
1623
|
if (interrupted) {
|
|
1503
1624
|
return {
|
|
1504
1625
|
content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
|
|
1505
1626
|
details,
|
|
1506
1627
|
};
|
|
1507
1628
|
}
|
|
1629
|
+
const detachedIndex = results.findIndex((result) => result.detached);
|
|
1630
|
+
const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
|
|
1631
|
+
if (detached) {
|
|
1632
|
+
return {
|
|
1633
|
+
content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
1634
|
+
details,
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1508
1637
|
|
|
1509
1638
|
const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
|
|
1510
1639
|
pi: deps.pi,
|
|
@@ -1776,11 +1905,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1776
1905
|
});
|
|
1777
1906
|
const details = compactForegroundDetails({
|
|
1778
1907
|
mode: "single",
|
|
1908
|
+
runId,
|
|
1779
1909
|
results: [r],
|
|
1780
1910
|
progress: params.includeProgress ? allProgress : undefined,
|
|
1781
1911
|
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1782
1912
|
truncation: r.truncation,
|
|
1783
1913
|
});
|
|
1914
|
+
rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
|
|
1784
1915
|
|
|
1785
1916
|
if (!r.detached && !r.interrupted) {
|
|
1786
1917
|
const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
|
|
@@ -1801,7 +1932,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1801
1932
|
|
|
1802
1933
|
if (r.detached) {
|
|
1803
1934
|
return {
|
|
1804
|
-
content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}
|
|
1935
|
+
content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
1805
1936
|
details,
|
|
1806
1937
|
};
|
|
1807
1938
|
}
|
|
@@ -1842,6 +1973,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1842
1973
|
ctx: ExtensionContext,
|
|
1843
1974
|
): Promise<AgentToolResult<Details>> => {
|
|
1844
1975
|
deps.state.baseCwd = ctx.cwd;
|
|
1976
|
+
deps.state.foregroundRuns ??= new Map();
|
|
1845
1977
|
deps.state.foregroundControls ??= new Map();
|
|
1846
1978
|
deps.state.lastForegroundControlId ??= null;
|
|
1847
1979
|
const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
|
|
@@ -69,6 +69,7 @@ export function captureSingleOutputSnapshot(outputPath: string | undefined): Sin
|
|
|
69
69
|
const stat = fs.statSync(outputPath);
|
|
70
70
|
return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
71
71
|
} catch {
|
|
72
|
+
// The snapshot is advisory; resolveSingleOutput reports concrete read/write failures.
|
|
72
73
|
return { exists: false };
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -94,18 +95,32 @@ export function resolveSingleOutput(
|
|
|
94
95
|
): { fullOutput: string; savedPath?: string; saveError?: string } {
|
|
95
96
|
if (!outputPath) return { fullOutput: fallbackOutput };
|
|
96
97
|
|
|
98
|
+
let changedSinceStart = false;
|
|
97
99
|
try {
|
|
98
100
|
const stat = fs.statSync(outputPath);
|
|
99
|
-
|
|
101
|
+
changedSinceStart = !beforeRun?.exists
|
|
100
102
|
|| stat.mtimeMs !== beforeRun.mtimeMs
|
|
101
103
|
|| stat.size !== beforeRun.size;
|
|
102
|
-
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
106
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
103
107
|
return {
|
|
104
|
-
fullOutput:
|
|
105
|
-
|
|
108
|
+
fullOutput: fallbackOutput,
|
|
109
|
+
saveError: `Failed to inspect output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
106
110
|
};
|
|
107
111
|
}
|
|
108
|
-
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (changedSinceStart) {
|
|
115
|
+
try {
|
|
116
|
+
return { fullOutput: fs.readFileSync(outputPath, "utf-8"), savedPath: outputPath };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
fullOutput: fallbackOutput,
|
|
120
|
+
saveError: `Failed to read changed output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
109
124
|
|
|
110
125
|
const save = persistSingleOutput(outputPath, fallbackOutput);
|
|
111
126
|
if (save.savedPath) return { fullOutput: fallbackOutput, savedPath: save.savedPath };
|
|
@@ -132,7 +147,7 @@ export function finalizeSingleOutput(params: {
|
|
|
132
147
|
return { displayOutput, savedPath: params.savedPath, outputReference };
|
|
133
148
|
}
|
|
134
149
|
if (params.exitCode === 0 && params.saveError && params.outputPath) {
|
|
135
|
-
displayOutput += `\n\
|
|
150
|
+
displayOutput += `\n\nOutput file error: ${params.outputPath}\n${params.saveError}`;
|
|
136
151
|
return { displayOutput, saveError: params.saveError };
|
|
137
152
|
}
|
|
138
153
|
return { displayOutput };
|