@bastani/atomic-workflows 0.4.27-3

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/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@bastani/atomic-workflows",
3
+ "version": "0.4.27-3",
4
+ "description": "Workflow SDK for defining multi-agent workflows in Atomic CLI",
5
+ "type": "module",
6
+ "license": "BUSL-1.1",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/flora131/atomic.git",
10
+ "directory": "packages/workflow-sdk"
11
+ },
12
+ "keywords": [
13
+ "workflow",
14
+ "dsl",
15
+ "coding-agents",
16
+ "atomic",
17
+ "bun"
18
+ ],
19
+ "exports": {
20
+ ".": "./src/index.ts"
21
+ },
22
+ "files": [
23
+ "src"
24
+ ],
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "oxlint --config=../../oxlint.json src",
28
+ "lint:fix": "oxlint --config=../../oxlint.json --fix src",
29
+ "test": "bun test ./tests/**/*.test.ts"
30
+ },
31
+ "devDependencies": {
32
+ "@types/bun": "^1.3.11",
33
+ "lefthook": "^2.1.4",
34
+ "oxlint": "^1.56.0",
35
+ "typescript": "^5.9.3",
36
+ "typescript-language-server": "^5.1.3"
37
+ },
38
+ "dependencies": {
39
+ "zod": "^4.3.6"
40
+ }
41
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Chainable Workflow Builder (SDK)
3
+ *
4
+ * Lightweight version of the workflow builder for end-user workflow files.
5
+ * Records an ordered instruction list and returns a branded "blueprint"
6
+ * from `.compile()` that the Atomic CLI binary compiles at load time.
7
+ *
8
+ * Usage:
9
+ * ```ts
10
+ * import { defineWorkflow } from "@bastani/atomic-workflows";
11
+ *
12
+ * export default defineWorkflow({
13
+ * name: "my-workflow",
14
+ * description: "A workflow that does X",
15
+ * globalState: {
16
+ * count: { default: 0, reducer: "sum" },
17
+ * items: { default: () => [], reducer: "concat" },
18
+ * },
19
+ * })
20
+ * .version("1.0.0")
21
+ * .argumentHint("<file-path>")
22
+ * .stage({ name: "planner", agent: "planner", ... })
23
+ * .if(ctx => ctx.stageOutputs.has("planner"))
24
+ * .stage({ name: "executor", agent: "executor", ... })
25
+ * .else()
26
+ * .stage({ name: "fallback", agent: "fallback", ... })
27
+ * .endIf()
28
+ * .compile();
29
+ * ```
30
+ */
31
+
32
+ import type {
33
+ BaseState,
34
+ StageContext,
35
+ StageOptions,
36
+ ToolOptions,
37
+ AskUserQuestionOptions,
38
+ LoopOptions,
39
+ StateFieldOptions,
40
+ WorkflowOptions,
41
+ CompiledWorkflow,
42
+ } from "./types.ts";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Instruction — internal discriminated union recorded by the builder
46
+ // ---------------------------------------------------------------------------
47
+
48
+ type Instruction =
49
+ | { readonly type: "stage"; readonly id: string; readonly config: StageOptions }
50
+ | { readonly type: "tool"; readonly id: string; readonly config: ToolOptions }
51
+ | { readonly type: "askUserQuestion"; readonly id: string; readonly config: AskUserQuestionOptions }
52
+ | { readonly type: "if"; readonly condition: (ctx: StageContext) => boolean }
53
+ | { readonly type: "elseIf"; readonly condition: (ctx: StageContext) => boolean }
54
+ | { readonly type: "else" }
55
+ | { readonly type: "endIf" }
56
+ | { readonly type: "loop"; readonly config: LoopOptions }
57
+ | { readonly type: "endLoop" }
58
+ | { readonly type: "break"; readonly condition?: () => (state: BaseState) => boolean };
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Blueprint — the data structure carried by the branded CompiledWorkflow
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * Internal blueprint data attached to the branded CompiledWorkflow.
66
+ * The Atomic CLI binary extracts this to compile the workflow at load time.
67
+ */
68
+ export interface WorkflowBlueprint {
69
+ readonly name: string;
70
+ readonly description: string;
71
+ readonly instructions: readonly Instruction[];
72
+ readonly version?: string;
73
+ readonly argumentHint?: string;
74
+ readonly stateSchema?: Record<string, StateFieldOptions>;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Entry Point
79
+ // ---------------------------------------------------------------------------
80
+
81
+ export function defineWorkflow(options: WorkflowOptions): WorkflowBuilder {
82
+ return new WorkflowBuilder(options);
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // WorkflowBuilder
87
+ // ---------------------------------------------------------------------------
88
+
89
+ export class WorkflowBuilder {
90
+ readonly name: string;
91
+ readonly description: string;
92
+ readonly instructions: Instruction[] = [];
93
+
94
+ private _version: string | undefined;
95
+ private _argumentHint: string | undefined;
96
+ private readonly _globalState: Record<string, StateFieldOptions> | undefined;
97
+ private loopDepth: number = 0;
98
+ private nodeNames: Set<string> = new Set();
99
+
100
+ constructor(options: WorkflowOptions) {
101
+ this.name = options.name;
102
+ this.description = options.description;
103
+ this._globalState = options.globalState;
104
+ }
105
+
106
+ // -- Metadata -------------------------------------------------------------
107
+
108
+ version(v: string): this {
109
+ this._version = v;
110
+ return this;
111
+ }
112
+
113
+ argumentHint(hint: string): this {
114
+ this._argumentHint = hint;
115
+ return this;
116
+ }
117
+
118
+ // -- Linear flow ----------------------------------------------------------
119
+
120
+ stage(options: StageOptions): this {
121
+ if (this.nodeNames.has(options.name)) {
122
+ throw new Error(
123
+ `Duplicate node name: "${options.name}". Each node must have a unique name within the workflow.`,
124
+ );
125
+ }
126
+ this.nodeNames.add(options.name);
127
+ this.instructions.push({ type: "stage", id: options.name, config: options });
128
+ return this;
129
+ }
130
+
131
+ tool(options: ToolOptions): this {
132
+ if (this.nodeNames.has(options.name)) {
133
+ throw new Error(
134
+ `Duplicate node name: "${options.name}". Each node must have a unique name within the workflow.`,
135
+ );
136
+ }
137
+ this.nodeNames.add(options.name);
138
+ this.instructions.push({ type: "tool", id: options.name, config: options });
139
+ return this;
140
+ }
141
+
142
+ askUserQuestion(options: AskUserQuestionOptions): this {
143
+ if (this.nodeNames.has(options.name)) {
144
+ throw new Error(
145
+ `Duplicate node name: "${options.name}". Each node must have a unique name within the workflow.`,
146
+ );
147
+ }
148
+ this.nodeNames.add(options.name);
149
+ this.instructions.push({ type: "askUserQuestion", id: options.name, config: options });
150
+ return this;
151
+ }
152
+
153
+ // -- Conditional branching ------------------------------------------------
154
+
155
+ if(condition: (ctx: StageContext) => boolean): this {
156
+ this.instructions.push({ type: "if", condition });
157
+ return this;
158
+ }
159
+
160
+ elseIf(condition: (ctx: StageContext) => boolean): this {
161
+ this.instructions.push({ type: "elseIf", condition });
162
+ return this;
163
+ }
164
+
165
+ else(): this {
166
+ this.instructions.push({ type: "else" });
167
+ return this;
168
+ }
169
+
170
+ endIf(): this {
171
+ this.instructions.push({ type: "endIf" });
172
+ return this;
173
+ }
174
+
175
+ // -- Bounded loops --------------------------------------------------------
176
+
177
+ loop(options?: LoopOptions): this {
178
+ this.loopDepth++;
179
+ this.instructions.push({ type: "loop", config: options ?? {} });
180
+ return this;
181
+ }
182
+
183
+ endLoop(): this {
184
+ if (this.loopDepth === 0) {
185
+ throw new Error("endLoop() called without a matching loop()");
186
+ }
187
+ this.loopDepth--;
188
+ this.instructions.push({ type: "endLoop" });
189
+ return this;
190
+ }
191
+
192
+ break(condition?: () => (state: BaseState) => boolean): this {
193
+ if (this.loopDepth === 0) {
194
+ throw new Error("break() can only be used inside a loop() block");
195
+ }
196
+ this.instructions.push({ type: "break", condition });
197
+ return this;
198
+ }
199
+
200
+ // -- Terminal -------------------------------------------------------------
201
+
202
+ /**
203
+ * Compile the recorded instructions into a `CompiledWorkflow`.
204
+ *
205
+ * Returns a branded "blueprint" object that the Atomic CLI binary
206
+ * detects and compiles at load time. The blueprint carries the
207
+ * recorded instructions and metadata — no heavy compilation runs
208
+ * in the SDK.
209
+ */
210
+ compile(): CompiledWorkflow {
211
+ return {
212
+ __compiledWorkflow: true,
213
+ name: this.name,
214
+ description: this.description,
215
+ __blueprint: {
216
+ name: this.name,
217
+ description: this.description,
218
+ instructions: this.instructions,
219
+ version: this._version,
220
+ argumentHint: this._argumentHint,
221
+ stateSchema: this.getStateSchema(),
222
+ },
223
+ } as CompiledWorkflow;
224
+ }
225
+
226
+ // -- Accessors (used by the binary's compiler via blueprint) ---------------
227
+
228
+ getVersion(): string | undefined {
229
+ return this._version;
230
+ }
231
+
232
+ getArgumentHint(): string | undefined {
233
+ return this._argumentHint;
234
+ }
235
+
236
+ getStateSchema(): Record<string, StateFieldOptions> | undefined {
237
+ const loopStates = this.instructions
238
+ .filter((i): i is Extract<Instruction, { type: "loop" }> => i.type === "loop")
239
+ .map((i) => i.config.loopState)
240
+ .filter((s): s is Record<string, StateFieldOptions> => s !== undefined);
241
+
242
+ if (!this._globalState && loopStates.length === 0) {
243
+ return undefined;
244
+ }
245
+
246
+ return {
247
+ ...this._globalState,
248
+ ...Object.assign({}, ...loopStates),
249
+ };
250
+ }
251
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @bastani/atomic-workflows — Workflow SDK
3
+ *
4
+ * Lightweight SDK for defining multi-agent workflows that run in the
5
+ * Atomic CLI. Install this package in your project and create workflow
6
+ * files in `.atomic/workflows/`.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { defineWorkflow } from "@bastani/atomic-workflows";
11
+ *
12
+ * export default defineWorkflow({
13
+ * name: "my-workflow",
14
+ * description: "A workflow that does X",
15
+ * })
16
+ * .stage({
17
+ * name: "planner",
18
+ * agent: "planner",
19
+ * description: "Plans the work",
20
+ * prompt: (ctx) => `Plan this: ${ctx.userPrompt}`,
21
+ * outputMapper: (response) => ({ plan: response }),
22
+ * })
23
+ * .compile();
24
+ * ```
25
+ */
26
+
27
+ export { defineWorkflow, WorkflowBuilder } from "./define-workflow.ts";
28
+ export type { WorkflowBlueprint } from "./define-workflow.ts";
29
+ export type {
30
+ BaseState,
31
+ ExecutionContext,
32
+ ExecutionError,
33
+ ErrorAction,
34
+ GraphConfig,
35
+ ModelSpec,
36
+ NodeExecuteFn,
37
+ NodeId,
38
+ NodeResult,
39
+ RetryConfig,
40
+ Signal,
41
+ SignalData,
42
+ StageContext,
43
+ StageOutput,
44
+ StageOutputStatus,
45
+ TaskItem,
46
+ ContextPressureSnapshot,
47
+ ContextPressureLevel,
48
+ ContinuationRecord,
49
+ AccumulatedContextPressure,
50
+ SessionConfig,
51
+ StageOptions,
52
+ ToolOptions,
53
+ AskUserQuestionConfig,
54
+ AskUserQuestionOptions,
55
+ LoopOptions,
56
+ StateFieldOptions,
57
+ WorkflowOptions,
58
+ CompiledWorkflow,
59
+ } from "./types.ts";
package/src/types.ts ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Workflow SDK Type Definitions
3
+ *
4
+ * All user-facing types for defining workflows with the chainable DSL.
5
+ * These types mirror the Atomic CLI runtime types so that user workflow
6
+ * files get full IDE support (autocomplete, type checking) without
7
+ * depending on the full CLI codebase.
8
+ */
9
+
10
+ import type { z } from "zod";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Graph Core
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export type NodeId = string;
17
+
18
+ export type ModelSpec = string | "inherit";
19
+
20
+ export type Signal =
21
+ | "context_window_warning"
22
+ | "checkpoint"
23
+ | "human_input_required"
24
+ | "debug_report_generated";
25
+
26
+ export interface SignalData {
27
+ type: Signal;
28
+ message?: string;
29
+ data?: Record<string, unknown>;
30
+ }
31
+
32
+ /**
33
+ * Base workflow state. All workflow state extends this shape.
34
+ * The `outputs` record is keyed by node ID and holds each node's output.
35
+ */
36
+ export interface BaseState {
37
+ executionId: string;
38
+ lastUpdated: string;
39
+ outputs: Record<NodeId, unknown>;
40
+ }
41
+
42
+ export interface ExecutionError {
43
+ nodeId: NodeId;
44
+ error: Error | string;
45
+ timestamp: string;
46
+ attempt: number;
47
+ }
48
+
49
+ export interface RetryConfig {
50
+ maxAttempts: number;
51
+ backoffMs: number;
52
+ backoffMultiplier: number;
53
+ retryOn?: (error: Error) => boolean;
54
+ }
55
+
56
+ export type ErrorAction<TState extends BaseState = BaseState> =
57
+ | { action: "retry"; delay?: number }
58
+ | { action: "skip"; fallbackState?: Partial<TState> }
59
+ | { action: "abort"; error?: Error }
60
+ | { action: "goto"; nodeId: NodeId };
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Graph Runtime
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * Result returned from a graph node execution.
68
+ */
69
+ export interface NodeResult<TState extends BaseState = BaseState> {
70
+ stateUpdate?: Partial<TState>;
71
+ goto?: NodeId | NodeId[];
72
+ signals?: SignalData[];
73
+ }
74
+
75
+ /**
76
+ * Graph configuration for workflow execution.
77
+ */
78
+ export interface GraphConfig<TState extends BaseState = BaseState> {
79
+ maxConcurrency?: number;
80
+ timeout?: number;
81
+ contextWindowThreshold?: number;
82
+ autoCheckpoint?: boolean;
83
+ metadata?: Record<string, unknown>;
84
+ defaultModel?: ModelSpec;
85
+ outputSchema?: z.ZodType<TState>;
86
+ }
87
+
88
+ /**
89
+ * Execute function signature for graph nodes.
90
+ */
91
+ export type NodeExecuteFn<TState extends BaseState = BaseState> = (
92
+ context: ExecutionContext<TState>,
93
+ ) => Promise<NodeResult<TState>>;
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Execution Context (passed to tool execute functions)
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Context provided to tool node `execute` functions.
101
+ *
102
+ * The most commonly used field is `state`, which gives access to the
103
+ * current workflow state including outputs from prior nodes.
104
+ */
105
+ export interface ExecutionContext<TState extends BaseState = BaseState> {
106
+ state: TState;
107
+ config: GraphConfig<TState>;
108
+ errors: ExecutionError[];
109
+ abortSignal?: AbortSignal;
110
+ emit?: (type: string, data?: Record<string, unknown>) => void;
111
+ getNodeOutput?: (nodeId: NodeId) => unknown;
112
+ model?: string;
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Conductor Types (passed to stage prompt/condition callbacks)
117
+ // ---------------------------------------------------------------------------
118
+
119
+ export type StageOutputStatus = "completed" | "interrupted" | "error";
120
+
121
+ export type ContextPressureLevel = "normal" | "elevated" | "critical";
122
+
123
+ export interface ContextPressureSnapshot {
124
+ readonly inputTokens: number;
125
+ readonly outputTokens: number;
126
+ readonly maxTokens: number;
127
+ readonly usagePercentage: number;
128
+ readonly level: ContextPressureLevel;
129
+ readonly timestamp: string;
130
+ }
131
+
132
+ export interface ContinuationRecord {
133
+ readonly stageId: string;
134
+ readonly continuationIndex: number;
135
+ readonly triggerSnapshot: ContextPressureSnapshot;
136
+ readonly partialResponse: string;
137
+ readonly timestamp: string;
138
+ }
139
+
140
+ export interface StageOutput {
141
+ readonly stageId: string;
142
+ readonly rawResponse: string;
143
+ readonly parsedOutput?: unknown;
144
+ readonly status: StageOutputStatus;
145
+ readonly error?: string;
146
+ readonly contextUsage?: ContextPressureSnapshot;
147
+ readonly continuations?: readonly ContinuationRecord[];
148
+ readonly originalByteLength?: number;
149
+ }
150
+
151
+ export interface AccumulatedContextPressure {
152
+ readonly totalInputTokens: number;
153
+ readonly totalOutputTokens: number;
154
+ readonly totalContinuations: number;
155
+ readonly stageSnapshots: ReadonlyMap<string, ContextPressureSnapshot>;
156
+ readonly continuations: readonly ContinuationRecord[];
157
+ }
158
+
159
+ /**
160
+ * Task item in the workflow task list.
161
+ * Populated after the planner stage and updated throughout execution.
162
+ */
163
+ export interface TaskItem {
164
+ id?: string;
165
+ description: string;
166
+ status: string;
167
+ summary: string;
168
+ blockedBy?: string[];
169
+ }
170
+
171
+ /**
172
+ * Read-only context provided to stage prompt builders and conditional
173
+ * callbacks (`.if()`, `.elseIf()`).
174
+ */
175
+ export interface StageContext {
176
+ readonly userPrompt: string;
177
+ readonly stageOutputs: ReadonlyMap<string, StageOutput>;
178
+ readonly tasks: readonly TaskItem[];
179
+ readonly abortSignal: AbortSignal;
180
+ readonly contextPressure?: AccumulatedContextPressure;
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Session Configuration
185
+ // ---------------------------------------------------------------------------
186
+
187
+ /**
188
+ * Agent session configuration overrides.
189
+ * Used in `StageOptions.sessionConfig` to customize per-stage sessions.
190
+ */
191
+ export interface SessionConfig {
192
+ model?: string;
193
+ sessionId?: string;
194
+ systemPrompt?: string;
195
+ additionalInstructions?: string;
196
+ tools?: string[];
197
+ permissionMode?: "auto" | "prompt" | "deny" | "bypass";
198
+ maxBudgetUsd?: number;
199
+ maxTurns?: number;
200
+ reasoningEffort?: string;
201
+ maxThinkingTokens?: number;
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // DSL Stage Options
206
+ // ---------------------------------------------------------------------------
207
+
208
+ export interface StageOptions {
209
+ readonly name: string;
210
+ readonly agent?: string | null;
211
+ readonly description: string;
212
+ readonly prompt: (context: StageContext) => string;
213
+ readonly outputMapper: (response: string) => Record<string, unknown>;
214
+ readonly sessionConfig?: Partial<SessionConfig>;
215
+ readonly maxOutputBytes?: number;
216
+ readonly reads?: string[];
217
+ readonly outputs?: string[];
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // DSL Tool Options
222
+ // ---------------------------------------------------------------------------
223
+
224
+ export interface ToolOptions {
225
+ readonly name: string;
226
+ readonly execute: (
227
+ context: ExecutionContext<BaseState>,
228
+ ) => Promise<Record<string, unknown>>;
229
+ readonly description?: string;
230
+ readonly reads?: string[];
231
+ readonly outputs?: string[];
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // DSL Ask User Question Options
236
+ // ---------------------------------------------------------------------------
237
+
238
+ export interface AskUserQuestionConfig {
239
+ readonly question: string;
240
+ readonly header?: string;
241
+ readonly options?: ReadonlyArray<{
242
+ readonly label: string;
243
+ readonly description?: string;
244
+ }>;
245
+ readonly multiSelect?: boolean;
246
+ }
247
+
248
+ export interface AskUserQuestionOptions {
249
+ readonly name: string;
250
+ readonly question:
251
+ | AskUserQuestionConfig
252
+ | ((state: BaseState) => AskUserQuestionConfig);
253
+ readonly description?: string;
254
+ readonly onAnswer?: (answer: string | string[]) => Record<string, unknown>;
255
+ readonly reads?: string[];
256
+ readonly outputs?: string[];
257
+ }
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // DSL Loop Options
261
+ // ---------------------------------------------------------------------------
262
+
263
+ export interface LoopOptions {
264
+ readonly maxCycles?: number;
265
+ readonly loopState?: Record<string, StateFieldOptions>;
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // State Field Options
270
+ // ---------------------------------------------------------------------------
271
+
272
+ export interface StateFieldOptions<T = unknown> {
273
+ readonly default: T | (() => T);
274
+ readonly reducer?:
275
+ | "replace"
276
+ | "concat"
277
+ | "merge"
278
+ | "mergeById"
279
+ | "max"
280
+ | "min"
281
+ | "sum"
282
+ | "or"
283
+ | "and"
284
+ | ((current: T, update: T) => T);
285
+ readonly key?: string;
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Workflow Options (passed to defineWorkflow)
290
+ // ---------------------------------------------------------------------------
291
+
292
+ export interface WorkflowOptions {
293
+ readonly name: string;
294
+ readonly description: string;
295
+ readonly globalState?: Record<string, StateFieldOptions>;
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Compiled Workflow (opaque return type of .compile())
300
+ // ---------------------------------------------------------------------------
301
+
302
+ /**
303
+ * Opaque branded type returned by `.compile()`.
304
+ *
305
+ * Export this value as the default export (or a named export) from your
306
+ * workflow file. The Atomic CLI binary detects the brand and compiles
307
+ * the workflow at load time.
308
+ *
309
+ * ```ts
310
+ * export default defineWorkflow({ ... }).stage({ ... }).compile();
311
+ * ```
312
+ */
313
+ export interface CompiledWorkflow {
314
+ readonly __compiledWorkflow: true;
315
+ readonly name: string;
316
+ readonly description: string;
317
+ }