@bastani/atomic 0.8.21 → 0.8.22-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -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 +13 -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 +95 -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
|
@@ -276,7 +276,7 @@ export function createStageControlRegistry(): StageControlRegistry {
|
|
|
276
276
|
_byRun.clear();
|
|
277
277
|
for (const handle of handles) {
|
|
278
278
|
void Promise.resolve(handle.dispose?.()).catch((err: unknown) => {
|
|
279
|
-
console.warn("
|
|
279
|
+
console.warn("atomic-workflows: stage handle dispose failed", err);
|
|
280
280
|
});
|
|
281
281
|
}
|
|
282
282
|
},
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
11
11
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
shouldApplyCodexFastModeForScope,
|
|
14
|
+
SessionManager,
|
|
15
|
+
type AgentSession,
|
|
16
|
+
type CreateAgentSessionOptions,
|
|
17
|
+
type PromptOptions,
|
|
18
|
+
} from "@bastani/atomic";
|
|
13
19
|
import type {
|
|
14
20
|
CompleteStageOpts,
|
|
15
21
|
StageContext,
|
|
@@ -19,6 +25,7 @@ import type {
|
|
|
19
25
|
StagePromptOptions,
|
|
20
26
|
WorkflowMaxOutput,
|
|
21
27
|
WorkflowModelAttempt,
|
|
28
|
+
WorkflowExecutionMode,
|
|
22
29
|
WorkflowModelCatalogPort,
|
|
23
30
|
} from "../../shared/types.js";
|
|
24
31
|
import {
|
|
@@ -47,6 +54,8 @@ export interface StageSessionRuntime {
|
|
|
47
54
|
readonly isStreaming: AgentSession["isStreaming"];
|
|
48
55
|
/** Number of SDK-level queued steering/follow-up messages, when supported. */
|
|
49
56
|
readonly pendingMessageCount?: number;
|
|
57
|
+
/** Settings manager supplied by the Atomic SDK when the adapter did not pre-create one. */
|
|
58
|
+
readonly settingsManager?: WorkflowFastModeSettingsManager;
|
|
50
59
|
navigateTree: AgentSession["navigateTree"];
|
|
51
60
|
compact: AgentSession["compact"];
|
|
52
61
|
abortCompaction(): void;
|
|
@@ -57,12 +66,27 @@ export interface StageSessionRuntime {
|
|
|
57
66
|
|
|
58
67
|
export type StageSessionCreateOptions = CreateAgentSessionOptions & Pick<StageOptions, "mcp" | "fallbackModels">;
|
|
59
68
|
|
|
69
|
+
type WorkflowFastModeSettings = {
|
|
70
|
+
readonly chat: boolean;
|
|
71
|
+
readonly workflow: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type WorkflowFastModeSettingsManager = {
|
|
75
|
+
getCodexFastModeSettings(): WorkflowFastModeSettings;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export interface StageSessionCreateResult {
|
|
79
|
+
readonly session: StageSessionRuntime;
|
|
80
|
+
readonly settingsManager?: WorkflowFastModeSettingsManager;
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
export interface AgentSessionAdapter {
|
|
61
|
-
create(options: StageSessionCreateOptions, meta?: StageExecutionMeta): Promise<StageSessionRuntime>;
|
|
84
|
+
create(options: StageSessionCreateOptions, meta?: StageExecutionMeta): Promise<StageSessionRuntime | StageSessionCreateResult>;
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
export interface StageModelFallbackMeta {
|
|
65
88
|
readonly model?: string;
|
|
89
|
+
readonly fastMode?: boolean;
|
|
66
90
|
readonly attemptedModels?: readonly string[];
|
|
67
91
|
readonly modelAttempts?: readonly WorkflowModelAttempt[];
|
|
68
92
|
readonly warnings?: readonly string[];
|
|
@@ -94,6 +118,10 @@ export interface StageRunnerOpts {
|
|
|
94
118
|
signal?: AbortSignal;
|
|
95
119
|
/** Optional model catalog used for fallback validation/resolution. */
|
|
96
120
|
models?: WorkflowModelCatalogPort;
|
|
121
|
+
/** Runtime execution mode forwarded to stage session adapters. */
|
|
122
|
+
executionMode?: WorkflowExecutionMode;
|
|
123
|
+
/** Internal: notifies the executor when an in-flight fallback changes model/fast metadata. */
|
|
124
|
+
onModelFallbackMetaChange?: (meta: StageModelFallbackMeta) => void;
|
|
97
125
|
}
|
|
98
126
|
|
|
99
127
|
export interface InternalStageContext extends StageContext {
|
|
@@ -164,17 +192,17 @@ type AgentSessionConsumer = "prompt" | "complete";
|
|
|
164
192
|
function missingAdapter(consumer: AgentSessionConsumer): never {
|
|
165
193
|
if (consumer === "complete") {
|
|
166
194
|
throw new Error(
|
|
167
|
-
"
|
|
195
|
+
"atomic-workflows: ctx.complete requires either RunOpts.adapters.complete or RunOpts.adapters.agentSession",
|
|
168
196
|
);
|
|
169
197
|
}
|
|
170
198
|
throw new Error(
|
|
171
|
-
"
|
|
199
|
+
"atomic-workflows: prompt adapter not configured — provide an AgentSessionAdapter via RunOpts.adapters.agentSession",
|
|
172
200
|
);
|
|
173
201
|
}
|
|
174
202
|
|
|
175
203
|
function unavailableSync(property: string): never {
|
|
176
204
|
throw new Error(
|
|
177
|
-
`
|
|
205
|
+
`atomic-workflows: stage AgentSession property "${property}" is unavailable until the SDK session has been created`,
|
|
178
206
|
);
|
|
179
207
|
}
|
|
180
208
|
|
|
@@ -400,7 +428,7 @@ function splitPromptOptions(options: StagePromptOptions | undefined): {
|
|
|
400
428
|
function validatePromptOutputOptions(outputOptions: StageOutputOptions): void {
|
|
401
429
|
if (outputOptions.outputMode === "file-only" && (typeof outputOptions.output !== "string" || outputOptions.output.length === 0)) {
|
|
402
430
|
throw new Error(
|
|
403
|
-
"
|
|
431
|
+
"atomic-workflows: prompt sets outputMode: \"file-only\" but does not configure an output file. Set output to a path or use outputMode: \"inline\".",
|
|
404
432
|
);
|
|
405
433
|
}
|
|
406
434
|
}
|
|
@@ -430,8 +458,8 @@ async function finalizePromptOutput(
|
|
|
430
458
|
}
|
|
431
459
|
|
|
432
460
|
export function createStageContext(opts: StageRunnerOpts): InternalStageContext {
|
|
433
|
-
const { stageId, stageName, adapters, runId, signal, stageOptions } = opts;
|
|
434
|
-
const meta: StageExecutionMeta = { runId, stageId, stageName, signal, stageOptions };
|
|
461
|
+
const { stageId, stageName, adapters, runId, signal, stageOptions, executionMode } = opts;
|
|
462
|
+
const meta: StageExecutionMeta = { runId, stageId, stageName, signal, stageOptions, executionMode };
|
|
435
463
|
let session: StageSessionRuntime | undefined;
|
|
436
464
|
let sessionPromise: Promise<StageSessionRuntime> | undefined;
|
|
437
465
|
let lastAssistantText: string | undefined;
|
|
@@ -532,19 +560,52 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
532
560
|
return { ...(stageOptions ?? {}), model: candidate.value, fallbackModels: undefined };
|
|
533
561
|
}
|
|
534
562
|
|
|
535
|
-
|
|
536
|
-
|
|
563
|
+
let sessionSettingsManager: WorkflowFastModeSettingsManager | undefined;
|
|
564
|
+
|
|
565
|
+
function isWorkflowFastModeEnabled(): boolean | undefined {
|
|
566
|
+
const model = session?.model;
|
|
567
|
+
const settingsManager = sessionSettingsManager ?? stageOptions?.settingsManager;
|
|
568
|
+
if (model === undefined || settingsManager === undefined) return undefined;
|
|
569
|
+
return shouldApplyCodexFastModeForScope(model, settingsManager.getCodexFastModeSettings(), "workflow");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function currentModelFallbackMeta(): StageModelFallbackMeta {
|
|
573
|
+
const attemptedModels = modelAttempts.map((attempt) => attempt.model);
|
|
574
|
+
const model = selectedModel ?? workflowModelId(session?.model);
|
|
575
|
+
const fastMode = isWorkflowFastModeEnabled();
|
|
576
|
+
return {
|
|
577
|
+
...(model !== undefined ? { model } : {}),
|
|
578
|
+
...(fastMode !== undefined ? { fastMode } : {}),
|
|
579
|
+
...(attemptedModels.length > 0 ? { attemptedModels } : {}),
|
|
580
|
+
...(modelAttempts.length > 0 ? { modelAttempts: [...modelAttempts] } : {}),
|
|
581
|
+
...(modelWarnings.length > 0 ? { warnings: [...modelWarnings] } : {}),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function notifyModelFallbackMetaChange(): void {
|
|
586
|
+
opts.onModelFallbackMetaChange?.(currentModelFallbackMeta());
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function normalizeSessionCreateResult(created: StageSessionRuntime | StageSessionCreateResult): StageSessionCreateResult {
|
|
590
|
+
if ("session" in created) return created;
|
|
591
|
+
return { session: created };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function attachSession(created: StageSessionRuntime | StageSessionCreateResult): StageSessionRuntime {
|
|
595
|
+
const result = normalizeSessionCreateResult(created);
|
|
596
|
+
session = result.session;
|
|
597
|
+
sessionSettingsManager = result.settingsManager ?? result.session.settingsManager;
|
|
537
598
|
if (pendingThinkingLevel !== undefined) {
|
|
538
|
-
|
|
599
|
+
result.session.setThinkingLevel(pendingThinkingLevel);
|
|
539
600
|
}
|
|
540
601
|
for (const listener of pendingListeners) {
|
|
541
|
-
listenerUnsubscribes.set(listener,
|
|
602
|
+
listenerUnsubscribes.set(listener, result.session.subscribe(listener));
|
|
542
603
|
}
|
|
543
604
|
// Track terminating tool calls for this session so the stage result text is
|
|
544
605
|
// derived deterministically from a tool that actually ended the turn.
|
|
545
606
|
unsubscribeTerminateWatcher?.();
|
|
546
|
-
unsubscribeTerminateWatcher =
|
|
547
|
-
return
|
|
607
|
+
unsubscribeTerminateWatcher = result.session.subscribe((event) => recordTerminatingToolCall(event));
|
|
608
|
+
return result.session;
|
|
548
609
|
}
|
|
549
610
|
|
|
550
611
|
async function createSession(
|
|
@@ -561,7 +622,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
561
622
|
}
|
|
562
623
|
|
|
563
624
|
async function ensureSession(consumer: AgentSessionConsumer = "prompt"): Promise<StageSessionRuntime> {
|
|
564
|
-
if (disposed) throw new Error(`
|
|
625
|
+
if (disposed) throw new Error(`atomic-workflows: stage "${stageName}" session has been disposed`);
|
|
565
626
|
if (!sessionPromise) {
|
|
566
627
|
sessionPromise = (async () => {
|
|
567
628
|
if (!hasExplicitModelFallbackConfig) return createSession(undefined, consumer);
|
|
@@ -580,6 +641,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
580
641
|
const current = session;
|
|
581
642
|
session = undefined;
|
|
582
643
|
sessionPromise = undefined;
|
|
644
|
+
sessionSettingsManager = undefined;
|
|
583
645
|
for (const unsubscribe of listenerUnsubscribes.values()) unsubscribe();
|
|
584
646
|
listenerUnsubscribes.clear();
|
|
585
647
|
unsubscribeTerminateWatcher?.();
|
|
@@ -651,6 +713,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
651
713
|
: await createSession(candidate, consumer);
|
|
652
714
|
activeCandidateIndex = index;
|
|
653
715
|
selectedModel = candidate.id;
|
|
716
|
+
notifyModelFallbackMetaChange();
|
|
654
717
|
try {
|
|
655
718
|
await promptWithPauseResume(activeSession, text, sdkOptions);
|
|
656
719
|
modelAttempts.push({ model: candidate.id, success: true });
|
|
@@ -705,7 +768,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
705
768
|
completeOpts?.fallbackModels !== undefined
|
|
706
769
|
) {
|
|
707
770
|
throw new Error(
|
|
708
|
-
"
|
|
771
|
+
"atomic-workflows: complete options require a CompleteAdapter via RunOpts.adapters.complete",
|
|
709
772
|
);
|
|
710
773
|
}
|
|
711
774
|
// Intentional fallback: when a CompleteAdapter is not configured,
|
|
@@ -835,14 +898,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
835
898
|
},
|
|
836
899
|
|
|
837
900
|
__modelFallbackMeta() {
|
|
838
|
-
|
|
839
|
-
const model = selectedModel ?? workflowModelId(session?.model);
|
|
840
|
-
return {
|
|
841
|
-
...(model !== undefined ? { model } : {}),
|
|
842
|
-
...(attemptedModels.length > 0 ? { attemptedModels } : {}),
|
|
843
|
-
...(modelAttempts.length > 0 ? { modelAttempts: [...modelAttempts] } : {}),
|
|
844
|
-
...(modelWarnings.length > 0 ? { warnings: [...modelWarnings] } : {}),
|
|
845
|
-
};
|
|
901
|
+
return currentModelFallbackMeta();
|
|
846
902
|
},
|
|
847
903
|
|
|
848
904
|
async __requestPause() {
|
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* validateInputs — check a parsed input bag against a workflow's declared
|
|
3
|
-
* input schema. Used by slash-command and programmatic SDK dispatch
|
|
4
|
-
* reject malformed input payloads before dispatch.
|
|
3
|
+
* TypeBox input schema. Used by slash-command and programmatic SDK dispatch
|
|
4
|
+
* paths to reject malformed input payloads before dispatch.
|
|
5
5
|
*
|
|
6
6
|
* Reports:
|
|
7
7
|
* - unknown input keys (catches typos like "propmt")
|
|
8
|
-
* - wrong-typed values (number/boolean/string/
|
|
9
|
-
* - select values not in the declared
|
|
8
|
+
* - wrong-typed values (number/boolean/string/select-union/integer)
|
|
9
|
+
* - select values not in the declared literal union
|
|
10
10
|
* - missing required inputs
|
|
11
|
+
* - non-JSON-serializable values
|
|
11
12
|
*
|
|
12
13
|
* Does NOT coerce: "true" is not a boolean, "3" is not a number. JSON parsing
|
|
13
14
|
* upstream already preserves types — string-typed values reaching this point
|
|
14
15
|
* are user mistakes worth surfacing.
|
|
16
|
+
*
|
|
17
|
+
* The legacy `{ type }` descriptor is gone; the field kind, choices, and
|
|
18
|
+
* required-ness are derived from the TypeBox schema via schema-introspection,
|
|
19
|
+
* keeping the historical error wording byte-for-byte stable.
|
|
15
20
|
*/
|
|
16
21
|
|
|
17
|
-
import
|
|
22
|
+
import { Value } from "typebox/value";
|
|
23
|
+
import {
|
|
24
|
+
workflowSerializableValidationError,
|
|
25
|
+
workflowSerializableTypeName,
|
|
26
|
+
} from "../../shared/serializable.js";
|
|
27
|
+
import {
|
|
28
|
+
schemaChoices,
|
|
29
|
+
schemaFieldKind,
|
|
30
|
+
schemaIsRequired,
|
|
31
|
+
} from "../../shared/schema-introspection.js";
|
|
32
|
+
import type { TSchema, WorkflowInputValues } from "../../shared/types.js";
|
|
18
33
|
|
|
19
34
|
export interface ValidationError {
|
|
20
35
|
key: string;
|
|
@@ -22,8 +37,8 @@ export interface ValidationError {
|
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
export function validateInputs(
|
|
25
|
-
schema: Readonly<Record<string,
|
|
26
|
-
inputs:
|
|
40
|
+
schema: Readonly<Record<string, TSchema>>,
|
|
41
|
+
inputs: WorkflowInputValues,
|
|
27
42
|
): ValidationError[] {
|
|
28
43
|
const errors: ValidationError[] = [];
|
|
29
44
|
|
|
@@ -35,49 +50,71 @@ export function validateInputs(
|
|
|
35
50
|
|
|
36
51
|
for (const [key, def] of Object.entries(schema)) {
|
|
37
52
|
const value = inputs[key];
|
|
53
|
+
let hasTypeError = false;
|
|
38
54
|
|
|
39
55
|
if (value === undefined) {
|
|
40
|
-
if (def
|
|
56
|
+
if (schemaIsRequired(def)) {
|
|
41
57
|
errors.push({ key, reason: "required input is missing" });
|
|
42
58
|
}
|
|
43
59
|
continue;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
const kind = schemaFieldKind(def);
|
|
63
|
+
switch (kind) {
|
|
47
64
|
case "text":
|
|
48
|
-
case "string":
|
|
49
65
|
if (typeof value !== "string") {
|
|
50
|
-
errors.push({ key, reason: `expected string, got ${
|
|
66
|
+
errors.push({ key, reason: `expected string, got ${workflowSerializableTypeName(value)}` });
|
|
67
|
+
hasTypeError = true;
|
|
51
68
|
}
|
|
52
69
|
break;
|
|
53
70
|
case "number":
|
|
54
|
-
if (typeof value !== "number" || Number.
|
|
55
|
-
errors.push({ key, reason: `expected number, got ${
|
|
71
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
72
|
+
errors.push({ key, reason: `expected finite number, got ${workflowSerializableTypeName(value)}` });
|
|
73
|
+
hasTypeError = true;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case "integer":
|
|
77
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
78
|
+
errors.push({ key, reason: `expected integer, got ${workflowSerializableTypeName(value)}` });
|
|
79
|
+
hasTypeError = true;
|
|
56
80
|
}
|
|
57
81
|
break;
|
|
58
82
|
case "boolean":
|
|
59
83
|
if (typeof value !== "boolean") {
|
|
60
|
-
errors.push({ key, reason: `expected boolean, got ${
|
|
84
|
+
errors.push({ key, reason: `expected boolean, got ${workflowSerializableTypeName(value)}` });
|
|
85
|
+
hasTypeError = true;
|
|
61
86
|
}
|
|
62
87
|
break;
|
|
63
88
|
case "select": {
|
|
64
|
-
const
|
|
89
|
+
const choices = schemaChoices(def) ?? [];
|
|
90
|
+
const allowed = choices.join(", ");
|
|
65
91
|
if (typeof value !== "string") {
|
|
66
|
-
errors.push({ key, reason: `expected one of [${allowed}], got ${
|
|
67
|
-
|
|
92
|
+
errors.push({ key, reason: `expected one of [${allowed}], got ${workflowSerializableTypeName(value)}` });
|
|
93
|
+
hasTypeError = true;
|
|
94
|
+
} else if (!choices.includes(value)) {
|
|
68
95
|
errors.push({ key, reason: `must be one of [${allowed}]` });
|
|
69
96
|
}
|
|
70
97
|
break;
|
|
71
98
|
}
|
|
99
|
+
default:
|
|
100
|
+
// object / array / unknown: defer to the schema's own checker so a
|
|
101
|
+
// precise declared shape (Type.Object({ ... }), Type.Array(...)) is
|
|
102
|
+
// still enforced, while loose schemas accept any serializable value.
|
|
103
|
+
if (!Value.Check(def, value)) {
|
|
104
|
+
const first = [...Value.Errors(def, value)][0];
|
|
105
|
+
errors.push({ key, reason: first === undefined ? `does not match ${kind} schema` : first.message });
|
|
106
|
+
hasTypeError = true;
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const serializableError = hasTypeError
|
|
112
|
+
? undefined
|
|
113
|
+
: workflowSerializableValidationError(value, `input "${key}"`);
|
|
114
|
+
if (serializableError !== undefined) {
|
|
115
|
+
errors.push({ key, reason: serializableError.replace(/^input "[^"]+" /, "") });
|
|
72
116
|
}
|
|
73
117
|
}
|
|
74
118
|
|
|
75
119
|
return errors;
|
|
76
120
|
}
|
|
77
|
-
|
|
78
|
-
function typeName(value: unknown): string {
|
|
79
|
-
if (value === null) return "null";
|
|
80
|
-
if (Array.isArray(value)) return "array";
|
|
81
|
-
if (typeof value === "number" && Number.isNaN(value)) return "NaN";
|
|
82
|
-
return typeof value;
|
|
83
|
-
}
|
|
@@ -11,6 +11,7 @@ import { buildRuntimeAdapters, type RuntimeAdapterBuildOptions, type RuntimeWiri
|
|
|
11
11
|
import { discoverWorkflows } from "../../extension/discovery.js";
|
|
12
12
|
import { createStore } from "../../shared/store.js";
|
|
13
13
|
import { renderInputsSchema } from "../../shared/render-inputs-schema.js";
|
|
14
|
+
import { deriveInputFields } from "../../shared/schema-introspection.js";
|
|
14
15
|
import { validateInputs, type ValidationError } from "./validate-inputs.js";
|
|
15
16
|
import type { CreateAgentSessionOptions } from "@bastani/atomic";
|
|
16
17
|
import type { StageSessionRuntime } from "../foreground/stage-runner.js";
|
|
@@ -22,6 +23,7 @@ import type {
|
|
|
22
23
|
WorkflowDirectOptions,
|
|
23
24
|
WorkflowDirectTaskItem,
|
|
24
25
|
WorkflowInputSchema,
|
|
26
|
+
WorkflowInputValues,
|
|
25
27
|
WorkflowMaxOutput,
|
|
26
28
|
WorkflowOutputMode,
|
|
27
29
|
} from "../../shared/types.js";
|
|
@@ -29,7 +31,7 @@ import type {
|
|
|
29
31
|
export interface WorkflowDefinition extends StageOptions {
|
|
30
32
|
mode?: "workflow" | "named" | "single" | "parallel" | "chain";
|
|
31
33
|
workflow?: string;
|
|
32
|
-
inputs?:
|
|
34
|
+
inputs?: WorkflowInputValues;
|
|
33
35
|
/** Direct single-task mode, or root task text for direct chain/parallel execution. */
|
|
34
36
|
task?: WorkflowDirectTaskItem | string;
|
|
35
37
|
/** Direct top-level parallel mode. */
|
|
@@ -221,12 +223,21 @@ async function runNamedWorkflow(
|
|
|
221
223
|
const available = discovery.registry.names();
|
|
222
224
|
throw new Error(`Workflow not found: "${workflowName}". Available: ${available.length > 0 ? available.join(", ") : "(none)"}`);
|
|
223
225
|
}
|
|
224
|
-
|
|
226
|
+
// Apply schema defaults before validating so an input declared with both
|
|
227
|
+
// `required: true` and a `default` is not rejected as "missing" when omitted,
|
|
228
|
+
// mirroring the resolve-then-validate order every other dispatch path uses.
|
|
229
|
+
// validateInputs (rather than resolveInputs) still surfaces the full set of
|
|
230
|
+
// input problems through formatWorkflowValidationFailure; run() re-resolves
|
|
231
|
+
// internally, which is idempotent on already-defaulted inputs.
|
|
232
|
+
const inputs = withResolvedDefaults(workflow.inputs, definition.inputs ?? {});
|
|
225
233
|
const errors = validateInputs(workflow.inputs, inputs);
|
|
226
234
|
if (errors.length > 0) {
|
|
227
235
|
throw new Error(formatWorkflowValidationFailure(workflow.name, workflow.inputs, errors));
|
|
228
236
|
}
|
|
229
|
-
const result = await run(workflow, inputs,
|
|
237
|
+
const result = await run(workflow, inputs, {
|
|
238
|
+
...runOptions,
|
|
239
|
+
registry: discovery.registry,
|
|
240
|
+
});
|
|
230
241
|
return {
|
|
231
242
|
action: "run",
|
|
232
243
|
mode: "named",
|
|
@@ -241,18 +252,29 @@ async function runNamedWorkflow(
|
|
|
241
252
|
};
|
|
242
253
|
}
|
|
243
254
|
|
|
255
|
+
function withResolvedDefaults(
|
|
256
|
+
schema: Readonly<Record<string, WorkflowInputSchema>>,
|
|
257
|
+
provided: WorkflowInputValues,
|
|
258
|
+
): WorkflowInputValues {
|
|
259
|
+
const resolved: Record<string, WorkflowInputValues[string]> = {};
|
|
260
|
+
for (const [key, value] of Object.entries(provided)) {
|
|
261
|
+
if (value !== undefined) resolved[key] = value;
|
|
262
|
+
}
|
|
263
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
264
|
+
const declaredDefault = (def as { default?: unknown }).default;
|
|
265
|
+
if (resolved[key] === undefined && declaredDefault !== undefined) {
|
|
266
|
+
resolved[key] = declaredDefault as WorkflowInputValues[string];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return resolved;
|
|
270
|
+
}
|
|
271
|
+
|
|
244
272
|
function formatWorkflowValidationFailure(
|
|
245
273
|
workflowName: string,
|
|
246
274
|
schema: Readonly<Record<string, WorkflowInputSchema>>,
|
|
247
275
|
errors: ValidationError[],
|
|
248
276
|
): string {
|
|
249
|
-
const entries =
|
|
250
|
-
name,
|
|
251
|
-
type: definition.type,
|
|
252
|
-
description: definition.description,
|
|
253
|
-
required: definition.required,
|
|
254
|
-
default: "default" in definition ? definition.default : undefined,
|
|
255
|
-
}));
|
|
277
|
+
const entries = deriveInputFields(schema);
|
|
256
278
|
const lines = errors.map((error) => ` - ${error.key}: ${error.reason}`);
|
|
257
279
|
return `Invalid inputs for "${workflowName}":\n${lines.join("\n")}\n\n${renderInputsSchema(workflowName, entries)}`;
|
|
258
280
|
}
|
|
@@ -7,9 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export { defineWorkflow } from "./workflows/define-workflow.js";
|
|
10
|
+
|
|
11
|
+
// TypeBox authoring surface so jiti-loaded workflows can `import { Type } from
|
|
12
|
+
// "@bastani/workflows"` (the virtual module is built from this file).
|
|
13
|
+
export { Type } from "typebox";
|
|
14
|
+
export type { Static, TSchema } from "typebox";
|
|
10
15
|
export { createRegistry } from "./workflows/registry.js";
|
|
11
16
|
export { normalizeWorkflowName, workflowNamesEqual } from "./workflows/identity.js";
|
|
12
17
|
export type * from "./shared/types.js";
|
|
18
|
+
export { INTERACTIVE_WORKFLOW_POLICY, NON_INTERACTIVE_WORKFLOW_POLICY } from "./shared/types.js";
|
|
13
19
|
export type { WorkflowBuilder, CompletedWorkflowBuilder } from "./workflows/define-workflow.js";
|
|
14
20
|
export type { WorkflowRegistry } from "./workflows/registry.js";
|
|
15
21
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RunSnapshot,
|
|
3
|
+
StageSnapshot,
|
|
4
|
+
StoreSnapshot,
|
|
5
|
+
} from "./store-types.js";
|
|
6
|
+
|
|
7
|
+
export interface ExpandedWorkflowStageTarget {
|
|
8
|
+
readonly runId: string;
|
|
9
|
+
readonly stageId: string;
|
|
10
|
+
readonly runName: string;
|
|
11
|
+
readonly depth: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ExpandedWorkflowStage extends StageSnapshot {
|
|
15
|
+
readonly workflowGraphTarget: ExpandedWorkflowStageTarget;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ExpandedWorkflowGraph {
|
|
19
|
+
readonly stages: readonly ExpandedWorkflowStage[];
|
|
20
|
+
readonly targets: ReadonlyMap<string, ExpandedWorkflowStageTarget>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ExpandedRunResult {
|
|
24
|
+
readonly stages: ExpandedWorkflowStage[];
|
|
25
|
+
readonly terminalIds: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function virtualStageId(runId: string, stageId: string, isRootRun: boolean): string {
|
|
29
|
+
return isRootRun ? stageId : `${runId}:${stageId}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function childRunIdFor(stage: StageSnapshot): string | undefined {
|
|
33
|
+
return stage.workflowChildRun?.runId ?? stage.workflowChild?.runId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function childAliasFor(stage: StageSnapshot): string | undefined {
|
|
37
|
+
return stage.workflowChildRun?.alias ?? stage.workflowChild?.alias;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function localTerminalStageIds(stages: readonly StageSnapshot[]): readonly string[] {
|
|
41
|
+
const parentIds = new Set<string>();
|
|
42
|
+
for (const stage of stages) {
|
|
43
|
+
for (const parentId of stage.parentIds) parentIds.add(parentId);
|
|
44
|
+
}
|
|
45
|
+
const terminals = stages
|
|
46
|
+
.filter((stage) => !parentIds.has(stage.id))
|
|
47
|
+
.map((stage) => stage.id);
|
|
48
|
+
return terminals.length > 0 ? terminals : stages.map((stage) => stage.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a view-only expanded graph for a run and any nested child workflow
|
|
53
|
+
* runs it references. Child stages are cloned with virtual ids so their local
|
|
54
|
+
* parent ids do not collide with parent-run stage ids; each virtual node keeps
|
|
55
|
+
* a target mapping back to the actual `{ runId, stageId }` for attach/control.
|
|
56
|
+
*
|
|
57
|
+
* Imported workflows are flattened: a boundary stage that wraps a child run
|
|
58
|
+
* with its own stages is NOT emitted as its own node. Instead the child's
|
|
59
|
+
* stages stand in for it, so a nested workflow reads as a flat layout with no
|
|
60
|
+
* extra "information" node marking the import boundary. The boundary's incoming
|
|
61
|
+
* parents become the child roots' parents, and stages downstream of the
|
|
62
|
+
* boundary rewire to the child's terminal stages. A boundary whose child run
|
|
63
|
+
* produced no stages of its own is kept as a single node so the import stays
|
|
64
|
+
* visible. Boundary stages are non-attachable, so dropping them loses no
|
|
65
|
+
* interactive capability.
|
|
66
|
+
*/
|
|
67
|
+
export function expandWorkflowGraph(
|
|
68
|
+
snapshot: StoreSnapshot,
|
|
69
|
+
rootRunId: string,
|
|
70
|
+
): ExpandedWorkflowGraph {
|
|
71
|
+
const runById = new Map(snapshot.runs.map((run) => [run.id, run]));
|
|
72
|
+
const root = runById.get(rootRunId);
|
|
73
|
+
if (!root) return { stages: [], targets: new Map() };
|
|
74
|
+
|
|
75
|
+
const targets = new Map<string, ExpandedWorkflowStageTarget>();
|
|
76
|
+
// Cycle guard only. This relies on the store invariant that each child run is
|
|
77
|
+
// referenced by exactly one parent stage (runIds are unique and a child run
|
|
78
|
+
// has a single boundary stage), so removing a run from `visiting` on exit
|
|
79
|
+
// cannot double-expand a shared child into duplicate virtual stage ids. If
|
|
80
|
+
// that invariant is ever relaxed, dedupe expanded stages by virtual id here.
|
|
81
|
+
const visiting = new Set<string>();
|
|
82
|
+
|
|
83
|
+
const expandRun = (
|
|
84
|
+
run: RunSnapshot,
|
|
85
|
+
depth: number,
|
|
86
|
+
incomingParentIds: readonly string[],
|
|
87
|
+
): ExpandedRunResult => {
|
|
88
|
+
if (visiting.has(run.id)) return { stages: [], terminalIds: [] };
|
|
89
|
+
visiting.add(run.id);
|
|
90
|
+
|
|
91
|
+
const isRootRun = run.id === rootRunId;
|
|
92
|
+
const expandedStages: ExpandedWorkflowStage[] = [];
|
|
93
|
+
const replacementTerminals = new Map<string, readonly string[]>();
|
|
94
|
+
|
|
95
|
+
for (const stage of run.stages) {
|
|
96
|
+
const id = virtualStageId(run.id, stage.id, isRootRun);
|
|
97
|
+
const resolvedParentIds = stage.parentIds.length === 0
|
|
98
|
+
? [...incomingParentIds]
|
|
99
|
+
: stage.parentIds.flatMap((parentId) =>
|
|
100
|
+
replacementTerminals.get(parentId) ?? [virtualStageId(run.id, parentId, isRootRun)],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const childRunId = childRunIdFor(stage);
|
|
104
|
+
const childRun = childRunId === undefined ? undefined : runById.get(childRunId);
|
|
105
|
+
|
|
106
|
+
// Flatten the imported workflow in place: when the boundary wraps a child
|
|
107
|
+
// run that has its own stages, splice those stages in instead of emitting
|
|
108
|
+
// the boundary node. Child roots inherit the boundary's incoming parents,
|
|
109
|
+
// and downstream stages rewire to the child's terminals below.
|
|
110
|
+
if (childRun !== undefined && childRun.stages.length > 0) {
|
|
111
|
+
const childExpanded = expandRun(childRun, depth + 1, resolvedParentIds);
|
|
112
|
+
expandedStages.push(...childExpanded.stages);
|
|
113
|
+
replacementTerminals.set(
|
|
114
|
+
stage.id,
|
|
115
|
+
childExpanded.terminalIds.length > 0 ? childExpanded.terminalIds : resolvedParentIds,
|
|
116
|
+
);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Regular stage, or a boundary whose child produced no stages of its own:
|
|
121
|
+
// emit it so the import still appears as exactly one node.
|
|
122
|
+
const target: ExpandedWorkflowStageTarget = {
|
|
123
|
+
runId: run.id,
|
|
124
|
+
stageId: stage.id,
|
|
125
|
+
runName: run.name,
|
|
126
|
+
depth,
|
|
127
|
+
};
|
|
128
|
+
targets.set(id, target);
|
|
129
|
+
expandedStages.push({
|
|
130
|
+
...stage,
|
|
131
|
+
id,
|
|
132
|
+
parentIds: Object.freeze(resolvedParentIds),
|
|
133
|
+
workflowGraphTarget: target,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const terminalIds = localTerminalStageIds(run.stages).flatMap((stageId) =>
|
|
138
|
+
replacementTerminals.get(stageId) ?? [virtualStageId(run.id, stageId, isRootRun)],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
visiting.delete(run.id);
|
|
142
|
+
return { stages: expandedStages, terminalIds };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const expanded = expandRun(root, 0, []);
|
|
146
|
+
return { stages: expanded.stages, targets };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function expandedStageTarget(
|
|
150
|
+
graph: ExpandedWorkflowGraph,
|
|
151
|
+
virtualStageIdValue: string,
|
|
152
|
+
): ExpandedWorkflowStageTarget | undefined {
|
|
153
|
+
return graph.targets.get(virtualStageIdValue);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function stageMatchesExpandedIdentifier(
|
|
157
|
+
stage: ExpandedWorkflowStage,
|
|
158
|
+
target: string,
|
|
159
|
+
): boolean {
|
|
160
|
+
return (
|
|
161
|
+
stage.id === target ||
|
|
162
|
+
stage.name === target ||
|
|
163
|
+
stage.id.startsWith(target) ||
|
|
164
|
+
stage.workflowGraphTarget.stageId === target ||
|
|
165
|
+
stage.workflowGraphTarget.stageId.startsWith(target) ||
|
|
166
|
+
stage.workflowGraphTarget.runId === target ||
|
|
167
|
+
stage.workflowGraphTarget.runId.startsWith(target)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function expandedStageLabel(stage: ExpandedWorkflowStage): string {
|
|
172
|
+
const runPrefix = stage.workflowGraphTarget.runId.slice(0, 8);
|
|
173
|
+
const stagePrefix = stage.workflowGraphTarget.stageId.slice(0, 8);
|
|
174
|
+
const depthPrefix = stage.workflowGraphTarget.depth > 0
|
|
175
|
+
? `${childAliasFor(stage) ?? stage.workflowGraphTarget.runName}:`
|
|
176
|
+
: "";
|
|
177
|
+
return `${depthPrefix}${stage.name} (${runPrefix}/${stagePrefix})`;
|
|
178
|
+
}
|