@bastani/atomic 0.5.23 → 0.5.24-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/.agents/skills/workflow-creator/SKILL.md +137 -326
- package/.agents/skills/workflow-creator/references/agent-sessions.md +211 -152
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +12 -37
- package/.agents/skills/workflow-creator/references/control-flow.md +20 -14
- package/.agents/skills/workflow-creator/references/discovery-and-verification.md +1 -1
- package/.agents/skills/workflow-creator/references/failure-modes.md +87 -62
- package/.agents/skills/workflow-creator/references/getting-started.md +14 -40
- package/.agents/skills/workflow-creator/references/running-workflows.md +235 -0
- package/.agents/skills/workflow-creator/references/session-config.md +24 -9
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +9 -26
- package/.agents/skills/workflow-creator/references/user-input.md +71 -43
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +25 -42
- package/dist/sdk/providers/claude.d.ts +7 -2
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/providers/opencode.d.ts +18 -2
- package/dist/sdk/providers/opencode.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts +5 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/sdk/providers/claude.ts +57 -12
- package/src/sdk/providers/headless-hil-policy.test.ts +171 -0
- package/src/sdk/providers/opencode.ts +62 -2
- package/src/sdk/runtime/executor.ts +57 -14
|
@@ -95,47 +95,38 @@ Claude maintains conversation context across calls within the same pane. Call `s
|
|
|
95
95
|
})
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
### Advanced: Claude Agent SDK `query()`
|
|
98
|
+
### Advanced: Claude Agent SDK `query()` option surface (reference only)
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
**Do not import `query` from `@anthropic-ai/claude-agent-sdk` inside a `ctx.stage()` callback.** In a non-headless stage it double-spawns Claude (idle TUI pane + in-process SDK call) — see `failure-modes.md` §F16. In a headless stage it bypasses the runtime's wiring. Always go through `s.session.query()`; the runtime forwards options to the SDK for headless stages and routes the interactive TUI for non-headless stages.
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
Two correct routes:
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
1. **Headless + SDK options** — `s.session.query(prompt, sdkOptions)` inside `{ headless: true }`.
|
|
105
|
+
2. **Interactive TUI + `--agent`** — `chatFlags: ["--agent", "<name>", ...]` in `clientOpts`.
|
|
106
106
|
|
|
107
|
-
.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
type: "json_schema",
|
|
122
|
-
schema: { type: "object", properties: { tasks: { type: "array", items: { type: "string" } } } },
|
|
123
|
-
},
|
|
124
|
-
agents: {
|
|
125
|
-
reviewer: { description: "Review code changes", prompt: "You are a code reviewer..." },
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
for await (const message of result) {
|
|
130
|
-
// Process streaming messages
|
|
131
|
-
}
|
|
107
|
+
For the full SDK option surface, see `session-config.md` §"`query()` options".
|
|
108
|
+
|
|
109
|
+
Example workflow usage in a headless stage:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
await ctx.stage({ name: "implement", headless: true }, {}, {}, async (s) => {
|
|
113
|
+
const messages = await s.session.query(s.inputs.prompt ?? "", {
|
|
114
|
+
model: "claude-opus-4-6",
|
|
115
|
+
permissionMode: "acceptEdits",
|
|
116
|
+
allowedTools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
|
|
117
|
+
outputFormat: {
|
|
118
|
+
type: "json_schema",
|
|
119
|
+
schema: { type: "object", properties: { tasks: { type: "array", items: { type: "string" } } } },
|
|
120
|
+
},
|
|
132
121
|
});
|
|
133
|
-
|
|
122
|
+
s.save(s.sessionId);
|
|
123
|
+
return extractAssistantText(messages, 0);
|
|
124
|
+
});
|
|
134
125
|
```
|
|
135
126
|
|
|
136
127
|
Key `query()` options:
|
|
137
128
|
- `model` — model ID (`"claude-opus-4-6"`, `"claude-sonnet-4-6"`) or alias (`"opus"`, `"sonnet"`, `"haiku"`)
|
|
138
|
-
- `effort` — reasoning effort (`"low"`, `"medium"`, `"high"`, `"max"` — `"max"` is Opus 4.6 only)
|
|
129
|
+
- `effort` — reasoning effort (`"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"` — `"max"` is Opus 4.6/4.7 only)
|
|
139
130
|
- `thinking` — thinking/reasoning config: `{ type: "adaptive" }` (default for supported models), `{ type: "enabled", budgetTokens: N }`, or `{ type: "disabled" }`
|
|
140
131
|
- `maxTurns` — maximum conversation turns
|
|
141
132
|
- `maxBudgetUsd` — spending cap in USD
|
|
@@ -155,43 +146,67 @@ Key `query()` options:
|
|
|
155
146
|
|
|
156
147
|
### Subagents
|
|
157
148
|
|
|
158
|
-
Claude supports parallel subagents via the `agents` option (a
|
|
149
|
+
Claude supports parallel subagents via the `agents` option (a
|
|
150
|
+
`Record<string, AgentDefinition>` keyed by agent name). In a workflow,
|
|
151
|
+
pass the option through `s.session.query(prompt, sdkOptions)` in a
|
|
152
|
+
**headless** stage — see §F16 for why the raw SDK `query()` import is
|
|
153
|
+
an anti-pattern:
|
|
159
154
|
|
|
160
155
|
```ts
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
156
|
+
await ctx.stage(
|
|
157
|
+
{ name: "implement-and-review", headless: true },
|
|
158
|
+
{}, {},
|
|
159
|
+
async (s) => {
|
|
160
|
+
const messages = await s.session.query(
|
|
161
|
+
"Implement and review the feature",
|
|
162
|
+
{
|
|
163
|
+
agents: {
|
|
164
|
+
worker: {
|
|
165
|
+
description: "Implement a single task",
|
|
166
|
+
prompt: "You are a task implementer...",
|
|
167
|
+
tools: ["Read", "Write", "Edit", "Bash"],
|
|
168
|
+
},
|
|
169
|
+
reviewer: {
|
|
170
|
+
description: "Review code changes",
|
|
171
|
+
prompt: "You are a code reviewer...",
|
|
172
|
+
tools: ["Read", "Grep", "Glob"],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
s.save(s.sessionId);
|
|
178
|
+
return extractAssistantText(messages, 0);
|
|
171
179
|
},
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const result = query({
|
|
175
|
-
prompt: "Implement and review the feature",
|
|
176
|
-
options: { agents },
|
|
177
|
-
});
|
|
180
|
+
);
|
|
178
181
|
```
|
|
179
182
|
|
|
180
183
|
### Session continuity
|
|
181
184
|
|
|
182
|
-
Resume or fork prior sessions
|
|
185
|
+
Resume or fork prior sessions through `s.session.query()` in a headless
|
|
186
|
+
stage (same reasoning as Subagents above — never import `query` directly):
|
|
183
187
|
|
|
184
188
|
```ts
|
|
185
189
|
// Resume a session (continues the same conversation)
|
|
186
|
-
|
|
190
|
+
await ctx.stage({ name: "continue", headless: true }, {}, {}, async (s) => {
|
|
191
|
+
const messages = await s.session.query("Continue...", { resume: sessionId });
|
|
192
|
+
s.save(s.sessionId);
|
|
193
|
+
return extractAssistantText(messages, 0);
|
|
194
|
+
});
|
|
187
195
|
|
|
188
196
|
// Fork a session (creates a new branch from the session's history)
|
|
189
|
-
|
|
197
|
+
await ctx.stage({ name: "fork", headless: true }, {}, {}, async (s) => {
|
|
198
|
+
const messages = await s.session.query(
|
|
199
|
+
"Try a different approach",
|
|
200
|
+
{ resume: sessionId, forkSession: true },
|
|
201
|
+
);
|
|
202
|
+
s.save(s.sessionId);
|
|
203
|
+
return extractAssistantText(messages, 0);
|
|
204
|
+
});
|
|
190
205
|
```
|
|
191
206
|
|
|
192
|
-
###
|
|
207
|
+
### Subagent delegation
|
|
193
208
|
|
|
194
|
-
For stages that call a single
|
|
209
|
+
For stages that call a single subagent, use `--agent` (interactive) or the SDK `agent` option (headless) to route all prompts through that agent. The agent must be defined in `.claude/agents/` or `.agents/skills/`.
|
|
195
210
|
|
|
196
211
|
**Interactive stages** — pass `--agent` via `chatFlags` in client opts (2nd arg):
|
|
197
212
|
|
|
@@ -318,38 +333,45 @@ A workflow is not just a sequence of agent calls — it is an **information
|
|
|
318
333
|
flow problem**. The single most common failure mode in Copilot workflows is
|
|
319
334
|
assuming context carries across session boundaries when it doesn't.
|
|
320
335
|
Designing a workflow without thinking about information flow produces
|
|
321
|
-
|
|
336
|
+
subagents that hallucinate, repeat work, or drop requirements silently.
|
|
322
337
|
|
|
323
338
|
**Treat this section as load-bearing**, not decorative. If you skip it, your
|
|
324
339
|
workflow will ship broken in subtle, non-deterministic ways.
|
|
325
340
|
|
|
326
|
-
####
|
|
341
|
+
#### Session lifecycle states
|
|
327
342
|
|
|
328
|
-
|
|
343
|
+
For normal workflow authoring, use the **3-state rubric** from SKILL.md:
|
|
344
|
+
`Fresh` / `Continued` / `Closed`. Every new `ctx.stage()` call is fresh; if
|
|
345
|
+
you need full history, prefer another turn inside the same stage callback.
|
|
346
|
+
|
|
347
|
+
Copilot also exposes an advanced `Resumed` state at the provider level. Each
|
|
329
348
|
state determines what context the model sees on its next turn:
|
|
330
349
|
|
|
331
|
-
| State
|
|
332
|
-
|
|
333
|
-
| **Fresh**
|
|
334
|
-
| **Continued**
|
|
335
|
-
| **Resumed** | `client.resumeSession(sessionId)`
|
|
336
|
-
| **Closed**
|
|
350
|
+
| State | How you get there | Context available | Action needed |
|
|
351
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
352
|
+
| **Fresh** | `client.createSession(...)` (what `ctx.stage()` does) | **None** — empty conversation | You MUST inject everything the agent needs in the first prompt |
|
|
353
|
+
| **Continued** | Same session, additional `send` calls | All prior turns in this session | Nothing — but watch total token usage |
|
|
354
|
+
| **Resumed** *(advanced)* | `client.resumeSession(sessionId)` | All persisted turns from the prior session of the SAME agent | Nothing — full history is reattached. Use only for same-role continuation |
|
|
355
|
+
| **Closed** | `session.disconnect()` or `client.stop()` (auto-handled by runtime after the stage callback returns) | **Gone** from the live client; persisted on disk if the host enables it | Either resume by ID (same agent) or start fresh and re-inject context |
|
|
337
356
|
|
|
338
357
|
The failure mode: you close a session, create a new one, and assume the new
|
|
339
358
|
one "remembers" the previous conversation. It doesn't. `client` is just the
|
|
340
|
-
transport — each session is a fully independent conversation.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
table: **every new `ctx.stage()` call is fresh**. If you need full history,
|
|
344
|
-
prefer another turn inside the same stage callback. Resume/fork APIs are
|
|
345
|
-
provider-specific escape hatches, not the default stage-to-stage handoff path.
|
|
359
|
+
transport — each session is a fully independent conversation. Resume/fork
|
|
360
|
+
APIs are provider-specific escape hatches, not the default stage-to-stage
|
|
361
|
+
handoff path.
|
|
346
362
|
|
|
347
363
|
```ts
|
|
348
|
-
// Buggy — the orchestrator
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
await
|
|
352
|
-
await
|
|
364
|
+
// Buggy — the orchestrator stage is fresh and knows NOTHING about what
|
|
365
|
+
// the planner just produced, because each ctx.stage() starts a brand-new
|
|
366
|
+
// conversation for Copilot.
|
|
367
|
+
await ctx.stage({ name: "planner" }, {}, { agent: "planner" }, async (s) => {
|
|
368
|
+
await s.session.send({ prompt: buildPlannerPrompt((s.inputs.prompt ?? "")) });
|
|
369
|
+
s.save(await s.session.getMessages());
|
|
370
|
+
});
|
|
371
|
+
await ctx.stage({ name: "orchestrator" }, {}, { agent: "orchestrator" }, async (s) => {
|
|
372
|
+
await s.session.send({ prompt: buildOrchestratorPrompt() });
|
|
373
|
+
s.save(await s.session.getMessages());
|
|
374
|
+
});
|
|
353
375
|
// ↑ orchestrator only sees buildOrchestratorPrompt() — no planner output,
|
|
354
376
|
// no original user spec, no context.
|
|
355
377
|
```
|
|
@@ -361,22 +383,37 @@ mutually exclusive — ralph uses (1) + (2) together as belt-and-braces.
|
|
|
361
383
|
|
|
362
384
|
**1. Explicit prompt handoff** — capture the prior session's last assistant
|
|
363
385
|
message and inject it (or a summary) into the next session's first prompt.
|
|
364
|
-
Simplest and most common fix
|
|
386
|
+
Simplest and most common fix. Use `ctx.stage()` — the runtime auto-creates
|
|
387
|
+
and cleans up each session, so never call `client.createSession()` directly
|
|
388
|
+
(see Structural Rule 7 in SKILL.md):
|
|
365
389
|
|
|
366
390
|
```ts
|
|
367
|
-
async function runAgent(agent: string, prompt: string): Promise<string> {
|
|
368
|
-
const session = await client.createSession({ agent, onPermissionRequest: approveAll });
|
|
369
|
-
await session.send({ prompt });
|
|
370
|
-
const messages = await session.getMessages();
|
|
371
|
-
await session.disconnect();
|
|
372
|
-
return getAssistantText(messages); // concatenate every top-level turn — see failure-modes.md §F1
|
|
373
|
-
}
|
|
374
|
-
|
|
375
391
|
// Correct — forward the planner's output into the orchestrator prompt
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
392
|
+
const plannerHandle = await ctx.stage(
|
|
393
|
+
{ name: "planner" },
|
|
394
|
+
{},
|
|
395
|
+
{ agent: "planner" },
|
|
396
|
+
async (s) => {
|
|
397
|
+
await s.session.send({ prompt: buildPlannerPrompt((s.inputs.prompt ?? "")) });
|
|
398
|
+
const messages = await s.session.getMessages();
|
|
399
|
+
s.save(messages);
|
|
400
|
+
return getAssistantText(messages); // see failure-modes.md §F1 for getAssistantText
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
await ctx.stage(
|
|
405
|
+
{ name: "orchestrator" },
|
|
406
|
+
{},
|
|
407
|
+
{ agent: "orchestrator" },
|
|
408
|
+
async (s) => {
|
|
409
|
+
await s.session.send({
|
|
410
|
+
prompt: buildOrchestratorPrompt(
|
|
411
|
+
(s.inputs.prompt ?? ""),
|
|
412
|
+
{ plannerNotes: plannerHandle.result },
|
|
413
|
+
),
|
|
414
|
+
});
|
|
415
|
+
s.save(await s.session.getMessages());
|
|
416
|
+
},
|
|
380
417
|
);
|
|
381
418
|
```
|
|
382
419
|
|
|
@@ -427,15 +464,15 @@ trade-offs in depth.
|
|
|
427
464
|
Information flow is a design problem, not an implementation detail. Before
|
|
428
465
|
committing to a session layout, pull in the relevant skills:
|
|
429
466
|
|
|
430
|
-
| When you're deciding...
|
|
431
|
-
|
|
432
|
-
| What context each session actually needs (anatomy + token budget)
|
|
467
|
+
| When you're deciding... | Consult |
|
|
468
|
+
| ------------------------------------------------------------------------ | ---------------------- |
|
|
469
|
+
| What context each session actually needs (anatomy + token budget) | `context-fundamentals` |
|
|
433
470
|
| How many sessions and how they hand off (orchestrator vs peers vs swarm) | `multi-agent-patterns` |
|
|
434
|
-
| How to compress large planner/reviewer output before re-injecting
|
|
435
|
-
| How to detect and prevent lost-in-middle, poisoning, and distraction
|
|
436
|
-
| How to use files as coordination medium across sessions
|
|
437
|
-
| How to persist knowledge across whole workflow runs
|
|
438
|
-
| Which turns to drop, which to cache, when to compact
|
|
471
|
+
| How to compress large planner/reviewer output before re-injecting | `context-compression` |
|
|
472
|
+
| How to detect and prevent lost-in-middle, poisoning, and distraction | `context-degradation` |
|
|
473
|
+
| How to use files as coordination medium across sessions | `filesystem-context` |
|
|
474
|
+
| How to persist knowledge across whole workflow runs | `memory-systems` |
|
|
475
|
+
| Which turns to drop, which to cache, when to compact | `context-optimization` |
|
|
439
476
|
|
|
440
477
|
These aren't optional reading — they're the difference between a workflow
|
|
441
478
|
that works on day one and a workflow that silently degrades as inputs grow.
|
|
@@ -518,27 +555,17 @@ await ctx.stage(
|
|
|
518
555
|
|
|
519
556
|
Do **not** just grab `.at(-1).data.content` — a Copilot turn's final
|
|
520
557
|
`assistant.message` often has empty `content` (tool-calls-only) and
|
|
521
|
-
|
|
522
|
-
every top-level turn's non-empty content instead.
|
|
523
|
-
|
|
524
|
-
|
|
558
|
+
subagent messages can pollute the stream via `parentToolCallId`. Concatenate
|
|
559
|
+
every top-level turn's non-empty content instead.
|
|
560
|
+
|
|
561
|
+
The canonical `getAssistantText` helper lives in `failure-modes.md` §F1 —
|
|
562
|
+
copy it into a sibling `helpers/parsers.ts` and import it. Usage:
|
|
525
563
|
|
|
526
564
|
```ts
|
|
527
|
-
import
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
* and computation-and-validation.md. */
|
|
532
|
-
function getAssistantText(messages: SessionEvent[]): string {
|
|
533
|
-
return messages
|
|
534
|
-
.filter(
|
|
535
|
-
(m): m is Extract<SessionEvent, { type: "assistant.message" }> =>
|
|
536
|
-
m.type === "assistant.message" && !m.data.parentToolCallId,
|
|
537
|
-
)
|
|
538
|
-
.map((m) => m.data.content)
|
|
539
|
-
.filter((c) => c.length > 0)
|
|
540
|
-
.join("\n\n");
|
|
541
|
-
}
|
|
565
|
+
import { getAssistantText } from "../helpers/parsers.ts";
|
|
566
|
+
|
|
567
|
+
const messages = await s.session.getMessages();
|
|
568
|
+
const text = getAssistantText(messages);
|
|
542
569
|
```
|
|
543
570
|
|
|
544
571
|
### Streaming events
|
|
@@ -554,9 +581,9 @@ s.session.on("assistant.reasoning_delta", (event) => {
|
|
|
554
581
|
});
|
|
555
582
|
```
|
|
556
583
|
|
|
557
|
-
###
|
|
584
|
+
### Subagent delegation
|
|
558
585
|
|
|
559
|
-
Pass the `agent` parameter in `sessionOpts` (3rd arg to `ctx.stage()`) to bind the session to a named
|
|
586
|
+
Pass the `agent` parameter in `sessionOpts` (3rd arg to `ctx.stage()`) to bind the session to a named subagent:
|
|
560
587
|
|
|
561
588
|
```ts
|
|
562
589
|
.run(async (ctx) => {
|
|
@@ -629,11 +656,11 @@ export default defineWorkflow({
|
|
|
629
656
|
|
|
630
657
|
OpenCode sessions have **exactly the same isolation semantics as Copilot
|
|
631
658
|
sessions**. Every call to `client.session.create(...)` returns a fresh,
|
|
632
|
-
empty conversation. Creating a new session for the next
|
|
659
|
+
empty conversation. Creating a new session for the next subagent wipes
|
|
633
660
|
everything the prior session knew — conversation history, tool-call
|
|
634
661
|
results, intermediate reasoning — unless you forward it explicitly.
|
|
635
662
|
|
|
636
|
-
The full explanation, the
|
|
663
|
+
The full explanation, the four lifecycle states (Fresh / Continued /
|
|
637
664
|
Resumed / Closed), the three valid ways to carry context across a session
|
|
638
665
|
boundary, compaction & clearing guidance, and the context engineering
|
|
639
666
|
skill-map live in the **Copilot** section above under
|
|
@@ -642,32 +669,70 @@ available"](#critical-pitfall-session-lifecycle-controls-what-context-is-availab
|
|
|
642
669
|
Every principle there applies to OpenCode without modification — just
|
|
643
670
|
substitute the OpenCode API equivalents:
|
|
644
671
|
|
|
645
|
-
| Concept
|
|
646
|
-
|
|
647
|
-
| Fresh session (auto-created) | `s.session` (runtime creates via `createSession`)
|
|
648
|
-
| Send a turn
|
|
649
|
-
| Close / disconnect
|
|
650
|
-
| Continue prior conversation
|
|
651
|
-
| Extract final text
|
|
672
|
+
| Concept | Copilot API | OpenCode API |
|
|
673
|
+
| ---------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
674
|
+
| Fresh session (auto-created) | `s.session` (runtime creates via `createSession`) | `s.session` (runtime creates via `session.create`) |
|
|
675
|
+
| Send a turn | `s.session.send({ prompt })` | `s.client.session.prompt({ sessionID: s.session.id, parts })` |
|
|
676
|
+
| Close / disconnect | Auto-handled by runtime | session lifecycle managed via server; no explicit disconnect in typical flow |
|
|
677
|
+
| Continue prior conversation | `s.client.resumeSession(sessionId)` (provider API; advanced) | Reuse the same `sessionID` with `s.client.session.prompt()` inside the same logical conversation. `ctx.stage()` itself still creates a fresh session every time |
|
|
678
|
+
| Extract final text | `getAssistantText(messages)` (see `failure-modes.md` §F1) | `extractResponseText(result.data!.parts)` |
|
|
652
679
|
|
|
653
680
|
**Multi-agent handoff example (applies the same pattern as Copilot):**
|
|
654
681
|
|
|
655
682
|
```ts
|
|
656
|
-
// Buggy — orchestrator
|
|
657
|
-
// produced because
|
|
658
|
-
await
|
|
659
|
-
|
|
683
|
+
// Buggy — orchestrator stage is fresh; it has no idea what the planner
|
|
684
|
+
// produced because each ctx.stage() starts a brand-new session.
|
|
685
|
+
await ctx.stage({ name: "planner" }, {}, { title: "planner" }, async (s) => {
|
|
686
|
+
const result = await s.client.session.prompt({
|
|
687
|
+
sessionID: s.session.id,
|
|
688
|
+
parts: [{ type: "text", text: buildPlannerPrompt((s.inputs.prompt ?? "")) }],
|
|
689
|
+
agent: "planner",
|
|
690
|
+
});
|
|
691
|
+
s.save(result.data!);
|
|
692
|
+
});
|
|
693
|
+
await ctx.stage({ name: "orchestrator" }, {}, { title: "orchestrator" }, async (s) => {
|
|
694
|
+
await s.client.session.prompt({
|
|
695
|
+
sessionID: s.session.id,
|
|
696
|
+
parts: [{ type: "text", text: buildOrchestratorPrompt() }],
|
|
697
|
+
agent: "orchestrator",
|
|
698
|
+
});
|
|
699
|
+
s.save(/* ... */);
|
|
700
|
+
});
|
|
660
701
|
|
|
661
702
|
// Correct — capture planner output and forward it into orchestrator prompt
|
|
662
|
-
const
|
|
663
|
-
"planner
|
|
664
|
-
|
|
665
|
-
|
|
703
|
+
const plannerHandle = await ctx.stage(
|
|
704
|
+
{ name: "planner" },
|
|
705
|
+
{},
|
|
706
|
+
{ title: "planner" },
|
|
707
|
+
async (s) => {
|
|
708
|
+
const result = await s.client.session.prompt({
|
|
709
|
+
sessionID: s.session.id,
|
|
710
|
+
parts: [{ type: "text", text: buildPlannerPrompt((s.inputs.prompt ?? "")) }],
|
|
711
|
+
agent: "planner",
|
|
712
|
+
});
|
|
713
|
+
s.save(result.data!);
|
|
714
|
+
return extractResponseText(result.data!.parts); // see failure-modes.md §F3
|
|
715
|
+
},
|
|
666
716
|
);
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
"orchestrator",
|
|
670
|
-
|
|
717
|
+
|
|
718
|
+
await ctx.stage(
|
|
719
|
+
{ name: "orchestrator" },
|
|
720
|
+
{},
|
|
721
|
+
{ title: "orchestrator" },
|
|
722
|
+
async (s) => {
|
|
723
|
+
const result = await s.client.session.prompt({
|
|
724
|
+
sessionID: s.session.id,
|
|
725
|
+
parts: [{
|
|
726
|
+
type: "text",
|
|
727
|
+
text: buildOrchestratorPrompt(
|
|
728
|
+
(s.inputs.prompt ?? ""),
|
|
729
|
+
{ plannerNotes: plannerHandle.result },
|
|
730
|
+
),
|
|
731
|
+
}],
|
|
732
|
+
agent: "orchestrator",
|
|
733
|
+
});
|
|
734
|
+
s.save(result.data!);
|
|
735
|
+
},
|
|
671
736
|
);
|
|
672
737
|
```
|
|
673
738
|
|
|
@@ -751,20 +816,14 @@ const result = await s.client.session.prompt({
|
|
|
751
816
|
|
|
752
817
|
### Extracting response text
|
|
753
818
|
|
|
819
|
+
Non-text parts (`tool`, `file`, `reasoning`, …) coexist with `text` parts in
|
|
820
|
+
`result.data!.parts`; naive `.map(p => p.text)` emits `undefined` for them.
|
|
821
|
+
The canonical `extractResponseText` helper lives in `failure-modes.md` §F3 —
|
|
822
|
+
copy it into a sibling `helpers/parsers.ts` and import it. Usage:
|
|
823
|
+
|
|
754
824
|
```ts
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
* and computation-and-validation.md. */
|
|
758
|
-
function extractResponseText(
|
|
759
|
-
parts: Array<{ type: string; [key: string]: unknown }>,
|
|
760
|
-
): string {
|
|
761
|
-
return parts
|
|
762
|
-
.filter((p) => p.type === "text")
|
|
763
|
-
.map((p) => (p as { type: string; text: string }).text)
|
|
764
|
-
.join("\n");
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// Usage inside a ctx.stage callback:
|
|
825
|
+
import { extractResponseText } from "../helpers/parsers.ts";
|
|
826
|
+
|
|
768
827
|
const result = await s.client.session.prompt({
|
|
769
828
|
sessionID: s.session.id,
|
|
770
829
|
parts: [{ type: "text", text: (s.inputs.prompt ?? "") }],
|
|
@@ -783,9 +842,9 @@ const unsubscribe = await s.client.event.subscribe((event) => {
|
|
|
783
842
|
});
|
|
784
843
|
```
|
|
785
844
|
|
|
786
|
-
###
|
|
845
|
+
### Subagent delegation
|
|
787
846
|
|
|
788
|
-
Pass the `agent` parameter to `s.client.session.prompt()` to route a prompt to a named
|
|
847
|
+
Pass the `agent` parameter to `s.client.session.prompt()` to route a prompt to a named subagent:
|
|
789
848
|
|
|
790
849
|
```ts
|
|
791
850
|
.run(async (ctx) => {
|
|
@@ -4,12 +4,14 @@ Deterministic computation — validation, data transforms, file I/O, API calls
|
|
|
4
4
|
|
|
5
5
|
## Inline computation
|
|
6
6
|
|
|
7
|
-
Any TypeScript code inside a session callback that doesn't call an SDK prompt function is deterministic computation:
|
|
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
8
|
|
|
9
9
|
```ts
|
|
10
|
+
const plannerHandle = await ctx.stage({ name: "planner" }, {}, {}, async (s) => { /* ... */ });
|
|
11
|
+
|
|
10
12
|
await ctx.stage({ name: "validate-and-fix", description: "Validate, then fix if needed" }, {}, {}, async (s) => {
|
|
11
|
-
// Step 1: Deterministic — parse prior session's output
|
|
12
|
-
const messages = await s.getMessages(
|
|
13
|
+
// Step 1: Deterministic — parse prior session's output (handle-based lookup)
|
|
14
|
+
const messages = await s.getMessages(plannerHandle);
|
|
13
15
|
const planText = extractText(messages);
|
|
14
16
|
const plan = JSON.parse(planText);
|
|
15
17
|
|
|
@@ -72,39 +74,12 @@ const text = extractResponseText(result.data!.parts);
|
|
|
72
74
|
|
|
73
75
|
### JSON parsing with fallback
|
|
74
76
|
|
|
75
|
-
Use a layered fallback: direct parse
|
|
76
|
-
|
|
77
|
-
object
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// 1. Direct parse
|
|
82
|
-
try { return JSON.parse(text); } catch {}
|
|
83
|
-
|
|
84
|
-
// 2. LAST fenced code block (prose often quotes examples before the real output)
|
|
85
|
-
const blockRe = /```(?:json)?\s*\n([\s\S]*?)\n```/g;
|
|
86
|
-
let lastBlock: string | null = null;
|
|
87
|
-
let m: RegExpExecArray | null;
|
|
88
|
-
while ((m = blockRe.exec(text)) !== null) {
|
|
89
|
-
if (m[1]) lastBlock = m[1];
|
|
90
|
-
}
|
|
91
|
-
if (lastBlock) {
|
|
92
|
-
try { return JSON.parse(lastBlock); } catch {}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 3. Last balanced JSON object
|
|
96
|
-
const objRe = /\{[\s\S]*\}/g;
|
|
97
|
-
let lastObj: string | null = null;
|
|
98
|
-
while ((m = objRe.exec(text)) !== null) {
|
|
99
|
-
if (m[0]) lastObj = m[0];
|
|
100
|
-
}
|
|
101
|
-
if (lastObj) {
|
|
102
|
-
try { return JSON.parse(lastObj); } catch {}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
```
|
|
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`.
|
|
108
83
|
|
|
109
84
|
### Zod validation
|
|
110
85
|
|
|
@@ -171,7 +146,7 @@ Transform data between sessions:
|
|
|
171
146
|
```ts
|
|
172
147
|
// Inside a ctx.stage() callback:
|
|
173
148
|
async (s) => {
|
|
174
|
-
const raw = await s.getMessages("planner");
|
|
149
|
+
const raw = await s.getMessages("planner"); // or s.getMessages(handle) if a handle is in scope (preferred)
|
|
175
150
|
|
|
176
151
|
// Transform: extract only task IDs and descriptions
|
|
177
152
|
const tasks = extractTasks(raw).map(t => ({
|