pi-subagents 0.31.1 → 0.32.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 +40 -4
- package/README.md +97 -7
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +6 -1
- package/src/agents/agent-management.ts +6 -1
- package/src/agents/agents.ts +55 -11
- package/src/extension/companion-suggestions.ts +359 -0
- package/src/extension/config.ts +27 -4
- package/src/extension/doctor.ts +2 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +58 -4
- package/src/extension/schemas.ts +2 -2
- package/src/runs/background/async-execution.ts +138 -33
- package/src/runs/background/async-job-tracker.ts +77 -1
- package/src/runs/background/async-status.ts +41 -9
- package/src/runs/background/chain-root-attachment.ts +34 -4
- package/src/runs/background/control-channel.ts +50 -0
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/stale-run-reconciler.ts +28 -1
- package/src/runs/background/subagent-runner.ts +454 -115
- package/src/runs/foreground/chain-execution.ts +29 -7
- package/src/runs/foreground/execution.ts +24 -6
- package/src/runs/foreground/subagent-executor.ts +209 -35
- package/src/runs/shared/acceptance.ts +45 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +4 -0
- package/src/runs/shared/nested-events.ts +58 -0
- package/src/runs/shared/parallel-utils.ts +49 -1
- package/src/runs/shared/pi-args.ts +5 -3
- 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 +2 -2
- package/src/runs/shared/worktree.ts +28 -5
- package/src/shared/artifacts.ts +15 -1
- package/src/shared/fork-context.ts +133 -22
- package/src/shared/types.ts +81 -3
- package/src/shared/utils.ts +99 -14
- package/src/slash/slash-commands.ts +117 -0
- package/src/tui/render.ts +16 -4
|
@@ -4,7 +4,7 @@ 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 { consumeInterruptRequest, watchAsyncControlInbox } from "./control-channel.ts";
|
|
7
|
+
import { consumeInterruptRequest, deliverInterruptRequest, deliverTimeoutRequest, watchAsyncControlInbox } from "./control-channel.ts";
|
|
8
8
|
import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
9
9
|
import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
|
|
10
10
|
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
@@ -15,14 +15,17 @@ import {
|
|
|
15
15
|
type AsyncParallelGroupStatus,
|
|
16
16
|
type AsyncStatus,
|
|
17
17
|
type ChainOutputMap,
|
|
18
|
+
type CostSummary,
|
|
18
19
|
type ModelAttempt,
|
|
19
20
|
type NestedRouteInfo,
|
|
21
|
+
type NestedRunSummary,
|
|
20
22
|
type ResolvedControlConfig,
|
|
21
23
|
type SubagentRunMode,
|
|
22
24
|
type Usage,
|
|
23
25
|
type WorkflowGraphSnapshot,
|
|
24
26
|
DEFAULT_MAX_OUTPUT,
|
|
25
27
|
type MaxOutputConfig,
|
|
28
|
+
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
26
29
|
truncateOutput,
|
|
27
30
|
getSubagentDepthEnv,
|
|
28
31
|
} from "../../shared/types.ts";
|
|
@@ -43,15 +46,17 @@ import {
|
|
|
43
46
|
mapConcurrent,
|
|
44
47
|
aggregateParallelOutputs,
|
|
45
48
|
MAX_PARALLEL_CONCURRENCY,
|
|
49
|
+
DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
|
|
50
|
+
Semaphore,
|
|
46
51
|
} from "../shared/parallel-utils.ts";
|
|
47
|
-
import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
52
|
+
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
48
53
|
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
49
54
|
import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
|
|
50
55
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
51
|
-
import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
|
|
56
|
+
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
52
57
|
import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
53
58
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
54
|
-
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
|
|
59
|
+
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
|
|
55
60
|
import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
56
61
|
import {
|
|
57
62
|
createMutatingFailureState,
|
|
@@ -101,6 +106,7 @@ interface SubagentRunConfig {
|
|
|
101
106
|
piArgv1?: string;
|
|
102
107
|
worktreeSetupHook?: string;
|
|
103
108
|
worktreeSetupHookTimeoutMs?: number;
|
|
109
|
+
worktreeBaseDir?: string;
|
|
104
110
|
controlConfig?: ResolvedControlConfig;
|
|
105
111
|
controlIntercomTarget?: string;
|
|
106
112
|
childIntercomTargets?: Array<string | undefined>;
|
|
@@ -109,6 +115,10 @@ interface SubagentRunConfig {
|
|
|
109
115
|
workflowGraph?: WorkflowGraphSnapshot;
|
|
110
116
|
nestedRoute?: NestedRouteInfo;
|
|
111
117
|
nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
|
|
118
|
+
timeoutMs?: number;
|
|
119
|
+
deadlineAt?: number;
|
|
120
|
+
/** Global cap on simultaneously-running subagent tasks within this run. */
|
|
121
|
+
globalConcurrencyLimit?: number;
|
|
112
122
|
}
|
|
113
123
|
|
|
114
124
|
interface StepResult {
|
|
@@ -118,11 +128,14 @@ interface StepResult {
|
|
|
118
128
|
success: boolean;
|
|
119
129
|
exitCode?: number | null;
|
|
120
130
|
skipped?: boolean;
|
|
131
|
+
interrupted?: boolean;
|
|
132
|
+
timedOut?: boolean;
|
|
121
133
|
sessionFile?: string;
|
|
122
134
|
intercomTarget?: string;
|
|
123
135
|
model?: string;
|
|
124
136
|
attemptedModels?: string[];
|
|
125
137
|
modelAttempts?: ModelAttempt[];
|
|
138
|
+
totalCost?: CostSummary;
|
|
126
139
|
artifactPaths?: ArtifactPaths;
|
|
127
140
|
truncated?: boolean;
|
|
128
141
|
structuredOutput?: unknown;
|
|
@@ -237,6 +250,21 @@ function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsag
|
|
|
237
250
|
return total > 0 ? { input, output, total } : null;
|
|
238
251
|
}
|
|
239
252
|
|
|
253
|
+
function costSummaryFromAttempts(attempts: ModelAttempt[] | undefined): CostSummary | undefined {
|
|
254
|
+
if (!attempts || attempts.length === 0) return undefined;
|
|
255
|
+
let inputTokens = 0;
|
|
256
|
+
let outputTokens = 0;
|
|
257
|
+
let costUsd = 0;
|
|
258
|
+
for (const attempt of attempts) {
|
|
259
|
+
inputTokens += attempt.usage?.input ?? 0;
|
|
260
|
+
outputTokens += attempt.usage?.output ?? 0;
|
|
261
|
+
costUsd += attempt.usage?.cost ?? 0;
|
|
262
|
+
}
|
|
263
|
+
return inputTokens > 0 || outputTokens > 0 || costUsd > 0
|
|
264
|
+
? { inputTokens, outputTokens, costUsd }
|
|
265
|
+
: undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
240
268
|
function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
|
|
241
269
|
const nonEmpty = lines.filter((line) => line.trim());
|
|
242
270
|
if (nonEmpty.length === 0) return;
|
|
@@ -295,6 +323,7 @@ interface RunPiStreamingResult {
|
|
|
295
323
|
error?: string;
|
|
296
324
|
finalOutput: string;
|
|
297
325
|
interrupted?: boolean;
|
|
326
|
+
timedOut?: boolean;
|
|
298
327
|
observedMutationAttempt?: boolean;
|
|
299
328
|
}
|
|
300
329
|
|
|
@@ -309,6 +338,8 @@ function runPiStreaming(
|
|
|
309
338
|
childEventContext?: ChildEventContext,
|
|
310
339
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void,
|
|
311
340
|
onChildEvent?: (event: ChildEvent) => void,
|
|
341
|
+
registerTimeout?: (interrupt: (() => void) | undefined) => void,
|
|
342
|
+
timeoutMessage?: string,
|
|
312
343
|
): Promise<RunPiStreamingResult> {
|
|
313
344
|
return new Promise((resolve) => {
|
|
314
345
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
@@ -332,6 +363,7 @@ function runPiStreaming(
|
|
|
332
363
|
let error: string | undefined;
|
|
333
364
|
let assistantError: string | undefined;
|
|
334
365
|
let interrupted = false;
|
|
366
|
+
let timedOut = false;
|
|
335
367
|
let observedMutationAttempt = false;
|
|
336
368
|
const rawStdoutLines: string[] = [];
|
|
337
369
|
|
|
@@ -430,11 +462,13 @@ function runPiStreaming(
|
|
|
430
462
|
// a lingering stdio holder after `exit`, or a child that never exits.
|
|
431
463
|
const FINAL_STOP_GRACE_MS = 1000;
|
|
432
464
|
const HARD_KILL_MS = 3000;
|
|
465
|
+
const TIMEOUT_HARD_KILL_MS = 3000;
|
|
433
466
|
let childExited = false;
|
|
434
467
|
let forcedTerminationSignal = false;
|
|
435
468
|
let cleanTerminalAssistantStopReceived = false;
|
|
436
469
|
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
437
470
|
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
471
|
+
let timeoutHardKillTimer: NodeJS.Timeout | undefined;
|
|
438
472
|
let settled = false;
|
|
439
473
|
const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
|
|
440
474
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
@@ -449,14 +483,25 @@ function runPiStreaming(
|
|
|
449
483
|
processStderrText(chunk.toString());
|
|
450
484
|
});
|
|
451
485
|
registerInterrupt?.(() => {
|
|
452
|
-
if (settled) return;
|
|
486
|
+
if (settled || timedOut) return;
|
|
453
487
|
interrupted = true;
|
|
454
488
|
if (!error) error = "Interrupted. Waiting for explicit next action.";
|
|
455
489
|
trySignalChild(child, "SIGINT");
|
|
456
490
|
setTimeout(() => {
|
|
457
|
-
if (!settled) trySignalChild(child, "SIGTERM");
|
|
491
|
+
if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
|
|
458
492
|
}, 1000).unref?.();
|
|
459
493
|
});
|
|
494
|
+
registerTimeout?.(() => {
|
|
495
|
+
if (settled || timedOut) return;
|
|
496
|
+
timedOut = true;
|
|
497
|
+
interrupted = false;
|
|
498
|
+
error = timeoutMessage ?? "Subagent timed out.";
|
|
499
|
+
trySignalChild(child, "SIGTERM");
|
|
500
|
+
timeoutHardKillTimer = setTimeout(() => {
|
|
501
|
+
if (!settled) trySignalChild(child, "SIGKILL");
|
|
502
|
+
}, TIMEOUT_HARD_KILL_MS);
|
|
503
|
+
timeoutHardKillTimer.unref?.();
|
|
504
|
+
});
|
|
460
505
|
const clearDrainTimers = () => {
|
|
461
506
|
if (finalDrainTimer) {
|
|
462
507
|
clearTimeout(finalDrainTimer);
|
|
@@ -466,6 +511,10 @@ function runPiStreaming(
|
|
|
466
511
|
clearTimeout(finalHardKillTimer);
|
|
467
512
|
finalHardKillTimer = undefined;
|
|
468
513
|
}
|
|
514
|
+
if (timeoutHardKillTimer) {
|
|
515
|
+
clearTimeout(timeoutHardKillTimer);
|
|
516
|
+
timeoutHardKillTimer = undefined;
|
|
517
|
+
}
|
|
469
518
|
};
|
|
470
519
|
function startFinalDrain(): void {
|
|
471
520
|
if (childExited || finalDrainTimer || settled) return;
|
|
@@ -492,6 +541,7 @@ function runPiStreaming(
|
|
|
492
541
|
child.on("close", (exitCode, signal) => {
|
|
493
542
|
settled = true;
|
|
494
543
|
registerInterrupt?.(undefined);
|
|
544
|
+
registerTimeout?.(undefined);
|
|
495
545
|
clearDrainTimers();
|
|
496
546
|
clearStdioGuard();
|
|
497
547
|
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
@@ -502,13 +552,14 @@ function runPiStreaming(
|
|
|
502
552
|
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
|
|
503
553
|
resolve({
|
|
504
554
|
stderr,
|
|
505
|
-
exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
555
|
+
exitCode: timedOut ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
506
556
|
messages,
|
|
507
557
|
usage,
|
|
508
558
|
model,
|
|
509
|
-
error: interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
510
|
-
finalOutput,
|
|
559
|
+
error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
560
|
+
finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput,
|
|
511
561
|
interrupted,
|
|
562
|
+
timedOut,
|
|
512
563
|
observedMutationAttempt,
|
|
513
564
|
});
|
|
514
565
|
});
|
|
@@ -516,12 +567,13 @@ function runPiStreaming(
|
|
|
516
567
|
child.on("error", (spawnError) => {
|
|
517
568
|
settled = true;
|
|
518
569
|
registerInterrupt?.(undefined);
|
|
570
|
+
registerTimeout?.(undefined);
|
|
519
571
|
clearDrainTimers();
|
|
520
572
|
clearStdioGuard();
|
|
521
573
|
outputStream.end();
|
|
522
574
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
523
575
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
524
|
-
resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
|
|
576
|
+
resolve({ stderr, exitCode: 1, messages, usage, model, error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : error ?? assistantError ?? spawnErrorMessage, finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, observedMutationAttempt });
|
|
525
577
|
});
|
|
526
578
|
});
|
|
527
579
|
}
|
|
@@ -649,11 +701,15 @@ interface SingleStepContext {
|
|
|
649
701
|
piPackageRoot?: string;
|
|
650
702
|
piArgv1?: string;
|
|
651
703
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void;
|
|
704
|
+
registerTimeout?: (interrupt: (() => void) | undefined) => void;
|
|
705
|
+
timeoutSignal?: AbortSignal;
|
|
706
|
+
timeoutMessage?: string;
|
|
652
707
|
childIntercomTarget?: string;
|
|
653
708
|
orchestratorIntercomTarget?: string;
|
|
654
709
|
nestedRoute?: NestedRouteInfo;
|
|
655
710
|
onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
|
|
656
711
|
onChildEvent?: (event: ChildEvent) => void;
|
|
712
|
+
skipAcceptance?: () => boolean;
|
|
657
713
|
}
|
|
658
714
|
|
|
659
715
|
/** Run a single pi agent step, returning output and metadata */
|
|
@@ -670,6 +726,7 @@ async function runSingleStep(
|
|
|
670
726
|
modelAttempts?: ModelAttempt[];
|
|
671
727
|
artifactPaths?: ArtifactPaths;
|
|
672
728
|
interrupted?: boolean;
|
|
729
|
+
timedOut?: boolean;
|
|
673
730
|
sessionFile?: string;
|
|
674
731
|
intercomTarget?: string;
|
|
675
732
|
completionGuardTriggered?: boolean;
|
|
@@ -679,27 +736,52 @@ async function runSingleStep(
|
|
|
679
736
|
acceptance?: import("../../shared/types.ts").AcceptanceLedger;
|
|
680
737
|
}> {
|
|
681
738
|
if (step.importAsyncRoot) {
|
|
682
|
-
|
|
739
|
+
let importTimedOut = false;
|
|
740
|
+
ctx.registerTimeout?.(() => {
|
|
741
|
+
importTimedOut = true;
|
|
742
|
+
let pid: number | undefined;
|
|
743
|
+
try {
|
|
744
|
+
pid = readStatus(step.importAsyncRoot!.asyncDir)?.pid;
|
|
745
|
+
} catch {
|
|
746
|
+
pid = undefined;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
deliverTimeoutRequest({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-timeout" });
|
|
750
|
+
} catch {
|
|
751
|
+
// The parent runner's own timeout result is authoritative for the attached step.
|
|
752
|
+
}
|
|
753
|
+
});
|
|
683
754
|
try {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
755
|
+
const imported = await waitForImportedAsyncRoot(step.importAsyncRoot, {
|
|
756
|
+
shouldAbort: () => importTimedOut || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true,
|
|
757
|
+
timeoutMessage: ctx.timeoutMessage,
|
|
758
|
+
});
|
|
759
|
+
try {
|
|
760
|
+
fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
|
|
761
|
+
} catch {
|
|
762
|
+
// Output files are observability only for imported roots.
|
|
763
|
+
}
|
|
764
|
+
const timedOut = importTimedOut || imported.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
|
|
765
|
+
return {
|
|
766
|
+
agent: imported.agent,
|
|
767
|
+
output: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.output,
|
|
768
|
+
exitCode: timedOut ? 1 : imported.exitCode,
|
|
769
|
+
error: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.error,
|
|
770
|
+
timedOut: timedOut ? true : undefined,
|
|
771
|
+
sessionFile: imported.sessionFile,
|
|
772
|
+
intercomTarget: imported.intercomTarget,
|
|
773
|
+
model: imported.model,
|
|
774
|
+
attemptedModels: imported.attemptedModels,
|
|
775
|
+
modelAttempts: imported.modelAttempts,
|
|
776
|
+
totalCost: imported.totalCost,
|
|
777
|
+
structuredOutput: timedOut ? undefined : imported.structuredOutput,
|
|
778
|
+
structuredOutputPath: timedOut ? undefined : imported.structuredOutputPath,
|
|
779
|
+
structuredOutputSchemaPath: timedOut ? undefined : imported.structuredOutputSchemaPath,
|
|
780
|
+
acceptance: timedOut ? undefined : imported.acceptance,
|
|
781
|
+
};
|
|
782
|
+
} finally {
|
|
783
|
+
ctx.registerTimeout?.(undefined);
|
|
687
784
|
}
|
|
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
785
|
}
|
|
704
786
|
|
|
705
787
|
const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
|
|
@@ -740,6 +822,7 @@ async function runSingleStep(
|
|
|
740
822
|
let completionGuardTriggeredFinal = false;
|
|
741
823
|
|
|
742
824
|
for (let index = 0; index < candidates.length; index++) {
|
|
825
|
+
if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
743
826
|
const candidate = candidates[index];
|
|
744
827
|
ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
|
|
745
828
|
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
@@ -791,13 +874,21 @@ async function runSingleStep(
|
|
|
791
874
|
{ eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
792
875
|
ctx.registerInterrupt,
|
|
793
876
|
ctx.onChildEvent,
|
|
877
|
+
ctx.registerTimeout,
|
|
878
|
+
ctx.timeoutMessage,
|
|
794
879
|
);
|
|
795
880
|
cleanupTempDir(tempDir);
|
|
796
881
|
|
|
797
882
|
const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
|
|
883
|
+
const missingStructuredOutput = effectiveStructuredOutput
|
|
884
|
+
? !fs.existsSync(effectiveStructuredOutput.outputPath)
|
|
885
|
+
: false;
|
|
886
|
+
const emptyOutputError = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !run.finalOutput.trim() && (!effectiveStructuredOutput || missingStructuredOutput)
|
|
887
|
+
? "Subagent produced no output (possible model cold-start or empty response)."
|
|
888
|
+
: undefined;
|
|
798
889
|
let structuredOutput: unknown;
|
|
799
890
|
let structuredError: string | undefined;
|
|
800
|
-
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError) {
|
|
891
|
+
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError) {
|
|
801
892
|
const structured = readStructuredOutput({
|
|
802
893
|
schema: effectiveStructuredOutput.schema,
|
|
803
894
|
schemaPath: effectiveStructuredOutput.schemaPath,
|
|
@@ -806,7 +897,7 @@ async function runSingleStep(
|
|
|
806
897
|
if (structured.error) structuredError = structured.error;
|
|
807
898
|
else structuredOutput = structured.value;
|
|
808
899
|
}
|
|
809
|
-
const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
|
|
900
|
+
const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
|
|
810
901
|
? evaluateCompletionMutationGuard({
|
|
811
902
|
agent: step.agent,
|
|
812
903
|
task: taskForCompletionGuard,
|
|
@@ -825,16 +916,18 @@ async function runSingleStep(
|
|
|
825
916
|
? 1
|
|
826
917
|
: hiddenError?.hasError
|
|
827
918
|
? (hiddenError.exitCode ?? 1)
|
|
828
|
-
:
|
|
919
|
+
: emptyOutputError
|
|
829
920
|
? 1
|
|
830
|
-
: run.exitCode
|
|
921
|
+
: run.error && run.exitCode === 0
|
|
922
|
+
? 1
|
|
923
|
+
: run.exitCode;
|
|
831
924
|
const error = completionGuardError
|
|
832
925
|
?? structuredError
|
|
833
926
|
?? (hiddenError?.hasError
|
|
834
927
|
? hiddenError.details
|
|
835
928
|
? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
|
|
836
929
|
: `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
|
|
837
|
-
: run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
|
|
930
|
+
: emptyOutputError ?? (run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)));
|
|
838
931
|
const attempt: ModelAttempt = {
|
|
839
932
|
model: candidate ?? run.model ?? step.model ?? "default",
|
|
840
933
|
success: effectiveExitCode === 0 && !error,
|
|
@@ -847,6 +940,7 @@ async function runSingleStep(
|
|
|
847
940
|
completionGuardTriggeredFinal = completionGuardTriggered;
|
|
848
941
|
finalOutputSnapshot = outputSnapshot;
|
|
849
942
|
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
|
|
943
|
+
if (run.timedOut || ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
850
944
|
if (attempt.success || completionGuardTriggered) break;
|
|
851
945
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
852
946
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
|
|
@@ -874,19 +968,25 @@ async function runSingleStep(
|
|
|
874
968
|
saveError: resolvedOutput.saveError,
|
|
875
969
|
});
|
|
876
970
|
outputForSummary = finalizedOutput.displayOutput;
|
|
877
|
-
const acceptance = step.effectiveAcceptance
|
|
971
|
+
const acceptance = step.effectiveAcceptance && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
|
|
878
972
|
? await evaluateAcceptance({
|
|
879
973
|
acceptance: step.effectiveAcceptance,
|
|
880
974
|
output: outputForAcceptance,
|
|
881
975
|
cwd: step.cwd ?? ctx.cwd,
|
|
976
|
+
signal: ctx.timeoutSignal,
|
|
977
|
+
abortMessage: ctx.timeoutMessage ?? "Subagent timed out.",
|
|
882
978
|
})
|
|
883
979
|
: undefined;
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
const
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
980
|
+
const timedOutAfterAcceptance = finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
|
|
981
|
+
const effectiveAcceptance = timedOutAfterAcceptance ? undefined : acceptance;
|
|
982
|
+
const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
|
|
983
|
+
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance;
|
|
984
|
+
const effectiveFinalExitCode = timedOutAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
985
|
+
const effectiveFinalError = timedOutAfterAcceptance
|
|
986
|
+
? ctx.timeoutMessage ?? "Subagent timed out."
|
|
987
|
+
: acceptanceCanFailRun
|
|
988
|
+
? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
|
|
989
|
+
: finalResult?.error;
|
|
890
990
|
|
|
891
991
|
if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
|
|
892
992
|
if (ctx.artifactConfig?.includeOutput !== false) {
|
|
@@ -921,13 +1021,15 @@ async function runSingleStep(
|
|
|
921
1021
|
model: finalResult?.model,
|
|
922
1022
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
923
1023
|
modelAttempts,
|
|
1024
|
+
totalCost: costSummaryFromAttempts(modelAttempts),
|
|
924
1025
|
artifactPaths,
|
|
925
|
-
interrupted: finalResult?.interrupted,
|
|
1026
|
+
interrupted: timedOutAfterAcceptance ? false : finalResult?.interrupted,
|
|
1027
|
+
timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
|
|
926
1028
|
completionGuardTriggered: completionGuardTriggeredFinal,
|
|
927
|
-
structuredOutput: (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
928
|
-
structuredOutputPath: effectiveStructuredOutput?.outputPath,
|
|
929
|
-
structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
|
|
930
|
-
acceptance,
|
|
1029
|
+
structuredOutput: timedOutAfterAcceptance ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
1030
|
+
structuredOutputPath: timedOutAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
|
|
1031
|
+
structuredOutputSchemaPath: timedOutAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
|
|
1032
|
+
acceptance: effectiveAcceptance,
|
|
931
1033
|
};
|
|
932
1034
|
}
|
|
933
1035
|
|
|
@@ -1056,9 +1158,12 @@ function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { pa
|
|
|
1056
1158
|
writeInitialProgressFile(cwd);
|
|
1057
1159
|
}
|
|
1058
1160
|
|
|
1161
|
+
type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
|
|
1162
|
+
|
|
1059
1163
|
async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
1060
1164
|
const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
|
|
1061
1165
|
config;
|
|
1166
|
+
const globalSemaphore = new Semaphore(config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
1062
1167
|
let previousOutput = "";
|
|
1063
1168
|
const outputs: ChainOutputMap = {};
|
|
1064
1169
|
const results: StepResult[] = [];
|
|
@@ -1069,10 +1174,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1069
1174
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
1070
1175
|
const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
|
|
1071
1176
|
const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
1072
|
-
|
|
1177
|
+
const activeChildInterrupts = new Map<number, () => void>();
|
|
1178
|
+
const activeChildTimeouts = new Map<number, () => void>();
|
|
1073
1179
|
let interrupted = false;
|
|
1074
1180
|
let currentActivityState: ActivityState | undefined;
|
|
1075
1181
|
let activityTimer: NodeJS.Timeout | undefined;
|
|
1182
|
+
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
1183
|
+
let timedOut = false;
|
|
1184
|
+
const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
|
|
1185
|
+
const timeoutAbortController = new AbortController();
|
|
1076
1186
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
1077
1187
|
let latestSessionFile: string | undefined;
|
|
1078
1188
|
|
|
@@ -1138,6 +1248,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1138
1248
|
|| shareEnabled
|
|
1139
1249
|
|| flatSteps.some((step) => Boolean(step.sessionFile));
|
|
1140
1250
|
const statusPayload: RunnerStatusPayload = {
|
|
1251
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
1141
1252
|
runId: id,
|
|
1142
1253
|
...(config.sessionId ? { sessionId: config.sessionId } : {}),
|
|
1143
1254
|
mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
|
|
@@ -1145,6 +1256,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1145
1256
|
lastActivityAt: overallStartTime,
|
|
1146
1257
|
startedAt: overallStartTime,
|
|
1147
1258
|
lastUpdate: overallStartTime,
|
|
1259
|
+
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
1260
|
+
...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
|
|
1148
1261
|
pid: process.pid,
|
|
1149
1262
|
cwd,
|
|
1150
1263
|
currentStep: 0,
|
|
@@ -1216,6 +1329,110 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1216
1329
|
writeAtomicJson(statusPath, statusPayload);
|
|
1217
1330
|
emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
|
|
1218
1331
|
};
|
|
1332
|
+
const registerStepInterrupt = (flatIndex: number, interrupt: (() => void) | undefined): void => {
|
|
1333
|
+
if (!interrupt) {
|
|
1334
|
+
activeChildInterrupts.delete(flatIndex);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
activeChildInterrupts.set(flatIndex, interrupt);
|
|
1338
|
+
if (interrupted) interrupt();
|
|
1339
|
+
};
|
|
1340
|
+
const registerStepTimeout = (flatIndex: number, interrupt: (() => void) | undefined): void => {
|
|
1341
|
+
if (!interrupt) {
|
|
1342
|
+
activeChildTimeouts.delete(flatIndex);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
activeChildTimeouts.set(flatIndex, interrupt);
|
|
1346
|
+
if (timedOut) interrupt();
|
|
1347
|
+
};
|
|
1348
|
+
const interruptActiveChildren = (): void => {
|
|
1349
|
+
for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
|
|
1350
|
+
};
|
|
1351
|
+
const timeoutActiveChildren = (): void => {
|
|
1352
|
+
for (const interrupt of [...activeChildTimeouts.values()]) interrupt();
|
|
1353
|
+
};
|
|
1354
|
+
const nestedRuns = function* (children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
|
|
1355
|
+
for (const child of children ?? []) {
|
|
1356
|
+
yield child;
|
|
1357
|
+
yield* nestedRuns(child.children);
|
|
1358
|
+
yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
const interruptNestedAsyncDescendants = (): void => {
|
|
1362
|
+
if (!config.nestedRoute) return;
|
|
1363
|
+
let registry: ReturnType<typeof projectNestedEvents>;
|
|
1364
|
+
try {
|
|
1365
|
+
registry = projectNestedEvents(config.nestedRoute);
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1368
|
+
type: "subagent.nested.interrupt_failed",
|
|
1369
|
+
ts: Date.now(),
|
|
1370
|
+
runId: id,
|
|
1371
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1372
|
+
}));
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
for (const run of nestedRuns(registry.children)) {
|
|
1376
|
+
if (run.state !== "running" && run.state !== "queued") continue;
|
|
1377
|
+
const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
|
|
1378
|
+
if (!nestedAsyncDir) continue;
|
|
1379
|
+
try {
|
|
1380
|
+
deliverInterruptRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-interrupt" });
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1383
|
+
type: "subagent.nested.interrupt_failed",
|
|
1384
|
+
ts: Date.now(),
|
|
1385
|
+
runId: id,
|
|
1386
|
+
targetRunId: run.id,
|
|
1387
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1388
|
+
}));
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
const timeoutNestedAsyncDescendants = (): void => {
|
|
1393
|
+
if (!config.nestedRoute) return;
|
|
1394
|
+
let registry: ReturnType<typeof projectNestedEvents>;
|
|
1395
|
+
try {
|
|
1396
|
+
registry = projectNestedEvents(config.nestedRoute);
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1399
|
+
type: "subagent.nested.timeout_failed",
|
|
1400
|
+
ts: Date.now(),
|
|
1401
|
+
runId: id,
|
|
1402
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1403
|
+
}));
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
for (const run of nestedRuns(registry.children)) {
|
|
1407
|
+
if (run.state !== "running" && run.state !== "queued") continue;
|
|
1408
|
+
const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
|
|
1409
|
+
if (!nestedAsyncDir) continue;
|
|
1410
|
+
try {
|
|
1411
|
+
deliverTimeoutRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-timeout" });
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1414
|
+
type: "subagent.nested.timeout_failed",
|
|
1415
|
+
ts: Date.now(),
|
|
1416
|
+
runId: id,
|
|
1417
|
+
targetRunId: run.id,
|
|
1418
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1419
|
+
}));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
const pausedStepResult = (agent: string): SingleStepResult => ({
|
|
1424
|
+
agent,
|
|
1425
|
+
output: "Paused after interrupt. Waiting for explicit next action.",
|
|
1426
|
+
exitCode: 0,
|
|
1427
|
+
interrupted: true,
|
|
1428
|
+
});
|
|
1429
|
+
const timedOutStepResult = (agent: string): SingleStepResult => ({
|
|
1430
|
+
agent,
|
|
1431
|
+
output: timeoutMessage ?? "Subagent timed out.",
|
|
1432
|
+
error: timeoutMessage ?? "Subagent timed out.",
|
|
1433
|
+
exitCode: 1,
|
|
1434
|
+
timedOut: true,
|
|
1435
|
+
});
|
|
1219
1436
|
const consumePendingAppendRequests = (): void => {
|
|
1220
1437
|
if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
|
|
1221
1438
|
const requests = consumeChainAppendRequests(asyncDir);
|
|
@@ -1531,17 +1748,59 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1531
1748
|
ts: now,
|
|
1532
1749
|
runId: id,
|
|
1533
1750
|
}));
|
|
1534
|
-
|
|
1751
|
+
interruptNestedAsyncDescendants();
|
|
1752
|
+
interruptActiveChildren();
|
|
1753
|
+
};
|
|
1754
|
+
const timeoutRunner = () => {
|
|
1755
|
+
if (timedOut || interrupted || statusPayload.state !== "running") return;
|
|
1756
|
+
timedOut = true;
|
|
1757
|
+
const now = Date.now();
|
|
1758
|
+
const message = timeoutMessage ?? "Subagent timed out.";
|
|
1759
|
+
statusPayload.state = "failed";
|
|
1760
|
+
statusPayload.timedOut = true;
|
|
1761
|
+
statusPayload.error = message;
|
|
1762
|
+
currentActivityState = undefined;
|
|
1763
|
+
statusPayload.activityState = undefined;
|
|
1764
|
+
statusPayload.lastUpdate = now;
|
|
1765
|
+
for (const step of statusPayload.steps) {
|
|
1766
|
+
if (step.status !== "running" && step.status !== "pending") continue;
|
|
1767
|
+
step.status = "failed";
|
|
1768
|
+
step.error = message;
|
|
1769
|
+
step.exitCode = 1;
|
|
1770
|
+
step.timedOut = true;
|
|
1771
|
+
step.activityState = undefined;
|
|
1772
|
+
step.endedAt = now;
|
|
1773
|
+
step.durationMs = step.startedAt ? now - step.startedAt : 0;
|
|
1774
|
+
step.lastActivityAt = now;
|
|
1775
|
+
}
|
|
1776
|
+
writeStatusPayload();
|
|
1777
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1778
|
+
type: "subagent.run.timed_out",
|
|
1779
|
+
ts: now,
|
|
1780
|
+
runId: id,
|
|
1781
|
+
timeoutMs: config.timeoutMs,
|
|
1782
|
+
deadlineAt: config.deadlineAt,
|
|
1783
|
+
message,
|
|
1784
|
+
}));
|
|
1785
|
+
timeoutAbortController.abort();
|
|
1786
|
+
timeoutNestedAsyncDescendants();
|
|
1787
|
+
timeoutActiveChildren();
|
|
1535
1788
|
};
|
|
1536
1789
|
process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
|
|
1537
1790
|
// Portable control inbox: the parent drops an interrupt request file here when
|
|
1538
1791
|
// it cannot deliver the OS signal (e.g. ENOSYS on Windows). Routes into the
|
|
1539
1792
|
// same graceful interruptRunner() so stop/steer work on every platform.
|
|
1540
|
-
const disposeControlInbox = watchAsyncControlInbox(asyncDir, { onInterrupt: interruptRunner });
|
|
1793
|
+
const disposeControlInbox = watchAsyncControlInbox(asyncDir, { onInterrupt: interruptRunner, onTimeout: timeoutRunner });
|
|
1794
|
+
if (config.deadlineAt !== undefined) {
|
|
1795
|
+
const remainingMs = Math.max(0, config.deadlineAt - Date.now());
|
|
1796
|
+
timeoutTimer = setTimeout(timeoutRunner, remainingMs);
|
|
1797
|
+
timeoutTimer.unref?.();
|
|
1798
|
+
}
|
|
1541
1799
|
appendJsonl(
|
|
1542
1800
|
eventsPath,
|
|
1543
1801
|
JSON.stringify({
|
|
1544
1802
|
type: "subagent.run.started",
|
|
1803
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
1545
1804
|
ts: overallStartTime,
|
|
1546
1805
|
runId: id,
|
|
1547
1806
|
mode: statusPayload.mode,
|
|
@@ -1554,7 +1813,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1554
1813
|
let stepCursor = 0;
|
|
1555
1814
|
|
|
1556
1815
|
while (true) {
|
|
1557
|
-
if (interrupted) break;
|
|
1816
|
+
if (interrupted || timedOut) break;
|
|
1558
1817
|
consumePendingAppendRequests();
|
|
1559
1818
|
if (stepCursor >= steps.length) break;
|
|
1560
1819
|
const stepIndex = stepCursor++;
|
|
@@ -1606,7 +1865,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1606
1865
|
placeholder.durationMs = 0;
|
|
1607
1866
|
}
|
|
1608
1867
|
previousOutput = "Dynamic fanout produced 0 results.";
|
|
1609
|
-
const groupAcceptance = step.effectiveAcceptance?.explicit
|
|
1868
|
+
const groupAcceptance = step.effectiveAcceptance?.explicit && !timedOut
|
|
1610
1869
|
? await evaluateAcceptance({
|
|
1611
1870
|
acceptance: step.effectiveAcceptance,
|
|
1612
1871
|
output: "",
|
|
@@ -1615,38 +1874,55 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1615
1874
|
notes: "Dynamic fanout produced 0 results.",
|
|
1616
1875
|
}),
|
|
1617
1876
|
cwd,
|
|
1877
|
+
signal: timeoutAbortController.signal,
|
|
1878
|
+
abortMessage: timeoutMessage ?? "Subagent timed out.",
|
|
1618
1879
|
})
|
|
1619
1880
|
: undefined;
|
|
1620
|
-
|
|
1621
|
-
const
|
|
1622
|
-
if (
|
|
1881
|
+
const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
|
|
1882
|
+
const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
|
|
1883
|
+
if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
|
|
1884
|
+
const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
1885
|
+
if (groupTimedOut || groupAcceptanceFailure) {
|
|
1886
|
+
const errorMessage = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
|
|
1623
1887
|
statusPayload.state = "failed";
|
|
1624
|
-
statusPayload.error =
|
|
1888
|
+
statusPayload.error = errorMessage;
|
|
1625
1889
|
if (placeholder) {
|
|
1626
1890
|
placeholder.status = "failed";
|
|
1627
|
-
placeholder.error =
|
|
1891
|
+
placeholder.error = errorMessage;
|
|
1628
1892
|
placeholder.exitCode = 1;
|
|
1893
|
+
placeholder.timedOut = groupTimedOut ? true : undefined;
|
|
1629
1894
|
}
|
|
1630
|
-
markDynamicGraphGroup(stepIndex, "failed",
|
|
1631
|
-
statusPayload.lastUpdate = now;
|
|
1895
|
+
markDynamicGraphGroup(stepIndex, "failed", errorMessage, effectiveGroupAcceptance);
|
|
1896
|
+
statusPayload.lastUpdate = Date.now();
|
|
1632
1897
|
writeStatusPayload();
|
|
1633
|
-
results.push({ agent: step.parallel.agent, output:
|
|
1898
|
+
results.push({ agent: step.parallel.agent, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, acceptance: effectiveGroupAcceptance });
|
|
1634
1899
|
break;
|
|
1635
1900
|
}
|
|
1636
1901
|
flatIndex++;
|
|
1637
1902
|
statusPayload.lastUpdate = now;
|
|
1638
|
-
markDynamicGraphGroup(stepIndex, "completed", undefined,
|
|
1903
|
+
markDynamicGraphGroup(stepIndex, "completed", undefined, effectiveGroupAcceptance);
|
|
1639
1904
|
writeStatusPayload();
|
|
1640
1905
|
continue;
|
|
1641
1906
|
}
|
|
1642
1907
|
|
|
1643
|
-
const dynamicSteps = materialized.parallel.map((task, itemIndex) =>
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1908
|
+
const dynamicSteps = materialized.parallel.map((task, itemIndex) => {
|
|
1909
|
+
const thinkingOverride = step.thinkingOverrides?.[itemIndex];
|
|
1910
|
+
const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
|
|
1911
|
+
const thinking = thinkingOverride ? resolveEffectiveThinking(model, thinkingOverride) : undefined;
|
|
1912
|
+
return {
|
|
1913
|
+
...step.parallel,
|
|
1914
|
+
task: task.task ?? step.parallel.task,
|
|
1915
|
+
label: task.label ?? step.parallel.label,
|
|
1916
|
+
...(step.sessionFiles?.[itemIndex] ? { sessionFile: step.sessionFiles[itemIndex] } : {}),
|
|
1917
|
+
...(thinkingOverride ? {
|
|
1918
|
+
...(model ? { model } : {}),
|
|
1919
|
+
...(thinking ? { thinking } : {}),
|
|
1920
|
+
...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.map((candidate) => applyThinkingSuffix(candidate, thinkingOverride, true)) } : {}),
|
|
1921
|
+
} : {}),
|
|
1922
|
+
structuredOutput: undefined,
|
|
1923
|
+
structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
|
|
1924
|
+
};
|
|
1925
|
+
});
|
|
1650
1926
|
const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task) => ({
|
|
1651
1927
|
agent: task.agent,
|
|
1652
1928
|
phase: task.phase ?? step.phase,
|
|
@@ -1710,6 +1986,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1710
1986
|
let aborted = false;
|
|
1711
1987
|
const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
|
|
1712
1988
|
const fi = groupStartFlatIndex + taskIdx;
|
|
1989
|
+
if (timedOut) return timedOutStepResult(task.agent);
|
|
1990
|
+
if (interrupted) return pausedStepResult(task.agent);
|
|
1713
1991
|
if (aborted && failFast) {
|
|
1714
1992
|
const skippedAt = Date.now();
|
|
1715
1993
|
statusPayload.steps[fi].status = "failed";
|
|
@@ -1747,22 +2025,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1747
2025
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
1748
2026
|
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
1749
2027
|
nestedRoute: config.nestedRoute,
|
|
1750
|
-
registerInterrupt: (interrupt) =>
|
|
1751
|
-
|
|
1752
|
-
|
|
2028
|
+
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2029
|
+
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
2030
|
+
timeoutSignal: timeoutAbortController.signal,
|
|
2031
|
+
timeoutMessage,
|
|
1753
2032
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
1754
2033
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2034
|
+
skipAcceptance: () => timedOut,
|
|
1755
2035
|
});
|
|
1756
2036
|
const taskEndTime = Date.now();
|
|
1757
|
-
|
|
2037
|
+
const childInterrupted = singleResult.interrupted === true;
|
|
2038
|
+
statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
1758
2039
|
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
1759
2040
|
statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
|
|
1760
|
-
statusPayload.steps[fi].exitCode = singleResult.exitCode;
|
|
2041
|
+
statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2042
|
+
statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
1761
2043
|
statusPayload.steps[fi].model = singleResult.model;
|
|
1762
2044
|
statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
|
|
1763
2045
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
1764
2046
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
1765
|
-
statusPayload.steps[fi].
|
|
2047
|
+
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2048
|
+
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
1766
2049
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
1767
2050
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
1768
2051
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -1770,13 +2053,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1770
2053
|
statusPayload.lastUpdate = taskEndTime;
|
|
1771
2054
|
writeStatusPayload();
|
|
1772
2055
|
appendJsonl(eventsPath, JSON.stringify({
|
|
1773
|
-
type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2056
|
+
type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
1774
2057
|
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
1775
|
-
exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
|
|
2058
|
+
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
|
|
1776
2059
|
}));
|
|
1777
2060
|
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
1778
|
-
return { ...singleResult, skipped: false };
|
|
1779
|
-
});
|
|
2061
|
+
return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
|
|
2062
|
+
}, globalSemaphore);
|
|
1780
2063
|
|
|
1781
2064
|
flatIndex += dynamicSteps.length;
|
|
1782
2065
|
for (const pr of parallelResults) {
|
|
@@ -1784,14 +2067,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1784
2067
|
agent: pr.agent,
|
|
1785
2068
|
output: pr.output,
|
|
1786
2069
|
error: pr.error,
|
|
1787
|
-
success: pr.exitCode === 0,
|
|
1788
|
-
exitCode: pr.exitCode,
|
|
2070
|
+
success: pr.interrupted !== true && pr.exitCode === 0,
|
|
2071
|
+
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
1789
2072
|
skipped: pr.skipped,
|
|
2073
|
+
interrupted: pr.interrupted,
|
|
2074
|
+
timedOut: pr.timedOut,
|
|
1790
2075
|
sessionFile: pr.sessionFile,
|
|
1791
2076
|
intercomTarget: pr.intercomTarget,
|
|
1792
2077
|
model: pr.model,
|
|
1793
2078
|
attemptedModels: pr.attemptedModels,
|
|
1794
2079
|
modelAttempts: pr.modelAttempts,
|
|
2080
|
+
totalCost: pr.totalCost,
|
|
1795
2081
|
artifactPaths: pr.artifactPaths,
|
|
1796
2082
|
structuredOutput: pr.structuredOutput,
|
|
1797
2083
|
structuredOutputPath: pr.structuredOutputPath,
|
|
@@ -1811,7 +2097,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1811
2097
|
stepIndex,
|
|
1812
2098
|
};
|
|
1813
2099
|
statusPayload.outputs = outputs;
|
|
1814
|
-
const groupAcceptance = step.effectiveAcceptance
|
|
2100
|
+
const groupAcceptance = step.effectiveAcceptance && !timedOut
|
|
1815
2101
|
? await evaluateAcceptance({
|
|
1816
2102
|
acceptance: step.effectiveAcceptance,
|
|
1817
2103
|
output: "",
|
|
@@ -1820,21 +2106,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1820
2106
|
notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
|
|
1821
2107
|
}),
|
|
1822
2108
|
cwd,
|
|
2109
|
+
signal: timeoutAbortController.signal,
|
|
2110
|
+
abortMessage: timeoutMessage ?? "Subagent timed out.",
|
|
1823
2111
|
})
|
|
1824
2112
|
: undefined;
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
|
|
2113
|
+
const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
|
|
2114
|
+
const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
|
|
2115
|
+
const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
|
|
2116
|
+
const groupError = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
|
|
2117
|
+
markDynamicGraphGroup(stepIndex, groupError ? "failed" : "completed", groupError, effectiveGroupAcceptance);
|
|
2118
|
+
if (groupError) {
|
|
1828
2119
|
results.push({
|
|
1829
2120
|
agent: step.parallel.agent,
|
|
1830
|
-
output:
|
|
1831
|
-
error:
|
|
2121
|
+
output: groupError,
|
|
2122
|
+
error: groupError,
|
|
1832
2123
|
success: false,
|
|
1833
2124
|
exitCode: 1,
|
|
2125
|
+
timedOut: groupTimedOut ? true : undefined,
|
|
1834
2126
|
structuredOutput: collection,
|
|
1835
|
-
acceptance:
|
|
2127
|
+
acceptance: effectiveGroupAcceptance,
|
|
1836
2128
|
});
|
|
1837
|
-
statusPayload.error =
|
|
2129
|
+
statusPayload.error = groupError;
|
|
1838
2130
|
}
|
|
1839
2131
|
} catch (error) {
|
|
1840
2132
|
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
@@ -1900,6 +2192,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1900
2192
|
setupHook: config.worktreeSetupHook
|
|
1901
2193
|
? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
|
|
1902
2194
|
: undefined,
|
|
2195
|
+
baseDir: config.worktreeBaseDir,
|
|
1903
2196
|
});
|
|
1904
2197
|
} catch (error) {
|
|
1905
2198
|
const setupError = error instanceof Error ? error.message : String(error);
|
|
@@ -1941,6 +2234,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1941
2234
|
concurrency,
|
|
1942
2235
|
async (task, taskIdx) => {
|
|
1943
2236
|
const fi = groupStartFlatIndex + taskIdx;
|
|
2237
|
+
if (timedOut) return timedOutStepResult(task.agent);
|
|
2238
|
+
if (interrupted) return pausedStepResult(task.agent);
|
|
1944
2239
|
if (aborted && failFast) {
|
|
1945
2240
|
const skippedAt = Date.now();
|
|
1946
2241
|
statusPayload.steps[fi].status = "failed";
|
|
@@ -1994,11 +2289,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1994
2289
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
1995
2290
|
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
1996
2291
|
nestedRoute: config.nestedRoute,
|
|
1997
|
-
registerInterrupt: (interrupt) =>
|
|
1998
|
-
|
|
1999
|
-
|
|
2292
|
+
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2293
|
+
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
2294
|
+
timeoutSignal: timeoutAbortController.signal,
|
|
2295
|
+
timeoutMessage,
|
|
2000
2296
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
2001
2297
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2298
|
+
skipAcceptance: () => timedOut,
|
|
2002
2299
|
});
|
|
2003
2300
|
if (task.sessionFile) {
|
|
2004
2301
|
latestSessionFile = task.sessionFile;
|
|
@@ -2006,16 +2303,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2006
2303
|
|
|
2007
2304
|
const taskEndTime = Date.now();
|
|
2008
2305
|
const taskDuration = taskEndTime - taskStartTime;
|
|
2306
|
+
const childInterrupted = singleResult.interrupted === true;
|
|
2009
2307
|
|
|
2010
|
-
statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2308
|
+
statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2011
2309
|
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
2012
2310
|
statusPayload.steps[fi].durationMs = taskDuration;
|
|
2013
|
-
statusPayload.steps[fi].exitCode = singleResult.exitCode;
|
|
2311
|
+
statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2312
|
+
statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
2014
2313
|
statusPayload.steps[fi].model = singleResult.model;
|
|
2015
2314
|
statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
|
|
2016
2315
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
2017
2316
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
2018
|
-
statusPayload.steps[fi].
|
|
2317
|
+
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2318
|
+
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2019
2319
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
2020
2320
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2021
2321
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -2024,9 +2324,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2024
2324
|
writeStatusPayload();
|
|
2025
2325
|
|
|
2026
2326
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2027
|
-
type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2327
|
+
type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2028
2328
|
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
2029
|
-
exitCode: singleResult.exitCode, durationMs: taskDuration,
|
|
2329
|
+
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
|
|
2030
2330
|
}));
|
|
2031
2331
|
if (singleResult.completionGuardTriggered) {
|
|
2032
2332
|
const event = buildControlEvent({
|
|
@@ -2043,8 +2343,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2043
2343
|
}
|
|
2044
2344
|
|
|
2045
2345
|
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
2046
|
-
return { ...singleResult, skipped: false };
|
|
2346
|
+
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
2347
|
},
|
|
2348
|
+
globalSemaphore,
|
|
2048
2349
|
);
|
|
2049
2350
|
|
|
2050
2351
|
flatIndex += group.parallel.length;
|
|
@@ -2072,14 +2373,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2072
2373
|
agent: pr.agent,
|
|
2073
2374
|
output: pr.output,
|
|
2074
2375
|
error: pr.error,
|
|
2075
|
-
success: pr.exitCode === 0,
|
|
2076
|
-
exitCode: pr.exitCode,
|
|
2376
|
+
success: pr.interrupted !== true && pr.exitCode === 0,
|
|
2377
|
+
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
2077
2378
|
skipped: pr.skipped,
|
|
2379
|
+
interrupted: pr.interrupted,
|
|
2380
|
+
timedOut: pr.timedOut,
|
|
2078
2381
|
sessionFile: pr.sessionFile,
|
|
2079
2382
|
intercomTarget: pr.intercomTarget,
|
|
2080
2383
|
model: pr.model,
|
|
2081
2384
|
attemptedModels: pr.attemptedModels,
|
|
2082
2385
|
modelAttempts: pr.modelAttempts,
|
|
2386
|
+
totalCost: pr.totalCost,
|
|
2083
2387
|
artifactPaths: pr.artifactPaths,
|
|
2084
2388
|
structuredOutput: pr.structuredOutput,
|
|
2085
2389
|
structuredOutputPath: pr.structuredOutputPath,
|
|
@@ -2159,11 +2463,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2159
2463
|
childIntercomTarget: config.childIntercomTargets?.[flatIndex],
|
|
2160
2464
|
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
2161
2465
|
nestedRoute: config.nestedRoute,
|
|
2162
|
-
registerInterrupt: (interrupt) =>
|
|
2163
|
-
|
|
2164
|
-
|
|
2466
|
+
registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
|
|
2467
|
+
registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
|
|
2468
|
+
timeoutSignal: timeoutAbortController.signal,
|
|
2469
|
+
timeoutMessage,
|
|
2165
2470
|
onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
|
|
2166
2471
|
onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
|
|
2472
|
+
skipAcceptance: () => timedOut,
|
|
2167
2473
|
});
|
|
2168
2474
|
if (seqStep.sessionFile) {
|
|
2169
2475
|
latestSessionFile = seqStep.sessionFile;
|
|
@@ -2172,20 +2478,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2172
2478
|
previousOutput = singleResult.output;
|
|
2173
2479
|
results.push({
|
|
2174
2480
|
agent: singleResult.agent,
|
|
2175
|
-
output: singleResult.output,
|
|
2176
|
-
error: singleResult.error,
|
|
2177
|
-
success: singleResult.exitCode === 0,
|
|
2178
|
-
exitCode: singleResult.exitCode,
|
|
2481
|
+
output: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
|
|
2482
|
+
error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
|
|
2483
|
+
success: !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
|
|
2484
|
+
exitCode: timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
|
|
2179
2485
|
sessionFile: singleResult.sessionFile,
|
|
2180
2486
|
intercomTarget: singleResult.intercomTarget,
|
|
2181
2487
|
model: singleResult.model,
|
|
2182
2488
|
attemptedModels: singleResult.attemptedModels,
|
|
2183
2489
|
modelAttempts: singleResult.modelAttempts,
|
|
2490
|
+
totalCost: singleResult.totalCost,
|
|
2184
2491
|
artifactPaths: singleResult.artifactPaths,
|
|
2185
2492
|
structuredOutput: singleResult.structuredOutput,
|
|
2186
2493
|
structuredOutputPath: singleResult.structuredOutputPath,
|
|
2187
2494
|
structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
|
|
2188
2495
|
acceptance: singleResult.acceptance,
|
|
2496
|
+
interrupted: singleResult.interrupted,
|
|
2497
|
+
timedOut: timedOut || singleResult.timedOut ? true : undefined,
|
|
2189
2498
|
});
|
|
2190
2499
|
if (seqStep.outputName) {
|
|
2191
2500
|
outputs[seqStep.outputName] = outputEntryFromAsyncResult({
|
|
@@ -2218,15 +2527,18 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2218
2527
|
}
|
|
2219
2528
|
|
|
2220
2529
|
const stepEndTime = Date.now();
|
|
2221
|
-
|
|
2530
|
+
const childInterrupted = singleResult.interrupted === true;
|
|
2531
|
+
statusPayload.steps[flatIndex].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2222
2532
|
statusPayload.steps[flatIndex].endedAt = stepEndTime;
|
|
2223
2533
|
statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
|
|
2224
|
-
statusPayload.steps[flatIndex].exitCode = singleResult.exitCode;
|
|
2534
|
+
statusPayload.steps[flatIndex].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2535
|
+
statusPayload.steps[flatIndex].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
2225
2536
|
statusPayload.steps[flatIndex].model = singleResult.model;
|
|
2226
2537
|
statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
|
|
2227
2538
|
statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
|
|
2228
2539
|
statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
|
|
2229
|
-
statusPayload.steps[flatIndex].
|
|
2540
|
+
statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
|
|
2541
|
+
statusPayload.steps[flatIndex].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2230
2542
|
statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
|
|
2231
2543
|
statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2232
2544
|
statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -2239,12 +2551,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2239
2551
|
writeStatusPayload();
|
|
2240
2552
|
|
|
2241
2553
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2242
|
-
type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2554
|
+
type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
|
|
2243
2555
|
ts: stepEndTime,
|
|
2244
2556
|
runId: id,
|
|
2245
2557
|
stepIndex: flatIndex,
|
|
2246
2558
|
agent: seqStep.agent,
|
|
2247
|
-
exitCode: singleResult.exitCode,
|
|
2559
|
+
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
|
|
2248
2560
|
durationMs: stepEndTime - stepStartTime,
|
|
2249
2561
|
tokens: stepTokens,
|
|
2250
2562
|
}));
|
|
@@ -2283,6 +2595,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2283
2595
|
}
|
|
2284
2596
|
|
|
2285
2597
|
const resultMode = config.resultMode ?? statusPayload.mode;
|
|
2598
|
+
const totalCost = results.reduce<CostSummary>((sum, result) => ({
|
|
2599
|
+
inputTokens: sum.inputTokens + (result.totalCost?.inputTokens ?? 0),
|
|
2600
|
+
outputTokens: sum.outputTokens + (result.totalCost?.outputTokens ?? 0),
|
|
2601
|
+
costUsd: sum.costUsd + (result.totalCost?.costUsd ?? 0),
|
|
2602
|
+
}), { inputTokens: 0, outputTokens: 0, costUsd: 0 });
|
|
2603
|
+
const finalTotalCost = totalCost.inputTokens > 0 || totalCost.outputTokens > 0 || totalCost.costUsd > 0 ? totalCost : undefined;
|
|
2286
2604
|
const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
|
|
2287
2605
|
const agentName = finalFlatAgents.length === 1
|
|
2288
2606
|
? finalFlatAgents[0]!
|
|
@@ -2323,14 +2641,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2323
2641
|
clearInterval(activityTimer);
|
|
2324
2642
|
activityTimer = undefined;
|
|
2325
2643
|
}
|
|
2644
|
+
if (timeoutTimer) {
|
|
2645
|
+
clearTimeout(timeoutTimer);
|
|
2646
|
+
timeoutTimer = undefined;
|
|
2647
|
+
}
|
|
2326
2648
|
disposeControlInbox();
|
|
2327
2649
|
const effectiveSessionFile = sessionFile ?? latestSessionFile;
|
|
2328
2650
|
const runEndedAt = Date.now();
|
|
2329
|
-
statusPayload.state = interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
|
|
2651
|
+
statusPayload.state = timedOut ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
|
|
2330
2652
|
statusPayload.activityState = undefined;
|
|
2653
|
+
if (timedOut) {
|
|
2654
|
+
statusPayload.timedOut = true;
|
|
2655
|
+
statusPayload.error = timeoutMessage ?? "Subagent timed out.";
|
|
2656
|
+
}
|
|
2331
2657
|
statusPayload.endedAt = runEndedAt;
|
|
2332
2658
|
statusPayload.lastUpdate = runEndedAt;
|
|
2333
2659
|
statusPayload.sessionFile = effectiveSessionFile;
|
|
2660
|
+
statusPayload.totalCost = finalTotalCost;
|
|
2334
2661
|
statusPayload.shareUrl = shareUrl;
|
|
2335
2662
|
statusPayload.gistUrl = gistUrl;
|
|
2336
2663
|
statusPayload.shareError = shareError;
|
|
@@ -2345,10 +2672,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2345
2672
|
eventsPath,
|
|
2346
2673
|
JSON.stringify({
|
|
2347
2674
|
type: "subagent.run.completed",
|
|
2675
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
2348
2676
|
ts: runEndedAt,
|
|
2349
2677
|
runId: id,
|
|
2350
2678
|
status: statusPayload.state,
|
|
2351
2679
|
durationMs: runEndedAt - overallStartTime,
|
|
2680
|
+
totalTokens: statusPayload.totalTokens,
|
|
2681
|
+
totalCost: finalTotalCost,
|
|
2352
2682
|
}),
|
|
2353
2683
|
);
|
|
2354
2684
|
writeRunLog(logPath, {
|
|
@@ -2372,23 +2702,30 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2372
2702
|
|
|
2373
2703
|
try {
|
|
2374
2704
|
writeAtomicJson(resultPath, {
|
|
2705
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
2375
2706
|
id,
|
|
2376
2707
|
agent: agentName,
|
|
2377
2708
|
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,
|
|
2709
|
+
success: !timedOut && !interrupted && results.every((r) => r.success),
|
|
2710
|
+
state: timedOut ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
|
|
2711
|
+
summary: timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
|
|
2712
|
+
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
2713
|
+
...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
|
|
2714
|
+
...(timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : {}),
|
|
2381
2715
|
results: results.map((r) => ({
|
|
2382
2716
|
agent: r.agent,
|
|
2383
2717
|
output: r.output,
|
|
2384
2718
|
error: r.error,
|
|
2385
2719
|
success: r.success,
|
|
2386
2720
|
skipped: r.skipped || undefined,
|
|
2721
|
+
interrupted: r.interrupted || undefined,
|
|
2722
|
+
timedOut: r.timedOut || undefined,
|
|
2387
2723
|
sessionFile: r.sessionFile,
|
|
2388
2724
|
intercomTarget: r.intercomTarget,
|
|
2389
2725
|
model: r.model,
|
|
2390
2726
|
attemptedModels: r.attemptedModels,
|
|
2391
2727
|
modelAttempts: r.modelAttempts,
|
|
2728
|
+
totalCost: r.totalCost,
|
|
2392
2729
|
artifactPaths: r.artifactPaths,
|
|
2393
2730
|
truncated: r.truncated,
|
|
2394
2731
|
structuredOutput: r.structuredOutput,
|
|
@@ -2398,9 +2735,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2398
2735
|
})),
|
|
2399
2736
|
outputs,
|
|
2400
2737
|
workflowGraph: statusPayload.workflowGraph,
|
|
2401
|
-
exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
2738
|
+
exitCode: timedOut ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
2402
2739
|
timestamp: runEndedAt,
|
|
2403
2740
|
durationMs: runEndedAt - overallStartTime,
|
|
2741
|
+
totalTokens: statusPayload.totalTokens,
|
|
2742
|
+
totalCost: finalTotalCost,
|
|
2404
2743
|
truncated,
|
|
2405
2744
|
artifactsDir,
|
|
2406
2745
|
cwd,
|