llmz 0.0.13 → 0.0.14

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/dist/tool.d.ts CHANGED
@@ -1,17 +1,194 @@
1
1
  import { TypeOf } from '@bpinternal/zui';
2
2
  import { JSONSchema7 } from 'json-schema';
3
3
  import { ZuiType } from './types.js';
4
+ /**
5
+ * Input parameters passed to tool retry functions.
6
+ *
7
+ * @template I - The input type for the tool
8
+ */
4
9
  type ToolRetryInput<I> = {
10
+ /** The original input that was passed to the tool */
5
11
  input: I;
12
+ /** The current attempt number (starting from 1) */
6
13
  attempt: number;
14
+ /** The error that caused the retry, if any */
7
15
  error?: unknown;
8
16
  };
17
+ /**
18
+ * Function signature for custom tool retry logic.
19
+ *
20
+ * Retry functions receive information about the failed attempt and return a boolean
21
+ * indicating whether the tool should be retried. This allows for sophisticated
22
+ * retry strategies based on error types, attempt counts, or other factors.
23
+ *
24
+ * @template I - The input type for the tool
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const retryLogic: ToolRetryFn<{ url: string }> = ({ attempt, error }) => {
29
+ * // Retry up to 3 times for network errors, but not for auth errors
30
+ * if (attempt >= 3) return false
31
+ * if (error?.message?.includes('401') || error?.message?.includes('403')) return false
32
+ * return error?.message?.includes('network') || error?.message?.includes('timeout')
33
+ * }
34
+ * ```
35
+ */
9
36
  export type ToolRetryFn<I> = (args: ToolRetryInput<I>) => boolean | Promise<boolean>;
37
+ /** @internal Utility type to check if T is an object (but not a function) */
10
38
  type IsObject<T> = T extends object ? (T extends Function ? false : true) : false;
39
+ /** @internal Utility type that makes object types partial, but leaves primitives unchanged */
11
40
  type SmartPartial<T> = IsObject<T> extends true ? Partial<T> : T;
41
+ /**
42
+ * Context information passed to tool handlers during execution.
43
+ *
44
+ * This context provides metadata about the tool call, which can be useful
45
+ * for logging, debugging, or implementing features like call tracking.
46
+ */
12
47
  type ToolCallContext = {
48
+ /** Unique identifier for this specific tool call */
13
49
  callId: string;
14
50
  };
51
+ /**
52
+ * Tool represents a callable function that agents can use to interact with external systems.
53
+ *
54
+ * Tools are the core building blocks of LLMz agents, providing type-safe interfaces between
55
+ * the generated TypeScript code and external APIs, databases, file systems, or any other
56
+ * service your agent needs to interact with.
57
+ *
58
+ * ## Key Features
59
+ * - **Type Safety**: Full TypeScript inference with Zui/Zod schema validation
60
+ * - **Input/Output Validation**: Automatic validation of inputs and outputs against schemas
61
+ * - **Retry Logic**: Built-in retry mechanisms with custom retry functions
62
+ * - **Static Values**: Pre-configure parameters that won't change between calls
63
+ * - **Tool Cloning**: Create variations of existing tools with modified behavior
64
+ * - **TypeScript Generation**: Generate type definitions for LLM context
65
+ * - **Error Handling**: Comprehensive error handling with detailed validation messages
66
+ *
67
+ * ## Basic Usage
68
+ *
69
+ * ### Simple Tool
70
+ * ```typescript
71
+ * const weatherTool = new Tool({
72
+ * name: 'getCurrentWeather',
73
+ * description: 'Gets current weather conditions for a city',
74
+ * input: z.object({
75
+ * city: z.string().describe('City name'),
76
+ * units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
77
+ * }),
78
+ * output: z.object({
79
+ * temperature: z.number(),
80
+ * condition: z.string(),
81
+ * humidity: z.number(),
82
+ * }),
83
+ * async handler({ city, units }) {
84
+ * const response = await fetch(`/api/weather?city=${city}&units=${units}`)
85
+ * return response.json()
86
+ * },
87
+ * })
88
+ * ```
89
+ *
90
+ * ### Using Tools in Execute
91
+ * ```typescript
92
+ * const result = await execute({
93
+ * instructions: 'Get the weather for San Francisco and recommend clothing',
94
+ * tools: [weatherTool],
95
+ * client,
96
+ * })
97
+ * ```
98
+ *
99
+ * ## Advanced Patterns
100
+ *
101
+ * ### Static Input Values
102
+ * Pre-configure parameters that shouldn't change:
103
+ * ```typescript
104
+ * const restrictedWeatherTool = weatherTool.setStaticInputValues({
105
+ * units: 'celsius' // Always use celsius, agent can't override
106
+ * })
107
+ * ```
108
+ *
109
+ * ### Retry Logic
110
+ * Add resilience with custom retry logic:
111
+ * ```typescript
112
+ * const resilientTool = new Tool({
113
+ * name: 'apiCall',
114
+ * // ... other properties
115
+ * async handler({ endpoint }) {
116
+ * const response = await fetch(endpoint)
117
+ * if (!response.ok) throw new Error(`API error: ${response.status}`)
118
+ * return response.json()
119
+ * },
120
+ * retry: ({ attempt, error }) => {
121
+ * // Retry up to 3 times for network errors, but not auth errors
122
+ * if (attempt >= 3) return false
123
+ * if (error.message.includes('401') || error.message.includes('403')) return false
124
+ * return true
125
+ * },
126
+ * })
127
+ * ```
128
+ *
129
+ * ### Tool Cloning
130
+ * Create variations of existing tools:
131
+ * ```typescript
132
+ * const enhancedWeatherTool = weatherTool.clone({
133
+ * name: 'getEnhancedWeather',
134
+ * description: 'Gets weather with additional forecasting',
135
+ * output: (original) => original!.extend({
136
+ * forecast: z.array(z.object({
137
+ * day: z.string(),
138
+ * temperature: z.number(),
139
+ * condition: z.string(),
140
+ * })),
141
+ * }),
142
+ * async handler(input) {
143
+ * const basicWeather = await weatherTool.execute(input, ctx)
144
+ * const forecast = await getForecast(input.city)
145
+ * return { ...basicWeather, forecast }
146
+ * },
147
+ * })
148
+ * ```
149
+ *
150
+ * ## Schema Design Best Practices
151
+ *
152
+ * ### Rich Descriptions
153
+ * Use detailed descriptions to help the LLM understand when and how to use tools:
154
+ * ```typescript
155
+ * input: z.object({
156
+ * query: z.string()
157
+ * .min(1)
158
+ * .max(500)
159
+ * .describe('Search query - be specific and include relevant keywords'),
160
+ * maxResults: z.number()
161
+ * .min(1)
162
+ * .max(50)
163
+ * .default(10)
164
+ * .describe('Maximum number of results to return (1-50, default: 10)'),
165
+ * includeSnippets: z.boolean()
166
+ * .default(true)
167
+ * .describe('Whether to include content snippets in results'),
168
+ * })
169
+ * ```
170
+ *
171
+ * ### Enums for Controlled Values
172
+ * Use enums to restrict inputs to valid options:
173
+ * ```typescript
174
+ * input: z.object({
175
+ * operation: z.enum(['create', 'read', 'update', 'delete'])
176
+ * .describe('CRUD operation to perform'),
177
+ * format: z.enum(['json', 'csv', 'xml'])
178
+ * .default('json')
179
+ * .describe('Output format for the response'),
180
+ * })
181
+ * ```
182
+ *
183
+ * ## Error Handling
184
+ *
185
+ * Tools automatically handle:
186
+ * - **Input validation**: Invalid inputs throw descriptive errors
187
+ * - **Output validation**: Outputs are validated but invalid outputs are still returned
188
+ * - **Handler errors**: Exceptions in handlers are caught and can trigger retries
189
+ * - **Type coercion**: Basic type coercion where possible
190
+ *
191
+ */
15
192
  export declare class Tool<I extends ZuiType = ZuiType, O extends ZuiType = ZuiType> {
16
193
  private _staticInputValues?;
17
194
  name: string;
@@ -22,10 +199,136 @@ export declare class Tool<I extends ZuiType = ZuiType, O extends ZuiType = ZuiTy
22
199
  output?: JSONSchema7;
23
200
  retry?: ToolRetryFn<TypeOf<I>>;
24
201
  MAX_RETRIES: number;
202
+ /**
203
+ * Sets static input values that will be automatically applied to all tool calls.
204
+ *
205
+ * Static values allow you to pre-configure certain parameters that shouldn't change
206
+ * between calls, effectively removing them from the LLM's control while ensuring
207
+ * they're always present when the tool executes.
208
+ *
209
+ * @param values - Partial input values to apply statically. Set to null/undefined to clear.
210
+ * @returns The tool instance for method chaining
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * // Create a tool with configurable parameters
215
+ * const searchTool = new Tool({
216
+ * name: 'search',
217
+ * input: z.object({
218
+ * query: z.string(),
219
+ * maxResults: z.number().default(10),
220
+ * includeSnippets: z.boolean().default(true),
221
+ * }),
222
+ * handler: async ({ query, maxResults, includeSnippets }) => {
223
+ * // Implementation
224
+ * },
225
+ * })
226
+ *
227
+ * // Create a restricted version with static values
228
+ * const restrictedSearch = searchTool.setStaticInputValues({
229
+ * maxResults: 5, // Always limit to 5 results
230
+ * includeSnippets: false, // Never include snippets
231
+ * })
232
+ *
233
+ * // LLM can only control 'query' parameter now
234
+ * // The tool will always use maxResults: 5, includeSnippets: false
235
+ * ```
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * // Clear static values
240
+ * const unrestricted = restrictedTool.setStaticInputValues(null)
241
+ * ```
242
+ */
25
243
  setStaticInputValues(values: SmartPartial<TypeOf<I>>): this;
244
+ /**
245
+ * Gets the computed input schema with static values applied.
246
+ *
247
+ * This property returns the final input schema that will be used for validation,
248
+ * including any static input values that have been set via setStaticInputValues().
249
+ *
250
+ * @returns The Zui schema for input validation
251
+ * @internal
252
+ */
26
253
  get zInput(): import("@bpinternal/zui").ZodTypeAny;
254
+ /**
255
+ * Gets the output schema for validation.
256
+ *
257
+ * @returns The Zui schema for output validation
258
+ * @internal
259
+ */
27
260
  get zOutput(): import("@bpinternal/zui").ZodTypeAny | import("@bpinternal/zui").ZodVoid;
261
+ /**
262
+ * Renames the tool and updates its aliases.
263
+ *
264
+ * @param name - New name for the tool (must be a valid identifier)
265
+ * @returns The tool instance for method chaining
266
+ * @throws Error if the name is not a valid identifier
267
+ *
268
+ * @example
269
+ * ```typescript
270
+ * const weatherTool = new Tool({ name: 'getWeather', ... })
271
+ * weatherTool.rename('getCurrentWeather')
272
+ * console.log(weatherTool.name) // 'getCurrentWeather'
273
+ * ```
274
+ */
28
275
  rename(name: string): this;
276
+ /**
277
+ * Creates a new tool based on this one with modified properties.
278
+ *
279
+ * Clone allows you to create variations of existing tools with different names,
280
+ * schemas, handlers, or other configuration. This is useful for creating specialized
281
+ * versions of tools or adding additional functionality.
282
+ *
283
+ * @param props - Properties to override in the cloned tool
284
+ * @returns A new Tool instance with the specified modifications
285
+ * @throws Error if cloning fails due to invalid configuration
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * // Create a basic search tool
290
+ * const basicSearch = new Tool({
291
+ * name: 'search',
292
+ * input: z.object({ query: z.string() }),
293
+ * output: z.object({ results: z.array(z.string()) }),
294
+ * handler: async ({ query }) => ({ results: await search(query) }),
295
+ * })
296
+ *
297
+ * // Clone with enhanced output schema
298
+ * const enhancedSearch = basicSearch.clone({
299
+ * name: 'enhancedSearch',
300
+ * description: 'Search with additional metadata',
301
+ * output: (original) => original!.extend({
302
+ * totalResults: z.number(),
303
+ * searchTime: z.number(),
304
+ * }),
305
+ * handler: async ({ query }) => {
306
+ * const startTime = Date.now()
307
+ * const results = await search(query)
308
+ * return {
309
+ * results: results.items,
310
+ * totalResults: results.total,
311
+ * searchTime: Date.now() - startTime,
312
+ * }
313
+ * },
314
+ * })
315
+ * ```
316
+ *
317
+ * @example
318
+ * ```typescript
319
+ * // Clone with restricted access
320
+ * const restrictedTool = adminTool.clone({
321
+ * name: 'userTool',
322
+ * handler: async (input, ctx) => {
323
+ * // Add authorization check
324
+ * if (!isAuthorized(ctx.callId)) {
325
+ * throw new Error('Unauthorized access')
326
+ * }
327
+ * return adminTool.execute(input, ctx)
328
+ * },
329
+ * })
330
+ * ```
331
+ */
29
332
  clone<IX extends ZuiType = I, OX extends ZuiType = O>(props?: Partial<{
30
333
  name: string;
31
334
  aliases?: string[];
@@ -38,6 +341,82 @@ export declare class Tool<I extends ZuiType = ZuiType, O extends ZuiType = ZuiTy
38
341
  retry: ToolRetryFn<TypeOf<IX>>;
39
342
  }>): Tool<IX, OX>;
40
343
  private _handler;
344
+ /**
345
+ * Creates a new Tool instance.
346
+ *
347
+ * @param props - Tool configuration properties
348
+ * @param props.name - Unique tool name (must be valid TypeScript identifier)
349
+ * @param props.description - Human-readable description for the LLM
350
+ * @param props.input - Zui/Zod schema for input validation (optional)
351
+ * @param props.output - Zui/Zod schema for output validation (optional)
352
+ * @param props.handler - Async function that implements the tool logic
353
+ * @param props.aliases - Alternative names for the tool (optional)
354
+ * @param props.metadata - Additional metadata for the tool (optional)
355
+ * @param props.staticInputValues - Default input values (optional)
356
+ * @param props.retry - Custom retry logic function (optional)
357
+ *
358
+ * @throws Error if name is not a valid identifier
359
+ * @throws Error if description is not a string
360
+ * @throws Error if metadata is not an object
361
+ * @throws Error if handler is not a function
362
+ * @throws Error if aliases contains invalid identifiers
363
+ * @throws Error if input/output schemas are invalid
364
+ *
365
+ * @example
366
+ * ```typescript
367
+ * const weatherTool = new Tool({
368
+ * name: 'getCurrentWeather',
369
+ * description: 'Fetches current weather data for a given city',
370
+ * aliases: ['weather', 'getWeather'], // Alternative names
371
+ *
372
+ * input: z.object({
373
+ * city: z.string().min(1).describe('City name to get weather for'),
374
+ * units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
375
+ * includeHourly: z.boolean().default(false),
376
+ * }),
377
+ *
378
+ * output: z.object({
379
+ * temperature: z.number(),
380
+ * description: z.string(),
381
+ * humidity: z.number().min(0).max(100),
382
+ * hourlyForecast: z.array(z.object({
383
+ * hour: z.number(),
384
+ * temp: z.number(),
385
+ * condition: z.string(),
386
+ * })).optional(),
387
+ * }),
388
+ *
389
+ * async handler({ city, units, includeHourly }) {
390
+ * const response = await fetch(`/api/weather?city=${encodeURIComponent(city)}&units=${units}`)
391
+ * const data = await response.json()
392
+ *
393
+ * const result = {
394
+ * temperature: data.main.temp,
395
+ * description: data.weather[0].description,
396
+ * humidity: data.main.humidity,
397
+ * }
398
+ *
399
+ * if (includeHourly) {
400
+ * result.hourlyForecast = data.hourly?.slice(0, 24) || []
401
+ * }
402
+ *
403
+ * return result
404
+ * },
405
+ *
406
+ * // Optional: Add retry logic for network errors
407
+ * retry: ({ attempt, error }) => {
408
+ * return attempt < 3 && error.message.includes('network')
409
+ * },
410
+ *
411
+ * // Optional: Add metadata
412
+ * metadata: {
413
+ * category: 'weather',
414
+ * rateLimit: 100,
415
+ * cacheable: true,
416
+ * },
417
+ * })
418
+ * ```
419
+ */
41
420
  constructor(props: {
42
421
  name: string;
43
422
  aliases?: string[];
@@ -49,8 +428,70 @@ export declare class Tool<I extends ZuiType = ZuiType, O extends ZuiType = ZuiTy
49
428
  handler: (args: TypeOf<I>, ctx: ToolCallContext) => Promise<TypeOf<O>>;
50
429
  retry?: ToolRetryFn<TypeOf<I>>;
51
430
  });
431
+ /**
432
+ * Executes the tool with the given input and context.
433
+ *
434
+ * This method handles input validation, retry logic, output validation, and error handling.
435
+ * It's called internally by the LLMz execution engine when generated code calls the tool.
436
+ *
437
+ * @param input - Input data to pass to the tool handler
438
+ * @param ctx - Tool call context containing call ID and other metadata
439
+ * @returns Promise resolving to the tool's output
440
+ * @throws Error if input validation fails
441
+ * @throws Error if tool execution fails after all retries
442
+ *
443
+ * @example
444
+ * ```typescript
445
+ * // Direct tool execution (usually done by LLMz internally)
446
+ * const result = await weatherTool.execute(
447
+ * { city: 'San Francisco', units: 'fahrenheit' },
448
+ * { callId: 'call_123' }
449
+ * )
450
+ * console.log(result.temperature) // 72
451
+ * ```
452
+ *
453
+ * @internal This method is primarily used internally by the LLMz execution engine
454
+ */
52
455
  execute(input: TypeOf<I>, ctx: ToolCallContext): Promise<TypeOf<O>>;
456
+ /**
457
+ * Generates TypeScript type definitions for this tool.
458
+ *
459
+ * This method creates TypeScript function declarations that are included in the
460
+ * LLM's context to help it understand how to call the tool correctly. The generated
461
+ * types include parameter types, return types, and JSDoc comments with descriptions.
462
+ *
463
+ * @returns Promise resolving to TypeScript declaration string
464
+ */
53
465
  getTypings(): Promise<string>;
466
+ /**
467
+ * Ensures all tools in an array have unique names by appending numbers to duplicates.
468
+ *
469
+ * When multiple tools have the same name, this method renames them by appending
470
+ * incrementing numbers (tool1, tool2, etc.) to avoid conflicts. Tools with already
471
+ * unique names are left unchanged.
472
+ *
473
+ * You usually don't need to call this method directly, as LLMz will automatically detect and rename tools with conflicting names.
474
+ *
475
+ * @param tools - Array of tools to process for unique names
476
+ * @returns Array of tools with guaranteed unique names
477
+ *
478
+ * @example
479
+ * ```typescript
480
+ * const tools = [
481
+ * new Tool({ name: 'search', handler: async () => {} }),
482
+ * new Tool({ name: 'search', handler: async () => {} }),
483
+ * new Tool({ name: 'unique', handler: async () => {} }),
484
+ * new Tool({ name: 'search', handler: async () => {} }),
485
+ * ]
486
+ *
487
+ * const uniqueTools = Tool.withUniqueNames(tools)
488
+ * console.log(uniqueTools.map(t => t.name))
489
+ * // Output: ['search1', 'search1', 'unique', 'search']
490
+ * // Note: The last occurrence keeps the original name
491
+ * ```
492
+ *
493
+ * @static
494
+ */
54
495
  static withUniqueNames: (tools: Tool<any, any>[]) => Tool<any, any>[];
55
496
  }
56
497
  export {};