ai-tool-set 0.1.0-alpha.0 → 0.1.0-alpha.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.
Files changed (2) hide show
  1. package/README.md +189 -140
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -15,10 +15,10 @@ This library provides an immutable and type-safe API to manage [`activeTools`](h
15
15
 
16
16
  The AI SDK provides an `activeTools` parameter to control which tools the model can use at any given time. However, managing tool activation becomes complex when you need to:
17
17
 
18
- - **Activate/deactivate tools**: Some tools should be inactive by default and should only be available after being explicitily activated
18
+ - **Statically activate/deactivate tools**: Some tools should be inactive by default and only available after being explicitly activated
19
19
  - **Dynamically infer tool activation**: Some tools should be activated based on runtime context like the conversation history
20
20
 
21
- This library wraps standard AI SDK `tool()` definitions with activation metadata and provides a chainable API to resolve `tools` and `activeTools` for any AI SDK function.
21
+ This library wraps standard AI SDK `tool()` definitions with chainable activation methods and resolves `tools` and `activeTools` for any AI SDK function.
22
22
 
23
23
  ### Installation
24
24
 
@@ -30,53 +30,35 @@ npm install ai-tool-set
30
30
 
31
31
  ### Creating a Tool Set
32
32
 
33
- Define your tools using the standard AI SDK `tool()` function and wrap them with `createToolSet()`. Each entry is either a plain `Tool` (always active) or a `{ tool, active }` object with explicit activation control.
33
+ Pass a plain record of AI SDK `tool()` definitions to `createToolSet()`. All tools are active by default.
34
34
 
35
35
  ```typescript
36
36
  import { tool } from 'ai';
37
37
  import { z } from 'zod';
38
38
  import { createToolSet } from 'ai-tool-set';
39
39
 
40
- const toolSet = createToolSet({
41
- // Plain tool — always active
42
- search: tool({
43
- description: 'Search for products',
44
- inputSchema: z.object({ query: z.string() }),
45
- execute: async ({ query }) => searchProducts(query),
46
- }),
47
-
48
- // Explicitly inactive — must be activated manually
49
- list_orders: {
50
- tool: tool({
51
- description: 'List orders for a customer',
52
- inputSchema: z.object({ customerId: z.string() }),
53
- execute: async ({ customerId }) => listOrders(customerId),
54
- }),
55
- active: false,
56
- },
40
+ const search = tool({
41
+ description: 'Search for products',
42
+ inputSchema: z.object({ query: z.string() }),
43
+ execute: async ({ query }) => searchProducts(query),
44
+ });
57
45
 
58
- // Callback-based activated dynamically based on messages
59
- // Only available when list_orders was called and returned unfulfilled orders
60
- cancel_order: {
61
- tool: tool({
62
- description: 'Cancel an order',
63
- inputSchema: z.object({ orderId: z.string() }),
64
- execute: async ({ orderId }) => cancelOrder(orderId),
65
- }),
66
- active: ({ messages }) =>
67
- messages.some((m) =>
68
- m.parts.some(
69
- (p) =>
70
- p.type === 'tool-list_orders' &&
71
- p.state === 'output-available' &&
72
- p.output.orders?.some((order) => order.status !== 'fulfilled'),
73
- ),
74
- ),
75
- },
46
+ const list_orders = tool({
47
+ description: 'List orders for a customer',
48
+ inputSchema: z.object({ customerId: z.string() }),
49
+ execute: async ({ customerId }) => listOrders(customerId),
50
+ });
51
+
52
+ const cancel_order = tool({
53
+ description: 'Cancel an order',
54
+ inputSchema: z.object({ orderId: z.string() }),
55
+ execute: async ({ orderId }) => cancelOrder(orderId),
76
56
  });
57
+
58
+ const toolSet = createToolSet({ search, list_orders, cancel_order });
77
59
  ```
78
60
 
79
- ### Use with `generateText` / `streamText` / `ToolLoopAgent`
61
+ ### Use with `generateText` / `streamText`
80
62
 
81
63
  The tool set exposes `tools` and `activeTools` as properties that can be used directly:
82
64
 
@@ -97,13 +79,12 @@ const result = await generateText({
97
79
 
98
80
  ### Activate and Deactivate Tools
99
81
 
100
- Use `activateTools()` and `deactivateTools()` to override static activation. Both methods are type-safe and only accept valid tool names:
82
+ Use `.activate()` and `.deactivate()` to statically control which tools are available:
101
83
 
102
84
  ```typescript
103
- // Active tools will include 'list_orders' and exclude 'search'
104
85
  const { tools, activeTools } = toolSet
105
- .activateTools(['list_orders'])
106
- .deactivateTools(['search']);
86
+ .deactivate(['cancel_order'])
87
+ .activate(['list_orders']);
107
88
 
108
89
  const result = await generateText({
109
90
  model,
@@ -113,9 +94,32 @@ const result = await generateText({
113
94
  });
114
95
  ```
115
96
 
116
- ### Infer Active Tools
97
+ ### Conditional Activation
98
+
99
+ Use `.activateWhen()` and `.deactivateWhen()` to conditionally control tools based on messages and context. The predicate receives an `ActivationInput` with `messages` and `context`.
100
+
101
+ ```typescript
102
+ const { tools, activeTools } = toolSet
103
+ .activateWhen('cancel_order', ({ messages }) =>
104
+ messages.some((m) =>
105
+ m.parts.some(
106
+ (p) =>
107
+ p.type === 'tool-list_orders' &&
108
+ p.state === 'output-available' &&
109
+ p.output.orders?.some((order) => order.status !== 'fulfilled'),
110
+ ),
111
+ ),
112
+ );
113
+
114
+ const result = await generateText({
115
+ model,
116
+ tools,
117
+ activeTools,
118
+ prompt: 'Cancel my order #1001',
119
+ });
120
+ ```
117
121
 
118
- Use `inferTools()` to evaluate callback-based tools against the current conversation context:
122
+ Use `.inferTools()` to evaluate predicates with the current messages and context:
119
123
 
120
124
  ```typescript
121
125
  const messages = [
@@ -125,104 +129,137 @@ const messages = [
125
129
  },
126
130
  {
127
131
  role: 'assistant',
128
- parts: [
129
- {
130
- type: 'tool-list_orders',
131
- state: 'output-available',
132
- toolCallId: 'call-1',
133
- input: { customerId: 'cust-123' },
134
- output: {
135
- orders: [
136
- { orderId: 'order-1', status: 'fulfilled' },
137
- { orderId: 'order-2', status: 'pending' },
138
- ],
139
- },
132
+ parts: [{
133
+ type: 'tool-list_orders',
134
+ state: 'output-available',
135
+ toolCallId: 'call-1',
136
+ input: { customerId: 'cust-123' },
137
+ output: {
138
+ orders: [
139
+ { orderId: '1000', status: 'fulfilled' },
140
+ { orderId: '1001', status: 'pending' },
141
+ ],
140
142
  },
141
- ],
143
+ }],
142
144
  },
143
145
  ];
144
146
 
145
- // Active tools will include 'cancel_order' based on the messages context
146
- const { tools, activeTools } = toolSet.inferTools({ messages });
147
+ // cancel_order is now active because list_orders returned unfulfilled orders
148
+ const { tools, activeTools } = toolSet.inferTools({ messages, context: {} });
147
149
 
148
- const result = await generateText({
149
- model,
150
- tools,
151
- activeTools,
152
- messages,
153
- });
150
+ const result = await generateText({ model, tools, activeTools, messages });
151
+ ```
152
+
153
+ You can also activate multiple tools at once:
154
+
155
+ ```typescript
156
+ const toolSet = createToolSet({ search, list_orders, cancel_order })
157
+ .activateWhen({
158
+ list_orders: ({ context }) => context.isAuthenticated,
159
+ cancel_order: ({ messages }) => hasUnfulfilledOrders(messages),
160
+ });
161
+ ```
162
+
163
+ ### Last-Call Wins
164
+
165
+ Each method appends to an internal list. For each tool, the **last entry** determines its state. This makes ordering explicit and predictable:
166
+
167
+ ```typescript
168
+ const toolSet = createToolSet({ search, list_orders, cancel_order })
169
+ .activate(['cancel_order']) // cancel_order: activated
170
+ .deactivate(['cancel_order']) // cancel_order: deactivated
171
+ .activateWhen('cancel_order', ({ messages }) => // cancel_order: conditional activation
172
+ hasUnfulfilledOrders(messages),
173
+ );
154
174
  ```
155
175
 
156
176
  ### Immutable
157
177
 
158
- All methods return new tool set instances, allowing you to maintain different tool states across different calls or steps without side effects:
178
+ All methods return new tool set instances. The original is never modified:
159
179
 
160
180
  ```typescript
161
- const toolSet1 = createToolSet({ ... });
162
- const toolSet2 = toolSet1.activateTools(['list_orders']);
163
- const toolSet3 = toolSet2.deactivateTools(['search', 'list_orders']);
181
+ const base = createToolSet({ search, list_orders, cancel_order });
182
+ const withoutCancel = base.deactivate(['cancel_order']);
183
+ const withCancel = withoutCancel.activate(['cancel_order']);
164
184
 
165
- toolSet1.activeTools; // ['search']
166
- toolSet2.activeTools; // ['search', 'list_orders']
167
- toolSet3.activeTools; // []
185
+ base.activeTools; // ['search', 'list_orders', 'cancel_order']
186
+ withoutCancel.activeTools; // ['search', 'list_orders']
187
+ withCancel.activeTools; // ['search', 'list_orders', 'cancel_order']
168
188
  ```
169
189
 
170
- ### Typed UI Messages
190
+ ### Typed UI Tool Set
171
191
 
172
192
  Use `InferUIToolSet` to get fully typed UI messages from your tool set:
173
193
 
174
194
  ```typescript
175
195
  import type { UIMessage } from 'ai';
176
- import type { InferUIToolSet, ToolSetConfig } from 'ai-tool-set';
177
-
178
- const tools = {
179
- search: tool({ ... }),
180
- list_orders: {
181
- tool: tool({ ... }),
182
- active: false,
183
- },
184
- cancel_order: {
185
- tool: tool({ ... }),
186
- active: ({ messages }) => /* callback logic */,
187
- },
188
- } satisfies ToolSetConfig;
196
+ import type { InferUIToolSet } from 'ai-tool-set';
189
197
 
198
+ const tools = { search, list_orders, cancel_order };
190
199
  const toolSet = createToolSet(tools);
191
200
 
192
- // Use tool set config
193
- type MyUIMessage = UIMessage<unknown, any, InferUIToolSet<typeof tools>>;
201
+ // From the tools record
202
+ type MyToolSet = InferUIToolSet<typeof tools>;
194
203
 
195
- // Or use ToolSet instance
196
- type MyUIMessage = UIMessage<unknown, any, InferUIToolSet<typeof toolSet>>;
204
+ // Or from the ToolSet instance
205
+ type MyToolSet = InferUIToolSet<typeof toolSet>;
206
+
207
+ // Use MyToolSet in your UIMessage type for type-safe access to tool invocation parts:
208
+ type MyUIMessage = UIMessage<unknown, any, MyToolSet>;
209
+ ```
210
+
211
+ ### Custom UIMessage
212
+
213
+ If you already have a custom `UIMessage` type, you can pass it as `MESSAGE` generic to `createToolSet()` and it will be used in predicates and `inferTools`:
214
+
215
+ ```typescript
216
+ import { myTools } from './my-tools.js';
217
+ import { MyUIMessage } from './my-ui-message.js';
218
+
219
+ const toolSet = createToolSet<typeof myTools, MyUIMessage>(myTools)
220
+ .activateWhen('cancel_order', ({ messages }) => hasUnfulfilledOrders(messages));
221
+ // ~~~~~~~~
222
+ // Messages are now typed as MyUIMessage, so you get type safety and autocompletion in predicates!
223
+
224
+ const { tools, activeTools } = toolSet.inferTools({ messages });
225
+ ```
226
+
227
+ ### Custom Context
228
+
229
+ Pass a `CONTEXT` generic to `createToolSet()` to type the `context` field in predicates and `inferTools`:
230
+
231
+ ```typescript
232
+ import { myTools } from './my-tools.js';
233
+ import { MyUIMessage } from './my-ui-message.js';
234
+
235
+ type MyContext = { userId: string; isAdmin: boolean };
236
+
237
+ const toolSet = createToolSet<typeof myTools, MyUIMessage, MyContext>(myTools)
238
+ .activateWhen('cancel_order', ({ context }) => context.isAdmin);
239
+ // ~~~~~~~~
240
+ // Context is now typed as MyContext, so you get type safety and autocompletion
241
+
242
+ const { tools, activeTools } = toolSet.inferTools({
243
+ messages,
244
+ context: { isAdmin: true },
245
+ });
197
246
  ```
198
247
 
199
248
  ## API
200
249
 
201
- ## `createToolSet(config)`
250
+ ## `createToolSet(tools)`
202
251
 
203
- - `config`, a record where each key maps to either:
204
- - a plain AI SDK `Tool` (always active), or
205
- - a `{ tool, active }` object with activation control (`active` can be `boolean` or `(context) => boolean`)
252
+ - `tools`, a plain `Record<string, Tool>` of AI SDK tools
206
253
 
207
- Returns a `ToolSet` instance.
254
+ Returns a `ToolSet` instance. All tools are active by default.
208
255
 
209
256
  ```ts
210
- const toolSet = createToolSet({
211
- search: tool({ ... }),
212
- list_orders: {
213
- tool: tool({ ... }),
214
- active: false,
215
- },
216
- cancel_order: {
217
- tool: tool({ ... }),
218
- active: ({ messages }) => /* callback logic */,
219
- },
220
- });
257
+ const toolSet = createToolSet({ search, list_orders, cancel_order });
221
258
  ```
222
259
 
223
260
  #### `.tools`
224
261
 
225
- All tools as a standard AI SDK tool record (`Record<string, Tool>`), regardless of activation state.
262
+ All tools as a standard AI SDK tool record, regardless of activation state.
226
263
 
227
264
  ```ts
228
265
  const { tools } = toolSet;
@@ -230,81 +267,93 @@ const { tools } = toolSet;
230
267
 
231
268
  #### `.activeTools`
232
269
 
233
- Resolved array of active tool names based on the current state. Accounts for static activation, explicit overrides, and callback evaluation.
270
+ Resolved array of active tool names based on the current state.
234
271
 
235
272
  ```ts
236
273
  const { activeTools } = toolSet;
237
274
  ```
238
275
 
239
- #### `.activateTools(names)`
276
+ #### `.activate(names)`
240
277
 
241
- Activate statically inactive tools by name. Returns a new `ToolSet` instance. Type-safe — only accepts names of tools with `active: false` or tools previously deactivated in the chain.
278
+ Statically activate tools by name. Returns a new `ToolSet`.
242
279
 
243
280
  ```ts
244
- const toolSetWithOrders = toolSet.activateTools(['list_orders']);
281
+ toolSet.activate(['cancel_order']);
245
282
  ```
246
283
 
247
- #### `.deactivateTools(names)`
284
+ #### `.deactivate(names)`
248
285
 
249
- Deactivate statically active tools by name. Returns a new `ToolSet` instance. Type-safe — only accepts names of plain tools, tools with `active: true`, or tools previously activated in the chain.
286
+ Statically deactivate tools by name. Returns a new `ToolSet`.
250
287
 
251
288
  ```ts
252
- const toolSetWithoutSearch = toolSet.deactivateTools(['search']);
289
+ toolSet.deactivate(['search']);
253
290
  ```
254
291
 
255
- #### `.inferTools(context)`
292
+ #### `.activateWhen(name, predicate)` / `.activateWhen(predicates)`
293
+
294
+ Conditionally activate tools. The predicate receives `{ messages, context }` and returns `true` to activate.
295
+
296
+ ```ts
297
+ toolSet.activateWhen('cancel_order', ({ messages }) => hasOrders(messages));
298
+
299
+ toolSet.activateWhen({
300
+ cancel_order: ({ messages }) => hasOrders(messages),
301
+ list_orders: ({ context }) => context.isAuthenticated,
302
+ });
303
+ ```
256
304
 
257
- Evaluate callback-based tools against the provided context. Returns a new `ToolSet` instance.
305
+ #### `.deactivateWhen(name, predicate)` / `.deactivateWhen(predicates)`
258
306
 
259
- - `context`:
260
- - `messages`, `Array<UIMessage>`, the current conversation messages used by activation callbacks to determine tool availability
307
+ Conditionally deactivate tools. The predicate receives `{ messages, context }` and returns `true` to deactivate.
261
308
 
262
309
  ```ts
263
- const toolSetInferred = toolSet.inferTools({ messages });
310
+ toolSet.deactivateWhen('search', ({ messages }) => messages.length > 10);
311
+ ```
312
+
313
+ #### `.inferTools(input)`
314
+
315
+ Evaluate all predicates with the provided messages and context. Returns a new `ToolSet`.
316
+
317
+ - `input`:
318
+ - `messages`, the current conversation messages
319
+ - `context`, arbitrary values passed to predicates
320
+
321
+ ```ts
322
+ toolSet.inferTools({ messages, context: {} });
264
323
  ```
265
324
 
266
325
  ## Types
267
326
 
268
- ### `ToolSetConfig`
327
+ ### `ActivationInput`
269
328
 
270
- The config record type accepted by `createToolSet()`. Each key maps to a plain `Tool` or a `{ tool, active }` object. Use with `satisfies` to get contextual typing for activation callbacks:
329
+ Input passed to activation predicates. Generic over `MESSAGE` and `CONTEXT`:
271
330
 
272
331
  ```ts
273
- import { type ToolSetConfig } from 'ai-tool-set';
332
+ import type { ActivationInput } from 'ai-tool-set';
274
333
 
275
- const tools = {
276
- search: tool({ ... }),
277
- list_orders: { tool: tool({ ... }), active: false },
278
- cancel_order: { tool: tool({ ... }), active: ({ messages }) => /* ... */ },
279
- } satisfies ToolSetConfig;
334
+ type MyInput = ActivationInput<MyUIMessage, { isAdmin: boolean }>;
335
+ // { messages: Array<MyUIMessage>; context: { isAdmin: boolean } }
280
336
  ```
281
337
 
282
338
  ### `InferToolSet`
283
339
 
284
- Extract a `Record<string, Tool>` from a `ToolSetConfig` object or `ToolSet` instance, stripping activation metadata:
340
+ Extract the raw tool record from a tool record or `ToolSet` instance:
285
341
 
286
342
  ```ts
287
- import { type InferToolSet } from 'ai-tool-set';
343
+ import type { InferToolSet } from 'ai-tool-set';
288
344
 
289
- type Tools = InferToolSet<typeof tools>;
290
- // { search: Tool<...>, list_orders: Tool<...>, cancel_order: Tool<...> }
291
-
292
- // Also works with a ToolSet instance
293
- const toolSet = createToolSet(tools);
294
345
  type Tools = InferToolSet<typeof toolSet>;
346
+ // { search: Tool<...>, list_orders: Tool<...>, cancel_order: Tool<...> }
295
347
  ```
296
348
 
297
349
  ### `InferUIToolSet`
298
350
 
299
- Derive typed UI tool parts from a `ToolSetConfig` object or `ToolSet` instance. Use with `UIMessage` to get type-safe access to tool invocation parts in the conversation:
351
+ Derive typed UI tool parts from a tool record or `ToolSet` instance. Use with `UIMessage` for type-safe access to tool invocation parts:
300
352
 
301
353
  ```ts
302
354
  import type { UIMessage } from 'ai';
303
- import { type InferUIToolSet } from 'ai-tool-set';
304
-
305
- type MyUIMessage = UIMessage<unknown, any, InferUIToolSet<typeof tools>>;
355
+ import type { InferUIToolSet } from 'ai-tool-set';
306
356
 
307
- // Also works with a ToolSet instance
308
357
  type MyUIMessage = UIMessage<unknown, any, InferUIToolSet<typeof toolSet>>;
309
358
 
310
359
  // Parts are now typed per tool:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-tool-set",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.1",
4
4
  "description": "Tool set utilities for the Vercel AI SDK",
5
5
  "keywords": [],
6
6
  "license": "MIT",