@bastani/atomic 0.6.4 → 0.6.5
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/create-spec/SKILL.md +6 -3
- package/.agents/skills/tdd/SKILL.md +107 -0
- package/.agents/skills/tdd/deep-modules.md +33 -0
- package/.agents/skills/tdd/interface-design.md +31 -0
- package/.agents/skills/tdd/mocking.md +59 -0
- package/.agents/skills/tdd/refactoring.md +10 -0
- package/.agents/skills/tdd/tests.md +61 -0
- package/.agents/skills/workflow-creator/SKILL.md +550 -0
- package/.agents/skills/workflow-creator/references/agent-sessions.md +891 -0
- package/.agents/skills/workflow-creator/references/agent-setup-recipe.md +266 -0
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +201 -0
- package/.agents/skills/workflow-creator/references/control-flow.md +470 -0
- package/.agents/skills/workflow-creator/references/failure-modes.md +1014 -0
- package/.agents/skills/workflow-creator/references/getting-started.md +392 -0
- package/.agents/skills/workflow-creator/references/registry-and-validation.md +141 -0
- package/.agents/skills/workflow-creator/references/running-workflows.md +418 -0
- package/.agents/skills/workflow-creator/references/session-config.md +384 -0
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +356 -0
- package/.agents/skills/workflow-creator/references/user-input.md +234 -0
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +392 -0
- package/.claude/agents/debugger.md +2 -2
- package/.claude/agents/reviewer.md +1 -1
- package/.claude/agents/worker.md +2 -2
- package/.github/agents/debugger.md +1 -1
- package/.github/agents/worker.md +1 -1
- package/.mcp.json +5 -1
- package/.opencode/agents/debugger.md +1 -1
- package/.opencode/agents/worker.md +1 -1
- package/README.md +236 -201
- package/dist/sdk/define-workflow.d.ts +11 -6
- package/dist/sdk/define-workflow.d.ts.map +1 -1
- package/dist/sdk/errors.d.ts +10 -0
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +21 -9
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/primitives/inputs.d.ts +36 -0
- package/dist/sdk/primitives/inputs.d.ts.map +1 -0
- package/dist/sdk/primitives/metadata.d.ts +40 -0
- package/dist/sdk/primitives/metadata.d.ts.map +1 -0
- package/dist/sdk/primitives/run.d.ts +57 -0
- package/dist/sdk/primitives/run.d.ts.map +1 -0
- package/dist/sdk/primitives/sessions.d.ts +128 -0
- package/dist/sdk/primitives/sessions.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +24 -56
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/orchestrator-entry.d.ts +26 -0
- package/dist/sdk/runtime/orchestrator-entry.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +20 -0
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +26 -86
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/index.d.ts +20 -12
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/dist/services/config/additional-instructions.d.ts +1 -1
- package/dist/services/config/additional-instructions.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/cli.ts +39 -56
- package/src/commands/builtin-registry.ts +37 -0
- package/src/commands/cli/chat/index.ts +1 -3
- package/src/{sdk → commands/cli}/management-commands.ts +15 -55
- package/src/commands/cli/session.ts +1 -1
- package/src/commands/cli/workflow-command.test.ts +250 -16
- package/src/commands/cli/workflow-inputs.test.ts +1 -0
- package/src/commands/cli/workflow-inputs.ts +13 -3
- package/src/commands/cli/workflow-list.test.ts +1 -0
- package/src/commands/cli/workflow-list.ts +0 -0
- package/src/commands/cli/workflow-status.ts +1 -1
- package/src/commands/cli/workflow.ts +191 -11
- package/src/sdk/define-workflow.test.ts +47 -16
- package/src/sdk/define-workflow.ts +24 -6
- package/src/sdk/errors.test.ts +11 -0
- package/src/sdk/errors.ts +13 -0
- package/src/sdk/index.test.ts +92 -0
- package/src/sdk/index.ts +71 -15
- package/src/sdk/primitives/inputs.ts +48 -0
- package/src/sdk/primitives/metadata.ts +63 -0
- package/src/sdk/primitives/run.ts +81 -0
- package/src/sdk/primitives/sessions.test.ts +594 -0
- package/src/sdk/primitives/sessions.ts +328 -0
- package/src/sdk/runtime/executor.ts +36 -115
- package/src/sdk/runtime/orchestrator-entry.ts +110 -0
- package/src/sdk/runtime/tmux.ts +33 -0
- package/src/sdk/types.ts +26 -91
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -0
- package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -0
- package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -0
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -0
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -0
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -0
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -0
- package/src/sdk/workflows/index.ts +68 -51
- package/src/services/config/additional-instructions.ts +1 -1
- package/.agents/skills/test-driven-development/SKILL.md +0 -371
- package/.agents/skills/test-driven-development/testing-anti-patterns.md +0 -299
- package/dist/commands/cli/session.d.ts +0 -67
- package/dist/commands/cli/session.d.ts.map +0 -1
- package/dist/commands/cli/workflow-status.d.ts +0 -63
- package/dist/commands/cli/workflow-status.d.ts.map +0 -1
- package/dist/sdk/commander.d.ts +0 -74
- package/dist/sdk/commander.d.ts.map +0 -1
- package/dist/sdk/management-commands.d.ts +0 -42
- package/dist/sdk/management-commands.d.ts.map +0 -1
- package/dist/sdk/workflow-cli.d.ts +0 -103
- package/dist/sdk/workflow-cli.d.ts.map +0 -1
- package/dist/sdk/workflows/builtin-registry.d.ts +0 -113
- package/dist/sdk/workflows/builtin-registry.d.ts.map +0 -1
- package/src/sdk/commander.ts +0 -161
- package/src/sdk/workflow-cli.ts +0 -409
- package/src/sdk/workflows/builtin-registry.ts +0 -23
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# State and Data Flow
|
|
2
|
+
|
|
3
|
+
Data flows between sessions via `s.save()` (in the producing session) and `transcript()` / `getMessages()` (in consuming sessions or at the `.run()` level). Within a session, use plain TypeScript variables. This is the programmatic equivalent of `globalState` and reducers.
|
|
4
|
+
|
|
5
|
+
## Between sessions: `s.save()` → `transcript()` / `getMessages()`
|
|
6
|
+
|
|
7
|
+
**Completion rule:** `transcript()` and `getMessages()` can only access data from sessions whose callbacks have already returned (i.e., sessions in the `completedRegistry`). In a `Promise.all()` group, sibling sessions cannot read each other's output — only sessions that completed before the group started are available.
|
|
8
|
+
|
|
9
|
+
### Saving output
|
|
10
|
+
|
|
11
|
+
Each SDK has its own save pattern:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// Claude — pass session ID (auto-reads transcript)
|
|
15
|
+
s.save(s.sessionId);
|
|
16
|
+
|
|
17
|
+
// Copilot — pass SessionEvent[] from getMessages()
|
|
18
|
+
s.save(await s.session.getMessages());
|
|
19
|
+
|
|
20
|
+
// OpenCode — pass response { info, parts } from session.prompt()
|
|
21
|
+
s.save(result.data!);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Retrieving as rendered text
|
|
25
|
+
|
|
26
|
+
`s.transcript(handle)` returns `{ path: string, content: string }`:
|
|
27
|
+
- `path` — absolute file path to the transcript on disk
|
|
28
|
+
- `content` — extracted assistant text, ready to embed in prompts
|
|
29
|
+
|
|
30
|
+
Pass the session handle returned by a prior `ctx.stage()` call (handle-based, recommended). The string name `s.transcript("name")` also works when no handle is in scope.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
.run(async (ctx) => {
|
|
34
|
+
const researchHandle = await ctx.stage({ name: "research" }, {}, {}, async (s) => {
|
|
35
|
+
await s.session.query("Research the topic.");
|
|
36
|
+
s.save(s.sessionId);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await ctx.stage({ name: "synthesize" }, {}, {}, async (s) => {
|
|
40
|
+
const research = await s.transcript(researchHandle);
|
|
41
|
+
|
|
42
|
+
// Use rendered text in a prompt
|
|
43
|
+
await s.session.query(`Synthesize this research:\n${research.content}`);
|
|
44
|
+
|
|
45
|
+
// Or reference the file path (useful for Claude file triggers)
|
|
46
|
+
await s.session.query(`Read ${research.path} and summarize the key findings.`);
|
|
47
|
+
|
|
48
|
+
s.save(s.sessionId);
|
|
49
|
+
});
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Retrieving as raw messages
|
|
54
|
+
|
|
55
|
+
`s.getMessages(handle)` returns `SavedMessage[]` — the native SDK messages exactly as stored:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
.run(async (ctx) => {
|
|
59
|
+
const researchHandle = await ctx.stage({ name: "research" }, {}, {}, async (s) => {
|
|
60
|
+
// ... research work ...
|
|
61
|
+
s.save(s.sessionId);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await ctx.stage({ name: "analyze-results" }, {}, {}, async (s) => {
|
|
65
|
+
const messages = await s.getMessages(researchHandle);
|
|
66
|
+
|
|
67
|
+
// messages is SavedMessage[], where each entry is:
|
|
68
|
+
// { provider: "copilot", data: SessionEvent }
|
|
69
|
+
// { provider: "opencode", data: SessionPromptResponse }
|
|
70
|
+
// { provider: "claude", data: SessionMessage }
|
|
71
|
+
|
|
72
|
+
// Process raw messages for detailed analysis
|
|
73
|
+
for (const msg of messages) {
|
|
74
|
+
if (msg.provider === "copilot") {
|
|
75
|
+
// Access Copilot-specific fields
|
|
76
|
+
const event = msg.data;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Returning values from session callbacks
|
|
84
|
+
|
|
85
|
+
Session callbacks can return a value directly. The handle exposes it via `.result`:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
.run(async (ctx) => {
|
|
89
|
+
const planHandle = await ctx.stage({ name: "plan" }, {}, {}, async (s) => {
|
|
90
|
+
// ... planning work ...
|
|
91
|
+
return { taskCount: 5, priority: "high" };
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Access the returned value on the handle
|
|
95
|
+
console.log(planHandle.result.taskCount); // 5
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Within a session: TypeScript variables
|
|
100
|
+
|
|
101
|
+
Use closures and variables for state within a single session:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
.run(async (ctx) => {
|
|
105
|
+
await ctx.stage({ name: "review-fix" }, {}, {}, async (s) => {
|
|
106
|
+
// Local state — plain variables
|
|
107
|
+
let consecutiveClean = 0;
|
|
108
|
+
let priorOutput = "";
|
|
109
|
+
const findings: string[] = [];
|
|
110
|
+
|
|
111
|
+
for (let cycle = 0; cycle < 10; cycle++) {
|
|
112
|
+
const result = await s.session.query(
|
|
113
|
+
buildReviewPrompt((s.inputs.prompt ?? ""), priorOutput),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Accumulate findings
|
|
117
|
+
const review = parseReviewResult(extractAssistantText(result, 0));
|
|
118
|
+
if (review) {
|
|
119
|
+
findings.push(...review.findings.map(f => f.title));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Track clean streak
|
|
123
|
+
if (!hasActionableFindings(review, extractAssistantText(result, 0))) {
|
|
124
|
+
consecutiveClean++;
|
|
125
|
+
if (consecutiveClean >= 2) break;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
consecutiveClean = 0;
|
|
129
|
+
|
|
130
|
+
// Apply fix
|
|
131
|
+
const fixResult = await s.session.query(buildFixSpec(review, (s.inputs.prompt ?? "")));
|
|
132
|
+
priorOutput = extractAssistantText(fixResult, 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// All local state is available here
|
|
136
|
+
console.log(`Total findings across cycles: ${findings.length}`);
|
|
137
|
+
s.save(s.sessionId);
|
|
138
|
+
});
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## File-based persistence
|
|
143
|
+
|
|
144
|
+
For data that needs to survive session restarts or be accessible outside the workflow, use file I/O:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
148
|
+
import { join } from "path";
|
|
149
|
+
|
|
150
|
+
.run(async (ctx) => {
|
|
151
|
+
const planHandle = await ctx.stage({ name: "plan" }, {}, {}, async (s) => {
|
|
152
|
+
// Write artifacts to session directory
|
|
153
|
+
const artifactDir = join(s.sessionDir, "artifacts");
|
|
154
|
+
await mkdir(artifactDir, { recursive: true });
|
|
155
|
+
|
|
156
|
+
const report = { timestamp: Date.now(), status: "complete" };
|
|
157
|
+
await writeFile(
|
|
158
|
+
join(artifactDir, "report.json"),
|
|
159
|
+
JSON.stringify(report, null, 2),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
s.save(s.sessionId);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await ctx.stage({ name: "generate-report" }, {}, {}, async (s) => {
|
|
166
|
+
// Read prior session's output via transcript (preferred over path traversal)
|
|
167
|
+
const planTranscript = await s.transcript(planHandle);
|
|
168
|
+
|
|
169
|
+
// Or read artifacts using the transcript's path to locate the session directory
|
|
170
|
+
const planSessionDir = join(planTranscript.path, "..");
|
|
171
|
+
const priorReport = JSON.parse(
|
|
172
|
+
await readFile(join(planSessionDir, "artifacts", "report.json"), "utf-8"),
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Shared helper functions
|
|
179
|
+
|
|
180
|
+
Extract SDK-agnostic logic into shared helpers. This is the key pattern for building workflows that work across all three SDKs:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
src/workflows/my-workflow/
|
|
184
|
+
├── claude.ts # Claude SDK code — exports WorkflowDefinition
|
|
185
|
+
├── copilot.ts # Copilot SDK code — exports WorkflowDefinition
|
|
186
|
+
├── opencode.ts # OpenCode SDK code — exports WorkflowDefinition
|
|
187
|
+
└── helpers/
|
|
188
|
+
├── prompts.ts # Prompt builders
|
|
189
|
+
├── parsers.ts # Response parsers
|
|
190
|
+
└── validation.ts # Validation logic
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Prompt builders
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// src/workflows/my-workflow/helpers/prompts.ts
|
|
197
|
+
export function buildPlanPrompt(spec: string): string {
|
|
198
|
+
return `Decompose into tasks:\n${spec}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function buildReviewPrompt(spec: string, priorOutput?: string): string {
|
|
202
|
+
let prompt = `Review the implementation against:\n${spec}`;
|
|
203
|
+
if (priorOutput) {
|
|
204
|
+
prompt += `\n\nPrior fixes:\n${priorOutput}`;
|
|
205
|
+
}
|
|
206
|
+
return prompt;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Response parsers
|
|
211
|
+
|
|
212
|
+
For tolerant JSON parsing, see `failure-modes.md` §F8 — the canonical
|
|
213
|
+
`parseReviewResult` helper uses a layered fallback (direct parse → last
|
|
214
|
+
fenced block → last balanced object) that survives prose interleaving.
|
|
215
|
+
Copy that implementation into `helpers/parsers.ts` and import.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// src/workflows/my-workflow/helpers/parsers.ts
|
|
219
|
+
export interface ReviewResult {
|
|
220
|
+
findings: Array<{ title: string; body: string; priority: number }>;
|
|
221
|
+
overall_correctness: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// See failure-modes.md §F8 for the full implementation.
|
|
225
|
+
export function parseReviewResult(text: string): ReviewResult | null {
|
|
226
|
+
// ... three-layer fallback per §F8
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Usage in workflows
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// src/workflows/my-workflow/claude.ts
|
|
234
|
+
import { buildPlanPrompt, buildReviewPrompt } from "./helpers/prompts.ts";
|
|
235
|
+
import { parseReviewResult } from "./helpers/parsers.ts";
|
|
236
|
+
|
|
237
|
+
// ... use in run() callbacks
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Data flow patterns
|
|
241
|
+
|
|
242
|
+
### Linear pipeline
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
Session A → s.save() → Session B reads via s.transcript(handleA)
|
|
246
|
+
→ s.save() → Session C reads via s.transcript(handleB)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Fan-in (multiple prior sessions)
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
.run(async (ctx) => {
|
|
253
|
+
const researchHandle = await ctx.stage({ name: "research" }, {}, {}, async (s) => {
|
|
254
|
+
// ... research work ...
|
|
255
|
+
s.save(s.sessionId);
|
|
256
|
+
});
|
|
257
|
+
const analysisHandle = await ctx.stage({ name: "analysis" }, {}, {}, async (s) => {
|
|
258
|
+
// ... analysis work ...
|
|
259
|
+
s.save(s.sessionId);
|
|
260
|
+
});
|
|
261
|
+
const feedbackHandle = await ctx.stage({ name: "feedback" }, {}, {}, async (s) => {
|
|
262
|
+
// ... feedback work ...
|
|
263
|
+
s.save(s.sessionId);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await ctx.stage({ name: "merge" }, {}, {}, async (s) => {
|
|
267
|
+
const research = await s.transcript(researchHandle);
|
|
268
|
+
const analysis = await s.transcript(analysisHandle);
|
|
269
|
+
const userFeedback = await s.transcript(feedbackHandle);
|
|
270
|
+
|
|
271
|
+
await s.session.query(`Combine these inputs:
|
|
272
|
+
Research: ${research.content}
|
|
273
|
+
Analysis: ${analysis.content}
|
|
274
|
+
Feedback: ${userFeedback.content}`);
|
|
275
|
+
s.save(s.sessionId);
|
|
276
|
+
});
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Accumulating state across sessions
|
|
281
|
+
|
|
282
|
+
Each session can read all prior completed steps (but not parallel siblings):
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
.run(async (ctx) => {
|
|
286
|
+
const h1 = await ctx.stage({ name: "session-1" }, {}, {}, async (s) => {
|
|
287
|
+
// ...
|
|
288
|
+
s.save(s.sessionId);
|
|
289
|
+
});
|
|
290
|
+
const h2 = await ctx.stage({ name: "session-2" }, {}, {}, async (s) => {
|
|
291
|
+
// ...
|
|
292
|
+
s.save(s.sessionId);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await ctx.stage({ name: "session-3" }, {}, {}, async (s) => {
|
|
296
|
+
// Read from any prior completed session via its handle
|
|
297
|
+
const s1 = await s.transcript(h1);
|
|
298
|
+
const s2 = await s.transcript(h2);
|
|
299
|
+
|
|
300
|
+
// Combine and process
|
|
301
|
+
const combined = `${s1.content}\n${s2.content}`;
|
|
302
|
+
// ...
|
|
303
|
+
});
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Context-Aware Transcript Handoff
|
|
308
|
+
|
|
309
|
+
When passing transcripts between sessions, compress at the boundary to prevent downstream context degradation. Use structured summaries that preserve actionable information while dropping verbose tool output (applies `context-compression` + `context-degradation`):
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
// helpers/compression.ts
|
|
313
|
+
export function compressTranscript(content: string, maxTokenEstimate: number = 4000): string {
|
|
314
|
+
// Rough estimate: ~4 chars/token for English prose, ~2-3 for code.
|
|
315
|
+
// For precise budgeting, use the provider's tokenizer instead.
|
|
316
|
+
const maxChars = maxTokenEstimate * 4;
|
|
317
|
+
if (content.length <= maxChars) return content;
|
|
318
|
+
|
|
319
|
+
// Preserve first and last sections (recency + primacy bias)
|
|
320
|
+
const headSize = Math.floor(maxChars * 0.4);
|
|
321
|
+
const tailSize = Math.floor(maxChars * 0.4);
|
|
322
|
+
const head = content.slice(0, headSize);
|
|
323
|
+
const tail = content.slice(-tailSize);
|
|
324
|
+
|
|
325
|
+
return `${head}\n\n[... ${content.length - headSize - tailSize} chars compressed ...]\n\n${tail}`;
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
await ctx.stage({ name: "synthesize" }, {}, {}, async (s) => {
|
|
331
|
+
const research = await s.transcript("research");
|
|
332
|
+
// Compress before injecting into prompt to stay within token budget
|
|
333
|
+
const compressed = compressTranscript(research.content, 4000);
|
|
334
|
+
await s.session.query(`Synthesize this research:\n${compressed}`);
|
|
335
|
+
s.save(s.sessionId);
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## File-Based Coordination
|
|
340
|
+
|
|
341
|
+
Use the filesystem as a coordination layer instead of inlining large data into prompts. This applies `filesystem-context`:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
.run(async (ctx) => {
|
|
345
|
+
await ctx.stage({ name: "plan" }, {}, {}, async (s) => {
|
|
346
|
+
await s.session.query(`Create a plan for: ${(s.inputs.prompt ?? "")}\n\nWrite it to plan.md.`);
|
|
347
|
+
s.save(s.sessionId);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await ctx.stage({ name: "execute" }, {}, {}, async (s) => {
|
|
351
|
+
// Reference the file by path — lets the agent read selectively
|
|
352
|
+
await s.session.query(`Read plan.md and implement each task. Mark tasks done as you go.`);
|
|
353
|
+
s.save(s.sessionId);
|
|
354
|
+
});
|
|
355
|
+
})
|
|
356
|
+
```
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# User Input (mid-workflow)
|
|
2
|
+
|
|
3
|
+
This reference covers **mid-workflow** user interaction — pausing a running stage to ask the user a question, approve a permission, or confirm a decision. It's the programmatic equivalent of `.askUserQuestion()`.
|
|
4
|
+
|
|
5
|
+
For **invocation-time** inputs (the values the user supplies when they launch the workflow from the CLI or the picker), see `workflow-inputs.md` instead. Invocation-time inputs are declared on `defineWorkflow({ inputs: [...] })` and arrive in `ctx.inputs` before any stage starts — the workflow author reads them via `ctx.inputs.<name>`.
|
|
6
|
+
|
|
7
|
+
## Claude
|
|
8
|
+
|
|
9
|
+
Never import `query` from `@anthropic-ai/claude-agent-sdk` inside a stage
|
|
10
|
+
callback — that's the F16 anti-pattern (see `failure-modes.md` §F16). All
|
|
11
|
+
options route through `s.session.query(prompt, sdkOptions)` in headless
|
|
12
|
+
stages, or through `chatFlags` in interactive stages.
|
|
13
|
+
|
|
14
|
+
### Via `canUseTool` callback (headless stages only)
|
|
15
|
+
|
|
16
|
+
`canUseTool` is an SDK option — it only applies in a headless stage, where
|
|
17
|
+
the second argument to `s.session.query()` is forwarded to the Agent SDK as
|
|
18
|
+
`Partial<SDKOptions>`. In interactive stages the option is silently ignored
|
|
19
|
+
because `s.session.query()` is driving the `claude` CLI binary, not the SDK.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
await ctx.stage(
|
|
23
|
+
{ name: "implement", headless: true },
|
|
24
|
+
{}, {},
|
|
25
|
+
async (s) => {
|
|
26
|
+
const messages = await s.session.query(
|
|
27
|
+
"Implement the feature, but ask me before making any database changes.",
|
|
28
|
+
{
|
|
29
|
+
canUseTool: async (toolName, toolInput) => {
|
|
30
|
+
if (toolName === "Write" && typeof toolInput.file_path === "string" && toolInput.file_path.includes("migration")) {
|
|
31
|
+
const approved = await promptUser("Allow database migration?");
|
|
32
|
+
return approved
|
|
33
|
+
? { behavior: "allow", updatedInput: toolInput }
|
|
34
|
+
: { behavior: "deny", message: "User declined migration" };
|
|
35
|
+
}
|
|
36
|
+
return { behavior: "allow", updatedInput: toolInput };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
s.save(s.sessionId);
|
|
41
|
+
return extractAssistantText(messages, 0);
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Via `AskUserQuestion` tool
|
|
47
|
+
|
|
48
|
+
Allow the agent to ask the user questions by including `AskUserQuestion` in
|
|
49
|
+
`allowedTools`. This works for both interactive stages (via `chatFlags`) and
|
|
50
|
+
headless stages (via sdkOptions on `s.session.query()`).
|
|
51
|
+
|
|
52
|
+
**Interactive stage** — pass the tool allowlist via `chatFlags`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
await ctx.stage(
|
|
56
|
+
{ name: "implement" },
|
|
57
|
+
{ chatFlags: ["--allowed-tools", "Read,Write,Edit,Bash,AskUserQuestion"] },
|
|
58
|
+
{},
|
|
59
|
+
async (s) => {
|
|
60
|
+
await s.session.query(s.inputs.prompt ?? "");
|
|
61
|
+
s.save(s.sessionId);
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Headless stage** — pass `allowedTools` in the sdkOptions:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
await ctx.stage(
|
|
70
|
+
{ name: "implement", headless: true },
|
|
71
|
+
{}, {},
|
|
72
|
+
async (s) => {
|
|
73
|
+
const messages = await s.session.query(s.inputs.prompt ?? "", {
|
|
74
|
+
allowedTools: ["Read", "Write", "Edit", "Bash", "AskUserQuestion"],
|
|
75
|
+
});
|
|
76
|
+
s.save(s.sessionId);
|
|
77
|
+
return extractAssistantText(messages, 0);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Via streaming input (headless stages only)
|
|
83
|
+
|
|
84
|
+
The Agent SDK's `streamInput()` feeds additional input while a query is
|
|
85
|
+
running. It's only reachable from headless stages via an async iterable
|
|
86
|
+
prompt — pass an `AsyncIterable<SDKUserMessage>` as the first argument to
|
|
87
|
+
`s.session.query()` instead of a plain string. In interactive stages, send
|
|
88
|
+
follow-up turns with another `s.session.query()` call to the same session.
|
|
89
|
+
|
|
90
|
+
## Copilot
|
|
91
|
+
|
|
92
|
+
Session callbacks (`onUserInputRequest`, `onElicitationRequest`,
|
|
93
|
+
`onPermissionRequest`) are passed as `sessionOpts` — the third argument to
|
|
94
|
+
`ctx.stage()`. The runtime forwards them to `client.createSession()`.
|
|
95
|
+
`onPermissionRequest` defaults to `approveAll` when not specified.
|
|
96
|
+
|
|
97
|
+
### Via `onUserInputRequest`
|
|
98
|
+
|
|
99
|
+
Handle `ask_user` tool requests from the agent:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await ctx.stage({ name: "plan" }, {}, {
|
|
103
|
+
onUserInputRequest: async (request) => {
|
|
104
|
+
// request.question contains the agent's question
|
|
105
|
+
const answer = await promptUser(request.question);
|
|
106
|
+
return answer;
|
|
107
|
+
},
|
|
108
|
+
}, async (s) => {
|
|
109
|
+
await s.session.send({ prompt: (s.inputs.prompt ?? "") });
|
|
110
|
+
s.save(await s.session.getMessages());
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Via `onElicitationRequest`
|
|
115
|
+
|
|
116
|
+
For form-style UI with structured options:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
await ctx.stage({ name: "plan" }, {}, {
|
|
120
|
+
onPermissionRequest: approveAll,
|
|
121
|
+
onElicitationRequest: async (request) => {
|
|
122
|
+
// request contains form fields and options
|
|
123
|
+
return {
|
|
124
|
+
action: "submit",
|
|
125
|
+
values: { strategy: "conservative", confirm: true },
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
}, async (s) => {
|
|
129
|
+
await s.session.send({ prompt: (s.inputs.prompt ?? "") });
|
|
130
|
+
s.save(await s.session.getMessages());
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Programmatic approval
|
|
135
|
+
|
|
136
|
+
For fully autonomous workflows, use `approveAll` to skip all permission prompts.
|
|
137
|
+
This is the **default** when `onPermissionRequest` is not specified in `sessionOpts`:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { approveAll } from "@github/copilot-sdk";
|
|
141
|
+
|
|
142
|
+
// Explicit (same as the default):
|
|
143
|
+
await ctx.stage({ name: "plan" }, {}, { onPermissionRequest: approveAll }, async (s) => {
|
|
144
|
+
await s.session.send({ prompt: (s.inputs.prompt ?? "") });
|
|
145
|
+
s.save(await s.session.getMessages());
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Custom permission handling
|
|
150
|
+
|
|
151
|
+
For fine-grained control over permissions:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await ctx.stage({ name: "plan" }, {}, {
|
|
155
|
+
onPermissionRequest: async (request) => {
|
|
156
|
+
// request.kind: "shell" | "write" | "read" | "mcp" | "custom-tool" | "url" | "memory" | "hook"
|
|
157
|
+
if (request.kind === "shell" && request.command?.includes("rm -rf")) {
|
|
158
|
+
return { kind: "denied-permanently", reason: "Dangerous command" };
|
|
159
|
+
}
|
|
160
|
+
return { kind: "approved" };
|
|
161
|
+
},
|
|
162
|
+
}, async (s) => {
|
|
163
|
+
await s.session.send({ prompt: (s.inputs.prompt ?? "") });
|
|
164
|
+
s.save(await s.session.getMessages());
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## OpenCode
|
|
169
|
+
|
|
170
|
+
The `s.client` and `s.session` are auto-created by the runtime. Use them
|
|
171
|
+
directly — no manual client creation needed.
|
|
172
|
+
|
|
173
|
+
### Via TUI control endpoints
|
|
174
|
+
|
|
175
|
+
OpenCode uses TUI control endpoints for user interaction:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// Inside a ctx.stage() callback:
|
|
179
|
+
async (s) => {
|
|
180
|
+
// Wait for the next TUI control request
|
|
181
|
+
const controlRequest = await s.client.tui.next();
|
|
182
|
+
|
|
183
|
+
// Respond to the control request
|
|
184
|
+
await s.client.tui.response({
|
|
185
|
+
requestID: controlRequest.data!.id,
|
|
186
|
+
response: "User's answer here",
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Via permission handling
|
|
192
|
+
|
|
193
|
+
Handle permission requests programmatically:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// Inside a ctx.stage() callback:
|
|
197
|
+
async (s) => {
|
|
198
|
+
// Subscribe to events and handle permission requests
|
|
199
|
+
const unsubscribe = await s.client.event.subscribe((event) => {
|
|
200
|
+
if (event.type === "permission.requested") {
|
|
201
|
+
s.client.session.permission({
|
|
202
|
+
sessionID: event.sessionID,
|
|
203
|
+
permissionID: event.permissionID,
|
|
204
|
+
approved: true,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Combining user input with control flow
|
|
212
|
+
|
|
213
|
+
Use user input results in conditional logic. This Claude example uses
|
|
214
|
+
`AskUserQuestion` by including it in `allowedTools` — the agent asks the
|
|
215
|
+
user directly, and you parse the response to branch:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// Inside a ctx.stage() callback (Claude example).
|
|
219
|
+
// AskUserQuestion must be in allowedTools — see "Via AskUserQuestion tool" above.
|
|
220
|
+
async (s) => {
|
|
221
|
+
const plan = await s.transcript("plan"); // or s.transcript(handle) if a handle is in scope (preferred)
|
|
222
|
+
|
|
223
|
+
// Let the agent ask the user for approval via AskUserQuestion
|
|
224
|
+
const result = await s.session.query(
|
|
225
|
+
`Here is the plan:\n${plan.content}\n\nAsk the user if they approve this plan. ` +
|
|
226
|
+
`If they approve, execute it. If not, revise it based on their feedback.`,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
s.save(s.sessionId);
|
|
230
|
+
},
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
For Copilot, use `onUserInputRequest` in `sessionOpts` (see above). For
|
|
234
|
+
OpenCode, use the TUI control endpoints.
|