@bastani/atomic 0.9.14-alpha.3 → 0.9.14-alpha.5

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 (150) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  5. package/dist/builtin/subagents/package.json +1 -1
  6. package/dist/builtin/subagents/src/extension/schemas.ts +5 -0
  7. package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +3 -1
  8. package/dist/builtin/subagents/src/runs/shared/progress-trend.ts +69 -0
  9. package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +12 -1
  10. package/dist/builtin/subagents/src/shared/types-results.ts +2 -0
  11. package/dist/builtin/web-access/CHANGELOG.md +10 -0
  12. package/dist/builtin/web-access/README.md +3 -6
  13. package/dist/builtin/web-access/index.ts +1 -2
  14. package/dist/builtin/web-access/package.json +1 -1
  15. package/dist/builtin/web-access/web-search-config.ts +2 -8
  16. package/dist/builtin/web-access/web-search-tool.ts +2 -7
  17. package/dist/builtin/web-access/web-search-workflow.ts +10 -0
  18. package/dist/builtin/workflows/CHANGELOG.md +46 -0
  19. package/dist/builtin/workflows/README.md +6 -5
  20. package/dist/builtin/workflows/builtin/adversarial-verification-prompts.ts +13 -5
  21. package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +376 -89
  22. package/dist/builtin/workflows/builtin/adversarial-verification.d.ts +30 -6
  23. package/dist/builtin/workflows/builtin/adversarial-verification.ts +14 -9
  24. package/dist/builtin/workflows/builtin/generate-and-filter-prompts.ts +26 -3
  25. package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +18 -14
  26. package/dist/builtin/workflows/builtin/goal-artifacts.ts +9 -8
  27. package/dist/builtin/workflows/builtin/goal-convergence.ts +87 -0
  28. package/dist/builtin/workflows/builtin/goal-ledger.ts +4 -0
  29. package/dist/builtin/workflows/builtin/goal-prompts.ts +2 -0
  30. package/dist/builtin/workflows/builtin/goal-reducer.ts +6 -1
  31. package/dist/builtin/workflows/builtin/goal-reverify.ts +305 -0
  32. package/dist/builtin/workflows/builtin/goal-runner.ts +75 -10
  33. package/dist/builtin/workflows/builtin/goal-schemas.ts +7 -0
  34. package/dist/builtin/workflows/builtin/goal-types.ts +6 -0
  35. package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +94 -6
  36. package/dist/builtin/workflows/builtin/loop-until-done.d.ts +8 -0
  37. package/dist/builtin/workflows/builtin/loop-until-done.ts +15 -0
  38. package/dist/builtin/workflows/builtin/progress-scoring.ts +230 -0
  39. package/dist/builtin/workflows/builtin/ralph-core.ts +11 -0
  40. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +1 -0
  41. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +2 -0
  42. package/dist/builtin/workflows/builtin/ralph-runner.ts +60 -10
  43. package/dist/builtin/workflows/builtin/selection-math.ts +156 -0
  44. package/dist/builtin/workflows/builtin/shared-prompts.ts +5 -0
  45. package/dist/builtin/workflows/builtin/tournament-prompts.ts +57 -75
  46. package/dist/builtin/workflows/builtin/tournament-runner.ts +384 -178
  47. package/dist/builtin/workflows/builtin/tournament.d.ts +46 -17
  48. package/dist/builtin/workflows/builtin/tournament.ts +66 -32
  49. package/dist/builtin/workflows/builtin/verification-criteria.ts +330 -0
  50. package/dist/builtin/workflows/builtin/verification-prompts.ts +206 -0
  51. package/dist/builtin/workflows/builtin/verification-usage.ts +44 -0
  52. package/dist/builtin/workflows/package.json +1 -1
  53. package/dist/builtin/workflows/skills/create-spec/SKILL.md +90 -30
  54. package/dist/builtin/workflows/skills/show-me/LICENSE.txt +21 -0
  55. package/dist/builtin/workflows/skills/show-me/SKILL.md +143 -0
  56. package/dist/builtin/workflows/src/authoring/workflow.ts +8 -0
  57. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  58. package/dist/builtin/workflows/src/durable/completed-catalog.ts +5 -2
  59. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +1 -1
  60. package/dist/builtin/workflows/src/durable/resume-eligibility.ts +5 -3
  61. package/dist/builtin/workflows/src/durable/run-timing.ts +41 -10
  62. package/dist/builtin/workflows/src/durable/tool-primitive.ts +24 -2
  63. package/dist/builtin/workflows/src/engine/options.ts +1 -0
  64. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +12 -3
  65. package/dist/builtin/workflows/src/engine/run-budget.ts +308 -0
  66. package/dist/builtin/workflows/src/engine/run-returned-status.ts +8 -0
  67. package/dist/builtin/workflows/src/engine/run-tool-node-lifecycle.ts +6 -0
  68. package/dist/builtin/workflows/src/engine/run.ts +124 -2
  69. package/dist/builtin/workflows/src/engine/runtime.ts +9 -0
  70. package/dist/builtin/workflows/src/extension/config-file-loader.ts +6 -0
  71. package/dist/builtin/workflows/src/extension/config-loader.ts +24 -1
  72. package/dist/builtin/workflows/src/extension/dispatcher.ts +6 -5
  73. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +2 -0
  74. package/dist/builtin/workflows/src/extension/index.bundle.mjs +3033 -872
  75. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +51 -4
  76. package/dist/builtin/workflows/src/extension/public-types.ts +3 -1
  77. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +7 -1
  78. package/dist/builtin/workflows/src/extension/runtime.ts +22 -10
  79. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +5 -0
  80. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
  81. package/dist/builtin/workflows/src/extension/workflow-schema.ts +16 -0
  82. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +44 -1
  83. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +10 -1
  84. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +21 -9
  85. package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +14 -0
  86. package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +15 -4
  87. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +62 -5
  88. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +4 -0
  89. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +2 -0
  90. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +3 -1
  91. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -1
  92. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +1 -0
  93. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +1 -0
  94. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +7 -0
  95. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +7 -0
  96. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +1 -0
  97. package/dist/builtin/workflows/src/shared/budget-meter.ts +34 -0
  98. package/dist/builtin/workflows/src/shared/budget.d.ts +67 -0
  99. package/dist/builtin/workflows/src/shared/budget.ts +127 -0
  100. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +92 -8
  101. package/dist/builtin/workflows/src/shared/persistence-restore.ts +11 -1
  102. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +15 -3
  103. package/dist/builtin/workflows/src/shared/returned-run-status.ts +35 -2
  104. package/dist/builtin/workflows/src/shared/store-public-types.ts +4 -1
  105. package/dist/builtin/workflows/src/shared/store-run-methods.ts +8 -1
  106. package/dist/builtin/workflows/src/shared/store-stage-methods.ts +1 -0
  107. package/dist/builtin/workflows/src/shared/store-types.ts +24 -0
  108. package/dist/builtin/workflows/src/shared/types.ts +3 -0
  109. package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +1 -0
  110. package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +3 -0
  111. package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +3 -0
  112. package/dist/builtin/workflows/src/tui/graph-theme.ts +11 -0
  113. package/dist/builtin/workflows/src/tui/graph-view-render.ts +19 -10
  114. package/dist/builtin/workflows/src/tui/tool-detail.ts +45 -26
  115. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  116. package/dist/core/atomic-guide-command.js +1 -0
  117. package/dist/core/atomic-guide-command.js.map +1 -1
  118. package/dist/core/extensions/ui-types.d.ts +13 -3
  119. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  120. package/dist/core/extensions/ui-types.js +15 -3
  121. package/dist/core/extensions/ui-types.js.map +1 -1
  122. package/dist/core/slash-commands.d.ts.map +1 -1
  123. package/dist/core/slash-commands.js +33 -3
  124. package/dist/core/slash-commands.js.map +1 -1
  125. package/dist/main-deferred-startup.d.ts.map +1 -1
  126. package/dist/main-deferred-startup.js +6 -2
  127. package/dist/main-deferred-startup.js.map +1 -1
  128. package/dist/modes/interactive/interactive-startup.js +4 -0
  129. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  130. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  131. package/dist/modes/interactive/interactive-tui.js +19 -1
  132. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  133. package/dist/modes/interactive-engine/isolated-runtime.d.ts +7 -0
  134. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  135. package/dist/modes/interactive-engine/isolated-runtime.js +94 -37
  136. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  137. package/dist/modes/rpc/rpc-client.d.ts +1 -0
  138. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  139. package/dist/modes/rpc/rpc-client.js +15 -2
  140. package/dist/modes/rpc/rpc-client.js.map +1 -1
  141. package/dist/modes/rpc/rpc-input-scheduler.d.ts +3 -2
  142. package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
  143. package/dist/modes/rpc/rpc-input-scheduler.js +5 -2
  144. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  145. package/docs/extensions.md +1 -1
  146. package/docs/quickstart.md +1 -0
  147. package/docs/skills.md +4 -0
  148. package/docs/workflows.md +75 -11
  149. package/npm-shrinkwrap.json +29 -29
  150. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ import type { BudgetReport } from "../shared/budget.js";
1
2
  import {
2
3
  actionableReturnedStatusText,
3
4
  effectiveRunStatus,
@@ -33,6 +34,7 @@ export type WorkflowLifecycleNoticeKind =
33
34
  | "completed"
34
35
  | "failed"
35
36
  | "blocked"
37
+ | "budget_warning"
36
38
  | "awaiting_input"
37
39
  | "paused"
38
40
  | "quit"
@@ -45,12 +47,12 @@ export type WorkflowLifecycleNoticeKind =
45
47
  * action to a user.
46
48
  */
47
49
  type WorkflowControlNoticeKind = "started" | "paused" | "quit" | "resumed";
48
-
49
50
  export const WORKFLOW_LIFECYCLE_NOTICE_KINDS = [
50
51
  "started",
51
52
  "completed",
52
53
  "failed",
53
54
  "blocked",
55
+ "budget_warning",
54
56
  "awaiting_input",
55
57
  "paused",
56
58
  "quit",
@@ -80,6 +82,7 @@ export interface WorkflowLifecycleNoticeDetails {
80
82
  readonly toolNodeId?: string;
81
83
  readonly toolName?: string;
82
84
  readonly durationMs?: number;
85
+ readonly budgetDimension?: "duration" | "tokens" | "cost";
83
86
  readonly active?: boolean;
84
87
  /** Whether a failed author exit or quit run advertises itself as resumable. */
85
88
  readonly resumable?: boolean;
@@ -156,6 +159,8 @@ export function seedWorkflowLifecycleNotificationState(
156
159
  state.deliveredTerminalRuns.add(controlKey);
157
160
  }
158
161
  }
162
+ for (const warning of budgetWarningReports(run))
163
+ state.deliveredTerminalRuns.add(budgetWarningKey(run, warning.dimension));
159
164
  if (run.pendingPrompt !== undefined) {
160
165
  state.deliveredInputPrompts.add(runAwaitingInputKey(run.id, run.pendingPrompt));
161
166
  }
@@ -308,6 +313,15 @@ export function installWorkflowLifecycleNotifications(options: WorkflowLifecycle
308
313
  // through workflow status/connect surfaces instead of the main chat context.
309
314
  };
310
315
 
316
+ const emitBudgetWarningOnce = (run: RunSnapshot): void => {
317
+ for (const warning of budgetWarningReports(run)) {
318
+ const key = budgetWarningKey(run, warning.dimension);
319
+ if (state.deliveredTerminalRuns.has(key) || state.pendingTerminalRuns.has(key)) continue;
320
+ if (state.suppressionDepth > 0) state.deliveredTerminalRuns.add(key);
321
+ else delivery.deliver(key, makeBudgetWarningNotice(run, warning));
322
+ }
323
+ };
324
+
311
325
  const inspect = (snapshot: StoreSnapshot): void => {
312
326
  for (const run of snapshot.runs) {
313
327
  if (!isTopLevelWorkflowRun(run)) continue;
@@ -316,6 +330,7 @@ export function installWorkflowLifecycleNotifications(options: WorkflowLifecycle
316
330
  emitTerminalNoticeOnce(run, "blocked");
317
331
  emitControlNoticesOnce(run);
318
332
 
333
+ if (notifyOn.has("budget_warning")) emitBudgetWarningOnce(run);
319
334
  if (!notifyOn.has("awaiting_input")) continue;
320
335
  emitRunAwaitingInputNoticeOnce(run);
321
336
  for (const stage of run.stages) {
@@ -386,6 +401,11 @@ export function formatWorkflowLifecycleNoticeText(details: WorkflowLifecycleNoti
386
401
  const stateText = details.active === true ? "is blocked" : "ended blocked";
387
402
  return `! Workflow "${workflowName}" ${stateText} (run ${details.runId})${origin}${errorText}. Inspect: /workflow status ${details.runId}`;
388
403
  }
404
+ if (details.kind === "budget_warning") {
405
+ const warningText = details.error ? `: ${details.error}` : "";
406
+ const dimension = details.budgetDimension ?? "duration";
407
+ return `! Workflow "${workflowName}" is approaching its ${dimension} budget (run ${details.runId})${origin}${warningText}. Inspect: /workflow status ${details.runId}`;
408
+ }
389
409
  if (details.kind === "paused" || details.kind === "quit") {
390
410
  const stopStage = details.stageName ?? details.stageId;
391
411
  // "(run 8c31), which you started, at stage review" — the stage clause takes a
@@ -448,13 +468,31 @@ function makeTerminalNotice(
448
468
  ...(failedToolNodeId !== undefined ? { toolNodeId: failedToolNodeId } : {}),
449
469
  ...(failedTool !== undefined ? { toolName: failedTool.name } : {}),
450
470
  ...(run.durationMs !== undefined ? { durationMs: run.durationMs } : {}),
451
- // Attribution renders on every kind, and is omitted for a run that never
452
- // recorded an origin rather than guessed at.
453
471
  ...(run.origin !== undefined ? { origin: run.origin } : {}),
454
472
  createdAt: lifecycleOccurrenceAt(run, kind) ?? Date.now(),
455
473
  };
456
474
  }
457
475
 
476
+ function budgetWarningReports(run: RunSnapshot): readonly BudgetReport[] {
477
+ const warnings = Object.values(run.budgetState?.warnings ?? {}).filter(
478
+ (warning): warning is BudgetReport => warning !== undefined,
479
+ );
480
+ return warnings.length > 0 ? warnings : run.budgetState?.warning === undefined ? [] : [run.budgetState.warning];
481
+ }
482
+ function makeBudgetWarningNotice(run: RunSnapshot, warning: BudgetReport): WorkflowLifecycleNoticeDetails {
483
+ return {
484
+ kind: "budget_warning",
485
+ scope: "run",
486
+ runId: run.id,
487
+ workflowName: run.name,
488
+ status: run.status,
489
+ budgetDimension: warning.dimension,
490
+ error: `At ${warning.percent.toFixed(1)}% of the ${warning.dimension} budget (${warning.reading} / ${warning.ceiling}).`,
491
+ ...(warning.dimension === "duration" ? { durationMs: warning.reading } : {}),
492
+ ...(run.origin !== undefined ? { origin: run.origin } : {}),
493
+ createdAt: run.startedAt,
494
+ };
495
+ }
458
496
  function makeControlNotice(run: RunSnapshot, occurrence: ControlOccurrence): WorkflowLifecycleNoticeDetails {
459
497
  const stage = occurrence.stage ?? restingStage(run);
460
498
  const elapsedMs = run.durationMs ?? (occurrence.at >= run.startedAt ? occurrence.at - run.startedAt : undefined);
@@ -531,6 +569,9 @@ function terminalRunKey(kind: "completed" | "failed" | "blocked", run: RunSnapsh
531
569
  const occurrence = kind === "blocked" ? (run.blockedAt ?? lifecycleOccurrenceAt(run, kind)) : "";
532
570
  return occurrence === undefined ? `${kind}:${run.id}` : `${kind}:${run.id}:${occurrence}`;
533
571
  }
572
+ function budgetWarningKey(run: RunSnapshot, dimension = run.budgetState?.warning?.dimension ?? "duration"): string {
573
+ return `budget_warning:${run.id}:${dimension}`;
574
+ }
534
575
 
535
576
  /** One deliberate control action a run currently carries. */
536
577
  interface ControlOccurrence {
@@ -697,12 +738,12 @@ function renderLifecycleNoticeCard(
697
738
  ...(opts.theme ? { theme: opts.theme } : {}),
698
739
  });
699
740
  }
700
-
701
741
  function lifecycleNoticeTone(kind: WorkflowLifecycleNoticeKind): WorkflowNoticeTone {
702
742
  switch (kind) {
703
743
  case "failed":
704
744
  return "error";
705
745
  case "blocked":
746
+ case "budget_warning":
706
747
  case "awaiting_input":
707
748
  case "paused":
708
749
  case "quit":
@@ -718,6 +759,8 @@ function lifecycleNoticeTitle(kind: WorkflowLifecycleNoticeKind): string {
718
759
  return "WORKFLOW STARTED";
719
760
  case "failed":
720
761
  return "WORKFLOW FAILED";
762
+ case "budget_warning":
763
+ return "WORKFLOW BUDGET WARNING";
721
764
  case "awaiting_input":
722
765
  return "WORKFLOW INPUT";
723
766
  case "blocked":
@@ -737,6 +780,8 @@ function lifecycleNoticeGlyph(kind: WorkflowLifecycleNoticeKind): string {
737
780
  switch (kind) {
738
781
  case "failed":
739
782
  return "✗";
783
+ case "budget_warning":
784
+ return "!";
740
785
  case "awaiting_input":
741
786
  return "?";
742
787
  case "blocked":
@@ -761,6 +806,8 @@ function lifecycleNoticeHeadline(details: WorkflowLifecycleNoticeDetails): strin
761
806
  return `Workflow "${name}" started`;
762
807
  case "failed":
763
808
  return `Workflow "${name}" failed`;
809
+ case "budget_warning":
810
+ return `Workflow "${name}" is approaching its duration budget`;
764
811
  case "awaiting_input":
765
812
  return `Workflow "${name}" needs input`;
766
813
  case "blocked":
@@ -3,7 +3,7 @@ import type { Api, Model } from "@earendil-works/pi-ai/compat";
3
3
  import type { StageSessionRuntime } from "../runs/foreground/stage-runner.js";
4
4
  import type { SessionManager } from "../shared/persistence-restore.js";
5
5
  import type { RunStatus, StageStatus } from "../shared/store-types.js";
6
- import type { WorkflowInputValues } from "../shared/types.js";
6
+ import type { WorkflowBudget, WorkflowInputValues } from "../shared/types.js";
7
7
  import type { WidgetFactory } from "../tui/store-widget-installer.js";
8
8
  import type { RenderResultOpts, WorkflowToolResult } from "./render-result.js";
9
9
  import type { PiUISurface } from "./wiring.js";
@@ -197,6 +197,8 @@ export interface ExtensionAPI {
197
197
  export interface WorkflowToolArgs {
198
198
  workflow?: string;
199
199
  inputs?: WorkflowInputValues;
200
+ /** Per-run budget override for action "run". */
201
+ budget?: WorkflowBudget;
200
202
  action?:
201
203
  | "models"
202
204
  | "run"
@@ -18,6 +18,7 @@ import type { ResumableWorkflowEntry } from "../durable/types.js";
18
18
  import type { JobTracker } from "../runs/background/job-tracker.js";
19
19
  import type { RunOpts } from "../runs/foreground/executor.js";
20
20
  import type { StageAdapters } from "../runs/foreground/stage-runner.js";
21
+ import type { WorkflowBudget } from "../shared/budget.js";
21
22
  import type { Store } from "../shared/store.js";
22
23
  import type { RunSnapshot, WorkflowActor } from "../shared/store-types.js";
23
24
  import type { WorkflowExecutionPolicy } from "../shared/types.js";
@@ -27,7 +28,11 @@ import { discoverWorkflows } from "./discovery.js";
27
28
  export interface DurableResumeRuntime {
28
29
  resumeDurableWorkflow(
29
30
  workflowId: string,
30
- options?: { readonly policy?: WorkflowExecutionPolicy; readonly actor?: WorkflowActor },
31
+ options?: {
32
+ readonly policy?: WorkflowExecutionPolicy;
33
+ readonly actor?: WorkflowActor;
34
+ readonly budget?: WorkflowBudget;
35
+ },
31
36
  ): Promise<ResumeDurableResult>;
32
37
  inspectDurableWorkflow(workflowId: string): Promise<TargetedDurableInspection>;
33
38
  listDurableResumable(): readonly ResumableWorkflowEntry[];
@@ -83,6 +88,7 @@ export function createDurableResumeRuntime(deps: DurableResumeRuntimeDeps): Dura
83
88
  registry: deps.registry,
84
89
  baseRunOpts: {
85
90
  ...deps.baseRunOpts(options?.policy),
91
+ ...(options?.budget === undefined ? {} : { budget: options.budget }),
86
92
  ...(options?.actor === undefined ? {} : { resumeActor: options.actor }),
87
93
  },
88
94
  durableBackend: backend,
@@ -20,6 +20,7 @@ import type { Store } from "../shared/store.js";
20
20
  import { store as defaultStore } from "../shared/store.js";
21
21
  import type { RunSnapshot, WorkflowActor } from "../shared/store-types.js";
22
22
  import type {
23
+ WorkflowBudget,
23
24
  WorkflowDefinition,
24
25
  WorkflowExecutionPolicy,
25
26
  WorkflowMcpPort,
@@ -86,7 +87,7 @@ export interface ExtensionRuntimeOpts {
86
87
  // Public interface
87
88
  // ---------------------------------------------------------------------------
88
89
  export type ResumeFailedRunResult =
89
- | { ok: true; runId: string; sourceRunId: string; resumeFromStageId: string; message: string }
90
+ | { ok: true; runId: string; sourceRunId: string; resumeFromStageId?: string; message: string }
90
91
  | {
91
92
  ok: false;
92
93
  reason: "run_not_found" | "not_resumable" | "workflow_not_found" | "insufficient_state";
@@ -119,6 +120,8 @@ export interface RuntimeDispatchOptions {
119
120
  readonly origin?: WorkflowActor;
120
121
  /** Who requested this resume. Only an attributable requester supplies it. */
121
122
  readonly actor?: WorkflowActor;
123
+ /** Run-level budget override used when a continuation is launched. */
124
+ readonly budget?: WorkflowBudget;
122
125
  }
123
126
  // ---------------------------------------------------------------------------
124
127
  // Factory
@@ -214,20 +217,21 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
214
217
  function resolveResumeStage(
215
218
  source: RunSnapshot,
216
219
  stageId?: string,
217
- ): { ok: true; stageId: string } | { ok: false; message: string } {
220
+ ): { ok: true; stageId?: string } | { ok: false; message: string } {
221
+ const budgetExceededSource =
222
+ source.result?.status === "budget_exceeded" && source.budgetState?.systemOwnedStop === true;
218
223
  if (stageId !== undefined) {
219
224
  const resolved = resolveUniqueResumeStage(source, stageId);
220
225
  if (!resolved.ok) return { ok: false, message: resolved.message };
221
226
  const stage = resolved.stage;
222
- if (stage.status !== "failed")
227
+ if (stage.status !== "failed" && !(budgetExceededSource && stage.id === source.failedStageId))
223
228
  return { ok: false, message: `insufficient_state: stage ${stage.name} is ${stage.status}, not failed` };
224
229
  return { ok: true, stageId: stage.id };
225
230
  }
226
231
  const failedStageId = source.failedStageId ?? source.stages.find((stage) => stage.status === "failed")?.id;
227
- if (failedStageId === undefined) {
228
- return { ok: false, message: `insufficient_state: failed run ${source.id} does not identify a failed stage` };
229
- }
230
- return { ok: true, stageId: failedStageId };
232
+ if (failedStageId !== undefined) return { ok: true, stageId: failedStageId };
233
+ if (budgetExceededSource && source.stages.length === 0) return { ok: true };
234
+ return { ok: false, message: `insufficient_state: failed run ${source.id} does not identify a failed stage` };
231
235
  }
232
236
 
233
237
  async function resumeFailedRun(
@@ -241,8 +245,12 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
241
245
  }
242
246
  const isTerminalFailedResumable =
243
247
  source.status === "failed" && source.endedAt !== undefined && source.resumable !== false;
248
+ const isBudgetResumable =
249
+ source.result?.status === "budget_exceeded" && source.budgetState?.systemOwnedStop === true;
244
250
  const isActiveBlockedResumable =
245
- source.endedAt === undefined && source.resumable === true && source.failureRecoverability === "recoverable";
251
+ (source.endedAt === undefined || isBudgetResumable) &&
252
+ source.resumable === true &&
253
+ source.failureRecoverability === "recoverable";
246
254
  if (!isTerminalFailedResumable && !isActiveBlockedResumable) {
247
255
  return { ok: false, reason: "not_resumable", message: `run ${sourceRunId} is not a resumable workflow run` };
248
256
  }
@@ -265,13 +273,17 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
265
273
  };
266
274
  }
267
275
  const stageMessage = (verb: string, runId: string): string =>
268
- `${verb} workflow "${def.name}" from run ${source.id} at stage ${resolvedStage.stageId} (run ${runId}).`;
276
+ `${verb} workflow "${def.name}" from run ${source.id}${resolvedStage.stageId === undefined ? " at workflow start" : ` at stage ${resolvedStage.stageId}`} (run ${runId}).`;
269
277
  const launchContinuation = () =>
270
278
  launchDetachedUntilStartup(def, sourceInputs, {
271
279
  ...runOptions(options?.policy),
272
- continuation: { source, resumeFromStageId: resolvedStage.stageId },
280
+ continuation: {
281
+ source,
282
+ ...(resolvedStage.stageId !== undefined ? { resumeFromStageId: resolvedStage.stageId } : {}),
283
+ },
273
284
  ...(options?.actor === undefined ? {} : { resumeActor: options.actor }),
274
285
  ...(jobs !== undefined ? { jobs } : {}),
286
+ ...(options?.budget === undefined ? {} : { budget: options.budget }),
275
287
  });
276
288
  if (isActiveBlockedResumable) {
277
289
  // Keep the durable blocked source recoverable until fresh-ID startup admission succeeds.
@@ -19,6 +19,7 @@ import ralph from "../../builtin/ralph.js";
19
19
  import tournament from "../../builtin/tournament.js";
20
20
  import { isBrandedWorkflowDefinition } from "../authoring/workflow.js";
21
21
  import * as workflowsSdkSurface from "../sdk-surface.js";
22
+ import { validateWorkflowBudget } from "../shared/budget.js";
22
23
 
23
24
  const WORKFLOWS_MODULE_SPECIFIER = "@bastani/workflows";
24
25
  const WORKFLOWS_BUILTIN_MODULE_SPECIFIER = `${WORKFLOWS_MODULE_SPECIFIER}/builtin`;
@@ -130,6 +131,10 @@ export function validateWorkflowDefinitionShape(value: unknown): string | null {
130
131
  if (typeof d.run !== "function") {
131
132
  return "run must be a function";
132
133
  }
134
+ if (d.budget !== undefined) {
135
+ const reason = validateWorkflowBudget(d.budget, "budget");
136
+ if (reason !== null) return reason;
137
+ }
133
138
  return null;
134
139
  }
135
140
 
@@ -3,6 +3,7 @@ export const WORKFLOW_TOOL_DESCRIPTION =
3
3
  "when workflow execution fits but another shape would better achieve the task, author a custom TypeScript workflow({...}) inline with normal coding tools, reload it, and run it; after successfully creating and reloading a newly authored custom workflow, report the folder containing its generated code as 'Custom workflow created. You can inspect its code at: <workflow-folder-path>'; do this only for newly created custom workflows, never builtin or pre-existing workflows; " +
4
4
  "discover with list/get/inputs/models, list session runs with status (no runId; statusFilter narrows the list), inspect status/stages/stage details, " +
5
5
  "send prompt answers or steering only while the root workflow is nonterminal, pause/resume/interrupt/quit runs, and reload workflow resources. " +
6
+ "For action 'run' and 'resume', budget accepts per-field duration, token, cost, and warning overrides; fields resolve over the workflow declaration and config, and 0 disables a field. " +
6
7
  "For primitive prompt answers, use booleans or the documented confirm labels, exact case-insensitive select labels or 1-based indexes, and text strings for input/editor; an invalid answer remains pending and returns guidance instead of choosing a default. " +
7
8
  "For large stage handoffs, write context to files/artifacts, pass paths via reads, and prompt downstream agents to 'Read the file at <path>...' instead of injecting large previous text. " +
8
9
  "Wrap critical parts of run inputs and steering messages in <keepContext>...</keepContext> so compaction preserves them verbatim in the stages that inherit them; tag role constraints, prohibitions, must-hold criteria, and identifiers, not background or bulk reference material. " +
@@ -28,6 +28,21 @@ const WorkflowResponseSchema = Type.Union(
28
28
  },
29
29
  );
30
30
 
31
+ /** Run-budget declaration accepted by config, authored workflows, and tool runs. */
32
+ export const WorkflowBudgetSchema = Type.Object(
33
+ {
34
+ maxDurationMs: Type.Optional(
35
+ Type.Integer({ minimum: 0, description: "Maximum run duration in milliseconds; 0 disables it." }),
36
+ ),
37
+ maxTokens: Type.Optional(Type.Integer({ minimum: 0, description: "Maximum charged tokens; 0 disables it." })),
38
+ maxCost: Type.Optional(Type.Number({ minimum: 0, description: "Maximum cost in USD; 0 disables it." })),
39
+ warnAtPercent: Type.Optional(
40
+ Type.Number({ minimum: 0, description: "Usage percentage at which to warn; 0 disables it." }),
41
+ ),
42
+ },
43
+ { additionalProperties: false, description: "Optional workflow run budget." },
44
+ );
45
+
31
46
  export const WorkflowParametersSchema = Type.Object(
32
47
  {
33
48
  workflow: Type.Optional(
@@ -41,6 +56,7 @@ export const WorkflowParametersSchema = Type.Object(
41
56
  description: "Key/value inputs passed to a named workflow run.",
42
57
  }),
43
58
  ),
59
+ budget: Type.Optional(WorkflowBudgetSchema),
44
60
  action: Type.Optional(
45
61
  Type.Union(
46
62
  [
@@ -16,6 +16,8 @@
16
16
  import { effectiveRunStatus } from "../shared/returned-run-status.js";
17
17
  import type {
18
18
  PendingPrompt,
19
+ RunBudgetSnapshot,
20
+ RunBudgetState,
19
21
  RunSnapshot,
20
22
  RunStatus,
21
23
  StageInputRequest,
@@ -91,6 +93,8 @@ export interface WorkflowRunStatusSummary {
91
93
  readonly tools?: readonly WorkflowStatusToolNode[];
92
94
  readonly awaitingInputCount: number;
93
95
  readonly awaitingInput: readonly WorkflowStatusAwaitingInput[];
96
+ readonly budget?: RunBudgetSnapshot;
97
+ readonly budgetState?: RunBudgetState;
94
98
  readonly exitReason?: string;
95
99
  readonly error?: string;
96
100
  }
@@ -154,16 +158,55 @@ function awaitingInputEntries(run: RunSnapshot): WorkflowStatusAwaitingInput[] {
154
158
  return entries;
155
159
  }
156
160
 
161
+ function budgetReport(dimension: "duration" | "tokens" | "cost", reading: number, ceiling: number) {
162
+ return { dimension, reading, ceiling, percent: ceiling === 0 ? 0 : (reading / ceiling) * 100 };
163
+ }
157
164
  /** Reduce one run snapshot to its concise status summary. */
158
165
  export function summarizeRunSnapshot(run: RunSnapshot, now = Date.now()): WorkflowRunStatusSummary {
159
166
  const awaitingInput = awaitingInputEntries(run);
167
+ const elapsedMs = elapsedRunMs(run, now);
168
+ const duration =
169
+ run.budget === undefined
170
+ ? undefined
171
+ : run.endedAt === undefined
172
+ ? {
173
+ dimension: "duration" as const,
174
+ reading: elapsedMs,
175
+ ceiling: run.budget.maxDurationMs,
176
+ percent: run.budget.maxDurationMs === 0 ? 0 : (elapsedMs / run.budget.maxDurationMs) * 100,
177
+ }
178
+ : (run.budgetState?.duration ?? {
179
+ dimension: "duration" as const,
180
+ reading: elapsedMs,
181
+ ceiling: run.budget.maxDurationMs,
182
+ percent: run.budget.maxDurationMs === 0 ? 0 : (elapsedMs / run.budget.maxDurationMs) * 100,
183
+ });
184
+ const usageBudgetReport = (dimension: "tokens" | "cost", ceiling: number | undefined) => {
185
+ if (ceiling === undefined || ceiling <= 0) return undefined;
186
+ return (
187
+ run.budgetState?.[dimension] ?? budgetReport(dimension, run.budgetState?.accounting?.[dimension] ?? 0, ceiling)
188
+ );
189
+ };
190
+ const tokens = usageBudgetReport("tokens", run.budget?.maxTokens) as RunBudgetState["tokens"];
191
+ const cost = usageBudgetReport("cost", run.budget?.maxCost) as RunBudgetState["cost"];
192
+ const budgetState =
193
+ run.budget === undefined
194
+ ? undefined
195
+ : {
196
+ ...(run.budgetState ?? {}),
197
+ duration,
198
+ ...(tokens !== undefined ? { tokens } : {}),
199
+ ...(cost !== undefined ? { cost } : {}),
200
+ };
160
201
  return {
161
202
  runId: run.id,
162
203
  name: run.name,
163
204
  status: effectiveRunStatus(run),
164
205
  startedAt: run.startedAt,
165
206
  endedAt: run.endedAt,
166
- elapsedMs: elapsedRunMs(run, now),
207
+ elapsedMs,
208
+ ...(run.budget !== undefined ? { budget: run.budget } : {}),
209
+ ...(budgetState !== undefined ? { budgetState } : {}),
167
210
  activeStages: run.stages.filter(stageIsActive).map((stage) => ({
168
211
  stageId: stage.id,
169
212
  name: stage.name,
@@ -43,6 +43,14 @@ function statusRunHint(run: WorkflowRunStatusSummary): string | undefined {
43
43
  const tools = run.tools ?? [];
44
44
  const toolHint =
45
45
  tools.length > 0 ? `tools: ${tools.map((tool) => `${tool.name} (${tool.status})`).join(", ")}` : undefined;
46
+ const budgetHint =
47
+ [run.budgetState?.duration, run.budgetState?.tokens, run.budgetState?.cost]
48
+ .filter((budget): budget is NonNullable<typeof budget> => budget !== undefined)
49
+ .map(
50
+ (budget) =>
51
+ `${budget.dimension === "duration" ? "budget" : budget.dimension}: ${budget.reading}/${budget.ceiling}${budget.dimension === "duration" ? "ms" : ""} (${budget.percent.toFixed(1)}%)`,
52
+ )
53
+ .join(", ") || undefined;
46
54
  let primary: string | undefined;
47
55
  if (run.awaitingInputCount > 0) {
48
56
  const stages = run.awaitingInput
@@ -55,7 +63,8 @@ function statusRunHint(run: WorkflowRunStatusSummary): string | undefined {
55
63
  } else if (run.activeStages.length > 0) {
56
64
  primary = `stage: ${run.activeStages.map((stage) => stage.name).join(", ")}`;
57
65
  }
58
- if (primary !== undefined) return toolHint === undefined ? primary : `${primary} · ${toolHint}`;
66
+ if (primary !== undefined) return [primary, budgetHint, toolHint].filter((part) => part !== undefined).join(" · ");
67
+ if (budgetHint !== undefined) return toolHint === undefined ? budgetHint : `${budgetHint} · ${toolHint}`;
59
68
  if (toolHint !== undefined) return toolHint;
60
69
  if (run.error !== undefined && run.error.length > 0) return truncateStatusText(run.error);
61
70
  if (run.exitReason !== undefined && run.exitReason.length > 0) {
@@ -343,6 +343,7 @@ export async function workflowInterruptAction(args: WorkflowToolArgs): Promise<W
343
343
  async function resumeDurableShadow(
344
344
  runId: string,
345
345
  deps: Pick<WorkflowControlActionDeps, "getRuntime" | "policy" | "ensureWorkflowResourcesLoaded">,
346
+ budget?: WorkflowToolArgs["budget"],
346
347
  ): Promise<WorkflowToolResult> {
347
348
  const runtime = deps.getRuntime();
348
349
  let warning: string | undefined;
@@ -354,7 +355,11 @@ async function resumeDurableShadow(
354
355
  } catch (error) {
355
356
  warning = formatWorkflowResourceLoadWarning(error);
356
357
  }
357
- const resumed = await runtime.resumeDurableWorkflow(runId, { policy: deps.policy, actor: "agent" });
358
+ const resumed = await runtime.resumeDurableWorkflow(runId, {
359
+ policy: deps.policy,
360
+ actor: "agent",
361
+ ...(budget === undefined ? {} : { budget }),
362
+ });
358
363
  const message = warning === undefined ? resumed.message : `${warning}\n\n${resumed.message}`;
359
364
  return {
360
365
  action: "resume",
@@ -367,9 +372,14 @@ async function resumeDurableShadow(
367
372
  async function resumePreparedDurableTarget(
368
373
  runId: string,
369
374
  deps: Pick<WorkflowControlActionDeps, "getRuntime" | "policy">,
375
+ budget?: WorkflowToolArgs["budget"],
370
376
  ): Promise<WorkflowToolResult> {
371
377
  try {
372
- const resumed = await deps.getRuntime().resumeDurableWorkflow(runId, { policy: deps.policy, actor: "agent" });
378
+ const resumed = await deps.getRuntime().resumeDurableWorkflow(runId, {
379
+ policy: deps.policy,
380
+ actor: "agent",
381
+ ...(budget === undefined ? {} : { budget }),
382
+ });
373
383
  return {
374
384
  action: "resume",
375
385
  runId: resumed.ok ? resumed.runId : runId,
@@ -398,7 +408,7 @@ async function resolveExplicitDurableTarget(
398
408
  if (resolved.kind === "malformed") {
399
409
  return { action: "resume", runId: target, status: "noop", message: resolved.message };
400
410
  }
401
- if (resolved.kind === "durable") return resumePreparedDurableTarget(resolved.workflowId, deps);
411
+ if (resolved.kind === "durable") return resumePreparedDurableTarget(resolved.workflowId, deps, args.budget);
402
412
  if (resolved.kind === "live") {
403
413
  return workflowResumeAction({ ...args, runId: resolved.workflowId }, deps);
404
414
  }
@@ -424,7 +434,7 @@ async function resolveExplicitDurableTarget(
424
434
  message: `Workflow ${target} has no durable checkpoint or pending prompt progress and is not resumable.`,
425
435
  };
426
436
  }
427
- if (durableHandle !== undefined) return resumePreparedDurableTarget(target, deps);
437
+ if (durableHandle !== undefined) return resumePreparedDurableTarget(target, deps, args.budget);
428
438
  return { action: "resume", runId: target, status: "noop", message: `Run not found: ${target}` };
429
439
  }
430
440
 
@@ -450,7 +460,7 @@ export async function workflowResumeAction(
450
460
  const backend = getDurableBackend();
451
461
  const exact = store.runs().find((run) => run.id === target.runId);
452
462
  const shadow = exact === undefined ? "not_shadow" : classifyDurableResumeShadow(exact, store, { backend });
453
- if (shadow === "eligible") return resumeDurableShadow(target.runId, deps);
463
+ if (shadow === "eligible") return resumeDurableShadow(target.runId, deps, args.budget);
454
464
  if (shadow === "ineligible") {
455
465
  return {
456
466
  action: "resume",
@@ -488,16 +498,18 @@ export async function workflowResumeAction(
488
498
  !isPaused &&
489
499
  run.exitReason !== "quit" &&
490
500
  isWorkflowRunResumable(workflowRunResumeCandidate(run));
491
- if (isDurableAuthorExit) return resumeDurableShadow(stageRunId, deps);
501
+ if (isDurableAuthorExit) return resumeDurableShadow(stageRunId, deps, args.budget);
492
502
  if (isResumableContinuation) {
493
503
  try {
494
504
  await deps.ensureWorkflowResourcesLoaded();
495
505
  } catch (error) {
496
506
  warning = formatWorkflowResourceLoadWarning(error);
497
507
  }
498
- const continuation = await deps
499
- .getRuntime()
500
- .resumeFailedRun(stageRunId, stage.stageId, { policy: deps.policy, actor: "agent" });
508
+ const continuation = await deps.getRuntime().resumeFailedRun(stageRunId, stage.stageId, {
509
+ policy: deps.policy,
510
+ actor: "agent",
511
+ ...(args.budget === undefined ? {} : { budget: args.budget }),
512
+ });
501
513
  const message = warning === undefined ? continuation.message : `${warning}\n\n${continuation.message}`;
502
514
  return {
503
515
  action: "resume",
@@ -62,6 +62,20 @@ export function createContinuationReplayIndex(continuation: RunContinuationOpts
62
62
  markPromptAnswerReplayed: () => {},
63
63
  };
64
64
  }
65
+ if (continuation.resumeFromStageId === undefined) {
66
+ if (continuation.source.stages.length > 0)
67
+ throw new Error(
68
+ `atomic-workflows: insufficient_state: continuation has no resume stage in source run ${continuation.source.id}`,
69
+ );
70
+ return {
71
+ decide: (input) => ({
72
+ kind: "execute",
73
+ parentIds: input.parentIds,
74
+ answerReplay: "unavailable",
75
+ }),
76
+ markPromptAnswerReplayed: () => {},
77
+ };
78
+ }
65
79
  const resumeStage = continuation.source.stages.find((stage) => stage.id === continuation.resumeFromStageId);
66
80
  if (resumeStage === undefined) {
67
81
  throw new Error(
@@ -2,6 +2,7 @@ import { appendRunBlocked, appendRunEnd } from "../../shared/persistence-session
2
2
  import type { Store } from "../../shared/store.js";
3
3
  import { isTerminalRunStatus } from "../../shared/store-internal.js";
4
4
  import type {
5
+ RunBudgetState,
5
6
  RunSnapshot,
6
7
  RunStatus,
7
8
  StageSnapshot,
@@ -101,6 +102,9 @@ export interface RunFailureMetadata {
101
102
  readonly failedToolNodeId?: string;
102
103
  readonly resumable: boolean;
103
104
  readonly retryAfterMs?: number;
105
+ readonly endedAt?: number;
106
+ readonly result?: WorkflowOutputValues;
107
+ readonly budgetState?: RunBudgetState;
104
108
  }
105
109
 
106
110
  /**
@@ -435,30 +439,33 @@ export function finalizeKilledByFailure(
435
439
  onRunEnd,
436
440
  );
437
441
  }
438
-
439
442
  export function recordActiveBlockedFailure(
440
443
  runId: string,
441
444
  runSnapshot: RunSnapshot,
442
445
  activeStore: Store,
443
446
  persistence: WorkflowPersistencePort | undefined,
444
- metadata: RunFailureMetadata & { readonly failureRecoverability: "recoverable"; readonly failedStageId: string },
447
+ metadata: RunFailureMetadata & { readonly failureRecoverability: "recoverable" },
445
448
  ): RunResult {
446
449
  const blockedAt = Date.now();
450
+ const endedAt = metadata.budgetState?.systemOwnedStop === true ? blockedAt : undefined;
447
451
  const recorded = activeStore.recordRunBlocked(runId, metadata.errorMessage, {
448
452
  failureKind: metadata.failureKind,
449
453
  ...(metadata.failureCode !== undefined ? { failureCode: metadata.failureCode } : {}),
450
454
  failureRecoverability: "recoverable",
451
455
  ...(metadata.failureDisposition !== undefined ? { failureDisposition: metadata.failureDisposition } : {}),
452
456
  failureMessage: metadata.failureMessage,
453
- failedStageId: metadata.failedStageId,
457
+ ...(metadata.failedStageId !== undefined ? { failedStageId: metadata.failedStageId } : {}),
454
458
  resumable: true,
455
459
  ...(metadata.retryAfterMs !== undefined ? { retryAfterMs: metadata.retryAfterMs } : {}),
460
+ ...(endedAt !== undefined ? { endedAt } : {}),
461
+ ...(metadata.result !== undefined ? { result: metadata.result } : {}),
462
+ ...(metadata.budgetState !== undefined ? { budgetState: metadata.budgetState } : {}),
456
463
  blockedAt,
457
464
  });
458
465
  if (recorded && persistence !== undefined) {
459
466
  appendRunBlocked(persistence, {
460
467
  runId,
461
- failedStageId: metadata.failedStageId,
468
+ ...(metadata.failedStageId !== undefined ? { failedStageId: metadata.failedStageId } : {}),
462
469
  error: metadata.errorMessage,
463
470
  failureKind: metadata.failureKind,
464
471
  ...(metadata.failureCode !== undefined ? { failureCode: metadata.failureCode } : {}),
@@ -467,12 +474,16 @@ export function recordActiveBlockedFailure(
467
474
  ...(metadata.failureDisposition !== undefined ? { failureDisposition: metadata.failureDisposition } : {}),
468
475
  ...(metadata.retryAfterMs !== undefined ? { retryAfterMs: metadata.retryAfterMs } : {}),
469
476
  resumable: true,
477
+ ...(endedAt !== undefined ? { endedAt } : {}),
478
+ ...(metadata.result !== undefined ? { result: metadata.result } : {}),
479
+ ...(metadata.budgetState !== undefined ? { budgetState: metadata.budgetState } : {}),
470
480
  ts: blockedAt,
471
481
  });
472
482
  }
473
483
  return {
474
484
  runId,
475
485
  status: "running",
486
+ ...(metadata.result !== undefined ? { result: metadata.result } : {}),
476
487
  error: metadata.errorMessage,
477
488
  stages: [...runSnapshot.stages],
478
489
  toolNodes: [...(runSnapshot.toolNodes ?? [])],