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 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