pi-cohort 7.2.2 → 7.3.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 +12 -0
- package/README.md +31 -2
- package/package.json +1 -1
- package/skills/pi-cohort/SKILL.md +10 -0
- package/skills/pi-cohort/reference/config-fields.md +1 -0
- package/src/agents/agent-management.ts +9 -1
- package/src/agents/agents.ts +2 -1
- package/src/agents/chain-serializer.ts +14 -0
- package/src/extension/schemas.ts +4 -0
- package/src/runs/background/async-execution.ts +3 -0
- package/src/runs/background/notify.ts +8 -2
- package/src/runs/background/subagent-runner.ts +57 -21
- package/src/runs/foreground/chain-execution.ts +148 -54
- package/src/runs/foreground/subagent-executor.ts +3 -2
- package/src/runs/shared/chain-outputs.ts +49 -9
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +3 -0
- package/src/runs/shared/pi-spawn.ts +38 -33
- package/src/runs/shared/stop-conditions.ts +100 -0
- package/src/runs/shared/worktree.ts +7 -1
- package/src/shared/formatters.ts +64 -15
- package/src/shared/settings.ts +4 -1
- package/src/shared/types.ts +37 -0
- package/src/slash/slash-commands.ts +1 -0
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
34
34
|
import { runSync } from "./execution.ts";
|
|
35
35
|
import type { PiRuntimeIdentity } from "../shared/pi-spawn.ts";
|
|
36
|
-
import { buildChainSummary } from "../../shared/formatters.ts";
|
|
36
|
+
import { buildChainSummary, buildChainOutputsSection, buildChainStopSection, type ChainSummaryOptions } from "../../shared/formatters.ts";
|
|
37
37
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "../../shared/utils.ts";
|
|
38
38
|
import { recordRun } from "../shared/run-history.ts";
|
|
39
39
|
import {
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
formatWorktreeDiffSummary,
|
|
45
45
|
formatWorktreeTaskCwdConflict,
|
|
46
46
|
type WorktreeSetup,
|
|
47
|
+
type WorktreeDiff,
|
|
48
|
+
patchPathForDiff,
|
|
47
49
|
} from "../shared/worktree.ts";
|
|
48
50
|
import {
|
|
49
51
|
type ActivityState,
|
|
@@ -66,7 +68,8 @@ import { ChainOutputValidationError, outputEntryFromResult, resolveOutputReferen
|
|
|
66
68
|
import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
67
69
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection, type DynamicCollectedResult } from "../shared/dynamic-fanout.ts";
|
|
68
70
|
import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, resolveEffectiveAcceptance } from "../shared/acceptance.ts";
|
|
69
|
-
import
|
|
71
|
+
import { buildStopRecord, evaluateStopCondition } from "../shared/stop-conditions.ts";
|
|
72
|
+
import type { ChainOutputMap, ChainTaskOutputEntry, StopRecord } from "../../shared/types.ts";
|
|
70
73
|
|
|
71
74
|
interface ChainExecutionDetailsInput {
|
|
72
75
|
results: SingleResult[];
|
|
@@ -80,6 +83,7 @@ interface ChainExecutionDetailsInput {
|
|
|
80
83
|
currentStepIndex?: number;
|
|
81
84
|
runId: string;
|
|
82
85
|
outputs?: ChainOutputMap;
|
|
86
|
+
stop?: StopRecord;
|
|
83
87
|
currentFlatIndex?: number;
|
|
84
88
|
dynamicChildren?: Record<number, Array<{ agent: string; label?: string; flatIndex: number; itemKey: string; outputName?: string; structured?: boolean; error?: string }>>;
|
|
85
89
|
dynamicGroupStatuses?: Record<number, { status: "pending" | "running" | "completed" | "failed" | "paused"; error?: string; acceptance?: SingleResult["acceptance"] }>;
|
|
@@ -147,7 +151,8 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
|
|
|
147
151
|
totalSteps: input.totalSteps,
|
|
148
152
|
currentStepIndex: input.currentStepIndex,
|
|
149
153
|
outputs: input.outputs,
|
|
150
|
-
|
|
154
|
+
stop: input.stop,
|
|
155
|
+
workflowGraph: buildWorkflowGraphSnapshot({
|
|
151
156
|
runId: input.runId,
|
|
152
157
|
mode: "chain",
|
|
153
158
|
steps: input.chainSteps,
|
|
@@ -160,9 +165,12 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
|
|
|
160
165
|
});
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
function buildChainExecutionErrorResult(message: string, input: ChainExecutionDetailsInput): ChainExecutionResult {
|
|
168
|
+
function buildChainExecutionErrorResult(message: string, input: ChainExecutionDetailsInput, summaryOptions?: ChainSummaryOptions): ChainExecutionResult {
|
|
169
|
+
const sections: string[] = [];
|
|
170
|
+
if (summaryOptions?.stop) sections.push(buildChainStopSection(summaryOptions.stop));
|
|
171
|
+
if (summaryOptions?.outputs) sections.push(buildChainOutputsSection(summaryOptions.outputs));
|
|
164
172
|
return {
|
|
165
|
-
content: [{ type: "text", text: message }],
|
|
173
|
+
content: [{ type: "text", text: sections.length ? `${message}\n\n${sections.join("\n\n")}\n` : message }],
|
|
166
174
|
isError: true,
|
|
167
175
|
details: buildChainExecutionDetails(input),
|
|
168
176
|
};
|
|
@@ -180,17 +188,21 @@ function ensureParallelProgressFile(
|
|
|
180
188
|
return true;
|
|
181
189
|
}
|
|
182
190
|
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
function captureParallelWorktreeDiffs(worktreeSetup: WorktreeSetup | undefined, diffsDir: string, agents: string[]): WorktreeDiff[] {
|
|
192
|
+
return worktreeSetup ? diffWorktrees(worktreeSetup, agents, diffsDir) : [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function appendParallelWorktreeSummary(output: string, diffs: WorktreeDiff[]): string {
|
|
196
|
+
if (diffs.length === 0) return output;
|
|
197
|
+
const summary = formatWorktreeDiffSummary(diffs);
|
|
198
|
+
return summary ? `${output}\n\n${summary}` : output;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function patchInfoForDiff(diff: WorktreeDiff | undefined): { patchPath?: string; patchError?: string } | undefined {
|
|
202
|
+
if (!diff) return undefined;
|
|
203
|
+
if (diff.captureError) return { patchError: diff.captureError };
|
|
204
|
+
const patchPath = patchPathForDiff(diff);
|
|
205
|
+
return patchPath ? { patchPath } : undefined;
|
|
194
206
|
}
|
|
195
207
|
|
|
196
208
|
async function runParallelChainTasks(input: ParallelChainRunInput): Promise<SingleResult[]> {
|
|
@@ -223,7 +235,22 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
223
235
|
templateHasPrevious ? undefined : input.prev,
|
|
224
236
|
);
|
|
225
237
|
|
|
226
|
-
let taskStr
|
|
238
|
+
let taskStr: string;
|
|
239
|
+
try {
|
|
240
|
+
taskStr = resolveOutputReferences(taskTemplate, input.outputs);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (!(error instanceof ChainOutputValidationError)) throw error;
|
|
243
|
+
if (failFast) aborted = true;
|
|
244
|
+
return {
|
|
245
|
+
agent: task.agent,
|
|
246
|
+
task: taskTemplate,
|
|
247
|
+
exitCode: 1,
|
|
248
|
+
finalOutput: error.message,
|
|
249
|
+
error: error.message,
|
|
250
|
+
messages: [],
|
|
251
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
252
|
+
} as SingleResult;
|
|
253
|
+
}
|
|
227
254
|
taskStr = taskStr.replace(/\{task\}/g, input.originalTask);
|
|
228
255
|
taskStr = taskStr.replace(/\{previous\}/g, input.prev);
|
|
229
256
|
taskStr = taskStr.replace(/\{chain_dir\}/g, input.chainDir);
|
|
@@ -432,6 +459,21 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
432
459
|
|
|
433
460
|
const results: SingleResult[] = [];
|
|
434
461
|
const outputs: ChainOutputMap = {};
|
|
462
|
+
const chainHasStopOn = chainSteps.some((step) => step.stopOn !== undefined);
|
|
463
|
+
const outputLog: ChainTaskOutputEntry[] = [];
|
|
464
|
+
let stopRecord: StopRecord | undefined;
|
|
465
|
+
const summaryOptions = (): ChainSummaryOptions | undefined =>
|
|
466
|
+
chainHasStopOn ? { ...(stopRecord ? { stop: stopRecord } : {}), outputs: outputLog } : undefined;
|
|
467
|
+
const failureSummaryOptions = (): ChainSummaryOptions | undefined => stopRecord ? summaryOptions() : undefined;
|
|
468
|
+
const logTaskOutput = (stepIndex: number, taskIndex: number, result: SingleResult, as?: string, label?: string) => {
|
|
469
|
+
outputLog.push({
|
|
470
|
+
stepIndex, taskIndex, agent: result.agent,
|
|
471
|
+
...(as ? { as } : {}), ...(label ? { label } : {}),
|
|
472
|
+
text: getSingleResultOutput(result),
|
|
473
|
+
...(result.structuredOutput !== undefined ? { structured: result.structuredOutput } : {}),
|
|
474
|
+
exitCode: result.exitCode, ...(result.error ? { error: result.error } : {}),
|
|
475
|
+
});
|
|
476
|
+
};
|
|
435
477
|
const dynamicChildren: ChainExecutionDetailsInput["dynamicChildren"] = {};
|
|
436
478
|
const dynamicGroupStatuses: ChainExecutionDetailsInput["dynamicGroupStatuses"] = {};
|
|
437
479
|
const allProgress: AgentProgress[] = [];
|
|
@@ -459,6 +501,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
459
501
|
outputs,
|
|
460
502
|
dynamicChildren,
|
|
461
503
|
dynamicGroupStatuses,
|
|
504
|
+
stop: stopRecord,
|
|
462
505
|
...overrides,
|
|
463
506
|
});
|
|
464
507
|
|
|
@@ -602,6 +645,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
602
645
|
return buildChainExecutionErrorResult(
|
|
603
646
|
`parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, parallelCwd)}`,
|
|
604
647
|
makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }),
|
|
648
|
+
failureSummaryOptions(),
|
|
605
649
|
);
|
|
606
650
|
}
|
|
607
651
|
try {
|
|
@@ -613,7 +657,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
613
657
|
});
|
|
614
658
|
} catch (error) {
|
|
615
659
|
const message = error instanceof Error ? error.message : String(error);
|
|
616
|
-
return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
|
|
660
|
+
return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
|
|
617
661
|
}
|
|
618
662
|
}
|
|
619
663
|
|
|
@@ -627,7 +671,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
627
671
|
? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
|
|
628
672
|
: undefined;
|
|
629
673
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${step.parallel[taskIndex]!.agent})`);
|
|
630
|
-
if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
|
|
674
|
+
if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }), failureSummaryOptions());
|
|
631
675
|
}
|
|
632
676
|
progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
|
|
633
677
|
createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
|
|
@@ -689,6 +733,19 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
689
733
|
};
|
|
690
734
|
}
|
|
691
735
|
|
|
736
|
+
const diffs = captureParallelWorktreeDiffs(worktreeSetup, path.join(chainDir, "worktree-diffs", `step-${stepIndex}`), agentNames);
|
|
737
|
+
for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
|
|
738
|
+
const result = parallelResults[taskIndex]!;
|
|
739
|
+
const task = step.parallel[taskIndex];
|
|
740
|
+
logTaskOutput(stepIndex, taskIndex, result, task?.as, task?.label);
|
|
741
|
+
if (task?.as && result.exitCode === 0) outputs[task.as] = outputEntryFromResult(result, stepIndex, patchInfoForDiff(diffs[taskIndex]));
|
|
742
|
+
}
|
|
743
|
+
if (step.stopOn) {
|
|
744
|
+
const matches = evaluateStopCondition(step.stopOn, stepIndex, parallelResults.map((result, i) => ({
|
|
745
|
+
agent: result.agent, as: step.parallel[i]?.as, label: step.parallel[i]?.label, exitCode: result.exitCode, structuredOutput: result.structuredOutput,
|
|
746
|
+
})));
|
|
747
|
+
if (matches.length > 0) stopRecord = buildStopRecord(step.stopOn, stepIndex, matches);
|
|
748
|
+
}
|
|
692
749
|
const failures = parallelResults
|
|
693
750
|
.map((result, originalIndex) => ({ ...result, originalIndex }))
|
|
694
751
|
.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
|
|
@@ -700,7 +757,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
700
757
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
701
758
|
index: stepIndex,
|
|
702
759
|
error: errorMsg,
|
|
703
|
-
});
|
|
760
|
+
}, failureSummaryOptions());
|
|
704
761
|
return {
|
|
705
762
|
content: [{ type: "text", text: summary }],
|
|
706
763
|
isError: true,
|
|
@@ -711,11 +768,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
711
768
|
};
|
|
712
769
|
}
|
|
713
770
|
|
|
714
|
-
for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
|
|
715
|
-
const outputName = step.parallel[taskIndex]?.as;
|
|
716
|
-
if (outputName) outputs[outputName] = outputEntryFromResult(parallelResults[taskIndex]!, stepIndex);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
771
|
const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => {
|
|
720
772
|
const outputTarget = parallelBehaviors[i]?.output;
|
|
721
773
|
const outputTargetPath = typeof outputTarget === "string"
|
|
@@ -732,12 +784,14 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
732
784
|
};
|
|
733
785
|
});
|
|
734
786
|
prev = aggregateParallelOutputs(taskResults);
|
|
735
|
-
prev = appendParallelWorktreeSummary(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
787
|
+
prev = appendParallelWorktreeSummary(prev, diffs);
|
|
788
|
+
if (stopRecord) {
|
|
789
|
+
return {
|
|
790
|
+
content: [{ type: "text", text: buildChainSummary(chainSteps, results, chainDir, "stopped", undefined, summaryOptions()) }],
|
|
791
|
+
details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex })),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
741
795
|
} finally {
|
|
742
796
|
if (worktreeSetup) cleanupWorktrees(worktreeSetup);
|
|
743
797
|
}
|
|
@@ -748,7 +802,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
748
802
|
} catch (error) {
|
|
749
803
|
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
750
804
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
|
|
751
|
-
return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
|
|
805
|
+
return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
|
|
752
806
|
}
|
|
753
807
|
|
|
754
808
|
dynamicChildren[stepIndex] = materialized.items.map((item, itemIndex) => ({
|
|
@@ -761,19 +815,19 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
761
815
|
|
|
762
816
|
if (materialized.parallel.length === 0) {
|
|
763
817
|
const collection: DynamicCollectedResult[] = [];
|
|
764
|
-
try {
|
|
765
|
-
validateDynamicCollection(step.collect.outputSchema, collection);
|
|
766
|
-
} catch (error) {
|
|
767
|
-
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
768
|
-
dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
|
|
769
|
-
return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
|
|
770
|
-
}
|
|
771
818
|
outputs[step.collect.as] = {
|
|
772
819
|
text: JSON.stringify(collection),
|
|
773
820
|
structured: collection,
|
|
774
821
|
agent: step.parallel.agent,
|
|
775
822
|
stepIndex,
|
|
776
823
|
};
|
|
824
|
+
try {
|
|
825
|
+
validateDynamicCollection(step.collect.outputSchema, collection);
|
|
826
|
+
} catch (error) {
|
|
827
|
+
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
828
|
+
dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
|
|
829
|
+
return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
|
|
830
|
+
}
|
|
777
831
|
dynamicGroupStatuses[stepIndex] = { status: "completed" };
|
|
778
832
|
if (step.acceptance !== undefined) {
|
|
779
833
|
const effectiveGroupAcceptance = resolveEffectiveAcceptance({
|
|
@@ -796,7 +850,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
796
850
|
const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
|
|
797
851
|
if (groupAcceptanceFailure) {
|
|
798
852
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
|
|
799
|
-
return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
|
|
853
|
+
return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
|
|
800
854
|
}
|
|
801
855
|
}
|
|
802
856
|
prev = "Dynamic fanout produced 0 results.";
|
|
@@ -820,7 +874,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
820
874
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Dynamic chain step ${stepIndex + 1} item ${taskIndex + 1} (${dynamicParallelStep.parallel[taskIndex]!.agent})`);
|
|
821
875
|
if (validationError) {
|
|
822
876
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: validationError };
|
|
823
|
-
return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
|
|
877
|
+
return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }), failureSummaryOptions());
|
|
824
878
|
}
|
|
825
879
|
}
|
|
826
880
|
|
|
@@ -882,6 +936,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
882
936
|
})),
|
|
883
937
|
};
|
|
884
938
|
}
|
|
939
|
+
for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
|
|
940
|
+
const result = parallelResults[taskIndex]!;
|
|
941
|
+
const task = materialized.parallel[taskIndex];
|
|
942
|
+
logTaskOutput(stepIndex, taskIndex, result, undefined, task?.label);
|
|
943
|
+
}
|
|
944
|
+
if (step.stopOn) {
|
|
945
|
+
const matches = evaluateStopCondition(step.stopOn, stepIndex, parallelResults.map((result, i) => ({
|
|
946
|
+
agent: result.agent, label: materialized.parallel[i]?.label, exitCode: result.exitCode, structuredOutput: result.structuredOutput,
|
|
947
|
+
})));
|
|
948
|
+
if (matches.length > 0) stopRecord = buildStopRecord(step.stopOn, stepIndex, matches);
|
|
949
|
+
}
|
|
950
|
+
outputs[step.collect.as] = {
|
|
951
|
+
text: JSON.stringify(collected),
|
|
952
|
+
structured: collected,
|
|
953
|
+
agent: step.parallel.agent,
|
|
954
|
+
stepIndex,
|
|
955
|
+
};
|
|
885
956
|
const failures = parallelResults
|
|
886
957
|
.map((result, originalIndex) => ({ ...result, originalIndex }))
|
|
887
958
|
.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
|
|
@@ -894,7 +965,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
894
965
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
895
966
|
index: stepIndex,
|
|
896
967
|
error: errorMsg,
|
|
897
|
-
});
|
|
968
|
+
}, failureSummaryOptions());
|
|
898
969
|
return {
|
|
899
970
|
content: [{ type: "text", text: summary }],
|
|
900
971
|
isError: true,
|
|
@@ -909,14 +980,11 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
909
980
|
} catch (error) {
|
|
910
981
|
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
911
982
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
|
|
912
|
-
return buildChainExecutionErrorResult(
|
|
983
|
+
return buildChainExecutionErrorResult(
|
|
984
|
+
stopRecord ? buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: message }, failureSummaryOptions()) : message,
|
|
985
|
+
makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }),
|
|
986
|
+
);
|
|
913
987
|
}
|
|
914
|
-
outputs[step.collect.as] = {
|
|
915
|
-
text: JSON.stringify(collected),
|
|
916
|
-
structured: collected,
|
|
917
|
-
agent: step.parallel.agent,
|
|
918
|
-
stepIndex,
|
|
919
|
-
};
|
|
920
988
|
dynamicGroupStatuses[stepIndex] = { status: "completed" };
|
|
921
989
|
const effectiveGroupAcceptance = resolveEffectiveAcceptance({
|
|
922
990
|
explicit: step.acceptance,
|
|
@@ -938,7 +1006,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
938
1006
|
const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
|
|
939
1007
|
if (groupAcceptanceFailure) {
|
|
940
1008
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
|
|
941
|
-
return buildChainExecutionErrorResult(
|
|
1009
|
+
return buildChainExecutionErrorResult(
|
|
1010
|
+
stopRecord ? buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: groupAcceptanceFailure }, failureSummaryOptions()) : groupAcceptanceFailure,
|
|
1011
|
+
makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }),
|
|
1012
|
+
);
|
|
942
1013
|
}
|
|
943
1014
|
const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => ({
|
|
944
1015
|
agent: result.agent,
|
|
@@ -948,6 +1019,12 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
948
1019
|
error: result.error,
|
|
949
1020
|
}));
|
|
950
1021
|
prev = aggregateParallelOutputs(taskResults, (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`);
|
|
1022
|
+
if (stopRecord) {
|
|
1023
|
+
return {
|
|
1024
|
+
content: [{ type: "text", text: buildChainSummary(chainSteps, results, chainDir, "stopped", undefined, summaryOptions()) }],
|
|
1025
|
+
details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex })),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
951
1028
|
} else {
|
|
952
1029
|
const seqStep = step as SequentialStep;
|
|
953
1030
|
const stepTemplate = stepTemplates as string;
|
|
@@ -988,7 +1065,13 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
988
1065
|
templateHasPrevious ? undefined : prev,
|
|
989
1066
|
);
|
|
990
1067
|
|
|
991
|
-
let stepTask
|
|
1068
|
+
let stepTask: string;
|
|
1069
|
+
try {
|
|
1070
|
+
stepTask = resolveOutputReferences(stepTemplate, outputs);
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
if (!(error instanceof ChainOutputValidationError)) throw error;
|
|
1073
|
+
return buildChainExecutionErrorResult(error.message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
|
|
1074
|
+
}
|
|
992
1075
|
stepTask = stepTask.replace(/\{task\}/g, originalTask);
|
|
993
1076
|
stepTask = stepTask.replace(/\{previous\}/g, prev);
|
|
994
1077
|
stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
|
|
@@ -1005,7 +1088,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1005
1088
|
: undefined;
|
|
1006
1089
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
|
|
1007
1090
|
if (validationError) {
|
|
1008
|
-
return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
|
|
1091
|
+
return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }), failureSummaryOptions());
|
|
1009
1092
|
}
|
|
1010
1093
|
const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
|
|
1011
1094
|
const interruptController = new AbortController();
|
|
@@ -1106,6 +1189,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1106
1189
|
|
|
1107
1190
|
globalTaskIndex++;
|
|
1108
1191
|
results.push(r);
|
|
1192
|
+
logTaskOutput(stepIndex, 0, r, seqStep.as, seqStep.label);
|
|
1109
1193
|
if (r.progress) allProgress.push(r.progress);
|
|
1110
1194
|
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
1111
1195
|
|
|
@@ -1120,7 +1204,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1120
1204
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
1121
1205
|
index: stepIndex,
|
|
1122
1206
|
error: r.error || "Chain failed",
|
|
1123
|
-
});
|
|
1207
|
+
}, failureSummaryOptions());
|
|
1124
1208
|
return {
|
|
1125
1209
|
content: [{ type: "text", text: summary }],
|
|
1126
1210
|
details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - 1 })),
|
|
@@ -1148,10 +1232,20 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1148
1232
|
|
|
1149
1233
|
if (seqStep.as) outputs[seqStep.as] = outputEntryFromResult(r, stepIndex);
|
|
1150
1234
|
prev = getSingleResultOutput(r);
|
|
1235
|
+
if (seqStep.stopOn) {
|
|
1236
|
+
const matches = evaluateStopCondition(seqStep.stopOn, stepIndex, [{ agent: r.agent, as: seqStep.as, label: seqStep.label, exitCode: r.exitCode, structuredOutput: r.structuredOutput }], { sequential: true });
|
|
1237
|
+
if (matches.length > 0) stopRecord = buildStopRecord(seqStep.stopOn, stepIndex, matches);
|
|
1238
|
+
}
|
|
1239
|
+
if (stopRecord) {
|
|
1240
|
+
return {
|
|
1241
|
+
content: [{ type: "text", text: buildChainSummary(chainSteps, results, chainDir, "stopped", undefined, summaryOptions()) }],
|
|
1242
|
+
details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex })),
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1151
1245
|
}
|
|
1152
1246
|
}
|
|
1153
1247
|
|
|
1154
|
-
const summary = buildChainSummary(chainSteps, results, chainDir, "completed");
|
|
1248
|
+
const summary = buildChainSummary(chainSteps, results, chainDir, "completed", undefined, summaryOptions());
|
|
1155
1249
|
|
|
1156
1250
|
return {
|
|
1157
1251
|
content: [{ type: "text", text: summary }],
|
|
@@ -3,7 +3,7 @@ import { capturePiRuntime, parsePiRuntime, type PiRuntimeIdentity } from "../sha
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
6
|
-
import type
|
|
6
|
+
import { getPackageDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { AgentConfig, AgentScope } from "../../agents/agents.ts";
|
|
8
8
|
import { getArtifactsDir } from "../../shared/artifacts.ts";
|
|
9
9
|
import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
|
|
@@ -2120,7 +2120,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2120
2120
|
let runtime: PiRuntimeIdentity | undefined;
|
|
2121
2121
|
let runtimeError: unknown;
|
|
2122
2122
|
try {
|
|
2123
|
-
|
|
2123
|
+
// SDK hosts are not launched through Pi's CLI; the installation this extension was loaded from is the fallback.
|
|
2124
|
+
runtime = (deps.captureRuntime ?? (() => capturePiRuntime({ installRoot: getPackageDir() })))();
|
|
2124
2125
|
} catch (error) {
|
|
2125
2126
|
runtimeError = error;
|
|
2126
2127
|
}
|
|
@@ -2,9 +2,20 @@ import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialS
|
|
|
2
2
|
import type { ChainOutputMap, ChainOutputMapEntry, SingleResult } from "../../shared/types.ts";
|
|
3
3
|
import { getSingleResultOutput } from "../../shared/utils.ts";
|
|
4
4
|
import { DynamicFanoutError, hasDynamicFanoutFields, type DynamicFanoutConfig, validateDynamicStepShape } from "./dynamic-fanout.ts";
|
|
5
|
+
import { validateStopConditions } from "./stop-conditions.ts";
|
|
5
6
|
|
|
6
7
|
const OUTPUT_REF_PATTERN = /\{outputs\.([^}]*)\}/g;
|
|
7
8
|
const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9
|
+
const PATCH_PATH_FIELD = "patchPath";
|
|
10
|
+
|
|
11
|
+
/** `name` or `name.patchPath`; anything else is invalid. */
|
|
12
|
+
function parseOutputReference(raw: string): { name: string; field?: typeof PATCH_PATH_FIELD } | undefined {
|
|
13
|
+
const dot = raw.indexOf(".");
|
|
14
|
+
const name = dot === -1 ? raw : raw.slice(0, dot);
|
|
15
|
+
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) return undefined;
|
|
16
|
+
if (dot === -1) return { name };
|
|
17
|
+
return raw.slice(dot + 1) === PATCH_PATH_FIELD ? { name, field: PATCH_PATH_FIELD } : undefined;
|
|
18
|
+
}
|
|
8
19
|
|
|
9
20
|
export class ChainOutputValidationError extends Error {}
|
|
10
21
|
|
|
@@ -22,6 +33,10 @@ function taskTemplatesForStep(step: ChainStep): string[] {
|
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutConfig: DynamicFanoutConfig = {}): void {
|
|
36
|
+
const stopError = validateStopConditions(steps);
|
|
37
|
+
if (stopError) throw new ChainOutputValidationError(stopError);
|
|
38
|
+
const worktreeProducers = new Map<string, number>();
|
|
39
|
+
const producerSteps = new Map<string, number>();
|
|
25
40
|
const available = new Set<string>();
|
|
26
41
|
const seen = new Set<string>();
|
|
27
42
|
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
@@ -52,29 +67,39 @@ export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutCon
|
|
|
52
67
|
for (const template of taskTemplatesForStep(step)) {
|
|
53
68
|
for (const match of template.matchAll(OUTPUT_REF_PATTERN)) {
|
|
54
69
|
const rawReference = match[0];
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
70
|
+
const parsed = parseOutputReference(match[1]!);
|
|
71
|
+
if (!parsed) {
|
|
57
72
|
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${stepIndex + 1}. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
|
|
58
73
|
}
|
|
59
|
-
if (!available.has(name)) {
|
|
74
|
+
if (!available.has(parsed.name)) {
|
|
60
75
|
throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${stepIndex + 1}. Named outputs are only available after producing step/group completes.`);
|
|
61
76
|
}
|
|
77
|
+
if (parsed.field === PATCH_PATH_FIELD && !worktreeProducers.has(parsed.name)) {
|
|
78
|
+
const producerStep = producerSteps.get(parsed.name)!;
|
|
79
|
+
throw new ChainOutputValidationError(`Chain step ${stepIndex + 1} references ${rawReference} but step ${producerStep + 1} is not a worktree step.`);
|
|
80
|
+
}
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
for (const name of outputNamesForStep(step)) {
|
|
65
84
|
available.add(name);
|
|
85
|
+
producerSteps.set(name, stepIndex);
|
|
86
|
+
if (isParallelStep(step) && step.worktree) worktreeProducers.set(name, stepIndex);
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
export function resolveOutputReferences(template: string, outputs: ChainOutputMap): string {
|
|
71
|
-
return template.replace(OUTPUT_REF_PATTERN, (rawReference,
|
|
72
|
-
|
|
92
|
+
return template.replace(OUTPUT_REF_PATTERN, (rawReference, rawName: string) => {
|
|
93
|
+
const parsed = parseOutputReference(rawName);
|
|
94
|
+
if (!parsed) {
|
|
73
95
|
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}'. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
|
|
74
96
|
}
|
|
75
|
-
const entry = outputs[name];
|
|
97
|
+
const entry = outputs[parsed.name];
|
|
76
98
|
if (!entry) throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}'.`);
|
|
77
|
-
return entry.text;
|
|
99
|
+
if (parsed.field !== PATCH_PATH_FIELD) return entry.text;
|
|
100
|
+
if (entry.patchPath) return entry.patchPath;
|
|
101
|
+
const reason = entry.patchError ? `capture failed: ${entry.patchError}` : "no changes";
|
|
102
|
+
throw new ChainOutputValidationError(`Output '${parsed.name}' has no patch (${reason}).`);
|
|
78
103
|
});
|
|
79
104
|
}
|
|
80
105
|
|
|
@@ -82,8 +107,22 @@ function compactStructuredText(value: unknown): string {
|
|
|
82
107
|
return JSON.stringify(value);
|
|
83
108
|
}
|
|
84
109
|
|
|
85
|
-
export
|
|
110
|
+
export interface OutputEntryPatchInfo {
|
|
111
|
+
patchPath?: string;
|
|
112
|
+
patchError?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function patchFields(patch: OutputEntryPatchInfo | undefined): Pick<ChainOutputMapEntry, "patchPath" | "patchError"> {
|
|
116
|
+
if (!patch) return {};
|
|
117
|
+
return {
|
|
118
|
+
...(patch.patchPath ? { patchPath: patch.patchPath } : {}),
|
|
119
|
+
...(patch.patchError ? { patchError: patch.patchError } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function outputEntryFromResult(result: SingleResult, stepIndex: number, patch?: OutputEntryPatchInfo): ChainOutputMapEntry {
|
|
86
124
|
return {
|
|
125
|
+
...patchFields(patch),
|
|
87
126
|
text: result.structuredOutput !== undefined ? compactStructuredText(result.structuredOutput) : getSingleResultOutput(result),
|
|
88
127
|
...(result.structuredOutput !== undefined ? { structured: result.structuredOutput } : {}),
|
|
89
128
|
agent: result.agent,
|
|
@@ -91,8 +130,9 @@ export function outputEntryFromResult(result: SingleResult, stepIndex: number):
|
|
|
91
130
|
};
|
|
92
131
|
}
|
|
93
132
|
|
|
94
|
-
export function outputEntryFromAsyncResult(result: { agent: string; output: string; structuredOutput?: unknown }, stepIndex: number): ChainOutputMapEntry {
|
|
133
|
+
export function outputEntryFromAsyncResult(result: { agent: string; output: string; structuredOutput?: unknown }, stepIndex: number, patch?: OutputEntryPatchInfo): ChainOutputMapEntry {
|
|
95
134
|
return {
|
|
135
|
+
...patchFields(patch),
|
|
96
136
|
text: result.structuredOutput !== undefined ? compactStructuredText(result.structuredOutput) : result.output,
|
|
97
137
|
...(result.structuredOutput !== undefined ? { structured: result.structuredOutput } : {}),
|
|
98
138
|
agent: result.agent,
|
|
@@ -40,7 +40,7 @@ const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
40
40
|
const ITEM_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
41
41
|
const ITEM_REF_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)(?:\.([^{}]+))?\}/g;
|
|
42
42
|
const RESERVED_TEMPLATE_NAMES = new Set(["task", "previous", "chain_dir", "outputs"]);
|
|
43
|
-
const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency", "failFast", "phase", "label", "acceptance"]);
|
|
43
|
+
const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency", "failFast", "phase", "label", "acceptance", "stopOn"]);
|
|
44
44
|
const RUNNER_DYNAMIC_STEP_KEYS = new Set([...DYNAMIC_STEP_KEYS, "effectiveAcceptance"]);
|
|
45
45
|
const DYNAMIC_EXPAND_KEYS = new Set(["from", "item", "key", "maxItems", "onEmpty"]);
|
|
46
46
|
const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
|
|
@@ -28,6 +28,7 @@ export interface RunnerSubagentStep {
|
|
|
28
28
|
};
|
|
29
29
|
structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
|
|
30
30
|
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
31
|
+
stopOn?: import("../../shared/types.ts").StopCondition;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export interface ParallelStepGroup {
|
|
@@ -35,6 +36,7 @@ export interface ParallelStepGroup {
|
|
|
35
36
|
concurrency?: number;
|
|
36
37
|
failFast?: boolean;
|
|
37
38
|
worktree?: boolean;
|
|
39
|
+
stopOn?: import("../../shared/types.ts").StopCondition;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface DynamicRunnerGroup {
|
|
@@ -46,6 +48,7 @@ export interface DynamicRunnerGroup {
|
|
|
46
48
|
phase?: string;
|
|
47
49
|
label?: string;
|
|
48
50
|
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
51
|
+
stopOn?: import("../../shared/types.ts").StopCondition;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
|