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
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdtempSync, unlinkSync } from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
7
9
|
import type { Message } from "@earendil-works/pi-ai";
|
|
8
10
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
9
11
|
import {
|
|
@@ -13,10 +15,13 @@ import {
|
|
|
13
15
|
writeMetadata,
|
|
14
16
|
} from "../../shared/artifacts.ts";
|
|
15
17
|
import {
|
|
18
|
+
type AcceptanceFinalizationTurn,
|
|
19
|
+
type AcceptanceLedger,
|
|
16
20
|
type AgentProgress,
|
|
17
21
|
type ArtifactPaths,
|
|
18
22
|
type ControlEvent,
|
|
19
23
|
type ModelAttempt,
|
|
24
|
+
type ResolvedAcceptanceConfig,
|
|
20
25
|
type RunSyncOptions,
|
|
21
26
|
type SingleResult,
|
|
22
27
|
type Usage,
|
|
@@ -41,11 +46,12 @@ import {
|
|
|
41
46
|
extractTextFromContent,
|
|
42
47
|
} from "../../shared/utils.ts";
|
|
43
48
|
import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
44
|
-
import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
49
|
+
import { evaluateCompletionMutationGuard, resolveCompletionPolicy, type CompletionPolicy } from "../shared/completion-guard.ts";
|
|
45
50
|
import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
|
|
46
51
|
import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
|
|
47
52
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
48
53
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
54
|
+
import { readStructuredOutput } from "../shared/structured-output.ts";
|
|
49
55
|
import { captureSingleOutputSnapshot, formatSavedOutputReference, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
50
56
|
import {
|
|
51
57
|
buildModelCandidates,
|
|
@@ -63,8 +69,23 @@ import {
|
|
|
63
69
|
shouldEscalateMutatingFailures,
|
|
64
70
|
summarizeRecentMutatingFailures,
|
|
65
71
|
} from "../shared/long-running-guard.ts";
|
|
72
|
+
import {
|
|
73
|
+
acceptanceFailureMessage,
|
|
74
|
+
acceptanceSelfReviewConfig,
|
|
75
|
+
attachFinalizationToLedger,
|
|
76
|
+
buildFinalizationProcessFailureLedger,
|
|
77
|
+
createFinalizationProcessFailureTurn,
|
|
78
|
+
createFinalizationTurn,
|
|
79
|
+
evaluateAcceptance,
|
|
80
|
+
formatAcceptanceFinalizationPrompt,
|
|
81
|
+
formatAcceptancePrompt,
|
|
82
|
+
resolveEffectiveAcceptance,
|
|
83
|
+
shouldRunAcceptanceFinalization,
|
|
84
|
+
stripAcceptanceReport,
|
|
85
|
+
} from "../shared/acceptance.ts";
|
|
66
86
|
|
|
67
87
|
const artifactOutputByResult = new WeakMap<SingleResult, string>();
|
|
88
|
+
const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
|
|
68
89
|
|
|
69
90
|
function emptyUsage(): Usage {
|
|
70
91
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
@@ -87,6 +108,17 @@ function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
|
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
|
|
111
|
+
function stripAcceptanceReportsFromMessages(messages: Message[] | undefined): void {
|
|
112
|
+
for (const message of messages ?? []) {
|
|
113
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
114
|
+
for (const part of message.content) {
|
|
115
|
+
if (part.type === "text" && "text" in part && typeof part.text === "string") {
|
|
116
|
+
part.text = stripAcceptanceReport(part.text);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
90
122
|
function snapshotProgress(progress: AgentProgress): AgentProgress {
|
|
91
123
|
return {
|
|
92
124
|
...progress,
|
|
@@ -133,6 +165,8 @@ async function runSingleAttempt(
|
|
|
133
165
|
artifactPaths?: ArtifactPaths;
|
|
134
166
|
attemptNotes: string[];
|
|
135
167
|
outputSnapshot?: SingleOutputSnapshot;
|
|
168
|
+
originalTask?: string;
|
|
169
|
+
completionPolicy: CompletionPolicy;
|
|
136
170
|
},
|
|
137
171
|
): Promise<SingleResult> {
|
|
138
172
|
const modelArg = applyThinkingSuffix(model, agent.thinking);
|
|
@@ -162,11 +196,12 @@ async function runSingleAttempt(
|
|
|
162
196
|
parentControlInbox: options.nestedRoute?.controlInbox,
|
|
163
197
|
parentRootRunId: options.nestedRoute?.rootRunId,
|
|
164
198
|
parentCapabilityToken: options.nestedRoute?.capabilityToken,
|
|
199
|
+
structuredOutput: options.structuredOutput,
|
|
165
200
|
});
|
|
166
201
|
|
|
167
202
|
const result: SingleResult = {
|
|
168
203
|
agent: agent.name,
|
|
169
|
-
task,
|
|
204
|
+
task: shared.originalTask ?? task,
|
|
170
205
|
exitCode: 0,
|
|
171
206
|
messages: [],
|
|
172
207
|
usage: emptyUsage(),
|
|
@@ -176,6 +211,13 @@ async function runSingleAttempt(
|
|
|
176
211
|
skillsWarning: shared.skillsWarning,
|
|
177
212
|
};
|
|
178
213
|
const startTime = Date.now();
|
|
214
|
+
if (options.structuredOutput) {
|
|
215
|
+
try {
|
|
216
|
+
if (existsSync(options.structuredOutput.outputPath)) unlinkSync(options.structuredOutput.outputPath);
|
|
217
|
+
} catch {
|
|
218
|
+
// Missing/stale structured-output files are handled after the child exits.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
179
221
|
const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
180
222
|
let interruptedByControl = false;
|
|
181
223
|
const allControlEvents: ControlEvent[] = [];
|
|
@@ -655,6 +697,21 @@ async function runSingleAttempt(
|
|
|
655
697
|
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
656
698
|
}
|
|
657
699
|
}
|
|
700
|
+
if (options.structuredOutput && result.exitCode === 0 && !result.error) {
|
|
701
|
+
const structured = readStructuredOutput({
|
|
702
|
+
schema: options.structuredOutput.schema,
|
|
703
|
+
schemaPath: options.structuredOutput.schemaPath,
|
|
704
|
+
outputPath: options.structuredOutput.outputPath,
|
|
705
|
+
});
|
|
706
|
+
result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
|
|
707
|
+
result.structuredOutputPath = options.structuredOutput.outputPath;
|
|
708
|
+
if (structured.error) {
|
|
709
|
+
result.exitCode = 1;
|
|
710
|
+
result.error = structured.error;
|
|
711
|
+
} else {
|
|
712
|
+
result.structuredOutput = structured.value;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
658
715
|
|
|
659
716
|
progress.status = result.exitCode === 0 ? "completed" : "failed";
|
|
660
717
|
progress.durationMs = Date.now() - startTime;
|
|
@@ -671,17 +728,19 @@ async function runSingleAttempt(
|
|
|
671
728
|
durationMs: progress.durationMs,
|
|
672
729
|
};
|
|
673
730
|
|
|
674
|
-
|
|
675
|
-
|
|
731
|
+
const acceptanceOutput = getFinalOutput(result.messages);
|
|
732
|
+
let fullOutput = stripAcceptanceReport(acceptanceOutput);
|
|
733
|
+
const completionGuard = result.exitCode === 0 && !result.error && shared.completionPolicy === "mutation-guard"
|
|
676
734
|
? evaluateCompletionMutationGuard({
|
|
677
735
|
agent: agent.name,
|
|
678
|
-
task,
|
|
736
|
+
task: shared.originalTask ?? task,
|
|
679
737
|
messages: result.messages,
|
|
680
738
|
tools: agent.tools,
|
|
681
739
|
mcpDirectTools: agent.mcpDirectTools,
|
|
682
740
|
})
|
|
683
741
|
: undefined;
|
|
684
|
-
|
|
742
|
+
const completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
|
|
743
|
+
if (completionGuardTriggered) {
|
|
685
744
|
result.exitCode = 1;
|
|
686
745
|
result.error = "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
|
|
687
746
|
progress.status = "failed";
|
|
@@ -699,7 +758,7 @@ async function runSingleAttempt(
|
|
|
699
758
|
}
|
|
700
759
|
if (options.outputPath && result.exitCode === 0) {
|
|
701
760
|
const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
|
|
702
|
-
fullOutput = resolvedOutput.fullOutput;
|
|
761
|
+
fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
|
|
703
762
|
result.savedOutputPath = resolvedOutput.savedPath;
|
|
704
763
|
result.outputSaveError = resolvedOutput.saveError;
|
|
705
764
|
if (resolvedOutput.savedPath) {
|
|
@@ -707,6 +766,7 @@ async function runSingleAttempt(
|
|
|
707
766
|
}
|
|
708
767
|
}
|
|
709
768
|
artifactOutputByResult.set(result, fullOutput);
|
|
769
|
+
acceptanceOutputByResult.set(result, acceptanceOutput);
|
|
710
770
|
result.outputMode = options.outputMode ?? "inline";
|
|
711
771
|
result.finalOutput = options.outputMode === "file-only" && result.savedOutputPath && result.outputReference
|
|
712
772
|
? result.outputReference.message
|
|
@@ -729,6 +789,99 @@ async function runSingleAttempt(
|
|
|
729
789
|
return result;
|
|
730
790
|
}
|
|
731
791
|
|
|
792
|
+
async function runAcceptanceFinalizationLoop(input: {
|
|
793
|
+
runtimeCwd: string;
|
|
794
|
+
agent: AgentConfig;
|
|
795
|
+
result: SingleResult;
|
|
796
|
+
initialLedger: AcceptanceLedger;
|
|
797
|
+
initialOutput: string;
|
|
798
|
+
acceptance: ResolvedAcceptanceConfig;
|
|
799
|
+
options: RunSyncOptions;
|
|
800
|
+
systemPrompt: string;
|
|
801
|
+
resolvedSkillNames?: string[];
|
|
802
|
+
skillsWarning?: string;
|
|
803
|
+
}): Promise<AcceptanceLedger> {
|
|
804
|
+
const sessionFile = input.result.sessionFile ?? input.options.sessionFile;
|
|
805
|
+
const maxTurns = input.acceptance.finalization.maxTurns;
|
|
806
|
+
const turns: AcceptanceFinalizationTurn[] = [];
|
|
807
|
+
if (!sessionFile) {
|
|
808
|
+
const message = "Acceptance finalization requires a session file for same-session continuation.";
|
|
809
|
+
turns.push(createFinalizationProcessFailureTurn({ turn: 1, prompt: "", message }));
|
|
810
|
+
return buildFinalizationProcessFailureLedger({ initialLedger: input.initialLedger, turns, maxTurns, message });
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const selfReviewAcceptance = acceptanceSelfReviewConfig(input.acceptance);
|
|
814
|
+
let previousFailure = acceptanceFailureMessage(input.initialLedger);
|
|
815
|
+
let authoritativeLedger = input.initialLedger;
|
|
816
|
+
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
817
|
+
const prompt = formatAcceptanceFinalizationPrompt({
|
|
818
|
+
acceptance: input.acceptance,
|
|
819
|
+
initialOutput: input.initialOutput,
|
|
820
|
+
initialLedger: input.initialLedger,
|
|
821
|
+
turn,
|
|
822
|
+
maxTurns,
|
|
823
|
+
...(previousFailure ? { previousFailure } : {}),
|
|
824
|
+
});
|
|
825
|
+
const finalizationOptions: RunSyncOptions = { ...input.options, sessionFile, outputMode: "inline" };
|
|
826
|
+
delete finalizationOptions.sessionDir;
|
|
827
|
+
delete finalizationOptions.outputPath;
|
|
828
|
+
delete finalizationOptions.structuredOutput;
|
|
829
|
+
delete finalizationOptions.onUpdate;
|
|
830
|
+
finalizationOptions.allowIntercomDetach = false;
|
|
831
|
+
const finalizationResult = await runSingleAttempt(
|
|
832
|
+
input.runtimeCwd,
|
|
833
|
+
input.agent,
|
|
834
|
+
prompt,
|
|
835
|
+
input.result.model,
|
|
836
|
+
finalizationOptions,
|
|
837
|
+
{
|
|
838
|
+
sessionEnabled: true,
|
|
839
|
+
systemPrompt: input.systemPrompt,
|
|
840
|
+
resolvedSkillNames: input.resolvedSkillNames,
|
|
841
|
+
skillsWarning: input.skillsWarning,
|
|
842
|
+
attemptNotes: [],
|
|
843
|
+
originalTask: prompt,
|
|
844
|
+
completionPolicy: "acceptance-contract",
|
|
845
|
+
},
|
|
846
|
+
);
|
|
847
|
+
sumUsage(input.result.usage, finalizationResult.usage);
|
|
848
|
+
input.result.progressSummary = {
|
|
849
|
+
toolCount: (input.result.progressSummary?.toolCount ?? 0) + (finalizationResult.progressSummary?.toolCount ?? 0),
|
|
850
|
+
tokens: input.result.usage.input + input.result.usage.output,
|
|
851
|
+
durationMs: (input.result.progressSummary?.durationMs ?? 0) + (finalizationResult.progressSummary?.durationMs ?? 0),
|
|
852
|
+
};
|
|
853
|
+
if (finalizationResult.controlEvents?.length) {
|
|
854
|
+
input.result.controlEvents = [...(input.result.controlEvents ?? []), ...finalizationResult.controlEvents];
|
|
855
|
+
}
|
|
856
|
+
const rawOutput = acceptanceOutputByResult.get(finalizationResult) ?? getFinalOutput(finalizationResult.messages) ?? finalizationResult.finalOutput ?? "";
|
|
857
|
+
if (finalizationResult.exitCode !== 0 || finalizationResult.error || finalizationResult.detached || finalizationResult.interrupted) {
|
|
858
|
+
const message = finalizationResult.error ?? "Acceptance finalization turn did not complete successfully.";
|
|
859
|
+
turns.push(createFinalizationProcessFailureTurn({ turn, prompt, rawOutput, message }));
|
|
860
|
+
return buildFinalizationProcessFailureLedger({ initialLedger: input.initialLedger, turns, maxTurns, message });
|
|
861
|
+
}
|
|
862
|
+
const selfReviewLedger = await evaluateAcceptance({
|
|
863
|
+
acceptance: selfReviewAcceptance,
|
|
864
|
+
output: rawOutput,
|
|
865
|
+
cwd: input.options.cwd ?? input.runtimeCwd,
|
|
866
|
+
});
|
|
867
|
+
authoritativeLedger = selfReviewLedger;
|
|
868
|
+
turns.push(createFinalizationTurn({ turn, prompt, rawOutput, ledger: selfReviewLedger }));
|
|
869
|
+
const failure = acceptanceFailureMessage(selfReviewLedger);
|
|
870
|
+
if (!failure) {
|
|
871
|
+
authoritativeLedger = input.acceptance === selfReviewAcceptance
|
|
872
|
+
? selfReviewLedger
|
|
873
|
+
: await evaluateAcceptance({
|
|
874
|
+
acceptance: input.acceptance,
|
|
875
|
+
output: rawOutput,
|
|
876
|
+
cwd: input.options.cwd ?? input.runtimeCwd,
|
|
877
|
+
});
|
|
878
|
+
return attachFinalizationToLedger({ initialLedger: input.initialLedger, authoritativeLedger, turns, status: "completed", maxTurns });
|
|
879
|
+
}
|
|
880
|
+
previousFailure = failure;
|
|
881
|
+
}
|
|
882
|
+
return attachFinalizationToLedger({ initialLedger: input.initialLedger, authoritativeLedger, turns, status: "failed", maxTurns });
|
|
883
|
+
}
|
|
884
|
+
|
|
732
885
|
/**
|
|
733
886
|
* Run a subagent synchronously (blocking until complete)
|
|
734
887
|
*/
|
|
@@ -764,6 +917,21 @@ export async function runSync(
|
|
|
764
917
|
}
|
|
765
918
|
|
|
766
919
|
const shareEnabled = options.share === true;
|
|
920
|
+
const effectiveAcceptance = resolveEffectiveAcceptance({
|
|
921
|
+
explicit: options.acceptance,
|
|
922
|
+
agentName,
|
|
923
|
+
task,
|
|
924
|
+
mode: options.acceptanceContext?.mode ?? "single",
|
|
925
|
+
async: options.acceptanceContext?.async,
|
|
926
|
+
dynamic: options.acceptanceContext?.dynamic,
|
|
927
|
+
dynamicGroup: options.acceptanceContext?.dynamicGroup,
|
|
928
|
+
});
|
|
929
|
+
if (shouldRunAcceptanceFinalization(effectiveAcceptance) && !options.sessionFile) {
|
|
930
|
+
const sessionDir = options.sessionDir ?? mkdtempSync(path.join(os.tmpdir(), "pi-subagent-finalization-"));
|
|
931
|
+
options.sessionFile = path.join(sessionDir, "session.jsonl");
|
|
932
|
+
}
|
|
933
|
+
const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance);
|
|
934
|
+
const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
|
|
767
935
|
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
768
936
|
const skillNames = options.skills ?? agent.skills ?? [];
|
|
769
937
|
const skillCwd = options.cwd ?? runtimeCwd;
|
|
@@ -803,7 +971,7 @@ export async function runSync(
|
|
|
803
971
|
artifactPathsResult = getArtifactPaths(options.artifactsDir, options.runId, agentName, options.index);
|
|
804
972
|
ensureArtifactsDir(options.artifactsDir);
|
|
805
973
|
if (options.artifactConfig?.includeInput !== false) {
|
|
806
|
-
|
|
974
|
+
writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${taskWithAcceptance}`);
|
|
807
975
|
}
|
|
808
976
|
if (options.artifactConfig?.includeJsonl !== false) {
|
|
809
977
|
jsonlPath = artifactPathsResult.jsonlPath;
|
|
@@ -816,7 +984,7 @@ export async function runSync(
|
|
|
816
984
|
const candidate = modelsToTry[i];
|
|
817
985
|
if (candidate) attemptedModels.push(candidate);
|
|
818
986
|
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
819
|
-
const result = await runSingleAttempt(runtimeCwd, agent,
|
|
987
|
+
const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, options, {
|
|
820
988
|
sessionEnabled,
|
|
821
989
|
systemPrompt,
|
|
822
990
|
resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
|
|
@@ -825,6 +993,15 @@ export async function runSync(
|
|
|
825
993
|
artifactPaths: artifactPathsResult,
|
|
826
994
|
attemptNotes,
|
|
827
995
|
outputSnapshot,
|
|
996
|
+
originalTask: task,
|
|
997
|
+
completionPolicy: resolveCompletionPolicy({
|
|
998
|
+
agent: agent.name,
|
|
999
|
+
task,
|
|
1000
|
+
completionGuardEnabled: agent.completionGuard !== false,
|
|
1001
|
+
usesAcceptanceContract: effectiveAcceptance.explicit,
|
|
1002
|
+
tools: agent.tools,
|
|
1003
|
+
mcpDirectTools: agent.mcpDirectTools,
|
|
1004
|
+
}),
|
|
828
1005
|
});
|
|
829
1006
|
lastResult = result;
|
|
830
1007
|
sumUsage(aggregateUsage, result.usage);
|
|
@@ -914,5 +1091,40 @@ export async function runSync(
|
|
|
914
1091
|
if (sessionFile) result.sessionFile = sessionFile;
|
|
915
1092
|
}
|
|
916
1093
|
|
|
1094
|
+
const initialAcceptanceOutput = acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "";
|
|
1095
|
+
const acceptanceForInitialReport = shouldRunAcceptanceFinalization(effectiveAcceptance)
|
|
1096
|
+
? acceptanceSelfReviewConfig(effectiveAcceptance)
|
|
1097
|
+
: effectiveAcceptance;
|
|
1098
|
+
const initialAcceptance = await evaluateAcceptance({
|
|
1099
|
+
acceptance: acceptanceForInitialReport,
|
|
1100
|
+
output: initialAcceptanceOutput,
|
|
1101
|
+
cwd: options.cwd ?? runtimeCwd,
|
|
1102
|
+
});
|
|
1103
|
+
result.acceptance = initialAcceptance;
|
|
1104
|
+
if (shouldRunAcceptanceFinalization(effectiveAcceptance) && result.exitCode === 0 && !result.detached && !result.interrupted) {
|
|
1105
|
+
result.acceptance = await runAcceptanceFinalizationLoop({
|
|
1106
|
+
runtimeCwd,
|
|
1107
|
+
agent,
|
|
1108
|
+
result,
|
|
1109
|
+
initialLedger: initialAcceptance,
|
|
1110
|
+
initialOutput: initialAcceptanceOutput,
|
|
1111
|
+
acceptance: effectiveAcceptance,
|
|
1112
|
+
options,
|
|
1113
|
+
systemPrompt,
|
|
1114
|
+
resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
|
|
1115
|
+
...(missingSkills.length > 0 ? { skillsWarning: `Skills not found: ${missingSkills.join(", ")}` } : {}),
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
|
|
1119
|
+
stripAcceptanceReportsFromMessages(result.messages);
|
|
1120
|
+
if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted) {
|
|
1121
|
+
result.exitCode = 1;
|
|
1122
|
+
result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
|
|
1123
|
+
if (result.progress) {
|
|
1124
|
+
result.progress.status = "failed";
|
|
1125
|
+
result.progress.error = result.error;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
917
1129
|
return result;
|
|
918
1130
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
writeInitialProgressFile,
|
|
22
22
|
getStepAgents,
|
|
23
23
|
isParallelStep,
|
|
24
|
+
isDynamicParallelStep,
|
|
24
25
|
resolveStepBehavior,
|
|
25
26
|
suppressProgressForReadOnlyTask,
|
|
26
27
|
taskDisallowsFileUpdates,
|
|
@@ -52,6 +53,7 @@ import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/
|
|
|
52
53
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
53
54
|
import { inspectSubagentStatus } from "../background/run-status.ts";
|
|
54
55
|
import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
|
|
56
|
+
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
55
57
|
import {
|
|
56
58
|
cleanupWorktrees,
|
|
57
59
|
createWorktrees,
|
|
@@ -63,6 +65,7 @@ import {
|
|
|
63
65
|
} from "../shared/worktree.ts";
|
|
64
66
|
import {
|
|
65
67
|
type AgentProgress,
|
|
68
|
+
type AcceptanceInput,
|
|
66
69
|
type ArtifactConfig,
|
|
67
70
|
type ArtifactPaths,
|
|
68
71
|
type ControlConfig,
|
|
@@ -103,6 +106,7 @@ interface TaskParam {
|
|
|
103
106
|
progress?: boolean;
|
|
104
107
|
model?: string;
|
|
105
108
|
skill?: string | string[] | boolean;
|
|
109
|
+
acceptance?: AcceptanceInput;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
export interface SubagentParamsLike {
|
|
@@ -134,6 +138,7 @@ export interface SubagentParamsLike {
|
|
|
134
138
|
outputMode?: "inline" | "file-only";
|
|
135
139
|
agentScope?: unknown;
|
|
136
140
|
chainDir?: string;
|
|
141
|
+
acceptance?: AcceptanceInput;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
interface ExecutorDeps {
|
|
@@ -756,6 +761,36 @@ async function maybeBuildForegroundIntercomReceipt(input: {
|
|
|
756
761
|
};
|
|
757
762
|
}
|
|
758
763
|
|
|
764
|
+
function validationErrorResult(mode: Details["mode"], text: string): AgentToolResult<Details> {
|
|
765
|
+
return { content: [{ type: "text", text }], isError: true, details: { mode, results: [] } };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function validateAcceptanceForExecution(params: SubagentParamsLike): AgentToolResult<Details> | null {
|
|
769
|
+
const topLevelErrors = validateAcceptanceInput(params.acceptance);
|
|
770
|
+
if (topLevelErrors.length > 0) return validationErrorResult("single", topLevelErrors.join(" "));
|
|
771
|
+
for (const [index, task] of (params.tasks ?? []).entries()) {
|
|
772
|
+
const errors = validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`);
|
|
773
|
+
if (errors.length > 0) return validationErrorResult("parallel", errors.join(" "));
|
|
774
|
+
}
|
|
775
|
+
for (const [stepIndex, step] of (params.chain ?? []).entries()) {
|
|
776
|
+
if (isParallelStep(step)) {
|
|
777
|
+
if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
|
|
778
|
+
for (const [taskIndex, task] of step.parallel.entries()) {
|
|
779
|
+
const errors = validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`);
|
|
780
|
+
if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
|
|
781
|
+
}
|
|
782
|
+
} else if (isDynamicParallelStep(step)) {
|
|
783
|
+
if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on dynamic fanout groups; set acceptance on chain[${stepIndex}].parallel.acceptance for each materialized child.`);
|
|
784
|
+
const errors = validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`);
|
|
785
|
+
if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
|
|
786
|
+
} else {
|
|
787
|
+
const stepErrors = validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`);
|
|
788
|
+
if (stepErrors.length > 0) return validationErrorResult("chain", stepErrors.join(" "));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
|
|
759
794
|
function validateExecutionInput(
|
|
760
795
|
params: SubagentParamsLike,
|
|
761
796
|
agents: AgentConfig[],
|
|
@@ -764,6 +799,9 @@ function validateExecutionInput(
|
|
|
764
799
|
hasSingle: boolean,
|
|
765
800
|
allowClarifyTaskPrompt: boolean,
|
|
766
801
|
): AgentToolResult<Details> | null {
|
|
802
|
+
const acceptanceError = validateAcceptanceForExecution(params);
|
|
803
|
+
if (acceptanceError) return acceptanceError;
|
|
804
|
+
|
|
767
805
|
if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
|
|
768
806
|
return {
|
|
769
807
|
content: [
|
|
@@ -816,6 +854,12 @@ function validateExecutionInput(
|
|
|
816
854
|
details: { mode: "chain" as const, results: [] },
|
|
817
855
|
};
|
|
818
856
|
}
|
|
857
|
+
} else if (isDynamicParallelStep(firstStep)) {
|
|
858
|
+
return {
|
|
859
|
+
content: [{ type: "text", text: "First step in chain cannot be dynamic fanout; expand.from requires a prior structured named output" }],
|
|
860
|
+
isError: true,
|
|
861
|
+
details: { mode: "chain" as const, results: [] },
|
|
862
|
+
};
|
|
819
863
|
} else if (!(firstStep as SequentialStep).task && !params.task && !allowClarifyTaskPrompt) {
|
|
820
864
|
return {
|
|
821
865
|
content: [{ type: "text", text: "First step in chain must have a task" }],
|
|
@@ -977,6 +1021,10 @@ function collectChainSessionFiles(
|
|
|
977
1021
|
}
|
|
978
1022
|
continue;
|
|
979
1023
|
}
|
|
1024
|
+
if (isDynamicParallelStep(step)) {
|
|
1025
|
+
sessionFiles.push(undefined);
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
980
1028
|
sessionFiles.push(sessionFileForIndex(flatIndex));
|
|
981
1029
|
flatIndex++;
|
|
982
1030
|
}
|
|
@@ -995,6 +1043,15 @@ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["
|
|
|
995
1043
|
})),
|
|
996
1044
|
};
|
|
997
1045
|
}
|
|
1046
|
+
if (isDynamicParallelStep(step)) {
|
|
1047
|
+
return {
|
|
1048
|
+
...step,
|
|
1049
|
+
parallel: {
|
|
1050
|
+
...step.parallel,
|
|
1051
|
+
task: wrapForkTask(step.parallel.task ?? "{previous}"),
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
998
1055
|
const sequential = step as SequentialStep;
|
|
999
1056
|
return {
|
|
1000
1057
|
...sequential,
|
|
@@ -1082,6 +1139,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1082
1139
|
...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
|
|
1083
1140
|
...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
|
|
1084
1141
|
...(task.progress !== undefined ? { progress: task.progress } : {}),
|
|
1142
|
+
...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
|
|
1085
1143
|
}));
|
|
1086
1144
|
return executeAsyncChain(id, {
|
|
1087
1145
|
chain: [{
|
|
@@ -1129,6 +1187,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1129
1187
|
sessionRoot,
|
|
1130
1188
|
chainSkills,
|
|
1131
1189
|
sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex),
|
|
1190
|
+
dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
|
|
1132
1191
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1133
1192
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1134
1193
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
@@ -1179,6 +1238,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1179
1238
|
controlIntercomTarget,
|
|
1180
1239
|
childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
|
|
1181
1240
|
nestedRoute,
|
|
1241
|
+
acceptance: params.acceptance,
|
|
1182
1242
|
});
|
|
1183
1243
|
}
|
|
1184
1244
|
|
|
@@ -1234,6 +1294,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1234
1294
|
nestedRoute: foregroundControl?.nestedRoute,
|
|
1235
1295
|
chainSkills,
|
|
1236
1296
|
chainDir: params.chainDir,
|
|
1297
|
+
dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
|
|
1237
1298
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1238
1299
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1239
1300
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
@@ -1269,6 +1330,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1269
1330
|
sessionRoot,
|
|
1270
1331
|
chainSkills: chainResult.requestedAsync.chainSkills,
|
|
1271
1332
|
sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForIndex),
|
|
1333
|
+
dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
|
|
1272
1334
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1273
1335
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1274
1336
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
@@ -1491,6 +1553,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1491
1553
|
availableModels: input.availableModels,
|
|
1492
1554
|
preferredModelProvider: input.ctx.model?.provider,
|
|
1493
1555
|
skills: effectiveSkills === false ? [] : effectiveSkills,
|
|
1556
|
+
acceptance: task.acceptance,
|
|
1557
|
+
acceptanceContext: { mode: "parallel" },
|
|
1494
1558
|
onUpdate: input.onUpdate
|
|
1495
1559
|
? (progressUpdate) => {
|
|
1496
1560
|
const stepResults = progressUpdate.details?.results || [];
|
|
@@ -1680,6 +1744,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1680
1744
|
...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
|
|
1681
1745
|
...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
|
|
1682
1746
|
...(progress !== undefined ? { progress } : {}),
|
|
1747
|
+
...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
|
|
1683
1748
|
};
|
|
1684
1749
|
});
|
|
1685
1750
|
return executeAsyncChain(id, {
|
|
@@ -2053,6 +2118,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2053
2118
|
availableModels,
|
|
2054
2119
|
preferredModelProvider: currentProvider,
|
|
2055
2120
|
skills: effectiveSkills,
|
|
2121
|
+
acceptance: params.acceptance,
|
|
2122
|
+
acceptanceContext: { mode: "single" },
|
|
2056
2123
|
});
|
|
2057
2124
|
if (foregroundControl?.currentIndex === 0) {
|
|
2058
2125
|
foregroundControl.interrupt = undefined;
|