pi-subagents 0.25.0 → 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 +21 -0
- package/README.md +129 -17
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/skills/pi-subagents/SKILL.md +32 -17
- 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 +1 -0
- package/src/extension/index.ts +1 -0
- package/src/extension/schemas.ts +138 -5
- package/src/runs/background/async-execution.ts +84 -6
- package/src/runs/background/async-status.ts +11 -1
- package/src/runs/background/run-status.ts +10 -1
- package/src/runs/background/subagent-runner.ts +600 -31
- package/src/runs/foreground/chain-execution.ts +325 -118
- package/src/runs/foreground/execution.ts +222 -10
- package/src/runs/foreground/subagent-executor.ts +67 -0
- 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/parallel-utils.ts +31 -1
- package/src/runs/shared/pi-args.ts +11 -0
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
- 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 +250 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +162 -34
|
@@ -8,16 +8,21 @@ import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
|
8
8
|
import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
|
|
9
9
|
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
10
10
|
import {
|
|
11
|
+
type AcceptanceFinalizationTurn,
|
|
12
|
+
type AcceptanceLedger,
|
|
11
13
|
type ActivityState,
|
|
12
14
|
type ArtifactConfig,
|
|
13
15
|
type ArtifactPaths,
|
|
14
16
|
type AsyncParallelGroupStatus,
|
|
15
17
|
type AsyncStatus,
|
|
18
|
+
type ChainOutputMap,
|
|
16
19
|
type ModelAttempt,
|
|
17
20
|
type NestedRouteInfo,
|
|
18
21
|
type ResolvedControlConfig,
|
|
19
22
|
type SubagentRunMode,
|
|
23
|
+
type TokenUsage,
|
|
20
24
|
type Usage,
|
|
25
|
+
type WorkflowGraphSnapshot,
|
|
21
26
|
DEFAULT_MAX_OUTPUT,
|
|
22
27
|
type MaxOutputConfig,
|
|
23
28
|
truncateOutput,
|
|
@@ -34,6 +39,7 @@ import {
|
|
|
34
39
|
import {
|
|
35
40
|
type RunnerSubagentStep as SubagentStep,
|
|
36
41
|
type RunnerStep,
|
|
42
|
+
isDynamicRunnerGroup,
|
|
37
43
|
isParallelGroup,
|
|
38
44
|
flattenSteps,
|
|
39
45
|
mapConcurrent,
|
|
@@ -41,11 +47,14 @@ import {
|
|
|
41
47
|
MAX_PARALLEL_CONCURRENCY,
|
|
42
48
|
} from "../shared/parallel-utils.ts";
|
|
43
49
|
import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
50
|
+
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
51
|
+
import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
|
|
52
|
+
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
44
53
|
import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
|
|
45
54
|
import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
46
55
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
47
56
|
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
|
|
48
|
-
import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
57
|
+
import { evaluateCompletionMutationGuard, resolveCompletionPolicy } from "../shared/completion-guard.ts";
|
|
49
58
|
import {
|
|
50
59
|
createMutatingFailureState,
|
|
51
60
|
didMutatingToolFail,
|
|
@@ -58,7 +67,6 @@ import {
|
|
|
58
67
|
summarizeRecentMutatingFailures,
|
|
59
68
|
} from "../shared/long-running-guard.ts";
|
|
60
69
|
import { parseSessionTokens } from "../../shared/session-tokens.ts";
|
|
61
|
-
import type { TokenUsage } from "../../shared/types.ts";
|
|
62
70
|
import {
|
|
63
71
|
cleanupWorktrees,
|
|
64
72
|
createWorktrees,
|
|
@@ -70,6 +78,20 @@ import {
|
|
|
70
78
|
} from "../shared/worktree.ts";
|
|
71
79
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
72
80
|
import { writeInitialProgressFile } from "../../shared/settings.ts";
|
|
81
|
+
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
82
|
+
import {
|
|
83
|
+
acceptanceFailureMessage,
|
|
84
|
+
acceptanceSelfReviewConfig,
|
|
85
|
+
attachFinalizationToLedger,
|
|
86
|
+
buildFinalizationProcessFailureLedger,
|
|
87
|
+
createFinalizationProcessFailureTurn,
|
|
88
|
+
createFinalizationTurn,
|
|
89
|
+
evaluateAcceptance,
|
|
90
|
+
formatAcceptanceFinalizationPrompt,
|
|
91
|
+
formatAcceptancePrompt,
|
|
92
|
+
shouldRunAcceptanceFinalization,
|
|
93
|
+
stripAcceptanceReport,
|
|
94
|
+
} from "../shared/acceptance.ts";
|
|
73
95
|
|
|
74
96
|
interface SubagentRunConfig {
|
|
75
97
|
id: string;
|
|
@@ -94,6 +116,8 @@ interface SubagentRunConfig {
|
|
|
94
116
|
controlIntercomTarget?: string;
|
|
95
117
|
childIntercomTargets?: Array<string | undefined>;
|
|
96
118
|
resultMode?: SubagentRunMode;
|
|
119
|
+
dynamicFanoutMaxItems?: number;
|
|
120
|
+
workflowGraph?: WorkflowGraphSnapshot;
|
|
97
121
|
nestedRoute?: NestedRouteInfo;
|
|
98
122
|
nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
|
|
99
123
|
}
|
|
@@ -103,6 +127,7 @@ interface StepResult {
|
|
|
103
127
|
output: string;
|
|
104
128
|
error?: string;
|
|
105
129
|
success: boolean;
|
|
130
|
+
exitCode?: number | null;
|
|
106
131
|
skipped?: boolean;
|
|
107
132
|
sessionFile?: string;
|
|
108
133
|
intercomTarget?: string;
|
|
@@ -111,6 +136,10 @@ interface StepResult {
|
|
|
111
136
|
modelAttempts?: ModelAttempt[];
|
|
112
137
|
artifactPaths?: ArtifactPaths;
|
|
113
138
|
truncated?: boolean;
|
|
139
|
+
structuredOutput?: unknown;
|
|
140
|
+
structuredOutputPath?: string;
|
|
141
|
+
structuredOutputSchemaPath?: string;
|
|
142
|
+
acceptance?: AcceptanceLedger;
|
|
114
143
|
}
|
|
115
144
|
|
|
116
145
|
const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
@@ -543,6 +572,7 @@ function writeRunLog(
|
|
|
543
572
|
/** Context for running a single step */
|
|
544
573
|
interface SingleStepContext {
|
|
545
574
|
previousOutput: string;
|
|
575
|
+
outputs?: ChainOutputMap;
|
|
546
576
|
placeholder: string;
|
|
547
577
|
cwd: string;
|
|
548
578
|
sessionEnabled: boolean;
|
|
@@ -580,9 +610,22 @@ async function runSingleStep(
|
|
|
580
610
|
sessionFile?: string;
|
|
581
611
|
intercomTarget?: string;
|
|
582
612
|
completionGuardTriggered?: boolean;
|
|
613
|
+
structuredOutput?: unknown;
|
|
614
|
+
structuredOutputPath?: string;
|
|
615
|
+
structuredOutputSchemaPath?: string;
|
|
616
|
+
acceptance?: AcceptanceLedger;
|
|
583
617
|
}> {
|
|
618
|
+
const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
|
|
619
|
+
? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
|
|
620
|
+
: undefined);
|
|
584
621
|
const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
585
|
-
|
|
622
|
+
let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
|
|
623
|
+
task = resolveOutputReferences(task, ctx.outputs ?? {});
|
|
624
|
+
const taskForCompletionGuard = task;
|
|
625
|
+
if (step.effectiveAcceptance) {
|
|
626
|
+
const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
|
|
627
|
+
if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
|
|
628
|
+
}
|
|
586
629
|
const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
|
|
587
630
|
const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
|
|
588
631
|
|
|
@@ -613,6 +656,13 @@ async function runSingleStep(
|
|
|
613
656
|
const candidate = candidates[index];
|
|
614
657
|
ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
|
|
615
658
|
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
659
|
+
if (effectiveStructuredOutput) {
|
|
660
|
+
try {
|
|
661
|
+
if (fs.existsSync(effectiveStructuredOutput.outputPath)) fs.unlinkSync(effectiveStructuredOutput.outputPath);
|
|
662
|
+
} catch {
|
|
663
|
+
// Missing/stale structured-output files are handled after the child exits.
|
|
664
|
+
}
|
|
665
|
+
}
|
|
616
666
|
const { args, env, tempDir } = buildPiArgs({
|
|
617
667
|
baseArgs: ["--mode", "json", "-p"],
|
|
618
668
|
task,
|
|
@@ -638,6 +688,7 @@ async function runSingleStep(
|
|
|
638
688
|
parentControlInbox: ctx.nestedRoute?.controlInbox,
|
|
639
689
|
parentRootRunId: ctx.nestedRoute?.rootRunId,
|
|
640
690
|
parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
|
|
691
|
+
structuredOutput: effectiveStructuredOutput,
|
|
641
692
|
});
|
|
642
693
|
const run = await runPiStreaming(
|
|
643
694
|
args,
|
|
@@ -654,10 +705,29 @@ async function runSingleStep(
|
|
|
654
705
|
cleanupTempDir(tempDir);
|
|
655
706
|
|
|
656
707
|
const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
|
|
657
|
-
|
|
708
|
+
let structuredOutput: unknown;
|
|
709
|
+
let structuredError: string | undefined;
|
|
710
|
+
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError) {
|
|
711
|
+
const structured = readStructuredOutput({
|
|
712
|
+
schema: effectiveStructuredOutput.schema,
|
|
713
|
+
schemaPath: effectiveStructuredOutput.schemaPath,
|
|
714
|
+
outputPath: effectiveStructuredOutput.outputPath,
|
|
715
|
+
});
|
|
716
|
+
if (structured.error) structuredError = structured.error;
|
|
717
|
+
else structuredOutput = structured.value;
|
|
718
|
+
}
|
|
719
|
+
const completionPolicy = resolveCompletionPolicy({
|
|
720
|
+
agent: step.agent,
|
|
721
|
+
task: taskForCompletionGuard,
|
|
722
|
+
completionGuardEnabled: step.completionGuard !== false,
|
|
723
|
+
usesAcceptanceContract: step.effectiveAcceptance?.explicit === true,
|
|
724
|
+
tools: step.tools,
|
|
725
|
+
mcpDirectTools: step.mcpDirectTools,
|
|
726
|
+
});
|
|
727
|
+
const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && completionPolicy === "mutation-guard"
|
|
658
728
|
? evaluateCompletionMutationGuard({
|
|
659
729
|
agent: step.agent,
|
|
660
|
-
task,
|
|
730
|
+
task: taskForCompletionGuard,
|
|
661
731
|
messages: run.messages,
|
|
662
732
|
tools: step.tools,
|
|
663
733
|
mcpDirectTools: step.mcpDirectTools,
|
|
@@ -667,14 +737,17 @@ async function runSingleStep(
|
|
|
667
737
|
const completionGuardError = completionGuardTriggered
|
|
668
738
|
? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
|
|
669
739
|
: undefined;
|
|
670
|
-
const effectiveExitCode =
|
|
740
|
+
const effectiveExitCode = completionGuardError
|
|
671
741
|
? 1
|
|
672
|
-
:
|
|
742
|
+
: structuredError
|
|
743
|
+
? 1
|
|
744
|
+
: hiddenError?.hasError
|
|
673
745
|
? (hiddenError.exitCode ?? 1)
|
|
674
746
|
: run.error && run.exitCode === 0
|
|
675
747
|
? 1
|
|
676
748
|
: run.exitCode;
|
|
677
749
|
const error = completionGuardError
|
|
750
|
+
?? structuredError
|
|
678
751
|
?? (hiddenError?.hasError
|
|
679
752
|
? hiddenError.details
|
|
680
753
|
? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
|
|
@@ -691,22 +764,24 @@ async function runSingleStep(
|
|
|
691
764
|
if (candidate) attemptedModels.push(candidate);
|
|
692
765
|
completionGuardTriggeredFinal = completionGuardTriggered;
|
|
693
766
|
finalOutputSnapshot = outputSnapshot;
|
|
694
|
-
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
|
|
695
|
-
if (attempt.success ||
|
|
767
|
+
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
|
|
768
|
+
if (attempt.success || completionGuardError) break;
|
|
696
769
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
697
770
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
|
|
698
771
|
}
|
|
699
772
|
|
|
700
773
|
const rawOutput = finalResult?.finalOutput ?? "";
|
|
774
|
+
const outputForPersistence = stripAcceptanceReport(rawOutput);
|
|
701
775
|
const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
|
|
702
|
-
? resolveSingleOutput(step.outputPath,
|
|
703
|
-
: { fullOutput:
|
|
776
|
+
? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
|
|
777
|
+
: { fullOutput: outputForPersistence };
|
|
704
778
|
const output = resolvedOutput.fullOutput;
|
|
705
779
|
const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
|
|
706
780
|
let outputForSummary = output;
|
|
707
781
|
if (attemptNotes.length > 0) {
|
|
708
782
|
outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
|
|
709
783
|
}
|
|
784
|
+
const outputForAcceptance = rawOutput;
|
|
710
785
|
const finalizedOutput = finalizeSingleOutput({
|
|
711
786
|
fullOutput: outputForSummary,
|
|
712
787
|
outputPath: step.outputPath,
|
|
@@ -717,6 +792,121 @@ async function runSingleStep(
|
|
|
717
792
|
saveError: resolvedOutput.saveError,
|
|
718
793
|
});
|
|
719
794
|
outputForSummary = finalizedOutput.displayOutput;
|
|
795
|
+
const acceptanceForInitialReport = step.effectiveAcceptance && shouldRunAcceptanceFinalization(step.effectiveAcceptance)
|
|
796
|
+
? acceptanceSelfReviewConfig(step.effectiveAcceptance)
|
|
797
|
+
: step.effectiveAcceptance;
|
|
798
|
+
let acceptance = acceptanceForInitialReport
|
|
799
|
+
? await evaluateAcceptance({
|
|
800
|
+
acceptance: acceptanceForInitialReport,
|
|
801
|
+
output: outputForAcceptance,
|
|
802
|
+
cwd: step.cwd ?? ctx.cwd,
|
|
803
|
+
})
|
|
804
|
+
: undefined;
|
|
805
|
+
if (acceptance && step.effectiveAcceptance && shouldRunAcceptanceFinalization(step.effectiveAcceptance) && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted) {
|
|
806
|
+
const sessionFile = step.sessionFile ?? (sessionDir ? findLatestSessionFile(sessionDir) ?? undefined : undefined);
|
|
807
|
+
const maxTurns = step.effectiveAcceptance.finalization.maxTurns;
|
|
808
|
+
const turns: AcceptanceFinalizationTurn[] = [];
|
|
809
|
+
if (!sessionFile) {
|
|
810
|
+
const message = "Acceptance finalization requires a session file for same-session continuation.";
|
|
811
|
+
turns.push(createFinalizationProcessFailureTurn({ turn: 1, prompt: "", message }));
|
|
812
|
+
acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
|
|
813
|
+
} else {
|
|
814
|
+
const selfReviewAcceptance = acceptanceSelfReviewConfig(step.effectiveAcceptance);
|
|
815
|
+
let previousFailure = acceptanceFailureMessage(acceptance);
|
|
816
|
+
let authoritativeLedger = acceptance;
|
|
817
|
+
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
818
|
+
const prompt = formatAcceptanceFinalizationPrompt({
|
|
819
|
+
acceptance: step.effectiveAcceptance,
|
|
820
|
+
initialOutput: outputForAcceptance,
|
|
821
|
+
initialLedger: acceptance,
|
|
822
|
+
turn,
|
|
823
|
+
maxTurns,
|
|
824
|
+
...(previousFailure ? { previousFailure } : {}),
|
|
825
|
+
});
|
|
826
|
+
const { args, env, tempDir } = buildPiArgs({
|
|
827
|
+
baseArgs: ["--mode", "json", "-p"],
|
|
828
|
+
task: prompt,
|
|
829
|
+
sessionEnabled: true,
|
|
830
|
+
sessionFile,
|
|
831
|
+
model: finalResult?.model ?? step.model,
|
|
832
|
+
thinking: step.thinking,
|
|
833
|
+
inheritProjectContext: step.inheritProjectContext,
|
|
834
|
+
inheritSkills: step.inheritSkills,
|
|
835
|
+
tools: step.tools,
|
|
836
|
+
extensions: step.extensions,
|
|
837
|
+
systemPrompt: step.systemPrompt,
|
|
838
|
+
systemPromptMode: step.systemPromptMode,
|
|
839
|
+
mcpDirectTools: step.mcpDirectTools,
|
|
840
|
+
cwd: step.cwd ?? ctx.cwd,
|
|
841
|
+
promptFileStem: `${step.agent}-acceptance-finalization`,
|
|
842
|
+
intercomSessionName: ctx.childIntercomTarget,
|
|
843
|
+
orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
|
|
844
|
+
runId: ctx.id,
|
|
845
|
+
childAgentName: step.agent,
|
|
846
|
+
childIndex: ctx.flatIndex,
|
|
847
|
+
parentEventSink: ctx.nestedRoute?.eventSink,
|
|
848
|
+
parentControlInbox: ctx.nestedRoute?.controlInbox,
|
|
849
|
+
parentRootRunId: ctx.nestedRoute?.rootRunId,
|
|
850
|
+
parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
|
|
851
|
+
});
|
|
852
|
+
ctx.onAttemptStart?.({ model: finalResult?.model ?? step.model, thinking: resolveEffectiveThinking(finalResult?.model ?? step.model, step.thinking) });
|
|
853
|
+
const finalizationRun = await runPiStreaming(
|
|
854
|
+
args,
|
|
855
|
+
step.cwd ?? ctx.cwd,
|
|
856
|
+
`${ctx.outputFile}.finalization-${turn}.log`,
|
|
857
|
+
env,
|
|
858
|
+
ctx.piPackageRoot,
|
|
859
|
+
ctx.piArgv1,
|
|
860
|
+
step.maxSubagentDepth,
|
|
861
|
+
{ eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
862
|
+
ctx.registerInterrupt,
|
|
863
|
+
ctx.onChildEvent,
|
|
864
|
+
);
|
|
865
|
+
cleanupTempDir(tempDir);
|
|
866
|
+
modelAttempts.push({
|
|
867
|
+
model: finalResult?.model ?? finalizationRun.model ?? step.model ?? "default",
|
|
868
|
+
success: finalizationRun.exitCode === 0 && !finalizationRun.error,
|
|
869
|
+
exitCode: finalizationRun.exitCode,
|
|
870
|
+
error: finalizationRun.error,
|
|
871
|
+
usage: finalizationRun.usage,
|
|
872
|
+
});
|
|
873
|
+
const finalizationOutput = finalizationRun.finalOutput;
|
|
874
|
+
if (finalizationRun.exitCode !== 0 || finalizationRun.error || finalizationRun.interrupted) {
|
|
875
|
+
const message = finalizationRun.error ?? "Acceptance finalization turn did not complete successfully.";
|
|
876
|
+
turns.push(createFinalizationProcessFailureTurn({ turn, prompt, rawOutput: finalizationOutput, message }));
|
|
877
|
+
acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
const selfReviewLedger = await evaluateAcceptance({
|
|
881
|
+
acceptance: selfReviewAcceptance,
|
|
882
|
+
output: finalizationOutput,
|
|
883
|
+
cwd: step.cwd ?? ctx.cwd,
|
|
884
|
+
});
|
|
885
|
+
authoritativeLedger = selfReviewLedger;
|
|
886
|
+
turns.push(createFinalizationTurn({ turn, prompt, rawOutput: finalizationOutput, ledger: selfReviewLedger }));
|
|
887
|
+
const failure = acceptanceFailureMessage(selfReviewLedger);
|
|
888
|
+
if (!failure) {
|
|
889
|
+
authoritativeLedger = step.effectiveAcceptance === selfReviewAcceptance
|
|
890
|
+
? selfReviewLedger
|
|
891
|
+
: await evaluateAcceptance({
|
|
892
|
+
acceptance: step.effectiveAcceptance,
|
|
893
|
+
output: finalizationOutput,
|
|
894
|
+
cwd: step.cwd ?? ctx.cwd,
|
|
895
|
+
});
|
|
896
|
+
acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "completed", maxTurns });
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
previousFailure = failure;
|
|
900
|
+
if (turn === maxTurns) acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "failed", maxTurns });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
|
|
905
|
+
const acceptanceCanFailRun = acceptanceFailure && acceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted;
|
|
906
|
+
const effectiveFinalExitCode = acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
907
|
+
const effectiveFinalError = acceptanceCanFailRun
|
|
908
|
+
? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
|
|
909
|
+
: finalResult?.error;
|
|
720
910
|
|
|
721
911
|
if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
|
|
722
912
|
if (ctx.artifactConfig?.includeOutput !== false) {
|
|
@@ -729,7 +919,7 @@ async function runSingleStep(
|
|
|
729
919
|
runId: ctx.id,
|
|
730
920
|
agent: step.agent,
|
|
731
921
|
task,
|
|
732
|
-
exitCode:
|
|
922
|
+
exitCode: effectiveFinalExitCode,
|
|
733
923
|
model: finalResult?.model,
|
|
734
924
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
735
925
|
modelAttempts,
|
|
@@ -744,8 +934,8 @@ async function runSingleStep(
|
|
|
744
934
|
return {
|
|
745
935
|
agent: step.agent,
|
|
746
936
|
output: outputForSummary,
|
|
747
|
-
exitCode:
|
|
748
|
-
error:
|
|
937
|
+
exitCode: effectiveFinalExitCode,
|
|
938
|
+
error: effectiveFinalError,
|
|
749
939
|
sessionFile: step.sessionFile,
|
|
750
940
|
intercomTarget: ctx.childIntercomTarget,
|
|
751
941
|
model: finalResult?.model,
|
|
@@ -754,6 +944,10 @@ async function runSingleStep(
|
|
|
754
944
|
artifactPaths,
|
|
755
945
|
interrupted: finalResult?.interrupted,
|
|
756
946
|
completionGuardTriggered: completionGuardTriggeredFinal,
|
|
947
|
+
structuredOutput: (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
948
|
+
structuredOutputPath: effectiveStructuredOutput?.outputPath,
|
|
949
|
+
structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
|
|
950
|
+
acceptance,
|
|
757
951
|
};
|
|
758
952
|
}
|
|
759
953
|
|
|
@@ -796,7 +990,7 @@ function markParallelGroupSetupFailure(input: {
|
|
|
796
990
|
input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
|
|
797
991
|
input.statusPayload.steps[flatTaskIndex].durationMs = 0;
|
|
798
992
|
input.statusPayload.steps[flatTaskIndex].exitCode = 1;
|
|
799
|
-
input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, sessionFile: input.group.parallel[taskIndex].sessionFile });
|
|
993
|
+
input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, exitCode: 1, sessionFile: input.group.parallel[taskIndex].sessionFile });
|
|
800
994
|
}
|
|
801
995
|
input.statusPayload.currentStep = input.groupStartFlatIndex;
|
|
802
996
|
input.statusPayload.lastUpdate = input.failedAt;
|
|
@@ -886,6 +1080,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
886
1080
|
const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
|
|
887
1081
|
config;
|
|
888
1082
|
let previousOutput = "";
|
|
1083
|
+
const outputs: ChainOutputMap = {};
|
|
889
1084
|
const results: StepResult[] = [];
|
|
890
1085
|
const overallStartTime = Date.now();
|
|
891
1086
|
const shareEnabled = config.share === true;
|
|
@@ -902,13 +1097,59 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
902
1097
|
let latestSessionFile: string | undefined;
|
|
903
1098
|
|
|
904
1099
|
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
1100
|
+
const initialStatusSteps: RunnerStatusStep[] = [];
|
|
905
1101
|
let flatStepCount = 0;
|
|
906
1102
|
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
907
1103
|
const step = steps[stepIndex]!;
|
|
908
1104
|
if (isParallelGroup(step)) {
|
|
909
1105
|
parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
|
|
1106
|
+
for (const task of step.parallel) {
|
|
1107
|
+
initialStatusSteps.push({
|
|
1108
|
+
agent: task.agent,
|
|
1109
|
+
phase: task.phase,
|
|
1110
|
+
label: task.label,
|
|
1111
|
+
outputName: task.outputName,
|
|
1112
|
+
structured: task.structured,
|
|
1113
|
+
status: "pending",
|
|
1114
|
+
...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
|
|
1115
|
+
skills: task.skills,
|
|
1116
|
+
model: task.model,
|
|
1117
|
+
thinking: task.thinking,
|
|
1118
|
+
attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
|
|
1119
|
+
recentTools: [],
|
|
1120
|
+
recentOutput: [],
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
910
1123
|
flatStepCount += step.parallel.length;
|
|
1124
|
+
} else if (isDynamicRunnerGroup(step)) {
|
|
1125
|
+
parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
|
|
1126
|
+
initialStatusSteps.push({
|
|
1127
|
+
agent: `expand:${step.parallel.agent}`,
|
|
1128
|
+
phase: step.phase ?? step.parallel.phase,
|
|
1129
|
+
label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
|
|
1130
|
+
outputName: step.collect.as,
|
|
1131
|
+
structured: Boolean(step.collect.outputSchema),
|
|
1132
|
+
status: "pending",
|
|
1133
|
+
recentTools: [],
|
|
1134
|
+
recentOutput: [],
|
|
1135
|
+
});
|
|
1136
|
+
flatStepCount++;
|
|
911
1137
|
} else {
|
|
1138
|
+
initialStatusSteps.push({
|
|
1139
|
+
agent: step.agent,
|
|
1140
|
+
phase: step.phase,
|
|
1141
|
+
label: step.label,
|
|
1142
|
+
outputName: step.outputName,
|
|
1143
|
+
structured: step.structured,
|
|
1144
|
+
status: "pending",
|
|
1145
|
+
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
1146
|
+
skills: step.skills,
|
|
1147
|
+
model: step.model,
|
|
1148
|
+
thinking: step.thinking,
|
|
1149
|
+
attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
|
|
1150
|
+
recentTools: [],
|
|
1151
|
+
recentOutput: [],
|
|
1152
|
+
});
|
|
912
1153
|
flatStepCount++;
|
|
913
1154
|
}
|
|
914
1155
|
}
|
|
@@ -929,17 +1170,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
929
1170
|
currentStep: 0,
|
|
930
1171
|
chainStepCount: steps.length,
|
|
931
1172
|
parallelGroups,
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
status: "pending",
|
|
935
|
-
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
936
|
-
skills: step.skills,
|
|
937
|
-
model: step.model,
|
|
938
|
-
thinking: step.thinking,
|
|
939
|
-
attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
|
|
940
|
-
recentTools: [],
|
|
941
|
-
recentOutput: [],
|
|
942
|
-
})),
|
|
1173
|
+
workflowGraph: config.workflowGraph,
|
|
1174
|
+
steps: initialStatusSteps,
|
|
943
1175
|
artifactsDir,
|
|
944
1176
|
sessionDir: config.sessionDir,
|
|
945
1177
|
outputFile: path.join(asyncDir, "output-0.log"),
|
|
@@ -969,10 +1201,48 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
969
1201
|
console.error("Failed to emit nested async status event:", error);
|
|
970
1202
|
}
|
|
971
1203
|
};
|
|
1204
|
+
const refreshWorkflowGraph = (): void => {
|
|
1205
|
+
if (!config.workflowGraph) return;
|
|
1206
|
+
const graph = structuredClone(statusPayload.workflowGraph ?? config.workflowGraph);
|
|
1207
|
+
const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "detached" => {
|
|
1208
|
+
if (status === "complete" || status === "completed") return "completed";
|
|
1209
|
+
if (status === "running" || status === "failed" || status === "paused" || status === "pending") return status;
|
|
1210
|
+
return "pending";
|
|
1211
|
+
};
|
|
1212
|
+
const updateNode = (node: NonNullable<typeof graph.nodes>[number]): void => {
|
|
1213
|
+
if (node.flatIndex !== undefined) {
|
|
1214
|
+
const step = statusPayload.steps[node.flatIndex];
|
|
1215
|
+
if (step) {
|
|
1216
|
+
node.status = normalize(step.status);
|
|
1217
|
+
node.error = step.error;
|
|
1218
|
+
node.acceptanceStatus = step.acceptance?.status;
|
|
1219
|
+
}
|
|
1220
|
+
if (statusPayload.currentStep === node.flatIndex) graph.currentNodeId = node.id;
|
|
1221
|
+
}
|
|
1222
|
+
for (const child of node.children ?? []) updateNode(child);
|
|
1223
|
+
if (node.children?.length) {
|
|
1224
|
+
if (node.children.every((child) => child.status === "completed")) node.status = "completed";
|
|
1225
|
+
else if (node.children.some((child) => child.status === "running")) node.status = "running";
|
|
1226
|
+
else if (node.children.some((child) => child.status === "failed")) node.status = "failed";
|
|
1227
|
+
else if (node.children.some((child) => child.status === "paused")) node.status = "paused";
|
|
1228
|
+
}
|
|
1229
|
+
if (node.error) node.status = "failed";
|
|
1230
|
+
};
|
|
1231
|
+
for (const node of graph.nodes) updateNode(node);
|
|
1232
|
+
statusPayload.workflowGraph = graph;
|
|
1233
|
+
};
|
|
972
1234
|
const writeStatusPayload = (): void => {
|
|
1235
|
+
refreshWorkflowGraph();
|
|
973
1236
|
writeAtomicJson(statusPath, statusPayload);
|
|
974
1237
|
emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
|
|
975
1238
|
};
|
|
1239
|
+
const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: AcceptanceLedger): void => {
|
|
1240
|
+
const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
|
|
1241
|
+
if (!groupNode) return;
|
|
1242
|
+
groupNode.status = status;
|
|
1243
|
+
groupNode.error = error;
|
|
1244
|
+
groupNode.acceptanceStatus = acceptance?.status ?? groupNode.acceptanceStatus;
|
|
1245
|
+
};
|
|
976
1246
|
|
|
977
1247
|
const stepOutputActivityAt = (index: number): number => {
|
|
978
1248
|
const step = statusPayload.steps[index];
|
|
@@ -989,8 +1259,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
989
1259
|
};
|
|
990
1260
|
const emittedControlEventKeys = new Set<string>();
|
|
991
1261
|
const activeLongRunningSteps = new Set<number>();
|
|
992
|
-
const mutatingFailureStates =
|
|
993
|
-
const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> =
|
|
1262
|
+
const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
|
|
1263
|
+
const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
|
|
994
1264
|
const mutatingFailureWindowMs = 5 * 60_000;
|
|
995
1265
|
const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
|
|
996
1266
|
if (!controlConfig.enabled) return;
|
|
@@ -1131,7 +1401,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1131
1401
|
resetMutatingFailureState(mutatingFailureStates[flatIndex]!);
|
|
1132
1402
|
}
|
|
1133
1403
|
} else if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
1134
|
-
appendRecentStepOutput(step, extractTextFromContent(event.message.content).split("\n").slice(-10));
|
|
1404
|
+
appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
|
|
1135
1405
|
step.turnCount = (step.turnCount ?? 0) + 1;
|
|
1136
1406
|
const usage = event.message.usage;
|
|
1137
1407
|
if (usage) {
|
|
@@ -1262,6 +1532,262 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1262
1532
|
if (interrupted) break;
|
|
1263
1533
|
const step = steps[stepIndex];
|
|
1264
1534
|
|
|
1535
|
+
if (isDynamicRunnerGroup(step)) {
|
|
1536
|
+
const groupStartFlatIndex = flatIndex;
|
|
1537
|
+
let materialized: ReturnType<typeof materializeDynamicParallelStep>;
|
|
1538
|
+
try {
|
|
1539
|
+
materialized = materializeDynamicParallelStep(step as Parameters<typeof materializeDynamicParallelStep>[0], outputs, stepIndex, { maxItems: config.dynamicFanoutMaxItems, allowRunnerFields: true });
|
|
1540
|
+
if (materialized.collectedOnEmpty) validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
const now = Date.now();
|
|
1543
|
+
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
1544
|
+
statusPayload.state = "failed";
|
|
1545
|
+
statusPayload.error = message;
|
|
1546
|
+
statusPayload.currentStep = flatIndex;
|
|
1547
|
+
const placeholder = statusPayload.steps[groupStartFlatIndex];
|
|
1548
|
+
if (placeholder) {
|
|
1549
|
+
placeholder.status = "failed";
|
|
1550
|
+
placeholder.error = message;
|
|
1551
|
+
placeholder.startedAt = now;
|
|
1552
|
+
placeholder.endedAt = now;
|
|
1553
|
+
placeholder.durationMs = 0;
|
|
1554
|
+
placeholder.exitCode = 1;
|
|
1555
|
+
}
|
|
1556
|
+
statusPayload.lastUpdate = now;
|
|
1557
|
+
markDynamicGraphGroup(stepIndex, "failed", message);
|
|
1558
|
+
writeStatusPayload();
|
|
1559
|
+
results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1 });
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if (materialized.parallel.length === 0) {
|
|
1564
|
+
const now = Date.now();
|
|
1565
|
+
const collection = materialized.collectedOnEmpty ?? [];
|
|
1566
|
+
outputs[step.collect.as] = {
|
|
1567
|
+
text: JSON.stringify(collection),
|
|
1568
|
+
structured: collection,
|
|
1569
|
+
agent: step.parallel.agent,
|
|
1570
|
+
stepIndex,
|
|
1571
|
+
};
|
|
1572
|
+
statusPayload.outputs = outputs;
|
|
1573
|
+
const placeholder = statusPayload.steps[groupStartFlatIndex];
|
|
1574
|
+
if (placeholder) {
|
|
1575
|
+
placeholder.status = "complete";
|
|
1576
|
+
placeholder.startedAt = now;
|
|
1577
|
+
placeholder.endedAt = now;
|
|
1578
|
+
placeholder.durationMs = 0;
|
|
1579
|
+
}
|
|
1580
|
+
previousOutput = "Dynamic fanout produced 0 results.";
|
|
1581
|
+
flatIndex++;
|
|
1582
|
+
statusPayload.lastUpdate = now;
|
|
1583
|
+
markDynamicGraphGroup(stepIndex, "completed");
|
|
1584
|
+
writeStatusPayload();
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const dynamicSteps = materialized.parallel.map((task, itemIndex) => ({
|
|
1589
|
+
...step.parallel,
|
|
1590
|
+
task: task.task ?? step.parallel.task,
|
|
1591
|
+
label: task.label ?? step.parallel.label,
|
|
1592
|
+
structuredOutput: undefined,
|
|
1593
|
+
structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
|
|
1594
|
+
}));
|
|
1595
|
+
const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task) => ({
|
|
1596
|
+
agent: task.agent,
|
|
1597
|
+
phase: task.phase ?? step.phase,
|
|
1598
|
+
label: task.label,
|
|
1599
|
+
outputName: undefined,
|
|
1600
|
+
structured: Boolean(task.structuredOutputSchema),
|
|
1601
|
+
status: "pending",
|
|
1602
|
+
...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
|
|
1603
|
+
skills: task.skills,
|
|
1604
|
+
model: task.model,
|
|
1605
|
+
thinking: task.thinking,
|
|
1606
|
+
attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
|
|
1607
|
+
recentTools: [],
|
|
1608
|
+
recentOutput: [],
|
|
1609
|
+
}));
|
|
1610
|
+
statusPayload.steps.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps);
|
|
1611
|
+
if (config.childIntercomTargets) {
|
|
1612
|
+
config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
|
|
1613
|
+
}
|
|
1614
|
+
mutatingFailureStates.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps.map(() => createMutatingFailureState()));
|
|
1615
|
+
pendingToolResults.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps.map(() => undefined));
|
|
1616
|
+
const materializedDelta = dynamicStatusSteps.length - 1;
|
|
1617
|
+
for (const group of statusPayload.parallelGroups) {
|
|
1618
|
+
if (group.stepIndex === stepIndex) {
|
|
1619
|
+
group.start = groupStartFlatIndex;
|
|
1620
|
+
group.count = dynamicStatusSteps.length;
|
|
1621
|
+
} else if (group.start > groupStartFlatIndex) {
|
|
1622
|
+
group.start += materializedDelta;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (statusPayload.workflowGraph) {
|
|
1626
|
+
const shiftFlatIndexes = (nodes: NonNullable<typeof statusPayload.workflowGraph>["nodes"]): void => {
|
|
1627
|
+
for (const node of nodes) {
|
|
1628
|
+
if (node.stepIndex !== undefined && node.stepIndex > stepIndex && node.flatIndex !== undefined && node.flatIndex >= groupStartFlatIndex) {
|
|
1629
|
+
node.flatIndex += dynamicStatusSteps.length;
|
|
1630
|
+
}
|
|
1631
|
+
if (node.children) shiftFlatIndexes(node.children);
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
shiftFlatIndexes(statusPayload.workflowGraph.nodes);
|
|
1635
|
+
const groupNode = statusPayload.workflowGraph.nodes.find((node) => node.id === `step-${stepIndex}`);
|
|
1636
|
+
if (groupNode) {
|
|
1637
|
+
groupNode.children = materialized.items.map((item, itemIndex) => ({
|
|
1638
|
+
id: `step-${stepIndex}-item-${item.idKey}`,
|
|
1639
|
+
kind: "agent",
|
|
1640
|
+
agent: step.parallel.agent,
|
|
1641
|
+
phase: dynamicSteps[itemIndex]?.phase ?? step.phase,
|
|
1642
|
+
label: dynamicSteps[itemIndex]?.label?.trim() || `${step.parallel.agent} ${item.key}`,
|
|
1643
|
+
status: "pending",
|
|
1644
|
+
flatIndex: groupStartFlatIndex + itemIndex,
|
|
1645
|
+
stepIndex,
|
|
1646
|
+
itemKey: item.key,
|
|
1647
|
+
structured: Boolean(dynamicSteps[itemIndex]?.structuredOutputSchema),
|
|
1648
|
+
}));
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
writeStatusPayload();
|
|
1652
|
+
|
|
1653
|
+
const concurrency = step.concurrency ?? MAX_PARALLEL_CONCURRENCY;
|
|
1654
|
+
const failFast = step.failFast ?? false;
|
|
1655
|
+
let aborted = false;
|
|
1656
|
+
const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
|
|
1657
|
+
const fi = groupStartFlatIndex + taskIdx;
|
|
1658
|
+
if (aborted && failFast) {
|
|
1659
|
+
const skippedAt = Date.now();
|
|
1660
|
+
statusPayload.steps[fi].status = "failed";
|
|
1661
|
+
statusPayload.steps[fi].error = "Skipped due to fail-fast";
|
|
1662
|
+
statusPayload.steps[fi].startedAt = skippedAt;
|
|
1663
|
+
statusPayload.steps[fi].endedAt = skippedAt;
|
|
1664
|
+
statusPayload.steps[fi].durationMs = 0;
|
|
1665
|
+
statusPayload.steps[fi].exitCode = -1;
|
|
1666
|
+
statusPayload.lastUpdate = skippedAt;
|
|
1667
|
+
writeStatusPayload();
|
|
1668
|
+
return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
|
|
1669
|
+
}
|
|
1670
|
+
const taskStartTime = Date.now();
|
|
1671
|
+
statusPayload.currentStep = fi;
|
|
1672
|
+
statusPayload.steps[fi].status = "running";
|
|
1673
|
+
statusPayload.steps[fi].error = undefined;
|
|
1674
|
+
statusPayload.steps[fi].activityState = undefined;
|
|
1675
|
+
resetStepLiveDetail(statusPayload.steps[fi]);
|
|
1676
|
+
statusPayload.steps[fi].startedAt = taskStartTime;
|
|
1677
|
+
statusPayload.steps[fi].lastActivityAt = taskStartTime;
|
|
1678
|
+
statusPayload.outputFile = path.join(asyncDir, `output-${fi}.log`);
|
|
1679
|
+
statusPayload.lastActivityAt = taskStartTime;
|
|
1680
|
+
statusPayload.lastUpdate = taskStartTime;
|
|
1681
|
+
writeStatusPayload();
|
|
1682
|
+
appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
|
|
1683
|
+
const singleResult = await runSingleStep(task, {
|
|
1684
|
+
previousOutput, placeholder, cwd, sessionEnabled,
|
|
1685
|
+
outputs,
|
|
1686
|
+
sessionDir: config.sessionDir ? path.join(config.sessionDir, `dynamic-${stepIndex}-${taskIdx}`) : undefined,
|
|
1687
|
+
artifactsDir, artifactConfig, id,
|
|
1688
|
+
flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
1689
|
+
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
1690
|
+
piPackageRoot: config.piPackageRoot,
|
|
1691
|
+
piArgv1: config.piArgv1,
|
|
1692
|
+
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
1693
|
+
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
1694
|
+
nestedRoute: config.nestedRoute,
|
|
1695
|
+
registerInterrupt: (interrupt) => {
|
|
1696
|
+
activeChildInterrupt = interrupt;
|
|
1697
|
+
},
|
|
1698
|
+
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
1699
|
+
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
1700
|
+
});
|
|
1701
|
+
const taskEndTime = Date.now();
|
|
1702
|
+
statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
|
|
1703
|
+
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
1704
|
+
statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
|
|
1705
|
+
statusPayload.steps[fi].exitCode = singleResult.exitCode;
|
|
1706
|
+
statusPayload.steps[fi].model = singleResult.model;
|
|
1707
|
+
statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
|
|
1708
|
+
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
1709
|
+
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
1710
|
+
statusPayload.steps[fi].error = singleResult.error;
|
|
1711
|
+
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
1712
|
+
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
1713
|
+
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
1714
|
+
statusPayload.steps[fi].acceptance = singleResult.acceptance;
|
|
1715
|
+
statusPayload.lastUpdate = taskEndTime;
|
|
1716
|
+
writeStatusPayload();
|
|
1717
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1718
|
+
type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
1719
|
+
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
1720
|
+
exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
|
|
1721
|
+
}));
|
|
1722
|
+
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
1723
|
+
return { ...singleResult, skipped: false };
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
flatIndex += dynamicSteps.length;
|
|
1727
|
+
for (const pr of parallelResults) {
|
|
1728
|
+
results.push({
|
|
1729
|
+
agent: pr.agent,
|
|
1730
|
+
output: pr.output,
|
|
1731
|
+
error: pr.error,
|
|
1732
|
+
success: pr.exitCode === 0,
|
|
1733
|
+
exitCode: pr.exitCode,
|
|
1734
|
+
skipped: pr.skipped,
|
|
1735
|
+
sessionFile: pr.sessionFile,
|
|
1736
|
+
intercomTarget: pr.intercomTarget,
|
|
1737
|
+
model: pr.model,
|
|
1738
|
+
attemptedModels: pr.attemptedModels,
|
|
1739
|
+
modelAttempts: pr.modelAttempts,
|
|
1740
|
+
artifactPaths: pr.artifactPaths,
|
|
1741
|
+
structuredOutput: pr.structuredOutput,
|
|
1742
|
+
structuredOutputPath: pr.structuredOutputPath,
|
|
1743
|
+
structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
|
|
1744
|
+
acceptance: pr.acceptance,
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
|
|
1748
|
+
const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
|
|
1749
|
+
if (failures.length === 0) {
|
|
1750
|
+
try {
|
|
1751
|
+
validateDynamicCollection(step.collect.outputSchema, collection);
|
|
1752
|
+
outputs[step.collect.as] = {
|
|
1753
|
+
text: JSON.stringify(collection),
|
|
1754
|
+
structured: collection,
|
|
1755
|
+
agent: step.parallel.agent,
|
|
1756
|
+
stepIndex,
|
|
1757
|
+
};
|
|
1758
|
+
statusPayload.outputs = outputs;
|
|
1759
|
+
markDynamicGraphGroup(stepIndex, "completed");
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
1762
|
+
results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
|
|
1763
|
+
statusPayload.error = message;
|
|
1764
|
+
markDynamicGraphGroup(stepIndex, "failed", message);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
previousOutput = aggregateParallelOutputs(
|
|
1768
|
+
parallelResults.map((r, i) => ({
|
|
1769
|
+
agent: r.agent,
|
|
1770
|
+
taskIndex: i,
|
|
1771
|
+
output: r.output,
|
|
1772
|
+
exitCode: r.exitCode,
|
|
1773
|
+
error: r.error,
|
|
1774
|
+
})),
|
|
1775
|
+
(i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`,
|
|
1776
|
+
);
|
|
1777
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1778
|
+
type: "subagent.dynamic.completed",
|
|
1779
|
+
ts: Date.now(),
|
|
1780
|
+
runId: id,
|
|
1781
|
+
stepIndex,
|
|
1782
|
+
success: failures.length === 0,
|
|
1783
|
+
}));
|
|
1784
|
+
if (failures.length > 0) markDynamicGraphGroup(stepIndex, "failed", failures[0]?.error ?? "Dynamic fanout child failed.");
|
|
1785
|
+
statusPayload.lastUpdate = Date.now();
|
|
1786
|
+
writeStatusPayload();
|
|
1787
|
+
if (failures.length > 0 || statusPayload.error) break;
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1265
1791
|
if (isParallelGroup(step)) {
|
|
1266
1792
|
const group = step;
|
|
1267
1793
|
const concurrency = group.concurrency ?? MAX_PARALLEL_CONCURRENCY;
|
|
@@ -1379,6 +1905,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1379
1905
|
|
|
1380
1906
|
const singleResult = await runSingleStep(taskForRun, {
|
|
1381
1907
|
previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
|
|
1908
|
+
outputs,
|
|
1382
1909
|
sessionDir: taskSessionDir,
|
|
1383
1910
|
artifactsDir, artifactConfig, id,
|
|
1384
1911
|
flatIndex: fi, flatStepCount: flatSteps.length,
|
|
@@ -1410,6 +1937,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1410
1937
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
1411
1938
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
1412
1939
|
statusPayload.steps[fi].error = singleResult.error;
|
|
1940
|
+
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
1941
|
+
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
1942
|
+
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
1943
|
+
statusPayload.steps[fi].acceptance = singleResult.acceptance;
|
|
1413
1944
|
statusPayload.lastUpdate = taskEndTime;
|
|
1414
1945
|
writeStatusPayload();
|
|
1415
1946
|
|
|
@@ -1463,6 +1994,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1463
1994
|
output: pr.output,
|
|
1464
1995
|
error: pr.error,
|
|
1465
1996
|
success: pr.exitCode === 0,
|
|
1997
|
+
exitCode: pr.exitCode,
|
|
1466
1998
|
skipped: pr.skipped,
|
|
1467
1999
|
sessionFile: pr.sessionFile,
|
|
1468
2000
|
intercomTarget: pr.intercomTarget,
|
|
@@ -1470,8 +2002,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1470
2002
|
attemptedModels: pr.attemptedModels,
|
|
1471
2003
|
modelAttempts: pr.modelAttempts,
|
|
1472
2004
|
artifactPaths: pr.artifactPaths,
|
|
1473
|
-
|
|
2005
|
+
structuredOutput: pr.structuredOutput,
|
|
2006
|
+
structuredOutputPath: pr.structuredOutputPath,
|
|
2007
|
+
structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
|
|
2008
|
+
acceptance: pr.acceptance,
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
for (let t = 0; t < group.parallel.length; t++) {
|
|
2012
|
+
const outputName = group.parallel[t]?.outputName;
|
|
2013
|
+
if (outputName) outputs[outputName] = outputEntryFromAsyncResult({
|
|
2014
|
+
agent: parallelResults[t]!.agent,
|
|
2015
|
+
output: parallelResults[t]!.output,
|
|
2016
|
+
structuredOutput: parallelResults[t]!.structuredOutput,
|
|
2017
|
+
}, stepIndex);
|
|
1474
2018
|
}
|
|
2019
|
+
statusPayload.outputs = outputs;
|
|
1475
2020
|
|
|
1476
2021
|
previousOutput = aggregateParallelOutputs(
|
|
1477
2022
|
parallelResults.map((r) => ({
|
|
@@ -1525,6 +2070,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1525
2070
|
|
|
1526
2071
|
const singleResult = await runSingleStep(seqStep, {
|
|
1527
2072
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
2073
|
+
outputs,
|
|
1528
2074
|
sessionDir: config.sessionDir,
|
|
1529
2075
|
artifactsDir, artifactConfig, id,
|
|
1530
2076
|
flatIndex, flatStepCount: flatSteps.length,
|
|
@@ -1550,13 +2096,26 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1550
2096
|
output: singleResult.output,
|
|
1551
2097
|
error: singleResult.error,
|
|
1552
2098
|
success: singleResult.exitCode === 0,
|
|
2099
|
+
exitCode: singleResult.exitCode,
|
|
1553
2100
|
sessionFile: singleResult.sessionFile,
|
|
1554
2101
|
intercomTarget: singleResult.intercomTarget,
|
|
1555
2102
|
model: singleResult.model,
|
|
1556
2103
|
attemptedModels: singleResult.attemptedModels,
|
|
1557
2104
|
modelAttempts: singleResult.modelAttempts,
|
|
1558
2105
|
artifactPaths: singleResult.artifactPaths,
|
|
2106
|
+
structuredOutput: singleResult.structuredOutput,
|
|
2107
|
+
structuredOutputPath: singleResult.structuredOutputPath,
|
|
2108
|
+
structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
|
|
2109
|
+
acceptance: singleResult.acceptance,
|
|
1559
2110
|
});
|
|
2111
|
+
if (seqStep.outputName) {
|
|
2112
|
+
outputs[seqStep.outputName] = outputEntryFromAsyncResult({
|
|
2113
|
+
agent: singleResult.agent,
|
|
2114
|
+
output: singleResult.output,
|
|
2115
|
+
structuredOutput: singleResult.structuredOutput,
|
|
2116
|
+
}, stepIndex);
|
|
2117
|
+
}
|
|
2118
|
+
statusPayload.outputs = outputs;
|
|
1560
2119
|
|
|
1561
2120
|
const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
|
|
1562
2121
|
let stepTokens: TokenUsage | null = cumulativeTokens
|
|
@@ -1589,6 +2148,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1589
2148
|
statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
|
|
1590
2149
|
statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
|
|
1591
2150
|
statusPayload.steps[flatIndex].error = singleResult.error;
|
|
2151
|
+
statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
|
|
2152
|
+
statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2153
|
+
statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
2154
|
+
statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
|
|
1592
2155
|
if (stepTokens) {
|
|
1593
2156
|
statusPayload.steps[flatIndex].tokens = stepTokens;
|
|
1594
2157
|
statusPayload.totalTokens = { ...previousCumulativeTokens };
|
|
@@ -1690,7 +2253,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1690
2253
|
statusPayload.shareUrl = shareUrl;
|
|
1691
2254
|
statusPayload.gistUrl = gistUrl;
|
|
1692
2255
|
statusPayload.shareError = shareError;
|
|
1693
|
-
if (statusPayload.state === "failed") {
|
|
2256
|
+
if (statusPayload.state === "failed" && !statusPayload.error) {
|
|
1694
2257
|
const failedStep = statusPayload.steps.find((s) => s.status === "failed");
|
|
1695
2258
|
if (failedStep?.agent) {
|
|
1696
2259
|
statusPayload.error = `Step failed: ${failedStep.agent}`;
|
|
@@ -1747,7 +2310,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1747
2310
|
modelAttempts: r.modelAttempts,
|
|
1748
2311
|
artifactPaths: r.artifactPaths,
|
|
1749
2312
|
truncated: r.truncated,
|
|
2313
|
+
structuredOutput: r.structuredOutput,
|
|
2314
|
+
structuredOutputPath: r.structuredOutputPath,
|
|
2315
|
+
structuredOutputSchemaPath: r.structuredOutputSchemaPath,
|
|
2316
|
+
acceptance: r.acceptance,
|
|
1750
2317
|
})),
|
|
2318
|
+
outputs,
|
|
2319
|
+
workflowGraph: statusPayload.workflowGraph,
|
|
1751
2320
|
exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
1752
2321
|
timestamp: runEndedAt,
|
|
1753
2322
|
durationMs: runEndedAt - overallStartTime,
|