@bastani/atomic 0.8.21 → 0.8.22
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 +66 -9
- package/dist/builtin/intercom/broker/broker.ts +3 -3
- package/dist/builtin/intercom/config.ts +3 -3
- package/dist/builtin/intercom/index.ts +1 -1
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/ui/compose.ts +2 -2
- package/dist/builtin/mcp/host-html-template.ts +0 -3
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +20 -4
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
- package/dist/builtin/subagents/agents/debugger.md +6 -6
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/builtin/subagents/skills/browser-use/SKILL.md +234 -0
- package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +76 -0
- package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +92 -0
- package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
- package/dist/builtin/subagents/src/agents/skills.ts +19 -1
- package/dist/builtin/subagents/src/extension/index.ts +24 -22
- package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +7 -1
- package/dist/builtin/subagents/src/runs/background/async-execution.ts +23 -7
- package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +98 -3
- package/dist/builtin/subagents/src/runs/background/async-status.ts +3 -1
- package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -1
- package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +3 -0
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +37 -12
- package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +15 -15
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +26 -2
- package/dist/builtin/subagents/src/runs/shared/nested-render.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +7 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +28 -1
- package/dist/builtin/subagents/src/shared/fast-mode.ts +80 -0
- package/dist/builtin/subagents/src/shared/formatters.ts +4 -2
- package/dist/builtin/subagents/src/shared/types.ts +4 -2
- package/dist/builtin/subagents/src/shared/utils.ts +3 -61
- package/dist/builtin/subagents/src/tui/render.ts +303 -157
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +113 -35
- package/dist/builtin/workflows/README.md +228 -41
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +535 -541
- package/dist/builtin/workflows/builtin/goal.ts +39 -25
- package/dist/builtin/workflows/builtin/open-claude-design.ts +66 -69
- package/dist/builtin/workflows/builtin/ralph.ts +21 -21
- package/dist/builtin/workflows/package.json +6 -5
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
- package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +2 -2
- package/dist/builtin/workflows/src/extension/discovery.ts +25 -146
- package/dist/builtin/workflows/src/extension/dispatcher.ts +72 -24
- package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +363 -0
- package/dist/builtin/workflows/src/extension/index.ts +690 -352
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +99 -62
- package/dist/builtin/workflows/src/extension/render-call.ts +2 -1
- package/dist/builtin/workflows/src/extension/render-result.ts +9 -3
- package/dist/builtin/workflows/src/extension/renderers.ts +5 -3
- package/dist/builtin/workflows/src/extension/runtime.ts +68 -33
- package/dist/builtin/workflows/src/extension/status-writer.ts +1 -1
- package/dist/builtin/workflows/src/extension/wiring.ts +34 -13
- package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +142 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +4 -4
- package/dist/builtin/workflows/src/index.ts +2 -0
- package/dist/builtin/workflows/src/intercom/result-intercom.ts +1 -1
- package/dist/builtin/workflows/src/runs/background/runner.ts +6 -4
- package/dist/builtin/workflows/src/runs/background/status.ts +45 -21
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +624 -52
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +80 -24
- package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +61 -24
- package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +32 -10
- package/dist/builtin/workflows/src/sdk-surface.ts +6 -0
- package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +178 -0
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +92 -12
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +21 -3
- package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +1 -2
- package/dist/builtin/workflows/src/shared/run-visibility.ts +9 -0
- package/dist/builtin/workflows/src/shared/schema-introspection.ts +121 -0
- package/dist/builtin/workflows/src/shared/serializable.ts +132 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +91 -9
- package/dist/builtin/workflows/src/shared/store-types.ts +31 -3
- package/dist/builtin/workflows/src/shared/store.ts +58 -14
- package/dist/builtin/workflows/src/shared/types.ts +105 -40
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +129 -13
- package/dist/builtin/workflows/src/tui/chat-surface.ts +6 -1
- package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +3 -2
- package/dist/builtin/workflows/src/tui/graph-canvas.ts +1 -1
- package/dist/builtin/workflows/src/tui/graph-view.ts +91 -65
- package/dist/builtin/workflows/src/tui/inline-form-card.ts +1 -1
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +3 -2
- package/dist/builtin/workflows/src/tui/inputs-overlay.ts +3 -2
- package/dist/builtin/workflows/src/tui/inputs-picker.ts +8 -7
- package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
- package/dist/builtin/workflows/src/tui/node-card.ts +34 -8
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -11
- package/dist/builtin/workflows/src/tui/prompt-card.ts +98 -50
- package/dist/builtin/workflows/src/tui/session-list.ts +7 -2
- package/dist/builtin/workflows/src/tui/session-picker.ts +2 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +226 -55
- package/dist/builtin/workflows/src/tui/status-helpers.ts +2 -0
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +37 -158
- package/dist/builtin/workflows/src/tui/toast.ts +2 -2
- package/dist/builtin/workflows/src/tui/widget.ts +53 -12
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +270 -19
- package/dist/builtin/workflows/src/tui/workflow-notice-card.ts +184 -0
- package/dist/builtin/workflows/src/workflows/define-workflow.ts +138 -43
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +27 -9
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +196 -17
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +2 -2
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/codex-fast-mode.d.ts +36 -0
- package/dist/core/codex-fast-mode.d.ts.map +1 -0
- package/dist/core/codex-fast-mode.js +117 -0
- package/dist/core/codex-fast-mode.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +1 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/index.d.ts +4 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +7 -2
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +23 -8
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/reactive-widget.d.ts +58 -0
- package/dist/core/extensions/reactive-widget.d.ts.map +1 -0
- package/dist/core/extensions/reactive-widget.js +182 -0
- package/dist/core/extensions/reactive-widget.js.map +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +26 -12
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/messages.d.ts +1 -1
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +8 -2
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +11 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/resource-loader.d.ts +9 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +49 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +22 -13
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +7 -5
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +5 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +16 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +64 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +7 -4
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.js +2 -2
- package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -1
- package/dist/modes/interactive/chat-input-actions.js.map +1 -1
- package/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/dist/modes/interactive/components/diff.js +0 -1
- package/dist/modes/interactive/components/diff.js.map +1 -1
- package/dist/modes/interactive/components/fast-mode-selector.d.ts +27 -0
- package/dist/modes/interactive/components/fast-mode-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/fast-mode-selector.js +105 -0
- package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +7 -12
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +132 -30
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +53 -6
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/custom-provider.md +2 -2
- package/docs/development.md +2 -2
- package/docs/docs.json +2 -2
- package/docs/extensions.md +18 -13
- package/docs/providers.md +5 -1
- package/docs/quickstart.md +5 -3
- package/docs/rpc.md +5 -5
- package/docs/sdk.md +12 -12
- package/docs/settings.md +18 -0
- package/docs/themes.md +6 -6
- package/docs/tui.md +20 -18
- package/docs/usage.md +2 -0
- package/docs/workflows.md +403 -39
- package/examples/extensions/qna.ts +2 -2
- package/package.json +4 -4
- package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +0 -392
- package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +0 -23
- package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +0 -39
- package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +0 -87
- package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +0 -241
- package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +0 -225
- package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
- package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +0 -275
- package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +0 -134
- package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +0 -139
- package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +0 -143
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
-
import { CONFIG_DIR_NAME, createAskUserQuestionToolDefinition } from "@bastani/atomic";
|
|
8
|
+
import { CONFIG_DIR_NAME, createAskUserQuestionToolDefinition, isCodexFastModeCandidateModelId } from "@bastani/atomic";
|
|
9
9
|
import { stageUiBroker } from "../../shared/stage-ui-broker.js";
|
|
10
10
|
import { buildStagePromptAdapter } from "../../shared/stage-prompt.js";
|
|
11
11
|
import type {
|
|
@@ -34,6 +34,13 @@ import type {
|
|
|
34
34
|
WorkflowPersistencePort,
|
|
35
35
|
WorkflowRuntimeConfig,
|
|
36
36
|
WorkflowModelCatalogPort,
|
|
37
|
+
WorkflowExecutionMode,
|
|
38
|
+
WorkflowRunChildOptions,
|
|
39
|
+
WorkflowChildResult,
|
|
40
|
+
WorkflowOutputSchema,
|
|
41
|
+
WorkflowOutputValues,
|
|
42
|
+
WorkflowInputValues,
|
|
43
|
+
WorkflowSerializableValue,
|
|
37
44
|
} from "../../shared/types.js";
|
|
38
45
|
import type { InternalStageContext, StageAdapters } from "./stage-runner.js";
|
|
39
46
|
import type {
|
|
@@ -45,9 +52,12 @@ import type {
|
|
|
45
52
|
WorkflowFailureKind,
|
|
46
53
|
PendingPrompt,
|
|
47
54
|
PromptKind,
|
|
55
|
+
WorkflowChildReplaySnapshot,
|
|
56
|
+
WorkflowChildRunRef,
|
|
48
57
|
} from "../../shared/store-types.js";
|
|
49
58
|
import type { StageControlHandle, StageControlRegistry, AgentSessionEventListener } from "./stage-control-registry.js";
|
|
50
59
|
import type { Store } from "../../shared/store.js";
|
|
60
|
+
import type { WorkflowRegistry } from "../../workflows/registry.js";
|
|
51
61
|
import type { CancellationRegistry } from "../background/cancellation-registry.js";
|
|
52
62
|
import { createStageContext } from "./stage-runner.js";
|
|
53
63
|
import { GraphFrontierTracker } from "../shared/graph-inference.js";
|
|
@@ -71,12 +81,21 @@ import {
|
|
|
71
81
|
appendStageEnd,
|
|
72
82
|
appendRunEnd,
|
|
73
83
|
} from "../../shared/persistence-session-entries.js";
|
|
74
|
-
import { validateWorkflowModels } from "../shared/model-fallback.js";
|
|
84
|
+
import { buildModelCandidatesFromCatalog, validateWorkflowModels, workflowModelId } from "../shared/model-fallback.js";
|
|
85
|
+
import { validateInputs, type ValidationError } from "../shared/validate-inputs.js";
|
|
86
|
+
import { Type, type TSchema } from "typebox";
|
|
87
|
+
import { Value } from "typebox/value";
|
|
88
|
+
import { schemaFieldKind, schemaChoices, schemaIsRequired } from "../../shared/schema-introspection.js";
|
|
75
89
|
import type { WorkflowFailure } from "../../shared/workflow-failures.js";
|
|
76
90
|
import { classifyWorkflowFailure } from "../../shared/workflow-failures.js";
|
|
77
91
|
import { selectPromptCallsiteFrame } from "../shared/prompt-callsite.js";
|
|
92
|
+
import {
|
|
93
|
+
assertWorkflowSerializableObject,
|
|
94
|
+
workflowSerializableValidationError,
|
|
95
|
+
workflowSerializableTypeName,
|
|
96
|
+
} from "../../shared/serializable.js";
|
|
78
97
|
|
|
79
|
-
export interface ResolvedInputs extends
|
|
98
|
+
export interface ResolvedInputs extends WorkflowInputValues {}
|
|
80
99
|
|
|
81
100
|
export interface RunContinuationOpts {
|
|
82
101
|
readonly source: RunSnapshot;
|
|
@@ -89,6 +108,8 @@ export interface RunOpts {
|
|
|
89
108
|
cwd?: string;
|
|
90
109
|
/** HIL adapter injected by the pi runtime or test harness. */
|
|
91
110
|
ui?: WorkflowUIAdapter;
|
|
111
|
+
/** Runtime execution mode. Controls child session policy metadata. */
|
|
112
|
+
executionMode?: WorkflowExecutionMode;
|
|
92
113
|
/** Internal detached-run mode: surface ctx.ui.* as node-local workflow prompt stages. */
|
|
93
114
|
usePromptNodesForUi?: boolean;
|
|
94
115
|
/**
|
|
@@ -132,6 +153,8 @@ export interface RunOpts {
|
|
|
132
153
|
config?: WorkflowRuntimeConfig;
|
|
133
154
|
/** Optional model catalog used for fallback validation/resolution. */
|
|
134
155
|
models?: WorkflowModelCatalogPort;
|
|
156
|
+
/** Registry metadata forwarded to workflow runs launched from discovery/tooling. */
|
|
157
|
+
registry?: WorkflowRegistry;
|
|
135
158
|
/**
|
|
136
159
|
* Current nesting depth of this workflow run. Starts at 0 for top-level runs.
|
|
137
160
|
* Callers that spawn nested runs must increment this by 1 before passing to
|
|
@@ -155,16 +178,22 @@ export interface RunOpts {
|
|
|
155
178
|
runId?: string;
|
|
156
179
|
/** Replay completed stages from a failed source run, then resume at this stage. */
|
|
157
180
|
continuation?: RunContinuationOpts;
|
|
181
|
+
/** Internal parent linkage for nested ctx.workflow(...) runs. */
|
|
182
|
+
parentRun?: {
|
|
183
|
+
readonly runId: string;
|
|
184
|
+
readonly stageId: string;
|
|
185
|
+
readonly rootRunId: string;
|
|
186
|
+
};
|
|
158
187
|
onRunStart?: (snapshot: RunSnapshot) => void;
|
|
159
188
|
onStageStart?: (runId: string, snapshot: StageSnapshot) => void;
|
|
160
189
|
onStageEnd?: (runId: string, snapshot: StageSnapshot) => void;
|
|
161
|
-
onRunEnd?: (runId: string, status: RunStatus, result?:
|
|
190
|
+
onRunEnd?: (runId: string, status: RunStatus, result?: WorkflowOutputValues, error?: string) => void;
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
export interface RunResult {
|
|
165
194
|
readonly runId: string;
|
|
166
195
|
readonly status: RunStatus;
|
|
167
|
-
readonly result?:
|
|
196
|
+
readonly result?: WorkflowOutputValues;
|
|
168
197
|
readonly error?: string;
|
|
169
198
|
readonly stages: StageSnapshot[];
|
|
170
199
|
}
|
|
@@ -175,19 +204,25 @@ export interface RunResult {
|
|
|
175
204
|
|
|
176
205
|
export function resolveInputs(
|
|
177
206
|
schema: Readonly<Record<string, WorkflowInputSchema>>,
|
|
178
|
-
provided: Record<string, unknown
|
|
207
|
+
provided: Readonly<Record<string, unknown>>,
|
|
179
208
|
): ResolvedInputs {
|
|
180
|
-
const resolved: Record<string,
|
|
209
|
+
const resolved: Record<string, WorkflowSerializableValue> = {};
|
|
210
|
+
for (const [key, value] of Object.entries(provided)) {
|
|
211
|
+
if (value !== undefined) resolved[key] = value as WorkflowSerializableValue;
|
|
212
|
+
}
|
|
181
213
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
214
|
+
// Apply declared TypeBox defaults (top-level and nested) for absent keys.
|
|
215
|
+
const withDefaults = Value.Default(
|
|
216
|
+
Type.Object(schema as Record<string, TSchema>, { additionalProperties: true }),
|
|
217
|
+
resolved,
|
|
218
|
+
) as Record<string, WorkflowSerializableValue>;
|
|
219
|
+
for (const [key, value] of Object.entries(withDefaults)) {
|
|
220
|
+
if (value !== undefined) resolved[key] = value;
|
|
186
221
|
}
|
|
187
222
|
|
|
188
223
|
for (const [key, schemaDef] of Object.entries(schema)) {
|
|
189
|
-
if (schemaDef
|
|
190
|
-
throw new TypeError(`
|
|
224
|
+
if (schemaIsRequired(schemaDef) && resolved[key] === undefined) {
|
|
225
|
+
throw new TypeError(`atomic-workflows: required input "${key}" not provided`);
|
|
191
226
|
}
|
|
192
227
|
}
|
|
193
228
|
|
|
@@ -198,7 +233,10 @@ function resolveInputConcurrency(
|
|
|
198
233
|
schema: Readonly<Record<string, WorkflowInputSchema>>,
|
|
199
234
|
resolvedInputs: ResolvedInputs,
|
|
200
235
|
): number | undefined {
|
|
201
|
-
|
|
236
|
+
const concurrencySchema = schema["max_concurrency"];
|
|
237
|
+
if (concurrencySchema === undefined || schemaFieldKind(concurrencySchema) !== "number") {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
202
240
|
|
|
203
241
|
const value = resolvedInputs["max_concurrency"];
|
|
204
242
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 1) return undefined;
|
|
@@ -287,12 +325,12 @@ function promptCallsiteHash(): string {
|
|
|
287
325
|
function hilAbortError(signal: AbortSignal): Error {
|
|
288
326
|
return signal.reason instanceof Error
|
|
289
327
|
? signal.reason
|
|
290
|
-
: new Error("
|
|
328
|
+
: new Error("atomic-workflows: HIL aborted");
|
|
291
329
|
}
|
|
292
330
|
|
|
293
331
|
function makeUnavailableUIContext(): WorkflowUIContext {
|
|
294
332
|
const msg = (primitive: string): string =>
|
|
295
|
-
`
|
|
333
|
+
`atomic-workflows: HIL ctx.ui.${primitive} is unavailable because Atomic runtime did not provide a UI adapter`;
|
|
296
334
|
return {
|
|
297
335
|
input: () => Promise.reject(new Error(msg("input"))),
|
|
298
336
|
confirm: () => Promise.reject(new Error(msg("confirm"))),
|
|
@@ -434,8 +472,9 @@ export async function askReadinessViaStageBroker(
|
|
|
434
472
|
// Expose a headless-answer adapter for the gate so it can be answered
|
|
435
473
|
// programmatically (e.g. `workflow send`) without a TUI host. The gate
|
|
436
474
|
// question params are known statically here.
|
|
475
|
+
const gatePromptId = `readiness-gate-${stageId}-${crypto.randomUUID()}`;
|
|
437
476
|
const gateAdapter = buildStagePromptAdapter(
|
|
438
|
-
|
|
477
|
+
gatePromptId,
|
|
439
478
|
"readiness_gate",
|
|
440
479
|
READINESS_GATE_QUESTION_PARAMS,
|
|
441
480
|
Date.now(),
|
|
@@ -443,7 +482,7 @@ export async function askReadinessViaStageBroker(
|
|
|
443
482
|
if (gateAdapter) stageUiBroker.provideStagePrompt(runId, stageId, gateAdapter);
|
|
444
483
|
try {
|
|
445
484
|
const result = await execute(
|
|
446
|
-
|
|
485
|
+
gatePromptId,
|
|
447
486
|
READINESS_GATE_QUESTION_PARAMS as Parameters<typeof execute>[1],
|
|
448
487
|
signal,
|
|
449
488
|
undefined,
|
|
@@ -499,7 +538,7 @@ function applyTaskContext(prompt: string, previous: WorkflowTaskOptions["previou
|
|
|
499
538
|
function taskPrompt(options: WorkflowTaskOptions): string {
|
|
500
539
|
const prompt = options.prompt ?? options.task;
|
|
501
540
|
if (prompt === undefined) {
|
|
502
|
-
throw new Error("
|
|
541
|
+
throw new Error("atomic-workflows: ctx.task requires options.prompt or options.task");
|
|
503
542
|
}
|
|
504
543
|
return prompt;
|
|
505
544
|
}
|
|
@@ -819,7 +858,7 @@ async function mapParallelSteps<T>(
|
|
|
819
858
|
if (failures.length > 0) {
|
|
820
859
|
throw new AggregateError(
|
|
821
860
|
failures.map((failure) => failure.error),
|
|
822
|
-
`
|
|
861
|
+
`atomic-workflows: ${failures.length} parallel ${failures.length === 1 ? "step" : "steps"} failed`,
|
|
823
862
|
);
|
|
824
863
|
}
|
|
825
864
|
|
|
@@ -831,7 +870,7 @@ function expandedParallelTasks(tasks: readonly WorkflowDirectTaskItem[]): Workfl
|
|
|
831
870
|
for (const task of tasks) {
|
|
832
871
|
const count = task.count ?? 1;
|
|
833
872
|
if (!Number.isInteger(count) || count < 1) {
|
|
834
|
-
throw new Error(`
|
|
873
|
+
throw new Error(`atomic-workflows: direct task "${task.name}" count must be a positive integer`);
|
|
835
874
|
}
|
|
836
875
|
for (let index = 0; index < count; index += 1) {
|
|
837
876
|
expanded.push(count === 1 ? task : {
|
|
@@ -933,7 +972,7 @@ function prepareDirectWorktrees(
|
|
|
933
972
|
}
|
|
934
973
|
|
|
935
974
|
if (typeof options.gitWorktreeDir === "string" || tasks.some((task) => typeof task.gitWorktreeDir === "string")) {
|
|
936
|
-
throw new Error("
|
|
975
|
+
throw new Error("atomic-workflows: worktree and gitWorktreeDir are mutually exclusive; use gitWorktreeDir for a reusable worktree or worktree:true for temporary isolated worktrees.");
|
|
937
976
|
}
|
|
938
977
|
|
|
939
978
|
const sharedCwd = resolveSharedDirectWorktreeCwd(tasks);
|
|
@@ -1088,13 +1127,27 @@ function workflowDetailsFromRun(
|
|
|
1088
1127
|
};
|
|
1089
1128
|
}
|
|
1090
1129
|
|
|
1091
|
-
const EMPTY_WORKFLOW_GRAPH_ERROR_MESSAGE = "Workflow run completed without creating any workflow stages. Create at least one stage with ctx.stage(), ctx.task(), ctx.chain(), or ctx.
|
|
1130
|
+
const EMPTY_WORKFLOW_GRAPH_ERROR_MESSAGE = "Workflow run completed without creating any workflow stages. Create at least one stage with ctx.stage(), ctx.task(), ctx.chain(), ctx.parallel(), or ctx.workflow().";
|
|
1092
1131
|
|
|
1093
1132
|
function assertWorkflowCreatedStage(runSnapshot: RunSnapshot): void {
|
|
1094
1133
|
if (runSnapshot.stages.length > 0) return;
|
|
1095
1134
|
throw new Error(EMPTY_WORKFLOW_GRAPH_ERROR_MESSAGE);
|
|
1096
1135
|
}
|
|
1097
1136
|
|
|
1137
|
+
// Direct (task/parallel/chain) execution synthesizes ephemeral workflows that
|
|
1138
|
+
// expose tool-parity outputs. They are declared explicitly like any other
|
|
1139
|
+
// workflow so the fully-explicit output contract holds on the direct path too.
|
|
1140
|
+
// `unknown` accepts any serializable value, and every key is optional because a
|
|
1141
|
+
// given direct mode only returns the subset it produces (e.g. `count` for chain/
|
|
1142
|
+
// parallel, `text` for single task, `worktreeSummary` only with worktrees).
|
|
1143
|
+
const DIRECT_WORKFLOW_OUTPUTS: Readonly<Record<string, WorkflowOutputSchema>> = Object.freeze({
|
|
1144
|
+
results: Type.Optional(Type.Unknown()),
|
|
1145
|
+
text: Type.Optional(Type.Unknown()),
|
|
1146
|
+
count: Type.Optional(Type.Unknown()),
|
|
1147
|
+
artifacts: Type.Optional(Type.Unknown()),
|
|
1148
|
+
worktreeSummary: Type.Optional(Type.Unknown()),
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1098
1151
|
function defineDirectWorkflow(
|
|
1099
1152
|
name: string,
|
|
1100
1153
|
runFn: WorkflowDefinition["run"],
|
|
@@ -1105,6 +1158,7 @@ function defineDirectWorkflow(
|
|
|
1105
1158
|
normalizedName: name,
|
|
1106
1159
|
description: "Direct workflow execution",
|
|
1107
1160
|
inputs: Object.freeze({}),
|
|
1161
|
+
outputs: DIRECT_WORKFLOW_OUTPUTS,
|
|
1108
1162
|
run: runFn,
|
|
1109
1163
|
});
|
|
1110
1164
|
}
|
|
@@ -1337,7 +1391,7 @@ function appendRunEndWhenRecorded(
|
|
|
1337
1391
|
payload: {
|
|
1338
1392
|
readonly runId: string;
|
|
1339
1393
|
readonly status: RunStatus;
|
|
1340
|
-
readonly result?:
|
|
1394
|
+
readonly result?: WorkflowOutputValues;
|
|
1341
1395
|
readonly error?: string;
|
|
1342
1396
|
readonly failureKind?: WorkflowFailureKind;
|
|
1343
1397
|
readonly failureMessage?: string;
|
|
@@ -1419,7 +1473,7 @@ interface ContinuationReplayInput {
|
|
|
1419
1473
|
readonly replayKey: string;
|
|
1420
1474
|
readonly parentIds: readonly string[];
|
|
1421
1475
|
readonly stageId: string;
|
|
1422
|
-
readonly kind: "stage" | "prompt";
|
|
1476
|
+
readonly kind: "stage" | "prompt" | "workflow";
|
|
1423
1477
|
}
|
|
1424
1478
|
|
|
1425
1479
|
interface ContinuationReplayIndex {
|
|
@@ -1450,7 +1504,7 @@ function createContinuationReplayIndex(continuation: RunContinuationOpts | undef
|
|
|
1450
1504
|
}
|
|
1451
1505
|
const resumeStage = continuation.source.stages.find((stage) => stage.id === continuation.resumeFromStageId);
|
|
1452
1506
|
if (resumeStage === undefined) {
|
|
1453
|
-
throw new Error(`
|
|
1507
|
+
throw new Error(`atomic-workflows: insufficient_state: resume stage ${continuation.resumeFromStageId} was not found in source run ${continuation.source.id}`);
|
|
1454
1508
|
}
|
|
1455
1509
|
|
|
1456
1510
|
const stagesByReplayIdentity = new Map<string, StageSnapshot[]>();
|
|
@@ -1472,7 +1526,7 @@ function createContinuationReplayIndex(continuation: RunContinuationOpts | undef
|
|
|
1472
1526
|
const replayablePromptContinuationStageIds = new Set<string>();
|
|
1473
1527
|
|
|
1474
1528
|
const failTopology = (displayName: string, replayKey: string, reason: "mismatch" | "ambiguous"): never => {
|
|
1475
|
-
throw new Error(`
|
|
1529
|
+
throw new Error(`atomic-workflows: insufficient_state: replay topology ${reason} for stage "${displayName}" (replayKey "${replayKey}") in source run ${continuation.source.id}`);
|
|
1476
1530
|
};
|
|
1477
1531
|
|
|
1478
1532
|
const translateSourceParents = (source: StageSnapshot): string[] | undefined => {
|
|
@@ -1618,15 +1672,197 @@ function nextEventLoopTurn(): Promise<void> {
|
|
|
1618
1672
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
1619
1673
|
}
|
|
1620
1674
|
|
|
1621
|
-
|
|
1675
|
+
function formatValidationErrors(errors: readonly ValidationError[]): string {
|
|
1676
|
+
return errors.map((error) => ` - ${error.key}: ${error.reason}`).join("\n");
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
export function resolveAndValidateInputs(
|
|
1680
|
+
schema: Readonly<Record<string, WorkflowInputSchema>>,
|
|
1681
|
+
provided: Readonly<Record<string, unknown>>,
|
|
1682
|
+
scope: string,
|
|
1683
|
+
): ResolvedInputs {
|
|
1684
|
+
const resolved = resolveInputs(schema, provided);
|
|
1685
|
+
const errors = validateInputs(schema, resolved);
|
|
1686
|
+
if (errors.length > 0) {
|
|
1687
|
+
throw new TypeError(
|
|
1688
|
+
`atomic-workflows: invalid inputs for ${scope}:\n${formatValidationErrors(errors)}`,
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
return resolved;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function hasOwnWorkflowOutput(record: WorkflowOutputValues | Readonly<Record<string, WorkflowOutputSchema>>, key: string): boolean {
|
|
1695
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// Workflow outputs are fully explicit: a workflow exposes exactly the outputs it
|
|
1699
|
+
// declares with `.output(...)`. There is no implicit `result` fallback, and a
|
|
1700
|
+
// `.run()` return that contains a key the workflow did not declare is an error,
|
|
1701
|
+
// so authors cannot silently leak undeclared values across a workflow boundary.
|
|
1702
|
+
function assertWorkflowOutputsExplicit(
|
|
1703
|
+
scope: string,
|
|
1704
|
+
sourceOutput: WorkflowOutputValues,
|
|
1705
|
+
declarations: Readonly<Record<string, WorkflowOutputSchema>>,
|
|
1706
|
+
missingOutputSuffix = "",
|
|
1707
|
+
): void {
|
|
1708
|
+
for (const key of Object.keys(sourceOutput)) {
|
|
1709
|
+
if (!hasOwnWorkflowOutput(declarations, key)) {
|
|
1710
|
+
throw new Error(
|
|
1711
|
+
`atomic-workflows: ${scope} returned undeclared output "${key}"; declare it with .output("${key}", Type....) or remove it from the .run() return`,
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
for (const [key, schema] of Object.entries(declarations)) {
|
|
1716
|
+
if (!(key in sourceOutput)) {
|
|
1717
|
+
if (schemaIsRequired(schema)) {
|
|
1718
|
+
throw new Error(
|
|
1719
|
+
`atomic-workflows: ${scope} missing output "${key}"${missingOutputSuffix}`,
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
const value = sourceOutput[key];
|
|
1725
|
+
const kind = schemaFieldKind(schema);
|
|
1726
|
+
if (!Value.Check(schema, value)) {
|
|
1727
|
+
const choices = schemaChoices(schema);
|
|
1728
|
+
if (kind === "select" && choices !== undefined && typeof value === "string") {
|
|
1729
|
+
throw new Error(
|
|
1730
|
+
`atomic-workflows: ${scope} output "${key}" must be one of [${choices.join(", ")}], got ${JSON.stringify(value)}`,
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
throw new Error(
|
|
1734
|
+
`atomic-workflows: ${scope} output "${key}" expected ${kind}, got ${workflowSerializableTypeName(value)}`,
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
const serializableError = workflowSerializableValidationError(
|
|
1738
|
+
value,
|
|
1739
|
+
`${scope} output "${key}"`,
|
|
1740
|
+
);
|
|
1741
|
+
if (serializableError !== undefined) {
|
|
1742
|
+
throw new Error(`atomic-workflows: ${serializableError}`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function normalizeWorkflowRunOutput(
|
|
1748
|
+
workflowName: string,
|
|
1749
|
+
rawOutput: unknown,
|
|
1750
|
+
): WorkflowOutputValues | undefined {
|
|
1751
|
+
if (rawOutput === undefined) return undefined;
|
|
1752
|
+
// Drop top-level keys explicitly set to `undefined` so conditional outputs
|
|
1753
|
+
// (e.g. `{ note: cond ? value : undefined }`) satisfy the JSON-serializable
|
|
1754
|
+
// contract instead of failing validation; selectWorkflowOutputs strips the
|
|
1755
|
+
// same way at the child boundary, keeping both paths consistent.
|
|
1756
|
+
const normalized =
|
|
1757
|
+
rawOutput !== null && typeof rawOutput === "object" && !Array.isArray(rawOutput)
|
|
1758
|
+
? Object.fromEntries(
|
|
1759
|
+
Object.entries(rawOutput as Record<string, unknown>).filter(([, v]) => v !== undefined),
|
|
1760
|
+
)
|
|
1761
|
+
: rawOutput;
|
|
1762
|
+
assertWorkflowSerializableObject(normalized, `workflow "${workflowName}" .run() return`);
|
|
1763
|
+
return normalized;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function assertWorkflowRunOutputs(
|
|
1767
|
+
workflowName: string,
|
|
1768
|
+
result: WorkflowOutputValues | undefined,
|
|
1769
|
+
declaredOutputs: Readonly<Record<string, WorkflowOutputSchema>> | undefined,
|
|
1770
|
+
): void {
|
|
1771
|
+
assertWorkflowOutputsExplicit(
|
|
1772
|
+
`workflow "${workflowName}"`,
|
|
1773
|
+
result ?? {},
|
|
1774
|
+
declaredOutputs ?? {},
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function selectWorkflowOutputs(
|
|
1779
|
+
child: WorkflowDefinition,
|
|
1780
|
+
rawOutput: WorkflowOutputValues | undefined,
|
|
1781
|
+
): WorkflowOutputValues {
|
|
1782
|
+
const declarations = child.outputs ?? {};
|
|
1783
|
+
const sourceOutput = rawOutput ?? {};
|
|
1784
|
+
// The child run already validated its return against these declared outputs
|
|
1785
|
+
// (assertWorkflowRunOutputs) before it could complete, so undeclared keys are
|
|
1786
|
+
// impossible here and a second assertWorkflowOutputsExplicit pass could never
|
|
1787
|
+
// fire. Just project the declared outputs the child returned. (An undeclared
|
|
1788
|
+
// key fails the child run itself; the parent surfaces that as a wrapped
|
|
1789
|
+
// "child workflow ... failed" error.)
|
|
1790
|
+
const selected: Record<string, WorkflowSerializableValue> = {};
|
|
1791
|
+
for (const key of Object.keys(declarations)) {
|
|
1792
|
+
const value = sourceOutput[key];
|
|
1793
|
+
if (value !== undefined) selected[key] = value;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
return selected;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function cloneWorkflowChildValue<T>(value: T): T {
|
|
1800
|
+
return structuredClone(value);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function workflowChildSerializationMessage(err: unknown): string {
|
|
1804
|
+
return err instanceof Error ? err.message : String(err);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function isWorkflowDefinition(value: unknown): value is WorkflowDefinition {
|
|
1808
|
+
if (value === null || typeof value !== "object") return false;
|
|
1809
|
+
const record = value as Partial<WorkflowDefinition>;
|
|
1810
|
+
return record.__piWorkflow === true &&
|
|
1811
|
+
typeof record.name === "string" && record.name.trim().length > 0 &&
|
|
1812
|
+
typeof record.normalizedName === "string" && record.normalizedName.trim().length > 0 &&
|
|
1813
|
+
typeof record.run === "function" &&
|
|
1814
|
+
// Compiled definitions always set `inputs: {}`; guard it so a handcrafted
|
|
1815
|
+
// object that passes the sentinel still fails here with the clear "requires
|
|
1816
|
+
// a compiled workflow definition" error rather than crashing later inside
|
|
1817
|
+
// resolveAndValidateInputs(child.inputs, ...) on `Object.entries(undefined)`.
|
|
1818
|
+
typeof record.inputs === "object" && record.inputs !== null;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function cloneWorkflowChildReplaySnapshot(snapshot: WorkflowChildReplaySnapshot): WorkflowChildReplaySnapshot {
|
|
1822
|
+
return {
|
|
1823
|
+
alias: snapshot.alias,
|
|
1824
|
+
workflow: snapshot.workflow,
|
|
1825
|
+
runId: snapshot.runId,
|
|
1826
|
+
status: snapshot.status,
|
|
1827
|
+
outputs: cloneWorkflowChildValue(snapshot.outputs),
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function workflowChildReplaySnapshot(
|
|
1832
|
+
alias: string,
|
|
1833
|
+
childResult: WorkflowChildResult,
|
|
1834
|
+
): WorkflowChildReplaySnapshot {
|
|
1835
|
+
const outputs: Record<string, WorkflowSerializableValue> = {};
|
|
1836
|
+
for (const [key, value] of Object.entries(childResult.outputs)) {
|
|
1837
|
+
if (value === undefined) continue;
|
|
1838
|
+
try {
|
|
1839
|
+
outputs[key] = cloneWorkflowChildValue(value);
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
throw new Error(
|
|
1842
|
+
`atomic-workflows: child workflow "${alias}" (${childResult.workflow}) exposed output "${key}" is not serializable for continuation replay: ${workflowChildSerializationMessage(err)}`,
|
|
1843
|
+
{ cause: err },
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
return {
|
|
1849
|
+
alias,
|
|
1850
|
+
workflow: childResult.workflow,
|
|
1851
|
+
runId: childResult.runId,
|
|
1852
|
+
status: childResult.status,
|
|
1853
|
+
outputs,
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
export async function run<TInputs extends WorkflowInputValues>(
|
|
1622
1858
|
def: WorkflowDefinition<TInputs>,
|
|
1623
|
-
inputs: Record<string, unknown
|
|
1859
|
+
inputs: Readonly<Record<string, unknown>>,
|
|
1624
1860
|
opts: RunOpts = {},
|
|
1625
1861
|
): Promise<RunResult> {
|
|
1626
1862
|
const activeStore = opts.store ?? defaultStore;
|
|
1627
1863
|
const adapters = opts.adapters ?? {};
|
|
1628
1864
|
if (opts.usePromptNodesForUi === true && opts.ui !== undefined) {
|
|
1629
|
-
console.warn("
|
|
1865
|
+
console.warn("atomic-workflows: usePromptNodesForUi ignores the provided RunOpts.ui adapter");
|
|
1630
1866
|
}
|
|
1631
1867
|
|
|
1632
1868
|
// 0. maxDepth guard — reject before any store/persistence side effects.
|
|
@@ -1637,13 +1873,17 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1637
1873
|
return {
|
|
1638
1874
|
runId: opts.runId ?? crypto.randomUUID(),
|
|
1639
1875
|
status: "failed",
|
|
1640
|
-
error: `
|
|
1876
|
+
error: `atomic-workflows: maxDepth exceeded (max ${max})`,
|
|
1641
1877
|
stages: [],
|
|
1642
1878
|
};
|
|
1643
1879
|
}
|
|
1644
1880
|
|
|
1645
1881
|
// 1. Resolve + validate inputs
|
|
1646
|
-
const resolvedInputs =
|
|
1882
|
+
const resolvedInputs = resolveAndValidateInputs(
|
|
1883
|
+
def.inputs,
|
|
1884
|
+
inputs,
|
|
1885
|
+
`workflow "${def.name}"`,
|
|
1886
|
+
);
|
|
1647
1887
|
|
|
1648
1888
|
// 2. Generate runId (or use pre-allocated seam from caller)
|
|
1649
1889
|
const runId = opts.runId ?? crypto.randomUUID();
|
|
@@ -1668,6 +1908,11 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1668
1908
|
status: "running",
|
|
1669
1909
|
stages: [],
|
|
1670
1910
|
startedAt: Date.now(),
|
|
1911
|
+
...(opts.parentRun !== undefined ? {
|
|
1912
|
+
parentRunId: opts.parentRun.runId,
|
|
1913
|
+
parentStageId: opts.parentRun.stageId,
|
|
1914
|
+
rootRunId: opts.parentRun.rootRunId,
|
|
1915
|
+
} : {}),
|
|
1671
1916
|
...(opts.continuation !== undefined ? {
|
|
1672
1917
|
resumedFromRunId: opts.continuation.source.id,
|
|
1673
1918
|
resumeFromStageId: opts.continuation.resumeFromStageId,
|
|
@@ -1692,6 +1937,9 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1692
1937
|
runId,
|
|
1693
1938
|
name: def.name,
|
|
1694
1939
|
inputs: resolvedInputs,
|
|
1940
|
+
...(runSnapshot.parentRunId !== undefined ? { parentRunId: runSnapshot.parentRunId } : {}),
|
|
1941
|
+
...(runSnapshot.parentStageId !== undefined ? { parentStageId: runSnapshot.parentStageId } : {}),
|
|
1942
|
+
...(runSnapshot.rootRunId !== undefined ? { rootRunId: runSnapshot.rootRunId } : {}),
|
|
1695
1943
|
...(runSnapshot.resumedFromRunId !== undefined ? { resumedFromRunId: runSnapshot.resumedFromRunId } : {}),
|
|
1696
1944
|
...(runSnapshot.resumeFromStageId !== undefined ? { resumeFromStageId: runSnapshot.resumeFromStageId } : {}),
|
|
1697
1945
|
ts: runSnapshot.startedAt,
|
|
@@ -1858,10 +2106,162 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
1858
2106
|
|
|
1859
2107
|
ownController.signal.addEventListener(
|
|
1860
2108
|
"abort",
|
|
1861
|
-
() => rejectReleaseBarriers(ownController.signal.reason ?? new Error("
|
|
2109
|
+
() => rejectReleaseBarriers(ownController.signal.reason ?? new Error("atomic-workflows: run aborted")),
|
|
1862
2110
|
{ once: true },
|
|
1863
2111
|
);
|
|
1864
2112
|
|
|
2113
|
+
interface WorkflowBoundaryStage {
|
|
2114
|
+
readonly id: string;
|
|
2115
|
+
readonly replayedChild?: WorkflowChildResult;
|
|
2116
|
+
finalizeReplay(): void;
|
|
2117
|
+
linkChildRun(ref: WorkflowChildRunRef): void;
|
|
2118
|
+
complete(summary: string, workflowChild: WorkflowChildReplaySnapshot): void;
|
|
2119
|
+
fail(error: unknown): void;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
const workflowChildResultFromReplay = (snapshot: WorkflowChildReplaySnapshot): WorkflowChildResult => ({
|
|
2123
|
+
workflow: snapshot.workflow,
|
|
2124
|
+
runId: snapshot.runId,
|
|
2125
|
+
status: snapshot.status,
|
|
2126
|
+
outputs: cloneWorkflowChildValue(snapshot.outputs),
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
const workflowBoundaryReplayCounts = new Map<string, number>();
|
|
2130
|
+
const nextWorkflowBoundaryReplayKey = (name: string): string => {
|
|
2131
|
+
const next = (workflowBoundaryReplayCounts.get(name) ?? 0) + 1;
|
|
2132
|
+
workflowBoundaryReplayCounts.set(name, next);
|
|
2133
|
+
return `workflow:${name}:${next}`;
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
const startWorkflowBoundaryStage = (name: string, replayKey: string): WorkflowBoundaryStage => {
|
|
2137
|
+
const stageId = crypto.randomUUID();
|
|
2138
|
+
const provisionalParentIds = tracker.onSpawn(stageId, name);
|
|
2139
|
+
const replayDecision = replayIndex.decide({
|
|
2140
|
+
displayName: name,
|
|
2141
|
+
replayKey,
|
|
2142
|
+
parentIds: provisionalParentIds,
|
|
2143
|
+
stageId,
|
|
2144
|
+
kind: "workflow",
|
|
2145
|
+
});
|
|
2146
|
+
const parentIds = replayDecision.parentIds;
|
|
2147
|
+
if (!sameStringSet(parentIds, provisionalParentIds)) {
|
|
2148
|
+
tracker.replaceParents(stageId, parentIds);
|
|
2149
|
+
}
|
|
2150
|
+
const replaySource = replayDecision.source;
|
|
2151
|
+
const replayChildSnapshot = replayDecision.kind === "replay" ? replayDecision.source.workflowChild : undefined;
|
|
2152
|
+
const replayedChild = replayChildSnapshot !== undefined
|
|
2153
|
+
? workflowChildResultFromReplay(replayChildSnapshot)
|
|
2154
|
+
: undefined;
|
|
2155
|
+
const startedAt = Date.now();
|
|
2156
|
+
const stageSnapshot: StageSnapshot = {
|
|
2157
|
+
id: stageId,
|
|
2158
|
+
name,
|
|
2159
|
+
replayKey,
|
|
2160
|
+
status: replayedChild !== undefined ? "completed" : "running",
|
|
2161
|
+
parentIds: Object.freeze([...parentIds]),
|
|
2162
|
+
startedAt,
|
|
2163
|
+
toolEvents: [],
|
|
2164
|
+
attachable: false,
|
|
2165
|
+
...(replaySource !== undefined ? {
|
|
2166
|
+
replayedFromStageId: replaySource.id,
|
|
2167
|
+
replayed: replayedChild !== undefined,
|
|
2168
|
+
} : {}),
|
|
2169
|
+
...(replayedChild !== undefined && replayChildSnapshot !== undefined ? {
|
|
2170
|
+
endedAt: startedAt,
|
|
2171
|
+
durationMs: 0,
|
|
2172
|
+
...(replayDecision.kind === "replay" && replayDecision.source.result !== undefined ? { result: replayDecision.source.result } : {}),
|
|
2173
|
+
workflowChild: cloneWorkflowChildReplaySnapshot(replayChildSnapshot),
|
|
2174
|
+
} : {}),
|
|
2175
|
+
};
|
|
2176
|
+
let finalized = false;
|
|
2177
|
+
|
|
2178
|
+
const appendStageStartOnce = (): void => {
|
|
2179
|
+
if (!opts.persistence) return;
|
|
2180
|
+
appendStageStart(opts.persistence, {
|
|
2181
|
+
runId,
|
|
2182
|
+
stageId,
|
|
2183
|
+
name,
|
|
2184
|
+
parentIds: stageSnapshot.parentIds,
|
|
2185
|
+
...stageReplayFields(stageSnapshot),
|
|
2186
|
+
ts: startedAt,
|
|
2187
|
+
});
|
|
2188
|
+
};
|
|
2189
|
+
|
|
2190
|
+
const appendStageEndForSnapshot = (): void => {
|
|
2191
|
+
if (!opts.persistence) return;
|
|
2192
|
+
appendStageEnd(opts.persistence, {
|
|
2193
|
+
runId,
|
|
2194
|
+
stageId,
|
|
2195
|
+
status: stageSnapshot.status,
|
|
2196
|
+
durationMs: stageSnapshot.durationMs,
|
|
2197
|
+
...(stageSnapshot.error !== undefined ? { error: stageSnapshot.error } : {}),
|
|
2198
|
+
...(stageSnapshot.failureKind !== undefined ? { failureKind: stageSnapshot.failureKind } : {}),
|
|
2199
|
+
...(stageSnapshot.failureMessage !== undefined ? { failureMessage: stageSnapshot.failureMessage } : {}),
|
|
2200
|
+
...(stageSnapshot.result !== undefined && stageSnapshot.status === "completed" ? { summary: stageSnapshot.result } : {}),
|
|
2201
|
+
...stageReplayFields(stageSnapshot),
|
|
2202
|
+
...(stageSnapshot.workflowChild !== undefined ? { workflowChild: stageSnapshot.workflowChild } : {}),
|
|
2203
|
+
});
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
const finalize = (
|
|
2207
|
+
status: "completed" | "failed",
|
|
2208
|
+
summaryOrError: string,
|
|
2209
|
+
workflowChild?: WorkflowChildReplaySnapshot,
|
|
2210
|
+
failureError?: unknown,
|
|
2211
|
+
): void => {
|
|
2212
|
+
if (finalized) return;
|
|
2213
|
+
finalized = true;
|
|
2214
|
+
stageSnapshot.status = status;
|
|
2215
|
+
if (status === "completed") {
|
|
2216
|
+
stageSnapshot.result = summaryOrError;
|
|
2217
|
+
if (workflowChild !== undefined) stageSnapshot.workflowChild = workflowChild;
|
|
2218
|
+
} else {
|
|
2219
|
+
const failure = classifyWorkflowFailure(failureError);
|
|
2220
|
+
stageSnapshot.error = failure.userMessage;
|
|
2221
|
+
stageSnapshot.failureKind = failure.kind;
|
|
2222
|
+
stageSnapshot.failureMessage = failure.message;
|
|
2223
|
+
}
|
|
2224
|
+
stageSnapshot.endedAt = Date.now();
|
|
2225
|
+
stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
|
|
2226
|
+
activeStore.recordStageEnd(runId, stageSnapshot);
|
|
2227
|
+
opts.onStageEnd?.(runId, stageSnapshot);
|
|
2228
|
+
appendStageEndForSnapshot();
|
|
2229
|
+
tracker.onSettle(stageId);
|
|
2230
|
+
};
|
|
2231
|
+
|
|
2232
|
+
activeStore.recordStageStart(runId, stageSnapshot);
|
|
2233
|
+
opts.onStageStart?.(runId, stageSnapshot);
|
|
2234
|
+
appendStageStartOnce();
|
|
2235
|
+
|
|
2236
|
+
const finalizeReplay = (): void => {
|
|
2237
|
+
if (replayedChild === undefined || finalized) return;
|
|
2238
|
+
finalized = true;
|
|
2239
|
+
activeStore.recordStageEnd(runId, stageSnapshot);
|
|
2240
|
+
opts.onStageEnd?.(runId, stageSnapshot);
|
|
2241
|
+
appendStageEndForSnapshot();
|
|
2242
|
+
tracker.onSettle(stageId);
|
|
2243
|
+
};
|
|
2244
|
+
|
|
2245
|
+
const linkChildRun = (ref: WorkflowChildRunRef): void => {
|
|
2246
|
+
if (finalized) return;
|
|
2247
|
+
stageSnapshot.workflowChildRun = { ...ref };
|
|
2248
|
+
activeStore.recordStageWorkflowChildRun(runId, stageId, ref);
|
|
2249
|
+
};
|
|
2250
|
+
|
|
2251
|
+
return {
|
|
2252
|
+
id: stageId,
|
|
2253
|
+
...(replayedChild !== undefined ? { replayedChild } : {}),
|
|
2254
|
+
finalizeReplay,
|
|
2255
|
+
linkChildRun,
|
|
2256
|
+
complete(summary: string, workflowChild: WorkflowChildReplaySnapshot): void {
|
|
2257
|
+
finalize("completed", summary, workflowChild);
|
|
2258
|
+
},
|
|
2259
|
+
fail(error: unknown): void {
|
|
2260
|
+
finalize("failed", error instanceof Error ? error.message : String(error), undefined, error);
|
|
2261
|
+
},
|
|
2262
|
+
};
|
|
2263
|
+
};
|
|
2264
|
+
|
|
1865
2265
|
const buildPromptNodeUiAdapter = (): WorkflowUIAdapter => {
|
|
1866
2266
|
const ask = async (descriptor: PromptDescriptor): Promise<unknown> => {
|
|
1867
2267
|
if (ownController.signal.aborted) {
|
|
@@ -2020,7 +2420,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2020
2420
|
},
|
|
2021
2421
|
async select<T extends string>(message: string, options: readonly T[]): Promise<T> {
|
|
2022
2422
|
if (options.length === 0) {
|
|
2023
|
-
throw new Error("
|
|
2423
|
+
throw new Error("atomic-workflows: ctx.ui.select requires at least one option");
|
|
2024
2424
|
}
|
|
2025
2425
|
const response = await ask({ kind: "select", message, choices: options });
|
|
2026
2426
|
if (typeof response === "string" && (options as readonly string[]).includes(response)) {
|
|
@@ -2139,7 +2539,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2139
2539
|
return replayResult;
|
|
2140
2540
|
};
|
|
2141
2541
|
const rejectReplayMutation = (action: string): never => {
|
|
2142
|
-
throw new Error(`
|
|
2542
|
+
throw new Error(`atomic-workflows: replayed stage "${name}" cannot ${action}`);
|
|
2143
2543
|
};
|
|
2144
2544
|
const replayContext: InternalStageContext = {
|
|
2145
2545
|
name,
|
|
@@ -2175,6 +2575,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2175
2575
|
__pendingMessageCount: () => 0,
|
|
2176
2576
|
__modelFallbackMeta: () => ({
|
|
2177
2577
|
...(replaySource.model !== undefined ? { model: replaySource.model } : {}),
|
|
2578
|
+
...(replaySource.fastMode === true ? { fastMode: replaySource.fastMode } : {}),
|
|
2178
2579
|
...(replaySource.attemptedModels !== undefined ? { attemptedModels: replaySource.attemptedModels } : {}),
|
|
2179
2580
|
...(replaySource.modelAttempts !== undefined ? { modelAttempts: replaySource.modelAttempts } : {}),
|
|
2180
2581
|
}),
|
|
@@ -2188,6 +2589,16 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2188
2589
|
// d. Create inner AgentSession-like StageContext (raw, without lifecycle wrapping).
|
|
2189
2590
|
// Must come before the registry registration because the handle
|
|
2190
2591
|
// delegates to it for every operation.
|
|
2592
|
+
const applyModelFallbackMeta = (meta: ReturnType<InternalStageContext["__modelFallbackMeta"]>): void => {
|
|
2593
|
+
if (meta.model !== undefined) stageSnapshot.model = meta.model;
|
|
2594
|
+
if (meta.fastMode !== undefined) {
|
|
2595
|
+
if (meta.fastMode) stageSnapshot.fastMode = true;
|
|
2596
|
+
else delete stageSnapshot.fastMode;
|
|
2597
|
+
}
|
|
2598
|
+
if (meta.attemptedModels !== undefined) stageSnapshot.attemptedModels = meta.attemptedModels;
|
|
2599
|
+
if (meta.modelAttempts !== undefined) stageSnapshot.modelAttempts = meta.modelAttempts;
|
|
2600
|
+
};
|
|
2601
|
+
|
|
2191
2602
|
const innerCtx: InternalStageContext = createStageContext({
|
|
2192
2603
|
stageId,
|
|
2193
2604
|
stageName: name,
|
|
@@ -2196,6 +2607,13 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2196
2607
|
signal: ownController.signal,
|
|
2197
2608
|
stageOptions: options,
|
|
2198
2609
|
models: opts.models,
|
|
2610
|
+
executionMode: opts.executionMode,
|
|
2611
|
+
onModelFallbackMetaChange(meta) {
|
|
2612
|
+
applyModelFallbackMeta(meta);
|
|
2613
|
+
if (stageSnapshot.status === "running") {
|
|
2614
|
+
activeStore.recordStageStart(runId, stageSnapshot);
|
|
2615
|
+
}
|
|
2616
|
+
},
|
|
2199
2617
|
});
|
|
2200
2618
|
const activeAskUserQuestionCalls = new Set<string>();
|
|
2201
2619
|
let activeAskUserQuestionAnonymousCalls = 0;
|
|
@@ -2213,11 +2631,9 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2213
2631
|
askUserQuestionObservedThisTurn = true;
|
|
2214
2632
|
if (toolEvent.callId !== undefined) activeAskUserQuestionCalls.add(toolEvent.callId);
|
|
2215
2633
|
else activeAskUserQuestionAnonymousCalls += 1;
|
|
2216
|
-
|
|
2217
|
-
//
|
|
2218
|
-
//
|
|
2219
|
-
// (runId, stageId) key joins this to the broker request the tool's
|
|
2220
|
-
// ctx.ui.custom() call raises.
|
|
2634
|
+
// Expose a headless-answer adapter before marking the stage awaiting
|
|
2635
|
+
// input so the main-chat steering notice can include the actual
|
|
2636
|
+
// structured question instead of a promptless placeholder.
|
|
2221
2637
|
const adapter = buildStagePromptAdapter(
|
|
2222
2638
|
toolEvent.callId ?? `ask-user-question-${stageId}`,
|
|
2223
2639
|
"ask_user_question",
|
|
@@ -2225,6 +2641,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2225
2641
|
Date.now(),
|
|
2226
2642
|
);
|
|
2227
2643
|
if (adapter) stageUiBroker.provideStagePrompt(runId, stageId, adapter);
|
|
2644
|
+
activeStore.recordStageAwaitingInput(runId, stageId, true);
|
|
2228
2645
|
return;
|
|
2229
2646
|
}
|
|
2230
2647
|
|
|
@@ -2352,12 +2769,14 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2352
2769
|
stageSnapshot.endedAt = Date.now();
|
|
2353
2770
|
stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
|
|
2354
2771
|
|
|
2355
|
-
|
|
2356
|
-
if (finalModelMeta.model !== undefined) stageSnapshot.model = finalModelMeta.model;
|
|
2357
|
-
if (finalModelMeta.attemptedModels !== undefined) stageSnapshot.attemptedModels = finalModelMeta.attemptedModels;
|
|
2358
|
-
if (finalModelMeta.modelAttempts !== undefined) stageSnapshot.modelAttempts = finalModelMeta.modelAttempts;
|
|
2772
|
+
applyModelFallbackMeta(innerCtx.__modelFallbackMeta());
|
|
2359
2773
|
|
|
2360
2774
|
activeStore.recordStageEnd(runId, stageSnapshot);
|
|
2775
|
+
stageUiBroker.cancelStagePrompt(
|
|
2776
|
+
runId,
|
|
2777
|
+
stageId,
|
|
2778
|
+
new Error(`atomic-workflows: stage ${stageId} completed with pending custom UI`),
|
|
2779
|
+
);
|
|
2361
2780
|
opts.onStageEnd?.(runId, stageSnapshot);
|
|
2362
2781
|
|
|
2363
2782
|
if (opts.persistence) {
|
|
@@ -2387,7 +2806,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2387
2806
|
stageSnapshot.skippedReason = "fail-fast";
|
|
2388
2807
|
};
|
|
2389
2808
|
const parallelFailFastError = (): unknown =>
|
|
2390
|
-
stageFailFastScope?.firstFailure ?? new Error("
|
|
2809
|
+
stageFailFastScope?.firstFailure ?? new Error("atomic-workflows: skipped after parallel fail-fast");
|
|
2391
2810
|
const skipForParallelFailFast = (): void => {
|
|
2392
2811
|
if (isTerminalStage(stageSnapshot)) return;
|
|
2393
2812
|
markSkippedForParallelFailFast();
|
|
@@ -2453,7 +2872,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2453
2872
|
}
|
|
2454
2873
|
};
|
|
2455
2874
|
|
|
2456
|
-
const runTrackedStageCall = async (call: () => Promise<string
|
|
2875
|
+
const runTrackedStageCall = async (call: () => Promise<string>, eagerSession = false): Promise<string> => {
|
|
2457
2876
|
await waitForStageRelease();
|
|
2458
2877
|
if (stageFinalized) {
|
|
2459
2878
|
throw parallelFailFastError();
|
|
@@ -2481,6 +2900,33 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2481
2900
|
}
|
|
2482
2901
|
stageSnapshot.status = "running";
|
|
2483
2902
|
stageSnapshot.startedAt = Date.now();
|
|
2903
|
+
const hasExplicitFastModeCandidate = async (): Promise<boolean> => {
|
|
2904
|
+
const rawCandidate = isCodexFastModeCandidateModelId(workflowModelId(options?.model))
|
|
2905
|
+
|| (Array.isArray(options?.fallbackModels) && options.fallbackModels.some((candidate) => isCodexFastModeCandidateModelId(workflowModelId(candidate))));
|
|
2906
|
+
if (rawCandidate) return true;
|
|
2907
|
+
try {
|
|
2908
|
+
const candidates = await buildModelCandidatesFromCatalog({
|
|
2909
|
+
primaryModel: options?.model,
|
|
2910
|
+
fallbackModels: options?.fallbackModels,
|
|
2911
|
+
catalog: opts.models,
|
|
2912
|
+
});
|
|
2913
|
+
return candidates.some((candidate) => isCodexFastModeCandidateModelId(candidate.id));
|
|
2914
|
+
} catch {
|
|
2915
|
+
return false;
|
|
2916
|
+
}
|
|
2917
|
+
};
|
|
2918
|
+
const hasNoExplicitModelConfig = options?.model === undefined && options?.fallbackModels === undefined;
|
|
2919
|
+
const promptAdapterHandlesInitialPrompt = adapters.prompt !== undefined;
|
|
2920
|
+
if (eagerSession && !promptAdapterHandlesInitialPrompt && (hasNoExplicitModelConfig || await hasExplicitFastModeCandidate())) {
|
|
2921
|
+
try {
|
|
2922
|
+
await innerCtx.__ensureSession();
|
|
2923
|
+
} catch (err) {
|
|
2924
|
+
if (!(err instanceof Error && err.message.includes("prompt adapter not configured"))) {
|
|
2925
|
+
throw err;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
applyModelFallbackMeta(innerCtx.__modelFallbackMeta());
|
|
2484
2930
|
activeStore.recordStageStart(runId, stageSnapshot);
|
|
2485
2931
|
|
|
2486
2932
|
// Persistence: append stage.start entry
|
|
@@ -2556,10 +3002,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2556
3002
|
if (meta.sessionId !== undefined || meta.sessionFile !== undefined) {
|
|
2557
3003
|
activeStore.recordStageSession(runId, stageId, meta);
|
|
2558
3004
|
}
|
|
2559
|
-
|
|
2560
|
-
if (modelMeta.model !== undefined) stageSnapshot.model = modelMeta.model;
|
|
2561
|
-
if (modelMeta.attemptedModels !== undefined) stageSnapshot.attemptedModels = modelMeta.attemptedModels;
|
|
2562
|
-
if (modelMeta.modelAttempts !== undefined) stageSnapshot.modelAttempts = modelMeta.modelAttempts;
|
|
3005
|
+
applyModelFallbackMeta(innerCtx.__modelFallbackMeta());
|
|
2563
3006
|
}
|
|
2564
3007
|
if (stageFailFastScope?.failed === true && stageFailFastScope.activeStages.has(stageId)) {
|
|
2565
3008
|
markSkippedForParallelFailFast();
|
|
@@ -2626,7 +3069,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2626
3069
|
|
|
2627
3070
|
const stageContext: StageContext & Pick<InternalStageContext, "__modelFallbackMeta"> = {
|
|
2628
3071
|
name: innerCtx.name,
|
|
2629
|
-
prompt: (text, promptOptions) => runTrackedStageCall(() => innerCtx.prompt(text, promptOptions)),
|
|
3072
|
+
prompt: (text, promptOptions) => runTrackedStageCall(() => innerCtx.prompt(text, promptOptions), true),
|
|
2630
3073
|
complete: (text, completeOptions) => runTrackedStageCall(() => innerCtx.complete(text, completeOptions)),
|
|
2631
3074
|
steer: (text) => innerCtx.steer(text),
|
|
2632
3075
|
followUp: (text) => innerCtx.followUp(text),
|
|
@@ -2706,6 +3149,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2706
3149
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
2707
3150
|
...(stage.sessionFile !== undefined ? { sessionFile: stage.sessionFile } : {}),
|
|
2708
3151
|
...(stageMeta.model !== undefined ? { model: stageMeta.model } : {}),
|
|
3152
|
+
...(stageMeta.fastMode === true ? { fastMode: stageMeta.fastMode } : {}),
|
|
2709
3153
|
...(stageMeta.attemptedModels !== undefined ? { attemptedModels: stageMeta.attemptedModels } : {}),
|
|
2710
3154
|
...(stageMeta.modelAttempts !== undefined ? { modelAttempts: stageMeta.modelAttempts } : {}),
|
|
2711
3155
|
...(stageMeta.warnings !== undefined ? { warnings: stageMeta.warnings } : {}),
|
|
@@ -2768,6 +3212,131 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2768
3212
|
}
|
|
2769
3213
|
});
|
|
2770
3214
|
},
|
|
3215
|
+
|
|
3216
|
+
async workflow<TChildInputs extends WorkflowInputValues, TChildOutputs extends WorkflowOutputValues>(
|
|
3217
|
+
child: WorkflowDefinition<TChildInputs, TChildOutputs>,
|
|
3218
|
+
options: WorkflowRunChildOptions<TChildInputs> = {},
|
|
3219
|
+
): Promise<WorkflowChildResult<TChildOutputs>> {
|
|
3220
|
+
// The executor operates on type-erased definitions at runtime; the child's
|
|
3221
|
+
// declared output contract is validated dynamically by the child run and
|
|
3222
|
+
// selectWorkflowOutputs, so the typed result is reconstructed via casts.
|
|
3223
|
+
if (!isWorkflowDefinition(child)) {
|
|
3224
|
+
throw new Error("atomic-workflows: ctx.workflow(definition) requires a compiled workflow definition");
|
|
3225
|
+
}
|
|
3226
|
+
const childName = child.normalizedName;
|
|
3227
|
+
const boundaryName = options.stageName ?? `workflow:${childName}`;
|
|
3228
|
+
const boundaryReplayKey = nextWorkflowBoundaryReplayKey(boundaryName);
|
|
3229
|
+
const boundary = startWorkflowBoundaryStage(boundaryName, boundaryReplayKey);
|
|
3230
|
+
if (boundary.replayedChild !== undefined) {
|
|
3231
|
+
// Continuation replay returns the persisted child boundary exactly as
|
|
3232
|
+
// written; input validation and output remapping are intentionally not
|
|
3233
|
+
// re-run against edited workflow code for a completed child boundary.
|
|
3234
|
+
// Defer settling by one microtask so concurrent replayed boundaries
|
|
3235
|
+
// spawned in the same turn see the same frontier as the source run.
|
|
3236
|
+
await Promise.resolve();
|
|
3237
|
+
boundary.finalizeReplay();
|
|
3238
|
+
return boundary.replayedChild as WorkflowChildResult<TChildOutputs>;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
// Tracked so the finally can detach the parent-abort listener and release
|
|
3242
|
+
// the pre-registered child controller on every exit path — including the
|
|
3243
|
+
// maxDepth early return inside run(), which returns before run()'s own
|
|
3244
|
+
// cleanup. Without this, sequential ctx.workflow(...) calls accumulate one
|
|
3245
|
+
// parent-signal listener (and a leaked registry entry) per child.
|
|
3246
|
+
let childRunId: string | undefined;
|
|
3247
|
+
let detachParentAbort: (() => void) | undefined;
|
|
3248
|
+
try {
|
|
3249
|
+
const childInputs = resolveAndValidateInputs(
|
|
3250
|
+
child.inputs,
|
|
3251
|
+
options.inputs ?? {},
|
|
3252
|
+
`child workflow "${childName}" (${child.name})`,
|
|
3253
|
+
);
|
|
3254
|
+
|
|
3255
|
+
childRunId = crypto.randomUUID();
|
|
3256
|
+
boundary.linkChildRun({
|
|
3257
|
+
alias: childName,
|
|
3258
|
+
workflow: child.normalizedName,
|
|
3259
|
+
runId: childRunId,
|
|
3260
|
+
});
|
|
3261
|
+
|
|
3262
|
+
const childController = new AbortController();
|
|
3263
|
+
if (ownController.signal.aborted) {
|
|
3264
|
+
childController.abort(ownController.signal.reason);
|
|
3265
|
+
} else {
|
|
3266
|
+
const onParentAbort = () => childController.abort(ownController.signal.reason);
|
|
3267
|
+
ownController.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
3268
|
+
detachParentAbort = () =>
|
|
3269
|
+
ownController.signal.removeEventListener("abort", onParentAbort);
|
|
3270
|
+
}
|
|
3271
|
+
// Pre-register the child controller under its own runId *before* run()
|
|
3272
|
+
// so a kill targeting the child runId works even before the nested run
|
|
3273
|
+
// would register itself. The nested run() sees opts.signal set and skips
|
|
3274
|
+
// its own cancellation.register (avoiding a double-register on the same
|
|
3275
|
+
// key) while still running its finally{} unregister(runId) cleanup, so
|
|
3276
|
+
// both branches must agree on this key.
|
|
3277
|
+
opts.cancellation?.register(childRunId, childController);
|
|
3278
|
+
|
|
3279
|
+
const {
|
|
3280
|
+
runId: _parentRunId,
|
|
3281
|
+
continuation: _parentContinuation,
|
|
3282
|
+
deferWorkflowStart: _parentDeferWorkflowStart,
|
|
3283
|
+
parentRun: _parentRun,
|
|
3284
|
+
onRunStart: _parentOnRunStart,
|
|
3285
|
+
onRunEnd: _parentOnRunEnd,
|
|
3286
|
+
...childBaseOpts
|
|
3287
|
+
} = opts;
|
|
3288
|
+
const childRun = await run(child, childInputs, {
|
|
3289
|
+
...childBaseOpts,
|
|
3290
|
+
runId: childRunId,
|
|
3291
|
+
cwd: resolveWorkflowCwd(),
|
|
3292
|
+
depth: depth + 1,
|
|
3293
|
+
...(opts.registry !== undefined ? { registry: opts.registry } : {}),
|
|
3294
|
+
parentRun: {
|
|
3295
|
+
runId,
|
|
3296
|
+
stageId: boundary.id,
|
|
3297
|
+
rootRunId: opts.parentRun?.rootRunId ?? runId,
|
|
3298
|
+
},
|
|
3299
|
+
signal: childController.signal,
|
|
3300
|
+
deferWorkflowStart: false,
|
|
3301
|
+
});
|
|
3302
|
+
|
|
3303
|
+
if (childRun.status !== "completed") {
|
|
3304
|
+
const failedChildStage = childRun.stages.find((stage) => stage.failureKind !== undefined);
|
|
3305
|
+
throw new Error(
|
|
3306
|
+
`atomic-workflows: child workflow "${childName}" (${child.name}) failed with status ${childRun.status}${childRun.error !== undefined ? `: ${childRun.error}` : ""}`,
|
|
3307
|
+
{
|
|
3308
|
+
cause: {
|
|
3309
|
+
...(failedChildStage?.failureKind !== undefined ? { code: failedChildStage.failureKind } : {}),
|
|
3310
|
+
...(failedChildStage?.failureMessage !== undefined ? { message: failedChildStage.failureMessage } : {}),
|
|
3311
|
+
},
|
|
3312
|
+
},
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
const outputs = selectWorkflowOutputs(child, childRun.result);
|
|
3317
|
+
const childResult: WorkflowChildResult<TChildOutputs> = {
|
|
3318
|
+
workflow: child.normalizedName,
|
|
3319
|
+
runId: childRun.runId,
|
|
3320
|
+
status: "completed",
|
|
3321
|
+
outputs: outputs as TChildOutputs,
|
|
3322
|
+
};
|
|
3323
|
+
const workflowChild = workflowChildReplaySnapshot(childName, childResult);
|
|
3324
|
+
const outputKeys = Object.keys(outputs);
|
|
3325
|
+
boundary.complete(
|
|
3326
|
+
`Workflow "${child.name}" completed (runId: ${childRun.runId}; outputs: ${outputKeys.length > 0 ? outputKeys.join(", ") : "(none)"})`,
|
|
3327
|
+
workflowChild,
|
|
3328
|
+
);
|
|
3329
|
+
return childResult;
|
|
3330
|
+
} catch (err) {
|
|
3331
|
+
boundary.fail(err);
|
|
3332
|
+
throw err;
|
|
3333
|
+
} finally {
|
|
3334
|
+
detachParentAbort?.();
|
|
3335
|
+
// Idempotent with run()'s own finally on the normal path; required on
|
|
3336
|
+
// the maxDepth early-return path where run() never reaches its cleanup.
|
|
3337
|
+
if (childRunId !== undefined) opts.cancellation?.unregister(childRunId);
|
|
3338
|
+
}
|
|
3339
|
+
},
|
|
2771
3340
|
};
|
|
2772
3341
|
|
|
2773
3342
|
// 6. Call def.run(ctx)
|
|
@@ -2779,7 +3348,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2779
3348
|
}
|
|
2780
3349
|
}
|
|
2781
3350
|
|
|
2782
|
-
const
|
|
3351
|
+
const rawResult = await def.run(ctx);
|
|
2783
3352
|
|
|
2784
3353
|
// Post-body abort check: if signal was aborted at any point before we record
|
|
2785
3354
|
// completion, the run must be finalized as "killed", never "completed".
|
|
@@ -2787,6 +3356,9 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2787
3356
|
return finalizeKilled(runId, runSnapshot, activeStore, opts.persistence, opts.onRunEnd);
|
|
2788
3357
|
}
|
|
2789
3358
|
|
|
3359
|
+
const result = normalizeWorkflowRunOutput(def.name, rawResult);
|
|
3360
|
+
assertWorkflowRunOutputs(def.name, result, def.outputs);
|
|
3361
|
+
|
|
2790
3362
|
assertWorkflowCreatedStage(runSnapshot);
|
|
2791
3363
|
|
|
2792
3364
|
const recorded = activeStore.recordRunEnd(runId, "completed", result);
|