toolwire 0.1.1
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/LICENSE +21 -0
- package/README.md +443 -0
- package/dist/index.cjs +719 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +325 -0
- package/dist/index.d.ts +325 -0
- package/dist/index.js +674 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 toolwire contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# toolwire
|
|
2
|
+
|
|
3
|
+
Framework-agnostic tool registry for LLM agents.
|
|
4
|
+
|
|
5
|
+
Define tools **once** with Zod schemas. Get input validation, structured error messages the LLM can act on, and one-line schema export to OpenAI, Anthropic, Gemini, or Vercel AI — with zero runtime dependencies.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install toolwire zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## The problem
|
|
14
|
+
|
|
15
|
+
Every team building an LLM agent writes the same three pieces of boilerplate:
|
|
16
|
+
|
|
17
|
+
1. A JSON schema for each tool
|
|
18
|
+
2. Validation of the LLM's arguments before calling the function
|
|
19
|
+
3. An error message the LLM can understand and retry
|
|
20
|
+
|
|
21
|
+
And they do it differently every time, for every framework, in every project. `toolwire` is the standard.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { tool, registry } from 'toolwire';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
|
|
31
|
+
// 1. Define a tool
|
|
32
|
+
const searchWeb = tool({
|
|
33
|
+
name: 'search_web',
|
|
34
|
+
description: 'Search the web for current information',
|
|
35
|
+
input: z.object({
|
|
36
|
+
query: z.string().min(1).describe('The search query'),
|
|
37
|
+
maxResults: z.number().int().min(1).max(20).default(5),
|
|
38
|
+
}),
|
|
39
|
+
handler: async ({ query, maxResults }) => {
|
|
40
|
+
return await mySearchAPI(query, maxResults);
|
|
41
|
+
},
|
|
42
|
+
timeout: 10_000,
|
|
43
|
+
retries: 2,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 2. Create a registry
|
|
47
|
+
const reg = registry([searchWeb, readFile, writeFile]);
|
|
48
|
+
|
|
49
|
+
// 3. Use with any LLM provider
|
|
50
|
+
const openaiTools = reg.toOpenAI();
|
|
51
|
+
const anthropicTools = reg.toAnthropic();
|
|
52
|
+
|
|
53
|
+
// 4. Execute a tool call — always resolves, never throws
|
|
54
|
+
const result = await reg.call(llmToolCall);
|
|
55
|
+
|
|
56
|
+
if (result.success) {
|
|
57
|
+
// result.data is the validated return value
|
|
58
|
+
messages.push({ role: 'tool', content: JSON.stringify(result.data) });
|
|
59
|
+
} else {
|
|
60
|
+
// result.error.llmMessage is pre-formatted for the LLM to retry
|
|
61
|
+
messages.push({ role: 'tool', content: result.error.llmMessage });
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
- **Type-safe** — Zod schemas infer TypeScript types end-to-end
|
|
70
|
+
- **Input + output validation** — validate arguments in, validate results out
|
|
71
|
+
- **LLM-readable errors** — every failure includes a `llmMessage` ready to append to messages
|
|
72
|
+
- **Four provider adapters** — OpenAI, Anthropic, Gemini, Vercel AI SDK
|
|
73
|
+
- **Middleware** — hook into beforeCall / afterCall / onError for logging, auth, caching
|
|
74
|
+
- **Hot-swap** — replace a tool at runtime without restarting the agent
|
|
75
|
+
- **Timeout + retries** — configurable per-tool, with exponential backoff
|
|
76
|
+
- **Runtime discovery** — load tools from a directory or remote manifest
|
|
77
|
+
- **Zero runtime dependencies** — only Zod (peer dep) required
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## API
|
|
82
|
+
|
|
83
|
+
### `tool(config)`
|
|
84
|
+
|
|
85
|
+
Define a tool. Returns a frozen `ToolDefinition` with pre-computed JSON Schema.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const myTool = tool({
|
|
89
|
+
name: 'my_tool', // 1–64 chars: letters, digits, _ or -
|
|
90
|
+
description: string, // shown to the LLM — explain when to use this tool
|
|
91
|
+
input: ZodSchema, // validates LLM arguments
|
|
92
|
+
output?: ZodSchema, // optional — validates handler return value
|
|
93
|
+
handler: async (input, context) => { ... },
|
|
94
|
+
timeout?: number, // ms, default 30_000
|
|
95
|
+
retries?: number, // additional attempts on execution failure, default 0
|
|
96
|
+
annotations?: { // informational hints (not enforced)
|
|
97
|
+
readOnly?: boolean,
|
|
98
|
+
destructive?: boolean,
|
|
99
|
+
expensive?: boolean,
|
|
100
|
+
requiresConfirmation?: boolean,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The `context` object passed to the handler:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
interface ToolContext {
|
|
109
|
+
signal: AbortSignal; // tied to the timeout — honour this for cooperative cancellation
|
|
110
|
+
attempt: number; // 0 = first try, 1 = first retry, …
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### `registry(tools, options?)`
|
|
117
|
+
|
|
118
|
+
Create a registry from an array of tool definitions.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const reg = registry([searchWeb, readFile, writeFile], {
|
|
122
|
+
defaultTimeout: 15_000, // fallback timeout for tools that don't set their own
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### `reg.call(request)`
|
|
129
|
+
|
|
130
|
+
Execute a tool call from an LLM. Always resolves — **never throws**.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const result = await reg.call({
|
|
134
|
+
name: 'search_web',
|
|
135
|
+
arguments: { query: 'TypeScript tips', maxResults: 5 },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ToolResult is a discriminated union
|
|
139
|
+
if (result.success) {
|
|
140
|
+
console.log(result.data); // validated return value
|
|
141
|
+
console.log(result.durationMs); // wall time in ms
|
|
142
|
+
} else {
|
|
143
|
+
console.log(result.error.code); // error category
|
|
144
|
+
console.log(result.error.message); // developer-readable message
|
|
145
|
+
console.log(result.error.llmMessage); // ready to send back to the LLM
|
|
146
|
+
console.log(result.error.retryable); // should the LLM retry?
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Error codes:**
|
|
151
|
+
|
|
152
|
+
| Code | When | Retryable |
|
|
153
|
+
|------|------|-----------|
|
|
154
|
+
| `NOT_FOUND` | Tool name not registered | ✓ |
|
|
155
|
+
| `DISABLED` | Tool is currently disabled | ✗ |
|
|
156
|
+
| `VALIDATION_INPUT` | Arguments fail Zod schema | ✓ |
|
|
157
|
+
| `VALIDATION_OUTPUT` | Return value fails output schema | ✗ |
|
|
158
|
+
| `TIMEOUT` | Handler exceeded timeout | ✓ |
|
|
159
|
+
| `EXECUTION` | Handler threw (all retries exhausted) | ✗ |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### Provider adapters
|
|
164
|
+
|
|
165
|
+
Export tool schemas in whatever format your LLM provider expects. All adapters exclude disabled tools.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// OpenAI function-calling
|
|
169
|
+
await openai.chat.completions.create({
|
|
170
|
+
model: 'gpt-4o',
|
|
171
|
+
tools: reg.toOpenAI(),
|
|
172
|
+
// or with strict mode:
|
|
173
|
+
tools: reg.toOpenAI({ strict: true }),
|
|
174
|
+
messages,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Anthropic tool-use
|
|
178
|
+
await anthropic.messages.create({
|
|
179
|
+
model: 'claude-opus-4-6',
|
|
180
|
+
tools: reg.toAnthropic(), // uses input_schema key
|
|
181
|
+
messages,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Google Gemini
|
|
185
|
+
await model.generateContent({
|
|
186
|
+
tools: [reg.toGemini()], // wraps in functionDeclarations
|
|
187
|
+
contents,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Vercel AI SDK
|
|
191
|
+
const { text } = await generateText({
|
|
192
|
+
model: openai('gpt-4o'),
|
|
193
|
+
tools: reg.toVercelAI(), // passes Zod schemas directly
|
|
194
|
+
prompt,
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Standalone adapter functions are also exported for use outside a registry:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { toOpenAI, toAnthropic, toGemini, toVercelAI } from 'toolwire';
|
|
202
|
+
|
|
203
|
+
const schemas = toOpenAI([searchWeb, readFile]);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### Middleware
|
|
209
|
+
|
|
210
|
+
Add hooks for logging, authentication, caching, or tracing.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
reg.use({
|
|
214
|
+
name: 'logger', // optional — used in error messages
|
|
215
|
+
|
|
216
|
+
// Runs before execution, in registration order
|
|
217
|
+
// Return a value to transform the arguments, or void to keep them
|
|
218
|
+
beforeCall: (toolName, args) => {
|
|
219
|
+
console.log(`→ ${toolName}`, args);
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
// Runs after success, in reverse registration order
|
|
223
|
+
// Return a ToolSuccess to transform the result, or void to keep it
|
|
224
|
+
afterCall: (toolName, args, result) => {
|
|
225
|
+
console.log(`← ${toolName} (${result.durationMs}ms)`);
|
|
226
|
+
tracer.record(toolName, result.data);
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// Runs on any failure
|
|
230
|
+
// Return a ToolResult to recover from the error, or void to propagate it
|
|
231
|
+
onError: (toolName, args, failure) => {
|
|
232
|
+
alerting.send(toolName, failure.error);
|
|
233
|
+
// return a ToolResult here to recover, or return nothing to propagate
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Multiple middleware are chained — `beforeCall` runs in order, `afterCall` in reverse (stack-style):
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
reg
|
|
242
|
+
.use({ name: 'auth', beforeCall: checkAuth })
|
|
243
|
+
.use({ name: 'cache', beforeCall: checkCache, afterCall: writeCache })
|
|
244
|
+
.use({ name: 'metrics', afterCall: recordMetrics });
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### Hot-swapping tools
|
|
250
|
+
|
|
251
|
+
Replace a registered tool in-place without restarting the agent:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Start with the live implementation
|
|
255
|
+
const reg = registry([searchWeb]);
|
|
256
|
+
|
|
257
|
+
// Mid-run: swap to a cached version
|
|
258
|
+
reg.swap('search_web', cachedSearchWeb);
|
|
259
|
+
|
|
260
|
+
// Disable a tool temporarily (returns DISABLED error if called)
|
|
261
|
+
reg.disable('send_email');
|
|
262
|
+
reg.enable('send_email');
|
|
263
|
+
|
|
264
|
+
// Add new tools at any time
|
|
265
|
+
reg.register(newTool);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### `reg.describe()`
|
|
271
|
+
|
|
272
|
+
Generate a human-readable tool list for injecting into a system prompt:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const systemPrompt = `You have access to the following tools:\n${reg.describe()}`;
|
|
276
|
+
// → "- search_web: Search the web for current information"
|
|
277
|
+
// "- calculate: Evaluate a mathematical expression"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### `ToolRegistry.fromDir(path)`
|
|
283
|
+
|
|
284
|
+
Load tools from a directory of compiled JavaScript files:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
const reg = await ToolRegistry.fromDir('./tools/');
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Each file may export:
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
// Option A: default export
|
|
294
|
+
export default tool({ name: 'my_tool', ... });
|
|
295
|
+
|
|
296
|
+
// Option B: named `tools` array
|
|
297
|
+
export const tools = [tool({ ... }), tool({ ... })];
|
|
298
|
+
|
|
299
|
+
// Option C: any named export that is a ToolDefinition
|
|
300
|
+
export const myTool = tool({ ... });
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Only `.js`, `.mjs`, and `.cjs` files are scanned. Files that fail to import are skipped with a warning.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### `ToolRegistry.fromManifest(url)`
|
|
308
|
+
|
|
309
|
+
Load tools from a remote JSON manifest and proxy calls over HTTP:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const reg = await ToolRegistry.fromManifest('https://tools.mycompany.com/manifest.json');
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Manifest format:
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"version": "1.0",
|
|
320
|
+
"tools": [
|
|
321
|
+
{
|
|
322
|
+
"name": "search_web",
|
|
323
|
+
"description": "Search the web",
|
|
324
|
+
"inputSchema": { "type": "object", "properties": { ... } },
|
|
325
|
+
"endpoint": "https://api.mycompany.com/tools/search"
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## TypeScript
|
|
334
|
+
|
|
335
|
+
`tool()` infers input and output types from your Zod schemas — no explicit generics needed:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
const greet = tool({
|
|
339
|
+
name: 'greet',
|
|
340
|
+
description: 'Greet someone',
|
|
341
|
+
input: z.object({ name: z.string() }),
|
|
342
|
+
output: z.object({ message: z.string() }),
|
|
343
|
+
handler: async ({ name }) => ({ message: `Hello, ${name}!` }),
|
|
344
|
+
// ^^^^ typed as { name: string }
|
|
345
|
+
// ^^^^ typed as { message: string }
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Use the inference helpers to extract types from definitions:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import type { InferInput, InferOutput } from 'toolwire';
|
|
353
|
+
|
|
354
|
+
type GreetInput = InferInput<typeof greet>; // { name: string }
|
|
355
|
+
type GreetOutput = InferOutput<typeof greet>; // { message: string }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Provider format reference
|
|
361
|
+
|
|
362
|
+
| Field | OpenAI | Anthropic | Gemini | Vercel AI |
|
|
363
|
+
|-------|--------|-----------|--------|-----------|
|
|
364
|
+
| Schema key | `parameters` | `input_schema` | `parametersJsonSchema` | Zod schema |
|
|
365
|
+
| Wrapper | `{ type: "function", function: {...} }` | direct object | `{ functionDeclarations: [...] }` | Record by name |
|
|
366
|
+
| Strict mode | `strict?: boolean` | — | — | — |
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Zod v3 support
|
|
371
|
+
|
|
372
|
+
Zod v4 (the default) has built-in JSON Schema generation. For Zod v3 support, install the optional peer:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
npm install zod-to-json-schema
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`toolwire` detects the Zod version automatically and uses the right conversion path.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Complete agent loop example
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
386
|
+
import { registry, tool } from 'toolwire';
|
|
387
|
+
import { z } from 'zod';
|
|
388
|
+
|
|
389
|
+
const searchTool = tool({
|
|
390
|
+
name: 'search_web',
|
|
391
|
+
description: 'Search the web for current information',
|
|
392
|
+
input: z.object({ query: z.string().min(1) }),
|
|
393
|
+
handler: async ({ query }) => ({ results: await mySearch(query) }),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const reg = registry([searchTool]).use({
|
|
397
|
+
beforeCall: (name, args) => console.log(`→ ${name}`, args),
|
|
398
|
+
afterCall: (name, _, r) => console.log(`← ${name} ${r.durationMs}ms`),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const client = new Anthropic();
|
|
402
|
+
const messages: Anthropic.MessageParam[] = [
|
|
403
|
+
{ role: 'user', content: 'What are the latest TypeScript features?' },
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
while (true) {
|
|
407
|
+
const response = await client.messages.create({
|
|
408
|
+
model: 'claude-opus-4-6',
|
|
409
|
+
max_tokens: 1024,
|
|
410
|
+
tools: reg.toAnthropic(),
|
|
411
|
+
messages,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
415
|
+
|
|
416
|
+
if (response.stop_reason === 'end_turn') break;
|
|
417
|
+
|
|
418
|
+
// Process tool calls
|
|
419
|
+
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
420
|
+
for (const block of response.content) {
|
|
421
|
+
if (block.type !== 'tool_use') continue;
|
|
422
|
+
|
|
423
|
+
const result = await reg.call({ name: block.name, arguments: block.input });
|
|
424
|
+
|
|
425
|
+
toolResults.push({
|
|
426
|
+
type: 'tool_result',
|
|
427
|
+
tool_use_id: block.id,
|
|
428
|
+
content: result.success
|
|
429
|
+
? JSON.stringify(result.data)
|
|
430
|
+
: result.error.llmMessage,
|
|
431
|
+
is_error: !result.success,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
messages.push({ role: 'user', content: toolResults });
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## License
|
|
442
|
+
|
|
443
|
+
MIT
|