@bastani/atomic 0.5.34 → 0.6.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/README.md +329 -50
- package/dist/commands/cli/session.d.ts +67 -0
- package/dist/commands/cli/session.d.ts.map +1 -0
- package/dist/commands/cli/workflow-status.d.ts +63 -0
- package/dist/commands/cli/workflow-status.d.ts.map +1 -0
- package/dist/sdk/commander.d.ts +74 -0
- package/dist/sdk/commander.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +14 -17
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
- package/dist/sdk/define-workflow.d.ts +18 -9
- package/dist/sdk/define-workflow.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +4 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/management-commands.d.ts +42 -0
- package/dist/sdk/management-commands.d.ts.map +1 -0
- package/dist/sdk/registry.d.ts +27 -0
- package/dist/sdk/registry.d.ts.map +1 -0
- package/dist/sdk/runtime/attached-footer.d.ts +1 -1
- package/dist/sdk/runtime/executor-env.d.ts +20 -0
- package/dist/sdk/runtime/executor-env.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +61 -10
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +147 -4
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/worker-shared.d.ts +42 -0
- package/dist/sdk/worker-shared.d.ts.map +1 -0
- package/dist/sdk/workflow-cli.d.ts +103 -0
- package/dist/sdk/workflow-cli.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin-registry.d.ts +113 -0
- package/dist/sdk/workflows/builtin-registry.d.ts.map +1 -0
- package/dist/sdk/workflows/index.d.ts +5 -5
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/cli.ts +85 -144
- package/src/commands/cli/chat/index.ts +10 -0
- package/src/commands/cli/workflow-command.test.ts +279 -938
- package/src/commands/cli/workflow-inputs.test.ts +41 -11
- package/src/commands/cli/workflow-inputs.ts +47 -12
- package/src/commands/cli/workflow-list.test.ts +234 -0
- package/src/commands/cli/workflow-list.ts +0 -0
- package/src/commands/cli/workflow.ts +11 -798
- package/src/scripts/constants.ts +2 -1
- package/src/sdk/commander.ts +161 -0
- package/src/sdk/components/workflow-picker-panel.tsx +78 -258
- package/src/sdk/define-workflow.test.ts +104 -11
- package/src/sdk/define-workflow.ts +47 -11
- package/src/sdk/errors.test.ts +16 -0
- package/src/sdk/index.ts +8 -8
- package/src/sdk/management-commands.ts +151 -0
- package/src/sdk/registry.ts +132 -0
- package/src/sdk/runtime/attached-footer.ts +1 -1
- package/src/sdk/runtime/executor-env.ts +45 -0
- package/src/sdk/runtime/executor.test.ts +37 -0
- package/src/sdk/runtime/executor.ts +147 -68
- package/src/sdk/types.ts +169 -4
- package/src/sdk/worker-shared.test.ts +163 -0
- package/src/sdk/worker-shared.ts +155 -0
- package/src/sdk/workflow-cli.ts +409 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin-registry.ts +23 -0
- package/src/sdk/workflows/index.ts +10 -20
- package/src/services/system/auth.test.ts +63 -1
- package/.agents/skills/workflow-creator/SKILL.md +0 -334
- package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -888
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
- package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
- package/.agents/skills/workflow-creator/references/discovery-and-verification.md +0 -232
- package/.agents/skills/workflow-creator/references/failure-modes.md +0 -903
- package/.agents/skills/workflow-creator/references/getting-started.md +0 -275
- package/.agents/skills/workflow-creator/references/running-workflows.md +0 -235
- package/.agents/skills/workflow-creator/references/session-config.md +0 -384
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -357
- package/.agents/skills/workflow-creator/references/user-input.md +0 -234
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -272
- package/dist/sdk/runtime/discovery.d.ts +0 -132
- package/dist/sdk/runtime/discovery.d.ts.map +0 -1
- package/dist/sdk/runtime/executor-entry.d.ts +0 -11
- package/dist/sdk/runtime/executor-entry.d.ts.map +0 -1
- package/dist/sdk/runtime/loader.d.ts +0 -70
- package/dist/sdk/runtime/loader.d.ts.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/src/commands/cli/workflow.test.ts +0 -317
- package/src/sdk/runtime/discovery.ts +0 -368
- package/src/sdk/runtime/executor-entry.ts +0 -18
- package/src/sdk/runtime/loader.ts +0 -267
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# Computation and Validation
|
|
2
|
-
|
|
3
|
-
Deterministic computation — validation, data transforms, file I/O, API calls — is written as plain TypeScript inside `.run()` or session callbacks. No LLM session is needed. This is the programmatic equivalent of a `.tool()` node.
|
|
4
|
-
|
|
5
|
-
## Inline computation
|
|
6
|
-
|
|
7
|
-
Any TypeScript code inside a session callback that doesn't call an SDK prompt function is deterministic computation. Prefer handle-based lookups (`s.getMessages(plannerHandle)`) over string names when a handle is in scope — it preserves type information and survives stage renames:
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
const plannerHandle = await ctx.stage({ name: "planner" }, {}, {}, async (s) => { /* ... */ });
|
|
11
|
-
|
|
12
|
-
await ctx.stage({ name: "validate-and-fix", description: "Validate, then fix if needed" }, {}, {}, async (s) => {
|
|
13
|
-
// Step 1: Deterministic — parse prior session's output (handle-based lookup)
|
|
14
|
-
const messages = await s.getMessages(plannerHandle);
|
|
15
|
-
const planText = extractText(messages);
|
|
16
|
-
const plan = JSON.parse(planText);
|
|
17
|
-
|
|
18
|
-
// Step 2: Deterministic — validate the plan
|
|
19
|
-
const isValid = plan.tasks?.length > 0 && plan.tasks.every((t: { id: string; description: string }) => t.id && t.description);
|
|
20
|
-
|
|
21
|
-
if (!isValid) {
|
|
22
|
-
// Step 3: Agent session — ask the agent to fix the plan
|
|
23
|
-
await s.session.query("The plan is invalid. Please create a valid plan with tasks.");
|
|
24
|
-
} else {
|
|
25
|
-
// Step 4: Agent session — execute the valid plan
|
|
26
|
-
await s.session.query(`Execute this plan:\n${JSON.stringify(plan.tasks)}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
s.save(s.sessionId);
|
|
30
|
-
});
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Parsing SDK responses
|
|
34
|
-
|
|
35
|
-
Each SDK returns responses in different formats. Use helpers to extract text:
|
|
36
|
-
|
|
37
|
-
### Claude
|
|
38
|
-
|
|
39
|
-
`s.session.query()` returns `SessionMessage[]` — the native SDK transcript messages from this turn. Use `extractAssistantText()` to extract the plain text:
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
import { extractAssistantText } from "@bastani/atomic/workflows";
|
|
43
|
-
|
|
44
|
-
const result = await s.session.query("...");
|
|
45
|
-
const text = extractAssistantText(result, 0); // Extract text from SessionMessage[]
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Copilot
|
|
49
|
-
|
|
50
|
-
`s.session.getMessages()` returns `SessionEvent[]`. Concatenate every
|
|
51
|
-
top-level assistant turn's non-empty content — picking only `.at(-1)` is a
|
|
52
|
-
silent-failure trap (see `failure-modes.md` §F1 / §F2). Use the
|
|
53
|
-
`getAssistantText` helper defined in `failure-modes.md` §F1:
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
// Usage:
|
|
57
|
-
const messages = await s.session.getMessages();
|
|
58
|
-
const text = getAssistantText(messages);
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### OpenCode
|
|
62
|
-
|
|
63
|
-
`session.prompt()` returns `{ data: { info, parts } }`. Filter for text
|
|
64
|
-
parts only — non-text parts produce `[object Object]` (see
|
|
65
|
-
`failure-modes.md` §F3). Use the `extractResponseText` helper defined
|
|
66
|
-
there:
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
// Usage:
|
|
70
|
-
const text = extractResponseText(result.data!.parts);
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Validation patterns
|
|
74
|
-
|
|
75
|
-
### JSON parsing with fallback
|
|
76
|
-
|
|
77
|
-
Use a layered fallback: direct parse → **last** fenced block (not the
|
|
78
|
-
first — prose often quotes examples earlier; see `failure-modes.md` §F8) →
|
|
79
|
-
last balanced object. Canonical helper lives in `state-and-data-flow.md`
|
|
80
|
-
§"Response parsers"; copy it into a sibling `helpers/parsers.ts` and
|
|
81
|
-
import. The full three-layer implementation, including the balanced-object
|
|
82
|
-
fallback, is in `src/sdk/workflows/builtin/ralph/helpers/prompts.ts`.
|
|
83
|
-
|
|
84
|
-
### Zod validation
|
|
85
|
-
|
|
86
|
-
Import Zod directly in your workflow file for runtime validation:
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
import { z } from "zod";
|
|
90
|
-
|
|
91
|
-
const TaskSchema = z.object({
|
|
92
|
-
id: z.string(),
|
|
93
|
-
description: z.string(),
|
|
94
|
-
status: z.enum(["pending", "in_progress", "completed", "error"]),
|
|
95
|
-
blockedBy: z.array(z.string()).optional(),
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// In run():
|
|
99
|
-
const parsed = parseJsonResponse(responseText);
|
|
100
|
-
const result = TaskSchema.array().safeParse(parsed?.tasks);
|
|
101
|
-
if (!result.success) {
|
|
102
|
-
console.error("Validation failed:", result.error.issues);
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## File I/O
|
|
107
|
-
|
|
108
|
-
Read and write files directly in `run()`:
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
112
|
-
import { join } from "path";
|
|
113
|
-
|
|
114
|
-
// Inside a ctx.stage() callback:
|
|
115
|
-
async (s) => {
|
|
116
|
-
// Write to session directory
|
|
117
|
-
const outputDir = join(s.sessionDir, "artifacts");
|
|
118
|
-
await mkdir(outputDir, { recursive: true });
|
|
119
|
-
await writeFile(join(outputDir, "report.json"), JSON.stringify(data));
|
|
120
|
-
|
|
121
|
-
// Read from project
|
|
122
|
-
const config = await readFile("./tsconfig.json", "utf-8");
|
|
123
|
-
},
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## API calls
|
|
127
|
-
|
|
128
|
-
Make HTTP requests for external integrations:
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
// Inside a ctx.stage() callback:
|
|
132
|
-
async (s) => {
|
|
133
|
-
const response = await fetch("https://api.example.com/data");
|
|
134
|
-
const data = await response.json();
|
|
135
|
-
|
|
136
|
-
// Use the data in a prompt
|
|
137
|
-
await s.session.query(`Process this data:\n${JSON.stringify(data)}`);
|
|
138
|
-
s.save(s.sessionId);
|
|
139
|
-
},
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Data transforms
|
|
143
|
-
|
|
144
|
-
Transform data between sessions:
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
// Inside a ctx.stage() callback:
|
|
148
|
-
async (s) => {
|
|
149
|
-
const raw = await s.getMessages("planner"); // or s.getMessages(handle) if a handle is in scope (preferred)
|
|
150
|
-
|
|
151
|
-
// Transform: extract only task IDs and descriptions
|
|
152
|
-
const tasks = extractTasks(raw).map(t => ({
|
|
153
|
-
id: t.id,
|
|
154
|
-
description: t.description,
|
|
155
|
-
priority: calculatePriority(t),
|
|
156
|
-
}));
|
|
157
|
-
|
|
158
|
-
// Sort by priority
|
|
159
|
-
tasks.sort((a, b) => b.priority - a.priority);
|
|
160
|
-
|
|
161
|
-
// Pass to agent
|
|
162
|
-
await s.session.query(`Execute these tasks in order:\n${JSON.stringify(tasks)}`);
|
|
163
|
-
s.save(s.sessionId);
|
|
164
|
-
},
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Quality Gate with LLM-as-Judge
|
|
168
|
-
|
|
169
|
-
Add automated quality checkpoints using evaluation rubrics. This pattern applies `evaluation` + `advanced-evaluation`:
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
.run(async (ctx) => {
|
|
173
|
-
const impl = await ctx.stage({ name: "implement" }, {}, {}, async (s) => {
|
|
174
|
-
await s.session.query((s.inputs.prompt ?? ""));
|
|
175
|
-
s.save(s.sessionId);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
await ctx.stage({ name: "quality-gate" }, {}, {}, async (s) => {
|
|
179
|
-
const implTranscript = await s.transcript(impl);
|
|
180
|
-
const result = await s.session.query(
|
|
181
|
-
`You are a code quality judge. Score this implementation 1-5 for:
|
|
182
|
-
- **Correctness**: Does it solve the stated problem?
|
|
183
|
-
- **Completeness**: Are edge cases handled?
|
|
184
|
-
- **Style**: Does it follow project conventions?
|
|
185
|
-
|
|
186
|
-
## Implementation to judge
|
|
187
|
-
${implTranscript.content}
|
|
188
|
-
|
|
189
|
-
Respond with JSON: { "correctness": N, "completeness": N, "style": N, "pass": boolean, "issues": [...] }`,
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const scores = parseJsonResponse(extractAssistantText(result, 0));
|
|
193
|
-
|
|
194
|
-
if (!scores.pass) {
|
|
195
|
-
await s.session.query(`Fix these quality issues:\n${scores.issues.join("\n")}`);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
s.save(s.sessionId);
|
|
199
|
-
});
|
|
200
|
-
})
|
|
201
|
-
```
|
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
# Control Flow
|
|
2
|
-
|
|
3
|
-
Control flow in workflows is plain TypeScript inside `.run()`. Use `if`/`else` for conditionals, `for`/`while` for loops, and `break`/`continue` for early termination.
|
|
4
|
-
|
|
5
|
-
There are two levels where control flow can live:
|
|
6
|
-
|
|
7
|
-
- **Intra-session**: multiple SDK calls within one `ctx.stage()` callback — the agent remembers context across all of them.
|
|
8
|
-
- **Inter-session**: loops/conditionals at the `.run()` level that spawn multiple `ctx.stage()` calls — each iteration becomes its own visible graph node in the UI.
|
|
9
|
-
|
|
10
|
-
Prefer inter-session control flow when you want the workflow graph to reflect what actually happened at runtime.
|
|
11
|
-
|
|
12
|
-
## Conditional branching
|
|
13
|
-
|
|
14
|
-
### Inter-session branching (recommended)
|
|
15
|
-
|
|
16
|
-
Run a triage session first, then branch at the `.run()` level to spawn a purpose-built session for each outcome. Every branch appears as a distinct node in the graph:
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import { extractAssistantText } from "@bastani/atomic/workflows";
|
|
20
|
-
|
|
21
|
-
.run(async (ctx) => {
|
|
22
|
-
// Step 1: Classify the request
|
|
23
|
-
const triage = await ctx.stage({ name: "triage" }, {}, {}, async (s) => {
|
|
24
|
-
const result = await s.session.query(
|
|
25
|
-
`Classify this as "bug", "feature", or "question": ${(s.inputs.prompt ?? "")}`,
|
|
26
|
-
);
|
|
27
|
-
s.save(s.sessionId);
|
|
28
|
-
return extractAssistantText(result, 0).toLowerCase();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const classification = triage.result;
|
|
32
|
-
|
|
33
|
-
// Step 2: Branch — each path spawns its own session
|
|
34
|
-
if (classification.includes("bug")) {
|
|
35
|
-
await ctx.stage({ name: "fix-bug" }, {}, {}, async (s) => {
|
|
36
|
-
await s.session.query("Diagnose and fix the bug described above.");
|
|
37
|
-
s.save(s.sessionId);
|
|
38
|
-
});
|
|
39
|
-
} else if (classification.includes("feature")) {
|
|
40
|
-
await ctx.stage({ name: "implement-feature" }, {}, {}, async (s) => {
|
|
41
|
-
await s.session.query("Design and implement the feature described above.");
|
|
42
|
-
s.save(s.sessionId);
|
|
43
|
-
});
|
|
44
|
-
} else {
|
|
45
|
-
await ctx.stage({ name: "answer-question" }, {}, {}, async (s) => {
|
|
46
|
-
await s.session.query("Research and answer the question above.");
|
|
47
|
-
s.save(s.sessionId);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Intra-session branching
|
|
54
|
-
|
|
55
|
-
When the branching logic is simple and you want the agent to retain full context across both the triage and the action, do it all inside a single session callback:
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
import { extractAssistantText } from "@bastani/atomic/workflows";
|
|
59
|
-
|
|
60
|
-
.run(async (ctx) => {
|
|
61
|
-
await ctx.stage({ name: "triage-and-act" }, {}, {}, async (s) => {
|
|
62
|
-
const triageResult = await s.session.query(
|
|
63
|
-
`Classify this as "bug", "feature", or "question": ${(s.inputs.prompt ?? "")}`,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const classification = extractAssistantText(triageResult, 0).toLowerCase();
|
|
67
|
-
|
|
68
|
-
if (classification.includes("bug")) {
|
|
69
|
-
await s.session.query("Diagnose and fix the bug described above.");
|
|
70
|
-
} else if (classification.includes("feature")) {
|
|
71
|
-
await s.session.query("Design and implement the feature described above.");
|
|
72
|
-
} else {
|
|
73
|
-
await s.session.query("Research and answer the question above.");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
s.save(s.sessionId);
|
|
77
|
-
});
|
|
78
|
-
})
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Bounded loops
|
|
82
|
-
|
|
83
|
-
### Inter-session loops (recommended)
|
|
84
|
-
|
|
85
|
-
Each iteration spawns its own session, so the graph shows exactly how many passes ran:
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import { extractAssistantText } from "@bastani/atomic/workflows";
|
|
89
|
-
|
|
90
|
-
.run(async (ctx) => {
|
|
91
|
-
const MAX_ITERATIONS = 5;
|
|
92
|
-
|
|
93
|
-
for (let i = 1; i <= MAX_ITERATIONS; i++) {
|
|
94
|
-
const iteration = await ctx.stage({ name: `refine-${i}` }, {}, {}, async (s) => {
|
|
95
|
-
const result = await s.session.query(`Iteration ${i}: Improve the implementation.`);
|
|
96
|
-
s.save(s.sessionId);
|
|
97
|
-
return extractAssistantText(result, 0);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
if (iteration.result.includes("LGTM") || iteration.result.includes("no issues")) {
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
})
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Intra-session loops
|
|
108
|
-
|
|
109
|
-
When the agent must remember every prior iteration's output to make progress, keep the loop inside one session:
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
import { extractAssistantText } from "@bastani/atomic/workflows";
|
|
113
|
-
|
|
114
|
-
.run(async (ctx) => {
|
|
115
|
-
await ctx.stage({ name: "iterative-refinement" }, {}, {}, async (s) => {
|
|
116
|
-
const MAX_ITERATIONS = 5;
|
|
117
|
-
|
|
118
|
-
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
119
|
-
const result = await s.session.query(`Iteration ${i + 1}: Improve the implementation.`);
|
|
120
|
-
|
|
121
|
-
if (extractAssistantText(result, 0).includes("LGTM") || extractAssistantText(result, 0).includes("no issues")) {
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
s.save(s.sessionId);
|
|
127
|
-
});
|
|
128
|
-
})
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Review/fix loop pattern
|
|
132
|
-
|
|
133
|
-
The inter-session pattern is the right fit here: every review and every fix becomes its own graph node, so the executed path is fully visible. This is the production-grade approach with consecutive clean-pass detection:
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
import { extractAssistantText } from "@bastani/atomic/workflows";
|
|
137
|
-
|
|
138
|
-
.run(async (ctx) => {
|
|
139
|
-
const MAX_CYCLES = 10;
|
|
140
|
-
const CLEAN_THRESHOLD = 2;
|
|
141
|
-
let consecutiveClean = 0;
|
|
142
|
-
|
|
143
|
-
for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
144
|
-
// Each review is a visible graph node
|
|
145
|
-
const review = await ctx.stage({ name: `review-${cycle}` }, {}, {}, async (s) => {
|
|
146
|
-
const result = await s.session.query(buildReviewPrompt((s.inputs.prompt ?? "")));
|
|
147
|
-
s.save(s.sessionId);
|
|
148
|
-
return extractAssistantText(result, 0);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const reviewRaw = review.result;
|
|
152
|
-
const parsed = parseReviewResult(reviewRaw);
|
|
153
|
-
|
|
154
|
-
if (!hasActionableFindings(parsed, reviewRaw)) {
|
|
155
|
-
consecutiveClean++;
|
|
156
|
-
if (consecutiveClean >= CLEAN_THRESHOLD) {
|
|
157
|
-
break; // Two consecutive clean passes → done
|
|
158
|
-
}
|
|
159
|
-
continue; // One clean pass → verify again
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
consecutiveClean = 0;
|
|
163
|
-
|
|
164
|
-
const fixPrompt = parsed
|
|
165
|
-
? buildFixSpecFromReview(parsed, (s.inputs.prompt ?? ""))
|
|
166
|
-
: buildFixSpecFromRawReview(reviewRaw, (s.inputs.prompt ?? ""));
|
|
167
|
-
|
|
168
|
-
// Each fix is also a visible graph node
|
|
169
|
-
await ctx.stage({ name: `fix-${cycle}` }, {}, {}, async (s) => {
|
|
170
|
-
await s.session.query(fixPrompt || "Fix any remaining issues.");
|
|
171
|
-
s.save(s.sessionId);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### Same pattern with Copilot
|
|
178
|
-
|
|
179
|
-
Copilot lacks a built-in text extractor — define `getAssistantText` as a
|
|
180
|
-
helper in your workflow (canonical definition in `failure-modes.md` §F1)
|
|
181
|
-
and import it from a sibling file:
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
import { getAssistantText } from "../helpers/parsers.ts"; // see failure-modes.md §F1
|
|
185
|
-
|
|
186
|
-
.run(async (ctx) => {
|
|
187
|
-
const MAX_CYCLES = 10;
|
|
188
|
-
let consecutiveClean = 0;
|
|
189
|
-
|
|
190
|
-
for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
191
|
-
const review = await ctx.stage({ name: `review-${cycle}` }, {}, {}, async (s) => {
|
|
192
|
-
await s.session.send({
|
|
193
|
-
prompt: buildReviewPrompt((s.inputs.prompt ?? "")),
|
|
194
|
-
});
|
|
195
|
-
const reviewRaw = getAssistantText(await s.session.getMessages());
|
|
196
|
-
|
|
197
|
-
s.save(await s.session.getMessages());
|
|
198
|
-
return reviewRaw;
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const reviewRaw = review.result;
|
|
202
|
-
const parsed = parseReviewResult(reviewRaw);
|
|
203
|
-
|
|
204
|
-
if (!hasActionableFindings(parsed, reviewRaw)) {
|
|
205
|
-
consecutiveClean++;
|
|
206
|
-
if (consecutiveClean >= 2) break;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
consecutiveClean = 0;
|
|
210
|
-
|
|
211
|
-
const fixPrompt = parsed
|
|
212
|
-
? buildFixSpecFromReview(parsed, (s.inputs.prompt ?? ""))
|
|
213
|
-
: buildFixSpecFromRawReview(reviewRaw, (s.inputs.prompt ?? ""));
|
|
214
|
-
|
|
215
|
-
await ctx.stage({ name: `fix-${cycle}` }, {}, {}, async (s) => {
|
|
216
|
-
await s.session.send({
|
|
217
|
-
prompt: fixPrompt || "Fix remaining issues.",
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
s.save(await s.session.getMessages());
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
})
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## Graph topology: auto-inferred from `await`/`Promise.all`
|
|
227
|
-
|
|
228
|
-
The runtime automatically infers the workflow graph topology from the JavaScript control flow. No explicit dependency declarations are needed or supported — the graph always reflects the actual execution structure.
|
|
229
|
-
|
|
230
|
-
### Sequential (`await`): `a → b` edge
|
|
231
|
-
|
|
232
|
-
Each sequential `await ctx.stage(...)` produces a parent-child edge from the previous stage. The graph draws a real chain:
|
|
233
|
-
|
|
234
|
-
```ts
|
|
235
|
-
// ✅ Graph infers: orchestrator → planner → worker
|
|
236
|
-
.run(async (ctx) => {
|
|
237
|
-
await ctx.stage({ name: "planner" }, {}, {}, async (s) => { /* ... */ });
|
|
238
|
-
await ctx.stage({ name: "worker" }, {}, {}, async (s) => { /* ... */ });
|
|
239
|
-
})
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### Parallel (`Promise.all`): both branch from same parent
|
|
243
|
-
|
|
244
|
-
Sessions passed to `Promise.all([...])` branch from the same parent and run concurrently. The runtime gives each a sibling edge from the enclosing scope:
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
// ✅ Graph infers: orchestrator → [summarize-a, summarize-b] (parallel siblings)
|
|
248
|
-
.run(async (ctx) => {
|
|
249
|
-
const [a, b] = await Promise.all([
|
|
250
|
-
ctx.stage({ name: "summarize-a" }, {}, {}, async (s) => { /* ... */ }),
|
|
251
|
-
ctx.stage({ name: "summarize-b" }, {}, {}, async (s) => { /* ... */ }),
|
|
252
|
-
]);
|
|
253
|
-
})
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### Fan-in: stage after `Promise.all` gets all parallel stages as parents
|
|
257
|
-
|
|
258
|
-
A stage awaited after a `Promise.all` resolves automatically receives all parallel stages as parents — the graph draws a merge node:
|
|
259
|
-
|
|
260
|
-
```ts
|
|
261
|
-
// ✅ Graph infers: A → [B, C] → D (fan-in merge)
|
|
262
|
-
.run(async (ctx) => {
|
|
263
|
-
await ctx.stage({ name: "A" }, {}, {}, async (s) => { /* ... */ });
|
|
264
|
-
|
|
265
|
-
await Promise.all([
|
|
266
|
-
ctx.stage({ name: "B" }, {}, {}, async (s) => { /* ... */ }),
|
|
267
|
-
ctx.stage({ name: "C" }, {}, {}, async (s) => { /* ... */ }),
|
|
268
|
-
]);
|
|
269
|
-
|
|
270
|
-
// D receives B and C as parents — rendered as a merge node.
|
|
271
|
-
await ctx.stage({ name: "D" }, {}, {}, async (s) => { /* ... */ });
|
|
272
|
-
})
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### Nested sub-sessions: child of the enclosing session
|
|
276
|
-
|
|
277
|
-
`s.stage()` inside a callback automatically becomes a child of the enclosing session — no declaration needed:
|
|
278
|
-
|
|
279
|
-
```ts
|
|
280
|
-
await ctx.stage({ name: "outer" }, {}, {}, async (s) => {
|
|
281
|
-
// inner is a child of outer in the graph automatically
|
|
282
|
-
await s.stage({ name: "inner" }, {}, {}, async (s2) => { /* ... */ });
|
|
283
|
-
});
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### Pattern: iterative loop chains
|
|
287
|
-
|
|
288
|
-
In iterative loops each stage is naturally the successor of the last because `await` serializes them within the loop body. The graph renders as a chain by default:
|
|
289
|
-
|
|
290
|
-
```ts
|
|
291
|
-
// ✅ Graph infers a spine: planner-1 → worker-1 → planner-2 → worker-2 → ...
|
|
292
|
-
.run(async (ctx) => {
|
|
293
|
-
for (let i = 1; i <= MAX_LOOPS; i++) {
|
|
294
|
-
await ctx.stage({ name: `planner-${i}` }, {}, {}, async (s) => { /* ... */ });
|
|
295
|
-
await ctx.stage({ name: `worker-${i}` }, {}, {}, async (s) => { /* ... */ });
|
|
296
|
-
|
|
297
|
-
if (needsReview) {
|
|
298
|
-
await ctx.stage({ name: `reviewer-${i}` }, {}, {}, async (s) => { /* ... */ });
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
})
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Each iteration's stages form a natural chain because each `await` follows the previous one. Conditional stages fit in seamlessly — the graph reflects whatever path was actually executed.
|
|
305
|
-
|
|
306
|
-
### Headless (background) stages: transparent to graph topology
|
|
307
|
-
|
|
308
|
-
Headless stages (`{ headless: true }`) are **invisible in the workflow graph** — they don't consume or update the execution frontier. This means they don't affect the parent-child edges inferred for visible stages.
|
|
309
|
-
|
|
310
|
-
```ts
|
|
311
|
-
import { extractAssistantText } from "@bastani/atomic/workflows";
|
|
312
|
-
|
|
313
|
-
// ✅ Graph renders: seed → merge (headless stages are transparent)
|
|
314
|
-
.run(async (ctx) => {
|
|
315
|
-
const seed = await ctx.stage({ name: "seed" }, {}, {}, async (s) => {
|
|
316
|
-
const result = await s.session.query("Describe the project.");
|
|
317
|
-
s.save(s.sessionId);
|
|
318
|
-
return extractAssistantText(result, 0);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// Three parallel headless stages — invisible in the graph
|
|
322
|
-
const [a, b, c] = await Promise.all([
|
|
323
|
-
ctx.stage({ name: "gather-a", headless: true }, {}, {}, async (s) => {
|
|
324
|
-
const result = await s.session.query(`List 3 pros:\n\n${seed.result}`);
|
|
325
|
-
s.save(s.sessionId);
|
|
326
|
-
return extractAssistantText(result, 0);
|
|
327
|
-
}),
|
|
328
|
-
ctx.stage({ name: "gather-b", headless: true }, {}, {}, async (s) => {
|
|
329
|
-
const result = await s.session.query(`List 3 cons:\n\n${seed.result}`);
|
|
330
|
-
s.save(s.sessionId);
|
|
331
|
-
return extractAssistantText(result, 0);
|
|
332
|
-
}),
|
|
333
|
-
ctx.stage({ name: "gather-c", headless: true }, {}, {}, async (s) => {
|
|
334
|
-
const result = await s.session.query(`List 3 uses:\n\n${seed.result}`);
|
|
335
|
-
s.save(s.sessionId);
|
|
336
|
-
return extractAssistantText(result, 0);
|
|
337
|
-
}),
|
|
338
|
-
]);
|
|
339
|
-
|
|
340
|
-
// Visible merge stage — chains from "seed" in the graph (not from headless stages)
|
|
341
|
-
await ctx.stage({ name: "merge" }, {}, {}, async (s) => {
|
|
342
|
-
await s.session.query(
|
|
343
|
-
`Combine:\n\n## Pros\n${a.result}\n\n## Cons\n${b.result}\n\n## Uses\n${c.result}`,
|
|
344
|
-
);
|
|
345
|
-
s.save(s.sessionId);
|
|
346
|
-
});
|
|
347
|
-
})
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
**Key behaviors:**
|
|
351
|
-
- Headless stages don't produce graph nodes — they are tracked by a background task counter in the statusline instead
|
|
352
|
-
- The execution frontier is not updated when a headless stage spawns or settles, so the next visible stage chains from the last visible stage
|
|
353
|
-
- Headless stages still participate in `Promise.all()` — the merge stage correctly awaits all three before running
|
|
354
|
-
- Return values (`handle.result`) and transcript access (`s.transcript(handle)`) work identically
|
|
355
|
-
|
|
356
|
-
**When to use headless vs. visible parallel stages:**
|
|
357
|
-
|
|
358
|
-
| Concern | Use visible (`headless: false`) | Use headless (`headless: true`) |
|
|
359
|
-
|---|---|---|
|
|
360
|
-
| User needs to see the work | Yes — each stage gets a tmux window | No — tracked by counter only |
|
|
361
|
-
| Debugging/monitoring | Yes — visible in graph + pane preview | No — errors tracked but no TUI |
|
|
362
|
-
| Data-gathering/analysis | Possible but clutters the graph | Ideal — keeps graph clean |
|
|
363
|
-
| Infrastructure discovery | Clutters graph for support work | Ideal — Ralph uses this pattern |
|
|
364
|
-
|
|
365
|
-
### Note on data flow vs. topology
|
|
366
|
-
|
|
367
|
-
Graph topology (parent-child edges) is inferred from control flow. Data flow between sessions is separate: use `s.transcript(handle)` to read a prior session's saved output. The two concerns are independent — you do not need explicit dependency declarations to access another session's transcript; you just need that session's `await` to have completed before you read it.
|
|
368
|
-
|
|
369
|
-
## Multi-turn conversations
|
|
370
|
-
|
|
371
|
-
Within a single session callback, each SDK call adds to the conversation context — the agent remembers every prior turn. This is inherently intra-session:
|
|
372
|
-
|
|
373
|
-
```ts
|
|
374
|
-
.run(async (ctx) => {
|
|
375
|
-
await ctx.stage({ name: "guided-implementation" }, {}, {}, async (s) => {
|
|
376
|
-
// The session remembers all prior turns within the same callback
|
|
377
|
-
await s.session.query("Step 1: Set up the project structure.");
|
|
378
|
-
await s.session.query("Step 2: Implement the core logic.");
|
|
379
|
-
await s.session.query("Step 3: Add error handling.");
|
|
380
|
-
await s.session.query("Step 4: Write tests.");
|
|
381
|
-
s.save(s.sessionId);
|
|
382
|
-
});
|
|
383
|
-
})
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
## Error handling and retry patterns
|
|
387
|
-
|
|
388
|
-
### Try/catch with fallback
|
|
389
|
-
|
|
390
|
-
```ts
|
|
391
|
-
.run(async (ctx) => {
|
|
392
|
-
await ctx.stage({ name: "implement" }, {}, {}, async (s) => {
|
|
393
|
-
try {
|
|
394
|
-
await s.session.query((s.inputs.prompt ?? ""));
|
|
395
|
-
} catch (error) {
|
|
396
|
-
// Retry with simpler prompt
|
|
397
|
-
await s.session.query(
|
|
398
|
-
`The previous attempt failed. Please try a simpler approach: ${(s.inputs.prompt ?? "")}`,
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
s.save(s.sessionId);
|
|
402
|
-
});
|
|
403
|
-
})
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
### Retry with exponential backoff
|
|
407
|
-
|
|
408
|
-
```ts
|
|
409
|
-
async function retryWithBackoff<T>(
|
|
410
|
-
fn: () => Promise<T>,
|
|
411
|
-
maxRetries: number = 3,
|
|
412
|
-
baseDelay: number = 1000,
|
|
413
|
-
): Promise<T> {
|
|
414
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
415
|
-
try {
|
|
416
|
-
return await fn();
|
|
417
|
-
} catch (error) {
|
|
418
|
-
if (attempt === maxRetries - 1) throw error;
|
|
419
|
-
await new Promise(r => setTimeout(r, baseDelay * Math.pow(2, attempt)));
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
throw new Error("Unreachable");
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
.run(async (ctx) => {
|
|
426
|
-
await ctx.stage({ name: "implement" }, {}, {}, async (s) => {
|
|
427
|
-
await retryWithBackoff(() => s.session.query((s.inputs.prompt ?? "")));
|
|
428
|
-
s.save(s.sessionId);
|
|
429
|
-
});
|
|
430
|
-
})
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
## Combining patterns
|
|
434
|
-
|
|
435
|
-
Combine loops, conditionals, and inter-session data passing. Session callbacks return typed values via `SessionHandle<T>.result`, and `s.transcript(handle)` accepts a prior `SessionHandle` to read another session's saved output:
|
|
436
|
-
|
|
437
|
-
```ts
|
|
438
|
-
import { extractAssistantText } from "@bastani/atomic/workflows";
|
|
439
|
-
|
|
440
|
-
.run(async (ctx) => {
|
|
441
|
-
// Step 1: Analyse — result is available as a typed handle
|
|
442
|
-
const analysisHandle = await ctx.stage({ name: "analyze" }, {}, {}, async (s) => {
|
|
443
|
-
const result = await s.session.query(`Analyse the task: ${(s.inputs.prompt ?? "")}`);
|
|
444
|
-
s.save(s.sessionId);
|
|
445
|
-
return extractAssistantText(result, 0);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
const isComplex = analysisHandle.result.includes("complex");
|
|
449
|
-
const maxIterations = isComplex ? 10 : 3;
|
|
450
|
-
|
|
451
|
-
// Step 2: Iterative implementation — each pass is a graph node
|
|
452
|
-
for (let i = 1; i <= maxIterations; i++) {
|
|
453
|
-
const impl = await ctx.stage({ name: `implement-${i}` }, {}, {}, async (s) => {
|
|
454
|
-
// Pass the analysis transcript into this session
|
|
455
|
-
const analysis = await s.transcript(analysisHandle);
|
|
456
|
-
const result = await s.session.query(
|
|
457
|
-
i === 1
|
|
458
|
-
? `Implement based on:\n${analysis.content}`
|
|
459
|
-
: "Continue improving the implementation.",
|
|
460
|
-
);
|
|
461
|
-
s.save(s.sessionId);
|
|
462
|
-
return extractAssistantText(result, 0);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
if (impl.result.includes("all tests pass")) {
|
|
466
|
-
break;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
})
|
|
470
|
-
```
|