@bastani/atomic 0.9.14-alpha.4 → 0.9.14-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/schemas.ts +5 -0
- package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +3 -1
- package/dist/builtin/subagents/src/runs/shared/progress-trend.ts +69 -0
- package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +12 -1
- package/dist/builtin/subagents/src/shared/types-results.ts +2 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +47 -1
- package/dist/builtin/workflows/README.md +6 -5
- package/dist/builtin/workflows/builtin/adversarial-verification-prompts.ts +13 -5
- package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +376 -89
- package/dist/builtin/workflows/builtin/adversarial-verification.d.ts +30 -6
- package/dist/builtin/workflows/builtin/adversarial-verification.ts +14 -9
- package/dist/builtin/workflows/builtin/generate-and-filter-prompts.ts +26 -3
- package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +18 -14
- package/dist/builtin/workflows/builtin/goal-artifacts.ts +9 -8
- package/dist/builtin/workflows/builtin/goal-convergence.ts +87 -0
- package/dist/builtin/workflows/builtin/goal-ledger.ts +4 -0
- package/dist/builtin/workflows/builtin/goal-prompts.ts +2 -0
- package/dist/builtin/workflows/builtin/goal-reducer.ts +6 -1
- package/dist/builtin/workflows/builtin/goal-reverify.ts +305 -0
- package/dist/builtin/workflows/builtin/goal-runner.ts +75 -10
- package/dist/builtin/workflows/builtin/goal-schemas.ts +7 -0
- package/dist/builtin/workflows/builtin/goal-types.ts +6 -0
- package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +94 -6
- package/dist/builtin/workflows/builtin/loop-until-done.d.ts +8 -0
- package/dist/builtin/workflows/builtin/loop-until-done.ts +15 -0
- package/dist/builtin/workflows/builtin/progress-scoring.ts +230 -0
- package/dist/builtin/workflows/builtin/ralph-core.ts +11 -0
- package/dist/builtin/workflows/builtin/ralph-review-gate.ts +1 -0
- package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +2 -0
- package/dist/builtin/workflows/builtin/ralph-runner.ts +60 -10
- package/dist/builtin/workflows/builtin/selection-math.ts +156 -0
- package/dist/builtin/workflows/builtin/shared-prompts.ts +5 -0
- package/dist/builtin/workflows/builtin/tournament-prompts.ts +57 -75
- package/dist/builtin/workflows/builtin/tournament-runner.ts +384 -178
- package/dist/builtin/workflows/builtin/tournament.d.ts +46 -17
- package/dist/builtin/workflows/builtin/tournament.ts +66 -32
- package/dist/builtin/workflows/builtin/verification-criteria.ts +330 -0
- package/dist/builtin/workflows/builtin/verification-prompts.ts +206 -0
- package/dist/builtin/workflows/builtin/verification-usage.ts +44 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/create-spec/SKILL.md +90 -30
- package/dist/builtin/workflows/skills/show-me/LICENSE.txt +21 -0
- package/dist/builtin/workflows/skills/show-me/SKILL.md +143 -0
- package/dist/builtin/workflows/src/authoring/workflow.ts +8 -0
- package/dist/builtin/workflows/src/authoring.d.ts +1 -1
- package/dist/builtin/workflows/src/durable/completed-catalog.ts +5 -2
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +1 -1
- package/dist/builtin/workflows/src/durable/resume-eligibility.ts +5 -3
- package/dist/builtin/workflows/src/durable/run-timing.ts +41 -10
- package/dist/builtin/workflows/src/durable/tool-primitive.ts +24 -2
- package/dist/builtin/workflows/src/engine/options.ts +1 -0
- package/dist/builtin/workflows/src/engine/primitives/workflow.ts +12 -3
- package/dist/builtin/workflows/src/engine/run-budget.ts +308 -0
- package/dist/builtin/workflows/src/engine/run-returned-status.ts +8 -0
- package/dist/builtin/workflows/src/engine/run-tool-node-lifecycle.ts +6 -0
- package/dist/builtin/workflows/src/engine/run.ts +124 -2
- package/dist/builtin/workflows/src/engine/runtime.ts +9 -0
- package/dist/builtin/workflows/src/extension/config-file-loader.ts +6 -0
- package/dist/builtin/workflows/src/extension/config-loader.ts +24 -1
- package/dist/builtin/workflows/src/extension/dispatcher.ts +6 -5
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +2 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +2975 -843
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +51 -4
- package/dist/builtin/workflows/src/extension/public-types.ts +3 -1
- package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +7 -1
- package/dist/builtin/workflows/src/extension/runtime.ts +22 -10
- package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +5 -0
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +16 -0
- package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +44 -1
- package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +10 -1
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +21 -9
- package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +14 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +15 -4
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +62 -5
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +4 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +2 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +3 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -1
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +1 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +1 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +7 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +7 -0
- package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +1 -0
- package/dist/builtin/workflows/src/shared/budget-meter.ts +34 -0
- package/dist/builtin/workflows/src/shared/budget.d.ts +67 -0
- package/dist/builtin/workflows/src/shared/budget.ts +127 -0
- package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +92 -8
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +11 -1
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +15 -3
- package/dist/builtin/workflows/src/shared/returned-run-status.ts +35 -2
- package/dist/builtin/workflows/src/shared/store-public-types.ts +4 -1
- package/dist/builtin/workflows/src/shared/store-run-methods.ts +8 -1
- package/dist/builtin/workflows/src/shared/store-stage-methods.ts +1 -0
- package/dist/builtin/workflows/src/shared/store-types.ts +24 -0
- package/dist/builtin/workflows/src/shared/types.ts +3 -0
- package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +1 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +3 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +3 -0
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +1 -0
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/extensions/ui-types.d.ts +13 -3
- package/dist/core/extensions/ui-types.d.ts.map +1 -1
- package/dist/core/extensions/ui-types.js +15 -3
- package/dist/core/extensions/ui-types.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +33 -3
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/main-deferred-startup.d.ts.map +1 -1
- package/dist/main-deferred-startup.js +6 -2
- package/dist/main-deferred-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +4 -0
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-tui.js +19 -1
- package/dist/modes/interactive/interactive-tui.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts +7 -0
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +94 -37
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +1 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +15 -2
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.d.ts +3 -2
- package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.js +5 -2
- package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
- package/docs/extensions.md +1 -1
- package/docs/quickstart.md +1 -0
- package/docs/skills.md +4 -0
- package/docs/workflows.md +74 -10
- package/npm-shrinkwrap.json +29 -29
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
+
}
|