pi-subagents 0.38.0 → 0.40.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 (59) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +85 -13
  3. package/agents/worker.md +1 -0
  4. package/package.json +1 -1
  5. package/prompts/review-loop.md +1 -1
  6. package/skills/pi-subagents/SKILL.md +4 -3
  7. package/skills/pi-subagents/references/execution-controls.md +22 -3
  8. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -1
  9. package/skills/pi-subagents/references/prompting-and-roles.md +17 -3
  10. package/src/agents/agent-management.ts +71 -30
  11. package/src/agents/agent-serializer.ts +4 -0
  12. package/src/agents/agents.ts +60 -1
  13. package/src/api/preflight.ts +16 -12
  14. package/src/extension/chain-validation.ts +17 -1
  15. package/src/extension/rpc.ts +2 -0
  16. package/src/extension/schemas.ts +16 -3
  17. package/src/extension/tool-description.ts +6 -5
  18. package/src/intercom/result-intercom.ts +33 -4
  19. package/src/runs/background/async-execution.ts +63 -16
  20. package/src/runs/background/async-job-tracker.ts +5 -1
  21. package/src/runs/background/async-resume.ts +1 -0
  22. package/src/runs/background/async-status.ts +18 -2
  23. package/src/runs/background/chain-append.ts +48 -5
  24. package/src/runs/background/control-channel.ts +60 -0
  25. package/src/runs/background/notify.ts +26 -4
  26. package/src/runs/background/result-watcher.ts +20 -3
  27. package/src/runs/background/run-status.ts +29 -3
  28. package/src/runs/background/subagent-runner.ts +302 -18
  29. package/src/runs/foreground/async-stop-action.ts +65 -0
  30. package/src/runs/foreground/chain-execution.ts +143 -43
  31. package/src/runs/foreground/execution.ts +384 -202
  32. package/src/runs/foreground/foreground-control.ts +30 -0
  33. package/src/runs/foreground/subagent-executor.ts +314 -77
  34. package/src/runs/shared/capability-ceiling.ts +51 -19
  35. package/src/runs/shared/chain-outputs.ts +3 -1
  36. package/src/runs/shared/dynamic-fanout.ts +1 -1
  37. package/src/runs/shared/nested-events.ts +97 -20
  38. package/src/runs/shared/parallel-utils.ts +43 -12
  39. package/src/runs/shared/pi-args.ts +45 -3
  40. package/src/runs/shared/process-signal.ts +19 -0
  41. package/src/runs/shared/run-history.ts +45 -9
  42. package/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
  43. package/src/runs/shared/subagent-prompt-runtime.ts +30 -0
  44. package/src/runs/shared/usage-budget.ts +65 -0
  45. package/src/runs/shared/workflow-graph.ts +26 -1
  46. package/src/shared/settings.ts +17 -1
  47. package/src/shared/types.ts +124 -8
  48. package/src/shared/utils.ts +12 -1
  49. package/src/slash/slash-commands.ts +43 -1
  50. package/src/slash/slash-live-state.ts +5 -3
  51. package/src/tui/fleet-status.ts +12 -2
  52. package/src/tui/fleet.ts +181 -8
  53. package/src/tui/render.ts +4 -2
  54. package/src/watchdog/register-main.ts +14 -7
  55. package/src/watchdog/review.ts +4 -3
  56. package/src/watchdog/runtime.ts +170 -17
  57. package/src/watchdog/scope.ts +62 -0
  58. package/src/watchdog/settings.ts +41 -1
  59. package/src/watchdog/types.ts +10 -0
@@ -18,6 +18,8 @@ import {
18
18
  type AsyncStatus,
19
19
  type ChainOutputMap,
20
20
  type CostSummary,
21
+ type LaunchResolvedChildExtensionsV1,
22
+ type RuntimeAcknowledgedChildExtensionsV1,
21
23
  type ModelAttempt,
22
24
  type NestedRouteInfo,
23
25
  type NestedRunSummary,
@@ -25,6 +27,8 @@ import {
25
27
  type ResolvedTurnBudget,
26
28
  type ResolvedToolBudget,
27
29
  type SubagentRunMode,
30
+ type SubagentOutputState,
31
+ type UsageBudgetConfig,
28
32
  type ToolBudgetState,
29
33
  type TurnBudgetState,
30
34
  type Usage,
@@ -35,6 +39,7 @@ import {
35
39
  DEFAULT_MAX_OUTPUT,
36
40
  type MaxOutputConfig,
37
41
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
42
+ POLL_INTERVAL_MS,
38
43
  truncateOutput,
39
44
  getSubagentDepthEnv,
40
45
  } from "../../shared/types.ts";
@@ -49,6 +54,7 @@ import {
49
54
  import {
50
55
  type RunnerSubagentStep as SubagentStep,
51
56
  type RunnerStep,
57
+ isCheckpointRunnerStep,
52
58
  isDynamicRunnerGroup,
53
59
  isParallelGroup,
54
60
  flattenSteps,
@@ -58,9 +64,11 @@ import {
58
64
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
59
65
  Semaphore,
60
66
  } from "../shared/parallel-utils.ts";
61
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
67
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
68
+ import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
62
69
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
63
70
  import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
71
+ import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
64
72
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
65
73
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
66
74
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
@@ -106,9 +114,10 @@ import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts
106
114
  import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
107
115
  import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
108
116
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
109
- import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
117
+ import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
110
118
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
111
119
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
120
+ import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
112
121
  import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
113
122
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
114
123
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
@@ -156,12 +165,15 @@ interface SubagentRunConfig {
156
165
  deadlineAt?: number;
157
166
  turnBudget?: ResolvedTurnBudget;
158
167
  toolBudget?: ResolvedToolBudget;
168
+ usageBudget?: UsageBudgetConfig;
159
169
  revivalLease?: SessionLeaseRequest;
160
170
  revivalLeaseToken?: string;
161
171
  /** Global cap on simultaneously-running subagent tasks within this run. */
162
172
  globalConcurrencyLimit?: number;
163
173
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
164
174
  launchContractDigest?: string;
175
+ launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
176
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
165
177
  runnerProcessInstanceId?: string;
166
178
  }
167
179
 
@@ -170,7 +182,10 @@ interface StepResult {
170
182
  context?: "fresh" | "fork";
171
183
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
172
184
  capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
185
+ launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
186
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
173
187
  output: string;
188
+ outputState?: SubagentOutputState;
174
189
  error?: string;
175
190
  protocolError?: ProtocolOutputLimit;
176
191
  success: boolean;
@@ -179,6 +194,7 @@ interface StepResult {
179
194
  interrupted?: boolean;
180
195
  timedOut?: boolean;
181
196
  stopped?: boolean;
197
+ processSignal?: string | null;
182
198
  turnBudget?: TurnBudgetState;
183
199
  turnBudgetExceeded?: boolean;
184
200
  wrapUpRequested?: boolean;
@@ -408,6 +424,7 @@ interface RunPiStreamingResult {
408
424
  error?: string;
409
425
  protocolError?: ProtocolOutputLimit;
410
426
  finalOutput: string;
427
+ outputState: SubagentOutputState;
411
428
  interrupted?: boolean;
412
429
  timedOut?: boolean;
413
430
  stopped?: boolean;
@@ -418,6 +435,7 @@ interface RunPiStreamingResult {
418
435
  toolBudgetBlocked?: boolean;
419
436
  observedMutationAttempt?: boolean;
420
437
  watchdog?: ChildWatchdogStateSnapshot;
438
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
421
439
  processInstanceId: string;
422
440
  processCloseObservedAt?: number;
423
441
  processSignal?: string | null;
@@ -802,7 +820,15 @@ function runPiStreaming(
802
820
  const stderr = stderrTail.text();
803
821
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
804
822
  const finalError = error ?? assistantError;
805
- const forcedDrainAfterFinalSuccess = forcedTerminationSignal && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
823
+ const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
824
+ const signalError = isUnexplainedProcessSignal({
825
+ processSignal: signal,
826
+ interrupted,
827
+ timedOut,
828
+ stopped,
829
+ turnBudgetExceeded,
830
+ forcedDrainAfterFinalSuccess,
831
+ }) ? formatProcessSignalError(signal!) : undefined;
806
832
  resolve({
807
833
  stderr,
808
834
  exitCode: timedOut || stopped ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
@@ -811,9 +837,10 @@ function runPiStreaming(
811
837
  toolCount,
812
838
  durationMs: Date.now() - startedAt,
813
839
  model,
814
- error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
840
+ error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError ?? signalError,
815
841
  protocolError,
816
842
  finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput,
843
+ outputState: finalOutput.trim() ? "present" : "absent",
817
844
  interrupted,
818
845
  timedOut,
819
846
  stopped,
@@ -847,7 +874,7 @@ function runPiStreaming(
847
874
  const stderr = stderrTail.text();
848
875
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
849
876
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
850
- resolve({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
877
+ resolve({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
851
878
  });
852
879
  });
853
880
  }
@@ -1006,6 +1033,8 @@ async function runSingleStep(
1006
1033
  context?: "fresh" | "fork";
1007
1034
  agentContract?: import("../../shared/types.ts").AgentContract;
1008
1035
  launchContractDigest?: string;
1036
+ launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
1037
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
1009
1038
  output: string;
1010
1039
  exitCode: number | null;
1011
1040
  error?: string;
@@ -1146,6 +1175,7 @@ async function runSingleStep(
1146
1175
  : [undefined];
1147
1176
  const attemptedModels: string[] = [];
1148
1177
  let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1178
+ let launchResolvedExtensions = step.launchResolvedExtensions;
1149
1179
  const modelAttempts: ModelAttempt[] = [];
1150
1180
  const writerProcesses: Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }> = [];
1151
1181
  let writerAttemptCount = 0;
@@ -1182,7 +1212,7 @@ async function runSingleStep(
1182
1212
  childIndex: ctx.flatIndex,
1183
1213
  })
1184
1214
  : undefined;
1185
- const { args, env, tempDir, toolDiagnosticPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs({
1215
+ const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs({
1186
1216
  parentSessionId: step.parentSessionId,
1187
1217
  baseArgs: ["--mode", "json", "-p"],
1188
1218
  task,
@@ -1231,6 +1261,7 @@ async function runSingleStep(
1231
1261
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1232
1262
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1233
1263
  });
1264
+ launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
1234
1265
  actualLaunchContractDigest = launchBindingDigest({
1235
1266
  definitionDigest: step.definitionDigest,
1236
1267
  task: step.launchBindingTask ?? task,
@@ -1303,6 +1334,7 @@ async function runSingleStep(
1303
1334
  const toolAvailabilityError = run.exitCode === 0 && !run.error
1304
1335
  ? readChildToolDiagnosticError(toolDiagnosticPath)
1305
1336
  : undefined;
1337
+ const runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
1306
1338
  cleanupTempDir(tempDir);
1307
1339
 
1308
1340
  const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError ? detectSubagentError(run.messages) : null;
@@ -1357,6 +1389,13 @@ async function runSingleStep(
1357
1389
  : run.error && run.exitCode === 0
1358
1390
  ? 1
1359
1391
  : run.exitCode;
1392
+ const signalError = run.exitCode !== 0 && isUnexplainedProcessSignal({
1393
+ processSignal: run.processSignal,
1394
+ interrupted: run.interrupted,
1395
+ timedOut: run.timedOut,
1396
+ stopped: run.stopped,
1397
+ turnBudgetExceeded: run.turnBudgetExceeded,
1398
+ }) ? formatProcessSignalError(run.processSignal!) : undefined;
1360
1399
  const error = toolAvailabilityError
1361
1400
  ?? completionGuardError
1362
1401
  ?? structuredError
@@ -1365,7 +1404,7 @@ async function runSingleStep(
1365
1404
  ? hiddenError.details
1366
1405
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1367
1406
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1368
- : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
1407
+ : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
1369
1408
  const attempt: ModelAttempt = {
1370
1409
  model: candidate ?? run.model ?? step.model ?? "default",
1371
1410
  success: effectiveExitCode === 0 && !error,
@@ -1383,7 +1422,7 @@ async function runSingleStep(
1383
1422
  toolBudgetBlocked = Boolean(blockedMessage);
1384
1423
  toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1385
1424
  }
1386
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect ? { effects: { fileMutation: fileMutationEffect } } : {}) } as RunPiStreamingResult & { structuredOutput?: unknown; agentContract?: import("../../shared/types.ts").AgentContract; effects?: import("../../shared/types.ts").EffectsProjection };
1425
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect ? { effects: { fileMutation: fileMutationEffect } } : {}) } as RunPiStreamingResult & { structuredOutput?: unknown; agentContract?: import("../../shared/types.ts").AgentContract; effects?: import("../../shared/types.ts").EffectsProjection };
1387
1426
  if (run.turnBudgetExceeded) break modelAttemptsLoop;
1388
1427
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1389
1428
  if (attempt.success || completionGuardTriggered) break modelAttemptsLoop;
@@ -1461,6 +1500,13 @@ async function runSingleStep(
1461
1500
  const childWrittenOutput = step.outputPath
1462
1501
  ? extractChildWrittenOutput(finalResult?.messages, step.outputPath, step.cwd ?? ctx.cwd)
1463
1502
  : undefined;
1503
+ const outputState: SubagentOutputState = finalResult?.outputState === "present"
1504
+ || (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
1505
+ || Boolean(childWrittenOutput?.trim())
1506
+ ? "present"
1507
+ : resolvedOutput.savedPath
1508
+ ? "unknown"
1509
+ : finalResult?.outputState ?? "unknown";
1464
1510
  const finalizedOutput = finalizeSingleOutput({
1465
1511
  fullOutput: outputForSummary,
1466
1512
  outputPath: step.outputPath,
@@ -1533,6 +1579,8 @@ async function runSingleStep(
1533
1579
  acceptance: effectiveAcceptance,
1534
1580
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1535
1581
  launchContractDigest: actualLaunchContractDigest,
1582
+ launchResolvedExtensions,
1583
+ ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1536
1584
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1537
1585
  transcriptError: transcriptWriter?.getError(),
1538
1586
  skills: step.skills,
@@ -1549,6 +1597,7 @@ async function runSingleStep(
1549
1597
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1550
1598
  launchContractDigest: actualLaunchContractDigest,
1551
1599
  output: outputForSummary,
1600
+ outputState,
1552
1601
  exitCode: effectiveFinalExitCode,
1553
1602
  error: effectiveFinalError,
1554
1603
  protocolError: finalResult?.protocolError,
@@ -1564,6 +1613,7 @@ async function runSingleStep(
1564
1613
  interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
1565
1614
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
1566
1615
  stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
1616
+ processSignal: finalResult?.processSignal,
1567
1617
  turnBudget,
1568
1618
  turnBudgetExceeded: turnBudgetExceeded || undefined,
1569
1619
  wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
@@ -1577,6 +1627,8 @@ async function runSingleStep(
1577
1627
  acceptance: effectiveAcceptance,
1578
1628
  watchdog: finalResult?.watchdog,
1579
1629
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1630
+ launchResolvedExtensions,
1631
+ ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1580
1632
  writerProcesses,
1581
1633
  writerAttemptCount,
1582
1634
  };
@@ -1585,6 +1637,7 @@ async function runSingleStep(
1585
1637
 
1586
1638
  type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1587
1639
  exitCode?: number | null;
1640
+ description?: string;
1588
1641
  };
1589
1642
 
1590
1643
  function appendCapabilityCeilingAppliedEvent(eventsPath: string, runId: string, stepIndex: number, agent: string, result: StepResult): void {
@@ -1782,6 +1835,10 @@ async function runSubagent(
1782
1835
  let timedOut = false;
1783
1836
  let stopped = false;
1784
1837
  let turnBudgetExceeded = false;
1838
+ let usageBudgetExceeded = false;
1839
+ let checkpointRejected = false;
1840
+ let pendingCheckpointDecision: "approved" | "rejected" | undefined;
1841
+ let wakeCheckpointDecision: (() => void) | undefined;
1785
1842
  const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
1786
1843
  const stopMessage = "Subagent stopped by user.";
1787
1844
  const timeoutAbortController = new AbortController();
@@ -1796,6 +1853,19 @@ async function runSubagent(
1796
1853
  let flatStepCount = 0;
1797
1854
  for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
1798
1855
  const step = steps[stepIndex]!;
1856
+ if (isCheckpointRunnerStep(step)) {
1857
+ initialStatusSteps.push({
1858
+ agent: `checkpoint:${step.checkpoint}`,
1859
+ phase: step.phase,
1860
+ label: step.label ?? step.checkpoint,
1861
+ status: "pending",
1862
+ checkpoint: { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending", stepIndex },
1863
+ recentTools: [],
1864
+ recentOutput: [],
1865
+ });
1866
+ flatStepCount++;
1867
+ continue;
1868
+ }
1799
1869
  if (isParallelGroup(step)) {
1800
1870
  parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
1801
1871
  for (const task of step.parallel) {
@@ -1803,6 +1873,7 @@ async function runSubagent(
1803
1873
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount });
1804
1874
  initialStatusSteps.push({
1805
1875
  agent: task.agent,
1876
+ ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
1806
1877
  ...(task.context ? { context: task.context } : {}),
1807
1878
  phase: task.phase,
1808
1879
  label: task.label,
@@ -1810,6 +1881,7 @@ async function runSubagent(
1810
1881
  structured: task.structured,
1811
1882
  ...(task.agentContract ? { agentContract: task.agentContract } : {}),
1812
1883
  ...(task.launchContractDigest ? { launchContractDigest: task.launchContractDigest } : {}),
1884
+ ...(task.launchResolvedExtensions ? { launchResolvedExtensions: task.launchResolvedExtensions } : {}),
1813
1885
  ...(task.capabilityCeiling ? { capabilityCeiling: task.capabilityCeiling } : {}),
1814
1886
  status: "pending",
1815
1887
  ...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
@@ -1846,6 +1918,7 @@ async function runSubagent(
1846
1918
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount });
1847
1919
  initialStatusSteps.push({
1848
1920
  agent: step.agent,
1921
+ ...(statusStepDescription(step.task) ? { description: statusStepDescription(step.task) } : {}),
1849
1922
  ...(step.context ? { context: step.context } : {}),
1850
1923
  phase: step.phase,
1851
1924
  label: step.label,
@@ -1853,6 +1926,7 @@ async function runSubagent(
1853
1926
  structured: step.structured,
1854
1927
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1855
1928
  ...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
1929
+ ...(step.launchResolvedExtensions ? { launchResolvedExtensions: step.launchResolvedExtensions } : {}),
1856
1930
  ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
1857
1931
  status: "pending",
1858
1932
  ...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
@@ -1891,6 +1965,7 @@ async function runSubagent(
1891
1965
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
1892
1966
  ...(config.turnBudget ? { turnBudget: initialTurnBudgetState(config.turnBudget) } : {}),
1893
1967
  ...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
1968
+ ...(config.usageBudget ? { usageBudget: usageBudgetState(config.usageBudget, undefined) } : {}),
1894
1969
  pid: process.pid,
1895
1970
  cwd,
1896
1971
  currentStep: 0,
@@ -1898,6 +1973,7 @@ async function runSubagent(
1898
1973
  parallelGroups,
1899
1974
  workflowGraph: config.workflowGraph,
1900
1975
  ...(config.launchContractDigest ? { launchContractDigest: config.launchContractDigest } : {}),
1976
+ ...(config.launchResolvedExtensions ? { launchResolvedExtensions: config.launchResolvedExtensions } : {}),
1901
1977
  ...(config.capabilityCeiling ? { capabilityCeiling: config.capabilityCeiling } : {}),
1902
1978
  ...(config.runnerProcessInstanceId ? { processTerminal: { version: 1 as const, state: "pending" as const, runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId } } : {}),
1903
1979
  steps: initialStatusSteps,
@@ -1908,6 +1984,23 @@ async function runSubagent(
1908
1984
 
1909
1985
  fs.mkdirSync(asyncDir, { recursive: true });
1910
1986
  writeAtomicJson(statusPath, statusPayload);
1987
+ let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
1988
+ const currentUsageTotals = (): CostSummary => {
1989
+ const cost = results.reduce<CostSummary>((sum, result) => ({
1990
+ inputTokens: sum.inputTokens + (result.totalCost?.inputTokens ?? result.usage?.input ?? 0),
1991
+ outputTokens: sum.outputTokens + (result.totalCost?.outputTokens ?? result.usage?.output ?? 0),
1992
+ costUsd: sum.costUsd + (result.totalCost?.costUsd ?? result.usage?.cost ?? 0),
1993
+ }), { inputTokens: pendingParallelUsageCost.inputTokens, outputTokens: pendingParallelUsageCost.outputTokens, costUsd: pendingParallelUsageCost.costUsd });
1994
+ return {
1995
+ inputTokens: Math.max(cost.inputTokens, statusPayload.totalTokens?.input ?? 0),
1996
+ outputTokens: Math.max(cost.outputTokens, statusPayload.totalTokens?.output ?? 0),
1997
+ costUsd: cost.costUsd,
1998
+ };
1999
+ };
2000
+ const refreshUsageBudget = () => {
2001
+ statusPayload.usageBudget = usageBudgetState(config.usageBudget, currentUsageTotals());
2002
+ return statusPayload.usageBudget;
2003
+ };
1911
2004
  const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
1912
2005
  if (!config.nestedRoute || !config.nestedSelf) return;
1913
2006
  try {
@@ -1933,9 +2026,9 @@ async function runSubagent(
1933
2026
  const refreshWorkflowGraph = (): void => {
1934
2027
  if (!config.workflowGraph) return;
1935
2028
  const graph = structuredClone(statusPayload.workflowGraph ?? config.workflowGraph);
1936
- const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "stopped" | "detached" => {
2029
+ const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "stopped" | "detached" | "rejected" => {
1937
2030
  if (status === "complete" || status === "completed") return "completed";
1938
- if (status === "running" || status === "failed" || status === "paused" || status === "stopped" || status === "pending") return status;
2031
+ if (status === "running" || status === "failed" || status === "paused" || status === "stopped" || status === "pending" || status === "rejected") return status;
1939
2032
  return "pending";
1940
2033
  };
1941
2034
  const updateNode = (node: NonNullable<typeof graph.nodes>[number]): void => {
@@ -1945,6 +2038,7 @@ async function runSubagent(
1945
2038
  node.status = normalize(step.status);
1946
2039
  node.error = step.error;
1947
2040
  node.acceptanceStatus = step.acceptance?.status;
2041
+ node.checkpoint = step.checkpoint;
1948
2042
  }
1949
2043
  if (statusPayload.currentStep === node.flatIndex) graph.currentNodeId = node.id;
1950
2044
  }
@@ -1953,10 +2047,11 @@ async function runSubagent(
1953
2047
  if (node.children.every((child) => child.status === "completed")) node.status = "completed";
1954
2048
  else if (node.children.some((child) => child.status === "running")) node.status = "running";
1955
2049
  else if (node.children.some((child) => child.status === "stopped")) node.status = "stopped";
2050
+ else if (node.children.some((child) => child.status === "rejected")) node.status = "rejected";
1956
2051
  else if (node.children.some((child) => child.status === "failed")) node.status = "failed";
1957
2052
  else if (node.children.some((child) => child.status === "paused")) node.status = "paused";
1958
2053
  }
1959
- if (node.error && node.status !== "stopped") node.status = "failed";
2054
+ if (node.error && node.status !== "stopped" && node.status !== "rejected") node.status = "failed";
1960
2055
  };
1961
2056
  for (const node of graph.nodes) updateNode(node);
1962
2057
  statusPayload.workflowGraph = graph;
@@ -2604,6 +2699,7 @@ async function runSubagent(
2604
2699
  const totalInput = statusPayload.totalTokens?.input ?? 0;
2605
2700
  const totalOutput = statusPayload.totalTokens?.output ?? 0;
2606
2701
  statusPayload.totalTokens = { input: totalInput + input, output: totalOutput + output, total: totalInput + totalOutput + input + output };
2702
+ refreshUsageBudget();
2607
2703
  }
2608
2704
  statusPayload.turnCount = Math.max(statusPayload.turnCount ?? 0, step.turnCount);
2609
2705
  updateStepTurnBudget(
@@ -2789,6 +2885,10 @@ async function runSubagent(
2789
2885
  onInterrupt: interruptRunner,
2790
2886
  onTimeout: timeoutRunner,
2791
2887
  onStop: stopRunner,
2888
+ onCheckpointDecision: (decision) => {
2889
+ pendingCheckpointDecision = decision;
2890
+ wakeCheckpointDecision?.();
2891
+ },
2792
2892
  onSteer: (request) => {
2793
2893
  const targetStep = request.targetIndex !== undefined ? statusPayload.steps[request.targetIndex] : undefined;
2794
2894
  if (targetStep?.status === "pending") {
@@ -2839,14 +2939,95 @@ async function runSubagent(
2839
2939
 
2840
2940
  let flatIndex = 0;
2841
2941
  let stepCursor = 0;
2942
+ const waitForCheckpointDecision = async (): Promise<"approved" | "rejected" | undefined> => {
2943
+ while (!pendingCheckpointDecision && !interrupted && !timedOut && !stopped) {
2944
+ await new Promise<void>((resolve) => {
2945
+ wakeCheckpointDecision = resolve;
2946
+ const timer = setTimeout(resolve, POLL_INTERVAL_MS);
2947
+ timer.unref?.();
2948
+ });
2949
+ wakeCheckpointDecision = undefined;
2950
+ }
2951
+ const decision = pendingCheckpointDecision;
2952
+ pendingCheckpointDecision = undefined;
2953
+ return decision;
2954
+ };
2842
2955
 
2843
2956
  while (true) {
2844
2957
  if (interrupted || timedOut || stopped || turnBudgetExceeded) break;
2845
2958
  consumePendingAppendRequests();
2846
2959
  if (stepCursor >= steps.length) break;
2960
+ refreshUsageBudget();
2961
+ if (statusPayload.usageBudget?.exhausted) {
2962
+ usageBudgetExceeded = true;
2963
+ statusPayload.state = "failed";
2964
+ statusPayload.error = usageBudgetExceededMessage(statusPayload.usageBudget);
2965
+ statusPayload.currentStep = flatIndex;
2966
+ statusPayload.lastUpdate = Date.now();
2967
+ writeStatusPayload();
2968
+ break;
2969
+ }
2847
2970
  const stepIndex = stepCursor++;
2848
2971
  const step = steps[stepIndex]!;
2849
2972
 
2973
+ if (isCheckpointRunnerStep(step)) {
2974
+ const now = Date.now();
2975
+ const statusStep = statusPayload.steps[flatIndex];
2976
+ const checkpoint = { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending" as const, stepIndex };
2977
+ statusPayload.state = "paused";
2978
+ statusPayload.currentStep = flatIndex;
2979
+ statusPayload.checkpoint = checkpoint;
2980
+ statusPayload.activityState = undefined;
2981
+ statusPayload.lastUpdate = now;
2982
+ if (statusStep) {
2983
+ statusStep.status = "paused";
2984
+ statusStep.startedAt = now;
2985
+ statusStep.checkpoint = checkpoint;
2986
+ }
2987
+ writeStatusPayload();
2988
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.checkpoint.paused", ts: now, runId: id, stepIndex, checkpoint }));
2989
+ const decision = await waitForCheckpointDecision();
2990
+ if (decision === "approved") {
2991
+ const approvedAt = Date.now();
2992
+ const approved = { ...checkpoint, status: "approved" as const, approvedAt };
2993
+ statusPayload.state = "running";
2994
+ statusPayload.checkpoint = approved;
2995
+ if (statusStep) {
2996
+ statusStep.status = "complete";
2997
+ statusStep.endedAt = approvedAt;
2998
+ statusStep.durationMs = approvedAt - now;
2999
+ statusStep.exitCode = 0;
3000
+ statusStep.checkpoint = approved;
3001
+ }
3002
+ statusPayload.lastUpdate = approvedAt;
3003
+ writeStatusPayload();
3004
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.checkpoint.approved", ts: approvedAt, runId: id, stepIndex, checkpoint: approved }));
3005
+ flatIndex++;
3006
+ continue;
3007
+ }
3008
+ if (decision === "rejected") {
3009
+ const rejectedAt = Date.now();
3010
+ const rejected = { ...checkpoint, status: "rejected" as const, rejectedAt };
3011
+ checkpointRejected = true;
3012
+ statusPayload.state = "rejected";
3013
+ statusPayload.error = `Checkpoint '${step.checkpoint}' rejected.`;
3014
+ statusPayload.checkpoint = rejected;
3015
+ if (statusStep) {
3016
+ statusStep.status = "rejected";
3017
+ statusStep.error = statusPayload.error;
3018
+ statusStep.endedAt = rejectedAt;
3019
+ statusStep.durationMs = rejectedAt - now;
3020
+ statusStep.exitCode = 1;
3021
+ statusStep.checkpoint = rejected;
3022
+ }
3023
+ statusPayload.lastUpdate = rejectedAt;
3024
+ writeStatusPayload();
3025
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.checkpoint.rejected", ts: rejectedAt, runId: id, stepIndex, checkpoint: rejected }));
3026
+ break;
3027
+ }
3028
+ break;
3029
+ }
3030
+
2850
3031
  if (isDynamicRunnerGroup(step)) {
2851
3032
  const groupStartFlatIndex = flatIndex;
2852
3033
  let materialized: ReturnType<typeof materializeDynamicParallelStep>;
@@ -2991,12 +3172,14 @@ async function runSubagent(
2991
3172
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount });
2992
3173
  return {
2993
3174
  agent: task.agent,
3175
+ ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
2994
3176
  ...(task.context ? { context: task.context } : {}),
2995
3177
  phase: task.phase ?? step.phase,
2996
3178
  label: task.label,
2997
3179
  outputName: undefined,
2998
3180
  structured: Boolean(task.structuredOutputSchema),
2999
3181
  ...(task.agentContract ? { agentContract: task.agentContract } : {}),
3182
+ ...(task.launchResolvedExtensions ? { launchResolvedExtensions: task.launchResolvedExtensions } : {}),
3000
3183
  ...(task.capabilityCeiling ? { capabilityCeiling: task.capabilityCeiling } : {}),
3001
3184
  status: "pending",
3002
3185
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
@@ -3057,6 +3240,22 @@ async function runSubagent(
3057
3240
  let aborted = false;
3058
3241
  const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
3059
3242
  const fi = groupStartFlatIndex + taskIdx;
3243
+ refreshUsageBudget();
3244
+ if (statusPayload.usageBudget?.exhausted) {
3245
+ const skippedAt = Date.now();
3246
+ const message = usageBudgetExceededMessage(statusPayload.usageBudget);
3247
+ statusPayload.steps[fi].status = "failed";
3248
+ statusPayload.steps[fi].error = message;
3249
+ statusPayload.steps[fi].startedAt = skippedAt;
3250
+ statusPayload.steps[fi].endedAt = skippedAt;
3251
+ statusPayload.steps[fi].durationMs = 0;
3252
+ statusPayload.steps[fi].exitCode = 1;
3253
+ statusPayload.lastUpdate = skippedAt;
3254
+ usageBudgetExceeded = true;
3255
+ writeStatusPayload();
3256
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: 1, durationMs: 0 }));
3257
+ return { agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true };
3258
+ }
3060
3259
  if (timedOut) return timedOutStepResult(task.agent, task.context);
3061
3260
  if (stopped) return stoppedStepResult(task.agent, task.context);
3062
3261
  if (interrupted) return pausedStepResult(task.agent, task.context);
@@ -3140,11 +3339,21 @@ async function runSubagent(
3140
3339
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
3141
3340
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
3142
3341
  statusPayload.steps[fi].totalCost = singleResult.totalCost;
3342
+ if (singleResult.totalCost) {
3343
+ pendingParallelUsageCost = {
3344
+ inputTokens: pendingParallelUsageCost.inputTokens + singleResult.totalCost.inputTokens,
3345
+ outputTokens: pendingParallelUsageCost.outputTokens + singleResult.totalCost.outputTokens,
3346
+ costUsd: pendingParallelUsageCost.costUsd + singleResult.totalCost.costUsd,
3347
+ };
3348
+ refreshUsageBudget();
3349
+ }
3143
3350
  statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3144
3351
  statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
3145
3352
  statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
3146
3353
  statusPayload.steps[fi].agentContract = singleResult.agentContract;
3147
3354
  statusPayload.steps[fi].launchContractDigest = singleResult.launchContractDigest;
3355
+ statusPayload.steps[fi].launchResolvedExtensions = singleResult.launchResolvedExtensions;
3356
+ statusPayload.steps[fi].runtimeAcknowledgedExtensions = singleResult.runtimeAcknowledgedExtensions;
3148
3357
  statusPayload.steps[fi].effects = singleResult.effects;
3149
3358
  statusPayload.steps[fi].execution = singleResult.execution;
3150
3359
  statusPayload.steps[fi].review = singleResult.review;
@@ -3176,7 +3385,10 @@ async function runSubagent(
3176
3385
  context: pr.context,
3177
3386
  agentContract: pr.agentContract,
3178
3387
  launchContractDigest: pr.launchContractDigest,
3388
+ launchResolvedExtensions: pr.launchResolvedExtensions,
3389
+ runtimeAcknowledgedExtensions: pr.runtimeAcknowledgedExtensions,
3179
3390
  output: pr.output,
3391
+ outputState: pr.outputState,
3180
3392
  error: pr.error,
3181
3393
  protocolError: pr.protocolError,
3182
3394
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
@@ -3211,6 +3423,8 @@ async function runSubagent(
3211
3423
  capabilityAudit: pr.capabilityAudit,
3212
3424
  });
3213
3425
  }
3426
+ pendingParallelUsageCost = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
3427
+ refreshUsageBudget();
3214
3428
  const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
3215
3429
  const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
3216
3430
  const acceptanceFailures = parallelResults
@@ -3376,6 +3590,25 @@ async function runSubagent(
3376
3590
  concurrency,
3377
3591
  async (task, taskIdx) => {
3378
3592
  const fi = groupStartFlatIndex + taskIdx;
3593
+ refreshUsageBudget();
3594
+ if (statusPayload.usageBudget?.exhausted) {
3595
+ const skippedAt = Date.now();
3596
+ const message = usageBudgetExceededMessage(statusPayload.usageBudget);
3597
+ statusPayload.steps[fi].status = "failed";
3598
+ statusPayload.steps[fi].error = message;
3599
+ statusPayload.steps[fi].startedAt = skippedAt;
3600
+ statusPayload.steps[fi].endedAt = skippedAt;
3601
+ statusPayload.steps[fi].durationMs = 0;
3602
+ statusPayload.steps[fi].exitCode = 1;
3603
+ statusPayload.steps[fi].activityState = undefined;
3604
+ statusPayload.lastUpdate = skippedAt;
3605
+ usageBudgetExceeded = true;
3606
+ writeStatusPayload();
3607
+ appendJsonl(eventsPath, JSON.stringify({
3608
+ type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: 1, durationMs: 0,
3609
+ }));
3610
+ return { agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true };
3611
+ }
3379
3612
  if (timedOut) return timedOutStepResult(task.agent, task.context);
3380
3613
  if (stopped) return stoppedStepResult(task.agent, task.context);
3381
3614
  if (interrupted) return pausedStepResult(task.agent, task.context);
@@ -3481,10 +3714,20 @@ async function runSubagent(
3481
3714
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
3482
3715
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
3483
3716
  statusPayload.steps[fi].totalCost = singleResult.totalCost;
3717
+ if (singleResult.totalCost) {
3718
+ pendingParallelUsageCost = {
3719
+ inputTokens: pendingParallelUsageCost.inputTokens + singleResult.totalCost.inputTokens,
3720
+ outputTokens: pendingParallelUsageCost.outputTokens + singleResult.totalCost.outputTokens,
3721
+ costUsd: pendingParallelUsageCost.costUsd + singleResult.totalCost.costUsd,
3722
+ };
3723
+ refreshUsageBudget();
3724
+ }
3484
3725
  statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3485
3726
  statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
3486
3727
  statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
3487
3728
  statusPayload.steps[fi].agentContract = singleResult.agentContract;
3729
+ statusPayload.steps[fi].launchResolvedExtensions = singleResult.launchResolvedExtensions;
3730
+ statusPayload.steps[fi].runtimeAcknowledgedExtensions = singleResult.runtimeAcknowledgedExtensions;
3488
3731
  statusPayload.steps[fi].effects = singleResult.effects;
3489
3732
  statusPayload.steps[fi].execution = singleResult.execution;
3490
3733
  statusPayload.steps[fi].review = singleResult.review;
@@ -3552,7 +3795,9 @@ async function runSubagent(
3552
3795
  context: pr.context,
3553
3796
  agentContract: pr.agentContract,
3554
3797
  launchContractDigest: pr.launchContractDigest,
3798
+ launchResolvedExtensions: pr.launchResolvedExtensions,
3555
3799
  output: pr.output,
3800
+ outputState: pr.outputState,
3556
3801
  error: pr.error,
3557
3802
  protocolError: pr.protocolError,
3558
3803
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
@@ -3585,6 +3830,8 @@ async function runSubagent(
3585
3830
  watchdog: pr.watchdog,
3586
3831
  });
3587
3832
  }
3833
+ pendingParallelUsageCost = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
3834
+ refreshUsageBudget();
3588
3835
  for (let t = 0; t < group.parallel.length; t++) {
3589
3836
  const outputName = group.parallel[t]?.outputName;
3590
3837
  if (outputName) outputs[outputName] = outputEntryFromAsyncResult({
@@ -3624,7 +3871,13 @@ async function runSubagent(
3624
3871
  cleanup,
3625
3872
  results: parallelResults.map((result) => ({
3626
3873
  agent: result.agent,
3627
- status: result.stopped ? "stopped" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
3874
+ status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal({
3875
+ processSignal: result.processSignal,
3876
+ interrupted: result.interrupted,
3877
+ timedOut: result.timedOut,
3878
+ stopped: result.stopped,
3879
+ turnBudgetExceeded: result.turnBudgetExceeded,
3880
+ })) ? "stopped" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
3628
3881
  summary: result.output || result.error || "(no output)",
3629
3882
  ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
3630
3883
  ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
@@ -3728,7 +3981,10 @@ async function runSubagent(
3728
3981
  context: singleResult.context,
3729
3982
  agentContract: singleResult.agentContract,
3730
3983
  launchContractDigest: singleResult.launchContractDigest,
3984
+ launchResolvedExtensions: singleResult.launchResolvedExtensions,
3985
+ runtimeAcknowledgedExtensions: singleResult.runtimeAcknowledgedExtensions,
3731
3986
  output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
3987
+ outputState: singleResult.outputState,
3732
3988
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
3733
3989
  protocolError: singleResult.protocolError,
3734
3990
  success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
@@ -3818,6 +4074,8 @@ async function runSubagent(
3818
4074
  statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
3819
4075
  statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
3820
4076
  statusPayload.steps[flatIndex].agentContract = singleResult.agentContract;
4077
+ statusPayload.steps[flatIndex].launchResolvedExtensions = singleResult.launchResolvedExtensions;
4078
+ statusPayload.steps[flatIndex].runtimeAcknowledgedExtensions = singleResult.runtimeAcknowledgedExtensions;
3821
4079
  statusPayload.steps[flatIndex].effects = singleResult.effects;
3822
4080
  statusPayload.steps[flatIndex].execution = singleResult.execution;
3823
4081
  statusPayload.steps[flatIndex].review = singleResult.review;
@@ -3888,6 +4146,7 @@ async function runSubagent(
3888
4146
  }
3889
4147
 
3890
4148
  const resultMode = config.resultMode ?? statusPayload.mode;
4149
+ const singleRuntimeAcknowledgedExtensions = results.length === 1 ? results[0]?.runtimeAcknowledgedExtensions : undefined;
3891
4150
  const totalCost = results.reduce<CostSummary>((sum, result) => ({
3892
4151
  inputTokens: sum.inputTokens + (result.totalCost?.inputTokens ?? 0),
3893
4152
  outputTokens: sum.outputTokens + (result.totalCost?.outputTokens ?? 0),
@@ -3938,7 +4197,14 @@ async function runSubagent(
3938
4197
  clearTimeout(timeoutTimer);
3939
4198
  timeoutTimer = undefined;
3940
4199
  }
3941
- statusPayload.state = stopped ? "stopped" : timedOut || turnBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
4200
+ const signalTerminated = !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal({
4201
+ processSignal: result.processSignal,
4202
+ interrupted: result.interrupted,
4203
+ timedOut: result.timedOut,
4204
+ stopped: result.stopped,
4205
+ turnBudgetExceeded: result.turnBudgetExceeded,
4206
+ }));
4207
+ statusPayload.state = checkpointRejected ? "rejected" : stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
3942
4208
  closeSteerInbox(asyncDir, statusPayload.state);
3943
4209
  disposeControlInbox();
3944
4210
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
@@ -3960,6 +4226,8 @@ async function runSubagent(
3960
4226
  if (stopped) {
3961
4227
  statusPayload.stopped = true;
3962
4228
  statusPayload.error = stopMessage;
4229
+ } else if (signalTerminated && !statusPayload.error) {
4230
+ statusPayload.error = results.find((result) => result.processSignal)?.error;
3963
4231
  }
3964
4232
  if (timedOut) {
3965
4233
  statusPayload.timedOut = true;
@@ -3969,10 +4237,15 @@ async function runSubagent(
3969
4237
  const budget = statusPayload.turnBudget;
3970
4238
  statusPayload.error = budget ? turnBudgetExceededMessage(budget, budget.turnCount) : "Subagent exceeded turn budget.";
3971
4239
  }
4240
+ if (usageBudgetExceeded && statusPayload.usageBudget && !statusPayload.error) {
4241
+ statusPayload.error = usageBudgetExceededMessage(statusPayload.usageBudget);
4242
+ }
3972
4243
  statusPayload.endedAt = runEndedAt;
3973
4244
  statusPayload.lastUpdate = runEndedAt;
3974
4245
  statusPayload.sessionFile = effectiveSessionFile;
4246
+ if (singleRuntimeAcknowledgedExtensions) statusPayload.runtimeAcknowledgedExtensions = singleRuntimeAcknowledgedExtensions;
3975
4247
  statusPayload.totalCost = finalTotalCost;
4248
+ statusPayload.usageBudget = usageBudgetState(config.usageBudget, currentUsageTotals());
3976
4249
  statusPayload.shareUrl = shareUrl;
3977
4250
  statusPayload.gistUrl = gistUrl;
3978
4251
  statusPayload.shareError = shareError;
@@ -3994,6 +4267,7 @@ async function runSubagent(
3994
4267
  durationMs: runEndedAt - overallStartTime,
3995
4268
  totalTokens: statusPayload.totalTokens,
3996
4269
  totalCost: finalTotalCost,
4270
+ usageBudget: statusPayload.usageBudget,
3997
4271
  }),
3998
4272
  );
3999
4273
  writeRunLog(logPath, {
@@ -4021,9 +4295,9 @@ async function runSubagent(
4021
4295
  id,
4022
4296
  agent: agentName,
4023
4297
  mode: resultMode,
4024
- success: !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.every((r) => r.success),
4025
- state: stopped ? "stopped" : timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
4026
- summary: stopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
4298
+ success: !checkpointRejected && !stopped && !timedOut && !turnBudgetExceeded && !usageBudgetExceeded && !interrupted && !signalTerminated && results.every((r) => r.success),
4299
+ state: checkpointRejected ? "rejected" : stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
4300
+ summary: checkpointRejected ? (statusPayload.error ?? "Checkpoint rejected.") : stopped ? stopMessage : signalTerminated ? (statusPayload.error ?? "Subagent process terminated by signal.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : usageBudgetExceeded ? (statusPayload.error ?? "Usage budget exhausted.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
4027
4301
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
4028
4302
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
4029
4303
  ...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
@@ -4031,11 +4305,14 @@ async function runSubagent(
4031
4305
  ...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
4032
4306
  ...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
4033
4307
  ...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
4034
- ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
4308
+ ...(statusPayload.usageBudget ? { usageBudget: statusPayload.usageBudget } : {}),
4309
+ ...(statusPayload.checkpoint ? { checkpoint: statusPayload.checkpoint } : {}),
4310
+ ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : usageBudgetExceeded ? { error: statusPayload.error ?? "Usage budget exhausted." } : {}),
4035
4311
  results: results.map((r) => ({
4036
4312
  agent: r.agent,
4037
4313
  context: r.context,
4038
4314
  output: r.output,
4315
+ outputState: r.outputState,
4039
4316
  error: r.error,
4040
4317
  protocolError: r.protocolError,
4041
4318
  success: r.success,
@@ -4043,6 +4320,7 @@ async function runSubagent(
4043
4320
  interrupted: r.interrupted || undefined,
4044
4321
  timedOut: r.timedOut || undefined,
4045
4322
  stopped: r.stopped || undefined,
4323
+ processSignal: r.processSignal || undefined,
4046
4324
  turnBudget: r.turnBudget,
4047
4325
  turnBudgetExceeded: r.turnBudgetExceeded || undefined,
4048
4326
  wrapUpRequested: r.wrapUpRequested || undefined,
@@ -4060,6 +4338,8 @@ async function runSubagent(
4060
4338
  transcriptError: r.transcriptError,
4061
4339
  agentContract: r.agentContract,
4062
4340
  launchContractDigest: r.launchContractDigest,
4341
+ launchResolvedExtensions: r.launchResolvedExtensions,
4342
+ runtimeAcknowledgedExtensions: r.runtimeAcknowledgedExtensions,
4063
4343
  execution: r.execution,
4064
4344
  review: r.review,
4065
4345
  effects: r.effects,
@@ -4076,16 +4356,20 @@ async function runSubagent(
4076
4356
  parallelHandoff: statusPayload.parallelHandoff,
4077
4357
  capabilityCeiling: statusPayload.capabilityCeiling,
4078
4358
  capabilityAudit: statusPayload.capabilityAudit,
4079
- exitCode: stopped || timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
4359
+ exitCode: checkpointRejected || stopped || timedOut || turnBudgetExceeded || usageBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
4080
4360
  timestamp: runEndedAt,
4081
4361
  durationMs: runEndedAt - overallStartTime,
4082
4362
  totalTokens: statusPayload.totalTokens,
4083
4363
  totalCost: finalTotalCost,
4364
+ usageBudget: statusPayload.usageBudget,
4365
+ checkpoint: statusPayload.checkpoint,
4084
4366
  truncated,
4085
4367
  artifactsDir,
4086
4368
  cwd,
4087
4369
  asyncDir,
4088
4370
  launchContractDigest: config.launchContractDigest,
4371
+ launchResolvedExtensions: config.launchResolvedExtensions,
4372
+ runtimeAcknowledgedExtensions: singleRuntimeAcknowledgedExtensions,
4089
4373
  sessionId: config.sessionId,
4090
4374
  sessionFile: effectiveSessionFile,
4091
4375
  intercomTarget: config.controlIntercomTarget,