pi-subagents 0.31.0 → 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 +48 -0
- package/README.md +170 -8
- 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 +69 -4
- package/src/extension/schemas.ts +2 -2
- package/src/intercom/intercom-bridge.ts +25 -1
- package/src/profiles/profiles.ts +637 -0
- package/src/runs/background/async-execution.ts +138 -33
- package/src/runs/background/async-job-tracker.ts +77 -1
- package/src/runs/background/async-resume.ts +11 -13
- 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 +227 -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 +459 -113
- package/src/runs/foreground/chain-execution.ts +29 -7
- package/src/runs/foreground/execution.ts +24 -6
- package/src/runs/foreground/subagent-executor.ts +240 -44
- 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 +3 -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 +82 -3
- package/src/shared/utils.ts +99 -14
- package/src/slash/slash-commands.ts +726 -40
- package/src/tui/render.ts +16 -4
|
@@ -4,6 +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, deliverInterruptRequest, deliverTimeoutRequest, watchAsyncControlInbox } from "./control-channel.ts";
|
|
7
8
|
import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
8
9
|
import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
|
|
9
10
|
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
@@ -14,14 +15,17 @@ import {
|
|
|
14
15
|
type AsyncParallelGroupStatus,
|
|
15
16
|
type AsyncStatus,
|
|
16
17
|
type ChainOutputMap,
|
|
18
|
+
type CostSummary,
|
|
17
19
|
type ModelAttempt,
|
|
18
20
|
type NestedRouteInfo,
|
|
21
|
+
type NestedRunSummary,
|
|
19
22
|
type ResolvedControlConfig,
|
|
20
23
|
type SubagentRunMode,
|
|
21
24
|
type Usage,
|
|
22
25
|
type WorkflowGraphSnapshot,
|
|
23
26
|
DEFAULT_MAX_OUTPUT,
|
|
24
27
|
type MaxOutputConfig,
|
|
28
|
+
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
25
29
|
truncateOutput,
|
|
26
30
|
getSubagentDepthEnv,
|
|
27
31
|
} from "../../shared/types.ts";
|
|
@@ -42,15 +46,17 @@ import {
|
|
|
42
46
|
mapConcurrent,
|
|
43
47
|
aggregateParallelOutputs,
|
|
44
48
|
MAX_PARALLEL_CONCURRENCY,
|
|
49
|
+
DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
|
|
50
|
+
Semaphore,
|
|
45
51
|
} from "../shared/parallel-utils.ts";
|
|
46
|
-
import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
52
|
+
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
47
53
|
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
48
54
|
import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
|
|
49
55
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
50
|
-
import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
|
|
56
|
+
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
51
57
|
import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
52
58
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
53
|
-
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
|
|
59
|
+
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
|
|
54
60
|
import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
55
61
|
import {
|
|
56
62
|
createMutatingFailureState,
|
|
@@ -100,6 +106,7 @@ interface SubagentRunConfig {
|
|
|
100
106
|
piArgv1?: string;
|
|
101
107
|
worktreeSetupHook?: string;
|
|
102
108
|
worktreeSetupHookTimeoutMs?: number;
|
|
109
|
+
worktreeBaseDir?: string;
|
|
103
110
|
controlConfig?: ResolvedControlConfig;
|
|
104
111
|
controlIntercomTarget?: string;
|
|
105
112
|
childIntercomTargets?: Array<string | undefined>;
|
|
@@ -108,6 +115,10 @@ interface SubagentRunConfig {
|
|
|
108
115
|
workflowGraph?: WorkflowGraphSnapshot;
|
|
109
116
|
nestedRoute?: NestedRouteInfo;
|
|
110
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;
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
interface StepResult {
|
|
@@ -117,11 +128,14 @@ interface StepResult {
|
|
|
117
128
|
success: boolean;
|
|
118
129
|
exitCode?: number | null;
|
|
119
130
|
skipped?: boolean;
|
|
131
|
+
interrupted?: boolean;
|
|
132
|
+
timedOut?: boolean;
|
|
120
133
|
sessionFile?: string;
|
|
121
134
|
intercomTarget?: string;
|
|
122
135
|
model?: string;
|
|
123
136
|
attemptedModels?: string[];
|
|
124
137
|
modelAttempts?: ModelAttempt[];
|
|
138
|
+
totalCost?: CostSummary;
|
|
125
139
|
artifactPaths?: ArtifactPaths;
|
|
126
140
|
truncated?: boolean;
|
|
127
141
|
structuredOutput?: unknown;
|
|
@@ -236,6 +250,21 @@ function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsag
|
|
|
236
250
|
return total > 0 ? { input, output, total } : null;
|
|
237
251
|
}
|
|
238
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
|
+
|
|
239
268
|
function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
|
|
240
269
|
const nonEmpty = lines.filter((line) => line.trim());
|
|
241
270
|
if (nonEmpty.length === 0) return;
|
|
@@ -294,6 +323,7 @@ interface RunPiStreamingResult {
|
|
|
294
323
|
error?: string;
|
|
295
324
|
finalOutput: string;
|
|
296
325
|
interrupted?: boolean;
|
|
326
|
+
timedOut?: boolean;
|
|
297
327
|
observedMutationAttempt?: boolean;
|
|
298
328
|
}
|
|
299
329
|
|
|
@@ -308,6 +338,8 @@ function runPiStreaming(
|
|
|
308
338
|
childEventContext?: ChildEventContext,
|
|
309
339
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void,
|
|
310
340
|
onChildEvent?: (event: ChildEvent) => void,
|
|
341
|
+
registerTimeout?: (interrupt: (() => void) | undefined) => void,
|
|
342
|
+
timeoutMessage?: string,
|
|
311
343
|
): Promise<RunPiStreamingResult> {
|
|
312
344
|
return new Promise((resolve) => {
|
|
313
345
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
@@ -331,6 +363,7 @@ function runPiStreaming(
|
|
|
331
363
|
let error: string | undefined;
|
|
332
364
|
let assistantError: string | undefined;
|
|
333
365
|
let interrupted = false;
|
|
366
|
+
let timedOut = false;
|
|
334
367
|
let observedMutationAttempt = false;
|
|
335
368
|
const rawStdoutLines: string[] = [];
|
|
336
369
|
|
|
@@ -429,11 +462,13 @@ function runPiStreaming(
|
|
|
429
462
|
// a lingering stdio holder after `exit`, or a child that never exits.
|
|
430
463
|
const FINAL_STOP_GRACE_MS = 1000;
|
|
431
464
|
const HARD_KILL_MS = 3000;
|
|
465
|
+
const TIMEOUT_HARD_KILL_MS = 3000;
|
|
432
466
|
let childExited = false;
|
|
433
467
|
let forcedTerminationSignal = false;
|
|
434
468
|
let cleanTerminalAssistantStopReceived = false;
|
|
435
469
|
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
436
470
|
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
471
|
+
let timeoutHardKillTimer: NodeJS.Timeout | undefined;
|
|
437
472
|
let settled = false;
|
|
438
473
|
const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
|
|
439
474
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
@@ -448,14 +483,25 @@ function runPiStreaming(
|
|
|
448
483
|
processStderrText(chunk.toString());
|
|
449
484
|
});
|
|
450
485
|
registerInterrupt?.(() => {
|
|
451
|
-
if (settled) return;
|
|
486
|
+
if (settled || timedOut) return;
|
|
452
487
|
interrupted = true;
|
|
453
488
|
if (!error) error = "Interrupted. Waiting for explicit next action.";
|
|
454
489
|
trySignalChild(child, "SIGINT");
|
|
455
490
|
setTimeout(() => {
|
|
456
|
-
if (!settled) trySignalChild(child, "SIGTERM");
|
|
491
|
+
if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
|
|
457
492
|
}, 1000).unref?.();
|
|
458
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
|
+
});
|
|
459
505
|
const clearDrainTimers = () => {
|
|
460
506
|
if (finalDrainTimer) {
|
|
461
507
|
clearTimeout(finalDrainTimer);
|
|
@@ -465,6 +511,10 @@ function runPiStreaming(
|
|
|
465
511
|
clearTimeout(finalHardKillTimer);
|
|
466
512
|
finalHardKillTimer = undefined;
|
|
467
513
|
}
|
|
514
|
+
if (timeoutHardKillTimer) {
|
|
515
|
+
clearTimeout(timeoutHardKillTimer);
|
|
516
|
+
timeoutHardKillTimer = undefined;
|
|
517
|
+
}
|
|
468
518
|
};
|
|
469
519
|
function startFinalDrain(): void {
|
|
470
520
|
if (childExited || finalDrainTimer || settled) return;
|
|
@@ -491,6 +541,7 @@ function runPiStreaming(
|
|
|
491
541
|
child.on("close", (exitCode, signal) => {
|
|
492
542
|
settled = true;
|
|
493
543
|
registerInterrupt?.(undefined);
|
|
544
|
+
registerTimeout?.(undefined);
|
|
494
545
|
clearDrainTimers();
|
|
495
546
|
clearStdioGuard();
|
|
496
547
|
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
@@ -501,13 +552,14 @@ function runPiStreaming(
|
|
|
501
552
|
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
|
|
502
553
|
resolve({
|
|
503
554
|
stderr,
|
|
504
|
-
exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
555
|
+
exitCode: timedOut ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
505
556
|
messages,
|
|
506
557
|
usage,
|
|
507
558
|
model,
|
|
508
|
-
error: interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
509
|
-
finalOutput,
|
|
559
|
+
error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
560
|
+
finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput,
|
|
510
561
|
interrupted,
|
|
562
|
+
timedOut,
|
|
511
563
|
observedMutationAttempt,
|
|
512
564
|
});
|
|
513
565
|
});
|
|
@@ -515,12 +567,13 @@ function runPiStreaming(
|
|
|
515
567
|
child.on("error", (spawnError) => {
|
|
516
568
|
settled = true;
|
|
517
569
|
registerInterrupt?.(undefined);
|
|
570
|
+
registerTimeout?.(undefined);
|
|
518
571
|
clearDrainTimers();
|
|
519
572
|
clearStdioGuard();
|
|
520
573
|
outputStream.end();
|
|
521
574
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
522
575
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
523
|
-
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 });
|
|
524
577
|
});
|
|
525
578
|
});
|
|
526
579
|
}
|
|
@@ -648,11 +701,15 @@ interface SingleStepContext {
|
|
|
648
701
|
piPackageRoot?: string;
|
|
649
702
|
piArgv1?: string;
|
|
650
703
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void;
|
|
704
|
+
registerTimeout?: (interrupt: (() => void) | undefined) => void;
|
|
705
|
+
timeoutSignal?: AbortSignal;
|
|
706
|
+
timeoutMessage?: string;
|
|
651
707
|
childIntercomTarget?: string;
|
|
652
708
|
orchestratorIntercomTarget?: string;
|
|
653
709
|
nestedRoute?: NestedRouteInfo;
|
|
654
710
|
onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
|
|
655
711
|
onChildEvent?: (event: ChildEvent) => void;
|
|
712
|
+
skipAcceptance?: () => boolean;
|
|
656
713
|
}
|
|
657
714
|
|
|
658
715
|
/** Run a single pi agent step, returning output and metadata */
|
|
@@ -669,6 +726,7 @@ async function runSingleStep(
|
|
|
669
726
|
modelAttempts?: ModelAttempt[];
|
|
670
727
|
artifactPaths?: ArtifactPaths;
|
|
671
728
|
interrupted?: boolean;
|
|
729
|
+
timedOut?: boolean;
|
|
672
730
|
sessionFile?: string;
|
|
673
731
|
intercomTarget?: string;
|
|
674
732
|
completionGuardTriggered?: boolean;
|
|
@@ -678,27 +736,52 @@ async function runSingleStep(
|
|
|
678
736
|
acceptance?: import("../../shared/types.ts").AcceptanceLedger;
|
|
679
737
|
}> {
|
|
680
738
|
if (step.importAsyncRoot) {
|
|
681
|
-
|
|
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
|
+
});
|
|
682
754
|
try {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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);
|
|
686
784
|
}
|
|
687
|
-
return {
|
|
688
|
-
agent: imported.agent,
|
|
689
|
-
output: imported.output,
|
|
690
|
-
exitCode: imported.exitCode,
|
|
691
|
-
error: imported.error,
|
|
692
|
-
sessionFile: imported.sessionFile,
|
|
693
|
-
intercomTarget: imported.intercomTarget,
|
|
694
|
-
model: imported.model,
|
|
695
|
-
attemptedModels: imported.attemptedModels,
|
|
696
|
-
modelAttempts: imported.modelAttempts,
|
|
697
|
-
structuredOutput: imported.structuredOutput,
|
|
698
|
-
structuredOutputPath: imported.structuredOutputPath,
|
|
699
|
-
structuredOutputSchemaPath: imported.structuredOutputSchemaPath,
|
|
700
|
-
acceptance: imported.acceptance,
|
|
701
|
-
};
|
|
702
785
|
}
|
|
703
786
|
|
|
704
787
|
const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
|
|
@@ -739,6 +822,7 @@ async function runSingleStep(
|
|
|
739
822
|
let completionGuardTriggeredFinal = false;
|
|
740
823
|
|
|
741
824
|
for (let index = 0; index < candidates.length; index++) {
|
|
825
|
+
if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
742
826
|
const candidate = candidates[index];
|
|
743
827
|
ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
|
|
744
828
|
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
@@ -790,13 +874,21 @@ async function runSingleStep(
|
|
|
790
874
|
{ eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
791
875
|
ctx.registerInterrupt,
|
|
792
876
|
ctx.onChildEvent,
|
|
877
|
+
ctx.registerTimeout,
|
|
878
|
+
ctx.timeoutMessage,
|
|
793
879
|
);
|
|
794
880
|
cleanupTempDir(tempDir);
|
|
795
881
|
|
|
796
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;
|
|
797
889
|
let structuredOutput: unknown;
|
|
798
890
|
let structuredError: string | undefined;
|
|
799
|
-
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError) {
|
|
891
|
+
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError) {
|
|
800
892
|
const structured = readStructuredOutput({
|
|
801
893
|
schema: effectiveStructuredOutput.schema,
|
|
802
894
|
schemaPath: effectiveStructuredOutput.schemaPath,
|
|
@@ -805,7 +897,7 @@ async function runSingleStep(
|
|
|
805
897
|
if (structured.error) structuredError = structured.error;
|
|
806
898
|
else structuredOutput = structured.value;
|
|
807
899
|
}
|
|
808
|
-
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
|
|
809
901
|
? evaluateCompletionMutationGuard({
|
|
810
902
|
agent: step.agent,
|
|
811
903
|
task: taskForCompletionGuard,
|
|
@@ -824,16 +916,18 @@ async function runSingleStep(
|
|
|
824
916
|
? 1
|
|
825
917
|
: hiddenError?.hasError
|
|
826
918
|
? (hiddenError.exitCode ?? 1)
|
|
827
|
-
:
|
|
919
|
+
: emptyOutputError
|
|
828
920
|
? 1
|
|
829
|
-
: run.exitCode
|
|
921
|
+
: run.error && run.exitCode === 0
|
|
922
|
+
? 1
|
|
923
|
+
: run.exitCode;
|
|
830
924
|
const error = completionGuardError
|
|
831
925
|
?? structuredError
|
|
832
926
|
?? (hiddenError?.hasError
|
|
833
927
|
? hiddenError.details
|
|
834
928
|
? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
|
|
835
929
|
: `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
|
|
836
|
-
: 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)));
|
|
837
931
|
const attempt: ModelAttempt = {
|
|
838
932
|
model: candidate ?? run.model ?? step.model ?? "default",
|
|
839
933
|
success: effectiveExitCode === 0 && !error,
|
|
@@ -846,6 +940,7 @@ async function runSingleStep(
|
|
|
846
940
|
completionGuardTriggeredFinal = completionGuardTriggered;
|
|
847
941
|
finalOutputSnapshot = outputSnapshot;
|
|
848
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;
|
|
849
944
|
if (attempt.success || completionGuardTriggered) break;
|
|
850
945
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
851
946
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
|
|
@@ -873,19 +968,25 @@ async function runSingleStep(
|
|
|
873
968
|
saveError: resolvedOutput.saveError,
|
|
874
969
|
});
|
|
875
970
|
outputForSummary = finalizedOutput.displayOutput;
|
|
876
|
-
const acceptance = step.effectiveAcceptance
|
|
971
|
+
const acceptance = step.effectiveAcceptance && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
|
|
877
972
|
? await evaluateAcceptance({
|
|
878
973
|
acceptance: step.effectiveAcceptance,
|
|
879
974
|
output: outputForAcceptance,
|
|
880
975
|
cwd: step.cwd ?? ctx.cwd,
|
|
976
|
+
signal: ctx.timeoutSignal,
|
|
977
|
+
abortMessage: ctx.timeoutMessage ?? "Subagent timed out.",
|
|
881
978
|
})
|
|
882
979
|
: undefined;
|
|
883
|
-
const
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
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;
|
|
889
990
|
|
|
890
991
|
if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
|
|
891
992
|
if (ctx.artifactConfig?.includeOutput !== false) {
|
|
@@ -920,13 +1021,15 @@ async function runSingleStep(
|
|
|
920
1021
|
model: finalResult?.model,
|
|
921
1022
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
922
1023
|
modelAttempts,
|
|
1024
|
+
totalCost: costSummaryFromAttempts(modelAttempts),
|
|
923
1025
|
artifactPaths,
|
|
924
|
-
interrupted: finalResult?.interrupted,
|
|
1026
|
+
interrupted: timedOutAfterAcceptance ? false : finalResult?.interrupted,
|
|
1027
|
+
timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
|
|
925
1028
|
completionGuardTriggered: completionGuardTriggeredFinal,
|
|
926
|
-
structuredOutput: (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
927
|
-
structuredOutputPath: effectiveStructuredOutput?.outputPath,
|
|
928
|
-
structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
|
|
929
|
-
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,
|
|
930
1033
|
};
|
|
931
1034
|
}
|
|
932
1035
|
|
|
@@ -1055,9 +1158,12 @@ function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { pa
|
|
|
1055
1158
|
writeInitialProgressFile(cwd);
|
|
1056
1159
|
}
|
|
1057
1160
|
|
|
1161
|
+
type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
|
|
1162
|
+
|
|
1058
1163
|
async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
1059
1164
|
const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
|
|
1060
1165
|
config;
|
|
1166
|
+
const globalSemaphore = new Semaphore(config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
1061
1167
|
let previousOutput = "";
|
|
1062
1168
|
const outputs: ChainOutputMap = {};
|
|
1063
1169
|
const results: StepResult[] = [];
|
|
@@ -1068,10 +1174,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1068
1174
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
1069
1175
|
const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
|
|
1070
1176
|
const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
1071
|
-
|
|
1177
|
+
const activeChildInterrupts = new Map<number, () => void>();
|
|
1178
|
+
const activeChildTimeouts = new Map<number, () => void>();
|
|
1072
1179
|
let interrupted = false;
|
|
1073
1180
|
let currentActivityState: ActivityState | undefined;
|
|
1074
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();
|
|
1075
1186
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
1076
1187
|
let latestSessionFile: string | undefined;
|
|
1077
1188
|
|
|
@@ -1137,6 +1248,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1137
1248
|
|| shareEnabled
|
|
1138
1249
|
|| flatSteps.some((step) => Boolean(step.sessionFile));
|
|
1139
1250
|
const statusPayload: RunnerStatusPayload = {
|
|
1251
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
1140
1252
|
runId: id,
|
|
1141
1253
|
...(config.sessionId ? { sessionId: config.sessionId } : {}),
|
|
1142
1254
|
mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
|
|
@@ -1144,6 +1256,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1144
1256
|
lastActivityAt: overallStartTime,
|
|
1145
1257
|
startedAt: overallStartTime,
|
|
1146
1258
|
lastUpdate: overallStartTime,
|
|
1259
|
+
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
1260
|
+
...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
|
|
1147
1261
|
pid: process.pid,
|
|
1148
1262
|
cwd,
|
|
1149
1263
|
currentStep: 0,
|
|
@@ -1215,6 +1329,110 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1215
1329
|
writeAtomicJson(statusPath, statusPayload);
|
|
1216
1330
|
emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
|
|
1217
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
|
+
});
|
|
1218
1436
|
const consumePendingAppendRequests = (): void => {
|
|
1219
1437
|
if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
|
|
1220
1438
|
const requests = consumeChainAppendRequests(asyncDir);
|
|
@@ -1507,6 +1725,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1507
1725
|
}
|
|
1508
1726
|
|
|
1509
1727
|
const interruptRunner = () => {
|
|
1728
|
+
consumeInterruptRequest(asyncDir);
|
|
1510
1729
|
if (interrupted || statusPayload.state !== "running") return;
|
|
1511
1730
|
interrupted = true;
|
|
1512
1731
|
const now = Date.now();
|
|
@@ -1529,13 +1748,59 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1529
1748
|
ts: now,
|
|
1530
1749
|
runId: id,
|
|
1531
1750
|
}));
|
|
1532
|
-
|
|
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();
|
|
1533
1788
|
};
|
|
1534
1789
|
process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
|
|
1790
|
+
// Portable control inbox: the parent drops an interrupt request file here when
|
|
1791
|
+
// it cannot deliver the OS signal (e.g. ENOSYS on Windows). Routes into the
|
|
1792
|
+
// same graceful interruptRunner() so stop/steer work on every platform.
|
|
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
|
+
}
|
|
1535
1799
|
appendJsonl(
|
|
1536
1800
|
eventsPath,
|
|
1537
1801
|
JSON.stringify({
|
|
1538
1802
|
type: "subagent.run.started",
|
|
1803
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
1539
1804
|
ts: overallStartTime,
|
|
1540
1805
|
runId: id,
|
|
1541
1806
|
mode: statusPayload.mode,
|
|
@@ -1548,7 +1813,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1548
1813
|
let stepCursor = 0;
|
|
1549
1814
|
|
|
1550
1815
|
while (true) {
|
|
1551
|
-
if (interrupted) break;
|
|
1816
|
+
if (interrupted || timedOut) break;
|
|
1552
1817
|
consumePendingAppendRequests();
|
|
1553
1818
|
if (stepCursor >= steps.length) break;
|
|
1554
1819
|
const stepIndex = stepCursor++;
|
|
@@ -1600,7 +1865,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1600
1865
|
placeholder.durationMs = 0;
|
|
1601
1866
|
}
|
|
1602
1867
|
previousOutput = "Dynamic fanout produced 0 results.";
|
|
1603
|
-
const groupAcceptance = step.effectiveAcceptance?.explicit
|
|
1868
|
+
const groupAcceptance = step.effectiveAcceptance?.explicit && !timedOut
|
|
1604
1869
|
? await evaluateAcceptance({
|
|
1605
1870
|
acceptance: step.effectiveAcceptance,
|
|
1606
1871
|
output: "",
|
|
@@ -1609,38 +1874,55 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1609
1874
|
notes: "Dynamic fanout produced 0 results.",
|
|
1610
1875
|
}),
|
|
1611
1876
|
cwd,
|
|
1877
|
+
signal: timeoutAbortController.signal,
|
|
1878
|
+
abortMessage: timeoutMessage ?? "Subagent timed out.",
|
|
1612
1879
|
})
|
|
1613
1880
|
: undefined;
|
|
1614
|
-
|
|
1615
|
-
const
|
|
1616
|
-
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!;
|
|
1617
1887
|
statusPayload.state = "failed";
|
|
1618
|
-
statusPayload.error =
|
|
1888
|
+
statusPayload.error = errorMessage;
|
|
1619
1889
|
if (placeholder) {
|
|
1620
1890
|
placeholder.status = "failed";
|
|
1621
|
-
placeholder.error =
|
|
1891
|
+
placeholder.error = errorMessage;
|
|
1622
1892
|
placeholder.exitCode = 1;
|
|
1893
|
+
placeholder.timedOut = groupTimedOut ? true : undefined;
|
|
1623
1894
|
}
|
|
1624
|
-
markDynamicGraphGroup(stepIndex, "failed",
|
|
1625
|
-
statusPayload.lastUpdate = now;
|
|
1895
|
+
markDynamicGraphGroup(stepIndex, "failed", errorMessage, effectiveGroupAcceptance);
|
|
1896
|
+
statusPayload.lastUpdate = Date.now();
|
|
1626
1897
|
writeStatusPayload();
|
|
1627
|
-
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 });
|
|
1628
1899
|
break;
|
|
1629
1900
|
}
|
|
1630
1901
|
flatIndex++;
|
|
1631
1902
|
statusPayload.lastUpdate = now;
|
|
1632
|
-
markDynamicGraphGroup(stepIndex, "completed", undefined,
|
|
1903
|
+
markDynamicGraphGroup(stepIndex, "completed", undefined, effectiveGroupAcceptance);
|
|
1633
1904
|
writeStatusPayload();
|
|
1634
1905
|
continue;
|
|
1635
1906
|
}
|
|
1636
1907
|
|
|
1637
|
-
const dynamicSteps = materialized.parallel.map((task, itemIndex) =>
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
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
|
+
});
|
|
1644
1926
|
const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task) => ({
|
|
1645
1927
|
agent: task.agent,
|
|
1646
1928
|
phase: task.phase ?? step.phase,
|
|
@@ -1704,6 +1986,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1704
1986
|
let aborted = false;
|
|
1705
1987
|
const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
|
|
1706
1988
|
const fi = groupStartFlatIndex + taskIdx;
|
|
1989
|
+
if (timedOut) return timedOutStepResult(task.agent);
|
|
1990
|
+
if (interrupted) return pausedStepResult(task.agent);
|
|
1707
1991
|
if (aborted && failFast) {
|
|
1708
1992
|
const skippedAt = Date.now();
|
|
1709
1993
|
statusPayload.steps[fi].status = "failed";
|
|
@@ -1741,22 +2025,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1741
2025
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
1742
2026
|
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
1743
2027
|
nestedRoute: config.nestedRoute,
|
|
1744
|
-
registerInterrupt: (interrupt) =>
|
|
1745
|
-
|
|
1746
|
-
|
|
2028
|
+
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2029
|
+
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
2030
|
+
timeoutSignal: timeoutAbortController.signal,
|
|
2031
|
+
timeoutMessage,
|
|
1747
2032
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
1748
2033
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2034
|
+
skipAcceptance: () => timedOut,
|
|
1749
2035
|
});
|
|
1750
2036
|
const taskEndTime = Date.now();
|
|
1751
|
-
|
|
2037
|
+
const childInterrupted = singleResult.interrupted === true;
|
|
2038
|
+
statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
1752
2039
|
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
1753
2040
|
statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
|
|
1754
|
-
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;
|
|
1755
2043
|
statusPayload.steps[fi].model = singleResult.model;
|
|
1756
2044
|
statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
|
|
1757
2045
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
1758
2046
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
1759
|
-
statusPayload.steps[fi].
|
|
2047
|
+
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2048
|
+
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
1760
2049
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
1761
2050
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
1762
2051
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -1764,13 +2053,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1764
2053
|
statusPayload.lastUpdate = taskEndTime;
|
|
1765
2054
|
writeStatusPayload();
|
|
1766
2055
|
appendJsonl(eventsPath, JSON.stringify({
|
|
1767
|
-
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",
|
|
1768
2057
|
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
1769
|
-
exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
|
|
2058
|
+
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
|
|
1770
2059
|
}));
|
|
1771
2060
|
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
1772
|
-
return { ...singleResult, skipped: false };
|
|
1773
|
-
});
|
|
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);
|
|
1774
2063
|
|
|
1775
2064
|
flatIndex += dynamicSteps.length;
|
|
1776
2065
|
for (const pr of parallelResults) {
|
|
@@ -1778,14 +2067,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1778
2067
|
agent: pr.agent,
|
|
1779
2068
|
output: pr.output,
|
|
1780
2069
|
error: pr.error,
|
|
1781
|
-
success: pr.exitCode === 0,
|
|
1782
|
-
exitCode: pr.exitCode,
|
|
2070
|
+
success: pr.interrupted !== true && pr.exitCode === 0,
|
|
2071
|
+
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
1783
2072
|
skipped: pr.skipped,
|
|
2073
|
+
interrupted: pr.interrupted,
|
|
2074
|
+
timedOut: pr.timedOut,
|
|
1784
2075
|
sessionFile: pr.sessionFile,
|
|
1785
2076
|
intercomTarget: pr.intercomTarget,
|
|
1786
2077
|
model: pr.model,
|
|
1787
2078
|
attemptedModels: pr.attemptedModels,
|
|
1788
2079
|
modelAttempts: pr.modelAttempts,
|
|
2080
|
+
totalCost: pr.totalCost,
|
|
1789
2081
|
artifactPaths: pr.artifactPaths,
|
|
1790
2082
|
structuredOutput: pr.structuredOutput,
|
|
1791
2083
|
structuredOutputPath: pr.structuredOutputPath,
|
|
@@ -1805,7 +2097,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1805
2097
|
stepIndex,
|
|
1806
2098
|
};
|
|
1807
2099
|
statusPayload.outputs = outputs;
|
|
1808
|
-
const groupAcceptance = step.effectiveAcceptance
|
|
2100
|
+
const groupAcceptance = step.effectiveAcceptance && !timedOut
|
|
1809
2101
|
? await evaluateAcceptance({
|
|
1810
2102
|
acceptance: step.effectiveAcceptance,
|
|
1811
2103
|
output: "",
|
|
@@ -1814,21 +2106,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1814
2106
|
notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
|
|
1815
2107
|
}),
|
|
1816
2108
|
cwd,
|
|
2109
|
+
signal: timeoutAbortController.signal,
|
|
2110
|
+
abortMessage: timeoutMessage ?? "Subagent timed out.",
|
|
1817
2111
|
})
|
|
1818
2112
|
: undefined;
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
|
|
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) {
|
|
1822
2119
|
results.push({
|
|
1823
2120
|
agent: step.parallel.agent,
|
|
1824
|
-
output:
|
|
1825
|
-
error:
|
|
2121
|
+
output: groupError,
|
|
2122
|
+
error: groupError,
|
|
1826
2123
|
success: false,
|
|
1827
2124
|
exitCode: 1,
|
|
2125
|
+
timedOut: groupTimedOut ? true : undefined,
|
|
1828
2126
|
structuredOutput: collection,
|
|
1829
|
-
acceptance:
|
|
2127
|
+
acceptance: effectiveGroupAcceptance,
|
|
1830
2128
|
});
|
|
1831
|
-
statusPayload.error =
|
|
2129
|
+
statusPayload.error = groupError;
|
|
1832
2130
|
}
|
|
1833
2131
|
} catch (error) {
|
|
1834
2132
|
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
@@ -1894,6 +2192,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1894
2192
|
setupHook: config.worktreeSetupHook
|
|
1895
2193
|
? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
|
|
1896
2194
|
: undefined,
|
|
2195
|
+
baseDir: config.worktreeBaseDir,
|
|
1897
2196
|
});
|
|
1898
2197
|
} catch (error) {
|
|
1899
2198
|
const setupError = error instanceof Error ? error.message : String(error);
|
|
@@ -1935,6 +2234,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1935
2234
|
concurrency,
|
|
1936
2235
|
async (task, taskIdx) => {
|
|
1937
2236
|
const fi = groupStartFlatIndex + taskIdx;
|
|
2237
|
+
if (timedOut) return timedOutStepResult(task.agent);
|
|
2238
|
+
if (interrupted) return pausedStepResult(task.agent);
|
|
1938
2239
|
if (aborted && failFast) {
|
|
1939
2240
|
const skippedAt = Date.now();
|
|
1940
2241
|
statusPayload.steps[fi].status = "failed";
|
|
@@ -1988,11 +2289,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1988
2289
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
1989
2290
|
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
1990
2291
|
nestedRoute: config.nestedRoute,
|
|
1991
|
-
registerInterrupt: (interrupt) =>
|
|
1992
|
-
|
|
1993
|
-
|
|
2292
|
+
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2293
|
+
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
2294
|
+
timeoutSignal: timeoutAbortController.signal,
|
|
2295
|
+
timeoutMessage,
|
|
1994
2296
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
1995
2297
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2298
|
+
skipAcceptance: () => timedOut,
|
|
1996
2299
|
});
|
|
1997
2300
|
if (task.sessionFile) {
|
|
1998
2301
|
latestSessionFile = task.sessionFile;
|
|
@@ -2000,16 +2303,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2000
2303
|
|
|
2001
2304
|
const taskEndTime = Date.now();
|
|
2002
2305
|
const taskDuration = taskEndTime - taskStartTime;
|
|
2306
|
+
const childInterrupted = singleResult.interrupted === true;
|
|
2003
2307
|
|
|
2004
|
-
statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2308
|
+
statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2005
2309
|
statusPayload.steps[fi].endedAt = taskEndTime;
|
|
2006
2310
|
statusPayload.steps[fi].durationMs = taskDuration;
|
|
2007
|
-
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;
|
|
2008
2313
|
statusPayload.steps[fi].model = singleResult.model;
|
|
2009
2314
|
statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
|
|
2010
2315
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
2011
2316
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
2012
|
-
statusPayload.steps[fi].
|
|
2317
|
+
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2318
|
+
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2013
2319
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
2014
2320
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2015
2321
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -2018,9 +2324,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2018
2324
|
writeStatusPayload();
|
|
2019
2325
|
|
|
2020
2326
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2021
|
-
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",
|
|
2022
2328
|
ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
|
|
2023
|
-
exitCode: singleResult.exitCode, durationMs: taskDuration,
|
|
2329
|
+
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
|
|
2024
2330
|
}));
|
|
2025
2331
|
if (singleResult.completionGuardTriggered) {
|
|
2026
2332
|
const event = buildControlEvent({
|
|
@@ -2037,8 +2343,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2037
2343
|
}
|
|
2038
2344
|
|
|
2039
2345
|
if (singleResult.exitCode !== 0 && failFast) aborted = true;
|
|
2040
|
-
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 };
|
|
2041
2347
|
},
|
|
2348
|
+
globalSemaphore,
|
|
2042
2349
|
);
|
|
2043
2350
|
|
|
2044
2351
|
flatIndex += group.parallel.length;
|
|
@@ -2066,14 +2373,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2066
2373
|
agent: pr.agent,
|
|
2067
2374
|
output: pr.output,
|
|
2068
2375
|
error: pr.error,
|
|
2069
|
-
success: pr.exitCode === 0,
|
|
2070
|
-
exitCode: pr.exitCode,
|
|
2376
|
+
success: pr.interrupted !== true && pr.exitCode === 0,
|
|
2377
|
+
exitCode: pr.interrupted === true ? 0 : pr.exitCode,
|
|
2071
2378
|
skipped: pr.skipped,
|
|
2379
|
+
interrupted: pr.interrupted,
|
|
2380
|
+
timedOut: pr.timedOut,
|
|
2072
2381
|
sessionFile: pr.sessionFile,
|
|
2073
2382
|
intercomTarget: pr.intercomTarget,
|
|
2074
2383
|
model: pr.model,
|
|
2075
2384
|
attemptedModels: pr.attemptedModels,
|
|
2076
2385
|
modelAttempts: pr.modelAttempts,
|
|
2386
|
+
totalCost: pr.totalCost,
|
|
2077
2387
|
artifactPaths: pr.artifactPaths,
|
|
2078
2388
|
structuredOutput: pr.structuredOutput,
|
|
2079
2389
|
structuredOutputPath: pr.structuredOutputPath,
|
|
@@ -2153,11 +2463,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2153
2463
|
childIntercomTarget: config.childIntercomTargets?.[flatIndex],
|
|
2154
2464
|
orchestratorIntercomTarget: config.controlIntercomTarget,
|
|
2155
2465
|
nestedRoute: config.nestedRoute,
|
|
2156
|
-
registerInterrupt: (interrupt) =>
|
|
2157
|
-
|
|
2158
|
-
|
|
2466
|
+
registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
|
|
2467
|
+
registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
|
|
2468
|
+
timeoutSignal: timeoutAbortController.signal,
|
|
2469
|
+
timeoutMessage,
|
|
2159
2470
|
onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
|
|
2160
2471
|
onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
|
|
2472
|
+
skipAcceptance: () => timedOut,
|
|
2161
2473
|
});
|
|
2162
2474
|
if (seqStep.sessionFile) {
|
|
2163
2475
|
latestSessionFile = seqStep.sessionFile;
|
|
@@ -2166,20 +2478,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2166
2478
|
previousOutput = singleResult.output;
|
|
2167
2479
|
results.push({
|
|
2168
2480
|
agent: singleResult.agent,
|
|
2169
|
-
output: singleResult.output,
|
|
2170
|
-
error: singleResult.error,
|
|
2171
|
-
success: singleResult.exitCode === 0,
|
|
2172
|
-
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,
|
|
2173
2485
|
sessionFile: singleResult.sessionFile,
|
|
2174
2486
|
intercomTarget: singleResult.intercomTarget,
|
|
2175
2487
|
model: singleResult.model,
|
|
2176
2488
|
attemptedModels: singleResult.attemptedModels,
|
|
2177
2489
|
modelAttempts: singleResult.modelAttempts,
|
|
2490
|
+
totalCost: singleResult.totalCost,
|
|
2178
2491
|
artifactPaths: singleResult.artifactPaths,
|
|
2179
2492
|
structuredOutput: singleResult.structuredOutput,
|
|
2180
2493
|
structuredOutputPath: singleResult.structuredOutputPath,
|
|
2181
2494
|
structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
|
|
2182
2495
|
acceptance: singleResult.acceptance,
|
|
2496
|
+
interrupted: singleResult.interrupted,
|
|
2497
|
+
timedOut: timedOut || singleResult.timedOut ? true : undefined,
|
|
2183
2498
|
});
|
|
2184
2499
|
if (seqStep.outputName) {
|
|
2185
2500
|
outputs[seqStep.outputName] = outputEntryFromAsyncResult({
|
|
@@ -2212,15 +2527,18 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2212
2527
|
}
|
|
2213
2528
|
|
|
2214
2529
|
const stepEndTime = Date.now();
|
|
2215
|
-
|
|
2530
|
+
const childInterrupted = singleResult.interrupted === true;
|
|
2531
|
+
statusPayload.steps[flatIndex].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
|
|
2216
2532
|
statusPayload.steps[flatIndex].endedAt = stepEndTime;
|
|
2217
2533
|
statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
|
|
2218
|
-
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;
|
|
2219
2536
|
statusPayload.steps[flatIndex].model = singleResult.model;
|
|
2220
2537
|
statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
|
|
2221
2538
|
statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
|
|
2222
2539
|
statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
|
|
2223
|
-
statusPayload.steps[flatIndex].
|
|
2540
|
+
statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
|
|
2541
|
+
statusPayload.steps[flatIndex].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2224
2542
|
statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
|
|
2225
2543
|
statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2226
2544
|
statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -2233,12 +2551,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2233
2551
|
writeStatusPayload();
|
|
2234
2552
|
|
|
2235
2553
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2236
|
-
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",
|
|
2237
2555
|
ts: stepEndTime,
|
|
2238
2556
|
runId: id,
|
|
2239
2557
|
stepIndex: flatIndex,
|
|
2240
2558
|
agent: seqStep.agent,
|
|
2241
|
-
exitCode: singleResult.exitCode,
|
|
2559
|
+
exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
|
|
2242
2560
|
durationMs: stepEndTime - stepStartTime,
|
|
2243
2561
|
tokens: stepTokens,
|
|
2244
2562
|
}));
|
|
@@ -2277,6 +2595,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2277
2595
|
}
|
|
2278
2596
|
|
|
2279
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;
|
|
2280
2604
|
const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
|
|
2281
2605
|
const agentName = finalFlatAgents.length === 1
|
|
2282
2606
|
? finalFlatAgents[0]!
|
|
@@ -2317,13 +2641,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2317
2641
|
clearInterval(activityTimer);
|
|
2318
2642
|
activityTimer = undefined;
|
|
2319
2643
|
}
|
|
2644
|
+
if (timeoutTimer) {
|
|
2645
|
+
clearTimeout(timeoutTimer);
|
|
2646
|
+
timeoutTimer = undefined;
|
|
2647
|
+
}
|
|
2648
|
+
disposeControlInbox();
|
|
2320
2649
|
const effectiveSessionFile = sessionFile ?? latestSessionFile;
|
|
2321
2650
|
const runEndedAt = Date.now();
|
|
2322
|
-
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";
|
|
2323
2652
|
statusPayload.activityState = undefined;
|
|
2653
|
+
if (timedOut) {
|
|
2654
|
+
statusPayload.timedOut = true;
|
|
2655
|
+
statusPayload.error = timeoutMessage ?? "Subagent timed out.";
|
|
2656
|
+
}
|
|
2324
2657
|
statusPayload.endedAt = runEndedAt;
|
|
2325
2658
|
statusPayload.lastUpdate = runEndedAt;
|
|
2326
2659
|
statusPayload.sessionFile = effectiveSessionFile;
|
|
2660
|
+
statusPayload.totalCost = finalTotalCost;
|
|
2327
2661
|
statusPayload.shareUrl = shareUrl;
|
|
2328
2662
|
statusPayload.gistUrl = gistUrl;
|
|
2329
2663
|
statusPayload.shareError = shareError;
|
|
@@ -2338,10 +2672,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2338
2672
|
eventsPath,
|
|
2339
2673
|
JSON.stringify({
|
|
2340
2674
|
type: "subagent.run.completed",
|
|
2675
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
2341
2676
|
ts: runEndedAt,
|
|
2342
2677
|
runId: id,
|
|
2343
2678
|
status: statusPayload.state,
|
|
2344
2679
|
durationMs: runEndedAt - overallStartTime,
|
|
2680
|
+
totalTokens: statusPayload.totalTokens,
|
|
2681
|
+
totalCost: finalTotalCost,
|
|
2345
2682
|
}),
|
|
2346
2683
|
);
|
|
2347
2684
|
writeRunLog(logPath, {
|
|
@@ -2365,23 +2702,30 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2365
2702
|
|
|
2366
2703
|
try {
|
|
2367
2704
|
writeAtomicJson(resultPath, {
|
|
2705
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
2368
2706
|
id,
|
|
2369
2707
|
agent: agentName,
|
|
2370
2708
|
mode: resultMode,
|
|
2371
|
-
success: !interrupted && results.every((r) => r.success),
|
|
2372
|
-
state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
|
|
2373
|
-
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." } : {}),
|
|
2374
2715
|
results: results.map((r) => ({
|
|
2375
2716
|
agent: r.agent,
|
|
2376
2717
|
output: r.output,
|
|
2377
2718
|
error: r.error,
|
|
2378
2719
|
success: r.success,
|
|
2379
2720
|
skipped: r.skipped || undefined,
|
|
2721
|
+
interrupted: r.interrupted || undefined,
|
|
2722
|
+
timedOut: r.timedOut || undefined,
|
|
2380
2723
|
sessionFile: r.sessionFile,
|
|
2381
2724
|
intercomTarget: r.intercomTarget,
|
|
2382
2725
|
model: r.model,
|
|
2383
2726
|
attemptedModels: r.attemptedModels,
|
|
2384
2727
|
modelAttempts: r.modelAttempts,
|
|
2728
|
+
totalCost: r.totalCost,
|
|
2385
2729
|
artifactPaths: r.artifactPaths,
|
|
2386
2730
|
truncated: r.truncated,
|
|
2387
2731
|
structuredOutput: r.structuredOutput,
|
|
@@ -2391,9 +2735,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2391
2735
|
})),
|
|
2392
2736
|
outputs,
|
|
2393
2737
|
workflowGraph: statusPayload.workflowGraph,
|
|
2394
|
-
exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
2738
|
+
exitCode: timedOut ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
2395
2739
|
timestamp: runEndedAt,
|
|
2396
2740
|
durationMs: runEndedAt - overallStartTime,
|
|
2741
|
+
totalTokens: statusPayload.totalTokens,
|
|
2742
|
+
totalCost: finalTotalCost,
|
|
2397
2743
|
truncated,
|
|
2398
2744
|
artifactsDir,
|
|
2399
2745
|
cwd,
|