pi-subagents 0.31.1 → 0.33.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 +67 -4
- package/README.md +219 -40
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +71 -10
- package/src/agents/agent-management.ts +179 -2
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +193 -19
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/config.ts +27 -4
- package/src/extension/doctor.ts +1 -7
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +70 -41
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +54 -10
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +187 -38
- package/src/runs/background/async-job-tracker.ts +88 -2
- package/src/runs/background/async-status.ts +67 -10
- package/src/runs/background/chain-root-attachment.ts +34 -4
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +156 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +167 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/stale-run-reconciler.ts +28 -1
- package/src/runs/background/subagent-runner.ts +840 -127
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +123 -27
- package/src/runs/foreground/execution.ts +174 -27
- package/src/runs/foreground/subagent-executor.ts +569 -81
- package/src/runs/shared/acceptance.ts +45 -22
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/model-fallback.ts +171 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +89 -0
- package/src/runs/shared/parallel-utils.ts +50 -1
- package/src/runs/shared/pi-args.ts +35 -4
- package/src/runs/shared/pi-spawn.ts +52 -20
- package/src/runs/shared/single-output.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +110 -4
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/runs/shared/worktree.ts +28 -5
- package/src/shared/artifacts.ts +16 -1
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/fork-context.ts +133 -22
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +197 -4
- package/src/shared/utils.ts +99 -14
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +133 -2
- package/src/tui/render.ts +32 -12
|
@@ -4,7 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import type { Message } from "@earendil-works/pi-ai";
|
|
6
6
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
8
|
+
import { consumeInterruptRequest, deliverInterruptRequest, deliverTimeoutRequest, enqueueStepSteer, stepSteerInboxDir, watchAsyncControlInbox, type SteerRequest } from "./control-channel.ts";
|
|
8
9
|
import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
9
10
|
import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
|
|
10
11
|
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
@@ -15,14 +16,21 @@ import {
|
|
|
15
16
|
type AsyncParallelGroupStatus,
|
|
16
17
|
type AsyncStatus,
|
|
17
18
|
type ChainOutputMap,
|
|
19
|
+
type CostSummary,
|
|
18
20
|
type ModelAttempt,
|
|
19
21
|
type NestedRouteInfo,
|
|
22
|
+
type NestedRunSummary,
|
|
20
23
|
type ResolvedControlConfig,
|
|
24
|
+
type ResolvedTurnBudget,
|
|
25
|
+
type ResolvedToolBudget,
|
|
21
26
|
type SubagentRunMode,
|
|
27
|
+
type ToolBudgetState,
|
|
28
|
+
type TurnBudgetState,
|
|
22
29
|
type Usage,
|
|
23
30
|
type WorkflowGraphSnapshot,
|
|
24
31
|
DEFAULT_MAX_OUTPUT,
|
|
25
32
|
type MaxOutputConfig,
|
|
33
|
+
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
26
34
|
truncateOutput,
|
|
27
35
|
getSubagentDepthEnv,
|
|
28
36
|
} from "../../shared/types.ts";
|
|
@@ -43,15 +51,17 @@ import {
|
|
|
43
51
|
mapConcurrent,
|
|
44
52
|
aggregateParallelOutputs,
|
|
45
53
|
MAX_PARALLEL_CONCURRENCY,
|
|
54
|
+
DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
|
|
55
|
+
Semaphore,
|
|
46
56
|
} from "../shared/parallel-utils.ts";
|
|
47
|
-
import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
57
|
+
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
48
58
|
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
49
59
|
import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
|
|
50
60
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
51
|
-
import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
|
|
61
|
+
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
52
62
|
import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
53
63
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
54
|
-
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
|
|
64
|
+
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
|
|
55
65
|
import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
56
66
|
import {
|
|
57
67
|
createMutatingFailureState,
|
|
@@ -81,6 +91,8 @@ import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts
|
|
|
81
91
|
import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
|
|
82
92
|
import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
83
93
|
import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
|
|
94
|
+
import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, shouldAbortForTurnBudget, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
|
|
95
|
+
import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
|
|
84
96
|
|
|
85
97
|
interface SubagentRunConfig {
|
|
86
98
|
id: string;
|
|
@@ -101,6 +113,7 @@ interface SubagentRunConfig {
|
|
|
101
113
|
piArgv1?: string;
|
|
102
114
|
worktreeSetupHook?: string;
|
|
103
115
|
worktreeSetupHookTimeoutMs?: number;
|
|
116
|
+
worktreeBaseDir?: string;
|
|
104
117
|
controlConfig?: ResolvedControlConfig;
|
|
105
118
|
controlIntercomTarget?: string;
|
|
106
119
|
childIntercomTargets?: Array<string | undefined>;
|
|
@@ -109,6 +122,12 @@ interface SubagentRunConfig {
|
|
|
109
122
|
workflowGraph?: WorkflowGraphSnapshot;
|
|
110
123
|
nestedRoute?: NestedRouteInfo;
|
|
111
124
|
nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
|
|
125
|
+
timeoutMs?: number;
|
|
126
|
+
deadlineAt?: number;
|
|
127
|
+
turnBudget?: ResolvedTurnBudget;
|
|
128
|
+
toolBudget?: ResolvedToolBudget;
|
|
129
|
+
/** Global cap on simultaneously-running subagent tasks within this run. */
|
|
130
|
+
globalConcurrencyLimit?: number;
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
interface StepResult {
|
|
@@ -118,13 +137,23 @@ interface StepResult {
|
|
|
118
137
|
success: boolean;
|
|
119
138
|
exitCode?: number | null;
|
|
120
139
|
skipped?: boolean;
|
|
140
|
+
interrupted?: boolean;
|
|
141
|
+
timedOut?: boolean;
|
|
142
|
+
turnBudget?: TurnBudgetState;
|
|
143
|
+
turnBudgetExceeded?: boolean;
|
|
144
|
+
wrapUpRequested?: boolean;
|
|
145
|
+
toolBudget?: ToolBudgetState;
|
|
146
|
+
toolBudgetBlocked?: boolean;
|
|
121
147
|
sessionFile?: string;
|
|
122
148
|
intercomTarget?: string;
|
|
123
149
|
model?: string;
|
|
124
150
|
attemptedModels?: string[];
|
|
125
151
|
modelAttempts?: ModelAttempt[];
|
|
152
|
+
totalCost?: CostSummary;
|
|
126
153
|
artifactPaths?: ArtifactPaths;
|
|
127
154
|
truncated?: boolean;
|
|
155
|
+
transcriptPath?: string;
|
|
156
|
+
transcriptError?: string;
|
|
128
157
|
structuredOutput?: unknown;
|
|
129
158
|
structuredOutputPath?: string;
|
|
130
159
|
structuredOutputSchemaPath?: string;
|
|
@@ -237,6 +266,21 @@ function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsag
|
|
|
237
266
|
return total > 0 ? { input, output, total } : null;
|
|
238
267
|
}
|
|
239
268
|
|
|
269
|
+
function costSummaryFromAttempts(attempts: ModelAttempt[] | undefined): CostSummary | undefined {
|
|
270
|
+
if (!attempts || attempts.length === 0) return undefined;
|
|
271
|
+
let inputTokens = 0;
|
|
272
|
+
let outputTokens = 0;
|
|
273
|
+
let costUsd = 0;
|
|
274
|
+
for (const attempt of attempts) {
|
|
275
|
+
inputTokens += attempt.usage?.input ?? 0;
|
|
276
|
+
outputTokens += attempt.usage?.output ?? 0;
|
|
277
|
+
costUsd += attempt.usage?.cost ?? 0;
|
|
278
|
+
}
|
|
279
|
+
return inputTokens > 0 || outputTokens > 0 || costUsd > 0
|
|
280
|
+
? { inputTokens, outputTokens, costUsd }
|
|
281
|
+
: undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
240
284
|
function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
|
|
241
285
|
const nonEmpty = lines.filter((line) => line.trim());
|
|
242
286
|
if (nonEmpty.length === 0) return;
|
|
@@ -247,6 +291,13 @@ function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
|
|
|
247
291
|
}
|
|
248
292
|
}
|
|
249
293
|
|
|
294
|
+
function isTerminalAssistantStop(message: Message): boolean {
|
|
295
|
+
const stopReason = (message as { stopReason?: string }).stopReason;
|
|
296
|
+
const hasToolCall = Array.isArray(message.content)
|
|
297
|
+
&& message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
298
|
+
return stopReason === "stop" && !hasToolCall;
|
|
299
|
+
}
|
|
300
|
+
|
|
250
301
|
function resetStepLiveDetail(step: RunnerStatusStep): void {
|
|
251
302
|
step.currentTool = undefined;
|
|
252
303
|
step.currentToolArgs = undefined;
|
|
@@ -295,6 +346,12 @@ interface RunPiStreamingResult {
|
|
|
295
346
|
error?: string;
|
|
296
347
|
finalOutput: string;
|
|
297
348
|
interrupted?: boolean;
|
|
349
|
+
timedOut?: boolean;
|
|
350
|
+
turnBudget?: TurnBudgetState;
|
|
351
|
+
turnBudgetExceeded?: boolean;
|
|
352
|
+
wrapUpRequested?: boolean;
|
|
353
|
+
toolBudget?: ToolBudgetState;
|
|
354
|
+
toolBudgetBlocked?: boolean;
|
|
298
355
|
observedMutationAttempt?: boolean;
|
|
299
356
|
}
|
|
300
357
|
|
|
@@ -309,6 +366,10 @@ function runPiStreaming(
|
|
|
309
366
|
childEventContext?: ChildEventContext,
|
|
310
367
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void,
|
|
311
368
|
onChildEvent?: (event: ChildEvent) => void,
|
|
369
|
+
transcriptWriter?: ChildTranscriptWriter,
|
|
370
|
+
registerTimeout?: (interrupt: (() => void) | undefined) => void,
|
|
371
|
+
timeoutMessage?: string,
|
|
372
|
+
registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
|
|
312
373
|
): Promise<RunPiStreamingResult> {
|
|
313
374
|
return new Promise((resolve) => {
|
|
314
375
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
@@ -332,6 +393,10 @@ function runPiStreaming(
|
|
|
332
393
|
let error: string | undefined;
|
|
333
394
|
let assistantError: string | undefined;
|
|
334
395
|
let interrupted = false;
|
|
396
|
+
let timedOut = false;
|
|
397
|
+
let turnBudgetExceeded = false;
|
|
398
|
+
let turnBudgetMessage: string | undefined;
|
|
399
|
+
let turnBudget: TurnBudgetState | undefined;
|
|
335
400
|
let observedMutationAttempt = false;
|
|
336
401
|
const rawStdoutLines: string[] = [];
|
|
337
402
|
|
|
@@ -361,6 +426,8 @@ function runPiStreaming(
|
|
|
361
426
|
|
|
362
427
|
const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
|
|
363
428
|
appendChildEvent({ type, line });
|
|
429
|
+
if (type === "subagent.child.stdout") transcriptWriter?.writeStdoutLine(line);
|
|
430
|
+
else transcriptWriter?.writeStderrLine(line);
|
|
364
431
|
};
|
|
365
432
|
|
|
366
433
|
const processStdoutLine = (line: string) => {
|
|
@@ -376,6 +443,7 @@ function runPiStreaming(
|
|
|
376
443
|
}
|
|
377
444
|
|
|
378
445
|
appendChildEvent(event);
|
|
446
|
+
transcriptWriter?.writeChildEvent(event);
|
|
379
447
|
onChildEvent?.(event);
|
|
380
448
|
|
|
381
449
|
if (event.type === "tool_execution_start" && event.toolName) {
|
|
@@ -402,10 +470,7 @@ function runPiStreaming(
|
|
|
402
470
|
usage.cacheWrite += eventUsage.cacheWrite ?? 0;
|
|
403
471
|
usage.cost += eventUsage.cost?.total ?? 0;
|
|
404
472
|
}
|
|
405
|
-
|
|
406
|
-
const hasToolCall = Array.isArray(event.message.content)
|
|
407
|
-
&& event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
408
|
-
if (stopReason === "stop" && !hasToolCall) {
|
|
473
|
+
if (isTerminalAssistantStop(event.message)) {
|
|
409
474
|
if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
|
|
410
475
|
cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
|
|
411
476
|
startFinalDrain();
|
|
@@ -430,11 +495,15 @@ function runPiStreaming(
|
|
|
430
495
|
// a lingering stdio holder after `exit`, or a child that never exits.
|
|
431
496
|
const FINAL_STOP_GRACE_MS = 1000;
|
|
432
497
|
const HARD_KILL_MS = 3000;
|
|
498
|
+
const TIMEOUT_HARD_KILL_MS = 3000;
|
|
433
499
|
let childExited = false;
|
|
434
500
|
let forcedTerminationSignal = false;
|
|
435
501
|
let cleanTerminalAssistantStopReceived = false;
|
|
436
502
|
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
437
503
|
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
504
|
+
let timeoutHardKillTimer: NodeJS.Timeout | undefined;
|
|
505
|
+
let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
|
|
506
|
+
let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
|
|
438
507
|
let settled = false;
|
|
439
508
|
const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
|
|
440
509
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
@@ -449,14 +518,42 @@ function runPiStreaming(
|
|
|
449
518
|
processStderrText(chunk.toString());
|
|
450
519
|
});
|
|
451
520
|
registerInterrupt?.(() => {
|
|
452
|
-
if (settled) return;
|
|
521
|
+
if (settled || timedOut) return;
|
|
453
522
|
interrupted = true;
|
|
454
523
|
if (!error) error = "Interrupted. Waiting for explicit next action.";
|
|
455
524
|
trySignalChild(child, "SIGINT");
|
|
456
525
|
setTimeout(() => {
|
|
457
|
-
if (!settled) trySignalChild(child, "SIGTERM");
|
|
526
|
+
if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
|
|
458
527
|
}, 1000).unref?.();
|
|
459
528
|
});
|
|
529
|
+
registerTimeout?.(() => {
|
|
530
|
+
if (settled || timedOut) return;
|
|
531
|
+
timedOut = true;
|
|
532
|
+
interrupted = false;
|
|
533
|
+
error = timeoutMessage ?? "Subagent timed out.";
|
|
534
|
+
trySignalChild(child, "SIGTERM");
|
|
535
|
+
timeoutHardKillTimer = setTimeout(() => {
|
|
536
|
+
if (!settled) trySignalChild(child, "SIGKILL");
|
|
537
|
+
}, TIMEOUT_HARD_KILL_MS);
|
|
538
|
+
timeoutHardKillTimer.unref?.();
|
|
539
|
+
});
|
|
540
|
+
registerTurnBudgetAbort?.((message, state) => {
|
|
541
|
+
if (settled || timedOut || turnBudgetExceeded) return;
|
|
542
|
+
turnBudgetExceeded = true;
|
|
543
|
+
turnBudgetMessage = message;
|
|
544
|
+
turnBudget = state;
|
|
545
|
+
interrupted = false;
|
|
546
|
+
error = message;
|
|
547
|
+
trySignalChild(child, "SIGINT");
|
|
548
|
+
turnBudgetTerminationTimer = setTimeout(() => {
|
|
549
|
+
if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
|
|
550
|
+
}, 1000);
|
|
551
|
+
turnBudgetTerminationTimer.unref?.();
|
|
552
|
+
turnBudgetHardKillTimer = setTimeout(() => {
|
|
553
|
+
if (!settled && !timedOut) trySignalChild(child, "SIGKILL");
|
|
554
|
+
}, 4000);
|
|
555
|
+
turnBudgetHardKillTimer.unref?.();
|
|
556
|
+
});
|
|
460
557
|
const clearDrainTimers = () => {
|
|
461
558
|
if (finalDrainTimer) {
|
|
462
559
|
clearTimeout(finalDrainTimer);
|
|
@@ -466,6 +563,18 @@ function runPiStreaming(
|
|
|
466
563
|
clearTimeout(finalHardKillTimer);
|
|
467
564
|
finalHardKillTimer = undefined;
|
|
468
565
|
}
|
|
566
|
+
if (timeoutHardKillTimer) {
|
|
567
|
+
clearTimeout(timeoutHardKillTimer);
|
|
568
|
+
timeoutHardKillTimer = undefined;
|
|
569
|
+
}
|
|
570
|
+
if (turnBudgetTerminationTimer) {
|
|
571
|
+
clearTimeout(turnBudgetTerminationTimer);
|
|
572
|
+
turnBudgetTerminationTimer = undefined;
|
|
573
|
+
}
|
|
574
|
+
if (turnBudgetHardKillTimer) {
|
|
575
|
+
clearTimeout(turnBudgetHardKillTimer);
|
|
576
|
+
turnBudgetHardKillTimer = undefined;
|
|
577
|
+
}
|
|
469
578
|
};
|
|
470
579
|
function startFinalDrain(): void {
|
|
471
580
|
if (childExited || finalDrainTimer || settled) return;
|
|
@@ -492,6 +601,8 @@ function runPiStreaming(
|
|
|
492
601
|
child.on("close", (exitCode, signal) => {
|
|
493
602
|
settled = true;
|
|
494
603
|
registerInterrupt?.(undefined);
|
|
604
|
+
registerTimeout?.(undefined);
|
|
605
|
+
registerTurnBudgetAbort?.(undefined);
|
|
495
606
|
clearDrainTimers();
|
|
496
607
|
clearStdioGuard();
|
|
497
608
|
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
@@ -502,13 +613,17 @@ function runPiStreaming(
|
|
|
502
613
|
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
|
|
503
614
|
resolve({
|
|
504
615
|
stderr,
|
|
505
|
-
exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
616
|
+
exitCode: timedOut ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
506
617
|
messages,
|
|
507
618
|
usage,
|
|
508
619
|
model,
|
|
509
|
-
error: interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
510
|
-
finalOutput,
|
|
620
|
+
error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
621
|
+
finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput,
|
|
511
622
|
interrupted,
|
|
623
|
+
timedOut,
|
|
624
|
+
turnBudget,
|
|
625
|
+
turnBudgetExceeded,
|
|
626
|
+
wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
|
|
512
627
|
observedMutationAttempt,
|
|
513
628
|
});
|
|
514
629
|
});
|
|
@@ -516,12 +631,14 @@ function runPiStreaming(
|
|
|
516
631
|
child.on("error", (spawnError) => {
|
|
517
632
|
settled = true;
|
|
518
633
|
registerInterrupt?.(undefined);
|
|
634
|
+
registerTimeout?.(undefined);
|
|
635
|
+
registerTurnBudgetAbort?.(undefined);
|
|
519
636
|
clearDrainTimers();
|
|
520
637
|
clearStdioGuard();
|
|
521
638
|
outputStream.end();
|
|
522
639
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
523
640
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
524
|
-
resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
|
|
641
|
+
resolve({ stderr, exitCode: 1, messages, usage, model, error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined, observedMutationAttempt });
|
|
525
642
|
});
|
|
526
643
|
});
|
|
527
644
|
}
|
|
@@ -646,14 +763,22 @@ interface SingleStepContext {
|
|
|
646
763
|
flatIndex: number;
|
|
647
764
|
flatStepCount: number;
|
|
648
765
|
outputFile: string;
|
|
766
|
+
steerInboxDir?: string;
|
|
767
|
+
transcriptPath?: string;
|
|
649
768
|
piPackageRoot?: string;
|
|
650
769
|
piArgv1?: string;
|
|
651
770
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void;
|
|
771
|
+
registerTimeout?: (interrupt: (() => void) | undefined) => void;
|
|
772
|
+
registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void;
|
|
773
|
+
timeoutSignal?: AbortSignal;
|
|
774
|
+
timeoutMessage?: string;
|
|
775
|
+
turnBudget?: ResolvedTurnBudget;
|
|
652
776
|
childIntercomTarget?: string;
|
|
653
777
|
orchestratorIntercomTarget?: string;
|
|
654
778
|
nestedRoute?: NestedRouteInfo;
|
|
655
779
|
onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
|
|
656
780
|
onChildEvent?: (event: ChildEvent) => void;
|
|
781
|
+
skipAcceptance?: () => boolean;
|
|
657
782
|
}
|
|
658
783
|
|
|
659
784
|
/** Run a single pi agent step, returning output and metadata */
|
|
@@ -669,7 +794,15 @@ async function runSingleStep(
|
|
|
669
794
|
attemptedModels?: string[];
|
|
670
795
|
modelAttempts?: ModelAttempt[];
|
|
671
796
|
artifactPaths?: ArtifactPaths;
|
|
797
|
+
transcriptPath?: string;
|
|
798
|
+
transcriptError?: string;
|
|
672
799
|
interrupted?: boolean;
|
|
800
|
+
timedOut?: boolean;
|
|
801
|
+
turnBudget?: TurnBudgetState;
|
|
802
|
+
turnBudgetExceeded?: boolean;
|
|
803
|
+
wrapUpRequested?: boolean;
|
|
804
|
+
toolBudget?: ToolBudgetState;
|
|
805
|
+
toolBudgetBlocked?: boolean;
|
|
673
806
|
sessionFile?: string;
|
|
674
807
|
intercomTarget?: string;
|
|
675
808
|
completionGuardTriggered?: boolean;
|
|
@@ -679,27 +812,52 @@ async function runSingleStep(
|
|
|
679
812
|
acceptance?: import("../../shared/types.ts").AcceptanceLedger;
|
|
680
813
|
}> {
|
|
681
814
|
if (step.importAsyncRoot) {
|
|
682
|
-
|
|
815
|
+
let importTimedOut = false;
|
|
816
|
+
ctx.registerTimeout?.(() => {
|
|
817
|
+
importTimedOut = true;
|
|
818
|
+
let pid: number | undefined;
|
|
819
|
+
try {
|
|
820
|
+
pid = readStatus(step.importAsyncRoot!.asyncDir)?.pid;
|
|
821
|
+
} catch {
|
|
822
|
+
pid = undefined;
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
deliverTimeoutRequest({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-timeout" });
|
|
826
|
+
} catch {
|
|
827
|
+
// The parent runner's own timeout result is authoritative for the attached step.
|
|
828
|
+
}
|
|
829
|
+
});
|
|
683
830
|
try {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
831
|
+
const imported = await waitForImportedAsyncRoot(step.importAsyncRoot, {
|
|
832
|
+
shouldAbort: () => importTimedOut || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true,
|
|
833
|
+
timeoutMessage: ctx.timeoutMessage,
|
|
834
|
+
});
|
|
835
|
+
try {
|
|
836
|
+
fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
|
|
837
|
+
} catch {
|
|
838
|
+
// Output files are observability only for imported roots.
|
|
839
|
+
}
|
|
840
|
+
const timedOut = importTimedOut || imported.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
|
|
841
|
+
return {
|
|
842
|
+
agent: imported.agent,
|
|
843
|
+
output: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.output,
|
|
844
|
+
exitCode: timedOut ? 1 : imported.exitCode,
|
|
845
|
+
error: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.error,
|
|
846
|
+
timedOut: timedOut ? true : undefined,
|
|
847
|
+
sessionFile: imported.sessionFile,
|
|
848
|
+
intercomTarget: imported.intercomTarget,
|
|
849
|
+
model: imported.model,
|
|
850
|
+
attemptedModels: imported.attemptedModels,
|
|
851
|
+
modelAttempts: imported.modelAttempts,
|
|
852
|
+
totalCost: imported.totalCost,
|
|
853
|
+
structuredOutput: timedOut ? undefined : imported.structuredOutput,
|
|
854
|
+
structuredOutputPath: timedOut ? undefined : imported.structuredOutputPath,
|
|
855
|
+
structuredOutputSchemaPath: timedOut ? undefined : imported.structuredOutputSchemaPath,
|
|
856
|
+
acceptance: timedOut ? undefined : imported.acceptance,
|
|
857
|
+
};
|
|
858
|
+
} finally {
|
|
859
|
+
ctx.registerTimeout?.(undefined);
|
|
687
860
|
}
|
|
688
|
-
return {
|
|
689
|
-
agent: imported.agent,
|
|
690
|
-
output: imported.output,
|
|
691
|
-
exitCode: imported.exitCode,
|
|
692
|
-
error: imported.error,
|
|
693
|
-
sessionFile: imported.sessionFile,
|
|
694
|
-
intercomTarget: imported.intercomTarget,
|
|
695
|
-
model: imported.model,
|
|
696
|
-
attemptedModels: imported.attemptedModels,
|
|
697
|
-
modelAttempts: imported.modelAttempts,
|
|
698
|
-
structuredOutput: imported.structuredOutput,
|
|
699
|
-
structuredOutputPath: imported.structuredOutputPath,
|
|
700
|
-
structuredOutputSchemaPath: imported.structuredOutputSchemaPath,
|
|
701
|
-
acceptance: imported.acceptance,
|
|
702
|
-
};
|
|
703
861
|
}
|
|
704
862
|
|
|
705
863
|
const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
|
|
@@ -717,6 +875,7 @@ async function runSingleStep(
|
|
|
717
875
|
const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
|
|
718
876
|
|
|
719
877
|
let artifactPaths: ArtifactPaths | undefined;
|
|
878
|
+
let transcriptWriter: ChildTranscriptWriter | undefined;
|
|
720
879
|
if (ctx.artifactsDir && ctx.artifactConfig?.enabled !== false) {
|
|
721
880
|
const index = ctx.flatStepCount > 1 ? ctx.flatIndex : undefined;
|
|
722
881
|
artifactPaths = getArtifactPaths(ctx.artifactsDir, ctx.id, step.agent, index);
|
|
@@ -724,7 +883,18 @@ async function runSingleStep(
|
|
|
724
883
|
if (ctx.artifactConfig?.includeInput !== false) {
|
|
725
884
|
fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${task}`, "utf-8");
|
|
726
885
|
}
|
|
886
|
+
if (ctx.artifactConfig?.includeTranscript !== false) {
|
|
887
|
+
transcriptWriter = createChildTranscriptWriter({
|
|
888
|
+
transcriptPath: artifactPaths.transcriptPath,
|
|
889
|
+
source: "async",
|
|
890
|
+
runId: ctx.id,
|
|
891
|
+
agent: step.agent,
|
|
892
|
+
childIndex: ctx.flatIndex,
|
|
893
|
+
cwd: step.cwd ?? ctx.cwd,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
727
896
|
}
|
|
897
|
+
transcriptWriter?.writeInitialUserMessage(task);
|
|
728
898
|
|
|
729
899
|
const candidates = step.modelCandidates && step.modelCandidates.length > 0
|
|
730
900
|
? step.modelCandidates
|
|
@@ -738,8 +908,12 @@ async function runSingleStep(
|
|
|
738
908
|
let finalResult: RunPiStreamingResult | undefined;
|
|
739
909
|
let finalOutputSnapshot: SingleOutputSnapshot | undefined;
|
|
740
910
|
let completionGuardTriggeredFinal = false;
|
|
911
|
+
let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
|
|
912
|
+
let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
|
|
913
|
+
let toolBudgetBlocked = false;
|
|
741
914
|
|
|
742
915
|
for (let index = 0; index < candidates.length; index++) {
|
|
916
|
+
if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
743
917
|
const candidate = candidates[index];
|
|
744
918
|
ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
|
|
745
919
|
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
@@ -764,7 +938,7 @@ async function runSingleStep(
|
|
|
764
938
|
tools: step.tools,
|
|
765
939
|
extensions: step.extensions,
|
|
766
940
|
subagentOnlyExtensions: step.subagentOnlyExtensions,
|
|
767
|
-
systemPrompt: step.systemPrompt,
|
|
941
|
+
systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
|
|
768
942
|
systemPromptMode: step.systemPromptMode,
|
|
769
943
|
mcpDirectTools: step.mcpDirectTools,
|
|
770
944
|
cwd: step.cwd ?? ctx.cwd,
|
|
@@ -778,7 +952,9 @@ async function runSingleStep(
|
|
|
778
952
|
parentControlInbox: ctx.nestedRoute?.controlInbox,
|
|
779
953
|
parentRootRunId: ctx.nestedRoute?.rootRunId,
|
|
780
954
|
parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
|
|
955
|
+
steerInboxDir: ctx.steerInboxDir,
|
|
781
956
|
structuredOutput: effectiveStructuredOutput,
|
|
957
|
+
toolBudget: step.toolBudget,
|
|
782
958
|
});
|
|
783
959
|
const run = await runPiStreaming(
|
|
784
960
|
args,
|
|
@@ -791,13 +967,38 @@ async function runSingleStep(
|
|
|
791
967
|
{ eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
792
968
|
ctx.registerInterrupt,
|
|
793
969
|
ctx.onChildEvent,
|
|
970
|
+
transcriptWriter,
|
|
971
|
+
ctx.registerTimeout,
|
|
972
|
+
ctx.timeoutMessage,
|
|
973
|
+
ctx.registerTurnBudgetAbort,
|
|
794
974
|
);
|
|
975
|
+
if (run.turnBudget) turnBudget = run.turnBudget;
|
|
976
|
+
else if (ctx.turnBudget) {
|
|
977
|
+
const assistantMessages = run.messages.filter((message) => message.role === "assistant");
|
|
978
|
+
const turnCount = assistantMessages.length;
|
|
979
|
+
const lastAssistantMessage = assistantMessages.at(-1);
|
|
980
|
+
if (turnCount > 0 && turnCount < ctx.turnBudget.maxTurns) {
|
|
981
|
+
turnBudget = { ...ctx.turnBudget, outcome: "within-budget", turnCount };
|
|
982
|
+
} else if (turnCount >= ctx.turnBudget.maxTurns) {
|
|
983
|
+
turnBudget = turnBudgetState(
|
|
984
|
+
ctx.turnBudget,
|
|
985
|
+
turnCount,
|
|
986
|
+
shouldAbortForTurnBudget(ctx.turnBudget, turnCount, lastAssistantMessage ? isTerminalAssistantStop(lastAssistantMessage) : false),
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
795
990
|
cleanupTempDir(tempDir);
|
|
796
991
|
|
|
797
992
|
const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
|
|
993
|
+
const missingStructuredOutput = effectiveStructuredOutput
|
|
994
|
+
? !fs.existsSync(effectiveStructuredOutput.outputPath)
|
|
995
|
+
: false;
|
|
996
|
+
const emptyOutputError = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !run.finalOutput.trim() && (!effectiveStructuredOutput || missingStructuredOutput)
|
|
997
|
+
? "Subagent produced no output (possible model cold-start or empty response)."
|
|
998
|
+
: undefined;
|
|
798
999
|
let structuredOutput: unknown;
|
|
799
1000
|
let structuredError: string | undefined;
|
|
800
|
-
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError) {
|
|
1001
|
+
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError) {
|
|
801
1002
|
const structured = readStructuredOutput({
|
|
802
1003
|
schema: effectiveStructuredOutput.schema,
|
|
803
1004
|
schemaPath: effectiveStructuredOutput.schemaPath,
|
|
@@ -806,7 +1007,7 @@ async function runSingleStep(
|
|
|
806
1007
|
if (structured.error) structuredError = structured.error;
|
|
807
1008
|
else structuredOutput = structured.value;
|
|
808
1009
|
}
|
|
809
|
-
const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
|
|
1010
|
+
const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
|
|
810
1011
|
? evaluateCompletionMutationGuard({
|
|
811
1012
|
agent: step.agent,
|
|
812
1013
|
task: taskForCompletionGuard,
|
|
@@ -825,16 +1026,18 @@ async function runSingleStep(
|
|
|
825
1026
|
? 1
|
|
826
1027
|
: hiddenError?.hasError
|
|
827
1028
|
? (hiddenError.exitCode ?? 1)
|
|
828
|
-
:
|
|
1029
|
+
: emptyOutputError
|
|
829
1030
|
? 1
|
|
830
|
-
: run.exitCode
|
|
1031
|
+
: run.error && run.exitCode === 0
|
|
1032
|
+
? 1
|
|
1033
|
+
: run.exitCode;
|
|
831
1034
|
const error = completionGuardError
|
|
832
1035
|
?? structuredError
|
|
833
1036
|
?? (hiddenError?.hasError
|
|
834
1037
|
? hiddenError.details
|
|
835
1038
|
? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
|
|
836
1039
|
: `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
|
|
837
|
-
: run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
|
|
1040
|
+
: emptyOutputError ?? (run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)));
|
|
838
1041
|
const attempt: ModelAttempt = {
|
|
839
1042
|
model: candidate ?? run.model ?? step.model ?? "default",
|
|
840
1043
|
success: effectiveExitCode === 0 && !error,
|
|
@@ -846,7 +1049,15 @@ async function runSingleStep(
|
|
|
846
1049
|
if (candidate) attemptedModels.push(candidate);
|
|
847
1050
|
completionGuardTriggeredFinal = completionGuardTriggered;
|
|
848
1051
|
finalOutputSnapshot = outputSnapshot;
|
|
1052
|
+
if (step.toolBudget) {
|
|
1053
|
+
const toolMessages = run.messages.filter((message) => message.role === "toolResult");
|
|
1054
|
+
const blockedMessage = toolMessages.find((message) => extractTextFromContent(message.content).includes("Tool budget hard limit reached"));
|
|
1055
|
+
toolBudgetBlocked = Boolean(blockedMessage);
|
|
1056
|
+
toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
|
|
1057
|
+
}
|
|
849
1058
|
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
|
|
1059
|
+
if (run.turnBudgetExceeded) break;
|
|
1060
|
+
if (run.timedOut || ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
850
1061
|
if (attempt.success || completionGuardTriggered) break;
|
|
851
1062
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
852
1063
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
|
|
@@ -863,6 +1074,12 @@ async function runSingleStep(
|
|
|
863
1074
|
if (attemptNotes.length > 0) {
|
|
864
1075
|
outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
|
|
865
1076
|
}
|
|
1077
|
+
if (!finalResult?.timedOut && finalResult?.turnBudgetExceeded && turnBudget) {
|
|
1078
|
+
outputForSummary = formatTurnBudgetOutput(turnBudgetExceededMessage(turnBudget, turnBudget.turnCount), outputForSummary);
|
|
1079
|
+
} else if (!finalResult?.timedOut && turnBudget?.outcome === "wrap-up-requested") {
|
|
1080
|
+
const note = turnBudgetSoftNote(turnBudget, turnBudget.wrapUpRequestedAtTurn ?? turnBudget.turnCount);
|
|
1081
|
+
outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
|
|
1082
|
+
}
|
|
866
1083
|
const outputForAcceptance = rawOutput;
|
|
867
1084
|
const finalizedOutput = finalizeSingleOutput({
|
|
868
1085
|
fullOutput: outputForSummary,
|
|
@@ -874,19 +1091,28 @@ async function runSingleStep(
|
|
|
874
1091
|
saveError: resolvedOutput.saveError,
|
|
875
1092
|
});
|
|
876
1093
|
outputForSummary = finalizedOutput.displayOutput;
|
|
877
|
-
const acceptance = step.effectiveAcceptance
|
|
1094
|
+
const acceptance = step.effectiveAcceptance && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
|
|
878
1095
|
? await evaluateAcceptance({
|
|
879
1096
|
acceptance: step.effectiveAcceptance,
|
|
880
1097
|
output: outputForAcceptance,
|
|
881
1098
|
cwd: step.cwd ?? ctx.cwd,
|
|
1099
|
+
signal: ctx.timeoutSignal,
|
|
1100
|
+
abortMessage: ctx.timeoutMessage ?? "Subagent timed out.",
|
|
882
1101
|
})
|
|
883
1102
|
: undefined;
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
const
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
1103
|
+
const timedOutAfterAcceptance = finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
|
|
1104
|
+
const turnBudgetExceeded = finalResult?.turnBudgetExceeded === true;
|
|
1105
|
+
const effectiveAcceptance = timedOutAfterAcceptance || turnBudgetExceeded ? undefined : acceptance;
|
|
1106
|
+
const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
|
|
1107
|
+
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !turnBudgetExceeded;
|
|
1108
|
+
const effectiveFinalExitCode = timedOutAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
1109
|
+
const effectiveFinalError = timedOutAfterAcceptance
|
|
1110
|
+
? ctx.timeoutMessage ?? "Subagent timed out."
|
|
1111
|
+
: turnBudgetExceeded
|
|
1112
|
+
? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
|
|
1113
|
+
: acceptanceCanFailRun
|
|
1114
|
+
? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
|
|
1115
|
+
: finalResult?.error;
|
|
890
1116
|
|
|
891
1117
|
if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
|
|
892
1118
|
if (ctx.artifactConfig?.includeOutput !== false) {
|
|
@@ -903,6 +1129,8 @@ async function runSingleStep(
|
|
|
903
1129
|
model: finalResult?.model,
|
|
904
1130
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
905
1131
|
modelAttempts,
|
|
1132
|
+
...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
|
|
1133
|
+
transcriptError: transcriptWriter?.getError(),
|
|
906
1134
|
skills: step.skills,
|
|
907
1135
|
timestamp: Date.now(),
|
|
908
1136
|
}, null, 2),
|
|
@@ -921,13 +1149,22 @@ async function runSingleStep(
|
|
|
921
1149
|
model: finalResult?.model,
|
|
922
1150
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
923
1151
|
modelAttempts,
|
|
1152
|
+
totalCost: costSummaryFromAttempts(modelAttempts),
|
|
924
1153
|
artifactPaths,
|
|
925
|
-
|
|
1154
|
+
transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
|
|
1155
|
+
transcriptError: transcriptWriter?.getError(),
|
|
1156
|
+
interrupted: timedOutAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
|
|
1157
|
+
timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
|
|
1158
|
+
turnBudget,
|
|
1159
|
+
turnBudgetExceeded: turnBudgetExceeded || undefined,
|
|
1160
|
+
wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
|
|
1161
|
+
toolBudget,
|
|
1162
|
+
toolBudgetBlocked: toolBudgetBlocked || undefined,
|
|
926
1163
|
completionGuardTriggered: completionGuardTriggeredFinal,
|
|
927
|
-
structuredOutput: (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
928
|
-
structuredOutputPath: effectiveStructuredOutput?.outputPath,
|
|
929
|
-
structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
|
|
930
|
-
acceptance,
|
|
1164
|
+
structuredOutput: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
1165
|
+
structuredOutputPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
|
|
1166
|
+
structuredOutputSchemaPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
|
|
1167
|
+
acceptance: effectiveAcceptance,
|
|
931
1168
|
};
|
|
932
1169
|
}
|
|
933
1170
|
|
|
@@ -1056,9 +1293,29 @@ function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { pa
|
|
|
1056
1293
|
writeInitialProgressFile(cwd);
|
|
1057
1294
|
}
|
|
1058
1295
|
|
|
1296
|
+
function resolveAsyncStepTranscriptPath(input: {
|
|
1297
|
+
artifactsDir?: string;
|
|
1298
|
+
artifactConfig?: Partial<ArtifactConfig>;
|
|
1299
|
+
runId: string;
|
|
1300
|
+
agent: string;
|
|
1301
|
+
flatIndex: number;
|
|
1302
|
+
flatStepCount: number;
|
|
1303
|
+
}): string | undefined {
|
|
1304
|
+
if (!input.artifactsDir || input.artifactConfig?.enabled === false || input.artifactConfig?.includeTranscript === false) return undefined;
|
|
1305
|
+
return getArtifactPaths(
|
|
1306
|
+
input.artifactsDir,
|
|
1307
|
+
input.runId,
|
|
1308
|
+
input.agent,
|
|
1309
|
+
input.flatStepCount > 1 ? input.flatIndex : undefined,
|
|
1310
|
+
).transcriptPath;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
|
|
1314
|
+
|
|
1059
1315
|
async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
1060
1316
|
const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
|
|
1061
1317
|
config;
|
|
1318
|
+
const globalSemaphore = new Semaphore(config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
1062
1319
|
let previousOutput = "";
|
|
1063
1320
|
const outputs: ChainOutputMap = {};
|
|
1064
1321
|
const results: StepResult[] = [];
|
|
@@ -1069,13 +1326,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1069
1326
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
1070
1327
|
const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
|
|
1071
1328
|
const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
1072
|
-
|
|
1329
|
+
const activeChildInterrupts = new Map<number, () => void>();
|
|
1330
|
+
const activeChildTimeouts = new Map<number, () => void>();
|
|
1331
|
+
const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
|
|
1332
|
+
const pendingStepSteers: SteerRequest[] = [];
|
|
1073
1333
|
let interrupted = false;
|
|
1074
1334
|
let currentActivityState: ActivityState | undefined;
|
|
1075
1335
|
let activityTimer: NodeJS.Timeout | undefined;
|
|
1336
|
+
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
1337
|
+
let timedOut = false;
|
|
1338
|
+
let turnBudgetExceeded = false;
|
|
1339
|
+
const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
|
|
1340
|
+
const timeoutAbortController = new AbortController();
|
|
1076
1341
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
1077
1342
|
let latestSessionFile: string | undefined;
|
|
1078
1343
|
|
|
1344
|
+
const flatSteps = flattenSteps(steps);
|
|
1345
|
+
const initialFlatStepCount = flatSteps.length;
|
|
1079
1346
|
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
1080
1347
|
const initialStatusSteps: RunnerStatusStep[] = [];
|
|
1081
1348
|
let flatStepCount = 0;
|
|
@@ -1084,6 +1351,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1084
1351
|
if (isParallelGroup(step)) {
|
|
1085
1352
|
parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
|
|
1086
1353
|
for (const task of step.parallel) {
|
|
1354
|
+
const taskFlatIndex = flatStepCount;
|
|
1355
|
+
const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount });
|
|
1087
1356
|
initialStatusSteps.push({
|
|
1088
1357
|
agent: task.agent,
|
|
1089
1358
|
phase: task.phase,
|
|
@@ -1091,7 +1360,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1091
1360
|
outputName: task.outputName,
|
|
1092
1361
|
structured: task.structured,
|
|
1093
1362
|
status: "pending",
|
|
1363
|
+
...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
|
|
1094
1364
|
...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
|
|
1365
|
+
...(transcriptPath ? { transcriptPath } : {}),
|
|
1095
1366
|
skills: task.skills,
|
|
1096
1367
|
model: task.model,
|
|
1097
1368
|
thinking: task.thinking,
|
|
@@ -1099,8 +1370,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1099
1370
|
recentTools: [],
|
|
1100
1371
|
recentOutput: [],
|
|
1101
1372
|
});
|
|
1373
|
+
flatStepCount++;
|
|
1102
1374
|
}
|
|
1103
|
-
flatStepCount += step.parallel.length;
|
|
1104
1375
|
} else if (isDynamicRunnerGroup(step)) {
|
|
1105
1376
|
parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
|
|
1106
1377
|
initialStatusSteps.push({
|
|
@@ -1110,11 +1381,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1110
1381
|
outputName: step.collect.as,
|
|
1111
1382
|
structured: Boolean(step.collect.outputSchema),
|
|
1112
1383
|
status: "pending",
|
|
1384
|
+
...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
|
|
1113
1385
|
recentTools: [],
|
|
1114
1386
|
recentOutput: [],
|
|
1115
1387
|
});
|
|
1116
1388
|
flatStepCount++;
|
|
1117
1389
|
} else {
|
|
1390
|
+
const stepFlatIndex = flatStepCount;
|
|
1391
|
+
const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount });
|
|
1118
1392
|
initialStatusSteps.push({
|
|
1119
1393
|
agent: step.agent,
|
|
1120
1394
|
phase: step.phase,
|
|
@@ -1122,7 +1396,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1122
1396
|
outputName: step.outputName,
|
|
1123
1397
|
structured: step.structured,
|
|
1124
1398
|
status: "pending",
|
|
1399
|
+
...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
|
|
1125
1400
|
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
1401
|
+
...(transcriptPath ? { transcriptPath } : {}),
|
|
1126
1402
|
skills: step.skills,
|
|
1127
1403
|
model: step.model,
|
|
1128
1404
|
thinking: step.thinking,
|
|
@@ -1133,11 +1409,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1133
1409
|
flatStepCount++;
|
|
1134
1410
|
}
|
|
1135
1411
|
}
|
|
1136
|
-
const flatSteps = flattenSteps(steps);
|
|
1137
1412
|
const sessionEnabled = Boolean(config.sessionDir)
|
|
1138
1413
|
|| shareEnabled
|
|
1139
1414
|
|| flatSteps.some((step) => Boolean(step.sessionFile));
|
|
1140
1415
|
const statusPayload: RunnerStatusPayload = {
|
|
1416
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
1141
1417
|
runId: id,
|
|
1142
1418
|
...(config.sessionId ? { sessionId: config.sessionId } : {}),
|
|
1143
1419
|
mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
|
|
@@ -1145,6 +1421,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1145
1421
|
lastActivityAt: overallStartTime,
|
|
1146
1422
|
startedAt: overallStartTime,
|
|
1147
1423
|
lastUpdate: overallStartTime,
|
|
1424
|
+
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
1425
|
+
...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
|
|
1426
|
+
...(config.turnBudget ? { turnBudget: initialTurnBudgetState(config.turnBudget) } : {}),
|
|
1427
|
+
...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
|
|
1148
1428
|
pid: process.pid,
|
|
1149
1429
|
cwd,
|
|
1150
1430
|
currentStep: 0,
|
|
@@ -1216,6 +1496,117 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1216
1496
|
writeAtomicJson(statusPath, statusPayload);
|
|
1217
1497
|
emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
|
|
1218
1498
|
};
|
|
1499
|
+
const registerStepInterrupt = (flatIndex: number, interrupt: (() => void) | undefined): void => {
|
|
1500
|
+
if (!interrupt) {
|
|
1501
|
+
activeChildInterrupts.delete(flatIndex);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
activeChildInterrupts.set(flatIndex, interrupt);
|
|
1505
|
+
if (interrupted) interrupt();
|
|
1506
|
+
};
|
|
1507
|
+
const registerStepTimeout = (flatIndex: number, interrupt: (() => void) | undefined): void => {
|
|
1508
|
+
if (!interrupt) {
|
|
1509
|
+
activeChildTimeouts.delete(flatIndex);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
activeChildTimeouts.set(flatIndex, interrupt);
|
|
1513
|
+
if (timedOut) interrupt();
|
|
1514
|
+
};
|
|
1515
|
+
const registerStepTurnBudgetAbort = (flatIndex: number, abort: ((message: string, state?: TurnBudgetState) => void) | undefined): void => {
|
|
1516
|
+
if (!abort) {
|
|
1517
|
+
activeChildTurnBudgetAborts.delete(flatIndex);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
activeChildTurnBudgetAborts.set(flatIndex, abort);
|
|
1521
|
+
};
|
|
1522
|
+
const interruptActiveChildren = (): void => {
|
|
1523
|
+
for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
|
|
1524
|
+
};
|
|
1525
|
+
const timeoutActiveChildren = (): void => {
|
|
1526
|
+
for (const interrupt of [...activeChildTimeouts.values()]) interrupt();
|
|
1527
|
+
};
|
|
1528
|
+
const nestedRuns = function* (children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
|
|
1529
|
+
for (const child of children ?? []) {
|
|
1530
|
+
yield child;
|
|
1531
|
+
yield* nestedRuns(child.children);
|
|
1532
|
+
yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
const interruptNestedAsyncDescendants = (): void => {
|
|
1536
|
+
if (!config.nestedRoute) return;
|
|
1537
|
+
let registry: ReturnType<typeof projectNestedEvents>;
|
|
1538
|
+
try {
|
|
1539
|
+
registry = projectNestedEvents(config.nestedRoute);
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1542
|
+
type: "subagent.nested.interrupt_failed",
|
|
1543
|
+
ts: Date.now(),
|
|
1544
|
+
runId: id,
|
|
1545
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1546
|
+
}));
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
for (const run of nestedRuns(registry.children)) {
|
|
1550
|
+
if (run.state !== "running" && run.state !== "queued") continue;
|
|
1551
|
+
const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
|
|
1552
|
+
if (!nestedAsyncDir) continue;
|
|
1553
|
+
try {
|
|
1554
|
+
deliverInterruptRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-interrupt" });
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1557
|
+
type: "subagent.nested.interrupt_failed",
|
|
1558
|
+
ts: Date.now(),
|
|
1559
|
+
runId: id,
|
|
1560
|
+
targetRunId: run.id,
|
|
1561
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1562
|
+
}));
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
const timeoutNestedAsyncDescendants = (): void => {
|
|
1567
|
+
if (!config.nestedRoute) return;
|
|
1568
|
+
let registry: ReturnType<typeof projectNestedEvents>;
|
|
1569
|
+
try {
|
|
1570
|
+
registry = projectNestedEvents(config.nestedRoute);
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1573
|
+
type: "subagent.nested.timeout_failed",
|
|
1574
|
+
ts: Date.now(),
|
|
1575
|
+
runId: id,
|
|
1576
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1577
|
+
}));
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
for (const run of nestedRuns(registry.children)) {
|
|
1581
|
+
if (run.state !== "running" && run.state !== "queued") continue;
|
|
1582
|
+
const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
|
|
1583
|
+
if (!nestedAsyncDir) continue;
|
|
1584
|
+
try {
|
|
1585
|
+
deliverTimeoutRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-timeout" });
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1588
|
+
type: "subagent.nested.timeout_failed",
|
|
1589
|
+
ts: Date.now(),
|
|
1590
|
+
runId: id,
|
|
1591
|
+
targetRunId: run.id,
|
|
1592
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1593
|
+
}));
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
const pausedStepResult = (agent: string): SingleStepResult => ({
|
|
1598
|
+
agent,
|
|
1599
|
+
output: "Paused after interrupt. Waiting for explicit next action.",
|
|
1600
|
+
exitCode: 0,
|
|
1601
|
+
interrupted: true,
|
|
1602
|
+
});
|
|
1603
|
+
const timedOutStepResult = (agent: string): SingleStepResult => ({
|
|
1604
|
+
agent,
|
|
1605
|
+
output: timeoutMessage ?? "Subagent timed out.",
|
|
1606
|
+
error: timeoutMessage ?? "Subagent timed out.",
|
|
1607
|
+
exitCode: 1,
|
|
1608
|
+
timedOut: true,
|
|
1609
|
+
});
|
|
1219
1610
|
const consumePendingAppendRequests = (): void => {
|
|
1220
1611
|
if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
|
|
1221
1612
|
const requests = consumeChainAppendRequests(asyncDir);
|
|
@@ -1346,6 +1737,58 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1346
1737
|
appendControlEvent(event);
|
|
1347
1738
|
return true;
|
|
1348
1739
|
};
|
|
1740
|
+
const deliverSteerRequest = (request: SteerRequest): void => {
|
|
1741
|
+
if (statusPayload.state !== "running") return;
|
|
1742
|
+
const runningIndexes = statusPayload.steps
|
|
1743
|
+
.map((step, index) => ({ step, index }))
|
|
1744
|
+
.filter(({ step }) => step.status === "running")
|
|
1745
|
+
.map(({ index }) => index);
|
|
1746
|
+
const targets = request.targetIndex !== undefined ? [request.targetIndex] : runningIndexes;
|
|
1747
|
+
const now = Date.now();
|
|
1748
|
+
const accepted: number[] = [];
|
|
1749
|
+
const rejected: Array<{ index: number; reason: string }> = [];
|
|
1750
|
+
for (const index of targets) {
|
|
1751
|
+
const step = statusPayload.steps[index];
|
|
1752
|
+
if (!step) {
|
|
1753
|
+
rejected.push({ index, reason: "child index out of range" });
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
if (step.status !== "running") {
|
|
1757
|
+
rejected.push({ index, reason: `child is ${step.status}` });
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
enqueueStepSteer(asyncDir, index, request);
|
|
1761
|
+
step.steerCount = (step.steerCount ?? 0) + 1;
|
|
1762
|
+
step.lastSteerAt = now;
|
|
1763
|
+
accepted.push(index);
|
|
1764
|
+
}
|
|
1765
|
+
if (accepted.length > 0) {
|
|
1766
|
+
statusPayload.steerCount = (statusPayload.steerCount ?? 0) + accepted.length;
|
|
1767
|
+
statusPayload.lastSteerAt = now;
|
|
1768
|
+
statusPayload.lastUpdate = now;
|
|
1769
|
+
writeStatusPayload();
|
|
1770
|
+
}
|
|
1771
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1772
|
+
type: "subagent.steer.requested",
|
|
1773
|
+
ts: now,
|
|
1774
|
+
runId: id,
|
|
1775
|
+
requestId: request.id,
|
|
1776
|
+
message: request.message,
|
|
1777
|
+
...(request.source ? { source: request.source } : {}),
|
|
1778
|
+
...(request.targetIndex !== undefined ? { targetIndex: request.targetIndex } : {}),
|
|
1779
|
+
acceptedIndexes: accepted,
|
|
1780
|
+
...(rejected.length ? { rejected } : {}),
|
|
1781
|
+
}));
|
|
1782
|
+
};
|
|
1783
|
+
const flushPendingStepSteers = (flatIndex: number): void => {
|
|
1784
|
+
const remaining: SteerRequest[] = [];
|
|
1785
|
+
for (const request of pendingStepSteers.splice(0)) {
|
|
1786
|
+
if (request.targetIndex === undefined) deliverSteerRequest({ ...request, targetIndex: flatIndex });
|
|
1787
|
+
else if (request.targetIndex === flatIndex) deliverSteerRequest(request);
|
|
1788
|
+
else remaining.push(request);
|
|
1789
|
+
}
|
|
1790
|
+
pendingStepSteers.push(...remaining);
|
|
1791
|
+
};
|
|
1349
1792
|
const updateStepModel = (flatIndex: number, model: string | undefined, thinking: string | undefined, now = Date.now()): void => {
|
|
1350
1793
|
const step = statusPayload.steps[flatIndex];
|
|
1351
1794
|
if (!step) return;
|
|
@@ -1354,6 +1797,40 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1354
1797
|
statusPayload.lastUpdate = now;
|
|
1355
1798
|
writeStatusPayload();
|
|
1356
1799
|
};
|
|
1800
|
+
const updateStepTurnBudget = (flatIndex: number, turnCount: number, now: number, terminalAssistantStop: boolean): void => {
|
|
1801
|
+
const budget = config.turnBudget;
|
|
1802
|
+
const step = statusPayload.steps[flatIndex];
|
|
1803
|
+
if (!budget || !step || timedOut || turnBudgetExceeded || step.turnBudgetExceeded) return;
|
|
1804
|
+
if (turnCount < budget.maxTurns) {
|
|
1805
|
+
const state: TurnBudgetState = { ...budget, outcome: "within-budget", turnCount };
|
|
1806
|
+
step.turnBudget = state;
|
|
1807
|
+
statusPayload.turnBudget = state;
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const state = turnBudgetState(budget, turnCount, false);
|
|
1811
|
+
step.turnBudget = state;
|
|
1812
|
+
statusPayload.turnBudget = state;
|
|
1813
|
+
if (!step.wrapUpRequested) {
|
|
1814
|
+
step.wrapUpRequested = true;
|
|
1815
|
+
statusPayload.wrapUpRequested = true;
|
|
1816
|
+
appendRecentStepOutput(step, [turnBudgetSoftNote(budget, turnCount)]);
|
|
1817
|
+
}
|
|
1818
|
+
if (!shouldAbortForTurnBudget(budget, turnCount, terminalAssistantStop)) return;
|
|
1819
|
+
const exceededState = turnBudgetState(budget, turnCount, true);
|
|
1820
|
+
const message = turnBudgetExceededMessage(budget, turnCount);
|
|
1821
|
+
step.turnBudget = exceededState;
|
|
1822
|
+
step.turnBudgetExceeded = true;
|
|
1823
|
+
step.wrapUpRequested = true;
|
|
1824
|
+
step.error = message;
|
|
1825
|
+
turnBudgetExceeded = true;
|
|
1826
|
+
statusPayload.turnBudget = exceededState;
|
|
1827
|
+
statusPayload.turnBudgetExceeded = true;
|
|
1828
|
+
statusPayload.wrapUpRequested = true;
|
|
1829
|
+
statusPayload.error = message;
|
|
1830
|
+
statusPayload.lastUpdate = now;
|
|
1831
|
+
appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.turn_budget_exceeded", ts: now, runId: id, stepIndex: flatIndex, agent: step.agent, turnCount, maxTurns: budget.maxTurns, graceTurns: budget.graceTurns, message }));
|
|
1832
|
+
activeChildTurnBudgetAborts.get(flatIndex)?.(message, exceededState);
|
|
1833
|
+
};
|
|
1357
1834
|
const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
|
|
1358
1835
|
const step = statusPayload.steps[flatIndex];
|
|
1359
1836
|
if (!step) return;
|
|
@@ -1363,6 +1840,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1363
1840
|
const mutates = isMutatingTool(event.toolName, event.args);
|
|
1364
1841
|
const currentPath = resolveCurrentPath(event.toolName, event.args);
|
|
1365
1842
|
step.toolCount = (step.toolCount ?? 0) + 1;
|
|
1843
|
+
const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
|
|
1844
|
+
if (configuredToolBudget) {
|
|
1845
|
+
step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount);
|
|
1846
|
+
statusPayload.toolBudget = step.toolBudget;
|
|
1847
|
+
}
|
|
1366
1848
|
step.currentTool = event.toolName;
|
|
1367
1849
|
step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
|
|
1368
1850
|
step.currentToolStartedAt = now;
|
|
@@ -1384,6 +1866,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1384
1866
|
const toolSnapshot = pendingToolResults[flatIndex];
|
|
1385
1867
|
pendingToolResults[flatIndex] = undefined;
|
|
1386
1868
|
const resultText = extractTextFromContent(event.message.content);
|
|
1869
|
+
if (toolSnapshot && resultText.includes("Tool budget hard limit reached")) {
|
|
1870
|
+
const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
|
|
1871
|
+
if (configuredToolBudget) {
|
|
1872
|
+
step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount ?? 0, toolSnapshot.tool);
|
|
1873
|
+
step.toolBudgetBlocked = true;
|
|
1874
|
+
statusPayload.toolBudget = step.toolBudget;
|
|
1875
|
+
statusPayload.toolBudgetBlocked = true;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1387
1878
|
appendRecentStepOutput(step, resultText.split("\n").slice(-10));
|
|
1388
1879
|
if (toolSnapshot?.mutates && didMutatingToolFail(resultText)) {
|
|
1389
1880
|
const state = mutatingFailureStates[flatIndex]!;
|
|
@@ -1434,6 +1925,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1434
1925
|
statusPayload.totalTokens = { input: totalInput + input, output: totalOutput + output, total: totalInput + totalOutput + input + output };
|
|
1435
1926
|
}
|
|
1436
1927
|
statusPayload.turnCount = Math.max(statusPayload.turnCount ?? 0, step.turnCount);
|
|
1928
|
+
updateStepTurnBudget(flatIndex, step.turnCount, now, isTerminalAssistantStop(event.message));
|
|
1437
1929
|
}
|
|
1438
1930
|
syncTopLevelCurrentTool();
|
|
1439
1931
|
step.lastActivityAt = now;
|
|
@@ -1531,17 +2023,68 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1531
2023
|
ts: now,
|
|
1532
2024
|
runId: id,
|
|
1533
2025
|
}));
|
|
1534
|
-
|
|
2026
|
+
interruptNestedAsyncDescendants();
|
|
2027
|
+
interruptActiveChildren();
|
|
2028
|
+
};
|
|
2029
|
+
const timeoutRunner = () => {
|
|
2030
|
+
if (timedOut || interrupted || statusPayload.state !== "running") return;
|
|
2031
|
+
timedOut = true;
|
|
2032
|
+
const now = Date.now();
|
|
2033
|
+
const message = timeoutMessage ?? "Subagent timed out.";
|
|
2034
|
+
statusPayload.state = "failed";
|
|
2035
|
+
statusPayload.timedOut = true;
|
|
2036
|
+
statusPayload.error = message;
|
|
2037
|
+
currentActivityState = undefined;
|
|
2038
|
+
statusPayload.activityState = undefined;
|
|
2039
|
+
statusPayload.lastUpdate = now;
|
|
2040
|
+
for (const step of statusPayload.steps) {
|
|
2041
|
+
if (step.status !== "running" && step.status !== "pending") continue;
|
|
2042
|
+
step.status = "failed";
|
|
2043
|
+
step.error = message;
|
|
2044
|
+
step.exitCode = 1;
|
|
2045
|
+
step.timedOut = true;
|
|
2046
|
+
step.activityState = undefined;
|
|
2047
|
+
step.endedAt = now;
|
|
2048
|
+
step.durationMs = step.startedAt ? now - step.startedAt : 0;
|
|
2049
|
+
step.lastActivityAt = now;
|
|
2050
|
+
}
|
|
2051
|
+
writeStatusPayload();
|
|
2052
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
2053
|
+
type: "subagent.run.timed_out",
|
|
2054
|
+
ts: now,
|
|
2055
|
+
runId: id,
|
|
2056
|
+
timeoutMs: config.timeoutMs,
|
|
2057
|
+
deadlineAt: config.deadlineAt,
|
|
2058
|
+
message,
|
|
2059
|
+
}));
|
|
2060
|
+
timeoutAbortController.abort();
|
|
2061
|
+
timeoutNestedAsyncDescendants();
|
|
2062
|
+
timeoutActiveChildren();
|
|
1535
2063
|
};
|
|
1536
2064
|
process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
|
|
1537
|
-
// Portable control inbox: the parent drops
|
|
1538
|
-
// it cannot deliver
|
|
1539
|
-
//
|
|
1540
|
-
const disposeControlInbox = watchAsyncControlInbox(asyncDir, {
|
|
2065
|
+
// Portable control inbox: the parent drops control request files here when
|
|
2066
|
+
// it cannot deliver OS signals (e.g. ENOSYS on Windows) or when steering a
|
|
2067
|
+
// live child. Interrupts still route into the same graceful interruptRunner().
|
|
2068
|
+
const disposeControlInbox = watchAsyncControlInbox(asyncDir, {
|
|
2069
|
+
onInterrupt: interruptRunner,
|
|
2070
|
+
onTimeout: timeoutRunner,
|
|
2071
|
+
onSteer: (request) => {
|
|
2072
|
+
const targetStep = request.targetIndex !== undefined ? statusPayload.steps[request.targetIndex] : undefined;
|
|
2073
|
+
if (targetStep?.status === "pending") pendingStepSteers.push(request);
|
|
2074
|
+
else if (request.targetIndex !== undefined || statusPayload.steps.some((step) => step.status === "running")) deliverSteerRequest(request);
|
|
2075
|
+
else pendingStepSteers.push(request);
|
|
2076
|
+
},
|
|
2077
|
+
});
|
|
2078
|
+
if (config.deadlineAt !== undefined) {
|
|
2079
|
+
const remainingMs = Math.max(0, config.deadlineAt - Date.now());
|
|
2080
|
+
timeoutTimer = setTimeout(timeoutRunner, remainingMs);
|
|
2081
|
+
timeoutTimer.unref?.();
|
|
2082
|
+
}
|
|
1541
2083
|
appendJsonl(
|
|
1542
2084
|
eventsPath,
|
|
1543
2085
|
JSON.stringify({
|
|
1544
2086
|
type: "subagent.run.started",
|
|
2087
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
1545
2088
|
ts: overallStartTime,
|
|
1546
2089
|
runId: id,
|
|
1547
2090
|
mode: statusPayload.mode,
|
|
@@ -1554,7 +2097,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1554
2097
|
let stepCursor = 0;
|
|
1555
2098
|
|
|
1556
2099
|
while (true) {
|
|
1557
|
-
if (interrupted) break;
|
|
2100
|
+
if (interrupted || timedOut || turnBudgetExceeded) break;
|
|
1558
2101
|
consumePendingAppendRequests();
|
|
1559
2102
|
if (stepCursor >= steps.length) break;
|
|
1560
2103
|
const stepIndex = stepCursor++;
|
|
@@ -1606,7 +2149,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1606
2149
|
placeholder.durationMs = 0;
|
|
1607
2150
|
}
|
|
1608
2151
|
previousOutput = "Dynamic fanout produced 0 results.";
|
|
1609
|
-
const groupAcceptance = step.effectiveAcceptance?.explicit
|
|
2152
|
+
const groupAcceptance = step.effectiveAcceptance?.explicit && !timedOut
|
|
1610
2153
|
? await evaluateAcceptance({
|
|
1611
2154
|
acceptance: step.effectiveAcceptance,
|
|
1612
2155
|
output: "",
|
|
@@ -1615,39 +2158,59 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1615
2158
|
notes: "Dynamic fanout produced 0 results.",
|
|
1616
2159
|
}),
|
|
1617
2160
|
cwd,
|
|
2161
|
+
signal: timeoutAbortController.signal,
|
|
2162
|
+
abortMessage: timeoutMessage ?? "Subagent timed out.",
|
|
1618
2163
|
})
|
|
1619
2164
|
: undefined;
|
|
1620
|
-
|
|
1621
|
-
const
|
|
1622
|
-
if (
|
|
2165
|
+
const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
|
|
2166
|
+
const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
|
|
2167
|
+
if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
|
|
2168
|
+
const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
2169
|
+
if (groupTimedOut || groupAcceptanceFailure) {
|
|
2170
|
+
const errorMessage = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
|
|
1623
2171
|
statusPayload.state = "failed";
|
|
1624
|
-
statusPayload.error =
|
|
2172
|
+
statusPayload.error = errorMessage;
|
|
1625
2173
|
if (placeholder) {
|
|
1626
2174
|
placeholder.status = "failed";
|
|
1627
|
-
placeholder.error =
|
|
2175
|
+
placeholder.error = errorMessage;
|
|
1628
2176
|
placeholder.exitCode = 1;
|
|
2177
|
+
placeholder.timedOut = groupTimedOut ? true : undefined;
|
|
1629
2178
|
}
|
|
1630
|
-
markDynamicGraphGroup(stepIndex, "failed",
|
|
1631
|
-
statusPayload.lastUpdate = now;
|
|
2179
|
+
markDynamicGraphGroup(stepIndex, "failed", errorMessage, effectiveGroupAcceptance);
|
|
2180
|
+
statusPayload.lastUpdate = Date.now();
|
|
1632
2181
|
writeStatusPayload();
|
|
1633
|
-
results.push({ agent: step.parallel.agent, output:
|
|
2182
|
+
results.push({ agent: step.parallel.agent, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, acceptance: effectiveGroupAcceptance });
|
|
1634
2183
|
break;
|
|
1635
2184
|
}
|
|
1636
2185
|
flatIndex++;
|
|
1637
2186
|
statusPayload.lastUpdate = now;
|
|
1638
|
-
markDynamicGraphGroup(stepIndex, "completed", undefined,
|
|
2187
|
+
markDynamicGraphGroup(stepIndex, "completed", undefined, effectiveGroupAcceptance);
|
|
1639
2188
|
writeStatusPayload();
|
|
1640
2189
|
continue;
|
|
1641
2190
|
}
|
|
1642
2191
|
|
|
1643
|
-
const dynamicSteps = materialized.parallel.map((task, itemIndex) =>
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
2192
|
+
const dynamicSteps = materialized.parallel.map((task, itemIndex) => {
|
|
2193
|
+
const thinkingOverride = step.thinkingOverrides?.[itemIndex];
|
|
2194
|
+
const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
|
|
2195
|
+
const thinking = thinkingOverride ? resolveEffectiveThinking(model, thinkingOverride) : undefined;
|
|
2196
|
+
return {
|
|
2197
|
+
...step.parallel,
|
|
2198
|
+
task: task.task ?? step.parallel.task,
|
|
2199
|
+
label: task.label ?? step.parallel.label,
|
|
2200
|
+
...(step.sessionFiles?.[itemIndex] ? { sessionFile: step.sessionFiles[itemIndex] } : {}),
|
|
2201
|
+
...(thinkingOverride ? {
|
|
2202
|
+
...(model ? { model } : {}),
|
|
2203
|
+
...(thinking ? { thinking } : {}),
|
|
2204
|
+
...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.map((candidate) => applyThinkingSuffix(candidate, thinkingOverride, true)) } : {}),
|
|
2205
|
+
} : {}),
|
|
2206
|
+
structuredOutput: undefined,
|
|
2207
|
+
structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
|
|
2208
|
+
};
|
|
2209
|
+
});
|
|
2210
|
+
const dynamicFlatStepCount = Math.max(statusPayload.steps.length - 1 + dynamicSteps.length, 1);
|
|
2211
|
+
const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task, itemIndex) => {
|
|
2212
|
+
const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount });
|
|
2213
|
+
return {
|
|
1651
2214
|
agent: task.agent,
|
|
1652
2215
|
phase: task.phase ?? step.phase,
|
|
1653
2216
|
label: task.label,
|
|
@@ -1655,13 +2218,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1655
2218
|
structured: Boolean(task.structuredOutputSchema),
|
|
1656
2219
|
status: "pending",
|
|
1657
2220
|
...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
|
|
2221
|
+
...(transcriptPath ? { transcriptPath } : {}),
|
|
1658
2222
|
skills: task.skills,
|
|
1659
2223
|
model: task.model,
|
|
1660
2224
|
thinking: task.thinking,
|
|
1661
2225
|
attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
|
|
1662
2226
|
recentTools: [],
|
|
1663
2227
|
recentOutput: [],
|
|
1664
|
-
}
|
|
2228
|
+
};
|
|
2229
|
+
});
|
|
1665
2230
|
statusPayload.steps.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps);
|
|
1666
2231
|
if (config.childIntercomTargets) {
|
|
1667
2232
|
config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
|
|
@@ -1710,6 +2275,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1710
2275
|
let aborted = false;
|
|
1711
2276
|
const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
|
|
1712
2277
|
const fi = groupStartFlatIndex + taskIdx;
|
|
2278
|
+
if (timedOut) return timedOutStepResult(task.agent);
|
|
2279
|
+
if (interrupted) return pausedStepResult(task.agent);
|
|
1713
2280
|
if (aborted && failFast) {
|
|
1714
2281
|
const skippedAt = Date.now();
|
|
1715
2282
|
statusPayload.steps[fi].status = "failed";
|
|
@@ -1735,6 +2302,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1735
2302
|
statusPayload.lastUpdate = taskStartTime;
|
|
1736
2303
|
writeStatusPayload();
|
|
1737
2304
|
appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
|
|
2305
|
+
flushPendingStepSteers(fi);
|
|
1738
2306
|
const singleResult = await runSingleStep(task, {
|
|
1739
2307
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
1740
2308
|
outputs,
|
|
@@ -1742,27 +2310,47 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1742
2310
|
artifactsDir, artifactConfig, id,
|
|
1743
2311
|
flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
1744
2312
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
2313
|
+
steerInboxDir: stepSteerInboxDir(asyncDir, fi),
|
|
1745
2314
|
piPackageRoot: config.piPackageRoot,
|
|
1746
2315
|
piArgv1: config.piArgv1,
|
|
1747
2316
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
1748
2317
|
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
1749
2318
|
nestedRoute: config.nestedRoute,
|
|
1750
|
-
registerInterrupt: (interrupt) =>
|
|
1751
|
-
|
|
1752
|
-
|
|
2319
|
+
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2320
|
+
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
2321
|
+
registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
|
|
2322
|
+
timeoutSignal: timeoutAbortController.signal,
|
|
2323
|
+
timeoutMessage,
|
|
2324
|
+
turnBudget: config.turnBudget,
|
|
1753
2325
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
1754
2326
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2327
|
+
skipAcceptance: () => timedOut,
|
|
1755
2328
|
});
|
|
1756
2329
|
const taskEndTime = Date.now();
|
|
1757
|
-
|
|
2330
|
+
const childInterrupted = singleResult.interrupted === true;
|
|
2331
|
+
statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
1758
2332
|
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
1759
2333
|
statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
|
|
1760
|
-
statusPayload.steps[fi].exitCode = singleResult.exitCode;
|
|
2334
|
+
statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2335
|
+
statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
2336
|
+
statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
|
|
2337
|
+
statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
|
|
2338
|
+
statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
|
|
2339
|
+
statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
|
|
2340
|
+
statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
|
|
2341
|
+
if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
|
|
2342
|
+
if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
|
|
2343
|
+
if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
|
|
2344
|
+
if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
|
|
2345
|
+
if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
|
|
1761
2346
|
statusPayload.steps[fi].model = singleResult.model;
|
|
1762
2347
|
statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
|
|
1763
2348
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
1764
2349
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
1765
|
-
statusPayload.steps[fi].
|
|
2350
|
+
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2351
|
+
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2352
|
+
statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
|
|
2353
|
+
statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
|
|
1766
2354
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
1767
2355
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
1768
2356
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -1770,13 +2358,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1770
2358
|
statusPayload.lastUpdate = taskEndTime;
|
|
1771
2359
|
writeStatusPayload();
|
|
1772
2360
|
appendJsonl(eventsPath, JSON.stringify({
|
|
1773
|
-
type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2361
|
+
type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
1774
2362
|
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
1775
|
-
exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
|
|
2363
|
+
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
|
|
1776
2364
|
}));
|
|
1777
2365
|
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
1778
|
-
return { ...singleResult, skipped: false };
|
|
1779
|
-
});
|
|
2366
|
+
return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
|
|
2367
|
+
}, globalSemaphore);
|
|
1780
2368
|
|
|
1781
2369
|
flatIndex += dynamicSteps.length;
|
|
1782
2370
|
for (const pr of parallelResults) {
|
|
@@ -1784,15 +2372,25 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1784
2372
|
agent: pr.agent,
|
|
1785
2373
|
output: pr.output,
|
|
1786
2374
|
error: pr.error,
|
|
1787
|
-
success: pr.exitCode === 0,
|
|
1788
|
-
exitCode: pr.exitCode,
|
|
2375
|
+
success: pr.interrupted !== true && pr.exitCode === 0,
|
|
2376
|
+
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
1789
2377
|
skipped: pr.skipped,
|
|
2378
|
+
interrupted: pr.interrupted,
|
|
2379
|
+
timedOut: pr.timedOut,
|
|
2380
|
+
turnBudget: pr.turnBudget,
|
|
2381
|
+
turnBudgetExceeded: pr.turnBudgetExceeded,
|
|
2382
|
+
wrapUpRequested: pr.wrapUpRequested,
|
|
2383
|
+
toolBudget: pr.toolBudget,
|
|
2384
|
+
toolBudgetBlocked: pr.toolBudgetBlocked,
|
|
1790
2385
|
sessionFile: pr.sessionFile,
|
|
1791
2386
|
intercomTarget: pr.intercomTarget,
|
|
1792
2387
|
model: pr.model,
|
|
1793
2388
|
attemptedModels: pr.attemptedModels,
|
|
1794
2389
|
modelAttempts: pr.modelAttempts,
|
|
2390
|
+
totalCost: pr.totalCost,
|
|
1795
2391
|
artifactPaths: pr.artifactPaths,
|
|
2392
|
+
transcriptPath: pr.transcriptPath,
|
|
2393
|
+
transcriptError: pr.transcriptError,
|
|
1796
2394
|
structuredOutput: pr.structuredOutput,
|
|
1797
2395
|
structuredOutputPath: pr.structuredOutputPath,
|
|
1798
2396
|
structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
|
|
@@ -1811,7 +2409,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1811
2409
|
stepIndex,
|
|
1812
2410
|
};
|
|
1813
2411
|
statusPayload.outputs = outputs;
|
|
1814
|
-
const groupAcceptance = step.effectiveAcceptance
|
|
2412
|
+
const groupAcceptance = step.effectiveAcceptance && !timedOut
|
|
1815
2413
|
? await evaluateAcceptance({
|
|
1816
2414
|
acceptance: step.effectiveAcceptance,
|
|
1817
2415
|
output: "",
|
|
@@ -1820,21 +2418,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1820
2418
|
notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
|
|
1821
2419
|
}),
|
|
1822
2420
|
cwd,
|
|
2421
|
+
signal: timeoutAbortController.signal,
|
|
2422
|
+
abortMessage: timeoutMessage ?? "Subagent timed out.",
|
|
1823
2423
|
})
|
|
1824
2424
|
: undefined;
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
|
|
2425
|
+
const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
|
|
2426
|
+
const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
|
|
2427
|
+
const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
2428
|
+
const groupError = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
|
|
2429
|
+
markDynamicGraphGroup(stepIndex, groupError ? "failed" : "completed", groupError, effectiveGroupAcceptance);
|
|
2430
|
+
if (groupError) {
|
|
1828
2431
|
results.push({
|
|
1829
2432
|
agent: step.parallel.agent,
|
|
1830
|
-
output:
|
|
1831
|
-
error:
|
|
2433
|
+
output: groupError,
|
|
2434
|
+
error: groupError,
|
|
1832
2435
|
success: false,
|
|
1833
2436
|
exitCode: 1,
|
|
2437
|
+
timedOut: groupTimedOut ? true : undefined,
|
|
1834
2438
|
structuredOutput: collection,
|
|
1835
|
-
acceptance:
|
|
2439
|
+
acceptance: effectiveGroupAcceptance,
|
|
1836
2440
|
});
|
|
1837
|
-
statusPayload.error =
|
|
2441
|
+
statusPayload.error = groupError;
|
|
1838
2442
|
}
|
|
1839
2443
|
} catch (error) {
|
|
1840
2444
|
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
@@ -1900,6 +2504,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1900
2504
|
setupHook: config.worktreeSetupHook
|
|
1901
2505
|
? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
|
|
1902
2506
|
: undefined,
|
|
2507
|
+
baseDir: config.worktreeBaseDir,
|
|
1903
2508
|
});
|
|
1904
2509
|
} catch (error) {
|
|
1905
2510
|
const setupError = error instanceof Error ? error.message : String(error);
|
|
@@ -1941,6 +2546,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1941
2546
|
concurrency,
|
|
1942
2547
|
async (task, taskIdx) => {
|
|
1943
2548
|
const fi = groupStartFlatIndex + taskIdx;
|
|
2549
|
+
if (timedOut) return timedOutStepResult(task.agent);
|
|
2550
|
+
if (interrupted) return pausedStepResult(task.agent);
|
|
1944
2551
|
if (aborted && failFast) {
|
|
1945
2552
|
const skippedAt = Date.now();
|
|
1946
2553
|
statusPayload.steps[fi].status = "failed";
|
|
@@ -1981,6 +2588,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1981
2588
|
? path.join(config.sessionDir, `parallel-${taskIdx}`)
|
|
1982
2589
|
: undefined;
|
|
1983
2590
|
const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
|
|
2591
|
+
flushPendingStepSteers(fi);
|
|
1984
2592
|
|
|
1985
2593
|
const singleResult = await runSingleStep(taskForRun, {
|
|
1986
2594
|
previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
|
|
@@ -1989,16 +2597,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1989
2597
|
artifactsDir, artifactConfig, id,
|
|
1990
2598
|
flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
1991
2599
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
2600
|
+
steerInboxDir: stepSteerInboxDir(asyncDir, fi),
|
|
1992
2601
|
piPackageRoot: config.piPackageRoot,
|
|
1993
2602
|
piArgv1: config.piArgv1,
|
|
1994
2603
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
1995
2604
|
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
1996
2605
|
nestedRoute: config.nestedRoute,
|
|
1997
|
-
registerInterrupt: (interrupt) =>
|
|
1998
|
-
|
|
1999
|
-
|
|
2606
|
+
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2607
|
+
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
2608
|
+
registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
|
|
2609
|
+
timeoutSignal: timeoutAbortController.signal,
|
|
2610
|
+
timeoutMessage,
|
|
2611
|
+
turnBudget: config.turnBudget,
|
|
2000
2612
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
2001
2613
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2614
|
+
skipAcceptance: () => timedOut,
|
|
2002
2615
|
});
|
|
2003
2616
|
if (task.sessionFile) {
|
|
2004
2617
|
latestSessionFile = task.sessionFile;
|
|
@@ -2006,16 +2619,31 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2006
2619
|
|
|
2007
2620
|
const taskEndTime = Date.now();
|
|
2008
2621
|
const taskDuration = taskEndTime - taskStartTime;
|
|
2622
|
+
const childInterrupted = singleResult.interrupted === true;
|
|
2009
2623
|
|
|
2010
|
-
statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2624
|
+
statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2011
2625
|
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
2012
2626
|
statusPayload.steps[fi].durationMs = taskDuration;
|
|
2013
|
-
statusPayload.steps[fi].exitCode = singleResult.exitCode;
|
|
2627
|
+
statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2628
|
+
statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
2629
|
+
statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
|
|
2630
|
+
statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
|
|
2631
|
+
statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
|
|
2632
|
+
statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
|
|
2633
|
+
statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
|
|
2634
|
+
if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
|
|
2635
|
+
if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
|
|
2636
|
+
if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
|
|
2637
|
+
if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
|
|
2638
|
+
if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
|
|
2014
2639
|
statusPayload.steps[fi].model = singleResult.model;
|
|
2015
2640
|
statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
|
|
2016
2641
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
2017
2642
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
2018
|
-
statusPayload.steps[fi].
|
|
2643
|
+
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2644
|
+
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2645
|
+
statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
|
|
2646
|
+
statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
|
|
2019
2647
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
2020
2648
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2021
2649
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -2024,9 +2652,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2024
2652
|
writeStatusPayload();
|
|
2025
2653
|
|
|
2026
2654
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2027
|
-
type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2655
|
+
type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2028
2656
|
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
2029
|
-
exitCode: singleResult.exitCode, durationMs: taskDuration,
|
|
2657
|
+
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
|
|
2030
2658
|
}));
|
|
2031
2659
|
if (singleResult.completionGuardTriggered) {
|
|
2032
2660
|
const event = buildControlEvent({
|
|
@@ -2043,8 +2671,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2043
2671
|
}
|
|
2044
2672
|
|
|
2045
2673
|
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
2046
|
-
return { ...singleResult, skipped: false };
|
|
2674
|
+
return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
|
|
2047
2675
|
},
|
|
2676
|
+
globalSemaphore,
|
|
2048
2677
|
);
|
|
2049
2678
|
|
|
2050
2679
|
flatIndex += group.parallel.length;
|
|
@@ -2072,15 +2701,25 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2072
2701
|
agent: pr.agent,
|
|
2073
2702
|
output: pr.output,
|
|
2074
2703
|
error: pr.error,
|
|
2075
|
-
success: pr.exitCode === 0,
|
|
2076
|
-
exitCode: pr.exitCode,
|
|
2704
|
+
success: pr.interrupted !== true && pr.exitCode === 0,
|
|
2705
|
+
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
2077
2706
|
skipped: pr.skipped,
|
|
2707
|
+
interrupted: pr.interrupted,
|
|
2708
|
+
timedOut: pr.timedOut,
|
|
2709
|
+
turnBudget: pr.turnBudget,
|
|
2710
|
+
turnBudgetExceeded: pr.turnBudgetExceeded,
|
|
2711
|
+
wrapUpRequested: pr.wrapUpRequested,
|
|
2712
|
+
toolBudget: pr.toolBudget,
|
|
2713
|
+
toolBudgetBlocked: pr.toolBudgetBlocked,
|
|
2078
2714
|
sessionFile: pr.sessionFile,
|
|
2079
2715
|
intercomTarget: pr.intercomTarget,
|
|
2080
2716
|
model: pr.model,
|
|
2081
2717
|
attemptedModels: pr.attemptedModels,
|
|
2082
2718
|
modelAttempts: pr.modelAttempts,
|
|
2719
|
+
totalCost: pr.totalCost,
|
|
2083
2720
|
artifactPaths: pr.artifactPaths,
|
|
2721
|
+
transcriptPath: pr.transcriptPath,
|
|
2722
|
+
transcriptError: pr.transcriptError,
|
|
2084
2723
|
structuredOutput: pr.structuredOutput,
|
|
2085
2724
|
structuredOutputPath: pr.structuredOutputPath,
|
|
2086
2725
|
structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
|
|
@@ -2147,6 +2786,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2147
2786
|
agent: seqStep.agent,
|
|
2148
2787
|
}));
|
|
2149
2788
|
|
|
2789
|
+
flushPendingStepSteers(flatIndex);
|
|
2150
2790
|
const singleResult = await runSingleStep(seqStep, {
|
|
2151
2791
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
2152
2792
|
outputs,
|
|
@@ -2154,16 +2794,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2154
2794
|
artifactsDir, artifactConfig, id,
|
|
2155
2795
|
flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
2156
2796
|
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
2797
|
+
steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
|
|
2157
2798
|
piPackageRoot: config.piPackageRoot,
|
|
2158
2799
|
piArgv1: config.piArgv1,
|
|
2159
2800
|
childIntercomTarget: config.childIntercomTargets?.[flatIndex],
|
|
2160
2801
|
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
2161
2802
|
nestedRoute: config.nestedRoute,
|
|
2162
|
-
registerInterrupt: (interrupt) =>
|
|
2163
|
-
|
|
2164
|
-
|
|
2803
|
+
registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
|
|
2804
|
+
registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
|
|
2805
|
+
registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(flatIndex, abort),
|
|
2806
|
+
timeoutSignal: timeoutAbortController.signal,
|
|
2807
|
+
timeoutMessage,
|
|
2808
|
+
turnBudget: config.turnBudget,
|
|
2165
2809
|
onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
|
|
2166
2810
|
onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
|
|
2811
|
+
skipAcceptance: () => timedOut,
|
|
2167
2812
|
});
|
|
2168
2813
|
if (seqStep.sessionFile) {
|
|
2169
2814
|
latestSessionFile = seqStep.sessionFile;
|
|
@@ -2172,20 +2817,30 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2172
2817
|
previousOutput = singleResult.output;
|
|
2173
2818
|
results.push({
|
|
2174
2819
|
agent: singleResult.agent,
|
|
2175
|
-
output: singleResult.output,
|
|
2176
|
-
error: singleResult.error,
|
|
2177
|
-
success: singleResult.exitCode === 0,
|
|
2178
|
-
exitCode: singleResult.exitCode,
|
|
2820
|
+
output: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
|
|
2821
|
+
error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
|
|
2822
|
+
success: !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
|
|
2823
|
+
exitCode: timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
|
|
2179
2824
|
sessionFile: singleResult.sessionFile,
|
|
2180
2825
|
intercomTarget: singleResult.intercomTarget,
|
|
2181
2826
|
model: singleResult.model,
|
|
2182
2827
|
attemptedModels: singleResult.attemptedModels,
|
|
2183
2828
|
modelAttempts: singleResult.modelAttempts,
|
|
2829
|
+
totalCost: singleResult.totalCost,
|
|
2184
2830
|
artifactPaths: singleResult.artifactPaths,
|
|
2831
|
+
transcriptPath: singleResult.transcriptPath,
|
|
2832
|
+
transcriptError: singleResult.transcriptError,
|
|
2185
2833
|
structuredOutput: singleResult.structuredOutput,
|
|
2186
2834
|
structuredOutputPath: singleResult.structuredOutputPath,
|
|
2187
2835
|
structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
|
|
2188
2836
|
acceptance: singleResult.acceptance,
|
|
2837
|
+
interrupted: singleResult.interrupted,
|
|
2838
|
+
timedOut: timedOut || singleResult.timedOut ? true : undefined,
|
|
2839
|
+
turnBudget: singleResult.turnBudget,
|
|
2840
|
+
turnBudgetExceeded: singleResult.turnBudgetExceeded,
|
|
2841
|
+
wrapUpRequested: singleResult.wrapUpRequested,
|
|
2842
|
+
toolBudget: singleResult.toolBudget,
|
|
2843
|
+
toolBudgetBlocked: singleResult.toolBudgetBlocked,
|
|
2189
2844
|
});
|
|
2190
2845
|
if (seqStep.outputName) {
|
|
2191
2846
|
outputs[seqStep.outputName] = outputEntryFromAsyncResult({
|
|
@@ -2218,15 +2873,30 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2218
2873
|
}
|
|
2219
2874
|
|
|
2220
2875
|
const stepEndTime = Date.now();
|
|
2221
|
-
|
|
2876
|
+
const childInterrupted = singleResult.interrupted === true;
|
|
2877
|
+
statusPayload.steps[flatIndex].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2222
2878
|
statusPayload.steps[flatIndex].endedAt = stepEndTime;
|
|
2223
2879
|
statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
|
|
2224
|
-
statusPayload.steps[flatIndex].exitCode = singleResult.exitCode;
|
|
2880
|
+
statusPayload.steps[flatIndex].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2881
|
+
statusPayload.steps[flatIndex].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
2882
|
+
statusPayload.steps[flatIndex].turnBudget = singleResult.turnBudget;
|
|
2883
|
+
statusPayload.steps[flatIndex].turnBudgetExceeded = singleResult.turnBudgetExceeded;
|
|
2884
|
+
statusPayload.steps[flatIndex].wrapUpRequested = singleResult.wrapUpRequested;
|
|
2885
|
+
statusPayload.steps[flatIndex].toolBudget = singleResult.toolBudget;
|
|
2886
|
+
statusPayload.steps[flatIndex].toolBudgetBlocked = singleResult.toolBudgetBlocked;
|
|
2887
|
+
if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
|
|
2888
|
+
if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
|
|
2889
|
+
if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
|
|
2890
|
+
if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
|
|
2891
|
+
if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
|
|
2225
2892
|
statusPayload.steps[flatIndex].model = singleResult.model;
|
|
2226
2893
|
statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
|
|
2227
2894
|
statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
|
|
2228
2895
|
statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
|
|
2229
|
-
statusPayload.steps[flatIndex].
|
|
2896
|
+
statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
|
|
2897
|
+
statusPayload.steps[flatIndex].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2898
|
+
statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
|
|
2899
|
+
statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
|
|
2230
2900
|
statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
|
|
2231
2901
|
statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2232
2902
|
statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -2239,12 +2909,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2239
2909
|
writeStatusPayload();
|
|
2240
2910
|
|
|
2241
2911
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2242
|
-
type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2912
|
+
type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2243
2913
|
ts: stepEndTime,
|
|
2244
2914
|
runId: id,
|
|
2245
2915
|
stepIndex: flatIndex,
|
|
2246
2916
|
agent: seqStep.agent,
|
|
2247
|
-
exitCode: singleResult.exitCode,
|
|
2917
|
+
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
|
|
2248
2918
|
durationMs: stepEndTime - stepStartTime,
|
|
2249
2919
|
tokens: stepTokens,
|
|
2250
2920
|
}));
|
|
@@ -2283,6 +2953,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2283
2953
|
}
|
|
2284
2954
|
|
|
2285
2955
|
const resultMode = config.resultMode ?? statusPayload.mode;
|
|
2956
|
+
const totalCost = results.reduce<CostSummary>((sum, result) => ({
|
|
2957
|
+
inputTokens: sum.inputTokens + (result.totalCost?.inputTokens ?? 0),
|
|
2958
|
+
outputTokens: sum.outputTokens + (result.totalCost?.outputTokens ?? 0),
|
|
2959
|
+
costUsd: sum.costUsd + (result.totalCost?.costUsd ?? 0),
|
|
2960
|
+
}), { inputTokens: 0, outputTokens: 0, costUsd: 0 });
|
|
2961
|
+
const finalTotalCost = totalCost.inputTokens > 0 || totalCost.outputTokens > 0 || totalCost.costUsd > 0 ? totalCost : undefined;
|
|
2286
2962
|
const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
|
|
2287
2963
|
const agentName = finalFlatAgents.length === 1
|
|
2288
2964
|
? finalFlatAgents[0]!
|
|
@@ -2323,14 +2999,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2323
2999
|
clearInterval(activityTimer);
|
|
2324
3000
|
activityTimer = undefined;
|
|
2325
3001
|
}
|
|
3002
|
+
if (timeoutTimer) {
|
|
3003
|
+
clearTimeout(timeoutTimer);
|
|
3004
|
+
timeoutTimer = undefined;
|
|
3005
|
+
}
|
|
2326
3006
|
disposeControlInbox();
|
|
2327
3007
|
const effectiveSessionFile = sessionFile ?? latestSessionFile;
|
|
2328
3008
|
const runEndedAt = Date.now();
|
|
2329
|
-
statusPayload.state = interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
|
|
3009
|
+
statusPayload.state = timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
|
|
2330
3010
|
statusPayload.activityState = undefined;
|
|
3011
|
+
if (timedOut) {
|
|
3012
|
+
statusPayload.timedOut = true;
|
|
3013
|
+
statusPayload.error = timeoutMessage ?? "Subagent timed out.";
|
|
3014
|
+
}
|
|
3015
|
+
if (turnBudgetExceeded && !statusPayload.error) {
|
|
3016
|
+
const budget = statusPayload.turnBudget;
|
|
3017
|
+
statusPayload.error = budget ? turnBudgetExceededMessage(budget, budget.turnCount) : "Subagent exceeded turn budget.";
|
|
3018
|
+
}
|
|
2331
3019
|
statusPayload.endedAt = runEndedAt;
|
|
2332
3020
|
statusPayload.lastUpdate = runEndedAt;
|
|
2333
3021
|
statusPayload.sessionFile = effectiveSessionFile;
|
|
3022
|
+
statusPayload.totalCost = finalTotalCost;
|
|
2334
3023
|
statusPayload.shareUrl = shareUrl;
|
|
2335
3024
|
statusPayload.gistUrl = gistUrl;
|
|
2336
3025
|
statusPayload.shareError = shareError;
|
|
@@ -2345,10 +3034,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2345
3034
|
eventsPath,
|
|
2346
3035
|
JSON.stringify({
|
|
2347
3036
|
type: "subagent.run.completed",
|
|
3037
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
2348
3038
|
ts: runEndedAt,
|
|
2349
3039
|
runId: id,
|
|
2350
3040
|
status: statusPayload.state,
|
|
2351
3041
|
durationMs: runEndedAt - overallStartTime,
|
|
3042
|
+
totalTokens: statusPayload.totalTokens,
|
|
3043
|
+
totalCost: finalTotalCost,
|
|
2352
3044
|
}),
|
|
2353
3045
|
);
|
|
2354
3046
|
writeRunLog(logPath, {
|
|
@@ -2372,25 +3064,44 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2372
3064
|
|
|
2373
3065
|
try {
|
|
2374
3066
|
writeAtomicJson(resultPath, {
|
|
3067
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
2375
3068
|
id,
|
|
2376
3069
|
agent: agentName,
|
|
2377
3070
|
mode: resultMode,
|
|
2378
|
-
success: !interrupted && results.every((r) => r.success),
|
|
2379
|
-
state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
|
|
2380
|
-
summary: interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
|
|
3071
|
+
success: !timedOut && !turnBudgetExceeded && !interrupted && results.every((r) => r.success),
|
|
3072
|
+
state: timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
|
|
3073
|
+
summary: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
|
|
3074
|
+
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
3075
|
+
...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
|
|
3076
|
+
...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
|
|
3077
|
+
...(statusPayload.turnBudgetExceeded ? { turnBudgetExceeded: true } : {}),
|
|
3078
|
+
...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
|
|
3079
|
+
...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
|
|
3080
|
+
...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
|
|
3081
|
+
...(timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
|
|
2381
3082
|
results: results.map((r) => ({
|
|
2382
3083
|
agent: r.agent,
|
|
2383
3084
|
output: r.output,
|
|
2384
3085
|
error: r.error,
|
|
2385
3086
|
success: r.success,
|
|
2386
3087
|
skipped: r.skipped || undefined,
|
|
3088
|
+
interrupted: r.interrupted || undefined,
|
|
3089
|
+
timedOut: r.timedOut || undefined,
|
|
3090
|
+
turnBudget: r.turnBudget,
|
|
3091
|
+
turnBudgetExceeded: r.turnBudgetExceeded || undefined,
|
|
3092
|
+
wrapUpRequested: r.wrapUpRequested || undefined,
|
|
3093
|
+
toolBudget: r.toolBudget,
|
|
3094
|
+
toolBudgetBlocked: r.toolBudgetBlocked || undefined,
|
|
2387
3095
|
sessionFile: r.sessionFile,
|
|
2388
3096
|
intercomTarget: r.intercomTarget,
|
|
2389
3097
|
model: r.model,
|
|
2390
3098
|
attemptedModels: r.attemptedModels,
|
|
2391
3099
|
modelAttempts: r.modelAttempts,
|
|
3100
|
+
totalCost: r.totalCost,
|
|
2392
3101
|
artifactPaths: r.artifactPaths,
|
|
2393
3102
|
truncated: r.truncated,
|
|
3103
|
+
transcriptPath: r.transcriptPath,
|
|
3104
|
+
transcriptError: r.transcriptError,
|
|
2394
3105
|
structuredOutput: r.structuredOutput,
|
|
2395
3106
|
structuredOutputPath: r.structuredOutputPath,
|
|
2396
3107
|
structuredOutputSchemaPath: r.structuredOutputSchemaPath,
|
|
@@ -2398,9 +3109,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2398
3109
|
})),
|
|
2399
3110
|
outputs,
|
|
2400
3111
|
workflowGraph: statusPayload.workflowGraph,
|
|
2401
|
-
exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
3112
|
+
exitCode: timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
2402
3113
|
timestamp: runEndedAt,
|
|
2403
3114
|
durationMs: runEndedAt - overallStartTime,
|
|
3115
|
+
totalTokens: statusPayload.totalTokens,
|
|
3116
|
+
totalCost: finalTotalCost,
|
|
2404
3117
|
truncated,
|
|
2405
3118
|
artifactsDir,
|
|
2406
3119
|
cwd,
|