pi-subagents 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -3
- package/README.md +147 -58
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +70 -14
- package/src/agents/agent-management.ts +177 -5
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +142 -12
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/doctor.ts +1 -9
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +65 -90
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +52 -8
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +51 -7
- package/src/runs/background/async-job-tracker.ts +12 -2
- package/src/runs/background/async-status.ts +27 -2
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +106 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +166 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/subagent-runner.ts +409 -35
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +95 -21
- package/src/runs/foreground/execution.ts +150 -21
- package/src/runs/foreground/subagent-executor.ts +378 -64
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +167 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +31 -0
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +30 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/shared/artifacts.ts +1 -0
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +134 -19
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +16 -2
- package/src/tui/render.ts +16 -8
- package/src/extension/companion-suggestions.ts +0 -359
|
@@ -4,7 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import type { Message } from "@earendil-works/pi-ai";
|
|
6
6
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
8
|
+
import { consumeInterruptRequest, deliverInterruptRequest, deliverTimeoutRequest, enqueueStepSteer, stepSteerInboxDir, watchAsyncControlInbox, type SteerRequest } from "./control-channel.ts";
|
|
8
9
|
import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
9
10
|
import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
|
|
10
11
|
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
@@ -20,7 +21,11 @@ import {
|
|
|
20
21
|
type NestedRouteInfo,
|
|
21
22
|
type NestedRunSummary,
|
|
22
23
|
type ResolvedControlConfig,
|
|
24
|
+
type ResolvedTurnBudget,
|
|
25
|
+
type ResolvedToolBudget,
|
|
23
26
|
type SubagentRunMode,
|
|
27
|
+
type ToolBudgetState,
|
|
28
|
+
type TurnBudgetState,
|
|
24
29
|
type Usage,
|
|
25
30
|
type WorkflowGraphSnapshot,
|
|
26
31
|
DEFAULT_MAX_OUTPUT,
|
|
@@ -86,6 +91,8 @@ import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts
|
|
|
86
91
|
import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
|
|
87
92
|
import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
88
93
|
import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
|
|
94
|
+
import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, shouldAbortForTurnBudget, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
|
|
95
|
+
import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
|
|
89
96
|
|
|
90
97
|
interface SubagentRunConfig {
|
|
91
98
|
id: string;
|
|
@@ -117,6 +124,8 @@ interface SubagentRunConfig {
|
|
|
117
124
|
nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
|
|
118
125
|
timeoutMs?: number;
|
|
119
126
|
deadlineAt?: number;
|
|
127
|
+
turnBudget?: ResolvedTurnBudget;
|
|
128
|
+
toolBudget?: ResolvedToolBudget;
|
|
120
129
|
/** Global cap on simultaneously-running subagent tasks within this run. */
|
|
121
130
|
globalConcurrencyLimit?: number;
|
|
122
131
|
}
|
|
@@ -130,6 +139,11 @@ interface StepResult {
|
|
|
130
139
|
skipped?: boolean;
|
|
131
140
|
interrupted?: boolean;
|
|
132
141
|
timedOut?: boolean;
|
|
142
|
+
turnBudget?: TurnBudgetState;
|
|
143
|
+
turnBudgetExceeded?: boolean;
|
|
144
|
+
wrapUpRequested?: boolean;
|
|
145
|
+
toolBudget?: ToolBudgetState;
|
|
146
|
+
toolBudgetBlocked?: boolean;
|
|
133
147
|
sessionFile?: string;
|
|
134
148
|
intercomTarget?: string;
|
|
135
149
|
model?: string;
|
|
@@ -138,6 +152,8 @@ interface StepResult {
|
|
|
138
152
|
totalCost?: CostSummary;
|
|
139
153
|
artifactPaths?: ArtifactPaths;
|
|
140
154
|
truncated?: boolean;
|
|
155
|
+
transcriptPath?: string;
|
|
156
|
+
transcriptError?: string;
|
|
141
157
|
structuredOutput?: unknown;
|
|
142
158
|
structuredOutputPath?: string;
|
|
143
159
|
structuredOutputSchemaPath?: string;
|
|
@@ -275,6 +291,13 @@ function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
|
|
|
275
291
|
}
|
|
276
292
|
}
|
|
277
293
|
|
|
294
|
+
function isTerminalAssistantStop(message: Message): boolean {
|
|
295
|
+
const stopReason = (message as { stopReason?: string }).stopReason;
|
|
296
|
+
const hasToolCall = Array.isArray(message.content)
|
|
297
|
+
&& message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
298
|
+
return stopReason === "stop" && !hasToolCall;
|
|
299
|
+
}
|
|
300
|
+
|
|
278
301
|
function resetStepLiveDetail(step: RunnerStatusStep): void {
|
|
279
302
|
step.currentTool = undefined;
|
|
280
303
|
step.currentToolArgs = undefined;
|
|
@@ -324,6 +347,11 @@ interface RunPiStreamingResult {
|
|
|
324
347
|
finalOutput: string;
|
|
325
348
|
interrupted?: boolean;
|
|
326
349
|
timedOut?: boolean;
|
|
350
|
+
turnBudget?: TurnBudgetState;
|
|
351
|
+
turnBudgetExceeded?: boolean;
|
|
352
|
+
wrapUpRequested?: boolean;
|
|
353
|
+
toolBudget?: ToolBudgetState;
|
|
354
|
+
toolBudgetBlocked?: boolean;
|
|
327
355
|
observedMutationAttempt?: boolean;
|
|
328
356
|
}
|
|
329
357
|
|
|
@@ -338,8 +366,10 @@ function runPiStreaming(
|
|
|
338
366
|
childEventContext?: ChildEventContext,
|
|
339
367
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void,
|
|
340
368
|
onChildEvent?: (event: ChildEvent) => void,
|
|
369
|
+
transcriptWriter?: ChildTranscriptWriter,
|
|
341
370
|
registerTimeout?: (interrupt: (() => void) | undefined) => void,
|
|
342
371
|
timeoutMessage?: string,
|
|
372
|
+
registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
|
|
343
373
|
): Promise<RunPiStreamingResult> {
|
|
344
374
|
return new Promise((resolve) => {
|
|
345
375
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
@@ -364,6 +394,9 @@ function runPiStreaming(
|
|
|
364
394
|
let assistantError: string | undefined;
|
|
365
395
|
let interrupted = false;
|
|
366
396
|
let timedOut = false;
|
|
397
|
+
let turnBudgetExceeded = false;
|
|
398
|
+
let turnBudgetMessage: string | undefined;
|
|
399
|
+
let turnBudget: TurnBudgetState | undefined;
|
|
367
400
|
let observedMutationAttempt = false;
|
|
368
401
|
const rawStdoutLines: string[] = [];
|
|
369
402
|
|
|
@@ -393,6 +426,8 @@ function runPiStreaming(
|
|
|
393
426
|
|
|
394
427
|
const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
|
|
395
428
|
appendChildEvent({ type, line });
|
|
429
|
+
if (type === "subagent.child.stdout") transcriptWriter?.writeStdoutLine(line);
|
|
430
|
+
else transcriptWriter?.writeStderrLine(line);
|
|
396
431
|
};
|
|
397
432
|
|
|
398
433
|
const processStdoutLine = (line: string) => {
|
|
@@ -408,6 +443,7 @@ function runPiStreaming(
|
|
|
408
443
|
}
|
|
409
444
|
|
|
410
445
|
appendChildEvent(event);
|
|
446
|
+
transcriptWriter?.writeChildEvent(event);
|
|
411
447
|
onChildEvent?.(event);
|
|
412
448
|
|
|
413
449
|
if (event.type === "tool_execution_start" && event.toolName) {
|
|
@@ -434,10 +470,7 @@ function runPiStreaming(
|
|
|
434
470
|
usage.cacheWrite += eventUsage.cacheWrite ?? 0;
|
|
435
471
|
usage.cost += eventUsage.cost?.total ?? 0;
|
|
436
472
|
}
|
|
437
|
-
|
|
438
|
-
const hasToolCall = Array.isArray(event.message.content)
|
|
439
|
-
&& event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
440
|
-
if (stopReason === "stop" && !hasToolCall) {
|
|
473
|
+
if (isTerminalAssistantStop(event.message)) {
|
|
441
474
|
if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
|
|
442
475
|
cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
|
|
443
476
|
startFinalDrain();
|
|
@@ -469,6 +502,8 @@ function runPiStreaming(
|
|
|
469
502
|
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
470
503
|
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
471
504
|
let timeoutHardKillTimer: NodeJS.Timeout | undefined;
|
|
505
|
+
let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
|
|
506
|
+
let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
|
|
472
507
|
let settled = false;
|
|
473
508
|
const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
|
|
474
509
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
@@ -502,6 +537,23 @@ function runPiStreaming(
|
|
|
502
537
|
}, TIMEOUT_HARD_KILL_MS);
|
|
503
538
|
timeoutHardKillTimer.unref?.();
|
|
504
539
|
});
|
|
540
|
+
registerTurnBudgetAbort?.((message, state) => {
|
|
541
|
+
if (settled || timedOut || turnBudgetExceeded) return;
|
|
542
|
+
turnBudgetExceeded = true;
|
|
543
|
+
turnBudgetMessage = message;
|
|
544
|
+
turnBudget = state;
|
|
545
|
+
interrupted = false;
|
|
546
|
+
error = message;
|
|
547
|
+
trySignalChild(child, "SIGINT");
|
|
548
|
+
turnBudgetTerminationTimer = setTimeout(() => {
|
|
549
|
+
if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
|
|
550
|
+
}, 1000);
|
|
551
|
+
turnBudgetTerminationTimer.unref?.();
|
|
552
|
+
turnBudgetHardKillTimer = setTimeout(() => {
|
|
553
|
+
if (!settled && !timedOut) trySignalChild(child, "SIGKILL");
|
|
554
|
+
}, 4000);
|
|
555
|
+
turnBudgetHardKillTimer.unref?.();
|
|
556
|
+
});
|
|
505
557
|
const clearDrainTimers = () => {
|
|
506
558
|
if (finalDrainTimer) {
|
|
507
559
|
clearTimeout(finalDrainTimer);
|
|
@@ -515,6 +567,14 @@ function runPiStreaming(
|
|
|
515
567
|
clearTimeout(timeoutHardKillTimer);
|
|
516
568
|
timeoutHardKillTimer = undefined;
|
|
517
569
|
}
|
|
570
|
+
if (turnBudgetTerminationTimer) {
|
|
571
|
+
clearTimeout(turnBudgetTerminationTimer);
|
|
572
|
+
turnBudgetTerminationTimer = undefined;
|
|
573
|
+
}
|
|
574
|
+
if (turnBudgetHardKillTimer) {
|
|
575
|
+
clearTimeout(turnBudgetHardKillTimer);
|
|
576
|
+
turnBudgetHardKillTimer = undefined;
|
|
577
|
+
}
|
|
518
578
|
};
|
|
519
579
|
function startFinalDrain(): void {
|
|
520
580
|
if (childExited || finalDrainTimer || settled) return;
|
|
@@ -542,6 +602,7 @@ function runPiStreaming(
|
|
|
542
602
|
settled = true;
|
|
543
603
|
registerInterrupt?.(undefined);
|
|
544
604
|
registerTimeout?.(undefined);
|
|
605
|
+
registerTurnBudgetAbort?.(undefined);
|
|
545
606
|
clearDrainTimers();
|
|
546
607
|
clearStdioGuard();
|
|
547
608
|
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
@@ -552,14 +613,17 @@ function runPiStreaming(
|
|
|
552
613
|
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
|
|
553
614
|
resolve({
|
|
554
615
|
stderr,
|
|
555
|
-
exitCode: timedOut ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
616
|
+
exitCode: timedOut ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
556
617
|
messages,
|
|
557
618
|
usage,
|
|
558
619
|
model,
|
|
559
|
-
error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
620
|
+
error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
|
|
560
621
|
finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput,
|
|
561
622
|
interrupted,
|
|
562
623
|
timedOut,
|
|
624
|
+
turnBudget,
|
|
625
|
+
turnBudgetExceeded,
|
|
626
|
+
wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
|
|
563
627
|
observedMutationAttempt,
|
|
564
628
|
});
|
|
565
629
|
});
|
|
@@ -568,12 +632,13 @@ function runPiStreaming(
|
|
|
568
632
|
settled = true;
|
|
569
633
|
registerInterrupt?.(undefined);
|
|
570
634
|
registerTimeout?.(undefined);
|
|
635
|
+
registerTurnBudgetAbort?.(undefined);
|
|
571
636
|
clearDrainTimers();
|
|
572
637
|
clearStdioGuard();
|
|
573
638
|
outputStream.end();
|
|
574
639
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
575
640
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
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 });
|
|
641
|
+
resolve({ stderr, exitCode: 1, messages, usage, model, error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined, observedMutationAttempt });
|
|
577
642
|
});
|
|
578
643
|
});
|
|
579
644
|
}
|
|
@@ -698,12 +763,16 @@ interface SingleStepContext {
|
|
|
698
763
|
flatIndex: number;
|
|
699
764
|
flatStepCount: number;
|
|
700
765
|
outputFile: string;
|
|
766
|
+
steerInboxDir?: string;
|
|
767
|
+
transcriptPath?: string;
|
|
701
768
|
piPackageRoot?: string;
|
|
702
769
|
piArgv1?: string;
|
|
703
770
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void;
|
|
704
771
|
registerTimeout?: (interrupt: (() => void) | undefined) => void;
|
|
772
|
+
registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void;
|
|
705
773
|
timeoutSignal?: AbortSignal;
|
|
706
774
|
timeoutMessage?: string;
|
|
775
|
+
turnBudget?: ResolvedTurnBudget;
|
|
707
776
|
childIntercomTarget?: string;
|
|
708
777
|
orchestratorIntercomTarget?: string;
|
|
709
778
|
nestedRoute?: NestedRouteInfo;
|
|
@@ -725,8 +794,15 @@ async function runSingleStep(
|
|
|
725
794
|
attemptedModels?: string[];
|
|
726
795
|
modelAttempts?: ModelAttempt[];
|
|
727
796
|
artifactPaths?: ArtifactPaths;
|
|
797
|
+
transcriptPath?: string;
|
|
798
|
+
transcriptError?: string;
|
|
728
799
|
interrupted?: boolean;
|
|
729
800
|
timedOut?: boolean;
|
|
801
|
+
turnBudget?: TurnBudgetState;
|
|
802
|
+
turnBudgetExceeded?: boolean;
|
|
803
|
+
wrapUpRequested?: boolean;
|
|
804
|
+
toolBudget?: ToolBudgetState;
|
|
805
|
+
toolBudgetBlocked?: boolean;
|
|
730
806
|
sessionFile?: string;
|
|
731
807
|
intercomTarget?: string;
|
|
732
808
|
completionGuardTriggered?: boolean;
|
|
@@ -799,6 +875,7 @@ async function runSingleStep(
|
|
|
799
875
|
const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
|
|
800
876
|
|
|
801
877
|
let artifactPaths: ArtifactPaths | undefined;
|
|
878
|
+
let transcriptWriter: ChildTranscriptWriter | undefined;
|
|
802
879
|
if (ctx.artifactsDir && ctx.artifactConfig?.enabled !== false) {
|
|
803
880
|
const index = ctx.flatStepCount > 1 ? ctx.flatIndex : undefined;
|
|
804
881
|
artifactPaths = getArtifactPaths(ctx.artifactsDir, ctx.id, step.agent, index);
|
|
@@ -806,7 +883,18 @@ async function runSingleStep(
|
|
|
806
883
|
if (ctx.artifactConfig?.includeInput !== false) {
|
|
807
884
|
fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${task}`, "utf-8");
|
|
808
885
|
}
|
|
886
|
+
if (ctx.artifactConfig?.includeTranscript !== false) {
|
|
887
|
+
transcriptWriter = createChildTranscriptWriter({
|
|
888
|
+
transcriptPath: artifactPaths.transcriptPath,
|
|
889
|
+
source: "async",
|
|
890
|
+
runId: ctx.id,
|
|
891
|
+
agent: step.agent,
|
|
892
|
+
childIndex: ctx.flatIndex,
|
|
893
|
+
cwd: step.cwd ?? ctx.cwd,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
809
896
|
}
|
|
897
|
+
transcriptWriter?.writeInitialUserMessage(task);
|
|
810
898
|
|
|
811
899
|
const candidates = step.modelCandidates && step.modelCandidates.length > 0
|
|
812
900
|
? step.modelCandidates
|
|
@@ -820,6 +908,9 @@ async function runSingleStep(
|
|
|
820
908
|
let finalResult: RunPiStreamingResult | undefined;
|
|
821
909
|
let finalOutputSnapshot: SingleOutputSnapshot | undefined;
|
|
822
910
|
let completionGuardTriggeredFinal = false;
|
|
911
|
+
let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
|
|
912
|
+
let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
|
|
913
|
+
let toolBudgetBlocked = false;
|
|
823
914
|
|
|
824
915
|
for (let index = 0; index < candidates.length; index++) {
|
|
825
916
|
if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
@@ -847,7 +938,7 @@ async function runSingleStep(
|
|
|
847
938
|
tools: step.tools,
|
|
848
939
|
extensions: step.extensions,
|
|
849
940
|
subagentOnlyExtensions: step.subagentOnlyExtensions,
|
|
850
|
-
systemPrompt: step.systemPrompt,
|
|
941
|
+
systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
|
|
851
942
|
systemPromptMode: step.systemPromptMode,
|
|
852
943
|
mcpDirectTools: step.mcpDirectTools,
|
|
853
944
|
cwd: step.cwd ?? ctx.cwd,
|
|
@@ -861,7 +952,9 @@ async function runSingleStep(
|
|
|
861
952
|
parentControlInbox: ctx.nestedRoute?.controlInbox,
|
|
862
953
|
parentRootRunId: ctx.nestedRoute?.rootRunId,
|
|
863
954
|
parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
|
|
955
|
+
steerInboxDir: ctx.steerInboxDir,
|
|
864
956
|
structuredOutput: effectiveStructuredOutput,
|
|
957
|
+
toolBudget: step.toolBudget,
|
|
865
958
|
});
|
|
866
959
|
const run = await runPiStreaming(
|
|
867
960
|
args,
|
|
@@ -874,9 +967,26 @@ async function runSingleStep(
|
|
|
874
967
|
{ eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
875
968
|
ctx.registerInterrupt,
|
|
876
969
|
ctx.onChildEvent,
|
|
970
|
+
transcriptWriter,
|
|
877
971
|
ctx.registerTimeout,
|
|
878
972
|
ctx.timeoutMessage,
|
|
973
|
+
ctx.registerTurnBudgetAbort,
|
|
879
974
|
);
|
|
975
|
+
if (run.turnBudget) turnBudget = run.turnBudget;
|
|
976
|
+
else if (ctx.turnBudget) {
|
|
977
|
+
const assistantMessages = run.messages.filter((message) => message.role === "assistant");
|
|
978
|
+
const turnCount = assistantMessages.length;
|
|
979
|
+
const lastAssistantMessage = assistantMessages.at(-1);
|
|
980
|
+
if (turnCount > 0 && turnCount < ctx.turnBudget.maxTurns) {
|
|
981
|
+
turnBudget = { ...ctx.turnBudget, outcome: "within-budget", turnCount };
|
|
982
|
+
} else if (turnCount >= ctx.turnBudget.maxTurns) {
|
|
983
|
+
turnBudget = turnBudgetState(
|
|
984
|
+
ctx.turnBudget,
|
|
985
|
+
turnCount,
|
|
986
|
+
shouldAbortForTurnBudget(ctx.turnBudget, turnCount, lastAssistantMessage ? isTerminalAssistantStop(lastAssistantMessage) : false),
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
880
990
|
cleanupTempDir(tempDir);
|
|
881
991
|
|
|
882
992
|
const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
|
|
@@ -939,7 +1049,14 @@ async function runSingleStep(
|
|
|
939
1049
|
if (candidate) attemptedModels.push(candidate);
|
|
940
1050
|
completionGuardTriggeredFinal = completionGuardTriggered;
|
|
941
1051
|
finalOutputSnapshot = outputSnapshot;
|
|
1052
|
+
if (step.toolBudget) {
|
|
1053
|
+
const toolMessages = run.messages.filter((message) => message.role === "toolResult");
|
|
1054
|
+
const blockedMessage = toolMessages.find((message) => extractTextFromContent(message.content).includes("Tool budget hard limit reached"));
|
|
1055
|
+
toolBudgetBlocked = Boolean(blockedMessage);
|
|
1056
|
+
toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
|
|
1057
|
+
}
|
|
942
1058
|
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
|
|
1059
|
+
if (run.turnBudgetExceeded) break;
|
|
943
1060
|
if (run.timedOut || ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
944
1061
|
if (attempt.success || completionGuardTriggered) break;
|
|
945
1062
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
@@ -957,6 +1074,12 @@ async function runSingleStep(
|
|
|
957
1074
|
if (attemptNotes.length > 0) {
|
|
958
1075
|
outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
|
|
959
1076
|
}
|
|
1077
|
+
if (!finalResult?.timedOut && finalResult?.turnBudgetExceeded && turnBudget) {
|
|
1078
|
+
outputForSummary = formatTurnBudgetOutput(turnBudgetExceededMessage(turnBudget, turnBudget.turnCount), outputForSummary);
|
|
1079
|
+
} else if (!finalResult?.timedOut && turnBudget?.outcome === "wrap-up-requested") {
|
|
1080
|
+
const note = turnBudgetSoftNote(turnBudget, turnBudget.wrapUpRequestedAtTurn ?? turnBudget.turnCount);
|
|
1081
|
+
outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
|
|
1082
|
+
}
|
|
960
1083
|
const outputForAcceptance = rawOutput;
|
|
961
1084
|
const finalizedOutput = finalizeSingleOutput({
|
|
962
1085
|
fullOutput: outputForSummary,
|
|
@@ -968,7 +1091,7 @@ async function runSingleStep(
|
|
|
968
1091
|
saveError: resolvedOutput.saveError,
|
|
969
1092
|
});
|
|
970
1093
|
outputForSummary = finalizedOutput.displayOutput;
|
|
971
|
-
const acceptance = step.effectiveAcceptance && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
|
|
1094
|
+
const acceptance = step.effectiveAcceptance && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
|
|
972
1095
|
? await evaluateAcceptance({
|
|
973
1096
|
acceptance: step.effectiveAcceptance,
|
|
974
1097
|
output: outputForAcceptance,
|
|
@@ -978,15 +1101,18 @@ async function runSingleStep(
|
|
|
978
1101
|
})
|
|
979
1102
|
: undefined;
|
|
980
1103
|
const timedOutAfterAcceptance = finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
|
|
981
|
-
const
|
|
1104
|
+
const turnBudgetExceeded = finalResult?.turnBudgetExceeded === true;
|
|
1105
|
+
const effectiveAcceptance = timedOutAfterAcceptance || turnBudgetExceeded ? undefined : acceptance;
|
|
982
1106
|
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;
|
|
1107
|
+
const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !turnBudgetExceeded;
|
|
1108
|
+
const effectiveFinalExitCode = timedOutAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
985
1109
|
const effectiveFinalError = timedOutAfterAcceptance
|
|
986
1110
|
? ctx.timeoutMessage ?? "Subagent timed out."
|
|
987
|
-
:
|
|
988
|
-
?
|
|
989
|
-
:
|
|
1111
|
+
: turnBudgetExceeded
|
|
1112
|
+
? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
|
|
1113
|
+
: acceptanceCanFailRun
|
|
1114
|
+
? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
|
|
1115
|
+
: finalResult?.error;
|
|
990
1116
|
|
|
991
1117
|
if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
|
|
992
1118
|
if (ctx.artifactConfig?.includeOutput !== false) {
|
|
@@ -1003,6 +1129,8 @@ async function runSingleStep(
|
|
|
1003
1129
|
model: finalResult?.model,
|
|
1004
1130
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
1005
1131
|
modelAttempts,
|
|
1132
|
+
...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
|
|
1133
|
+
transcriptError: transcriptWriter?.getError(),
|
|
1006
1134
|
skills: step.skills,
|
|
1007
1135
|
timestamp: Date.now(),
|
|
1008
1136
|
}, null, 2),
|
|
@@ -1023,12 +1151,19 @@ async function runSingleStep(
|
|
|
1023
1151
|
modelAttempts,
|
|
1024
1152
|
totalCost: costSummaryFromAttempts(modelAttempts),
|
|
1025
1153
|
artifactPaths,
|
|
1026
|
-
|
|
1154
|
+
transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
|
|
1155
|
+
transcriptError: transcriptWriter?.getError(),
|
|
1156
|
+
interrupted: timedOutAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
|
|
1027
1157
|
timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
|
|
1158
|
+
turnBudget,
|
|
1159
|
+
turnBudgetExceeded: turnBudgetExceeded || undefined,
|
|
1160
|
+
wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
|
|
1161
|
+
toolBudget,
|
|
1162
|
+
toolBudgetBlocked: toolBudgetBlocked || undefined,
|
|
1028
1163
|
completionGuardTriggered: completionGuardTriggeredFinal,
|
|
1029
|
-
structuredOutput: timedOutAfterAcceptance ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
1030
|
-
structuredOutputPath: timedOutAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
|
|
1031
|
-
structuredOutputSchemaPath: timedOutAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
|
|
1164
|
+
structuredOutput: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
|
|
1165
|
+
structuredOutputPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
|
|
1166
|
+
structuredOutputSchemaPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
|
|
1032
1167
|
acceptance: effectiveAcceptance,
|
|
1033
1168
|
};
|
|
1034
1169
|
}
|
|
@@ -1158,6 +1293,23 @@ function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { pa
|
|
|
1158
1293
|
writeInitialProgressFile(cwd);
|
|
1159
1294
|
}
|
|
1160
1295
|
|
|
1296
|
+
function resolveAsyncStepTranscriptPath(input: {
|
|
1297
|
+
artifactsDir?: string;
|
|
1298
|
+
artifactConfig?: Partial<ArtifactConfig>;
|
|
1299
|
+
runId: string;
|
|
1300
|
+
agent: string;
|
|
1301
|
+
flatIndex: number;
|
|
1302
|
+
flatStepCount: number;
|
|
1303
|
+
}): string | undefined {
|
|
1304
|
+
if (!input.artifactsDir || input.artifactConfig?.enabled === false || input.artifactConfig?.includeTranscript === false) return undefined;
|
|
1305
|
+
return getArtifactPaths(
|
|
1306
|
+
input.artifactsDir,
|
|
1307
|
+
input.runId,
|
|
1308
|
+
input.agent,
|
|
1309
|
+
input.flatStepCount > 1 ? input.flatIndex : undefined,
|
|
1310
|
+
).transcriptPath;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1161
1313
|
type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
|
|
1162
1314
|
|
|
1163
1315
|
async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
@@ -1176,16 +1328,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1176
1328
|
const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
1177
1329
|
const activeChildInterrupts = new Map<number, () => void>();
|
|
1178
1330
|
const activeChildTimeouts = new Map<number, () => void>();
|
|
1331
|
+
const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
|
|
1332
|
+
const pendingStepSteers: SteerRequest[] = [];
|
|
1179
1333
|
let interrupted = false;
|
|
1180
1334
|
let currentActivityState: ActivityState | undefined;
|
|
1181
1335
|
let activityTimer: NodeJS.Timeout | undefined;
|
|
1182
1336
|
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
1183
1337
|
let timedOut = false;
|
|
1338
|
+
let turnBudgetExceeded = false;
|
|
1184
1339
|
const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
|
|
1185
1340
|
const timeoutAbortController = new AbortController();
|
|
1186
1341
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
1187
1342
|
let latestSessionFile: string | undefined;
|
|
1188
1343
|
|
|
1344
|
+
const flatSteps = flattenSteps(steps);
|
|
1345
|
+
const initialFlatStepCount = flatSteps.length;
|
|
1189
1346
|
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
1190
1347
|
const initialStatusSteps: RunnerStatusStep[] = [];
|
|
1191
1348
|
let flatStepCount = 0;
|
|
@@ -1194,6 +1351,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1194
1351
|
if (isParallelGroup(step)) {
|
|
1195
1352
|
parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
|
|
1196
1353
|
for (const task of step.parallel) {
|
|
1354
|
+
const taskFlatIndex = flatStepCount;
|
|
1355
|
+
const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount });
|
|
1197
1356
|
initialStatusSteps.push({
|
|
1198
1357
|
agent: task.agent,
|
|
1199
1358
|
phase: task.phase,
|
|
@@ -1201,7 +1360,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1201
1360
|
outputName: task.outputName,
|
|
1202
1361
|
structured: task.structured,
|
|
1203
1362
|
status: "pending",
|
|
1363
|
+
...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
|
|
1204
1364
|
...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
|
|
1365
|
+
...(transcriptPath ? { transcriptPath } : {}),
|
|
1205
1366
|
skills: task.skills,
|
|
1206
1367
|
model: task.model,
|
|
1207
1368
|
thinking: task.thinking,
|
|
@@ -1209,8 +1370,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1209
1370
|
recentTools: [],
|
|
1210
1371
|
recentOutput: [],
|
|
1211
1372
|
});
|
|
1373
|
+
flatStepCount++;
|
|
1212
1374
|
}
|
|
1213
|
-
flatStepCount += step.parallel.length;
|
|
1214
1375
|
} else if (isDynamicRunnerGroup(step)) {
|
|
1215
1376
|
parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
|
|
1216
1377
|
initialStatusSteps.push({
|
|
@@ -1220,11 +1381,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1220
1381
|
outputName: step.collect.as,
|
|
1221
1382
|
structured: Boolean(step.collect.outputSchema),
|
|
1222
1383
|
status: "pending",
|
|
1384
|
+
...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
|
|
1223
1385
|
recentTools: [],
|
|
1224
1386
|
recentOutput: [],
|
|
1225
1387
|
});
|
|
1226
1388
|
flatStepCount++;
|
|
1227
1389
|
} else {
|
|
1390
|
+
const stepFlatIndex = flatStepCount;
|
|
1391
|
+
const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount });
|
|
1228
1392
|
initialStatusSteps.push({
|
|
1229
1393
|
agent: step.agent,
|
|
1230
1394
|
phase: step.phase,
|
|
@@ -1232,7 +1396,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1232
1396
|
outputName: step.outputName,
|
|
1233
1397
|
structured: step.structured,
|
|
1234
1398
|
status: "pending",
|
|
1399
|
+
...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
|
|
1235
1400
|
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
1401
|
+
...(transcriptPath ? { transcriptPath } : {}),
|
|
1236
1402
|
skills: step.skills,
|
|
1237
1403
|
model: step.model,
|
|
1238
1404
|
thinking: step.thinking,
|
|
@@ -1243,7 +1409,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1243
1409
|
flatStepCount++;
|
|
1244
1410
|
}
|
|
1245
1411
|
}
|
|
1246
|
-
const flatSteps = flattenSteps(steps);
|
|
1247
1412
|
const sessionEnabled = Boolean(config.sessionDir)
|
|
1248
1413
|
|| shareEnabled
|
|
1249
1414
|
|| flatSteps.some((step) => Boolean(step.sessionFile));
|
|
@@ -1258,6 +1423,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1258
1423
|
lastUpdate: overallStartTime,
|
|
1259
1424
|
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
1260
1425
|
...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
|
|
1426
|
+
...(config.turnBudget ? { turnBudget: initialTurnBudgetState(config.turnBudget) } : {}),
|
|
1427
|
+
...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
|
|
1261
1428
|
pid: process.pid,
|
|
1262
1429
|
cwd,
|
|
1263
1430
|
currentStep: 0,
|
|
@@ -1345,6 +1512,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1345
1512
|
activeChildTimeouts.set(flatIndex, interrupt);
|
|
1346
1513
|
if (timedOut) interrupt();
|
|
1347
1514
|
};
|
|
1515
|
+
const registerStepTurnBudgetAbort = (flatIndex: number, abort: ((message: string, state?: TurnBudgetState) => void) | undefined): void => {
|
|
1516
|
+
if (!abort) {
|
|
1517
|
+
activeChildTurnBudgetAborts.delete(flatIndex);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
activeChildTurnBudgetAborts.set(flatIndex, abort);
|
|
1521
|
+
};
|
|
1348
1522
|
const interruptActiveChildren = (): void => {
|
|
1349
1523
|
for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
|
|
1350
1524
|
};
|
|
@@ -1563,6 +1737,58 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1563
1737
|
appendControlEvent(event);
|
|
1564
1738
|
return true;
|
|
1565
1739
|
};
|
|
1740
|
+
const deliverSteerRequest = (request: SteerRequest): void => {
|
|
1741
|
+
if (statusPayload.state !== "running") return;
|
|
1742
|
+
const runningIndexes = statusPayload.steps
|
|
1743
|
+
.map((step, index) => ({ step, index }))
|
|
1744
|
+
.filter(({ step }) => step.status === "running")
|
|
1745
|
+
.map(({ index }) => index);
|
|
1746
|
+
const targets = request.targetIndex !== undefined ? [request.targetIndex] : runningIndexes;
|
|
1747
|
+
const now = Date.now();
|
|
1748
|
+
const accepted: number[] = [];
|
|
1749
|
+
const rejected: Array<{ index: number; reason: string }> = [];
|
|
1750
|
+
for (const index of targets) {
|
|
1751
|
+
const step = statusPayload.steps[index];
|
|
1752
|
+
if (!step) {
|
|
1753
|
+
rejected.push({ index, reason: "child index out of range" });
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
if (step.status !== "running") {
|
|
1757
|
+
rejected.push({ index, reason: `child is ${step.status}` });
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
enqueueStepSteer(asyncDir, index, request);
|
|
1761
|
+
step.steerCount = (step.steerCount ?? 0) + 1;
|
|
1762
|
+
step.lastSteerAt = now;
|
|
1763
|
+
accepted.push(index);
|
|
1764
|
+
}
|
|
1765
|
+
if (accepted.length > 0) {
|
|
1766
|
+
statusPayload.steerCount = (statusPayload.steerCount ?? 0) + accepted.length;
|
|
1767
|
+
statusPayload.lastSteerAt = now;
|
|
1768
|
+
statusPayload.lastUpdate = now;
|
|
1769
|
+
writeStatusPayload();
|
|
1770
|
+
}
|
|
1771
|
+
appendJsonl(eventsPath, JSON.stringify({
|
|
1772
|
+
type: "subagent.steer.requested",
|
|
1773
|
+
ts: now,
|
|
1774
|
+
runId: id,
|
|
1775
|
+
requestId: request.id,
|
|
1776
|
+
message: request.message,
|
|
1777
|
+
...(request.source ? { source: request.source } : {}),
|
|
1778
|
+
...(request.targetIndex !== undefined ? { targetIndex: request.targetIndex } : {}),
|
|
1779
|
+
acceptedIndexes: accepted,
|
|
1780
|
+
...(rejected.length ? { rejected } : {}),
|
|
1781
|
+
}));
|
|
1782
|
+
};
|
|
1783
|
+
const flushPendingStepSteers = (flatIndex: number): void => {
|
|
1784
|
+
const remaining: SteerRequest[] = [];
|
|
1785
|
+
for (const request of pendingStepSteers.splice(0)) {
|
|
1786
|
+
if (request.targetIndex === undefined) deliverSteerRequest({ ...request, targetIndex: flatIndex });
|
|
1787
|
+
else if (request.targetIndex === flatIndex) deliverSteerRequest(request);
|
|
1788
|
+
else remaining.push(request);
|
|
1789
|
+
}
|
|
1790
|
+
pendingStepSteers.push(...remaining);
|
|
1791
|
+
};
|
|
1566
1792
|
const updateStepModel = (flatIndex: number, model: string | undefined, thinking: string | undefined, now = Date.now()): void => {
|
|
1567
1793
|
const step = statusPayload.steps[flatIndex];
|
|
1568
1794
|
if (!step) return;
|
|
@@ -1571,6 +1797,40 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1571
1797
|
statusPayload.lastUpdate = now;
|
|
1572
1798
|
writeStatusPayload();
|
|
1573
1799
|
};
|
|
1800
|
+
const updateStepTurnBudget = (flatIndex: number, turnCount: number, now: number, terminalAssistantStop: boolean): void => {
|
|
1801
|
+
const budget = config.turnBudget;
|
|
1802
|
+
const step = statusPayload.steps[flatIndex];
|
|
1803
|
+
if (!budget || !step || timedOut || turnBudgetExceeded || step.turnBudgetExceeded) return;
|
|
1804
|
+
if (turnCount < budget.maxTurns) {
|
|
1805
|
+
const state: TurnBudgetState = { ...budget, outcome: "within-budget", turnCount };
|
|
1806
|
+
step.turnBudget = state;
|
|
1807
|
+
statusPayload.turnBudget = state;
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const state = turnBudgetState(budget, turnCount, false);
|
|
1811
|
+
step.turnBudget = state;
|
|
1812
|
+
statusPayload.turnBudget = state;
|
|
1813
|
+
if (!step.wrapUpRequested) {
|
|
1814
|
+
step.wrapUpRequested = true;
|
|
1815
|
+
statusPayload.wrapUpRequested = true;
|
|
1816
|
+
appendRecentStepOutput(step, [turnBudgetSoftNote(budget, turnCount)]);
|
|
1817
|
+
}
|
|
1818
|
+
if (!shouldAbortForTurnBudget(budget, turnCount, terminalAssistantStop)) return;
|
|
1819
|
+
const exceededState = turnBudgetState(budget, turnCount, true);
|
|
1820
|
+
const message = turnBudgetExceededMessage(budget, turnCount);
|
|
1821
|
+
step.turnBudget = exceededState;
|
|
1822
|
+
step.turnBudgetExceeded = true;
|
|
1823
|
+
step.wrapUpRequested = true;
|
|
1824
|
+
step.error = message;
|
|
1825
|
+
turnBudgetExceeded = true;
|
|
1826
|
+
statusPayload.turnBudget = exceededState;
|
|
1827
|
+
statusPayload.turnBudgetExceeded = true;
|
|
1828
|
+
statusPayload.wrapUpRequested = true;
|
|
1829
|
+
statusPayload.error = message;
|
|
1830
|
+
statusPayload.lastUpdate = now;
|
|
1831
|
+
appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.turn_budget_exceeded", ts: now, runId: id, stepIndex: flatIndex, agent: step.agent, turnCount, maxTurns: budget.maxTurns, graceTurns: budget.graceTurns, message }));
|
|
1832
|
+
activeChildTurnBudgetAborts.get(flatIndex)?.(message, exceededState);
|
|
1833
|
+
};
|
|
1574
1834
|
const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
|
|
1575
1835
|
const step = statusPayload.steps[flatIndex];
|
|
1576
1836
|
if (!step) return;
|
|
@@ -1580,6 +1840,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1580
1840
|
const mutates = isMutatingTool(event.toolName, event.args);
|
|
1581
1841
|
const currentPath = resolveCurrentPath(event.toolName, event.args);
|
|
1582
1842
|
step.toolCount = (step.toolCount ?? 0) + 1;
|
|
1843
|
+
const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
|
|
1844
|
+
if (configuredToolBudget) {
|
|
1845
|
+
step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount);
|
|
1846
|
+
statusPayload.toolBudget = step.toolBudget;
|
|
1847
|
+
}
|
|
1583
1848
|
step.currentTool = event.toolName;
|
|
1584
1849
|
step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
|
|
1585
1850
|
step.currentToolStartedAt = now;
|
|
@@ -1601,6 +1866,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1601
1866
|
const toolSnapshot = pendingToolResults[flatIndex];
|
|
1602
1867
|
pendingToolResults[flatIndex] = undefined;
|
|
1603
1868
|
const resultText = extractTextFromContent(event.message.content);
|
|
1869
|
+
if (toolSnapshot && resultText.includes("Tool budget hard limit reached")) {
|
|
1870
|
+
const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
|
|
1871
|
+
if (configuredToolBudget) {
|
|
1872
|
+
step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount ?? 0, toolSnapshot.tool);
|
|
1873
|
+
step.toolBudgetBlocked = true;
|
|
1874
|
+
statusPayload.toolBudget = step.toolBudget;
|
|
1875
|
+
statusPayload.toolBudgetBlocked = true;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1604
1878
|
appendRecentStepOutput(step, resultText.split("\n").slice(-10));
|
|
1605
1879
|
if (toolSnapshot?.mutates && didMutatingToolFail(resultText)) {
|
|
1606
1880
|
const state = mutatingFailureStates[flatIndex]!;
|
|
@@ -1651,6 +1925,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1651
1925
|
statusPayload.totalTokens = { input: totalInput + input, output: totalOutput + output, total: totalInput + totalOutput + input + output };
|
|
1652
1926
|
}
|
|
1653
1927
|
statusPayload.turnCount = Math.max(statusPayload.turnCount ?? 0, step.turnCount);
|
|
1928
|
+
updateStepTurnBudget(flatIndex, step.turnCount, now, isTerminalAssistantStop(event.message));
|
|
1654
1929
|
}
|
|
1655
1930
|
syncTopLevelCurrentTool();
|
|
1656
1931
|
step.lastActivityAt = now;
|
|
@@ -1787,10 +2062,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1787
2062
|
timeoutActiveChildren();
|
|
1788
2063
|
};
|
|
1789
2064
|
process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
|
|
1790
|
-
// Portable control inbox: the parent drops
|
|
1791
|
-
// it cannot deliver
|
|
1792
|
-
//
|
|
1793
|
-
const disposeControlInbox = watchAsyncControlInbox(asyncDir, {
|
|
2065
|
+
// Portable control inbox: the parent drops control request files here when
|
|
2066
|
+
// it cannot deliver OS signals (e.g. ENOSYS on Windows) or when steering a
|
|
2067
|
+
// live child. Interrupts still route into the same graceful interruptRunner().
|
|
2068
|
+
const disposeControlInbox = watchAsyncControlInbox(asyncDir, {
|
|
2069
|
+
onInterrupt: interruptRunner,
|
|
2070
|
+
onTimeout: timeoutRunner,
|
|
2071
|
+
onSteer: (request) => {
|
|
2072
|
+
const targetStep = request.targetIndex !== undefined ? statusPayload.steps[request.targetIndex] : undefined;
|
|
2073
|
+
if (targetStep?.status === "pending") pendingStepSteers.push(request);
|
|
2074
|
+
else if (request.targetIndex !== undefined || statusPayload.steps.some((step) => step.status === "running")) deliverSteerRequest(request);
|
|
2075
|
+
else pendingStepSteers.push(request);
|
|
2076
|
+
},
|
|
2077
|
+
});
|
|
1794
2078
|
if (config.deadlineAt !== undefined) {
|
|
1795
2079
|
const remainingMs = Math.max(0, config.deadlineAt - Date.now());
|
|
1796
2080
|
timeoutTimer = setTimeout(timeoutRunner, remainingMs);
|
|
@@ -1813,7 +2097,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1813
2097
|
let stepCursor = 0;
|
|
1814
2098
|
|
|
1815
2099
|
while (true) {
|
|
1816
|
-
if (interrupted || timedOut) break;
|
|
2100
|
+
if (interrupted || timedOut || turnBudgetExceeded) break;
|
|
1817
2101
|
consumePendingAppendRequests();
|
|
1818
2102
|
if (stepCursor >= steps.length) break;
|
|
1819
2103
|
const stepIndex = stepCursor++;
|
|
@@ -1923,7 +2207,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1923
2207
|
structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
|
|
1924
2208
|
};
|
|
1925
2209
|
});
|
|
1926
|
-
const
|
|
2210
|
+
const dynamicFlatStepCount = Math.max(statusPayload.steps.length - 1 + dynamicSteps.length, 1);
|
|
2211
|
+
const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task, itemIndex) => {
|
|
2212
|
+
const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount });
|
|
2213
|
+
return {
|
|
1927
2214
|
agent: task.agent,
|
|
1928
2215
|
phase: task.phase ?? step.phase,
|
|
1929
2216
|
label: task.label,
|
|
@@ -1931,13 +2218,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1931
2218
|
structured: Boolean(task.structuredOutputSchema),
|
|
1932
2219
|
status: "pending",
|
|
1933
2220
|
...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
|
|
2221
|
+
...(transcriptPath ? { transcriptPath } : {}),
|
|
1934
2222
|
skills: task.skills,
|
|
1935
2223
|
model: task.model,
|
|
1936
2224
|
thinking: task.thinking,
|
|
1937
2225
|
attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
|
|
1938
2226
|
recentTools: [],
|
|
1939
2227
|
recentOutput: [],
|
|
1940
|
-
}
|
|
2228
|
+
};
|
|
2229
|
+
});
|
|
1941
2230
|
statusPayload.steps.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps);
|
|
1942
2231
|
if (config.childIntercomTargets) {
|
|
1943
2232
|
config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
|
|
@@ -2013,6 +2302,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2013
2302
|
statusPayload.lastUpdate = taskStartTime;
|
|
2014
2303
|
writeStatusPayload();
|
|
2015
2304
|
appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
|
|
2305
|
+
flushPendingStepSteers(fi);
|
|
2016
2306
|
const singleResult = await runSingleStep(task, {
|
|
2017
2307
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
2018
2308
|
outputs,
|
|
@@ -2020,6 +2310,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2020
2310
|
artifactsDir, artifactConfig, id,
|
|
2021
2311
|
flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
2022
2312
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
2313
|
+
steerInboxDir: stepSteerInboxDir(asyncDir, fi),
|
|
2023
2314
|
piPackageRoot: config.piPackageRoot,
|
|
2024
2315
|
piArgv1: config.piArgv1,
|
|
2025
2316
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
@@ -2027,8 +2318,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2027
2318
|
nestedRoute: config.nestedRoute,
|
|
2028
2319
|
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2029
2320
|
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
2321
|
+
registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
|
|
2030
2322
|
timeoutSignal: timeoutAbortController.signal,
|
|
2031
2323
|
timeoutMessage,
|
|
2324
|
+
turnBudget: config.turnBudget,
|
|
2032
2325
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
2033
2326
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2034
2327
|
skipAcceptance: () => timedOut,
|
|
@@ -2040,12 +2333,24 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2040
2333
|
statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
|
|
2041
2334
|
statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2042
2335
|
statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
2336
|
+
statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
|
|
2337
|
+
statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
|
|
2338
|
+
statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
|
|
2339
|
+
statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
|
|
2340
|
+
statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
|
|
2341
|
+
if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
|
|
2342
|
+
if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
|
|
2343
|
+
if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
|
|
2344
|
+
if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
|
|
2345
|
+
if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
|
|
2043
2346
|
statusPayload.steps[fi].model = singleResult.model;
|
|
2044
2347
|
statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
|
|
2045
2348
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
2046
2349
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
2047
2350
|
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2048
2351
|
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2352
|
+
statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
|
|
2353
|
+
statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
|
|
2049
2354
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
2050
2355
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2051
2356
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -2072,6 +2377,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2072
2377
|
skipped: pr.skipped,
|
|
2073
2378
|
interrupted: pr.interrupted,
|
|
2074
2379
|
timedOut: pr.timedOut,
|
|
2380
|
+
turnBudget: pr.turnBudget,
|
|
2381
|
+
turnBudgetExceeded: pr.turnBudgetExceeded,
|
|
2382
|
+
wrapUpRequested: pr.wrapUpRequested,
|
|
2383
|
+
toolBudget: pr.toolBudget,
|
|
2384
|
+
toolBudgetBlocked: pr.toolBudgetBlocked,
|
|
2075
2385
|
sessionFile: pr.sessionFile,
|
|
2076
2386
|
intercomTarget: pr.intercomTarget,
|
|
2077
2387
|
model: pr.model,
|
|
@@ -2079,6 +2389,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2079
2389
|
modelAttempts: pr.modelAttempts,
|
|
2080
2390
|
totalCost: pr.totalCost,
|
|
2081
2391
|
artifactPaths: pr.artifactPaths,
|
|
2392
|
+
transcriptPath: pr.transcriptPath,
|
|
2393
|
+
transcriptError: pr.transcriptError,
|
|
2082
2394
|
structuredOutput: pr.structuredOutput,
|
|
2083
2395
|
structuredOutputPath: pr.structuredOutputPath,
|
|
2084
2396
|
structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
|
|
@@ -2276,6 +2588,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2276
2588
|
? path.join(config.sessionDir, `parallel-${taskIdx}`)
|
|
2277
2589
|
: undefined;
|
|
2278
2590
|
const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
|
|
2591
|
+
flushPendingStepSteers(fi);
|
|
2279
2592
|
|
|
2280
2593
|
const singleResult = await runSingleStep(taskForRun, {
|
|
2281
2594
|
previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
|
|
@@ -2284,6 +2597,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2284
2597
|
artifactsDir, artifactConfig, id,
|
|
2285
2598
|
flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
2286
2599
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
2600
|
+
steerInboxDir: stepSteerInboxDir(asyncDir, fi),
|
|
2287
2601
|
piPackageRoot: config.piPackageRoot,
|
|
2288
2602
|
piArgv1: config.piArgv1,
|
|
2289
2603
|
childIntercomTarget: config.childIntercomTargets?.[fi],
|
|
@@ -2291,8 +2605,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2291
2605
|
nestedRoute: config.nestedRoute,
|
|
2292
2606
|
registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
|
|
2293
2607
|
registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
|
|
2608
|
+
registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
|
|
2294
2609
|
timeoutSignal: timeoutAbortController.signal,
|
|
2295
2610
|
timeoutMessage,
|
|
2611
|
+
turnBudget: config.turnBudget,
|
|
2296
2612
|
onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
|
|
2297
2613
|
onChildEvent: (event) => updateStepFromChildEvent(fi, event),
|
|
2298
2614
|
skipAcceptance: () => timedOut,
|
|
@@ -2310,12 +2626,24 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2310
2626
|
statusPayload.steps[fi].durationMs = taskDuration;
|
|
2311
2627
|
statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2312
2628
|
statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
2629
|
+
statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
|
|
2630
|
+
statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
|
|
2631
|
+
statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
|
|
2632
|
+
statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
|
|
2633
|
+
statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
|
|
2634
|
+
if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
|
|
2635
|
+
if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
|
|
2636
|
+
if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
|
|
2637
|
+
if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
|
|
2638
|
+
if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
|
|
2313
2639
|
statusPayload.steps[fi].model = singleResult.model;
|
|
2314
2640
|
statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
|
|
2315
2641
|
statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
|
|
2316
2642
|
statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
|
|
2317
2643
|
statusPayload.steps[fi].totalCost = singleResult.totalCost;
|
|
2318
2644
|
statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2645
|
+
statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
|
|
2646
|
+
statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
|
|
2319
2647
|
statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
|
|
2320
2648
|
statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2321
2649
|
statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -2378,6 +2706,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2378
2706
|
skipped: pr.skipped,
|
|
2379
2707
|
interrupted: pr.interrupted,
|
|
2380
2708
|
timedOut: pr.timedOut,
|
|
2709
|
+
turnBudget: pr.turnBudget,
|
|
2710
|
+
turnBudgetExceeded: pr.turnBudgetExceeded,
|
|
2711
|
+
wrapUpRequested: pr.wrapUpRequested,
|
|
2712
|
+
toolBudget: pr.toolBudget,
|
|
2713
|
+
toolBudgetBlocked: pr.toolBudgetBlocked,
|
|
2381
2714
|
sessionFile: pr.sessionFile,
|
|
2382
2715
|
intercomTarget: pr.intercomTarget,
|
|
2383
2716
|
model: pr.model,
|
|
@@ -2385,6 +2718,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2385
2718
|
modelAttempts: pr.modelAttempts,
|
|
2386
2719
|
totalCost: pr.totalCost,
|
|
2387
2720
|
artifactPaths: pr.artifactPaths,
|
|
2721
|
+
transcriptPath: pr.transcriptPath,
|
|
2722
|
+
transcriptError: pr.transcriptError,
|
|
2388
2723
|
structuredOutput: pr.structuredOutput,
|
|
2389
2724
|
structuredOutputPath: pr.structuredOutputPath,
|
|
2390
2725
|
structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
|
|
@@ -2451,6 +2786,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2451
2786
|
agent: seqStep.agent,
|
|
2452
2787
|
}));
|
|
2453
2788
|
|
|
2789
|
+
flushPendingStepSteers(flatIndex);
|
|
2454
2790
|
const singleResult = await runSingleStep(seqStep, {
|
|
2455
2791
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
2456
2792
|
outputs,
|
|
@@ -2458,6 +2794,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2458
2794
|
artifactsDir, artifactConfig, id,
|
|
2459
2795
|
flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
|
|
2460
2796
|
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
2797
|
+
steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
|
|
2461
2798
|
piPackageRoot: config.piPackageRoot,
|
|
2462
2799
|
piArgv1: config.piArgv1,
|
|
2463
2800
|
childIntercomTarget: config.childIntercomTargets?.[flatIndex],
|
|
@@ -2465,8 +2802,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2465
2802
|
nestedRoute: config.nestedRoute,
|
|
2466
2803
|
registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
|
|
2467
2804
|
registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
|
|
2805
|
+
registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(flatIndex, abort),
|
|
2468
2806
|
timeoutSignal: timeoutAbortController.signal,
|
|
2469
2807
|
timeoutMessage,
|
|
2808
|
+
turnBudget: config.turnBudget,
|
|
2470
2809
|
onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
|
|
2471
2810
|
onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
|
|
2472
2811
|
skipAcceptance: () => timedOut,
|
|
@@ -2489,12 +2828,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2489
2828
|
modelAttempts: singleResult.modelAttempts,
|
|
2490
2829
|
totalCost: singleResult.totalCost,
|
|
2491
2830
|
artifactPaths: singleResult.artifactPaths,
|
|
2831
|
+
transcriptPath: singleResult.transcriptPath,
|
|
2832
|
+
transcriptError: singleResult.transcriptError,
|
|
2492
2833
|
structuredOutput: singleResult.structuredOutput,
|
|
2493
2834
|
structuredOutputPath: singleResult.structuredOutputPath,
|
|
2494
2835
|
structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
|
|
2495
2836
|
acceptance: singleResult.acceptance,
|
|
2496
2837
|
interrupted: singleResult.interrupted,
|
|
2497
2838
|
timedOut: timedOut || singleResult.timedOut ? true : undefined,
|
|
2839
|
+
turnBudget: singleResult.turnBudget,
|
|
2840
|
+
turnBudgetExceeded: singleResult.turnBudgetExceeded,
|
|
2841
|
+
wrapUpRequested: singleResult.wrapUpRequested,
|
|
2842
|
+
toolBudget: singleResult.toolBudget,
|
|
2843
|
+
toolBudgetBlocked: singleResult.toolBudgetBlocked,
|
|
2498
2844
|
});
|
|
2499
2845
|
if (seqStep.outputName) {
|
|
2500
2846
|
outputs[seqStep.outputName] = outputEntryFromAsyncResult({
|
|
@@ -2533,12 +2879,24 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2533
2879
|
statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
|
|
2534
2880
|
statusPayload.steps[flatIndex].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
|
|
2535
2881
|
statusPayload.steps[flatIndex].timedOut = timedOut || singleResult.timedOut ? true : undefined;
|
|
2882
|
+
statusPayload.steps[flatIndex].turnBudget = singleResult.turnBudget;
|
|
2883
|
+
statusPayload.steps[flatIndex].turnBudgetExceeded = singleResult.turnBudgetExceeded;
|
|
2884
|
+
statusPayload.steps[flatIndex].wrapUpRequested = singleResult.wrapUpRequested;
|
|
2885
|
+
statusPayload.steps[flatIndex].toolBudget = singleResult.toolBudget;
|
|
2886
|
+
statusPayload.steps[flatIndex].toolBudgetBlocked = singleResult.toolBudgetBlocked;
|
|
2887
|
+
if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
|
|
2888
|
+
if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
|
|
2889
|
+
if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
|
|
2890
|
+
if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
|
|
2891
|
+
if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
|
|
2536
2892
|
statusPayload.steps[flatIndex].model = singleResult.model;
|
|
2537
2893
|
statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
|
|
2538
2894
|
statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
|
|
2539
2895
|
statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
|
|
2540
2896
|
statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
|
|
2541
2897
|
statusPayload.steps[flatIndex].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
|
|
2898
|
+
statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
|
|
2899
|
+
statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
|
|
2542
2900
|
statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
|
|
2543
2901
|
statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
|
|
2544
2902
|
statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
|
|
@@ -2648,12 +3006,16 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2648
3006
|
disposeControlInbox();
|
|
2649
3007
|
const effectiveSessionFile = sessionFile ?? latestSessionFile;
|
|
2650
3008
|
const runEndedAt = Date.now();
|
|
2651
|
-
statusPayload.state = timedOut ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
|
|
3009
|
+
statusPayload.state = timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
|
|
2652
3010
|
statusPayload.activityState = undefined;
|
|
2653
3011
|
if (timedOut) {
|
|
2654
3012
|
statusPayload.timedOut = true;
|
|
2655
3013
|
statusPayload.error = timeoutMessage ?? "Subagent timed out.";
|
|
2656
3014
|
}
|
|
3015
|
+
if (turnBudgetExceeded && !statusPayload.error) {
|
|
3016
|
+
const budget = statusPayload.turnBudget;
|
|
3017
|
+
statusPayload.error = budget ? turnBudgetExceededMessage(budget, budget.turnCount) : "Subagent exceeded turn budget.";
|
|
3018
|
+
}
|
|
2657
3019
|
statusPayload.endedAt = runEndedAt;
|
|
2658
3020
|
statusPayload.lastUpdate = runEndedAt;
|
|
2659
3021
|
statusPayload.sessionFile = effectiveSessionFile;
|
|
@@ -2706,12 +3068,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2706
3068
|
id,
|
|
2707
3069
|
agent: agentName,
|
|
2708
3070
|
mode: resultMode,
|
|
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,
|
|
3071
|
+
success: !timedOut && !turnBudgetExceeded && !interrupted && results.every((r) => r.success),
|
|
3072
|
+
state: timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
|
|
3073
|
+
summary: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
|
|
2712
3074
|
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
2713
3075
|
...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
|
|
2714
|
-
...(
|
|
3076
|
+
...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
|
|
3077
|
+
...(statusPayload.turnBudgetExceeded ? { turnBudgetExceeded: true } : {}),
|
|
3078
|
+
...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
|
|
3079
|
+
...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
|
|
3080
|
+
...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
|
|
3081
|
+
...(timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
|
|
2715
3082
|
results: results.map((r) => ({
|
|
2716
3083
|
agent: r.agent,
|
|
2717
3084
|
output: r.output,
|
|
@@ -2720,6 +3087,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2720
3087
|
skipped: r.skipped || undefined,
|
|
2721
3088
|
interrupted: r.interrupted || undefined,
|
|
2722
3089
|
timedOut: r.timedOut || undefined,
|
|
3090
|
+
turnBudget: r.turnBudget,
|
|
3091
|
+
turnBudgetExceeded: r.turnBudgetExceeded || undefined,
|
|
3092
|
+
wrapUpRequested: r.wrapUpRequested || undefined,
|
|
3093
|
+
toolBudget: r.toolBudget,
|
|
3094
|
+
toolBudgetBlocked: r.toolBudgetBlocked || undefined,
|
|
2723
3095
|
sessionFile: r.sessionFile,
|
|
2724
3096
|
intercomTarget: r.intercomTarget,
|
|
2725
3097
|
model: r.model,
|
|
@@ -2728,6 +3100,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2728
3100
|
totalCost: r.totalCost,
|
|
2729
3101
|
artifactPaths: r.artifactPaths,
|
|
2730
3102
|
truncated: r.truncated,
|
|
3103
|
+
transcriptPath: r.transcriptPath,
|
|
3104
|
+
transcriptError: r.transcriptError,
|
|
2731
3105
|
structuredOutput: r.structuredOutput,
|
|
2732
3106
|
structuredOutputPath: r.structuredOutputPath,
|
|
2733
3107
|
structuredOutputSchemaPath: r.structuredOutputSchemaPath,
|
|
@@ -2735,7 +3109,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2735
3109
|
})),
|
|
2736
3110
|
outputs,
|
|
2737
3111
|
workflowGraph: statusPayload.workflowGraph,
|
|
2738
|
-
exitCode: timedOut ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
3112
|
+
exitCode: timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
2739
3113
|
timestamp: runEndedAt,
|
|
2740
3114
|
durationMs: runEndedAt - overallStartTime,
|
|
2741
3115
|
totalTokens: statusPayload.totalTokens,
|