@bastani/atomic 0.6.4 → 0.6.5-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.
Files changed (120) hide show
  1. package/.agents/skills/create-spec/SKILL.md +6 -3
  2. package/.agents/skills/tdd/SKILL.md +107 -0
  3. package/.agents/skills/tdd/deep-modules.md +33 -0
  4. package/.agents/skills/tdd/interface-design.md +31 -0
  5. package/.agents/skills/tdd/mocking.md +59 -0
  6. package/.agents/skills/tdd/refactoring.md +10 -0
  7. package/.agents/skills/tdd/tests.md +61 -0
  8. package/.agents/skills/workflow-creator/SKILL.md +550 -0
  9. package/.agents/skills/workflow-creator/references/agent-sessions.md +891 -0
  10. package/.agents/skills/workflow-creator/references/agent-setup-recipe.md +266 -0
  11. package/.agents/skills/workflow-creator/references/computation-and-validation.md +201 -0
  12. package/.agents/skills/workflow-creator/references/control-flow.md +470 -0
  13. package/.agents/skills/workflow-creator/references/failure-modes.md +1014 -0
  14. package/.agents/skills/workflow-creator/references/getting-started.md +392 -0
  15. package/.agents/skills/workflow-creator/references/registry-and-validation.md +141 -0
  16. package/.agents/skills/workflow-creator/references/running-workflows.md +418 -0
  17. package/.agents/skills/workflow-creator/references/session-config.md +384 -0
  18. package/.agents/skills/workflow-creator/references/state-and-data-flow.md +356 -0
  19. package/.agents/skills/workflow-creator/references/user-input.md +234 -0
  20. package/.agents/skills/workflow-creator/references/workflow-inputs.md +392 -0
  21. package/.claude/agents/debugger.md +2 -2
  22. package/.claude/agents/reviewer.md +1 -1
  23. package/.claude/agents/worker.md +2 -2
  24. package/.github/agents/debugger.md +1 -1
  25. package/.github/agents/worker.md +1 -1
  26. package/.mcp.json +5 -1
  27. package/.opencode/agents/debugger.md +1 -1
  28. package/.opencode/agents/worker.md +1 -1
  29. package/README.md +236 -201
  30. package/dist/sdk/define-workflow.d.ts +11 -6
  31. package/dist/sdk/define-workflow.d.ts.map +1 -1
  32. package/dist/sdk/errors.d.ts +10 -0
  33. package/dist/sdk/errors.d.ts.map +1 -1
  34. package/dist/sdk/index.d.ts +21 -9
  35. package/dist/sdk/index.d.ts.map +1 -1
  36. package/dist/sdk/primitives/inputs.d.ts +36 -0
  37. package/dist/sdk/primitives/inputs.d.ts.map +1 -0
  38. package/dist/sdk/primitives/metadata.d.ts +40 -0
  39. package/dist/sdk/primitives/metadata.d.ts.map +1 -0
  40. package/dist/sdk/primitives/run.d.ts +57 -0
  41. package/dist/sdk/primitives/run.d.ts.map +1 -0
  42. package/dist/sdk/primitives/sessions.d.ts +128 -0
  43. package/dist/sdk/primitives/sessions.d.ts.map +1 -0
  44. package/dist/sdk/runtime/executor.d.ts +24 -56
  45. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  46. package/dist/sdk/runtime/orchestrator-entry.d.ts +26 -0
  47. package/dist/sdk/runtime/orchestrator-entry.d.ts.map +1 -0
  48. package/dist/sdk/runtime/tmux.d.ts +20 -0
  49. package/dist/sdk/runtime/tmux.d.ts.map +1 -1
  50. package/dist/sdk/types.d.ts +26 -86
  51. package/dist/sdk/types.d.ts.map +1 -1
  52. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
  53. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
  54. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  55. package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +1 -1
  56. package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +1 -1
  57. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
  58. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
  59. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
  60. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
  61. package/dist/sdk/workflows/index.d.ts +20 -12
  62. package/dist/sdk/workflows/index.d.ts.map +1 -1
  63. package/dist/services/config/additional-instructions.d.ts +1 -1
  64. package/dist/services/config/additional-instructions.d.ts.map +1 -1
  65. package/package.json +4 -4
  66. package/src/cli.ts +39 -56
  67. package/src/commands/builtin-registry.ts +37 -0
  68. package/src/commands/cli/chat/index.ts +1 -3
  69. package/src/{sdk → commands/cli}/management-commands.ts +15 -55
  70. package/src/commands/cli/session.ts +1 -1
  71. package/src/commands/cli/workflow-command.test.ts +250 -16
  72. package/src/commands/cli/workflow-inputs.test.ts +1 -0
  73. package/src/commands/cli/workflow-inputs.ts +13 -3
  74. package/src/commands/cli/workflow-list.test.ts +1 -0
  75. package/src/commands/cli/workflow-list.ts +0 -0
  76. package/src/commands/cli/workflow-status.ts +1 -1
  77. package/src/commands/cli/workflow.ts +191 -11
  78. package/src/sdk/define-workflow.test.ts +47 -16
  79. package/src/sdk/define-workflow.ts +24 -6
  80. package/src/sdk/errors.test.ts +11 -0
  81. package/src/sdk/errors.ts +13 -0
  82. package/src/sdk/index.test.ts +92 -0
  83. package/src/sdk/index.ts +71 -15
  84. package/src/sdk/primitives/inputs.ts +48 -0
  85. package/src/sdk/primitives/metadata.ts +63 -0
  86. package/src/sdk/primitives/run.ts +81 -0
  87. package/src/sdk/primitives/sessions.test.ts +594 -0
  88. package/src/sdk/primitives/sessions.ts +328 -0
  89. package/src/sdk/runtime/executor.ts +36 -115
  90. package/src/sdk/runtime/orchestrator-entry.ts +110 -0
  91. package/src/sdk/runtime/tmux.ts +33 -0
  92. package/src/sdk/types.ts +26 -91
  93. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -0
  94. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -0
  95. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -0
  96. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -0
  97. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -0
  98. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -0
  99. package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -0
  100. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -0
  101. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -0
  102. package/src/sdk/workflows/index.ts +68 -51
  103. package/src/services/config/additional-instructions.ts +1 -1
  104. package/.agents/skills/test-driven-development/SKILL.md +0 -371
  105. package/.agents/skills/test-driven-development/testing-anti-patterns.md +0 -299
  106. package/dist/commands/cli/session.d.ts +0 -67
  107. package/dist/commands/cli/session.d.ts.map +0 -1
  108. package/dist/commands/cli/workflow-status.d.ts +0 -63
  109. package/dist/commands/cli/workflow-status.d.ts.map +0 -1
  110. package/dist/sdk/commander.d.ts +0 -74
  111. package/dist/sdk/commander.d.ts.map +0 -1
  112. package/dist/sdk/management-commands.d.ts +0 -42
  113. package/dist/sdk/management-commands.d.ts.map +0 -1
  114. package/dist/sdk/workflow-cli.d.ts +0 -103
  115. package/dist/sdk/workflow-cli.d.ts.map +0 -1
  116. package/dist/sdk/workflows/builtin-registry.d.ts +0 -113
  117. package/dist/sdk/workflows/builtin-registry.d.ts.map +0 -1
  118. package/src/sdk/commander.ts +0 -161
  119. package/src/sdk/workflow-cli.ts +0 -409
  120. package/src/sdk/workflows/builtin-registry.ts +0 -23
@@ -0,0 +1,470 @@
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
+ ```