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.
Files changed (52) hide show
  1. package/CHANGELOG.md +30 -3
  2. package/README.md +147 -58
  3. package/install.mjs +2 -1
  4. package/package.json +3 -2
  5. package/skills/pi-subagents/SKILL.md +70 -14
  6. package/src/agents/agent-management.ts +177 -5
  7. package/src/agents/agent-memory.ts +254 -0
  8. package/src/agents/agent-serializer.ts +11 -0
  9. package/src/agents/agents.ts +142 -12
  10. package/src/agents/chain-serializer.ts +27 -2
  11. package/src/extension/doctor.ts +1 -9
  12. package/src/extension/fanout-child.ts +2 -2
  13. package/src/extension/index.ts +65 -90
  14. package/src/extension/rpc.ts +369 -0
  15. package/src/extension/schemas.ts +52 -8
  16. package/src/extension/tool-description.ts +200 -0
  17. package/src/intercom/intercom-bridge.ts +21 -253
  18. package/src/intercom/native-supervisor-channel.ts +510 -0
  19. package/src/runs/background/async-execution.ts +51 -7
  20. package/src/runs/background/async-job-tracker.ts +12 -2
  21. package/src/runs/background/async-status.ts +27 -2
  22. package/src/runs/background/completion-batcher.ts +166 -0
  23. package/src/runs/background/control-channel.ts +106 -1
  24. package/src/runs/background/fleet-view.ts +515 -0
  25. package/src/runs/background/notify.ts +161 -44
  26. package/src/runs/background/result-watcher.ts +1 -2
  27. package/src/runs/background/run-id-resolver.ts +3 -2
  28. package/src/runs/background/run-status.ts +166 -6
  29. package/src/runs/background/scheduled-runs.ts +514 -0
  30. package/src/runs/background/subagent-runner.ts +409 -35
  31. package/src/runs/background/wait.ts +353 -0
  32. package/src/runs/foreground/chain-execution.ts +95 -21
  33. package/src/runs/foreground/execution.ts +150 -21
  34. package/src/runs/foreground/subagent-executor.ts +378 -64
  35. package/src/runs/shared/dynamic-fanout.ts +1 -1
  36. package/src/runs/shared/model-fallback.ts +167 -20
  37. package/src/runs/shared/model-scope.ts +128 -0
  38. package/src/runs/shared/nested-events.ts +31 -0
  39. package/src/runs/shared/parallel-utils.ts +1 -0
  40. package/src/runs/shared/pi-args.ts +30 -1
  41. package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
  42. package/src/runs/shared/tool-budget.ts +74 -0
  43. package/src/runs/shared/turn-budget.ts +52 -0
  44. package/src/shared/artifacts.ts +1 -0
  45. package/src/shared/atomic-json.ts +15 -2
  46. package/src/shared/child-transcript.ts +212 -0
  47. package/src/shared/settings.ts +3 -1
  48. package/src/shared/types.ts +134 -19
  49. package/src/slash/prompt-workflows.ts +330 -0
  50. package/src/slash/slash-commands.ts +16 -2
  51. package/src/tui/render.ts +16 -8
  52. 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 { consumeInterruptRequest, deliverInterruptRequest, deliverTimeoutRequest, watchAsyncControlInbox } from "./control-channel.ts";
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
- const stopReason = (event.message as { stopReason?: string }).stopReason;
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 effectiveAcceptance = timedOutAfterAcceptance ? undefined : acceptance;
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
- : acceptanceCanFailRun
988
- ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
989
- : finalResult?.error;
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
- interrupted: timedOutAfterAcceptance ? false : finalResult?.interrupted,
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 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 });
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 dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task) => ({
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
- ...(timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : {}),
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,