@ax-llm/ax 19.0.15 → 19.0.17
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/index.cjs +462 -313
- package/index.cjs.map +1 -1
- package/index.d.cts +789 -572
- package/index.d.ts +789 -572
- package/index.global.js +409 -260
- package/index.global.js.map +1 -1
- package/index.js +462 -313
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/ax-agent.md +225 -56
- package/skills/ax-ai.md +245 -0
- package/skills/ax-flow.md +402 -0
- package/skills/ax-gen.md +323 -0
- package/skills/ax-gepa.md +244 -0
- package/skills/ax-learn.md +268 -0
- package/skills/ax-llm.md +150 -1675
- package/skills/ax-signature.md +192 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ax-flow
|
|
3
|
+
description: This skill helps an LLM generate correct AxFlow workflow code using @ax-llm/ax. Use when the user asks about flow(), AxFlow, workflow orchestration, parallel execution, DAG workflows, conditional routing, map/reduce patterns, or multi-node AI pipelines.
|
|
4
|
+
version: "19.0.17"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# AxFlow Codegen Rules (@ax-llm/ax)
|
|
8
|
+
|
|
9
|
+
Use this skill to generate `AxFlow` workflow code. Prefer short, modern, copyable patterns. Do not write tutorial prose unless the user explicitly asks for explanation.
|
|
10
|
+
|
|
11
|
+
## Use These Defaults
|
|
12
|
+
|
|
13
|
+
- Use `flow()` factory, not `new AxFlow()`.
|
|
14
|
+
- Import: `import { ai, flow, f } from '@ax-llm/ax';`
|
|
15
|
+
- `autoParallel: true` is the default; independent executes run in parallel automatically.
|
|
16
|
+
- Node results are stored as `${nodeName}Result` in state.
|
|
17
|
+
- Always define `.node()` before `.execute()` for that node.
|
|
18
|
+
- Use `.returns()` (or `.r()`) as the last step to lock the output type.
|
|
19
|
+
- Use descriptive node names: `documentSummarizer`, not `proc1`.
|
|
20
|
+
- Use descriptive field names: `userInput`, `responseText`, not `text`, `result`.
|
|
21
|
+
|
|
22
|
+
## Critical Rules
|
|
23
|
+
|
|
24
|
+
- Use `flow()` factory syntax for new code.
|
|
25
|
+
- Node results in state follow the pattern `state.${nodeName}Result.${fieldName}`.
|
|
26
|
+
- `.execute()` maps current state to node inputs; `.map()` transforms state without AI calls.
|
|
27
|
+
- `.returns()` maps final state to the flow output type.
|
|
28
|
+
- Always define nodes before executing them; reversed order throws at runtime.
|
|
29
|
+
- Keep state flat; avoid deep nesting in `.map()`.
|
|
30
|
+
- Ensure loop conditions can change to avoid infinite loops.
|
|
31
|
+
- Structure independent executes to maximize auto-parallelization.
|
|
32
|
+
- Use `flow<InputType, OutputType>()` for typed flows.
|
|
33
|
+
- Aliases: `.n()` = `.node()`, `.nx()` = `.nodeExtended()`, `.m()` = `.map()`, `.r()` = `.returns()`.
|
|
34
|
+
|
|
35
|
+
## Canonical Pattern
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { ai, flow } from '@ax-llm/ax';
|
|
39
|
+
|
|
40
|
+
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
|
|
41
|
+
|
|
42
|
+
const wf = flow<{ userInput: string }, { responseText: string }>()
|
|
43
|
+
.node('testNode', 'userInput:string -> responseText:string')
|
|
44
|
+
.execute('testNode', (state) => ({ userInput: state.userInput }))
|
|
45
|
+
.returns((state) => ({ responseText: state.testNodeResult.responseText }));
|
|
46
|
+
|
|
47
|
+
const result = await wf.forward(llm, { userInput: 'Hello world' });
|
|
48
|
+
console.log(result.responseText);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Factory Options
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// Basic
|
|
55
|
+
const wf = flow();
|
|
56
|
+
|
|
57
|
+
// With options
|
|
58
|
+
const wf = flow({ autoParallel: false });
|
|
59
|
+
|
|
60
|
+
// Typed
|
|
61
|
+
const wf = flow<InputType, OutputType>();
|
|
62
|
+
|
|
63
|
+
// Typed with options
|
|
64
|
+
const wf = flow<InputType, OutputType>({ autoParallel: true, batchSize: 5 });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## State Evolution
|
|
68
|
+
|
|
69
|
+
State grows with each executed node. Results are stored as `${nodeName}Result`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Initial state: { userInput: 'Hello' }
|
|
73
|
+
flow.execute('processor', (state) => ({ input: state.userInput }));
|
|
74
|
+
// State: { userInput: 'Hello', processorResult: { output: '...' } }
|
|
75
|
+
|
|
76
|
+
flow.execute('analyzer', (state) => ({ text: state.processorResult.output }));
|
|
77
|
+
// State: { ..., analyzerResult: { sentiment: '...', confidence: 0.8 } }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Node Definition
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// String signature (creates AxGen automatically)
|
|
84
|
+
flow.node('processor', 'input:string -> output:string');
|
|
85
|
+
|
|
86
|
+
// Multiple outputs
|
|
87
|
+
flow.node('analyzer', 'text:string -> sentiment:string, confidence:number');
|
|
88
|
+
|
|
89
|
+
// Array outputs
|
|
90
|
+
flow.node('extractor', 'documentText:string -> entities:string[]');
|
|
91
|
+
|
|
92
|
+
// Short alias
|
|
93
|
+
flow.n('processor', 'input:string -> output:string');
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Extended Nodes (nx)
|
|
97
|
+
|
|
98
|
+
Add fields to a base signature without rewriting it:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { f, flow } from '@ax-llm/ax';
|
|
102
|
+
|
|
103
|
+
// Chain-of-thought reasoning
|
|
104
|
+
flow.nx('reasoner', 'question:string -> answer:string', {
|
|
105
|
+
prependOutputs: [
|
|
106
|
+
{ name: 'reasoning', type: f.internal(f.string('Step-by-step reasoning')) },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Add confidence scoring
|
|
111
|
+
flow.nx('analyzer', 'input:string -> result:string', {
|
|
112
|
+
appendOutputs: [{ name: 'confidence', type: f.number('Confidence 0-1') }],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Add optional context input
|
|
116
|
+
flow.nx('processor', 'query:string -> response:string', {
|
|
117
|
+
appendInputs: [{ name: 'context', type: f.optional(f.string('Extra context')) }],
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Extension options: `prependInputs`, `appendInputs`, `prependOutputs`, `appendOutputs`.
|
|
122
|
+
|
|
123
|
+
## Execute With Input Mapping
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
flow.execute('summarizer', (state) => ({ documentText: state.document }));
|
|
127
|
+
|
|
128
|
+
// With AI override (use a different model for this node)
|
|
129
|
+
flow.execute('processor', (state) => ({ input: state.data }), { ai: alternativeAI });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Map (State Transformation)
|
|
133
|
+
|
|
134
|
+
Use `map()` for data shaping without AI calls:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Sync
|
|
138
|
+
flow.map((state) => ({ ...state, upperText: state.rawText.toUpperCase() }));
|
|
139
|
+
|
|
140
|
+
// Async
|
|
141
|
+
flow.map(async (state) => {
|
|
142
|
+
const data = await fetchFromAPI(state.query);
|
|
143
|
+
return { ...state, enrichedData: data };
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Parallel async transforms
|
|
147
|
+
flow.map([
|
|
148
|
+
async (state) => ({ ...state, result1: await api1(state.data) }),
|
|
149
|
+
async (state) => ({ ...state, result2: await api2(state.data) }),
|
|
150
|
+
], { parallel: true });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Returns (Final Output)
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const wf = flow<{ input: string }>()
|
|
157
|
+
.map((state) => ({ ...state, upper: state.input.toUpperCase(), len: state.input.length }))
|
|
158
|
+
.returns((state) => ({ upper: state.upper, isLong: state.len > 20 }));
|
|
159
|
+
|
|
160
|
+
// Result is typed as { upper: string; isLong: boolean }
|
|
161
|
+
const result = await wf.forward(llm, { input: 'test' });
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Sequential Processing
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const wf = flow<{ input: string }, { finalResult: string }>()
|
|
168
|
+
.node('step1', 'input:string -> intermediate:string')
|
|
169
|
+
.node('step2', 'intermediate:string -> output:string')
|
|
170
|
+
.execute('step1', (state) => ({ input: state.input }))
|
|
171
|
+
.execute('step2', (state) => ({ intermediate: state.step1Result.intermediate }))
|
|
172
|
+
.returns((state) => ({ finalResult: state.step2Result.output }));
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Auto-Parallel Execution
|
|
176
|
+
|
|
177
|
+
Independent executes run in parallel automatically (`autoParallel: true` by default):
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const wf = flow<{ text: string }, { combined: string }>()
|
|
181
|
+
.node('sentimentAnalyzer', 'text:string -> sentiment:string')
|
|
182
|
+
.node('topicExtractor', 'text:string -> topics:string[]')
|
|
183
|
+
.node('entityRecognizer', 'text:string -> entities:string[]')
|
|
184
|
+
// These three run in parallel (all depend only on state.text)
|
|
185
|
+
.execute('sentimentAnalyzer', (state) => ({ text: state.text }))
|
|
186
|
+
.execute('topicExtractor', (state) => ({ text: state.text }))
|
|
187
|
+
.execute('entityRecognizer', (state) => ({ text: state.text }))
|
|
188
|
+
// This waits for all three
|
|
189
|
+
.returns((state) => ({
|
|
190
|
+
combined: JSON.stringify({
|
|
191
|
+
sentiment: state.sentimentAnalyzerResult.sentiment,
|
|
192
|
+
topics: state.topicExtractorResult.topics,
|
|
193
|
+
entities: state.entityRecognizerResult.entities,
|
|
194
|
+
}),
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
// Inspect execution plan
|
|
198
|
+
const plan = wf.getExecutionPlan();
|
|
199
|
+
console.log(plan.parallelGroups, plan.maxParallelism);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Disable auto-parallel:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const wf = flow({ autoParallel: false });
|
|
206
|
+
// or per execution:
|
|
207
|
+
await wf.forward(llm, input, { autoParallel: false });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Conditional Branching
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const wf = flow<{ query: string; expertMode: boolean }, { response: string }>()
|
|
214
|
+
.node('simple', 'query:string -> response:string')
|
|
215
|
+
.node('expert', 'query:string -> response:string')
|
|
216
|
+
.branch((state) => state.expertMode)
|
|
217
|
+
.when(true)
|
|
218
|
+
.execute('expert', (state) => ({ query: state.query }))
|
|
219
|
+
.when(false)
|
|
220
|
+
.execute('simple', (state) => ({ query: state.query }))
|
|
221
|
+
.merge()
|
|
222
|
+
.returns((state) => ({
|
|
223
|
+
response: state.expertResult?.response ?? state.simpleResult?.response,
|
|
224
|
+
}));
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
After `.merge()`, only the taken branch's result exists; use optional chaining (`?.`) on untaken branch results.
|
|
228
|
+
|
|
229
|
+
## While Loops
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const wf = flow<{ content: string }, { finalContent: string }>()
|
|
233
|
+
.node('processor', 'content:string -> processedContent:string')
|
|
234
|
+
.node('qualityChecker', 'content:string -> qualityScore:number')
|
|
235
|
+
.map((state) => ({ currentContent: state.content, iteration: 0, qualityScore: 0 }))
|
|
236
|
+
.while((state) => state.iteration < 3 && state.qualityScore < 0.8)
|
|
237
|
+
.map((state) => ({ ...state, iteration: state.iteration + 1 }))
|
|
238
|
+
.execute('processor', (state) => ({ content: state.currentContent }))
|
|
239
|
+
.execute('qualityChecker', (state) => ({
|
|
240
|
+
content: state.processorResult.processedContent,
|
|
241
|
+
}))
|
|
242
|
+
.map((state) => ({
|
|
243
|
+
...state,
|
|
244
|
+
currentContent: state.processorResult.processedContent,
|
|
245
|
+
qualityScore: state.qualityCheckerResult.qualityScore,
|
|
246
|
+
}))
|
|
247
|
+
.endWhile()
|
|
248
|
+
.returns((state) => ({ finalContent: state.currentContent }));
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Rules:
|
|
252
|
+
- Every `.while()` needs a matching `.endWhile()`.
|
|
253
|
+
- Ensure the loop condition can change to avoid infinite loops.
|
|
254
|
+
|
|
255
|
+
## Feedback Loops (label/feedback)
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const wf = flow<{ prompt: string }, { result: string }>()
|
|
259
|
+
.node('gen', 'prompt:string -> result:string, quality:number')
|
|
260
|
+
.map((state) => ({ ...state, tries: 0 }))
|
|
261
|
+
.label('retry')
|
|
262
|
+
.map((state) => ({ ...state, tries: state.tries + 1 }))
|
|
263
|
+
.execute('gen', (state) => ({ prompt: state.prompt }))
|
|
264
|
+
.feedback((state) => state.genResult.quality < 0.9 && state.tries < 3, 'retry')
|
|
265
|
+
.returns((state) => ({ result: state.genResult.result }));
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Rules:
|
|
269
|
+
- Define the label before referencing it in `.feedback()`.
|
|
270
|
+
- Always include a max-iteration guard to avoid infinite loops.
|
|
271
|
+
|
|
272
|
+
## Explicit Parallel Sub-Flows
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
flow
|
|
276
|
+
.parallel([
|
|
277
|
+
(sub) => sub.execute('analyzer1', (state) => ({ text: state.input })),
|
|
278
|
+
(sub) => sub.execute('analyzer2', (state) => ({ text: state.input })),
|
|
279
|
+
(sub) => sub.execute('analyzer3', (state) => ({ text: state.input })),
|
|
280
|
+
])
|
|
281
|
+
.merge('combinedResults', (r1, r2, r3) => ({
|
|
282
|
+
a1: r1.analyzer1Result.analysis,
|
|
283
|
+
a2: r2.analyzer2Result.analysis,
|
|
284
|
+
a3: r3.analyzer3Result.analysis,
|
|
285
|
+
}));
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Derive (Batch/Array Processing)
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const wf = flow<{ items: string[] }, { processed: string[] }>({ batchSize: 3 })
|
|
292
|
+
.derive('processed', 'items', (item, index) => `processed-${item}-${index}`, {
|
|
293
|
+
batchSize: 2,
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Dynamic AI Context (Multi-Model)
|
|
298
|
+
|
|
299
|
+
Route nodes to different AI providers:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const fast = ai({ name: 'groq', apiKey: '...' });
|
|
303
|
+
const smart = ai({ name: 'anthropic', apiKey: '...' });
|
|
304
|
+
|
|
305
|
+
const wf = flow<{ text: string }, { out: string }>()
|
|
306
|
+
.node('draft', 'text:string -> out:string')
|
|
307
|
+
.node('refine', 'text:string -> out:string')
|
|
308
|
+
.execute('draft', (state) => ({ text: state.text }), { ai: fast })
|
|
309
|
+
.execute('refine', (state) => ({ text: state.draftResult.out }), { ai: smart })
|
|
310
|
+
.returns((state) => ({ out: state.refineResult.out }));
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Description and toFunction
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
const wf = flow<{ userQuestion: string }, { responseText: string }>()
|
|
317
|
+
.node('qa', 'userQuestion:string -> responseText:string')
|
|
318
|
+
.execute('qa', (state) => ({ userQuestion: state.userQuestion }))
|
|
319
|
+
.returns((state) => ({ responseText: state.qaResult.responseText }))
|
|
320
|
+
.description('Question Answerer', 'Answers user questions concisely.');
|
|
321
|
+
|
|
322
|
+
const fn = wf.toFunction();
|
|
323
|
+
// fn.name, fn.parameters (JSON Schema), fn.func
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Instrumentation (Tracing)
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { ai, flow } from '@ax-llm/ax';
|
|
330
|
+
import { context, trace } from '@opentelemetry/api';
|
|
331
|
+
|
|
332
|
+
const tracer = trace.getTracer('axflow');
|
|
333
|
+
const llm = ai({ name: 'openai', apiKey: '...' });
|
|
334
|
+
|
|
335
|
+
const wf = flow<{ userQuestion: string }>()
|
|
336
|
+
.node('summarizer', 'documentText:string -> summaryText:string')
|
|
337
|
+
.execute('summarizer', (s) => ({ documentText: s.userQuestion }))
|
|
338
|
+
.returns((s) => ({ answer: s.summarizerResult.summaryText }));
|
|
339
|
+
|
|
340
|
+
const result = await wf.forward(llm, { userQuestion: 'hi' }, {
|
|
341
|
+
tracer,
|
|
342
|
+
traceContext: context.active(),
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Program IDs and Demos
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
const wf = flow<{ input: string }>()
|
|
350
|
+
.node('summarizer', 'text:string -> summary:string')
|
|
351
|
+
.node('classifier', 'text:string -> category:string');
|
|
352
|
+
|
|
353
|
+
// Discover program IDs
|
|
354
|
+
console.log(wf.namedPrograms());
|
|
355
|
+
// [{ id: 'root.summarizer', ... }, { id: 'root.classifier', ... }]
|
|
356
|
+
|
|
357
|
+
// Set demos (TypeScript catches typos)
|
|
358
|
+
wf.setDemos([{ programId: 'root.summarizer', traces: [] }]);
|
|
359
|
+
|
|
360
|
+
// Apply optimization
|
|
361
|
+
wf.applyOptimization(optimizedProgram);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Error Handling
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
try {
|
|
368
|
+
const result = await wf.forward(llm, input);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error('Flow execution failed:', error);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Common errors:
|
|
375
|
+
- `"Node 'x' not found"` -- define `.node()` before `.execute()`.
|
|
376
|
+
- `"endWhile() without matching while()"` -- every `.while()` needs `.endWhile()`.
|
|
377
|
+
- `"when() without matching branch()"` -- `.when()` must be inside `.branch()`/`.merge()`.
|
|
378
|
+
- `"merge() without matching branch()"` -- every `.branch()` needs `.merge()`.
|
|
379
|
+
- `"Label 'x' not found"` -- define `.label()` before `.feedback()` references it.
|
|
380
|
+
|
|
381
|
+
## Examples
|
|
382
|
+
|
|
383
|
+
Fetch these for full working code:
|
|
384
|
+
|
|
385
|
+
- [Flow](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow.ts) — complete flow usage
|
|
386
|
+
- [Auto-Parallel](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-auto-parallel.ts) — auto-parallelization
|
|
387
|
+
- [Async Map](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-async-map.ts) — async map transforms
|
|
388
|
+
- [Enhanced Demo](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-enhanced-demo.ts) — instance-based nodes
|
|
389
|
+
- [Flow as Function](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-to-function.ts) — flow as callable function
|
|
390
|
+
- [Fluent Builder](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/fluent-flow-example.ts) — fluent builder pattern
|
|
391
|
+
- [Flow Logging](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/flow-logging-simple.ts) — flow logging
|
|
392
|
+
- [Load Balancing](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/balancer.ts) — load balancing
|
|
393
|
+
|
|
394
|
+
## Do Not Generate
|
|
395
|
+
|
|
396
|
+
- Do not use `new AxFlow(...)` for new code.
|
|
397
|
+
- Do not execute a node before defining it with `.node()`.
|
|
398
|
+
- Do not use generic field names like `text`, `result`, `data`, `input`, `output`.
|
|
399
|
+
- Do not create deep-nested state objects in `.map()`.
|
|
400
|
+
- Do not create loop conditions that can never change.
|
|
401
|
+
- Do not add unnecessary dependencies between executes (kills auto-parallelism).
|
|
402
|
+
- Do not forget to use optional chaining on branch results after `.merge()`.
|