pi-subagents 0.25.0 → 0.28.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 +34 -0
- package/README.md +175 -19
- 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 +60 -17
- package/src/agents/agent-management.ts +71 -15
- package/src/agents/agent-serializer.ts +13 -2
- package/src/agents/agents.ts +88 -17
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +2 -0
- package/src/extension/index.ts +5 -2
- package/src/extension/schemas.ts +132 -6
- package/src/intercom/result-intercom.ts +5 -0
- package/src/runs/background/async-execution.ts +88 -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 +665 -39
- package/src/runs/foreground/chain-execution.ts +369 -118
- package/src/runs/foreground/execution.ts +392 -19
- package/src/runs/foreground/subagent-executor.ts +126 -3
- package/src/runs/shared/acceptance-contract.ts +318 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +173 -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 +33 -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 +210 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +265 -1
- package/src/shared/utils.ts +7 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +178 -45
|
@@ -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,
|
|
@@ -39,13 +44,15 @@ import {
|
|
|
39
44
|
detectSubagentError,
|
|
40
45
|
extractToolArgsPreview,
|
|
41
46
|
extractTextFromContent,
|
|
47
|
+
formatResourceLimitExceeded,
|
|
42
48
|
} from "../../shared/utils.ts";
|
|
43
49
|
import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
44
|
-
import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
50
|
+
import { evaluateCompletionMutationGuard, resolveCompletionPolicy, type CompletionPolicy } from "../shared/completion-guard.ts";
|
|
45
51
|
import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
|
|
46
52
|
import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
|
|
47
53
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
48
54
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
55
|
+
import { readStructuredOutput } from "../shared/structured-output.ts";
|
|
49
56
|
import { captureSingleOutputSnapshot, formatSavedOutputReference, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
50
57
|
import {
|
|
51
58
|
buildModelCandidates,
|
|
@@ -63,8 +70,23 @@ import {
|
|
|
63
70
|
shouldEscalateMutatingFailures,
|
|
64
71
|
summarizeRecentMutatingFailures,
|
|
65
72
|
} from "../shared/long-running-guard.ts";
|
|
73
|
+
import {
|
|
74
|
+
acceptanceFailureMessage,
|
|
75
|
+
acceptanceSelfReviewConfig,
|
|
76
|
+
attachFinalizationToLedger,
|
|
77
|
+
buildFinalizationProcessFailureLedger,
|
|
78
|
+
createFinalizationProcessFailureTurn,
|
|
79
|
+
createFinalizationTurn,
|
|
80
|
+
evaluateAcceptance,
|
|
81
|
+
formatAcceptanceFinalizationPrompt,
|
|
82
|
+
formatAcceptancePrompt,
|
|
83
|
+
resolveEffectiveAcceptance,
|
|
84
|
+
shouldRunAcceptanceFinalization,
|
|
85
|
+
stripAcceptanceReport,
|
|
86
|
+
} from "../shared/acceptance.ts";
|
|
66
87
|
|
|
67
88
|
const artifactOutputByResult = new WeakMap<SingleResult, string>();
|
|
89
|
+
const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
|
|
68
90
|
|
|
69
91
|
function emptyUsage(): Usage {
|
|
70
92
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
@@ -87,6 +109,54 @@ function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
|
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
111
|
|
|
112
|
+
const FOREGROUND_TIMEOUT_EXIT_CODE = 124;
|
|
113
|
+
|
|
114
|
+
function formatForegroundTimeoutMessage(timeoutMs: number | undefined): string {
|
|
115
|
+
return timeoutMs ? `Timed out after ${timeoutMs}ms.` : "Timed out.";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function createTimedOutResult(agent: string, task: string, options: RunSyncOptions): SingleResult {
|
|
119
|
+
const message = formatForegroundTimeoutMessage(options.timeoutMs);
|
|
120
|
+
return {
|
|
121
|
+
agent,
|
|
122
|
+
task,
|
|
123
|
+
exitCode: FOREGROUND_TIMEOUT_EXIT_CODE,
|
|
124
|
+
messages: [],
|
|
125
|
+
usage: emptyUsage(),
|
|
126
|
+
error: message,
|
|
127
|
+
finalOutput: message,
|
|
128
|
+
timedOut: true,
|
|
129
|
+
progress: {
|
|
130
|
+
index: options.index ?? 0,
|
|
131
|
+
agent,
|
|
132
|
+
status: "failed",
|
|
133
|
+
task,
|
|
134
|
+
recentTools: [],
|
|
135
|
+
recentOutput: [message],
|
|
136
|
+
toolCount: 0,
|
|
137
|
+
tokens: 0,
|
|
138
|
+
durationMs: 0,
|
|
139
|
+
lastActivityAt: Date.now(),
|
|
140
|
+
},
|
|
141
|
+
progressSummary: {
|
|
142
|
+
toolCount: 0,
|
|
143
|
+
tokens: 0,
|
|
144
|
+
durationMs: 0,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function stripAcceptanceReportsFromMessages(messages: Message[] | undefined): void {
|
|
150
|
+
for (const message of messages ?? []) {
|
|
151
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
152
|
+
for (const part of message.content) {
|
|
153
|
+
if (part.type === "text" && "text" in part && typeof part.text === "string") {
|
|
154
|
+
part.text = stripAcceptanceReport(part.text);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
90
160
|
function snapshotProgress(progress: AgentProgress): AgentProgress {
|
|
91
161
|
return {
|
|
92
162
|
...progress,
|
|
@@ -133,6 +203,8 @@ async function runSingleAttempt(
|
|
|
133
203
|
artifactPaths?: ArtifactPaths;
|
|
134
204
|
attemptNotes: string[];
|
|
135
205
|
outputSnapshot?: SingleOutputSnapshot;
|
|
206
|
+
originalTask?: string;
|
|
207
|
+
completionPolicy: CompletionPolicy;
|
|
136
208
|
},
|
|
137
209
|
): Promise<SingleResult> {
|
|
138
210
|
const modelArg = applyThinkingSuffix(model, agent.thinking);
|
|
@@ -162,11 +234,12 @@ async function runSingleAttempt(
|
|
|
162
234
|
parentControlInbox: options.nestedRoute?.controlInbox,
|
|
163
235
|
parentRootRunId: options.nestedRoute?.rootRunId,
|
|
164
236
|
parentCapabilityToken: options.nestedRoute?.capabilityToken,
|
|
237
|
+
structuredOutput: options.structuredOutput,
|
|
165
238
|
});
|
|
166
239
|
|
|
167
240
|
const result: SingleResult = {
|
|
168
241
|
agent: agent.name,
|
|
169
|
-
task,
|
|
242
|
+
task: shared.originalTask ?? task,
|
|
170
243
|
exitCode: 0,
|
|
171
244
|
messages: [],
|
|
172
245
|
usage: emptyUsage(),
|
|
@@ -176,6 +249,13 @@ async function runSingleAttempt(
|
|
|
176
249
|
skillsWarning: shared.skillsWarning,
|
|
177
250
|
};
|
|
178
251
|
const startTime = Date.now();
|
|
252
|
+
if (options.structuredOutput) {
|
|
253
|
+
try {
|
|
254
|
+
if (existsSync(options.structuredOutput.outputPath)) unlinkSync(options.structuredOutput.outputPath);
|
|
255
|
+
} catch {
|
|
256
|
+
// Missing/stale structured-output files are handled after the child exits.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
179
259
|
const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
180
260
|
let interruptedByControl = false;
|
|
181
261
|
const allControlEvents: ControlEvent[] = [];
|
|
@@ -221,6 +301,12 @@ async function runSingleAttempt(
|
|
|
221
301
|
let detached = false;
|
|
222
302
|
let intercomStarted = false;
|
|
223
303
|
let assistantError: string | undefined;
|
|
304
|
+
let timedOut = false;
|
|
305
|
+
let resourceLimited = false;
|
|
306
|
+
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
307
|
+
let timeoutEscalationTimer: NodeJS.Timeout | undefined;
|
|
308
|
+
let resourceLimitTimer: NodeJS.Timeout | undefined;
|
|
309
|
+
let resourceLimitEscalationTimer: NodeJS.Timeout | undefined;
|
|
224
310
|
let removeAbortListener: (() => void) | undefined;
|
|
225
311
|
let removeInterruptListener: (() => void) | undefined;
|
|
226
312
|
let activityTimer: NodeJS.Timeout | undefined;
|
|
@@ -292,6 +378,22 @@ async function runSingleAttempt(
|
|
|
292
378
|
settled = true;
|
|
293
379
|
clearFinalDrainTimers();
|
|
294
380
|
clearStdioGuard();
|
|
381
|
+
if (timeoutTimer) {
|
|
382
|
+
clearTimeout(timeoutTimer);
|
|
383
|
+
timeoutTimer = undefined;
|
|
384
|
+
}
|
|
385
|
+
if (timeoutEscalationTimer) {
|
|
386
|
+
clearTimeout(timeoutEscalationTimer);
|
|
387
|
+
timeoutEscalationTimer = undefined;
|
|
388
|
+
}
|
|
389
|
+
if (resourceLimitTimer) {
|
|
390
|
+
clearTimeout(resourceLimitTimer);
|
|
391
|
+
resourceLimitTimer = undefined;
|
|
392
|
+
}
|
|
393
|
+
if (resourceLimitEscalationTimer) {
|
|
394
|
+
clearTimeout(resourceLimitEscalationTimer);
|
|
395
|
+
resourceLimitEscalationTimer = undefined;
|
|
396
|
+
}
|
|
295
397
|
if (activityTimer) {
|
|
296
398
|
clearInterval(activityTimer);
|
|
297
399
|
activityTimer = undefined;
|
|
@@ -386,6 +488,26 @@ async function runSingleAttempt(
|
|
|
386
488
|
};
|
|
387
489
|
|
|
388
490
|
|
|
491
|
+
const triggerResourceLimit = (kind: "maxExecutionTimeMs" | "maxTokens", limit: number, observed?: number) => {
|
|
492
|
+
if (processClosed || detached || settled || timedOut || resourceLimited) return;
|
|
493
|
+
resourceLimited = true;
|
|
494
|
+
const message = formatResourceLimitExceeded({ agent: agent.name, kind, limit, observed });
|
|
495
|
+
result.resourceLimitExceeded = { kind, limit, ...(observed !== undefined ? { observed } : {}), message };
|
|
496
|
+
result.error = message;
|
|
497
|
+
result.finalOutput = message;
|
|
498
|
+
progress.status = "failed";
|
|
499
|
+
progress.durationMs = Date.now() - startTime;
|
|
500
|
+
appendRecentOutput(progress, [message]);
|
|
501
|
+
progress.activityState = undefined;
|
|
502
|
+
fireUpdate();
|
|
503
|
+
trySignalChild(proc, "SIGINT");
|
|
504
|
+
resourceLimitEscalationTimer = setTimeout(() => {
|
|
505
|
+
if (settled || processClosed || detached) return;
|
|
506
|
+
trySignalChild(proc, "SIGTERM");
|
|
507
|
+
}, 1000);
|
|
508
|
+
resourceLimitEscalationTimer.unref?.();
|
|
509
|
+
};
|
|
510
|
+
|
|
389
511
|
const emitUpdateSnapshot = (text: string) => {
|
|
390
512
|
if (!options.onUpdate || processClosed) return;
|
|
391
513
|
const progressSnapshot = snapshotProgress(progress);
|
|
@@ -470,6 +592,9 @@ async function runSingleAttempt(
|
|
|
470
592
|
result.usage.cacheWrite += u.cacheWrite || 0;
|
|
471
593
|
result.usage.cost += u.cost?.total || 0;
|
|
472
594
|
progress.tokens = result.usage.input + result.usage.output;
|
|
595
|
+
if (options.maxTokens !== undefined && progress.tokens >= options.maxTokens) {
|
|
596
|
+
triggerResourceLimit("maxTokens", options.maxTokens, progress.tokens);
|
|
597
|
+
}
|
|
473
598
|
}
|
|
474
599
|
if (!result.model && evt.message.model) result.model = evt.message.model;
|
|
475
600
|
if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
|
|
@@ -598,9 +723,45 @@ async function runSingleAttempt(
|
|
|
598
723
|
}
|
|
599
724
|
}
|
|
600
725
|
|
|
726
|
+
if (options.timeoutAt !== undefined) {
|
|
727
|
+
const triggerTimeout = () => {
|
|
728
|
+
if (processClosed || detached || settled || timedOut || resourceLimited) return;
|
|
729
|
+
timedOut = true;
|
|
730
|
+
const message = formatForegroundTimeoutMessage(options.timeoutMs);
|
|
731
|
+
result.timedOut = true;
|
|
732
|
+
result.error = message;
|
|
733
|
+
result.finalOutput = message;
|
|
734
|
+
progress.status = "failed";
|
|
735
|
+
progress.durationMs = Date.now() - startTime;
|
|
736
|
+
appendRecentOutput(progress, [message]);
|
|
737
|
+
progress.activityState = undefined;
|
|
738
|
+
fireUpdate();
|
|
739
|
+
trySignalChild(proc, "SIGINT");
|
|
740
|
+
timeoutEscalationTimer = setTimeout(() => {
|
|
741
|
+
if (settled || processClosed || detached) return;
|
|
742
|
+
trySignalChild(proc, "SIGTERM");
|
|
743
|
+
}, 1000);
|
|
744
|
+
timeoutEscalationTimer.unref?.();
|
|
745
|
+
};
|
|
746
|
+
const delay = options.timeoutAt - Date.now();
|
|
747
|
+
if (delay <= 0) triggerTimeout();
|
|
748
|
+
else {
|
|
749
|
+
timeoutTimer = setTimeout(triggerTimeout, delay);
|
|
750
|
+
timeoutTimer.unref?.();
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (options.maxExecutionTimeMs !== undefined) {
|
|
755
|
+
const maxExecutionTimeMs = options.maxExecutionTimeMs;
|
|
756
|
+
resourceLimitTimer = setTimeout(() => {
|
|
757
|
+
triggerResourceLimit("maxExecutionTimeMs", maxExecutionTimeMs);
|
|
758
|
+
}, maxExecutionTimeMs);
|
|
759
|
+
resourceLimitTimer.unref?.();
|
|
760
|
+
}
|
|
761
|
+
|
|
601
762
|
if (options.interruptSignal) {
|
|
602
763
|
const interrupt = () => {
|
|
603
|
-
if (processClosed || detached || settled) return;
|
|
764
|
+
if (processClosed || detached || settled || timedOut || resourceLimited) return;
|
|
604
765
|
interruptedByControl = true;
|
|
605
766
|
progress.status = "running";
|
|
606
767
|
progress.durationMs = Date.now() - startTime;
|
|
@@ -622,6 +783,40 @@ async function runSingleAttempt(
|
|
|
622
783
|
}
|
|
623
784
|
});
|
|
624
785
|
result.exitCode = exitCode;
|
|
786
|
+
if (result.resourceLimitExceeded) {
|
|
787
|
+
result.exitCode = 1;
|
|
788
|
+
result.error = result.error ?? result.resourceLimitExceeded.message;
|
|
789
|
+
result.finalOutput = result.finalOutput || result.error;
|
|
790
|
+
if (result.progress) {
|
|
791
|
+
result.progress.status = "failed";
|
|
792
|
+
result.progress.activityState = undefined;
|
|
793
|
+
result.progress.durationMs = Date.now() - startTime;
|
|
794
|
+
}
|
|
795
|
+
result.progressSummary = {
|
|
796
|
+
toolCount: progress.toolCount,
|
|
797
|
+
tokens: progress.tokens,
|
|
798
|
+
durationMs: result.progress?.durationMs ?? Date.now() - startTime,
|
|
799
|
+
};
|
|
800
|
+
result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
803
|
+
if (result.timedOut) {
|
|
804
|
+
result.exitCode = FOREGROUND_TIMEOUT_EXIT_CODE;
|
|
805
|
+
result.error = result.error ?? formatForegroundTimeoutMessage(options.timeoutMs);
|
|
806
|
+
result.finalOutput = result.finalOutput || result.error;
|
|
807
|
+
if (result.progress) {
|
|
808
|
+
result.progress.status = "failed";
|
|
809
|
+
result.progress.activityState = undefined;
|
|
810
|
+
result.progress.durationMs = Date.now() - startTime;
|
|
811
|
+
}
|
|
812
|
+
result.progressSummary = {
|
|
813
|
+
toolCount: progress.toolCount,
|
|
814
|
+
tokens: progress.tokens,
|
|
815
|
+
durationMs: result.progress?.durationMs ?? Date.now() - startTime,
|
|
816
|
+
};
|
|
817
|
+
result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
625
820
|
if (interruptedByControl) {
|
|
626
821
|
result.exitCode = 0;
|
|
627
822
|
result.interrupted = true;
|
|
@@ -655,6 +850,21 @@ async function runSingleAttempt(
|
|
|
655
850
|
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
656
851
|
}
|
|
657
852
|
}
|
|
853
|
+
if (options.structuredOutput && result.exitCode === 0 && !result.error) {
|
|
854
|
+
const structured = readStructuredOutput({
|
|
855
|
+
schema: options.structuredOutput.schema,
|
|
856
|
+
schemaPath: options.structuredOutput.schemaPath,
|
|
857
|
+
outputPath: options.structuredOutput.outputPath,
|
|
858
|
+
});
|
|
859
|
+
result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
|
|
860
|
+
result.structuredOutputPath = options.structuredOutput.outputPath;
|
|
861
|
+
if (structured.error) {
|
|
862
|
+
result.exitCode = 1;
|
|
863
|
+
result.error = structured.error;
|
|
864
|
+
} else {
|
|
865
|
+
result.structuredOutput = structured.value;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
658
868
|
|
|
659
869
|
progress.status = result.exitCode === 0 ? "completed" : "failed";
|
|
660
870
|
progress.durationMs = Date.now() - startTime;
|
|
@@ -671,17 +881,19 @@ async function runSingleAttempt(
|
|
|
671
881
|
durationMs: progress.durationMs,
|
|
672
882
|
};
|
|
673
883
|
|
|
674
|
-
|
|
675
|
-
|
|
884
|
+
const acceptanceOutput = getFinalOutput(result.messages);
|
|
885
|
+
let fullOutput = stripAcceptanceReport(acceptanceOutput);
|
|
886
|
+
const completionGuard = result.exitCode === 0 && !result.error && shared.completionPolicy === "mutation-guard"
|
|
676
887
|
? evaluateCompletionMutationGuard({
|
|
677
888
|
agent: agent.name,
|
|
678
|
-
task,
|
|
889
|
+
task: shared.originalTask ?? task,
|
|
679
890
|
messages: result.messages,
|
|
680
891
|
tools: agent.tools,
|
|
681
892
|
mcpDirectTools: agent.mcpDirectTools,
|
|
682
893
|
})
|
|
683
894
|
: undefined;
|
|
684
|
-
|
|
895
|
+
const completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
|
|
896
|
+
if (completionGuardTriggered) {
|
|
685
897
|
result.exitCode = 1;
|
|
686
898
|
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
899
|
progress.status = "failed";
|
|
@@ -699,7 +911,7 @@ async function runSingleAttempt(
|
|
|
699
911
|
}
|
|
700
912
|
if (options.outputPath && result.exitCode === 0) {
|
|
701
913
|
const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
|
|
702
|
-
fullOutput = resolvedOutput.fullOutput;
|
|
914
|
+
fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
|
|
703
915
|
result.savedOutputPath = resolvedOutput.savedPath;
|
|
704
916
|
result.outputSaveError = resolvedOutput.saveError;
|
|
705
917
|
if (resolvedOutput.savedPath) {
|
|
@@ -707,6 +919,7 @@ async function runSingleAttempt(
|
|
|
707
919
|
}
|
|
708
920
|
}
|
|
709
921
|
artifactOutputByResult.set(result, fullOutput);
|
|
922
|
+
acceptanceOutputByResult.set(result, acceptanceOutput);
|
|
710
923
|
result.outputMode = options.outputMode ?? "inline";
|
|
711
924
|
result.finalOutput = options.outputMode === "file-only" && result.savedOutputPath && result.outputReference
|
|
712
925
|
? result.outputReference.message
|
|
@@ -729,6 +942,99 @@ async function runSingleAttempt(
|
|
|
729
942
|
return result;
|
|
730
943
|
}
|
|
731
944
|
|
|
945
|
+
async function runAcceptanceFinalizationLoop(input: {
|
|
946
|
+
runtimeCwd: string;
|
|
947
|
+
agent: AgentConfig;
|
|
948
|
+
result: SingleResult;
|
|
949
|
+
initialLedger: AcceptanceLedger;
|
|
950
|
+
initialOutput: string;
|
|
951
|
+
acceptance: ResolvedAcceptanceConfig;
|
|
952
|
+
options: RunSyncOptions;
|
|
953
|
+
systemPrompt: string;
|
|
954
|
+
resolvedSkillNames?: string[];
|
|
955
|
+
skillsWarning?: string;
|
|
956
|
+
}): Promise<AcceptanceLedger> {
|
|
957
|
+
const sessionFile = input.result.sessionFile ?? input.options.sessionFile;
|
|
958
|
+
const maxTurns = input.acceptance.finalization.maxTurns;
|
|
959
|
+
const turns: AcceptanceFinalizationTurn[] = [];
|
|
960
|
+
if (!sessionFile) {
|
|
961
|
+
const message = "Acceptance finalization requires a session file for same-session continuation.";
|
|
962
|
+
turns.push(createFinalizationProcessFailureTurn({ turn: 1, prompt: "", message }));
|
|
963
|
+
return buildFinalizationProcessFailureLedger({ initialLedger: input.initialLedger, turns, maxTurns, message });
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const selfReviewAcceptance = acceptanceSelfReviewConfig(input.acceptance);
|
|
967
|
+
let previousFailure = acceptanceFailureMessage(input.initialLedger);
|
|
968
|
+
let authoritativeLedger = input.initialLedger;
|
|
969
|
+
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
970
|
+
const prompt = formatAcceptanceFinalizationPrompt({
|
|
971
|
+
acceptance: input.acceptance,
|
|
972
|
+
initialOutput: input.initialOutput,
|
|
973
|
+
initialLedger: input.initialLedger,
|
|
974
|
+
turn,
|
|
975
|
+
maxTurns,
|
|
976
|
+
...(previousFailure ? { previousFailure } : {}),
|
|
977
|
+
});
|
|
978
|
+
const finalizationOptions: RunSyncOptions = { ...input.options, sessionFile, outputMode: "inline" };
|
|
979
|
+
delete finalizationOptions.sessionDir;
|
|
980
|
+
delete finalizationOptions.outputPath;
|
|
981
|
+
delete finalizationOptions.structuredOutput;
|
|
982
|
+
delete finalizationOptions.onUpdate;
|
|
983
|
+
finalizationOptions.allowIntercomDetach = false;
|
|
984
|
+
const finalizationResult = await runSingleAttempt(
|
|
985
|
+
input.runtimeCwd,
|
|
986
|
+
input.agent,
|
|
987
|
+
prompt,
|
|
988
|
+
input.result.model,
|
|
989
|
+
finalizationOptions,
|
|
990
|
+
{
|
|
991
|
+
sessionEnabled: true,
|
|
992
|
+
systemPrompt: input.systemPrompt,
|
|
993
|
+
resolvedSkillNames: input.resolvedSkillNames,
|
|
994
|
+
skillsWarning: input.skillsWarning,
|
|
995
|
+
attemptNotes: [],
|
|
996
|
+
originalTask: prompt,
|
|
997
|
+
completionPolicy: "acceptance-contract",
|
|
998
|
+
},
|
|
999
|
+
);
|
|
1000
|
+
sumUsage(input.result.usage, finalizationResult.usage);
|
|
1001
|
+
input.result.progressSummary = {
|
|
1002
|
+
toolCount: (input.result.progressSummary?.toolCount ?? 0) + (finalizationResult.progressSummary?.toolCount ?? 0),
|
|
1003
|
+
tokens: input.result.usage.input + input.result.usage.output,
|
|
1004
|
+
durationMs: (input.result.progressSummary?.durationMs ?? 0) + (finalizationResult.progressSummary?.durationMs ?? 0),
|
|
1005
|
+
};
|
|
1006
|
+
if (finalizationResult.controlEvents?.length) {
|
|
1007
|
+
input.result.controlEvents = [...(input.result.controlEvents ?? []), ...finalizationResult.controlEvents];
|
|
1008
|
+
}
|
|
1009
|
+
const rawOutput = acceptanceOutputByResult.get(finalizationResult) ?? getFinalOutput(finalizationResult.messages) ?? finalizationResult.finalOutput ?? "";
|
|
1010
|
+
if (finalizationResult.exitCode !== 0 || finalizationResult.error || finalizationResult.detached || finalizationResult.interrupted) {
|
|
1011
|
+
const message = finalizationResult.error ?? "Acceptance finalization turn did not complete successfully.";
|
|
1012
|
+
turns.push(createFinalizationProcessFailureTurn({ turn, prompt, rawOutput, message }));
|
|
1013
|
+
return buildFinalizationProcessFailureLedger({ initialLedger: input.initialLedger, turns, maxTurns, message });
|
|
1014
|
+
}
|
|
1015
|
+
const selfReviewLedger = await evaluateAcceptance({
|
|
1016
|
+
acceptance: selfReviewAcceptance,
|
|
1017
|
+
output: rawOutput,
|
|
1018
|
+
cwd: input.options.cwd ?? input.runtimeCwd,
|
|
1019
|
+
});
|
|
1020
|
+
authoritativeLedger = selfReviewLedger;
|
|
1021
|
+
turns.push(createFinalizationTurn({ turn, prompt, rawOutput, ledger: selfReviewLedger }));
|
|
1022
|
+
const failure = acceptanceFailureMessage(selfReviewLedger);
|
|
1023
|
+
if (!failure) {
|
|
1024
|
+
authoritativeLedger = input.acceptance === selfReviewAcceptance
|
|
1025
|
+
? selfReviewLedger
|
|
1026
|
+
: await evaluateAcceptance({
|
|
1027
|
+
acceptance: input.acceptance,
|
|
1028
|
+
output: rawOutput,
|
|
1029
|
+
cwd: input.options.cwd ?? input.runtimeCwd,
|
|
1030
|
+
});
|
|
1031
|
+
return attachFinalizationToLedger({ initialLedger: input.initialLedger, authoritativeLedger, turns, status: "completed", maxTurns });
|
|
1032
|
+
}
|
|
1033
|
+
previousFailure = failure;
|
|
1034
|
+
}
|
|
1035
|
+
return attachFinalizationToLedger({ initialLedger: input.initialLedger, authoritativeLedger, turns, status: "failed", maxTurns });
|
|
1036
|
+
}
|
|
1037
|
+
|
|
732
1038
|
/**
|
|
733
1039
|
* Run a subagent synchronously (blocking until complete)
|
|
734
1040
|
*/
|
|
@@ -762,8 +1068,31 @@ export async function runSync(
|
|
|
762
1068
|
error: outputModeValidationError,
|
|
763
1069
|
};
|
|
764
1070
|
}
|
|
1071
|
+
if (options.timeoutAt !== undefined && Date.now() >= options.timeoutAt) {
|
|
1072
|
+
return createTimedOutResult(agentName, task, options);
|
|
1073
|
+
}
|
|
1074
|
+
const effectiveOptions: RunSyncOptions = {
|
|
1075
|
+
...options,
|
|
1076
|
+
maxExecutionTimeMs: options.maxExecutionTimeMs ?? agent.maxExecutionTimeMs,
|
|
1077
|
+
maxTokens: options.maxTokens ?? agent.maxTokens,
|
|
1078
|
+
};
|
|
765
1079
|
|
|
766
|
-
const shareEnabled =
|
|
1080
|
+
const shareEnabled = effectiveOptions.share === true;
|
|
1081
|
+
const effectiveAcceptance = resolveEffectiveAcceptance({
|
|
1082
|
+
explicit: options.acceptance,
|
|
1083
|
+
agentName,
|
|
1084
|
+
task,
|
|
1085
|
+
mode: options.acceptanceContext?.mode ?? "single",
|
|
1086
|
+
async: options.acceptanceContext?.async,
|
|
1087
|
+
dynamic: options.acceptanceContext?.dynamic,
|
|
1088
|
+
dynamicGroup: options.acceptanceContext?.dynamicGroup,
|
|
1089
|
+
});
|
|
1090
|
+
if (shouldRunAcceptanceFinalization(effectiveAcceptance) && !options.sessionFile) {
|
|
1091
|
+
const sessionDir = options.sessionDir ?? mkdtempSync(path.join(os.tmpdir(), "pi-subagent-finalization-"));
|
|
1092
|
+
options.sessionFile = path.join(sessionDir, "session.jsonl");
|
|
1093
|
+
}
|
|
1094
|
+
const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance);
|
|
1095
|
+
const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
|
|
767
1096
|
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
768
1097
|
const skillNames = options.skills ?? agent.skills ?? [];
|
|
769
1098
|
const skillCwd = options.cwd ?? runtimeCwd;
|
|
@@ -799,13 +1128,13 @@ export async function runSync(
|
|
|
799
1128
|
|
|
800
1129
|
let artifactPathsResult: ArtifactPaths | undefined;
|
|
801
1130
|
let jsonlPath: string | undefined;
|
|
802
|
-
if (
|
|
803
|
-
artifactPathsResult = getArtifactPaths(
|
|
804
|
-
ensureArtifactsDir(
|
|
805
|
-
if (
|
|
806
|
-
|
|
1131
|
+
if (effectiveOptions.artifactsDir && effectiveOptions.artifactConfig?.enabled !== false) {
|
|
1132
|
+
artifactPathsResult = getArtifactPaths(effectiveOptions.artifactsDir, effectiveOptions.runId, agentName, effectiveOptions.index);
|
|
1133
|
+
ensureArtifactsDir(effectiveOptions.artifactsDir);
|
|
1134
|
+
if (effectiveOptions.artifactConfig?.includeInput !== false) {
|
|
1135
|
+
writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${taskWithAcceptance}`);
|
|
807
1136
|
}
|
|
808
|
-
if (
|
|
1137
|
+
if (effectiveOptions.artifactConfig?.includeJsonl !== false) {
|
|
809
1138
|
jsonlPath = artifactPathsResult.jsonlPath;
|
|
810
1139
|
}
|
|
811
1140
|
}
|
|
@@ -815,8 +1144,8 @@ export async function runSync(
|
|
|
815
1144
|
for (let i = 0; i < modelsToTry.length; i++) {
|
|
816
1145
|
const candidate = modelsToTry[i];
|
|
817
1146
|
if (candidate) attemptedModels.push(candidate);
|
|
818
|
-
const outputSnapshot = captureSingleOutputSnapshot(
|
|
819
|
-
const result = await runSingleAttempt(runtimeCwd, agent,
|
|
1147
|
+
const outputSnapshot = captureSingleOutputSnapshot(effectiveOptions.outputPath);
|
|
1148
|
+
const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, effectiveOptions, {
|
|
820
1149
|
sessionEnabled,
|
|
821
1150
|
systemPrompt,
|
|
822
1151
|
resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
|
|
@@ -825,6 +1154,15 @@ export async function runSync(
|
|
|
825
1154
|
artifactPaths: artifactPathsResult,
|
|
826
1155
|
attemptNotes,
|
|
827
1156
|
outputSnapshot,
|
|
1157
|
+
originalTask: task,
|
|
1158
|
+
completionPolicy: resolveCompletionPolicy({
|
|
1159
|
+
agent: agent.name,
|
|
1160
|
+
task,
|
|
1161
|
+
completionGuardEnabled: agent.completionGuard !== false,
|
|
1162
|
+
usesAcceptanceContract: effectiveAcceptance.explicit,
|
|
1163
|
+
tools: agent.tools,
|
|
1164
|
+
mcpDirectTools: agent.mcpDirectTools,
|
|
1165
|
+
}),
|
|
828
1166
|
});
|
|
829
1167
|
lastResult = result;
|
|
830
1168
|
sumUsage(aggregateUsage, result.usage);
|
|
@@ -842,7 +1180,7 @@ export async function runSync(
|
|
|
842
1180
|
if (attemptSucceeded) {
|
|
843
1181
|
break;
|
|
844
1182
|
}
|
|
845
|
-
if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
|
|
1183
|
+
if (result.timedOut || result.resourceLimitExceeded || !isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
|
|
846
1184
|
break;
|
|
847
1185
|
}
|
|
848
1186
|
attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
|
|
@@ -914,5 +1252,40 @@ export async function runSync(
|
|
|
914
1252
|
if (sessionFile) result.sessionFile = sessionFile;
|
|
915
1253
|
}
|
|
916
1254
|
|
|
1255
|
+
const initialAcceptanceOutput = acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "";
|
|
1256
|
+
const acceptanceForInitialReport = shouldRunAcceptanceFinalization(effectiveAcceptance)
|
|
1257
|
+
? acceptanceSelfReviewConfig(effectiveAcceptance)
|
|
1258
|
+
: effectiveAcceptance;
|
|
1259
|
+
const initialAcceptance = await evaluateAcceptance({
|
|
1260
|
+
acceptance: acceptanceForInitialReport,
|
|
1261
|
+
output: initialAcceptanceOutput,
|
|
1262
|
+
cwd: options.cwd ?? runtimeCwd,
|
|
1263
|
+
});
|
|
1264
|
+
result.acceptance = initialAcceptance;
|
|
1265
|
+
if (shouldRunAcceptanceFinalization(effectiveAcceptance) && result.exitCode === 0 && !result.detached && !result.interrupted) {
|
|
1266
|
+
result.acceptance = await runAcceptanceFinalizationLoop({
|
|
1267
|
+
runtimeCwd,
|
|
1268
|
+
agent,
|
|
1269
|
+
result,
|
|
1270
|
+
initialLedger: initialAcceptance,
|
|
1271
|
+
initialOutput: initialAcceptanceOutput,
|
|
1272
|
+
acceptance: effectiveAcceptance,
|
|
1273
|
+
options,
|
|
1274
|
+
systemPrompt,
|
|
1275
|
+
resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
|
|
1276
|
+
...(missingSkills.length > 0 ? { skillsWarning: `Skills not found: ${missingSkills.join(", ")}` } : {}),
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
|
|
1280
|
+
stripAcceptanceReportsFromMessages(result.messages);
|
|
1281
|
+
if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted) {
|
|
1282
|
+
result.exitCode = 1;
|
|
1283
|
+
result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
|
|
1284
|
+
if (result.progress) {
|
|
1285
|
+
result.progress.status = "failed";
|
|
1286
|
+
result.progress.error = result.error;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
917
1290
|
return result;
|
|
918
1291
|
}
|