@axlsdk/axl 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +313 -95
- package/dist/index.cjs +651 -258
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +197 -104
- package/dist/index.d.ts +197 -104
- package/dist/index.js +650 -258
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# axl
|
|
1
|
+
# @axlsdk/axl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@axlsdk/axl)
|
|
4
|
+
|
|
5
|
+
Core SDK for orchestrating agentic systems in TypeScript. Part of the [Axl](https://github.com/axl-sdk/axl) monorepo.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -8,6 +10,120 @@ Core SDK for orchestrating agentic systems in TypeScript.
|
|
|
8
10
|
npm install @axlsdk/axl zod
|
|
9
11
|
```
|
|
10
12
|
|
|
13
|
+
## Project Structure
|
|
14
|
+
|
|
15
|
+
The recommended pattern separates config, tools, agents, workflows, and runtime into their own modules. Dependencies flow one direction: tools → agents → workflows → runtime.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/
|
|
19
|
+
config.ts — defineConfig (providers, state, trace)
|
|
20
|
+
runtime.ts — creates AxlRuntime, registers everything
|
|
21
|
+
|
|
22
|
+
tools/
|
|
23
|
+
db.ts — tool wrapping database queries
|
|
24
|
+
email.ts — tool wrapping email service
|
|
25
|
+
|
|
26
|
+
agents/
|
|
27
|
+
support.ts — support agent (imports its tools)
|
|
28
|
+
billing.ts — billing agent
|
|
29
|
+
|
|
30
|
+
workflows/
|
|
31
|
+
handle-ticket.ts — orchestrates support + billing agents
|
|
32
|
+
|
|
33
|
+
axl.config.ts — re-exports runtime for Axl Studio
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Config
|
|
37
|
+
|
|
38
|
+
Use `defineConfig` to create a typed configuration. Keep this separate from your runtime so you can swap configs per environment:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// src/config.ts
|
|
42
|
+
import { defineConfig } from '@axlsdk/axl';
|
|
43
|
+
|
|
44
|
+
export const config = defineConfig({
|
|
45
|
+
providers: {
|
|
46
|
+
openai: { apiKey: process.env.OPENAI_API_KEY },
|
|
47
|
+
anthropic: { apiKey: process.env.ANTHROPIC_API_KEY },
|
|
48
|
+
google: { apiKey: process.env.GOOGLE_API_KEY },
|
|
49
|
+
},
|
|
50
|
+
state: { store: 'sqlite', sqlite: { path: './data/axl.db' } },
|
|
51
|
+
trace: { enabled: true, level: 'steps' },
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Provider API keys are also read automatically from environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`/`GEMINI_API_KEY`), so for local development you can skip the `providers` block entirely.
|
|
56
|
+
|
|
57
|
+
State store options: `'memory'` (default), `'sqlite'` (requires `better-sqlite3`), or a `RedisStore` instance for multi-process deployments. See [State Stores](#state-stores).
|
|
58
|
+
|
|
59
|
+
### Tools, Agents, and Workflows
|
|
60
|
+
|
|
61
|
+
Define each in its own module. Tools wrap your services, agents import the tools they need, workflows orchestrate agents:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// src/tools/db.ts
|
|
65
|
+
import { tool } from '@axlsdk/axl';
|
|
66
|
+
import { z } from 'zod';
|
|
67
|
+
import { db } from '../services/db.js';
|
|
68
|
+
|
|
69
|
+
export const lookupOrder = tool({
|
|
70
|
+
name: 'lookup_order',
|
|
71
|
+
description: 'Look up an order by ID',
|
|
72
|
+
input: z.object({ orderId: z.string() }),
|
|
73
|
+
handler: async ({ orderId }) => db.orders.findById(orderId),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// src/agents/support.ts
|
|
79
|
+
import { agent } from '@axlsdk/axl';
|
|
80
|
+
import { lookupOrder } from '../tools/db.js';
|
|
81
|
+
|
|
82
|
+
export const supportAgent = agent({
|
|
83
|
+
name: 'support',
|
|
84
|
+
model: 'openai-responses:gpt-5.4',
|
|
85
|
+
system: 'You are a customer support agent. Use tools to look up order information.',
|
|
86
|
+
tools: [lookupOrder],
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// src/workflows/handle-ticket.ts
|
|
92
|
+
import { workflow } from '@axlsdk/axl';
|
|
93
|
+
import { z } from 'zod';
|
|
94
|
+
import { supportAgent } from '../agents/support.js';
|
|
95
|
+
|
|
96
|
+
export const handleTicket = workflow({
|
|
97
|
+
name: 'handle-ticket',
|
|
98
|
+
input: z.object({ message: z.string() }),
|
|
99
|
+
handler: async (ctx) => ctx.ask(supportAgent, ctx.input.message),
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Runtime
|
|
104
|
+
|
|
105
|
+
The runtime is the composition root — it imports the config and registers all workflows. Your application and [Axl Studio](https://github.com/axl-sdk/axl/tree/main/packages/axl-studio) both import this module:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// src/runtime.ts
|
|
109
|
+
import { AxlRuntime } from '@axlsdk/axl';
|
|
110
|
+
import { config } from './config.js';
|
|
111
|
+
import { handleTicket } from './workflows/handle-ticket.js';
|
|
112
|
+
import { supportAgent } from './agents/support.js';
|
|
113
|
+
import { lookupOrder } from './tools/db.js';
|
|
114
|
+
|
|
115
|
+
export const runtime = new AxlRuntime(config);
|
|
116
|
+
runtime.register(handleTicket);
|
|
117
|
+
runtime.registerAgent(supportAgent);
|
|
118
|
+
runtime.registerTool(lookupOrder);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// axl.config.ts — thin entry point for Axl Studio
|
|
123
|
+
import { runtime } from './src/runtime.js';
|
|
124
|
+
export default runtime;
|
|
125
|
+
```
|
|
126
|
+
|
|
11
127
|
## API
|
|
12
128
|
|
|
13
129
|
### `tool(config)`
|
|
@@ -26,11 +142,23 @@ const calculator = tool({
|
|
|
26
142
|
const result = new Function(`return (${expression})`)();
|
|
27
143
|
return { result };
|
|
28
144
|
},
|
|
145
|
+
// handler also accepts (input, ctx) for nested agent invocations — see below
|
|
29
146
|
retry: { attempts: 3, backoff: 'exponential' },
|
|
30
147
|
sensitive: false,
|
|
31
148
|
});
|
|
32
149
|
```
|
|
33
150
|
|
|
151
|
+
Tool handlers receive a second parameter `ctx: WorkflowContext` (a child context), enabling the "agent-as-tool" composition pattern:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const researchTool = tool({
|
|
155
|
+
name: 'research',
|
|
156
|
+
description: 'Delegate to a specialist',
|
|
157
|
+
input: z.object({ question: z.string() }),
|
|
158
|
+
handler: async (input, ctx) => ctx.ask(researcher, input.question),
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
34
162
|
### `agent(config)`
|
|
35
163
|
|
|
36
164
|
Define an agent with model, system prompt, tools, and handoffs:
|
|
@@ -40,10 +168,10 @@ import { agent } from '@axlsdk/axl';
|
|
|
40
168
|
|
|
41
169
|
const researcher = agent({
|
|
42
170
|
name: 'researcher',
|
|
43
|
-
model: 'openai:gpt-
|
|
171
|
+
model: 'openai-responses:gpt-5.4',
|
|
44
172
|
system: 'You are a research assistant.',
|
|
45
173
|
tools: [calculator],
|
|
46
|
-
|
|
174
|
+
effort: 'high',
|
|
47
175
|
maxTurns: 10,
|
|
48
176
|
timeout: '30s',
|
|
49
177
|
temperature: 0.7,
|
|
@@ -55,41 +183,76 @@ Dynamic model and system prompt selection:
|
|
|
55
183
|
|
|
56
184
|
```typescript
|
|
57
185
|
const dynamicAgent = agent({
|
|
58
|
-
model: (ctx) =>
|
|
59
|
-
|
|
60
|
-
|
|
186
|
+
model: (ctx) =>
|
|
187
|
+
ctx.metadata?.tier === 'premium'
|
|
188
|
+
? 'openai-responses:gpt-5.4'
|
|
189
|
+
: 'openai-responses:gpt-5-nano',
|
|
61
190
|
system: (ctx) => `You are a ${ctx.metadata?.role ?? 'general'} assistant.`,
|
|
62
191
|
});
|
|
63
192
|
```
|
|
64
193
|
|
|
65
|
-
####
|
|
194
|
+
#### Dynamic Handoffs
|
|
195
|
+
|
|
196
|
+
`handoffs` accepts a static array or a function for runtime-conditional routing:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const router = agent({
|
|
200
|
+
model: 'openai-responses:gpt-5-mini',
|
|
201
|
+
system: 'Route to the right specialist.',
|
|
202
|
+
handoffs: (ctx) => {
|
|
203
|
+
const base = [
|
|
204
|
+
{ agent: billingAgent, description: 'Billing issues' },
|
|
205
|
+
{ agent: shippingAgent, description: 'Shipping questions' },
|
|
206
|
+
];
|
|
207
|
+
if (ctx.metadata?.tier === 'enterprise') {
|
|
208
|
+
base.push({ agent: priorityAgent, description: 'Priority support' });
|
|
209
|
+
}
|
|
210
|
+
return base;
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### Workflow-Level Routing with `ctx.delegate()`
|
|
216
|
+
|
|
217
|
+
When your workflow (not an agent's LLM) needs to pick the best agent:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const result = await ctx.delegate(
|
|
221
|
+
[billingAgent, shippingAgent, returnsAgent],
|
|
222
|
+
customerMessage,
|
|
223
|
+
);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`ctx.delegate()` creates a temporary router agent that uses handoffs to select the best candidate. For a single agent, it calls `ctx.ask()` directly with no routing overhead.
|
|
227
|
+
|
|
228
|
+
#### Effort (cross-provider reasoning control)
|
|
66
229
|
|
|
67
|
-
The `
|
|
230
|
+
The `effort` parameter provides a unified way to control reasoning depth across all providers:
|
|
68
231
|
|
|
69
232
|
```typescript
|
|
70
233
|
// Simple levels — works on any provider
|
|
71
234
|
const reasoner = agent({
|
|
72
|
-
model: 'anthropic:claude-
|
|
235
|
+
model: 'anthropic:claude-opus-4-6',
|
|
73
236
|
system: 'You are a careful analyst.',
|
|
74
|
-
|
|
237
|
+
effort: 'high', // 'none' | 'low' | 'medium' | 'high' | 'max'
|
|
75
238
|
});
|
|
76
239
|
|
|
77
|
-
// Explicit budget (in tokens)
|
|
240
|
+
// Explicit thinking budget (in tokens — supported on Gemini 2.x and Anthropic)
|
|
78
241
|
const budgetReasoner = agent({
|
|
79
|
-
model: 'google:gemini-2.5-
|
|
242
|
+
model: 'google:gemini-2.5-pro',
|
|
80
243
|
system: 'Think step by step.',
|
|
81
|
-
|
|
244
|
+
thinkingBudget: 5000,
|
|
82
245
|
});
|
|
83
246
|
|
|
84
247
|
// Per-call override
|
|
85
|
-
const result = await reasoner.ask('Analyze this data', {
|
|
248
|
+
const result = await reasoner.ask('Analyze this data', { effort: 'low' });
|
|
86
249
|
```
|
|
87
250
|
|
|
88
|
-
Each provider maps `
|
|
251
|
+
Each provider maps `effort` to its native API: reasoning effort (OpenAI), adaptive thinking (Anthropic), thinking level/budget (Gemini). See [docs/providers.md](../../docs/providers.md) for the full mapping table.
|
|
89
252
|
|
|
90
253
|
### `workflow(config)`
|
|
91
254
|
|
|
92
|
-
Define a named workflow with typed input
|
|
255
|
+
Define a named workflow with typed input:
|
|
93
256
|
|
|
94
257
|
```typescript
|
|
95
258
|
import { workflow } from '@axlsdk/axl';
|
|
@@ -98,10 +261,28 @@ import { z } from 'zod';
|
|
|
98
261
|
const myWorkflow = workflow({
|
|
99
262
|
name: 'my-workflow',
|
|
100
263
|
input: z.object({ query: z.string() }),
|
|
101
|
-
output: z.object({ answer: z.string() }),
|
|
102
264
|
handler: async (ctx) => {
|
|
103
|
-
|
|
104
|
-
|
|
265
|
+
return ctx.ask(researcher, ctx.input.query, {
|
|
266
|
+
schema: z.object({ answer: z.string() }),
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
For single-ask workflows, use `schema` on `ctx.ask()` — it instructs the LLM and retries automatically on invalid output. The optional `output` field validates your handler's return value *after* it runs (no LLM retry), which is useful for multi-step workflows where your orchestration logic (spawn, vote, transform) could assemble the wrong shape:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const answerSchema = z.object({ answer: z.number() });
|
|
276
|
+
|
|
277
|
+
const reliable = workflow({
|
|
278
|
+
name: 'reliable',
|
|
279
|
+
input: z.object({ question: z.string() }),
|
|
280
|
+
output: answerSchema, // validates the spawn+vote result, not the LLM
|
|
281
|
+
handler: async (ctx) => {
|
|
282
|
+
const results = await ctx.spawn(3, async (_i) =>
|
|
283
|
+
ctx.ask(mathAgent, ctx.input.question, { schema: answerSchema }),
|
|
284
|
+
);
|
|
285
|
+
return ctx.vote(results, { strategy: 'majority', key: 'answer' });
|
|
105
286
|
},
|
|
106
287
|
});
|
|
107
288
|
```
|
|
@@ -111,9 +292,6 @@ const myWorkflow = workflow({
|
|
|
111
292
|
Register and execute workflows:
|
|
112
293
|
|
|
113
294
|
```typescript
|
|
114
|
-
import { AxlRuntime } from '@axlsdk/axl';
|
|
115
|
-
|
|
116
|
-
const runtime = new AxlRuntime();
|
|
117
295
|
runtime.register(myWorkflow);
|
|
118
296
|
|
|
119
297
|
// Execute
|
|
@@ -129,7 +307,12 @@ for await (const event of stream) {
|
|
|
129
307
|
const session = runtime.session('user-123');
|
|
130
308
|
await session.send('my-workflow', { query: 'Hello' });
|
|
131
309
|
await session.send('my-workflow', { query: 'Follow-up' });
|
|
132
|
-
|
|
310
|
+
|
|
311
|
+
// Stream a session turn
|
|
312
|
+
const sessionStream = await session.stream('my-workflow', { query: 'Hello' });
|
|
313
|
+
for await (const event of sessionStream) {
|
|
314
|
+
if (event.type === 'token') process.stdout.write(event.data);
|
|
315
|
+
}
|
|
133
316
|
```
|
|
134
317
|
|
|
135
318
|
### Context Primitives
|
|
@@ -137,32 +320,33 @@ const history = await session.history();
|
|
|
137
320
|
All available on `ctx` inside workflow handlers. See the [API Reference](../../docs/api-reference.md) for complete option types, valid values, and defaults.
|
|
138
321
|
|
|
139
322
|
```typescript
|
|
140
|
-
// Invoke an agent
|
|
323
|
+
// Invoke an agent (schema retries rebuild the call with the failed output + error in the prompt)
|
|
141
324
|
const answer = await ctx.ask(agent, 'prompt', { schema, retries });
|
|
142
325
|
|
|
143
326
|
// Run 3 agents in parallel — each gets the same question independently
|
|
144
327
|
const results = await ctx.spawn(3, async (i) => ctx.ask(agent, prompts[i]));
|
|
145
328
|
|
|
146
|
-
// Pick the answer that appeared most often
|
|
147
|
-
const winner = ctx.vote(results, { strategy: 'majority', key: 'answer' });
|
|
329
|
+
// Pick the answer that appeared most often — also supports LLM-as-judge via scorer
|
|
330
|
+
const winner = await ctx.vote(results, { strategy: 'majority', key: 'answer' });
|
|
148
331
|
|
|
149
|
-
//
|
|
332
|
+
// Generic retry-until-valid loop (not conversation-aware — you decide how to use the error)
|
|
150
333
|
const valid = await ctx.verify(
|
|
151
|
-
async (lastOutput, error) => ctx.ask(agent, prompt),
|
|
334
|
+
async (lastOutput, error) => ctx.ask(agent, error ? `Fix: ${error}` : prompt),
|
|
152
335
|
schema,
|
|
153
336
|
{ retries: 3, fallback: defaultValue },
|
|
154
337
|
);
|
|
155
338
|
|
|
156
|
-
// Cost control
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
339
|
+
// Cost control — returns { value, budgetExceeded, totalCost }
|
|
340
|
+
const { value } = await ctx.budget(
|
|
341
|
+
{ cost: '$1.00', onExceed: 'hard_stop' },
|
|
342
|
+
async () => ctx.ask(agent, prompt),
|
|
343
|
+
);
|
|
160
344
|
|
|
161
345
|
// First to complete
|
|
162
|
-
const fastest = await ctx.race(
|
|
163
|
-
() => ctx.ask(agentA, prompt),
|
|
164
|
-
|
|
165
|
-
|
|
346
|
+
const fastest = await ctx.race(
|
|
347
|
+
[() => ctx.ask(agentA, prompt), () => ctx.ask(agentB, prompt)],
|
|
348
|
+
{ schema },
|
|
349
|
+
);
|
|
166
350
|
|
|
167
351
|
// Concurrent independent tasks
|
|
168
352
|
const [a, b] = await ctx.parallel([
|
|
@@ -176,14 +360,16 @@ const mapped = await ctx.map(items, async (item) => ctx.ask(agent, item), {
|
|
|
176
360
|
quorum: 3,
|
|
177
361
|
});
|
|
178
362
|
|
|
179
|
-
// Human-in-the-loop
|
|
363
|
+
// Human-in-the-loop — suspends until resolved via API or Studio
|
|
180
364
|
const decision = await ctx.awaitHuman({
|
|
181
|
-
channel: '
|
|
365
|
+
channel: 'approvals',
|
|
182
366
|
prompt: 'Approve this action?',
|
|
183
367
|
});
|
|
184
368
|
|
|
185
|
-
// Durable checkpoint
|
|
186
|
-
|
|
369
|
+
// Durable checkpoint — on first run, executes and saves the result.
|
|
370
|
+
// On replay after a restart, returns the saved result without re-executing,
|
|
371
|
+
// preventing duplicate side effects (double API calls, double charges, etc.)
|
|
372
|
+
const checkpointed = await ctx.checkpoint(async () => expensiveOperation());
|
|
187
373
|
```
|
|
188
374
|
|
|
189
375
|
### OpenTelemetry Observability
|
|
@@ -192,13 +378,18 @@ Automatic span emission for every `ctx.*` primitive with cost-per-span attributi
|
|
|
192
378
|
|
|
193
379
|
```typescript
|
|
194
380
|
import { defineConfig, AxlRuntime } from '@axlsdk/axl';
|
|
195
|
-
import {
|
|
381
|
+
import {
|
|
382
|
+
BasicTracerProvider,
|
|
383
|
+
SimpleSpanProcessor,
|
|
384
|
+
} from '@opentelemetry/sdk-trace-base';
|
|
196
385
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
197
386
|
|
|
198
387
|
const tracerProvider = new BasicTracerProvider();
|
|
199
|
-
tracerProvider.addSpanProcessor(
|
|
200
|
-
new
|
|
201
|
-
)
|
|
388
|
+
tracerProvider.addSpanProcessor(
|
|
389
|
+
new SimpleSpanProcessor(
|
|
390
|
+
new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' }),
|
|
391
|
+
),
|
|
392
|
+
);
|
|
202
393
|
|
|
203
394
|
const config = defineConfig({
|
|
204
395
|
telemetry: {
|
|
@@ -222,7 +413,7 @@ import { createSpanManager, NoopSpanManager } from '@axlsdk/axl';
|
|
|
222
413
|
|
|
223
414
|
### Memory Primitives
|
|
224
415
|
|
|
225
|
-
Working memory backed by the
|
|
416
|
+
Working memory backed by the `StateStore` interface:
|
|
226
417
|
|
|
227
418
|
```typescript
|
|
228
419
|
// Store and retrieve structured state
|
|
@@ -235,18 +426,20 @@ await ctx.remember('user-profile', data, { scope: 'global' });
|
|
|
235
426
|
const profile = await ctx.recall('user-profile', { scope: 'global' });
|
|
236
427
|
```
|
|
237
428
|
|
|
238
|
-
Semantic recall requires a vector store and embedder
|
|
429
|
+
Semantic recall requires a vector store and embedder on the config:
|
|
239
430
|
|
|
240
431
|
```typescript
|
|
241
|
-
import { AxlRuntime, InMemoryVectorStore, OpenAIEmbedder } from '@axlsdk/axl';
|
|
432
|
+
import { defineConfig, AxlRuntime, InMemoryVectorStore, OpenAIEmbedder } from '@axlsdk/axl';
|
|
242
433
|
|
|
243
|
-
const
|
|
434
|
+
const config = defineConfig({
|
|
244
435
|
memory: {
|
|
245
436
|
vectorStore: new InMemoryVectorStore(),
|
|
246
437
|
embedder: new OpenAIEmbedder({ model: 'text-embedding-3-small' }),
|
|
247
438
|
},
|
|
248
439
|
});
|
|
249
440
|
|
|
441
|
+
const runtime = new AxlRuntime(config);
|
|
442
|
+
|
|
250
443
|
// In a workflow:
|
|
251
444
|
const relevant = await ctx.recall('knowledge-base', {
|
|
252
445
|
query: 'refund policy',
|
|
@@ -266,7 +459,7 @@ const containsPII = (text: string) => /\b\d{3}-\d{2}-\d{4}\b/.test(text);
|
|
|
266
459
|
const isOffTopic = (text: string) => !text.toLowerCase().includes('support');
|
|
267
460
|
|
|
268
461
|
const safe = agent({
|
|
269
|
-
model: 'openai:gpt-
|
|
462
|
+
model: 'openai-responses:gpt-5.4',
|
|
270
463
|
system: 'You are a helpful assistant.',
|
|
271
464
|
guardrails: {
|
|
272
465
|
input: async (prompt, ctx) => {
|
|
@@ -274,83 +467,108 @@ const safe = agent({
|
|
|
274
467
|
return { block: false };
|
|
275
468
|
},
|
|
276
469
|
output: async (response, ctx) => {
|
|
277
|
-
if (isOffTopic(response))
|
|
470
|
+
if (isOffTopic(response))
|
|
471
|
+
return { block: true, reason: 'Off-topic response' };
|
|
278
472
|
return { block: false };
|
|
279
473
|
},
|
|
280
|
-
onBlock: 'retry',
|
|
474
|
+
onBlock: 'retry', // 'retry' | 'throw' | (reason, ctx) => fallbackResponse
|
|
281
475
|
maxRetries: 2,
|
|
282
476
|
},
|
|
283
477
|
});
|
|
284
478
|
```
|
|
285
479
|
|
|
286
|
-
When `onBlock` is `'retry'`, the LLM
|
|
480
|
+
When `onBlock` is `'retry'`, the LLM's blocked output is appended to the conversation (as an assistant message) along with a system message containing the block reason, then the LLM is re-called so it can self-correct. These messages **accumulate** across retries — if the guardrail blocks multiple times, the LLM sees all prior failed attempts and corrections before its next try. All retry messages are ephemeral — they are **not** persisted to session history, so subsequent session turns never see the blocked attempts. Note: `ctx.ask()` schema retries work differently — each retry rebuilds the call from scratch and only includes the most recent failed output and error (previous failures do not accumulate). Input guardrails always throw since the prompt is user-supplied. Throws `GuardrailError` if retries are exhausted or `onBlock` is `'throw'`.
|
|
481
|
+
|
|
482
|
+
### State Stores
|
|
483
|
+
|
|
484
|
+
Three built-in implementations. All persist the same data: workflow execution checkpoints, `awaitHuman` decisions, session history, memory entries, and the execution state needed for suspend/resume.
|
|
485
|
+
|
|
486
|
+
**Memory** (default) — in-process, no persistence. Use for development and stateless workflows.
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
const runtime = new AxlRuntime();
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**SQLite** — file-based persistence. Use for single-process deployments that need durable state across restarts.
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
npm install better-sqlite3
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
const runtime = new AxlRuntime({
|
|
500
|
+
state: { store: 'sqlite', sqlite: { path: './data/axl.db' } },
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Redis** — shared state across multiple processes. Use for multi-replica deployments or any setup where more than one process runs `AxlRuntime`.
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
npm install redis
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import { AxlRuntime, RedisStore } from '@axlsdk/axl';
|
|
512
|
+
|
|
513
|
+
const store = await RedisStore.create('redis://localhost:6379');
|
|
514
|
+
const runtime = new AxlRuntime({ state: { store } });
|
|
515
|
+
|
|
516
|
+
// Graceful shutdown — closes the Redis connection
|
|
517
|
+
await runtime.shutdown();
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
`RedisStore.create()` connects before returning, so any connection error surfaces at startup rather than on first use. The runtime's `shutdown()` closes the connection automatically.
|
|
287
521
|
|
|
288
522
|
### Session Options
|
|
289
523
|
|
|
290
524
|
```typescript
|
|
291
525
|
const session = runtime.session('user-123', {
|
|
292
526
|
history: {
|
|
293
|
-
maxMessages: 100,
|
|
294
|
-
summarize: true,
|
|
295
|
-
summaryModel: 'openai:gpt-
|
|
527
|
+
maxMessages: 100,
|
|
528
|
+
summarize: true,
|
|
529
|
+
summaryModel: 'openai-responses:gpt-5-mini',
|
|
296
530
|
},
|
|
297
|
-
persist: true,
|
|
531
|
+
persist: true,
|
|
298
532
|
});
|
|
299
533
|
```
|
|
300
534
|
|
|
301
|
-
`
|
|
535
|
+
When `maxMessages` is exceeded:
|
|
536
|
+
|
|
537
|
+
- **`summarize: false`** (default) — oldest messages beyond the limit are dropped. Only the most recent `maxMessages` are kept.
|
|
538
|
+
- **`summarize: true`** — before dropping, the overflow messages are sent to `summaryModel` for summarization. The summary is saved to session metadata and included as context on subsequent turns. Each time the limit is exceeded again, the new overflow is summarized together with the previous summary, so context accumulates incrementally.
|
|
302
539
|
|
|
303
|
-
| Option
|
|
304
|
-
|
|
305
|
-
| `history.maxMessages`
|
|
306
|
-
| `history.summarize`
|
|
307
|
-
| `history.summaryModel` | `string`
|
|
308
|
-
| `persist`
|
|
540
|
+
| Option | Type | Default | Description |
|
|
541
|
+
| ---------------------- | --------- | --------- | ------------------------------------------------------------- |
|
|
542
|
+
| `history.maxMessages` | `number` | unlimited | Max messages to retain in history |
|
|
543
|
+
| `history.summarize` | `boolean` | `false` | Summarize overflow messages instead of dropping them |
|
|
544
|
+
| `history.summaryModel` | `string` | — | Model URI for summarization (required when `summarize: true`) |
|
|
545
|
+
| `persist` | `boolean` | `true` | Persist history to StateStore |
|
|
309
546
|
|
|
310
547
|
### Error Hierarchy
|
|
311
548
|
|
|
312
549
|
```typescript
|
|
313
550
|
import {
|
|
314
|
-
AxlError,
|
|
315
|
-
VerifyError,
|
|
316
|
-
QuorumNotMet,
|
|
317
|
-
NoConsensus,
|
|
318
|
-
TimeoutError,
|
|
319
|
-
MaxTurnsError,
|
|
551
|
+
AxlError, // Base class
|
|
552
|
+
VerifyError, // Schema validation failed after retries
|
|
553
|
+
QuorumNotMet, // Quorum threshold not reached
|
|
554
|
+
NoConsensus, // Vote could not reach consensus
|
|
555
|
+
TimeoutError, // Operation exceeded timeout
|
|
556
|
+
MaxTurnsError, // Agent exceeded max tool-calling turns
|
|
320
557
|
BudgetExceededError, // Budget limit exceeded
|
|
321
|
-
GuardrailError,
|
|
322
|
-
ToolDenied,
|
|
558
|
+
GuardrailError, // Guardrail blocked input or output
|
|
559
|
+
ToolDenied, // Agent tried to call unauthorized tool
|
|
323
560
|
} from '@axlsdk/axl';
|
|
324
561
|
```
|
|
325
562
|
|
|
326
|
-
### State Stores
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
import { MemoryStore, SQLiteStore, RedisStore } from '@axlsdk/axl';
|
|
330
|
-
|
|
331
|
-
// In-memory (default)
|
|
332
|
-
const runtime = new AxlRuntime();
|
|
333
|
-
|
|
334
|
-
// SQLite (requires better-sqlite3)
|
|
335
|
-
const runtime = new AxlRuntime({
|
|
336
|
-
state: { store: 'sqlite', sqlite: { path: './data/axl.db' } },
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
// Redis (requires ioredis)
|
|
340
|
-
const runtime = new AxlRuntime({
|
|
341
|
-
state: { store: 'redis', redis: { url: 'redis://localhost:6379' } },
|
|
342
|
-
});
|
|
343
|
-
```
|
|
344
|
-
|
|
345
563
|
### Provider URIs
|
|
346
564
|
|
|
347
565
|
Four built-in providers using the `provider:model` URI scheme:
|
|
348
566
|
|
|
349
567
|
```
|
|
350
|
-
openai:gpt-
|
|
351
|
-
openai
|
|
352
|
-
anthropic:claude-sonnet-4-
|
|
353
|
-
google:gemini-
|
|
568
|
+
openai-responses:gpt-5.4 # OpenAI Responses API (preferred over Chat Completions)
|
|
569
|
+
openai:gpt-5.4 # OpenAI Chat Completions
|
|
570
|
+
anthropic:claude-sonnet-4-6 # Anthropic
|
|
571
|
+
google:gemini-3.1-pro-preview # Google Gemini
|
|
354
572
|
```
|
|
355
573
|
|
|
356
574
|
See [docs/providers.md](../../docs/providers.md) for the full model list including reasoning models.
|