@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,5 +1,6 @@
1
+ import { BUDGET_WRAP_UP_PROMPT, WorkflowBudgetExceededError } from "../../engine/run-budget.js";
1
2
  import { rebasedStageStartedAt } from "../../shared/timing.js";
2
- import type { StageOptions } from "../../shared/types.js";
3
+ import type { StageOptions, WorkflowModelUsage } from "../../shared/types.js";
3
4
  import type { ConcurrencyLimiter } from "../shared/concurrency.js";
4
5
  import { raceAbort } from "./executor-abort.js";
5
6
  import { hasExplicitFastModeCandidate } from "./executor-direct-helpers.js";
@@ -119,10 +120,26 @@ export function createTrackedStageCaller(input: {
119
120
  captureChatAnswer();
120
121
  return { result, chatAnswerObserved };
121
122
  };
123
+ const deliverBudgetWrapUp = async (): Promise<never> => {
124
+ let summary: string | undefined;
125
+ let usage: WorkflowModelUsage | undefined;
126
+ try {
127
+ const response = await runtime.raceStageSessionHeartbeat(
128
+ raceAbort(runtime.innerCtx.prompt(BUDGET_WRAP_UP_PROMPT), runtime.signal),
129
+ );
130
+ summary = typeof response === "string" ? response : runtime.innerCtx.__getLastAssistantText();
131
+ usage = runtime.innerCtx.__modelFallbackMeta().modelAttempts?.at(-1)?.usage;
132
+ } catch {
133
+ // A failed or aborted wrap-up did not deliver a summary and must not
134
+ // consume the once-per-run delivery allowance.
135
+ }
136
+ throw runtime.budget.finishWrapUp(runtime.name, summary, usage, summary !== undefined);
137
+ };
122
138
 
123
139
  return async <T>(call: () => Promise<T>, eagerSessionOrOptions?: boolean | TrackedStageCallOptions): Promise<T> => {
124
140
  const callOptions = normalizeTrackedStageCallOptions(eagerSessionOrOptions);
125
141
  runtime.exit.throwIfWorkflowExitSelected();
142
+ if (runtime.budget.enabled) await runtime.budget.stopAtBoundaryAsync(runtime.name);
126
143
  await runtime.scheduler.waitForStageRelease(runtime.stageId, runtime.releaseLiveHandle);
127
144
  if (runtime.state.stageFinalized && !callOptions.allowFinalized) throw runtime.parallelFailFastError();
128
145
 
@@ -142,6 +159,8 @@ export function createTrackedStageCaller(input: {
142
159
  // final durable checkpoint can replace it without touching a real
143
160
  // failure/skip classification recorded by the catch block.
144
161
  let terminalStateIsSuccess = false;
162
+ let stageResultBeforeBudget: string | undefined;
163
+ const unregisterBudgetWrapUp = runtime.budget.registerWrapUp(runtime.name, deliverBudgetWrapUp);
145
164
  try {
146
165
  let refreshedParentIds: readonly string[] | undefined;
147
166
  if (
@@ -210,8 +229,10 @@ export function createTrackedStageCaller(input: {
210
229
  runtime.state.askUserQuestionObservedThisTurn = false;
211
230
  runtime.state.chatAnswerObservedThisTurn = false;
212
231
  result = await runtime.raceStageSessionHeartbeat(raceAbort(call(), runtime.signal));
232
+ if (runtime.budget.enabled && typeof result === "string") stageResultBeforeBudget = result;
213
233
  const initialDrain = await drainResumeContinuations(result);
214
234
  result = initialDrain.result;
235
+ if (runtime.budget.enabled && typeof result === "string") stageResultBeforeBudget = result;
215
236
  let repeatReadinessAfterChatTurn = initialDrain.chatAnswerObserved;
216
237
 
217
238
  if (
@@ -239,6 +260,7 @@ export function createTrackedStageCaller(input: {
239
260
  result = (await runtime.raceStageSessionHeartbeat(
240
261
  raceAbort(runtime.innerCtx.prompt(decision.message), runtime.signal),
241
262
  )) as T;
263
+ if (runtime.budget.enabled && typeof result === "string") stageResultBeforeBudget = result;
242
264
  } else {
243
265
  runtime.state.waitingForStageChatTurn = true;
244
266
  try {
@@ -256,10 +278,15 @@ export function createTrackedStageCaller(input: {
256
278
  runtime.state.waitingForStageChatTurn = false;
257
279
  }
258
280
  if (runtime.signal.aborted) break;
259
- result = (runtime.innerCtx.__getLastAssistantText() ?? result) as T;
281
+ const responseText = runtime.innerCtx.__getLastAssistantText();
282
+ if (responseText !== undefined) {
283
+ result = responseText as T;
284
+ if (runtime.budget.enabled) stageResultBeforeBudget = responseText;
285
+ }
260
286
  }
261
287
  const continuationDrain = await drainResumeContinuations(result);
262
288
  result = continuationDrain.result;
289
+ if (runtime.budget.enabled && typeof result === "string") stageResultBeforeBudget = result;
263
290
  repeatReadinessAfterChatTurn ||= continuationDrain.chatAnswerObserved;
264
291
  if (runtime.innerCtx.__structuredOutputFinalized()) break;
265
292
  }
@@ -272,9 +299,14 @@ export function createTrackedStageCaller(input: {
272
299
  } finally {
273
300
  runtime.signal.removeEventListener("abort", abortSession);
274
301
  }
302
+ if (runtime.budget.enabled) stageResultBeforeBudget ??= runtime.innerCtx.__getLastAssistantText();
303
+ runtime.applyModelFallbackMeta(runtime.innerCtx.__modelFallbackMeta());
304
+ const afterBudget = runtime.budget.checkpoint(runtime.name);
305
+ if (afterBudget.kind === "wrap_up") await runtime.budget.deliverWrapUp(runtime.name);
306
+ if (afterBudget.kind === "exhausted" && runtime.budget.enabled)
307
+ await runtime.budget.stopAtBoundaryAsync(runtime.name);
275
308
  await runtime.innerCtx.__closeGeneration();
276
309
  await runtime.captureStageSessionMeta({ awaitDurable: true });
277
- runtime.applyModelFallbackMeta(runtime.innerCtx.__modelFallbackMeta());
278
310
  if (
279
311
  trackStageLifecycle &&
280
312
  runtime.stageFailFastScope?.failed === true &&
@@ -285,7 +317,9 @@ export function createTrackedStageCaller(input: {
285
317
  }
286
318
  if (trackStageLifecycle && runtime.state.stageFinalized) throw runtime.parallelFailFastError();
287
319
  if (trackStageLifecycle) {
288
- const assistantText = runtime.innerCtx.__getLastAssistantText();
320
+ const assistantText = runtime.budget.enabled
321
+ ? (stageResultBeforeBudget ?? runtime.innerCtx.__getLastAssistantText())
322
+ : runtime.innerCtx.__getLastAssistantText();
289
323
  terminalStateIsSuccess = true;
290
324
  applyTerminalStageState = () => {
291
325
  runtime.stageSnapshot.status = "completed";
@@ -295,7 +329,29 @@ export function createTrackedStageCaller(input: {
295
329
  return result;
296
330
  } catch (err) {
297
331
  const workflowExitAbort = runtime.signal.aborted ? runtime.exit.currentWorkflowExitAbortReason() : undefined;
298
- if (workflowExitAbort !== undefined && !runtime.state.skippedForParallelFailFast) {
332
+ const budgetError = err instanceof WorkflowBudgetExceededError ? err : undefined;
333
+ if (budgetError !== undefined && trackStageLifecycle && !runtime.state.skippedForParallelFailFast) {
334
+ let selectedBudgetError = budgetError;
335
+ if (
336
+ runtime.activeStore.runs().find((run) => run.id === runtime.runId)?.budgetState?.wrapUpCompleted !== true
337
+ ) {
338
+ try {
339
+ await runtime.budget.deliverWrapUp(runtime.name);
340
+ } catch (wrapUpError) {
341
+ if (wrapUpError instanceof WorkflowBudgetExceededError) selectedBudgetError = wrapUpError;
342
+ }
343
+ }
344
+ applyTerminalStageState = () => {
345
+ if (stageResultBeforeBudget !== undefined) {
346
+ runtime.stageSnapshot.status = "completed";
347
+ runtime.stageSnapshot.result = stageResultBeforeBudget;
348
+ } else {
349
+ runtime.stageSnapshot.status = "failed";
350
+ runtime.stageSnapshot.error = selectedBudgetError.message;
351
+ }
352
+ };
353
+ throw selectedBudgetError;
354
+ } else if (workflowExitAbort !== undefined && !runtime.state.skippedForParallelFailFast) {
299
355
  runtime.state.stageClosedByWorkflowExit = true;
300
356
  if (trackStageLifecycle && !isTerminalStage(runtime.stageSnapshot)) {
301
357
  const skippedReason = runtime.exit.workflowExitSkippedReason(workflowExitAbort.reason);
@@ -310,6 +366,7 @@ export function createTrackedStageCaller(input: {
310
366
  }
311
367
  throw err;
312
368
  } finally {
369
+ unregisterBudgetWrapUp();
313
370
  // Finalization, handle release, and limiter release are each independent.
314
371
  // If finalizeStageSnapshot() throws, the limiter must still be released
315
372
  // so the concurrency semaphore is not leaked.
@@ -1,6 +1,7 @@
1
1
  import { runCallback, runSynchronousCallback } from "@bastani/atomic";
2
2
  import type { GraphFrontierTracker } from "../../engine/graph-inference.js";
3
3
  import type { EngineStageRuntimeOptions } from "../../engine/options.js";
4
+ import type { RunBudgetController } from "../../engine/run-budget.js";
4
5
  import { appendStageEnd, appendStageStart } from "../../shared/persistence-session-entries.js";
5
6
  import { buildStagePromptAdapter } from "../../shared/stage-prompt.js";
6
7
  import { stageUiBroker } from "../../shared/stage-ui-broker.js";
@@ -58,6 +59,7 @@ export function createWorkflowStageFactory(input: {
58
59
  readonly gitWorktreeSetupCache: GitWorktreeSetupCache;
59
60
  readonly stageRegistry: StageControlRegistry;
60
61
  readonly exit: WorkflowExitManager;
62
+ readonly budget: RunBudgetController;
61
63
  readonly classifyExecutorFailure: (error: unknown) => WorkflowFailure;
62
64
  readonly createMcpScope: (stageId: string, options: StageOptions | undefined) => StageMcpScope;
63
65
  }): (name: string, options?: StageOptions, stageFailFastScope?: ParallelFailFastScope) => StageContextWithMeta {
@@ -350,6 +352,7 @@ export function createWorkflowStageFactory(input: {
350
352
  ...(stageSnapshot.skippedReason !== undefined ? { skippedReason: stageSnapshot.skippedReason } : {}),
351
353
  ...(stageSnapshot.sessionId !== undefined ? { sessionId: stageSnapshot.sessionId } : {}),
352
354
  ...(stageSnapshot.sessionFile !== undefined ? { sessionFile: stageSnapshot.sessionFile } : {}),
355
+ ...(stageSnapshot.modelAttempts !== undefined ? { modelAttempts: stageSnapshot.modelAttempts } : {}),
353
356
  ...(stageSnapshot.result !== undefined && stageSnapshot.status === "completed"
354
357
  ? { summary: stageSnapshot.result }
355
358
  : {}),
@@ -381,6 +384,7 @@ export function createWorkflowStageFactory(input: {
381
384
  scheduler: input.scheduler,
382
385
  signal: input.signal,
383
386
  exit: input.exit,
387
+ budget: input.budget,
384
388
  classifyExecutorFailure: input.classifyExecutorFailure,
385
389
  mcpScope: input.createMcpScope(stageId, options),
386
390
  ...(stageFailFastScope !== undefined ? { stageFailFastScope } : {}),
@@ -1,4 +1,5 @@
1
1
  import type { EngineStageRuntimeOptions } from "../../engine/options.js";
2
+ import type { RunBudgetController } from "../../engine/run-budget.js";
2
3
  import type { Store } from "../../shared/store.js";
3
4
  import type { StageNotice, StageSnapshot } from "../../shared/store-types.js";
4
5
  import type { StageContext } from "../../shared/types.js";
@@ -44,6 +45,7 @@ export interface LiveStageRuntime {
44
45
  readonly scheduler: StageScheduler;
45
46
  readonly signal: AbortSignal;
46
47
  readonly exit: WorkflowExitManager;
48
+ readonly budget: RunBudgetController;
47
49
  readonly classifyExecutorFailure: (error: unknown) => WorkflowFailure;
48
50
  readonly mcpScope: StageMcpScope;
49
51
  readonly stageFailFastScope?: ParallelFailFastScope;
@@ -29,7 +29,7 @@ export interface ResolvedInputs extends WorkflowInputValues {}
29
29
 
30
30
  export interface RunContinuationOpts {
31
31
  readonly source: RunSnapshot;
32
- readonly resumeFromStageId: string;
32
+ readonly resumeFromStageId?: string;
33
33
  }
34
34
 
35
35
  export interface StageSessionCheckpointOptions {
@@ -140,6 +140,8 @@ export interface RunOpts
140
140
  readonly stageId: string;
141
141
  readonly rootRunId: string;
142
142
  };
143
+ /** Internal root budget scope shared by nested workflow runs. */
144
+ rootBudget?: import("../../engine/run-budget.js").RunBudgetController;
143
145
  onRunStart?: (snapshot: RunSnapshot) => void;
144
146
  onStageStart?: (runId: string, snapshot: StageSnapshot) => void;
145
147
  onStageEnd?: (runId: string, snapshot: StageSnapshot) => unknown;
@@ -424,7 +424,16 @@ export class StageSessionController {
424
424
  const activeSession = await this.ensureSession(consumer);
425
425
  const resumedText = this.pendingCreationResumeMessage;
426
426
  this.pendingCreationResumeMessage = undefined;
427
- await this.promptWithThrownErrorRetry(activeSession, resumedText ?? text, sdkOptions);
427
+ const value = activeSession.model ?? "default";
428
+ const candidate = { id: workflowModelId(value) ?? "default", value };
429
+ this.beginAttemptUsage(activeSession);
430
+ try {
431
+ await this.promptWithThrownErrorRetry(activeSession, resumedText ?? text, sdkOptions);
432
+ } catch (error) {
433
+ await this.handleCandidateFailure(error, candidate, [candidate], 0);
434
+ throw error;
435
+ }
436
+ this.recordSuccessfulAttempt(candidate);
428
437
  } catch (error) {
429
438
  if (error instanceof StageSessionCreationCancelled) return;
430
439
  throw error;
@@ -372,6 +372,7 @@ export type WorkflowTaskSessionOptions = StageOptions & WorkflowTaskSessionField
372
372
  export interface WorkflowRunChildOptions<TInputs extends WorkflowInputValues = WorkflowInputValues> {
373
373
  readonly inputs?: TInputs;
374
374
  readonly stageName?: string;
375
+ readonly budget?: import("./budget.js").WorkflowBudget;
375
376
  }
376
377
  type WorkflowRequiredKeys<T extends object> = {
377
378
  [K in keyof T]-?: Record<never, never> extends Pick<T, K> ? never : K;
@@ -446,6 +446,7 @@ export type WorkflowTaskSessionOptions = StageOptions & WorkflowTaskSessionField
446
446
  export interface WorkflowRunChildOptions<TInputs extends WorkflowInputValues = WorkflowInputValues> {
447
447
  readonly inputs?: TInputs;
448
448
  readonly stageName?: string;
449
+ readonly budget?: import("./budget.js").WorkflowBudget;
449
450
  }
450
451
 
451
452
  type WorkflowRequiredKeys<T extends object> = {
@@ -3,6 +3,7 @@ import type { KeybindingsManager, Theme } from "@bastani/atomic";
3
3
  import type { Component, OverlayHandle, OverlayOptions, TUI } from "@earendil-works/pi-tui";
4
4
  import type { TSchema } from "typebox";
5
5
  import type { RunStatus, StageAdapters, StageContext, StageOptions, WorkflowAction, WorkflowActor, WorkflowArtifact, WorkflowChainOptions, WorkflowChildResult, WorkflowContextMode, WorkflowDetailsMode, WorkflowDetailsStatus, WorkflowExecutionMode, WorkflowExitOptions, WorkflowInputSchemaMap, WorkflowInputValues, WorkflowMcpPort, WorkflowModelCatalogPort, WorkflowOutputSchemaMap, WorkflowOutputValues, WorkflowParallelOptions, WorkflowPersistencePort, WorkflowRunChildArgs, WorkflowSerializableObject, WorkflowSerializableValue, WorkflowTaskOptions, WorkflowTaskResult, WorkflowTaskStep } from "./authoring-contract-stage.js";
6
+ import type { EffectiveBudget, WorkflowBudget } from "./budget.js";
6
7
  export type WorkflowCustomUiComponent = Component & {
7
8
  dispose?(): void;
8
9
  };
@@ -138,6 +139,8 @@ export interface WorkflowRuntimeConfig {
138
139
  readonly statusFile: boolean;
139
140
  readonly statusFilePath?: string;
140
141
  readonly resumeInFlight: "ask" | "auto" | "never";
142
+ /** Resolved default budget for workflow runs. */
143
+ readonly budget?: EffectiveBudget;
141
144
  readonly worktree?: {
142
145
  readonly symlinkDirectories: readonly string[];
143
146
  };
@@ -157,6 +160,8 @@ export interface WorkflowDefinition<TInputs extends WorkflowInputValues = Workfl
157
160
  readonly description: string;
158
161
  readonly autoAttach?: true;
159
162
  readonly heartbeatIntervalMinutes: number;
163
+ /** Optional budget declaration resolved when this workflow runs. */
164
+ readonly budget?: WorkflowBudget;
160
165
  readonly inputs: WorkflowInputSchemaMap;
161
166
  readonly outputs?: WorkflowOutputSchemaMap;
162
167
  readonly inputBindings?: WorkflowInputBindings;
@@ -208,6 +213,8 @@ export interface RunOpts {
208
213
  readonly signal?: AbortSignal;
209
214
  readonly deferWorkflowStart?: boolean;
210
215
  readonly config?: WorkflowRuntimeConfig;
216
+ /** Per-run budget override. Each field resolves over definition and config values. */
217
+ readonly budget?: WorkflowBudget;
211
218
  readonly models?: WorkflowModelCatalogPort;
212
219
  readonly registry?: object;
213
220
  readonly depth?: number;
@@ -33,6 +33,7 @@ import type {
33
33
  WorkflowTaskResult,
34
34
  WorkflowTaskStep,
35
35
  } from "./authoring-contract-stage.js";
36
+ import type { EffectiveBudget, WorkflowBudget } from "./budget.js";
36
37
 
37
38
  export type WorkflowCustomUiComponent = Component & { dispose?(): void };
38
39
  export type WorkflowCustomUiTui = TUI;
@@ -216,6 +217,8 @@ export interface WorkflowRuntimeConfig {
216
217
  readonly statusFile: boolean;
217
218
  readonly statusFilePath?: string;
218
219
  readonly resumeInFlight: "ask" | "auto" | "never";
220
+ /** Resolved default budget for workflow runs. */
221
+ readonly budget?: EffectiveBudget;
219
222
  readonly worktree?: {
220
223
  readonly symlinkDirectories: readonly string[];
221
224
  };
@@ -243,6 +246,8 @@ export interface WorkflowDefinition<
243
246
  readonly description: string;
244
247
  readonly autoAttach?: true;
245
248
  readonly heartbeatIntervalMinutes: number;
249
+ /** Optional budget declaration resolved when this workflow runs. */
250
+ readonly budget?: WorkflowBudget;
246
251
  readonly inputs: WorkflowInputSchemaMap;
247
252
  readonly outputs?: WorkflowOutputSchemaMap;
248
253
  readonly inputBindings?: WorkflowInputBindings;
@@ -299,6 +304,8 @@ export interface RunOpts {
299
304
  readonly signal?: AbortSignal;
300
305
  readonly deferWorkflowStart?: boolean;
301
306
  readonly config?: WorkflowRuntimeConfig;
307
+ /** Per-run budget override. Each field resolves over definition and config values. */
308
+ readonly budget?: WorkflowBudget;
302
309
  readonly models?: WorkflowModelCatalogPort;
303
310
  readonly registry?: object;
304
311
  readonly depth?: number;
@@ -8,3 +8,4 @@
8
8
  export type { Static, TSchema } from "typebox";
9
9
  export * from "./authoring-contract-stage.js";
10
10
  export * from "./authoring-contract-ui.js";
11
+ export type { EffectiveBudget, ResolveBudgetLayers, WorkflowBudget } from "./budget.js";
@@ -0,0 +1,34 @@
1
+ import { fold_usage } from "../../builtin/verification-usage.js";
2
+ import type { RunSnapshot } from "./store-types.js";
3
+ import { elapsedRunMs } from "./timing.js";
4
+ export type RunMeterCounters = Readonly<Record<"input" | "output" | "cacheRead" | "cacheWrite", number>>;
5
+ export type RunMeters = Readonly<{ durationMs: number; tokens: number; cost: number; perCounter: RunMeterCounters }>;
6
+ /** A run and its nested child scopes, used by the tree-wide usage meter. */
7
+ export type RunUsageTree =
8
+ | { readonly run: RunSnapshot; readonly children?: readonly RunUsageTree[] }
9
+ | (RunSnapshot & { readonly children?: readonly RunUsageTree[] });
10
+ type UsageCounters = RunMeterCounters & { readonly cost: number };
11
+ const usageFields = ["input", "output", "cacheRead", "cacheWrite", "cost"] as const;
12
+ const addUsage = (left: UsageCounters, right: UsageCounters): UsageCounters =>
13
+ Object.fromEntries(usageFields.map((field) => [field, left[field] + right[field]])) as unknown as UsageCounters;
14
+ function foldTreeUsage(tree: RunUsageTree): UsageCounters {
15
+ const run = "run" in tree ? tree.run : tree;
16
+ const folded = fold_usage(
17
+ run.stages
18
+ .filter((stage) => stage.replayed !== true)
19
+ .map((stage) => ({ stageName: stage.name, text: stage.result ?? "", modelAttempts: stage.modelAttempts })),
20
+ );
21
+ return (tree.children ?? []).reduce<UsageCounters>((total, child) => addUsage(total, foldTreeUsage(child)), folded);
22
+ }
23
+ /** Measure one run scope without mutating it or charging a budget. */
24
+ export function meter_run(tree: RunUsageTree, now: number): RunMeters {
25
+ const usage = foldTreeUsage(tree);
26
+ return {
27
+ durationMs: elapsedRunMs("run" in tree ? tree.run : tree, now),
28
+ tokens: usage.input + usage.output,
29
+ cost: usage.cost,
30
+ perCounter: Object.fromEntries(
31
+ usageFields.filter((field) => field !== "cost").map((field) => [field, usage[field]]),
32
+ ) as RunMeterCounters,
33
+ };
34
+ }
@@ -0,0 +1,67 @@
1
+ /** Pure workflow run-budget declarations and resolution. */
2
+ export type BudgetDimension = "duration" | "tokens" | "cost";
3
+ export interface BudgetReport {
4
+ readonly dimension: BudgetDimension;
5
+ readonly reading: number;
6
+ readonly ceiling: number;
7
+ readonly percent: number;
8
+ }
9
+ export type DurationBudgetReport = BudgetReport & {
10
+ readonly dimension: "duration";
11
+ };
12
+ export type UsageBudgetReport = BudgetReport & {
13
+ readonly dimension: "tokens" | "cost";
14
+ };
15
+ export interface WorkflowBudget {
16
+ readonly maxDurationMs?: number;
17
+ readonly maxTokens?: number;
18
+ readonly maxCost?: number;
19
+ readonly warnAtPercent?: number;
20
+ }
21
+ export type DurationBudgetCheck = {
22
+ readonly kind: "continue";
23
+ readonly report: DurationBudgetReport;
24
+ readonly warning: boolean;
25
+ } | {
26
+ readonly kind: "exhausted";
27
+ readonly report: DurationBudgetReport;
28
+ };
29
+ export declare function enforceDurationBudget(reading: number, budget: EffectiveBudget, options?: {
30
+ readonly warned?: boolean;
31
+ }): DurationBudgetCheck;
32
+ export type UsageBudgetCheck = {
33
+ readonly kind: "continue";
34
+ readonly report: UsageBudgetReport;
35
+ readonly warning: boolean;
36
+ } | {
37
+ readonly kind: "exhausted";
38
+ readonly report: UsageBudgetReport;
39
+ };
40
+ export declare function enforceUsageBudget(dimension: "tokens" | "cost", reading: number, ceiling: number, warnAtPercent: number, options?: {
41
+ readonly warned?: boolean;
42
+ }): UsageBudgetCheck;
43
+ /** A fully resolved budget. Create one only with {@link resolve_budget}. */
44
+ declare class ResolvedWorkflowBudget {
45
+ private readonly brand;
46
+ readonly maxDurationMs: number;
47
+ readonly maxTokens: number;
48
+ readonly maxCost: number;
49
+ readonly warnAtPercent: number;
50
+ private constructor();
51
+ static resolve(layers: ResolveBudgetLayers): ResolvedWorkflowBudget;
52
+ }
53
+ /** A validated, fully resolved workflow budget. */
54
+ export type EffectiveBudget = ResolvedWorkflowBudget;
55
+ /** Budget declarations in precedence order: run > definition > config. */
56
+ export interface ResolveBudgetLayers {
57
+ readonly config?: WorkflowBudget;
58
+ readonly definition?: WorkflowBudget;
59
+ readonly run?: WorkflowBudget;
60
+ }
61
+ /** Return a reason when a budget declaration is invalid. */
62
+ export declare function validateWorkflowBudget(value: unknown, label?: string): string | null;
63
+ /** Reject an invalid optional budget declaration. */
64
+ export declare function assertWorkflowBudget(value: WorkflowBudget | undefined, label?: string): void;
65
+ /** Resolve budget declarations, with later layers winning independently per field. */
66
+ export declare function resolve_budget(layers: ResolveBudgetLayers): EffectiveBudget;
67
+ export {};
@@ -0,0 +1,127 @@
1
+ /** Pure workflow run-budget declarations and resolution. */
2
+
3
+ export type BudgetDimension = "duration" | "tokens" | "cost";
4
+ export interface BudgetReport {
5
+ readonly dimension: BudgetDimension;
6
+ readonly reading: number;
7
+ readonly ceiling: number;
8
+ readonly percent: number;
9
+ }
10
+ export type DurationBudgetReport = BudgetReport & { readonly dimension: "duration" };
11
+ export type UsageBudgetReport = BudgetReport & { readonly dimension: "tokens" | "cost" };
12
+ export interface WorkflowBudget {
13
+ readonly maxDurationMs?: number;
14
+ readonly maxTokens?: number;
15
+ readonly maxCost?: number;
16
+ readonly warnAtPercent?: number;
17
+ }
18
+
19
+ export type DurationBudgetCheck =
20
+ | { readonly kind: "continue"; readonly report: DurationBudgetReport; readonly warning: boolean }
21
+ | { readonly kind: "exhausted"; readonly report: DurationBudgetReport };
22
+ export function enforceDurationBudget(
23
+ reading: number,
24
+ budget: EffectiveBudget,
25
+ options: { readonly warned?: boolean } = {},
26
+ ): DurationBudgetCheck {
27
+ const ceiling = budget.maxDurationMs;
28
+ const report: DurationBudgetReport = {
29
+ dimension: "duration",
30
+ reading,
31
+ ceiling,
32
+ percent: ceiling === 0 ? 0 : (reading / ceiling) * 100,
33
+ };
34
+ if (ceiling > 0 && reading >= ceiling) return { kind: "exhausted", report };
35
+ const warning = options.warned !== true && budget.warnAtPercent > 0 && report.percent >= budget.warnAtPercent;
36
+ return { kind: "continue", report, warning };
37
+ }
38
+ export type UsageBudgetCheck =
39
+ | { readonly kind: "continue"; readonly report: UsageBudgetReport; readonly warning: boolean }
40
+ | { readonly kind: "exhausted"; readonly report: UsageBudgetReport };
41
+ export function enforceUsageBudget(
42
+ dimension: "tokens" | "cost",
43
+ reading: number,
44
+ ceiling: number,
45
+ warnAtPercent: number,
46
+ options: { readonly warned?: boolean } = {},
47
+ ): UsageBudgetCheck {
48
+ const report: UsageBudgetReport = { dimension, reading, ceiling, percent: ceiling ? (reading / ceiling) * 100 : 0 };
49
+ if (ceiling > 0 && reading >= ceiling) return { kind: "exhausted", report };
50
+ const warning = options.warned !== true && warnAtPercent > 0 && report.percent >= warnAtPercent;
51
+ return { kind: "continue", report, warning };
52
+ }
53
+ /** A fully resolved budget. Create one only with {@link resolve_budget}. */
54
+ class ResolvedWorkflowBudget {
55
+ private declare readonly brand: undefined;
56
+ readonly maxDurationMs: number;
57
+ readonly maxTokens: number;
58
+ readonly maxCost: number;
59
+ readonly warnAtPercent: number;
60
+
61
+ private constructor(maxDurationMs: number, maxTokens: number, maxCost: number, warnAtPercent: number) {
62
+ this.maxDurationMs = maxDurationMs;
63
+ this.maxTokens = maxTokens;
64
+ this.maxCost = maxCost;
65
+ this.warnAtPercent = warnAtPercent;
66
+ }
67
+
68
+ static resolve(layers: ResolveBudgetLayers): ResolvedWorkflowBudget {
69
+ assertWorkflowBudget(layers.config, "config budget");
70
+ assertWorkflowBudget(layers.definition, "definition budget");
71
+ assertWorkflowBudget(layers.run, "run budget");
72
+
73
+ return new ResolvedWorkflowBudget(
74
+ layers.run?.maxDurationMs ?? layers.definition?.maxDurationMs ?? layers.config?.maxDurationMs ?? 0,
75
+ layers.run?.maxTokens ?? layers.definition?.maxTokens ?? layers.config?.maxTokens ?? 0,
76
+ layers.run?.maxCost ?? layers.definition?.maxCost ?? layers.config?.maxCost ?? 0,
77
+ layers.run?.warnAtPercent ?? layers.definition?.warnAtPercent ?? layers.config?.warnAtPercent ?? 80,
78
+ );
79
+ }
80
+ }
81
+
82
+ /** A validated, fully resolved workflow budget. */
83
+ export type EffectiveBudget = ResolvedWorkflowBudget;
84
+
85
+ /** Budget declarations in precedence order: run > definition > config. */
86
+ export interface ResolveBudgetLayers {
87
+ readonly config?: WorkflowBudget;
88
+ readonly definition?: WorkflowBudget;
89
+ readonly run?: WorkflowBudget;
90
+ }
91
+
92
+ /** Return a reason when a budget declaration is invalid. */
93
+ export function validateWorkflowBudget(value: unknown, label = "budget"): string | null {
94
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
95
+ return `"${label}" must be a JSON object, got ${JSON.stringify(typeof value)}`;
96
+ }
97
+
98
+ const budget = value as Record<string, unknown>;
99
+ for (const field of ["maxDurationMs", "maxTokens"] as const) {
100
+ const limit = budget[field];
101
+ if (
102
+ field in budget &&
103
+ (typeof limit !== "number" || !Number.isFinite(limit) || !Number.isInteger(limit) || limit < 0)
104
+ ) {
105
+ return `"${label}.${field}" must be a non-negative finite integer, got ${JSON.stringify(limit)}`;
106
+ }
107
+ }
108
+ for (const field of ["maxCost", "warnAtPercent"] as const) {
109
+ const limit = budget[field];
110
+ if (field in budget && (typeof limit !== "number" || !Number.isFinite(limit) || limit < 0)) {
111
+ return `"${label}.${field}" must be a non-negative finite number, got ${JSON.stringify(limit)}`;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /** Reject an invalid optional budget declaration. */
118
+ export function assertWorkflowBudget(value: WorkflowBudget | undefined, label = "budget"): void {
119
+ if (value === undefined) return;
120
+ const reason = validateWorkflowBudget(value, label);
121
+ if (reason !== null) throw new TypeError(reason);
122
+ }
123
+
124
+ /** Resolve budget declarations, with later layers winning independently per field. */
125
+ export function resolve_budget(layers: ResolveBudgetLayers): EffectiveBudget {
126
+ return ResolvedWorkflowBudget.resolve(layers);
127
+ }