@fifthrevision/axle 0.19.0 → 0.21.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,621 +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 `TurnEvent` — 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 `TurnEvent`s.
344
-
345
- #### Turn 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
- `TurnEvent` 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`, `annotation:start`, `annotation:update`,
393
- `annotation:end`, `error`.
394
-
395
- `part:start` carries a `TurnPart`, discriminated by `part.type` (`"text"`,
396
- `"thinking"`, `"file"`, `"action"`). Action parts further discriminate on
397
- `part.kind` (`"tool" | "agent" | "provider-tool"`).
398
-
399
- Callbacks are registered once and fire on every subsequent `send()`.
400
-
401
- #### Turn accumulator
402
-
403
- `Turn` objects are accumulated render state. They are the snapshot counterpart
404
- to `TurnEvent` streams: text deltas are folded into text parts, tool call
405
- lifecycles become stable action parts, and tool results are collapsed back into
406
- the action part that produced them. `AxleMessage[]` remains the canonical model
407
- conversation state; turns do not affect model input or tool routing.
408
-
409
- Hosts that transport Axle events over SSE, WebSockets, or another mixed event
410
- stream can use `TurnAccumulator` instead of reimplementing this reducer:
411
-
412
- ```typescript
413
- import { TurnAccumulator, type Annotation } from "@fifthrevision/axle/ui";
414
-
415
- type AppAnnotation =
416
- | Annotation<{ image: string }, "sandbox">
417
- | Annotation<{ score: number; passed: boolean }, "eval">;
418
-
419
- type HostEvent = { type: "run:terminal"; status: string };
420
-
421
- const accumulator = new TurnAccumulator<AppAnnotation, HostEvent>();
422
-
423
- for await (const event of events) {
424
- const { handled, state } = accumulator.apply(event);
425
-
426
- if (!handled) {
427
- // event is typed as HostEvent here
428
- applyHostEvent(event);
429
- }
430
-
431
- render(state.turns);
432
- }
433
- ```
434
-
435
- Use `@fifthrevision/axle/ui` for browser-safe presentation primitives. It
436
- exports turns, annotations, turn events, and `TurnAccumulator` without importing
437
- providers, MCP, tools, or other server-side runtime code.
438
-
439
- The accumulator accepts open event objects. Unknown host events, such as
440
- `run:terminal` or `session:expired`, return `handled: false` and leave the
441
- state unchanged. Session-level annotations are accumulated in
442
- `state.sessionAnnotations`; turn and part annotations are embedded on their
443
- targets. The accumulator is not idempotent; callers should deduplicate replayed
444
- transport events before applying them.
445
-
446
- #### Annotations
447
-
448
- Annotations are embedded render metadata for sessions, turns, and parts. They
449
- are useful for out-of-band UI such as sandbox startup, eval results, deployment
450
- state, or any other consumer-owned status that should render alongside turns
451
- without becoming model state.
452
-
453
- ```typescript
454
- type EvalAnnotation = Annotation<{ score: number; passed: boolean }, "eval">;
455
-
456
- const annotation: EvalAnnotation = {
457
- id: crypto.randomUUID(),
458
- kind: "eval",
459
- label: "Plan adherence",
460
- placement: "after",
461
- status: "complete",
462
- data: { score: 0.92, passed: true },
463
- };
464
-
465
- agentEventSink({
466
- type: "annotation:end",
467
- target: { type: "turn", turnId },
468
- annotation,
469
- });
470
- ```
471
-
472
- Annotation `label` is required so generic renderers have a common UI surface.
473
- `placement` defaults to `"after"`, and `annotation:end` defaults missing
474
- `status` to `"complete"` in accumulated state. `annotation:update` and
475
- `annotation:end` carry the full updated annotation object; Axle does not define
476
- patch or merge semantics for annotation data.
477
-
478
- #### stream() events
479
-
480
- The low-level `stream()` primitive emits a different event shape — closer
481
- to the raw provider stream, with separate `start`/`end` events for each
482
- text and thinking block, and distinct events for tool request, execution,
483
- and completion.
484
-
485
- `StreamEvent` types: `text:start`, `text:delta`, `text:end`,
486
- `thinking:start`, `thinking:delta`, `thinking:end`, `tool:request`,
487
- `tool:exec-start`, `tool:exec-delta`, `tool:exec-complete`,
488
- `provider-tool:start`, `provider-tool:complete`, `turn:complete`,
489
- `tool-results:start`, `tool-results:complete`, `error`.
490
-
491
- The `turn:complete` and `tool-results:complete` events carry complete
492
- `AxleAssistantMessage` and `AxleToolCallMessage` objects for client-server
493
- architectures that need authoritative message boundaries.
494
-
495
- ### Hosting / Sessions
496
-
497
- Axle stops at the agent runtime boundary. If you need long-lived sessions,
498
- SSE transport, resumable cursors, or React client hooks, build those concerns
499
- in your host application on top of `Agent`, `agent.on(...)`, and the streamed
500
- turn events that Axle emits.
501
-
502
- ## Known Limitations
503
-
504
- 1. Axle does not support multi-modal output right now.
505
-
506
- ## CLI
507
-
508
- In accordance to Axle's lineage of a workflow tool, Axle exposes a command
509
- line interface that accepts a declarative config file.
510
-
511
- ### Installation
512
-
513
- ```bash
514
- npm install -g @fifthrevision/axle
515
- ```
516
-
517
- ### Usage
518
-
519
- The CLI looks for `axle.job.yaml` and `axle.config.yaml` in the current
520
- directory by default. You can also specify them using the `-j` and `-c` flags
521
-
522
- ```bash
523
- axle
524
- axle -j path/to/job.yaml -c path/to/config.yaml
525
- axle --args key=value other=thing
526
- axle --debug
527
- ```
528
-
529
- A job file specifies the provider, task prompt, and optional tools/files:
530
-
531
- ```yaml
532
- # axle.job.yaml
533
- provider:
534
- type: anthropic
535
- model: claude-sonnet-4-5-20250929
536
-
537
- task: |
538
- Summarize the attached document.
539
-
540
- tools:
541
- - calculator
542
-
543
- provider_tools:
544
- - web_search
545
-
546
- files:
547
- - ./data/report.txt
548
- ```
549
-
550
- ### Batch
551
-
552
- Add a `batch` key to the job file to run the same task across multiple files.
553
- Each matched file is attached to the instruct automatically.
554
-
555
- ```yaml
556
- # axle.job.yaml
557
- provider:
558
- type: openai
559
-
560
- task: |
561
- Summarize this file.
562
-
563
- batch:
564
- files: "./data/*.txt"
565
- concurrency: 3
566
- resume: true
567
- ```
568
-
569
- - `files` — glob pattern for input files
570
- - `concurrency` — max parallel runs (default 3)
571
- - `resume` — skip files already processed in a previous run
572
-
573
- ### MCP Servers
574
-
575
- Add an `mcps` key to connect to MCP servers. Both stdio and HTTP transports
576
- are supported.
577
-
578
- ```yaml
579
- # axle.job.yaml
580
- provider:
581
- type: anthropic
582
-
583
- mcps:
584
- - name: wc
585
- transport: stdio
586
- command: npx
587
- args: ["tsx", "examples/mcps/wordcount-server.ts"]
588
- - transport: http
589
- url: http://localhost:3100/mcp
590
-
591
- task: |
592
- Count the words in "hello world"
593
- ```
594
-
595
- Each entry supports:
596
-
597
- - `transport` — `"stdio"` or `"http"` (required)
598
- - `name` — prefix for tool names from this server (optional)
599
- - `command` / `args` / `env` — for stdio transport
600
- - `url` / `headers` — for HTTP transport
601
-
602
- ### Configuration
603
-
604
- For CLI use, create an `axle.config.yaml` in your working directory with API
605
- keys:
606
-
607
- ```yaml
608
- # axle.config.yaml
609
- openai:
610
- api-key: "<api-key>"
611
- anthropic:
612
- api-key: "<api-key>"
613
- gemini:
614
- api-key: "<api-key>"
615
- chatcompletions:
616
- base-url: "http://localhost:11434/v1"
617
- model: "llama3"
618
- api-key: "<api-key>" # optional
619
- ```
620
-
621
- Provider-level keys in the job file override the config file.