@fifthrevision/axle 0.18.0 → 0.20.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 DELETED
@@ -1,543 +0,0 @@
1
- # Axle
2
-
3
- Axle is a TypeScript library for building multi-turn LLM agents. It provides a
4
- small, focused API for building agentic applications.
5
-
6
- **Documentation:** https://axle.fifthrevision.com
7
-
8
- ## Quick Start
9
-
10
- ```typescript
11
- import { Agent, Instruct, anthropic } from "@fifthrevision/axle";
12
-
13
- const provider = anthropic(process.env.ANTHROPIC_API_KEY);
14
- const agent = new Agent({ provider, model: "claude-sonnet-4-5-20250929" });
15
-
16
- const r1 = await agent.send("What is the capital of France?").final;
17
- if (!r1.ok) throw new Error(r1.error.kind);
18
- console.log(r1.response); // "Paris is the capital of France."
19
-
20
- // Multi-turn — history is managed automatically
21
- const r2 = await agent.send("And what about Germany?").final;
22
- if (!r2.ok) throw new Error(r2.error.kind);
23
- ```
24
-
25
- ## Philosophy
26
-
27
- Axle has two big goals
28
-
29
- 1. A small, focused, and ergonomic interface for building agents. The Agent,
30
- Instruct, and other APIs are the entire surface, and there is a lot of thought
31
- to make them distinct and composable.
32
- 2. Systematic prompt improvement. Log what was sent, validate what came back, feed
33
- learnings into the next run. (This is where the roadmap is headed.)
34
-
35
- Axle started as a DSPy-inspired workflow tool. As models got better with reasoning
36
- and tool use, rigid workflow graphs felt unnecessary — but the goals behind them
37
- (structured output, verification, multi-step reasoning) didn't go away. The project
38
- shifted toward making those capabilities composable primitives rather than
39
- fixed pipelines.
40
-
41
- ### Roadmap
42
-
43
- - **Memory:** Ways to remember previous runs to retrieve them and add them back
44
- into the prompt for future runs.
45
- - **Verification:** Automatic and manual ways to verify the output hits goals
46
-
47
- ## Core Concepts
48
-
49
- ### Agent
50
-
51
- Agent is the primary interface. It owns the provider, model, system prompt,
52
- tools, and conversation history. `send()` is the only verb — it accepts either a
53
- plain string or an Instruct.
54
-
55
- ```typescript
56
- const agent = new Agent({
57
- provider: anthropic(apiKey),
58
- model: "claude-sonnet-4-5-20250929",
59
- system: "You are a helpful assistant.",
60
- tools: [calculatorTool],
61
- });
62
- ```
63
-
64
- ### Instruct
65
-
66
- Instruct is a rich message. Use it when you need structured output, file
67
- attachments, bound template inputs, or additional instructions.
68
-
69
- ```typescript
70
- import * as z from "zod";
71
-
72
- const instruct = new Instruct({
73
- prompt: "Summarize the following {{topic}}.",
74
- schema: z.object({
75
- summary: z.string(),
76
- keyPoints: z.array(z.string()),
77
- }),
78
- }).withInputs({ topic: "document" });
79
- instruct.addFile(await loadFileContent("./report.pdf"));
80
-
81
- const result = await agent.send(instruct).final;
82
- if (!result.ok) throw new Error(result.error.kind);
83
- // result.response is { summary: string, keyPoints: string[] }
84
- ```
85
-
86
- For plain text interactions, pass a string directly to `send()` instead.
87
-
88
- ### Providers
89
-
90
- Axle ships with first-party support for Anthropic, OpenAI, and Gemini, plus a
91
- generic ChatCompletions provider for any OpenAI-compatible API.
92
-
93
- ```typescript
94
- import { anthropic, openai, gemini, chatCompletions } from "@fifthrevision/axle";
95
-
96
- const a = anthropic(process.env.ANTHROPIC_API_KEY);
97
- const o = openai(process.env.OPENAI_API_KEY);
98
- const g = gemini(process.env.GEMINI_API_KEY);
99
- const local = chatCompletions("http://localhost:11434/v1");
100
- ```
101
-
102
- ### `stream()` and `generate()`
103
-
104
- Agent is built on two lower-level primitives that can be used directly when you
105
- want full control without conversation management.
106
-
107
- `stream()` runs a tool loop over a streaming request and returns a handle with
108
- callbacks for real-time output:
109
-
110
- ```typescript
111
- import { stream } from "@fifthrevision/axle";
112
-
113
- const handle = stream({
114
- provider,
115
- model,
116
- messages: [{ role: "user", content: "Hello" }],
117
- tools: [myTool],
118
- onToolCall: async (name, params) => ({ type: "success", content: "result" }),
119
- });
120
-
121
- handle.on((event) => {
122
- if (event.type === "text:delta") process.stdout.write(event.delta);
123
- });
124
-
125
- const result = await handle.final;
126
- if (!result.ok) throw new Error(result.error.kind);
127
- ```
128
-
129
- `generate()` does the same but without streaming — it returns the final result
130
- directly as a promise:
131
-
132
- ```typescript
133
- import { generate } from "@fifthrevision/axle";
134
-
135
- const result = await generate({
136
- provider,
137
- model,
138
- messages: [{ role: "user", content: "Hello" }],
139
- tools: [myTool],
140
- onToolCall: async (name, params) => ({ type: "success", content: "result" }),
141
- });
142
-
143
- if (!result.ok) throw new Error(result.error.kind);
144
- result.response; // final assistant message
145
- ```
146
-
147
- Both `stream()` and `generate()` also accept an `Instruct` as the latest user
148
- turn. When `messages` is provided with `instruct`, `messages` is treated as
149
- prior context and the rendered `Instruct` is appended as the new user message.
150
-
151
- ```typescript
152
- import * as z from "zod";
153
- import { generate, Instruct } from "@fifthrevision/axle";
154
-
155
- const result = await generate({
156
- provider,
157
- model,
158
- messages: previousMessages,
159
- instruct: new Instruct({
160
- prompt: "Answer {{question}}.",
161
- schema: z.object({
162
- answer: z.string(),
163
- }),
164
- }).withInput("question", "Should we proceed?"),
165
- });
166
-
167
- if (!result.ok) throw new Error(result.error.kind);
168
- result.response.answer; // string
169
- ```
170
-
171
- Both handle the full tool-call loop automatically. Agent uses `stream()`
172
- internally and adds history management, system prompt, and callback wiring on
173
- top.
174
-
175
- ### Results
176
-
177
- `generate(...)`, `stream(...).final`, and `agent.send(...).final` all resolve to
178
- a two-state result:
179
-
180
- ```typescript
181
- if (!result.ok) {
182
- result.error.kind; // "model" | "tool" | "parse"
183
- if (result.error.kind === "parse") {
184
- result.error.message;
185
- }
186
- return;
187
- }
188
-
189
- result.response; // always present when ok is true
190
- ```
191
-
192
- For `generate()` and `stream()`, plain calls return the final assistant message.
193
- For `Agent.send("...")`, plain calls return the assistant text. `Instruct`
194
- calls return the parsed schema value. Model, tool, and parse failures return
195
- `ok: false`; abort, fatal tool, configuration, and unexpected execution errors
196
- still throw.
197
-
198
- Cancellation follows standard JavaScript abort semantics:
199
-
200
- - `handle.cancel(reason)` aborts a `stream()` or `agent.send()` handle.
201
- - `stream().final`, `generate(...)`, and `agent.send(...).final` reject with an error whose `name` is `"AbortError"`.
202
- - Axle abort errors preserve `reason`, `usage`, and partial state where available (`messages`, `partial`, and for `Agent.send`, `turn`).
203
-
204
- ## Details
205
-
206
- ### Structured Output
207
-
208
- Pass a Zod schema to Instruct. Axle compiles the schema
209
- into output format instructions, then parses the response back into typed
210
- objects.
211
-
212
- ```typescript
213
- import * as z from "zod";
214
-
215
- const instruct = new Instruct({
216
- prompt: "Tell me about Mars.",
217
- schema: z.object({
218
- name: z.string(),
219
- distanceFromSun: z.number(),
220
- moons: z.array(z.string()),
221
- }),
222
- });
223
-
224
- const agent = new Agent({ provider, model });
225
- const result = await agent.send(instruct).final;
226
- if (!result.ok) throw new Error(result.error.kind);
227
-
228
- result.response.name; // string
229
- result.response.distanceFromSun; // number
230
- result.response.moons; // string[]
231
- ```
232
-
233
- For one-shot structured calls without agent-managed history, pass the same
234
- `Instruct` directly to `generate()` or `stream()`.
235
-
236
- ### Tools
237
-
238
- A tool is an object with a name, description, Zod schema, and an `execute`
239
- function. Pass tools to the Agent constructor.
240
-
241
- ```typescript
242
- import { z } from "zod";
243
-
244
- const weatherTool = {
245
- name: "getWeather",
246
- description: "Get current weather for a city",
247
- schema: z.object({ city: z.string() }),
248
- async execute(input) {
249
- return JSON.stringify({ temp: 72, condition: "sunny" });
250
- },
251
- };
252
-
253
- const agent = new Agent({
254
- provider,
255
- model,
256
- tools: [weatherTool],
257
- });
258
- ```
259
-
260
- Axle includes several built-in tools: `braveSearchTool`, `calculatorTool`,
261
- `execTool`, `readFileTool`, `writeFileTool`, and `patchFileTool`.
262
-
263
- ### Provider Tools
264
-
265
- Provider tools are tools that execute on the LLM provider's side (e.g. web
266
- search, code interpreter). Pass them via the `providerTools` option using
267
- `{ type: "provider", name: "..." }`.
268
-
269
- ```typescript
270
- import { Agent, calculatorTool } from "@fifthrevision/axle";
271
- import type { ProviderTool } from "@fifthrevision/axle";
272
-
273
- const agent = new Agent({
274
- provider,
275
- model,
276
- tools: [calculatorTool],
277
- providerTools: [{ type: "provider", name: "web_search" }],
278
- });
279
- ```
280
-
281
- Axle maps common names to provider-specific identifiers automatically:
282
-
283
- | Name | Anthropic | OpenAI | Gemini |
284
- | ---------------- | --------------------- | -------------------- | --------------- |
285
- | `web_search` | `web_search_20250305` | `web_search_preview` | `googleSearch` |
286
- | `code_execution` | — | `code_interpreter` | `codeExecution` |
287
-
288
- You can also pass provider-specific names directly. Use the optional `config`
289
- field for provider-specific options:
290
-
291
- ```typescript
292
- { type: "provider", name: "web_search", config: { max_results: 5 } }
293
- ```
294
-
295
- Provider tool events stream as `provider-tool:start` and `provider-tool:complete`.
296
-
297
- ### MCP (Model Context Protocol)
298
-
299
- Axle supports connecting to MCP servers via stdio or HTTP transport. Create an
300
- MCP instance, connect it, and pass it to Agent.
301
-
302
- ```typescript
303
- import { Agent, MCP } from "@fifthrevision/axle";
304
-
305
- const mcp = new MCP({
306
- transport: "stdio",
307
- name: "wc",
308
- command: "npx",
309
- args: ["tsx", "path/to/wordcount-server.ts"],
310
- });
311
- await mcp.connect();
312
-
313
- const agent = new Agent({ provider, model, mcps: [mcp] });
314
- const result = await agent.send("Count the words in 'hello world'").final;
315
- if (!result.ok) throw new Error(result.error.kind);
316
-
317
- await mcp.close();
318
- ```
319
-
320
- The optional `name` field prefixes all tool names from that server (e.g.
321
- `wc_word_count`) to avoid collisions when using multiple MCPs. When omitted,
322
- the server's self-reported name is used as the prefix if available.
323
-
324
- HTTP transport works the same way:
325
-
326
- ```typescript
327
- const mcp = new MCP({
328
- transport: "http",
329
- url: "http://localhost:3100/mcp",
330
- });
331
- ```
332
-
333
- ### Streaming
334
-
335
- Axle has two event models, used at different levels:
336
-
337
- - `Agent.on(...)` emits `AgentEvent` — a high-level turn view organized
338
- around parts (text, thinking, action).
339
- - `stream(...).on(...)` emits `StreamEvent` — a lower-level view that
340
- surfaces every text/thinking/tool transition the provider produces.
341
-
342
- `Agent` uses `stream()` internally and translates each `StreamEvent` into
343
- one or more `AgentEvent`s.
344
-
345
- #### Agent events
346
-
347
- ```typescript
348
- const agent = new Agent({ provider, model });
349
-
350
- agent.on((event) => {
351
- switch (event.type) {
352
- case "text:delta":
353
- process.stdout.write(event.delta);
354
- break;
355
- case "part:start":
356
- if (event.part.type === "action") {
357
- console.log(`Tool: ${event.part.detail.name}`);
358
- }
359
- break;
360
- case "action:complete":
361
- console.log("Tool complete");
362
- break;
363
- case "turn:end":
364
- console.log(`Turn ${event.status} (in: ${event.usage.in})`);
365
- break;
366
- case "error":
367
- console.error(event.error);
368
- break;
369
- }
370
- });
371
-
372
- const handle = agent.send("Write me a poem.");
373
- // handle.cancel(reason) aborts mid-stream and rejects handle.final with an AbortError
374
- try {
375
- const result = await handle.final;
376
- if (!result.ok) {
377
- console.error(result.error);
378
- }
379
- } catch (err) {
380
- if (err instanceof Error && err.name === "AbortError") {
381
- // Cancellation preserves partial state on AxleAbortError: reason, turn, partial, usage
382
- console.log("Cancelled");
383
- } else {
384
- throw err;
385
- }
386
- }
387
- ```
388
-
389
- `AgentEvent` types: `session:restore`, `turn:user`, `turn:start`, `turn:end`,
390
- `part:start`, `part:end`, `text:delta`, `thinking:delta`, `action:args-delta`,
391
- `action:running`, `action:progress`, `action:complete`, `action:error`,
392
- `action:child-event`, `error`.
393
-
394
- `part:start` carries a `TurnPart`, discriminated by `part.type` (`"text"`,
395
- `"thinking"`, `"file"`, `"action"`). Action parts further discriminate on
396
- `part.kind` (`"tool" | "agent" | "provider-tool"`).
397
-
398
- Callbacks are registered once and fire on every subsequent `send()`.
399
-
400
- #### stream() events
401
-
402
- The low-level `stream()` primitive emits a different event shape — closer
403
- to the raw provider stream, with separate `start`/`end` events for each
404
- text and thinking block, and distinct events for tool request, execution,
405
- and completion.
406
-
407
- `StreamEvent` types: `text:start`, `text:delta`, `text:end`,
408
- `thinking:start`, `thinking:delta`, `thinking:end`, `tool:request`,
409
- `tool:exec-start`, `tool:exec-delta`, `tool:exec-complete`,
410
- `provider-tool:start`, `provider-tool:complete`, `turn:complete`,
411
- `tool-results:start`, `tool-results:complete`, `error`.
412
-
413
- The `turn:complete` and `tool-results:complete` events carry complete
414
- `AxleAssistantMessage` and `AxleToolCallMessage` objects for client-server
415
- architectures that need authoritative message boundaries.
416
-
417
- ### Hosting / Sessions
418
-
419
- Axle stops at the agent runtime boundary. If you need long-lived sessions,
420
- SSE transport, resumable cursors, or React client hooks, build those concerns
421
- in your host application on top of `Agent`, `agent.on(...)`, and the streamed
422
- turn events that Axle emits.
423
-
424
- ## Known Limitations
425
-
426
- 1. Axle does not support multi-modal output right now.
427
-
428
- ## CLI
429
-
430
- In accordance to Axle's lineage of a workflow tool, Axle exposes a command
431
- line interface that accepts a declarative config file.
432
-
433
- ### Installation
434
-
435
- ```bash
436
- npm install -g @fifthrevision/axle
437
- ```
438
-
439
- ### Usage
440
-
441
- The CLI looks for `axle.job.yaml` and `axle.config.yaml` in the current
442
- directory by default. You can also specify them using the `-j` and `-c` flags
443
-
444
- ```bash
445
- axle
446
- axle -j path/to/job.yaml -c path/to/config.yaml
447
- axle --args key=value other=thing
448
- axle --debug
449
- ```
450
-
451
- A job file specifies the provider, task prompt, and optional tools/files:
452
-
453
- ```yaml
454
- # axle.job.yaml
455
- provider:
456
- type: anthropic
457
- model: claude-sonnet-4-5-20250929
458
-
459
- task: |
460
- Summarize the attached document.
461
-
462
- tools:
463
- - calculator
464
-
465
- provider_tools:
466
- - web_search
467
-
468
- files:
469
- - ./data/report.txt
470
- ```
471
-
472
- ### Batch
473
-
474
- Add a `batch` key to the job file to run the same task across multiple files.
475
- Each matched file is attached to the instruct automatically.
476
-
477
- ```yaml
478
- # axle.job.yaml
479
- provider:
480
- type: openai
481
-
482
- task: |
483
- Summarize this file.
484
-
485
- batch:
486
- files: "./data/*.txt"
487
- concurrency: 3
488
- resume: true
489
- ```
490
-
491
- - `files` — glob pattern for input files
492
- - `concurrency` — max parallel runs (default 3)
493
- - `resume` — skip files already processed in a previous run
494
-
495
- ### MCP Servers
496
-
497
- Add an `mcps` key to connect to MCP servers. Both stdio and HTTP transports
498
- are supported.
499
-
500
- ```yaml
501
- # axle.job.yaml
502
- provider:
503
- type: anthropic
504
-
505
- mcps:
506
- - name: wc
507
- transport: stdio
508
- command: npx
509
- args: ["tsx", "examples/mcps/wordcount-server.ts"]
510
- - transport: http
511
- url: http://localhost:3100/mcp
512
-
513
- task: |
514
- Count the words in "hello world"
515
- ```
516
-
517
- Each entry supports:
518
-
519
- - `transport` — `"stdio"` or `"http"` (required)
520
- - `name` — prefix for tool names from this server (optional)
521
- - `command` / `args` / `env` — for stdio transport
522
- - `url` / `headers` — for HTTP transport
523
-
524
- ### Configuration
525
-
526
- For CLI use, create an `axle.config.yaml` in your working directory with API
527
- keys:
528
-
529
- ```yaml
530
- # axle.config.yaml
531
- openai:
532
- api-key: "<api-key>"
533
- anthropic:
534
- api-key: "<api-key>"
535
- gemini:
536
- api-key: "<api-key>"
537
- chatcompletions:
538
- base-url: "http://localhost:11434/v1"
539
- model: "llama3"
540
- api-key: "<api-key>" # optional
541
- ```
542
-
543
- Provider-level keys in the job file override the config file.