pi-subagents 0.37.2 → 0.38.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 +18 -0
- package/LICENSE +21 -0
- package/README.md +16 -4
- package/package.json +1 -1
- package/src/agents/agents.ts +58 -8
- package/src/extension/index.ts +3 -0
- package/src/extension/rpc.ts +221 -4
- package/src/extension/schemas.ts +13 -1
- package/src/intercom/intercom-bridge.ts +4 -2
- package/src/runs/background/control-channel.ts +8 -1
- package/src/runs/background/subagent-runner.ts +71 -11
- package/src/runs/foreground/chain-clarify.ts +3 -3
- package/src/runs/foreground/execution.ts +149 -51
- package/src/runs/foreground/foreground-control.ts +12 -0
- package/src/runs/foreground/subagent-executor.ts +1 -0
- package/src/runs/shared/acceptance.ts +15 -7
- package/src/runs/shared/pi-args.ts +4 -0
- package/src/runs/shared/subagent-startup-retry.ts +101 -0
- package/src/shared/types.ts +16 -0
- package/src/shared/utils.ts +43 -0
- package/src/slash/slash-commands.ts +3 -3
- package/src/tui/fleet-status.ts +13 -6
|
@@ -65,6 +65,13 @@ import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
|
65
65
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
66
66
|
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
67
67
|
import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
68
|
+
import {
|
|
69
|
+
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
70
|
+
formatSubagentStartupRetryExhaustedError,
|
|
71
|
+
formatSubagentStartupRetryNote,
|
|
72
|
+
isRetryableSubagentStartupFailure,
|
|
73
|
+
waitForSubagentStartupRetry,
|
|
74
|
+
} from "../shared/subagent-startup-retry.ts";
|
|
68
75
|
import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
|
|
69
76
|
import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
|
|
70
77
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
@@ -395,6 +402,8 @@ interface RunPiStreamingResult {
|
|
|
395
402
|
exitCode: number | null;
|
|
396
403
|
messages: Message[];
|
|
397
404
|
usage: Usage;
|
|
405
|
+
toolCount: number;
|
|
406
|
+
durationMs: number;
|
|
398
407
|
model?: string;
|
|
399
408
|
error?: string;
|
|
400
409
|
protocolError?: ProtocolOutputLimit;
|
|
@@ -434,6 +443,7 @@ function runPiStreaming(
|
|
|
434
443
|
onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
|
|
435
444
|
): Promise<RunPiStreamingResult> {
|
|
436
445
|
return new Promise((resolve) => {
|
|
446
|
+
const startedAt = Date.now();
|
|
437
447
|
const processInstanceId = randomUUID();
|
|
438
448
|
onWriterProcess?.({ state: "spawning" });
|
|
439
449
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
@@ -471,6 +481,7 @@ function runPiStreaming(
|
|
|
471
481
|
let turnBudgetMessage: string | undefined;
|
|
472
482
|
let turnBudget: TurnBudgetState | undefined;
|
|
473
483
|
let observedMutationAttempt = false;
|
|
484
|
+
let toolCount = 0;
|
|
474
485
|
const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
|
|
475
486
|
let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
|
|
476
487
|
let applyChildLifecycle = (_action: ChildLifecycleAction): void => {};
|
|
@@ -557,6 +568,7 @@ function runPiStreaming(
|
|
|
557
568
|
onChildEvent?.(event);
|
|
558
569
|
|
|
559
570
|
if (event.type === "tool_execution_start" && event.toolName) {
|
|
571
|
+
toolCount += 1;
|
|
560
572
|
observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args);
|
|
561
573
|
const toolArgs = extractToolArgsPreview(event.args ?? {});
|
|
562
574
|
writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
|
|
@@ -796,6 +808,8 @@ function runPiStreaming(
|
|
|
796
808
|
exitCode: timedOut || stopped ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
797
809
|
messages,
|
|
798
810
|
usage,
|
|
811
|
+
toolCount,
|
|
812
|
+
durationMs: Date.now() - startedAt,
|
|
799
813
|
model,
|
|
800
814
|
error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
801
815
|
protocolError,
|
|
@@ -833,7 +847,7 @@ function runPiStreaming(
|
|
|
833
847
|
const stderr = stderrTail.text();
|
|
834
848
|
const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
|
|
835
849
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
836
|
-
resolve({ stderr, exitCode: 1, messages, usage, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
|
|
850
|
+
resolve({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
|
|
837
851
|
});
|
|
838
852
|
});
|
|
839
853
|
}
|
|
@@ -1145,9 +1159,11 @@ async function runSingleStep(
|
|
|
1145
1159
|
let toolBudgetBlocked = false;
|
|
1146
1160
|
let actualLaunchContractDigest = step.launchContractDigest;
|
|
1147
1161
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1162
|
+
let modelIndex = 0;
|
|
1163
|
+
let startupAttemptIndex = 0;
|
|
1164
|
+
modelAttemptsLoop: while (modelIndex < candidates.length) {
|
|
1165
|
+
if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
1166
|
+
const candidate = candidates[modelIndex];
|
|
1151
1167
|
ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
|
|
1152
1168
|
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
1153
1169
|
if (effectiveStructuredOutput) {
|
|
@@ -1259,7 +1275,7 @@ async function runSingleStep(
|
|
|
1259
1275
|
writerProcesses.push({
|
|
1260
1276
|
processInstanceId: run.processInstanceId,
|
|
1261
1277
|
kind: "pi-writer",
|
|
1262
|
-
attempt:
|
|
1278
|
+
attempt: writerAttemptCount - 1,
|
|
1263
1279
|
closeObservedAt: run.processCloseObservedAt,
|
|
1264
1280
|
exitCode: run.exitCode,
|
|
1265
1281
|
signal: run.processSignal ?? null,
|
|
@@ -1358,7 +1374,7 @@ async function runSingleStep(
|
|
|
1358
1374
|
usage: run.usage,
|
|
1359
1375
|
};
|
|
1360
1376
|
modelAttempts.push(attempt);
|
|
1361
|
-
if (candidate) attemptedModels.push(candidate);
|
|
1377
|
+
if (candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
|
|
1362
1378
|
completionGuardTriggeredFinal = completionGuardTriggered;
|
|
1363
1379
|
finalOutputSnapshot = outputSnapshot;
|
|
1364
1380
|
if (step.toolBudget) {
|
|
@@ -1368,11 +1384,55 @@ async function runSingleStep(
|
|
|
1368
1384
|
toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
|
|
1369
1385
|
}
|
|
1370
1386
|
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect ? { effects: { fileMutation: fileMutationEffect } } : {}) } as RunPiStreamingResult & { structuredOutput?: unknown; agentContract?: import("../../shared/types.ts").AgentContract; effects?: import("../../shared/types.ts").EffectsProjection };
|
|
1371
|
-
if (run.turnBudgetExceeded) break;
|
|
1372
|
-
if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
1373
|
-
if (attempt.success || completionGuardTriggered) break;
|
|
1374
|
-
|
|
1375
|
-
|
|
1387
|
+
if (run.turnBudgetExceeded) break modelAttemptsLoop;
|
|
1388
|
+
if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
|
|
1389
|
+
if (attempt.success || completionGuardTriggered) break modelAttemptsLoop;
|
|
1390
|
+
|
|
1391
|
+
const startupFailure = isRetryableSubagentStartupFailure({
|
|
1392
|
+
exitCode: effectiveExitCode,
|
|
1393
|
+
error,
|
|
1394
|
+
finalOutput: run.finalOutput,
|
|
1395
|
+
messageCount: run.messages.length,
|
|
1396
|
+
toolCount: run.toolCount,
|
|
1397
|
+
usage: run.usage,
|
|
1398
|
+
durationMs: run.durationMs,
|
|
1399
|
+
protocolError: run.protocolError,
|
|
1400
|
+
processSignal: run.processSignal,
|
|
1401
|
+
observedMutationAttempt: run.observedMutationAttempt,
|
|
1402
|
+
interrupted: run.interrupted,
|
|
1403
|
+
timedOut: run.timedOut,
|
|
1404
|
+
stopped: run.stopped,
|
|
1405
|
+
turnBudgetExceeded: run.turnBudgetExceeded,
|
|
1406
|
+
});
|
|
1407
|
+
const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
|
|
1408
|
+
if (startupFailure && retryDelayMs !== undefined) {
|
|
1409
|
+
const retryNote = formatSubagentStartupRetryNote({
|
|
1410
|
+
model: attempt.model,
|
|
1411
|
+
attempt: startupAttemptIndex + 1,
|
|
1412
|
+
maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
|
|
1413
|
+
delayMs: retryDelayMs,
|
|
1414
|
+
});
|
|
1415
|
+
const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [ctx.timeoutSignal, ctx.stopSignal]);
|
|
1416
|
+
if (!shouldRetry || ctx.skipAcceptance?.()) break modelAttemptsLoop;
|
|
1417
|
+
attempt.error = retryNote;
|
|
1418
|
+
attemptNotes.push(retryNote);
|
|
1419
|
+
startupAttemptIndex += 1;
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
if (startupFailure) {
|
|
1423
|
+
const startupError = formatSubagentStartupRetryExhaustedError({
|
|
1424
|
+
model: attempt.model,
|
|
1425
|
+
attempts: startupAttemptIndex + 1,
|
|
1426
|
+
});
|
|
1427
|
+
attempt.error = startupError;
|
|
1428
|
+
finalResult.error = startupError;
|
|
1429
|
+
finalResult.finalOutput = startupError;
|
|
1430
|
+
break modelAttemptsLoop;
|
|
1431
|
+
}
|
|
1432
|
+
if (!isRetryableModelFailure(error) || modelIndex === candidates.length - 1) break modelAttemptsLoop;
|
|
1433
|
+
attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
|
|
1434
|
+
modelIndex += 1;
|
|
1435
|
+
startupAttemptIndex = 0;
|
|
1376
1436
|
}
|
|
1377
1437
|
|
|
1378
1438
|
const rawOutput = finalResult?.finalOutput ?? "";
|
|
@@ -451,13 +451,13 @@ export class ChainClarifyComponent implements Component {
|
|
|
451
451
|
return;
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
if (matchesKey(data, "up")) {
|
|
454
|
+
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
455
455
|
this.selectedStep = Math.max(0, this.selectedStep - 1);
|
|
456
456
|
this.tui.requestRender();
|
|
457
457
|
return;
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
if (matchesKey(data, "down")) {
|
|
460
|
+
if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
461
461
|
const maxStep = Math.max(0, this.agentConfigs.length - 1);
|
|
462
462
|
this.selectedStep = Math.min(maxStep, this.selectedStep + 1);
|
|
463
463
|
this.tui.requestRender();
|
|
@@ -1123,7 +1123,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
1123
1123
|
private getFooterText(): string {
|
|
1124
1124
|
return this.mode === 'single'
|
|
1125
1125
|
? " [Enter] Run • [Esc] Cancel "
|
|
1126
|
-
: " [Enter] Run • [Esc] Cancel • [
|
|
1126
|
+
: " [Enter] Run • [Esc] Cancel • [↑↓/jk] Navigate ";
|
|
1127
1127
|
}
|
|
1128
1128
|
|
|
1129
1129
|
private appendNotice(lines: string[]): void {
|
|
@@ -45,6 +45,9 @@ import {
|
|
|
45
45
|
hasEmptyTerminalAssistantResponse,
|
|
46
46
|
extractToolArgsPreview,
|
|
47
47
|
extractTextFromContent,
|
|
48
|
+
boundStreamedRecentTools,
|
|
49
|
+
boundStreamedRecentOutput,
|
|
50
|
+
boundStreamedToolCalls,
|
|
48
51
|
} from "../../shared/utils.ts";
|
|
49
52
|
import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
50
53
|
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
@@ -63,6 +66,13 @@ import {
|
|
|
63
66
|
formatModelAttemptNote,
|
|
64
67
|
isRetryableModelFailure,
|
|
65
68
|
} from "../shared/model-fallback.ts";
|
|
69
|
+
import {
|
|
70
|
+
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
71
|
+
formatSubagentStartupRetryExhaustedError,
|
|
72
|
+
formatSubagentStartupRetryNote,
|
|
73
|
+
isRetryableSubagentStartupFailure,
|
|
74
|
+
waitForSubagentStartupRetry,
|
|
75
|
+
} from "../shared/subagent-startup-retry.ts";
|
|
66
76
|
import {
|
|
67
77
|
createMutatingFailureState,
|
|
68
78
|
didMutatingToolFail,
|
|
@@ -161,8 +171,8 @@ function snapshotProgress(progress: AgentProgress): AgentProgress {
|
|
|
161
171
|
return {
|
|
162
172
|
...progress,
|
|
163
173
|
skills: progress.skills ? [...progress.skills] : undefined,
|
|
164
|
-
recentTools: progress.recentTools
|
|
165
|
-
recentOutput:
|
|
174
|
+
recentTools: boundStreamedRecentTools(progress.recentTools),
|
|
175
|
+
recentOutput: boundStreamedRecentOutput(progress.recentOutput),
|
|
166
176
|
};
|
|
167
177
|
}
|
|
168
178
|
|
|
@@ -188,6 +198,20 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
|
|
|
188
198
|
};
|
|
189
199
|
}
|
|
190
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Streaming variant of snapshotResult for `onUpdate` progress events: it drops the
|
|
203
|
+
* unbounded `messages` transcript in favour of compact tool-call summaries so a
|
|
204
|
+
* single `tool_execution_update` line stays well under the child-stdout protocol
|
|
205
|
+
* cap (`MAX_CHILD_PENDING_LINE_BYTES`). Non-streaming consumers such as
|
|
206
|
+
* detached-exit recovery call snapshotResult directly and keep the full transcript.
|
|
207
|
+
*/
|
|
208
|
+
function snapshotStreamResult(result: SingleResult, progress: AgentProgress): SingleResult {
|
|
209
|
+
const snapshot = snapshotResult(result, progress);
|
|
210
|
+
snapshot.messages = undefined;
|
|
211
|
+
snapshot.toolCalls = boundStreamedToolCalls(result);
|
|
212
|
+
return snapshot;
|
|
213
|
+
}
|
|
214
|
+
|
|
191
215
|
async function runSingleAttempt(
|
|
192
216
|
runtimeCwd: string,
|
|
193
217
|
agent: AgentConfig,
|
|
@@ -337,6 +361,10 @@ async function runSingleAttempt(
|
|
|
337
361
|
recentOutput: [...shared.attemptNotes],
|
|
338
362
|
toolCount: 0,
|
|
339
363
|
tokens: 0,
|
|
364
|
+
...(modelArg ? { model: modelArg } : {}),
|
|
365
|
+
...(resolvedThinking ? { thinking: resolvedThinking } : {}),
|
|
366
|
+
inputTokens: 0,
|
|
367
|
+
outputTokens: 0,
|
|
340
368
|
durationMs: 0,
|
|
341
369
|
lastActivityAt: startTime,
|
|
342
370
|
};
|
|
@@ -683,7 +711,7 @@ async function runSingleAttempt(
|
|
|
683
711
|
const emitUpdateSnapshot = (text: string) => {
|
|
684
712
|
if (!options.onUpdate || processClosed) return;
|
|
685
713
|
const progressSnapshot = snapshotProgress(progress);
|
|
686
|
-
const resultSnapshot =
|
|
714
|
+
const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
|
|
687
715
|
const controlEvents = drainPendingControlEvents();
|
|
688
716
|
options.onUpdate({
|
|
689
717
|
content: [{ type: "text", text }],
|
|
@@ -703,6 +731,7 @@ async function runSingleAttempt(
|
|
|
703
731
|
emitUpdateSnapshot(output || "(running...)");
|
|
704
732
|
};
|
|
705
733
|
|
|
734
|
+
const rawStdoutTail = createBoundedByteTail();
|
|
706
735
|
const processLine = (line: string) => {
|
|
707
736
|
if (!line.trim()) return;
|
|
708
737
|
jsonlWriter.writeLine(line);
|
|
@@ -710,6 +739,7 @@ async function runSingleAttempt(
|
|
|
710
739
|
try {
|
|
711
740
|
evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown; willRetry?: unknown };
|
|
712
741
|
} catch {
|
|
742
|
+
rawStdoutTail.push(`${line}\n`);
|
|
713
743
|
shared.transcriptWriter?.writeStdoutLine(line);
|
|
714
744
|
// Non-JSON stdout lines are expected; only structured events are parsed.
|
|
715
745
|
return;
|
|
@@ -805,8 +835,13 @@ async function runSingleAttempt(
|
|
|
805
835
|
result.usage.cacheWrite += u.cacheWrite || 0;
|
|
806
836
|
result.usage.cost += u.cost?.total || 0;
|
|
807
837
|
progress.tokens = result.usage.input + result.usage.output;
|
|
838
|
+
progress.inputTokens = result.usage.input;
|
|
839
|
+
progress.outputTokens = result.usage.output;
|
|
840
|
+
}
|
|
841
|
+
if (evt.message.model) {
|
|
842
|
+
progress.model = evt.message.model;
|
|
843
|
+
if (!result.model) result.model = evt.message.model;
|
|
808
844
|
}
|
|
809
|
-
if (!result.model && evt.message.model) result.model = evt.message.model;
|
|
810
845
|
if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
|
|
811
846
|
const assistantText = extractTextFromContent(evt.message.content);
|
|
812
847
|
appendRecentOutput(progress, assistantText.split("\n").slice(-10));
|
|
@@ -938,12 +973,17 @@ async function runSingleAttempt(
|
|
|
938
973
|
stdoutReader.end();
|
|
939
974
|
stderrReader.end();
|
|
940
975
|
const stderr = stderrTail.text();
|
|
976
|
+
const rawStdout = rawStdoutTail.text();
|
|
941
977
|
let closeError = result.error ?? toolDiagnosticError ?? assistantError;
|
|
942
978
|
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
|
|
979
|
+
if (code !== 0 && rawStdout.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
|
|
980
|
+
closeError = rawStdout.trim();
|
|
981
|
+
}
|
|
943
982
|
if (code !== 0 && stderr.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
|
|
944
983
|
closeError = stderr.trim();
|
|
945
984
|
}
|
|
946
985
|
const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
|
|
986
|
+
if (signal) result.processSignal = signal;
|
|
947
987
|
if (detached) {
|
|
948
988
|
const recoveredProgress = snapshotProgress(progress);
|
|
949
989
|
const recoveredResult = snapshotResult(result, recoveredProgress);
|
|
@@ -1208,7 +1248,7 @@ async function runSingleAttempt(
|
|
|
1208
1248
|
if (options.onUpdate) {
|
|
1209
1249
|
const finalText = result.finalOutput || result.error || "(no output)";
|
|
1210
1250
|
const progressSnapshot = snapshotProgress(progress);
|
|
1211
|
-
const resultSnapshot =
|
|
1251
|
+
const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
|
|
1212
1252
|
options.onUpdate({
|
|
1213
1253
|
content: [{ type: "text", text: finalText }],
|
|
1214
1254
|
details: {
|
|
@@ -1351,6 +1391,7 @@ export async function runSync(
|
|
|
1351
1391
|
agent: agentName,
|
|
1352
1392
|
task,
|
|
1353
1393
|
exitCode: target.exitCode,
|
|
1394
|
+
processSignal: target.processSignal,
|
|
1354
1395
|
usage: target.usage,
|
|
1355
1396
|
model: target.model,
|
|
1356
1397
|
attemptedModels: target.attemptedModels,
|
|
@@ -1416,47 +1457,111 @@ export async function runSync(
|
|
|
1416
1457
|
|
|
1417
1458
|
let lastResult: SingleResult | undefined;
|
|
1418
1459
|
const modelsToTry = candidates.length > 0 ? candidates : [undefined];
|
|
1419
|
-
for (let
|
|
1420
|
-
const candidate = modelsToTry[
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1460
|
+
modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
|
|
1461
|
+
const candidate = modelsToTry[modelIndex];
|
|
1462
|
+
for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
|
|
1463
|
+
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
1464
|
+
const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, detachedAwareOptions, {
|
|
1465
|
+
sessionEnabled,
|
|
1466
|
+
systemPrompt,
|
|
1467
|
+
resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
|
|
1468
|
+
skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
|
|
1469
|
+
jsonlPath,
|
|
1470
|
+
artifactPaths: artifactPathsResult,
|
|
1471
|
+
transcriptWriter,
|
|
1472
|
+
attemptNotes,
|
|
1473
|
+
modelCandidates: candidates
|
|
1474
|
+
.map((modelCandidate) => applyThinkingSuffix(modelCandidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined))
|
|
1475
|
+
.filter((modelCandidate): modelCandidate is string => Boolean(modelCandidate)),
|
|
1476
|
+
outputSnapshot,
|
|
1477
|
+
originalTask: task,
|
|
1478
|
+
});
|
|
1479
|
+
lastResult = result;
|
|
1480
|
+
if (startupAttemptIndex === 0) {
|
|
1481
|
+
if (result.model) attemptedModels.push(result.model);
|
|
1482
|
+
else if (candidate) attemptedModels.push(candidate);
|
|
1483
|
+
}
|
|
1484
|
+
sumUsage(aggregateUsage, result.usage);
|
|
1485
|
+
totalToolCount += result.progressSummary?.toolCount ?? 0;
|
|
1486
|
+
totalDurationMs += result.progressSummary?.durationMs ?? 0;
|
|
1487
|
+
const attemptSucceeded = result.exitCode === 0 && !result.error;
|
|
1488
|
+
const attempt: ModelAttempt = {
|
|
1489
|
+
model: result.model ?? candidate ?? agent.model ?? "default",
|
|
1490
|
+
success: attemptSucceeded,
|
|
1491
|
+
exitCode: result.exitCode,
|
|
1492
|
+
error: result.error,
|
|
1493
|
+
usage: { ...result.usage },
|
|
1494
|
+
};
|
|
1495
|
+
modelAttempts.push(attempt);
|
|
1496
|
+
if (result.detached || result.timedOut || result.turnBudgetExceeded) break modelAttemptsLoop;
|
|
1497
|
+
if (attemptSucceeded) break modelAttemptsLoop;
|
|
1498
|
+
|
|
1499
|
+
const startupFailure = isRetryableSubagentStartupFailure({
|
|
1500
|
+
exitCode: result.exitCode,
|
|
1501
|
+
error: result.error,
|
|
1502
|
+
finalOutput: result.finalOutput,
|
|
1503
|
+
messageCount: result.messages?.length ?? 0,
|
|
1504
|
+
toolCount: result.progressSummary?.toolCount ?? 0,
|
|
1505
|
+
usage: result.usage,
|
|
1506
|
+
durationMs: result.progressSummary?.durationMs ?? Number.POSITIVE_INFINITY,
|
|
1507
|
+
protocolError: result.protocolError,
|
|
1508
|
+
processSignal: result.processSignal,
|
|
1509
|
+
detached: result.detached,
|
|
1510
|
+
interrupted: result.interrupted,
|
|
1511
|
+
timedOut: result.timedOut,
|
|
1512
|
+
stopped: result.stopped,
|
|
1513
|
+
turnBudgetExceeded: result.turnBudgetExceeded,
|
|
1514
|
+
});
|
|
1515
|
+
const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
|
|
1516
|
+
if (startupFailure && retryDelayMs !== undefined) {
|
|
1517
|
+
const retryNote = formatSubagentStartupRetryNote({
|
|
1518
|
+
model: attempt.model,
|
|
1519
|
+
attempt: startupAttemptIndex + 1,
|
|
1520
|
+
maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
|
|
1521
|
+
delayMs: retryDelayMs,
|
|
1522
|
+
});
|
|
1523
|
+
const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [options.signal, options.interruptSignal]);
|
|
1524
|
+
if (!shouldRetry) {
|
|
1525
|
+
if (options.interruptSignal?.aborted) {
|
|
1526
|
+
result.exitCode = 0;
|
|
1527
|
+
result.interrupted = true;
|
|
1528
|
+
result.error = undefined;
|
|
1529
|
+
result.finalOutput = "Interrupted. Waiting for explicit next action.";
|
|
1530
|
+
if (result.progress) {
|
|
1531
|
+
result.progress.status = "running";
|
|
1532
|
+
result.progress.error = undefined;
|
|
1533
|
+
}
|
|
1534
|
+
} else {
|
|
1535
|
+
const cancellationError = "Subagent startup retry cancelled before relaunch.";
|
|
1536
|
+
result.error = cancellationError;
|
|
1537
|
+
result.finalOutput = cancellationError;
|
|
1538
|
+
attempt.error = cancellationError;
|
|
1539
|
+
if (result.progress) result.progress.error = cancellationError;
|
|
1540
|
+
}
|
|
1541
|
+
break modelAttemptsLoop;
|
|
1542
|
+
}
|
|
1543
|
+
attempt.error = retryNote;
|
|
1544
|
+
attemptNotes.push(retryNote);
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (startupFailure) {
|
|
1548
|
+
const startupError = formatSubagentStartupRetryExhaustedError({
|
|
1549
|
+
model: attempt.model,
|
|
1550
|
+
attempts: startupAttemptIndex + 1,
|
|
1551
|
+
});
|
|
1552
|
+
result.error = startupError;
|
|
1553
|
+
result.finalOutput = startupError;
|
|
1554
|
+
if (result.progress) {
|
|
1555
|
+
result.progress.error = startupError;
|
|
1556
|
+
result.progress.status = "failed";
|
|
1557
|
+
}
|
|
1558
|
+
attempt.error = startupError;
|
|
1559
|
+
break modelAttemptsLoop;
|
|
1560
|
+
}
|
|
1561
|
+
if (!isRetryableModelFailure(result.error) || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
|
|
1562
|
+
attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[modelIndex + 1]));
|
|
1457
1563
|
break;
|
|
1458
1564
|
}
|
|
1459
|
-
attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
|
|
1460
1565
|
}
|
|
1461
1566
|
|
|
1462
1567
|
const result = withRunContext(lastResult ?? {
|
|
@@ -1476,13 +1581,6 @@ export async function runSync(
|
|
|
1476
1581
|
tokens: aggregateUsage.input + aggregateUsage.output,
|
|
1477
1582
|
durationMs: totalDurationMs,
|
|
1478
1583
|
};
|
|
1479
|
-
if (attemptNotes.length > 0 && result.progress) {
|
|
1480
|
-
result.progress.recentOutput = [...attemptNotes, ...result.progress.recentOutput];
|
|
1481
|
-
if (result.progress.recentOutput.length > 50) {
|
|
1482
|
-
result.progress.recentOutput.splice(50);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
1584
|
if (transcriptWriter) result.transcriptPath = artifactPathsResult?.transcriptPath;
|
|
1487
1585
|
if (transcriptWriter?.getError()) result.transcriptError = transcriptWriter.getError();
|
|
1488
1586
|
|
|
@@ -16,6 +16,10 @@ function copyProgress(target: ForegroundChildControl, progress: AgentProgress |
|
|
|
16
16
|
target.currentPath = progress.currentPath;
|
|
17
17
|
target.turnCount = progress.turnCount;
|
|
18
18
|
target.tokens = progress.tokens;
|
|
19
|
+
target.inputTokens = progress.inputTokens;
|
|
20
|
+
target.outputTokens = progress.outputTokens;
|
|
21
|
+
target.model = progress.model;
|
|
22
|
+
target.thinking = progress.thinking;
|
|
19
23
|
target.toolCount = progress.toolCount;
|
|
20
24
|
}
|
|
21
25
|
|
|
@@ -30,6 +34,10 @@ function syncCurrentChild(control: ForegroundRunControl, child: ForegroundChildC
|
|
|
30
34
|
control.currentPath = child.currentPath;
|
|
31
35
|
control.turnCount = child.turnCount;
|
|
32
36
|
control.tokens = child.tokens;
|
|
37
|
+
control.inputTokens = child.inputTokens;
|
|
38
|
+
control.outputTokens = child.outputTokens;
|
|
39
|
+
control.model = child.model;
|
|
40
|
+
control.thinking = child.thinking;
|
|
33
41
|
control.toolCount = child.toolCount;
|
|
34
42
|
control.interrupt = child.interrupt;
|
|
35
43
|
control.updatedAt = child.updatedAt;
|
|
@@ -45,6 +53,10 @@ function clearCurrentChild(control: ForegroundRunControl): void {
|
|
|
45
53
|
control.currentPath = undefined;
|
|
46
54
|
control.turnCount = undefined;
|
|
47
55
|
control.tokens = undefined;
|
|
56
|
+
control.inputTokens = undefined;
|
|
57
|
+
control.outputTokens = undefined;
|
|
58
|
+
control.model = undefined;
|
|
59
|
+
control.thinking = undefined;
|
|
48
60
|
control.toolCount = undefined;
|
|
49
61
|
control.interrupt = undefined;
|
|
50
62
|
}
|
|
@@ -4095,6 +4095,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4095
4095
|
? undefined
|
|
4096
4096
|
: {
|
|
4097
4097
|
runId,
|
|
4098
|
+
...(deps.state.currentSessionId ? { sessionId: deps.state.currentSessionId } : {}),
|
|
4098
4099
|
mode: foregroundMode,
|
|
4099
4100
|
startedAt: Date.now(),
|
|
4100
4101
|
updatedAt: Date.now(),
|
|
@@ -32,7 +32,7 @@ const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"]);
|
|
35
|
-
const
|
|
35
|
+
const VALID_EVIDENCE_KINDS: AcceptanceEvidenceKind[] = [
|
|
36
36
|
"changed-files",
|
|
37
37
|
"tests-added",
|
|
38
38
|
"commands-run",
|
|
@@ -42,7 +42,10 @@ const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
|
|
|
42
42
|
"diff-summary",
|
|
43
43
|
"review-findings",
|
|
44
44
|
"manual-notes",
|
|
45
|
-
]
|
|
45
|
+
];
|
|
46
|
+
const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>(VALID_EVIDENCE_KINDS);
|
|
47
|
+
const ACCEPTANCE_EVIDENCE_HELP = `Supported evidence kinds: ${VALID_EVIDENCE_KINDS.join(", ")}. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.`;
|
|
48
|
+
const ACCEPTANCE_OBJECT_EXAMPLE = "Example: { level: \"checked\", evidence: [\"commands-run\", \"changed-files\"] }.";
|
|
46
49
|
const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
|
|
47
50
|
const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
|
|
48
51
|
const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
|
|
@@ -153,6 +156,11 @@ function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
|
|
|
153
156
|
return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
function unsupportedEvidenceKindMessage(pathLabel: string, item: unknown): string {
|
|
160
|
+
const value = typeof item === "string" ? ` "${item}"` : "";
|
|
161
|
+
return `${pathLabel}${value} is not a supported evidence kind. ${ACCEPTANCE_EVIDENCE_HELP}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
156
164
|
export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"): string[] {
|
|
157
165
|
const errors: string[] = [];
|
|
158
166
|
if (input === undefined) return errors;
|
|
@@ -164,7 +172,7 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
164
172
|
return errors;
|
|
165
173
|
}
|
|
166
174
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
167
|
-
errors.push(`${pathLabel} must be a string level, false, or an object
|
|
175
|
+
errors.push(`${pathLabel} must be a string level, false, or an object. ${ACCEPTANCE_OBJECT_EXAMPLE}`);
|
|
168
176
|
return errors;
|
|
169
177
|
}
|
|
170
178
|
const value = input as Record<string, unknown>;
|
|
@@ -202,11 +210,11 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
202
210
|
criterionIds.add(normalizedId);
|
|
203
211
|
}
|
|
204
212
|
if (typeof gate.must !== "string" || !gate.must.trim()) errors.push(`${criterionPath}.must is required.`);
|
|
205
|
-
if (gate.evidence !== undefined && !Array.isArray(gate.evidence)) errors.push(`${criterionPath}.evidence must be an array
|
|
213
|
+
if (gate.evidence !== undefined && !Array.isArray(gate.evidence)) errors.push(`${criterionPath}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
|
|
206
214
|
if (Array.isArray(gate.evidence)) {
|
|
207
215
|
for (const [evidenceIndex, item] of gate.evidence.entries()) {
|
|
208
216
|
if (typeof item !== "string" || !VALID_EVIDENCE.has(item as AcceptanceEvidenceKind)) {
|
|
209
|
-
errors.push(`${criterionPath}.evidence[${evidenceIndex}]
|
|
217
|
+
errors.push(unsupportedEvidenceKindMessage(`${criterionPath}.evidence[${evidenceIndex}]`, item));
|
|
210
218
|
}
|
|
211
219
|
}
|
|
212
220
|
}
|
|
@@ -218,11 +226,11 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
218
226
|
if (Array.isArray(value.evidence)) {
|
|
219
227
|
for (const [index, item] of value.evidence.entries()) {
|
|
220
228
|
if (typeof item !== "string" || !VALID_EVIDENCE.has(item as AcceptanceEvidenceKind)) {
|
|
221
|
-
errors.push(`${pathLabel}.evidence[${index}]
|
|
229
|
+
errors.push(unsupportedEvidenceKindMessage(`${pathLabel}.evidence[${index}]`, item));
|
|
222
230
|
}
|
|
223
231
|
}
|
|
224
232
|
} else if (value.evidence !== undefined) {
|
|
225
|
-
errors.push(`${pathLabel}.evidence must be an array
|
|
233
|
+
errors.push(`${pathLabel}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
|
|
226
234
|
}
|
|
227
235
|
if (value.verify !== undefined && !Array.isArray(value.verify)) errors.push(`${pathLabel}.verify must be an array.`);
|
|
228
236
|
if (Array.isArray(value.verify)) {
|
|
@@ -38,6 +38,8 @@ export const SUBAGENT_PARENT_SESSION_ENV = "PI_SUBAGENT_PARENT_SESSION";
|
|
|
38
38
|
export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
|
|
39
39
|
export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
|
|
40
40
|
export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
|
|
41
|
+
export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
|
|
42
|
+
export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
|
|
41
43
|
|
|
42
44
|
export interface BuildPiArgsInput {
|
|
43
45
|
parentSessionId?: string;
|
|
@@ -331,6 +333,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
331
333
|
: "";
|
|
332
334
|
env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
|
|
333
335
|
env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
|
|
336
|
+
env[PI_INTERCOM_STABLE_ID_ENV] = input.intercomSessionName || undefined;
|
|
337
|
+
env[PI_INTERCOM_SESSION_ID_ENV] = undefined;
|
|
334
338
|
if (input.intercomSessionName) {
|
|
335
339
|
env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
|
|
336
340
|
}
|