ai-tool-set 0.1.0-alpha.1 → 0.1.0-alpha.2

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 CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  </div>
11
11
 
12
- This library provides an immutable and type-safe API to manage [`activeTools`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text#active-tools) for [`generateText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text) and [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) in the AI SDK.
12
+ This library provides a type-safe API to manage [`activeTools`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text#active-tools) for [`generateText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text) and [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) in the AI SDK.
13
13
 
14
14
  ### Why?
15
15
 
@@ -37,25 +37,25 @@ import { tool } from 'ai';
37
37
  import { z } from 'zod';
38
38
  import { createToolSet } from 'ai-tool-set';
39
39
 
40
- const search = tool({
41
- description: 'Search for products',
42
- inputSchema: z.object({ query: z.string() }),
43
- execute: async ({ query }) => searchProducts(query),
44
- });
45
-
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),
56
- });
57
-
58
- const toolSet = createToolSet({ search, list_orders, cancel_order });
40
+ const tools = {
41
+ search: tool({
42
+ description: 'Search for products',
43
+ inputSchema: z.object({ query: z.string() }),
44
+ execute: async ({ query }) => searchProducts(query),
45
+ }),
46
+ 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
+ cancel_order: tool({
52
+ description: 'Cancel an order',
53
+ inputSchema: z.object({ orderId: z.string() }),
54
+ execute: async ({ orderId }) => cancelOrder(orderId),
55
+ }),
56
+ };
57
+
58
+ const toolSet = createToolSet({ tools });
59
59
  ```
60
60
 
61
61
  ### Use with `generateText` / `streamText`
@@ -82,9 +82,7 @@ const result = await generateText({
82
82
  Use `.activate()` and `.deactivate()` to statically control which tools are available:
83
83
 
84
84
  ```typescript
85
- const { tools, activeTools } = toolSet
86
- .deactivate(['cancel_order'])
87
- .activate(['list_orders']);
85
+ const { tools, activeTools } = toolSet.deactivate(['cancel_order']).activate(['list_orders']);
88
86
 
89
87
  const result = await generateText({
90
88
  model,
@@ -99,27 +97,19 @@ const result = await generateText({
99
97
  Use `.activateWhen()` and `.deactivateWhen()` to conditionally control tools based on messages and context. The predicate receives an `ActivationInput` with `messages` and `context`.
100
98
 
101
99
  ```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
- ),
100
+ const toolSet = createToolSet({ tools }).activateWhen('cancel_order', ({ messages }) =>
101
+ messages.some((m) =>
102
+ m.parts.some(
103
+ (p) =>
104
+ p.type === 'tool-list_orders' &&
105
+ p.state === 'output-available' &&
106
+ p.output.orders?.some((order) => order.status !== 'fulfilled'),
111
107
  ),
112
- );
113
-
114
- const result = await generateText({
115
- model,
116
- tools,
117
- activeTools,
118
- prompt: 'Cancel my order #1001',
119
- });
108
+ ),
109
+ );
120
110
  ```
121
111
 
122
- Use `.inferTools()` to evaluate predicates with the current messages and context:
112
+ Use `.inferTools()` to evaluate predicates with the current messages and context. It returns `{ tools, activeTools }` that can be spread directly into `generateText()`:
123
113
 
124
114
  ```typescript
125
115
  const messages = [
@@ -129,18 +119,20 @@ const messages = [
129
119
  },
130
120
  {
131
121
  role: 'assistant',
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
- ],
122
+ parts: [
123
+ {
124
+ type: 'tool-list_orders',
125
+ state: 'output-available',
126
+ toolCallId: 'call-1',
127
+ input: { customerId: 'cust-123' },
128
+ output: {
129
+ orders: [
130
+ { orderId: '1000', status: 'fulfilled' },
131
+ { orderId: '1001', status: 'pending' },
132
+ ],
133
+ },
142
134
  },
143
- }],
135
+ ],
144
136
  },
145
137
  ];
146
138
 
@@ -153,11 +145,10 @@ const result = await generateText({ model, tools, activeTools, messages });
153
145
  You can also activate multiple tools at once:
154
146
 
155
147
  ```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
- });
148
+ const toolSet = createToolSet({ tools }).activateWhen({
149
+ list_orders: ({ context }) => context.isAuthenticated,
150
+ cancel_order: ({ messages }) => hasUnfulfilledOrders(messages),
151
+ });
161
152
  ```
162
153
 
163
154
  ### Last-Call Wins
@@ -165,26 +156,65 @@ const toolSet = createToolSet({ search, list_orders, cancel_order })
165
156
  Each method appends to an internal list. For each tool, the **last entry** determines its state. This makes ordering explicit and predictable:
166
157
 
167
158
  ```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),
159
+ const toolSet = createToolSet({ tools })
160
+ .activate(['cancel_order']) // cancel_order: activated
161
+ .deactivate(['cancel_order']) // cancel_order: deactivated
162
+ .activateWhen(
163
+ 'cancel_order',
164
+ (
165
+ { messages }, // cancel_order: conditional activation
166
+ ) => hasUnfulfilledOrders(messages),
173
167
  );
174
168
  ```
175
169
 
176
- ### Immutable
170
+ ### Immutable vs Mutable
171
+
172
+ By default, `createToolSet()` returns an **immutable** tool set, that means every method returns a new instance and the original is never modified. This is ideal when the tool set is created once in the global scope and shared across requests:
177
173
 
178
- All methods return new tool set instances. The original is never modified:
174
+ ```typescript
175
+ // Global scope: created once, shared across requests
176
+ const toolSet = createToolSet({ tools }).deactivate(['list_order', 'cancel_order']);
177
+
178
+ export async function POST(req: Request) {
179
+ const { messages } = await req.json();
180
+
181
+ // Activate list_orders only for this request
182
+ // myToolSet !== toolSet, original toolSet is unchanged for next request
183
+ const myToolSet = toolSet.activate(['list_orders']);
184
+
185
+ const { tools, activeTools } = myToolSet;
186
+
187
+ const result = await generateText({
188
+ model,
189
+ tools,
190
+ activeTools,
191
+ messages,
192
+ });
193
+ }
194
+ ```
195
+
196
+ Pass `mutable: true` to get a **mutable** tool set where each method mutates in-place and returns `this` for chaining. This is useful when the tool set is created per-request in a local scope:
179
197
 
180
198
  ```typescript
181
- const base = createToolSet({ search, list_orders, cancel_order });
182
- const withoutCancel = base.deactivate(['cancel_order']);
183
- const withCancel = withoutCancel.activate(['cancel_order']);
199
+ export async function POST(req: Request) {
200
+ const { messages } = await req.json();
201
+
202
+ // Local scope: created and mutated per request
203
+ const toolSet = createToolSet({ tools, mutable: true }).deactivate(['list_order', 'cancel_order']);
184
204
 
185
- base.activeTools; // ['search', 'list_orders', 'cancel_order']
186
- withoutCancel.activeTools; // ['search', 'list_orders']
187
- withCancel.activeTools; // ['search', 'list_orders', 'cancel_order']
205
+ // Activate list_orders only for this request
206
+ // myToolSet === toolSet, original toolSet is mutated for this request
207
+ const myToolSet = toolSet.activate(['list_orders']);
208
+
209
+ const { tools, activeTools } = myToolSet;
210
+
211
+ const result = await generateText({
212
+ model,
213
+ tools,
214
+ activeTools,
215
+ messages,
216
+ });
217
+ }
188
218
  ```
189
219
 
190
220
  ### Typed UI Tool Set
@@ -196,7 +226,7 @@ import type { UIMessage } from 'ai';
196
226
  import type { InferUIToolSet } from 'ai-tool-set';
197
227
 
198
228
  const tools = { search, list_orders, cancel_order };
199
- const toolSet = createToolSet(tools);
229
+ const toolSet = createToolSet({ tools });
200
230
 
201
231
  // From the tools record
202
232
  type MyToolSet = InferUIToolSet<typeof tools>;
@@ -216,12 +246,14 @@ If you already have a custom `UIMessage` type, you can pass it as `MESSAGE` gene
216
246
  import { myTools } from './my-tools.js';
217
247
  import { MyUIMessage } from './my-ui-message.js';
218
248
 
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!
249
+ const toolSet = createToolSet<typeof myTools, MyUIMessage>({ tools: myTools }).activateWhen(
250
+ 'cancel_order',
251
+ ({ messages }) => hasUnfulfilledOrders(messages),
252
+ );
253
+ // ~~~~~~~~
254
+ // Messages are now typed as MyUIMessage, so you get type safety and autocompletion in predicates!
223
255
 
224
- const { tools, activeTools } = toolSet.inferTools({ messages });
256
+ const { tools, activeTools } = toolSet.inferTools({ messages, context: {} });
225
257
  ```
226
258
 
227
259
  ### Custom Context
@@ -234,10 +266,12 @@ import { MyUIMessage } from './my-ui-message.js';
234
266
 
235
267
  type MyContext = { userId: string; isAdmin: boolean };
236
268
 
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
269
+ const toolSet = createToolSet<typeof myTools, MyUIMessage, MyContext>({ tools: myTools }).activateWhen(
270
+ 'cancel_order',
271
+ ({ context }) => context.isAdmin,
272
+ );
273
+ // ~~~~~~~~
274
+ // Context is now typed as MyContext, so you get type safety and autocompletion
241
275
 
242
276
  const { tools, activeTools } = toolSet.inferTools({
243
277
  messages,
@@ -247,14 +281,18 @@ const { tools, activeTools } = toolSet.inferTools({
247
281
 
248
282
  ## API
249
283
 
250
- ## `createToolSet(tools)`
284
+ ## `createToolSet(options)`
251
285
 
252
- - `tools`, a plain `Record<string, Tool>` of AI SDK tools
286
+ - `options.tools`, a plain `Record<string, Tool>` of AI SDK tools
287
+ - `options.mutable` (optional), set to `true` for a mutable tool set (default: `false`)
253
288
 
254
289
  Returns a `ToolSet` instance. All tools are active by default.
255
290
 
256
291
  ```ts
257
- const toolSet = createToolSet({ search, list_orders, cancel_order });
292
+ const toolSet = createToolSet({ tools: { search, list_orders, cancel_order } });
293
+
294
+ // Mutable mode — methods mutate in-place and return `this`
295
+ const toolSet = createToolSet({ tools: { search, list_orders, cancel_order }, mutable: true });
258
296
  ```
259
297
 
260
298
  #### `.tools`
@@ -275,7 +313,7 @@ const { activeTools } = toolSet;
275
313
 
276
314
  #### `.activate(names)`
277
315
 
278
- Statically activate tools by name. Returns a new `ToolSet`.
316
+ Statically activate tools by name. Returns a new instance (immutable) or `this` (mutable).
279
317
 
280
318
  ```ts
281
319
  toolSet.activate(['cancel_order']);
@@ -283,7 +321,7 @@ toolSet.activate(['cancel_order']);
283
321
 
284
322
  #### `.deactivate(names)`
285
323
 
286
- Statically deactivate tools by name. Returns a new `ToolSet`.
324
+ Statically deactivate tools by name. Returns a new instance (immutable) or `this` (mutable).
287
325
 
288
326
  ```ts
289
327
  toolSet.deactivate(['search']);
@@ -312,14 +350,16 @@ toolSet.deactivateWhen('search', ({ messages }) => messages.length > 10);
312
350
 
313
351
  #### `.inferTools(input)`
314
352
 
315
- Evaluate all predicates with the provided messages and context. Returns a new `ToolSet`.
353
+ Evaluate all predicates with the provided messages and context. Returns `{ tools, activeTools }` — directly spreadable into `generateText()` or `streamText()`. This is a terminal operation: messages and context are consumed but not stored.
316
354
 
317
355
  - `input`:
318
356
  - `messages`, the current conversation messages
319
357
  - `context`, arbitrary values passed to predicates
320
358
 
321
359
  ```ts
322
- toolSet.inferTools({ messages, context: {} });
360
+ const { tools, activeTools } = toolSet.inferTools({ messages, context: {} });
361
+
362
+ const result = await generateText({ model, tools, activeTools, messages });
323
363
  ```
324
364
 
325
365
  ## Types
package/dist/index.d.mts CHANGED
@@ -8,9 +8,19 @@ type MessageType = UIMessage | ModelMessage;
8
8
  /** The fully-typed UIMessage for a given tool record. */
9
9
  type InferUIMessage<TOOLS extends ToolRecord> = UIMessage<unknown, any, InferUIToolSet<TOOLS>>;
10
10
  /** Infer the raw tool record from a ToolRecord or ToolSet instance. */
11
- type InferToolSet<T extends ToolRecord | ToolSet<any>> = T extends ToolSet<infer TOOLS, any, any> ? TOOLS : T;
11
+ type InferToolSet<T extends ToolRecord | AnyToolSet> = T extends ImmutableToolSet<infer TOOLS, any, any> ? TOOLS : T extends MutableToolSet<infer TOOLS, any, any> ? TOOLS : T;
12
12
  /** Infer the UI tool types from a tool record or ToolSet instance. */
13
- type InferUIToolSet<T extends ToolRecord | ToolSet<any>> = { [K in keyof InferToolSet<T> & string]: InferUITool<InferToolSet<T>[K]> };
13
+ type InferUIToolSet<T extends ToolRecord | AnyToolSet> = { [K in keyof InferToolSet<T> & string]: InferUITool<InferToolSet<T>[K]> };
14
+ /**
15
+ * Extract tool names tracked as active from an ImmutableToolSet instance.
16
+ * Returns `never` for MutableToolSet (cannot be determined at compile time).
17
+ */
18
+ type ActiveTools<T extends AnyToolSet> = T extends ImmutableToolSet<any, any, any, infer A, any> ? A : never;
19
+ /**
20
+ * Extract tool names tracked as inactive from an ImmutableToolSet instance.
21
+ * Returns `never` for MutableToolSet (cannot be determined at compile time).
22
+ */
23
+ type InactiveTools<T extends AnyToolSet> = T extends ImmutableToolSet<any, any, any, any, infer D> ? D : never;
14
24
  /**
15
25
  * Input passed to activation predicates.
16
26
  * Use `ActivationInput<MyMsg>` to get per-tool narrowing in callbacks.
@@ -25,28 +35,42 @@ type ActivationEntry = {
25
35
  toolName: string;
26
36
  resolve: (input: ActivationInput<any, any>) => boolean;
27
37
  };
28
- type ToolSetState<TOOLS extends ToolRecord, MESSAGE extends MessageType, CONTEXT extends Record<string, unknown>> = {
38
+ /** Resolved tools and active tool names returned by `inferTools()`. */
39
+ type ResolvedToolSet<TOOLS extends ToolRecord> = {
29
40
  tools: TOOLS;
30
- entries: Array<ActivationEntry>;
31
- input: ActivationInput<MESSAGE, CONTEXT>;
41
+ activeTools: Array<keyof TOOLS & string>;
32
42
  };
43
+ /** Union of both toolset classes for type utility constraints. */
44
+ type AnyToolSet = ImmutableToolSet<any> | MutableToolSet<any>;
45
+ /**
46
+ * Immutable state container for tool activation.
47
+ *
48
+ * All mutation methods return a new ToolSetState instance.
49
+ * Resolution follows "last-call wins": each method appends entries,
50
+ * and the last entry for each tool determines its state.
51
+ */
52
+ declare class ToolSetState<TOOLS extends ToolRecord, MESSAGE extends MessageType = UIMessage, CONTEXT extends Record<string, unknown> = Record<string, unknown>> {
53
+ #private;
54
+ constructor(tools: TOOLS, entries: Array<ActivationEntry>);
55
+ /** All tools as a standard AI SDK tool record. */
56
+ get tools(): TOOLS;
57
+ /** Resolved list of active tool names based on current entries (no predicates, default input). */
58
+ get activeTools(): Array<keyof TOOLS & string>;
59
+ activate(names: Array<string>): ToolSetState<TOOLS, MESSAGE, CONTEXT>;
60
+ deactivate(names: Array<string>): ToolSetState<TOOLS, MESSAGE, CONTEXT>;
61
+ activateWhen(nameOrPredicates: string | Partial<Record<string, ActivationPredicate<MESSAGE, CONTEXT>>>, predicate?: ActivationPredicate<MESSAGE, CONTEXT>): ToolSetState<TOOLS, MESSAGE, CONTEXT>;
62
+ deactivateWhen(nameOrPredicates: string | Partial<Record<string, ActivationPredicate<MESSAGE, CONTEXT>>>, predicate?: ActivationPredicate<MESSAGE, CONTEXT>): ToolSetState<TOOLS, MESSAGE, CONTEXT>;
63
+ /** Evaluate all predicates with the provided input and return resolved tools + activeTools. */
64
+ inferTools(input: ActivationInput<MESSAGE, CONTEXT>): ResolvedToolSet<TOOLS>;
65
+ }
33
66
  /**
34
67
  * An immutable tool set with chainable activation methods.
35
68
  *
36
69
  * Resolution follows "last-call wins": each method appends an entry,
37
70
  * and the last entry for each tool determines its state.
38
71
  * Default (no entry) is active.
39
- *
40
- * @example
41
- * ```typescript
42
- * const toolSet = createToolSet({ search, list_orders, cancel_order })
43
- * .deactivate(['cancel_order'])
44
- * .activateWhen('cancel_order', ({ messages }) => hasUnfulfilledOrders(messages));
45
- *
46
- * const { tools, activeTools } = toolSet.inferTools({ messages, context: {} });
47
- * ```
48
72
  */
49
- declare class ToolSet<TOOLS extends ToolRecord, MESSAGE extends MessageType = UIMessage, CONTEXT extends Record<string, unknown> = Record<string, unknown>, ACTIVATED extends string = never, DEACTIVATED extends string = never> {
73
+ declare class ImmutableToolSet<TOOLS extends ToolRecord, MESSAGE extends MessageType = UIMessage, CONTEXT extends Record<string, unknown> = Record<string, unknown>, ACTIVATED extends string = never, DEACTIVATED extends string = never> {
50
74
  #private;
51
75
  /** All tools as a standard AI SDK tool record. */
52
76
  readonly tools: TOOLS;
@@ -54,36 +78,70 @@ declare class ToolSet<TOOLS extends ToolRecord, MESSAGE extends MessageType = UI
54
78
  readonly activeTools: Array<keyof TOOLS & string>;
55
79
  constructor(state: ToolSetState<TOOLS, MESSAGE, CONTEXT>);
56
80
  /** Statically activate tools by name. */
57
- activate<NAMES extends (Exclude<keyof TOOLS & string, ACTIVATED> | DEACTIVATED) & string>(names: Array<NAMES>): ToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED | NAMES, DEACTIVATED>;
81
+ activate<NAMES extends keyof TOOLS & string>(names: Array<NAMES>): ImmutableToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED | NAMES, Exclude<DEACTIVATED, NAMES>>;
82
+ /** Statically deactivate tools by name. */
83
+ deactivate<NAMES extends keyof TOOLS & string>(names: Array<NAMES>): ImmutableToolSet<TOOLS, MESSAGE, CONTEXT, Exclude<ACTIVATED, NAMES>, DEACTIVATED | NAMES>;
84
+ /**
85
+ * Conditionally activate a tool — inactive by default, becomes active when predicate returns true.
86
+ * Tracks names in DEACTIVATED since the tool starts inactive.
87
+ */
88
+ activateWhen<NAME extends keyof TOOLS & string>(name: NAME, predicate: ActivationPredicate<MESSAGE, CONTEXT>): ImmutableToolSet<TOOLS, MESSAGE, CONTEXT, Exclude<ACTIVATED, NAME>, DEACTIVATED | NAME>;
89
+ activateWhen<NAMES extends keyof TOOLS & string>(predicates: Partial<Record<NAMES, ActivationPredicate<MESSAGE, CONTEXT>>>): ImmutableToolSet<TOOLS, MESSAGE, CONTEXT, Exclude<ACTIVATED, NAMES>, DEACTIVATED | NAMES>;
90
+ /**
91
+ * Conditionally deactivate a tool — active by default, becomes inactive when predicate returns true.
92
+ * Tracks names in ACTIVATED since the tool starts active.
93
+ */
94
+ deactivateWhen<NAME extends keyof TOOLS & string>(name: NAME, predicate: ActivationPredicate<MESSAGE, CONTEXT>): ImmutableToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED | NAME, Exclude<DEACTIVATED, NAME>>;
95
+ deactivateWhen<NAMES extends keyof TOOLS & string>(predicates: Partial<Record<NAMES, ActivationPredicate<MESSAGE, CONTEXT>>>): ImmutableToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED | NAMES, Exclude<DEACTIVATED, NAMES>>;
96
+ /** Evaluate all predicates with the provided input. Returns resolved `{ tools, activeTools }`. */
97
+ inferTools(input: ActivationInput<MESSAGE, CONTEXT>): ResolvedToolSet<TOOLS>;
98
+ }
99
+ /**
100
+ * A mutable tool set with chainable activation methods.
101
+ *
102
+ * Same resolution semantics as ImmutableToolSet, but methods mutate
103
+ * in-place and return `this` instead of creating new instances.
104
+ */
105
+ declare class MutableToolSet<TOOLS extends ToolRecord, MESSAGE extends MessageType = UIMessage, CONTEXT extends Record<string, unknown> = Record<string, unknown>> {
106
+ #private;
107
+ /** All tools as a standard AI SDK tool record. */
108
+ readonly tools: TOOLS;
109
+ /** Resolved list of active tool names based on current state. */
110
+ activeTools: Array<keyof TOOLS & string>;
111
+ constructor(state: ToolSetState<TOOLS, MESSAGE, CONTEXT>);
112
+ /** Statically activate tools by name. */
113
+ activate(names: Array<keyof TOOLS & string>): this;
58
114
  /** Statically deactivate tools by name. */
59
- deactivate<NAMES extends ((keyof TOOLS & string) | ACTIVATED) & string>(names: Array<NAMES>): ToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED, DEACTIVATED | NAMES>;
115
+ deactivate(names: Array<keyof TOOLS & string>): this;
60
116
  /**
61
- * Conditionally activate a tool — becomes active when predicate returns true.
62
- *
63
- * - `.activateWhen('tool_name', (input) => ...)`
64
- * - `.activateWhen({ tool_name: (input) => ... })`
117
+ * Conditionally activate a tool — inactive by default, becomes active when predicate returns true.
65
118
  */
66
- activateWhen<NAME extends keyof TOOLS & string>(name: NAME, predicate: ActivationPredicate<MESSAGE, CONTEXT>): ToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED, DEACTIVATED>;
67
- activateWhen(predicates: Partial<Record<keyof TOOLS & string, ActivationPredicate<MESSAGE, CONTEXT>>>): ToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED, DEACTIVATED>;
119
+ activateWhen(name: keyof TOOLS & string, predicate: ActivationPredicate<MESSAGE, CONTEXT>): this;
120
+ activateWhen(predicates: Partial<Record<keyof TOOLS & string, ActivationPredicate<MESSAGE, CONTEXT>>>): this;
68
121
  /**
69
- * Conditionally deactivate a tool — becomes inactive when predicate returns true.
70
- * Internally wraps with negation so resolve returns true (active) when pred is false.
71
- *
72
- * - `.deactivateWhen('tool_name', (input) => ...)`
73
- * - `.deactivateWhen({ tool_name: (input) => ... })`
122
+ * Conditionally deactivate a tool — active by default, becomes inactive when predicate returns true.
74
123
  */
75
- deactivateWhen<NAME extends keyof TOOLS & string>(name: NAME, predicate: ActivationPredicate<MESSAGE, CONTEXT>): ToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED, DEACTIVATED>;
76
- deactivateWhen(predicates: Partial<Record<keyof TOOLS & string, ActivationPredicate<MESSAGE, CONTEXT>>>): ToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED, DEACTIVATED>;
77
- /** Provide messages and context for predicate-based tools. Returns a new ToolSet. */
78
- inferTools(input: ActivationInput<MESSAGE, CONTEXT>): ToolSet<TOOLS, MESSAGE, CONTEXT, ACTIVATED, DEACTIVATED>;
124
+ deactivateWhen(name: keyof TOOLS & string, predicate: ActivationPredicate<MESSAGE, CONTEXT>): this;
125
+ deactivateWhen(predicates: Partial<Record<keyof TOOLS & string, ActivationPredicate<MESSAGE, CONTEXT>>>): this;
126
+ /** Evaluate all predicates with the provided input. Returns resolved `{ tools, activeTools }`. */
127
+ inferTools(input: ActivationInput<MESSAGE, CONTEXT>): ResolvedToolSet<TOOLS>;
79
128
  }
129
+ type CreateToolSetOptions<TOOLS extends ToolRecord> = {
130
+ tools: TOOLS;
131
+ mutable?: boolean;
132
+ };
80
133
  /**
81
- * Create a chainable, immutable tool set.
134
+ * Create a chainable tool set.
82
135
  *
83
136
  * @typeParam TOOLS — inferred from the argument
84
137
  * @typeParam MESSAGE — defaults to the fully-typed UIMessage derived from TOOLS
85
138
  * @typeParam CONTEXT — defaults to Record<string, unknown>
86
139
  */
87
- declare const createToolSet: <const TOOLS extends ToolRecord, MESSAGE extends MessageType = InferUIMessage<TOOLS>, CONTEXT extends Record<string, unknown> = Record<string, unknown>>(tools: TOOLS) => ToolSet<TOOLS, MESSAGE, CONTEXT>;
140
+ declare function createToolSet<const TOOLS extends ToolRecord, MESSAGE extends MessageType = InferUIMessage<TOOLS>, CONTEXT extends Record<string, unknown> = Record<string, unknown>>(options: CreateToolSetOptions<TOOLS> & {
141
+ mutable: true;
142
+ }): MutableToolSet<TOOLS, MESSAGE, CONTEXT>;
143
+ declare function createToolSet<const TOOLS extends ToolRecord, MESSAGE extends MessageType = InferUIMessage<TOOLS>, CONTEXT extends Record<string, unknown> = Record<string, unknown>>(options: CreateToolSetOptions<TOOLS> & {
144
+ mutable?: false;
145
+ }): ImmutableToolSet<TOOLS, MESSAGE, CONTEXT, keyof TOOLS & string>;
88
146
  //#endregion
89
- export { type ActivationInput, type InferToolSet, type InferUIToolSet, createToolSet };
147
+ export { type ActivationInput, type ActiveTools, type InactiveTools, type InferToolSet, type InferUIToolSet, createToolSet };
package/dist/index.mjs CHANGED
@@ -10,99 +10,151 @@ const toEntries = (nameOrPredicates, predicate) => {
10
10
  }));
11
11
  };
12
12
  /**
13
- * An immutable tool set with chainable activation methods.
13
+ * Immutable state container for tool activation.
14
14
  *
15
- * Resolution follows "last-call wins": each method appends an entry,
15
+ * All mutation methods return a new ToolSetState instance.
16
+ * Resolution follows "last-call wins": each method appends entries,
16
17
  * and the last entry for each tool determines its state.
17
- * Default (no entry) is active.
18
- *
19
- * @example
20
- * ```typescript
21
- * const toolSet = createToolSet({ search, list_orders, cancel_order })
22
- * .deactivate(['cancel_order'])
23
- * .activateWhen('cancel_order', ({ messages }) => hasUnfulfilledOrders(messages));
24
- *
25
- * const { tools, activeTools } = toolSet.inferTools({ messages, context: {} });
26
- * ```
27
18
  */
28
- var ToolSet = class ToolSet {
29
- #state;
19
+ var ToolSetState = class ToolSetState {
20
+ #tools;
21
+ #entries;
22
+ constructor(tools, entries) {
23
+ this.#tools = tools;
24
+ this.#entries = entries;
25
+ }
30
26
  /** All tools as a standard AI SDK tool record. */
31
- tools;
32
- /** Resolved list of active tool names based on current state. */
33
- activeTools;
34
- constructor(state) {
35
- this.#state = state;
36
- this.tools = state.tools;
37
- this.activeTools = Object.keys(state.tools).filter((name) => {
38
- const lastEntry = state.entries.findLast((e) => e.toolName === name);
39
- if (!lastEntry) return true;
40
- return lastEntry.resolve(state.input);
41
- });
27
+ get tools() {
28
+ return this.#tools;
29
+ }
30
+ /** Resolved list of active tool names based on current entries (no predicates, default input). */
31
+ get activeTools() {
32
+ return this.inferTools({
33
+ messages: [],
34
+ context: {}
35
+ }).activeTools;
42
36
  }
43
- /** Statically activate tools by name. */
44
37
  activate(names) {
45
38
  const newEntries = names.map((name) => ({
46
39
  toolName: name,
47
40
  resolve: () => true
48
41
  }));
49
- return new ToolSet({
50
- ...this.#state,
51
- entries: [...this.#state.entries, ...newEntries]
52
- });
42
+ return new ToolSetState(this.#tools, [...this.#entries, ...newEntries]);
53
43
  }
54
- /** Statically deactivate tools by name. */
55
44
  deactivate(names) {
56
45
  const newEntries = names.map((name) => ({
57
46
  toolName: name,
58
47
  resolve: () => false
59
48
  }));
60
- return new ToolSet({
61
- ...this.#state,
62
- entries: [...this.#state.entries, ...newEntries]
63
- });
49
+ return new ToolSetState(this.#tools, [...this.#entries, ...newEntries]);
64
50
  }
65
51
  activateWhen(nameOrPredicates, predicate) {
66
- const newEntries = toEntries(nameOrPredicates, predicate);
67
- return new ToolSet({
68
- ...this.#state,
69
- entries: [...this.#state.entries, ...newEntries]
70
- });
52
+ return new ToolSetState(this.#tools, [...this.#entries, ...toEntries(nameOrPredicates, predicate)]);
71
53
  }
72
54
  deactivateWhen(nameOrPredicates, predicate) {
73
55
  const newEntries = toEntries(nameOrPredicates, predicate).map((e) => ({
74
56
  ...e,
75
57
  resolve: (input) => !e.resolve(input)
76
58
  }));
77
- return new ToolSet({
78
- ...this.#state,
79
- entries: [...this.#state.entries, ...newEntries]
80
- });
59
+ return new ToolSetState(this.#tools, [...this.#entries, ...newEntries]);
81
60
  }
82
- /** Provide messages and context for predicate-based tools. Returns a new ToolSet. */
61
+ /** Evaluate all predicates with the provided input and return resolved tools + activeTools. */
83
62
  inferTools(input) {
84
- return new ToolSet({
85
- ...this.#state,
86
- input
63
+ const activeTools = Object.keys(this.#tools).filter((name) => {
64
+ const lastEntry = this.#entries.findLast((e) => e.toolName === name);
65
+ if (!lastEntry) return true;
66
+ return lastEntry.resolve(input);
87
67
  });
68
+ return {
69
+ tools: this.#tools,
70
+ activeTools
71
+ };
88
72
  }
89
73
  };
90
74
  /**
91
- * Create a chainable, immutable tool set.
75
+ * An immutable tool set with chainable activation methods.
92
76
  *
93
- * @typeParam TOOLS inferred from the argument
94
- * @typeParam MESSAGE defaults to the fully-typed UIMessage derived from TOOLS
95
- * @typeParam CONTEXT defaults to Record<string, unknown>
77
+ * Resolution follows "last-call wins": each method appends an entry,
78
+ * and the last entry for each tool determines its state.
79
+ * Default (no entry) is active.
96
80
  */
97
- const createToolSet = (tools) => {
98
- return new ToolSet({
99
- tools,
100
- entries: [],
101
- input: {
102
- messages: [],
103
- context: {}
104
- }
105
- });
81
+ var ImmutableToolSet = class ImmutableToolSet {
82
+ #state;
83
+ /** All tools as a standard AI SDK tool record. */
84
+ tools;
85
+ /** Resolved list of active tool names based on current state. */
86
+ activeTools;
87
+ constructor(state) {
88
+ this.#state = state;
89
+ this.tools = state.tools;
90
+ this.activeTools = state.activeTools;
91
+ }
92
+ /** Statically activate tools by name. */
93
+ activate(names) {
94
+ return new ImmutableToolSet(this.#state.activate(names));
95
+ }
96
+ /** Statically deactivate tools by name. */
97
+ deactivate(names) {
98
+ return new ImmutableToolSet(this.#state.deactivate(names));
99
+ }
100
+ activateWhen(nameOrPredicates, predicate) {
101
+ return new ImmutableToolSet(this.#state.activateWhen(nameOrPredicates, predicate));
102
+ }
103
+ deactivateWhen(nameOrPredicates, predicate) {
104
+ return new ImmutableToolSet(this.#state.deactivateWhen(nameOrPredicates, predicate));
105
+ }
106
+ /** Evaluate all predicates with the provided input. Returns resolved `{ tools, activeTools }`. */
107
+ inferTools(input) {
108
+ return this.#state.inferTools(input);
109
+ }
110
+ };
111
+ /**
112
+ * A mutable tool set with chainable activation methods.
113
+ *
114
+ * Same resolution semantics as ImmutableToolSet, but methods mutate
115
+ * in-place and return `this` instead of creating new instances.
116
+ */
117
+ var MutableToolSet = class {
118
+ #state;
119
+ /** All tools as a standard AI SDK tool record. */
120
+ tools;
121
+ /** Resolved list of active tool names based on current state. */
122
+ activeTools;
123
+ constructor(state) {
124
+ this.#state = state;
125
+ this.tools = state.tools;
126
+ this.activeTools = state.activeTools;
127
+ }
128
+ #apply(state) {
129
+ this.#state = state;
130
+ this.activeTools = state.activeTools;
131
+ }
132
+ /** Statically activate tools by name. */
133
+ activate(names) {
134
+ this.#apply(this.#state.activate(names));
135
+ return this;
136
+ }
137
+ /** Statically deactivate tools by name. */
138
+ deactivate(names) {
139
+ this.#apply(this.#state.deactivate(names));
140
+ return this;
141
+ }
142
+ activateWhen(nameOrPredicates, predicate) {
143
+ this.#apply(this.#state.activateWhen(nameOrPredicates, predicate));
144
+ return this;
145
+ }
146
+ deactivateWhen(nameOrPredicates, predicate) {
147
+ this.#apply(this.#state.deactivateWhen(nameOrPredicates, predicate));
148
+ return this;
149
+ }
150
+ /** Evaluate all predicates with the provided input. Returns resolved `{ tools, activeTools }`. */
151
+ inferTools(input) {
152
+ return this.#state.inferTools(input);
153
+ }
106
154
  };
155
+ function createToolSet(options) {
156
+ const state = new ToolSetState(options.tools, []);
157
+ return options.mutable ? new MutableToolSet(state) : new ImmutableToolSet(state);
158
+ }
107
159
  //#endregion
108
160
  export { createToolSet };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-tool-set",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Tool set utilities for the Vercel AI SDK",
5
5
  "keywords": [],
6
6
  "license": "MIT",