This library provides a type-safe API to manage activeTools, toolApproval, and toolChoice for generateText() and streamText() in the AI SDK.
The AI SDK provides an activeTools parameter to control which tools the model can use, a toolApproval parameter to gate tool execution, and a toolChoice parameter to control which tool is called. However, managing this becomes complex when you need to:
- Statically activate/deactivate tools: Some tools should be inactive by default and only available after being explicitly activated
- Dynamically infer tool activation: Some tools should be activated based on runtime context like the conversation history
- Control tool approval: Some tools should require human approval, or be auto-approved or denied based on context
- Force tool choice: Sometimes the model should be required to call a specific tool, or any tool, based on context
This library wraps standard AI SDK tool() definitions with chainable activation, approval, and choice methods, and resolves tools, activeTools, toolApproval, and toolChoice for any AI SDK function.
npm install ai-tool-set@1 # AI SDK v6
npm install ai-tool-set@2 # AI SDK v7Pass a plain record of AI SDK tool() definitions to createToolSet(). All tools are active by default.
import { tool } from 'ai';
import { z } from 'zod';
import { createToolSet } from 'ai-tool-set';
const tools = {
search: tool({
description: 'Search for products',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => searchProducts(query),
}),
list_orders: tool({
description: 'List orders for a customer',
inputSchema: z.object({ customerId: z.string() }),
execute: async ({ customerId }) => listOrders(customerId),
}),
cancel_order: tool({
description: 'Cancel an order',
inputSchema: z.object({ orderId: z.string() }),
execute: async ({ orderId }) => cancelOrder(orderId),
}),
};
const toolSet = createToolSet({ tools });Use .activate() and .deactivate() to statically control which tools are available. Call .inferTools() to resolve activeTools and pass into generateText() or streamText():
import { generateText } from 'ai';
// Activate and deactivate tools
const toolSet = createToolSet({ tools }).deactivate(['cancel_order']).activate(['list_orders']);
// Infer active tools
const { tools, activeTools } = toolSet.inferTools();
const result = await generateText({
model,
// Pass tools and activeTools:
tools,
activeTools,
// Or spread directly:
// ...toolSet.deactivate(['cancel_order']).activate(['list_orders']).inferTools(),
prompt: 'Show me my orders',
});Use .activateWhen() and .deactivateWhen() to conditionally control tools based on messages, step history, and context. The predicate receives an input with messages, steps, and toolSetContext (all can be undefined if not provided to inferTools) and should return a boolean (or undefined) to determine whether the tool should be activated/deactivated.
const toolSet = createToolSet({ tools })
// toolSetContext: activate list_orders for authenticated users
.activateWhen('list_orders', ({ toolSetContext }) => toolSetContext?.isAuthenticated)
// messages: activate cancel_order when the conversation has unfulfilled orders
.activateWhen('cancel_order', ({ messages }) =>
messages?.some((m) =>
m.parts.some(
(p) =>
p.type === 'tool-list_orders' &&
p.state === 'output-available' &&
p.output.orders?.some((order) => order.status !== 'fulfilled'),
),
),
)
// steps: deactivate search once it has been called in this run
.deactivateWhen('search', ({ steps }) =>
steps?.some((s) => s.staticToolCalls.some((c) => c.toolName === 'search')),
);You can also activate or deactivate multiple tools at once using the object form:
const toolSet = createToolSet({ tools })
.activateWhen({
list_orders: ({ toolSetContext }) => { /* ... */ },
cancel_order: ({ messages }) => { /* ... */ },
})
.deactivateWhen({
search: ({ steps }) => { /* ... */ },
});Call .inferTools() with messages and/or toolSetContext to evaluate activation predicates and resolve activeTools:
const messages = [
{
role: 'user',
parts: [{ type: 'text', text: 'Show me my orders' }],
},
{
role: 'assistant',
parts: [
{
type: 'tool-list_orders',
state: 'output-available',
toolCallId: 'call-1',
input: { customerId: 'cust-123' },
output: {
orders: [
{ orderId: '1000', status: 'fulfilled' },
{ orderId: '1001', status: 'pending' },
],
},
},
],
},
];
const toolSetContext = { isAuthenticated: true };
// cancel_order is now active because list_orders returned unfulfilled orders
const { tools, activeTools } = toolSet.inferTools({ messages, toolSetContext });
const result = await generateText({ model, tools, activeTools, messages });In a multi-step run, you can call .inferTools() inside prepareStep() to re-evaluate active tools after each step. Here you also have access to the steps array, which contains the completed steps of the current run:
import { generateText, stepCountIs } from 'ai';
const { tools } = toolSet;
const result = await generateText({
model,
tools,
stopWhen: stepCountIs(10),
prompt: 'Find my orders and cancel the unfulfilled one',
prepareStep: ({ steps }) => {
// Resolve activeTools from the steps completed so far
const { activeTools } = toolSet.inferTools({ steps });
return { activeTools };
},
});.activateWhen() marks a tool as inactive by default. It only becomes active when the predicate returns true. If the predicate returns undefined or false, the tool stays inactive:
const toolSet = createToolSet({ tools })
// undefined when messages is not provided → tool stays inactive
// false when no orders found → tool stays inactive
// true when orders found → tool becomes active
.activateWhen('cancel_order', ({ messages }) => messages?.some((m) => hasOrders(m)));
toolSet.inferTools().activeTools; // cancel_order is inactive (predicate received undefined)
toolSet.inferTools({ messages: [] }).activeTools; // cancel_order is inactive (no orders).deactivateWhen() marks a tool as active by default. It only becomes inactive when the predicate returns true. If the predicate returns undefined or false, the tool stays active:
const toolSet = createToolSet({ tools })
// undefined when messages is not provided → tool stays active
// false when few messages → tool stays active
// true when too many messages → tool becomes inactive
.deactivateWhen('search', ({ messages }) => messages && messages.length > 10);
toolSet.inferTools().activeTools; // search is active (predicate received undefined)
toolSet.inferTools({ messages: [] }).activeTools; // search is active (few messages)Each activation method appends to an internal list. For each tool, the last entry determines its state. This makes ordering explicit and predictable:
const toolSet = createToolSet({ tools })
// cancel_order: activated
.activate(['cancel_order'])
// cancel_order: deactivated
.deactivate(['cancel_order'])
// cancel_order: deactivated with conditional activation
.activateWhen('cancel_order', ({ messages }) => hasUnfulfilledOrders(messages));Note
Tool approval requires AI SDK v7 and ai-tool-set@2.x
The AI SDK can gate tool execution with toolApproval. .inferTools() resolves a toolApproval record alongside tools and activeTools, so a single tool set drives both activation and approval.
Use .approve() and .deny() for static decisions, and .approval() for dynamic ones:
const toolSet = createToolSet({ tools })
// Always auto-approve
.approve(['list_orders'])
// Always require human approval
.approval('cancel_order', 'user-approval')
// Decide dynamically from the toolset context
.approval('search', ({ toolSetContext }) => (toolSetContext?.isAdmin ? 'approved' : 'denied'));
const { tools, activeTools, toolApproval } = toolSet.inferTools({ toolSetContext: { isAdmin: false } });
const result = await generateText({ model, tools, activeTools, toolApproval, prompt: 'Cancel order 1001' });A tool with no approval entry defaults to 'not-applicable' (runs without approval). Approval resolution follows the same last-call wins rule as activation.
An .approval() entry is either a constant ToolApprovalStatus ('approved', 'denied', 'user-approval', 'not-applicable') or a resolver. The resolver runs inside .inferTools() with your toolset values ({ messages, steps, toolSetContext }) and returns either:
- a final status decided right there, or
- a
SingleToolApprovalFunctiondeferred to tool-call time, which the AI SDK invokes with the tool input and its own runtime values (runtimeContext, model messages, tool context).
This two-layer shape keeps the two contexts distinct: toolSetContext (yours, available now in inferTools) versus the AI SDK's runtimeContext (live at tool-call time). Decide with what you know up front, and defer to the live tool input when you don't:
type ToolSetContext = { isAdmin: boolean };
type RuntimeContext = { region: string };
const toolSet = createToolSet<typeof tools, MyUIMessage, ToolSetContext, RuntimeContext>({ tools }).approval(
'cancel_order',
({ toolSetContext }) =>
// Decide now from the toolset context...
toolSetContext?.isAdmin
? 'approved'
: // ...or defer to the AI SDK with the actual tool input + runtime context
(input, { runtimeContext }) =>
input.orderId.startsWith('eu-') && runtimeContext.region === 'eu' ? 'approved' : 'user-approval',
);The AI SDK can steer tool calls with toolChoice, which controls whether and which tool the model must call. .inferTools() resolves a toolChoice value alongside tools, activeTools, and toolApproval, so a single tool set drives activation, approval, and choice together.
Use .choice() with a constant for static choices or use a resolver to decide dynamically:
const toolSet = createToolSet({ tools })
.deactivate(['cancel_order'])
// Force the model to call one of the active tools this turn
.choice('required')
// Or resolve dynamically, e.g. force unauthenticated users into search
.choice(({ toolSetContext }) => (toolSetContext?.isAuthenticated ? 'auto' : { type: 'tool', toolName: 'search' }));
const { tools, activeTools, toolChoice } = toolSet.inferTools();
const result = await generateText({ model, tools, activeTools, toolChoice, prompt: 'Show me my orders' });A .choice() entry is either a constant ToolChoice ('auto', 'required', 'none', or { type: 'tool', toolName } with toolName typed to your tools) or a resolver. The resolver runs inside .inferTools() with your toolset values ({ messages, steps, toolSetContext }) and returns a ToolChoice, or undefined to leave the choice unconstrained (the AI SDK then defaults to 'auto'). Since it sets a single value, it follows the same last-call wins rule as activation and approval: the last entry decides, and a resolver returning undefined does not fall back to an earlier one, so express any fallback inside the resolver itself.
Note
toolChoice is resolved independently of activeTools and passed through as-is. Because the AI SDK filters tools by activeTools first, forcing { type: 'tool', toolName } for a tool you have deactivated leaves the model unable to see it and surfaces as a provider error, so keep a forced tool active.
Resolving from steps makes it easy to guide a multi-step run. Call .inferTools() inside prepareStep() to recompute toolChoice (and activeTools) after each step, for example to force a tool on the first step and then hand control back to the model:
import { generateText, stepCountIs } from 'ai';
const toolSet = createToolSet({ tools })
.choice(({ steps }) => steps?.length === 0 ? { type: 'tool', toolName: 'search' } : 'auto');
const { tools } = toolSet;
const result = await generateText({
model,
tools,
stopWhen: stepCountIs(10),
prompt: 'Find my orders and cancel the unfulfilled one',
prepareStep: ({ steps }) => toolSet.inferTools({ steps }),
});Note
Tool ordering requires AI SDK v7 and ai-tool-set@2.x
The AI SDK sends tools to the provider in the insertion order of the tools record, filtered by activeTools, unless you override it with the toolOrder parameter. When you toggle tools with conditional activation, a dynamic tool in the middle of the record shifts every tool after it as it flips on and off, which can invalidate the provider's prompt cache for the tool block.
ai-tool-set resolves this into a toolOrder for you. By default it uses 'stable': always-active (static) tools stay in a fixed prefix and conditionally-activated (dynamic) tools sort to the tail, so the static prefix stays byte-identical as the dynamic tools toggle. .inferTools() returns toolOrder next to activeTools, and because it is a value (not a reordered record) it flows through prepareStep — keeping the order stable across every step of a run:
import { generateText, stepCountIs } from 'ai';
// Stable tool ordering is default when omitted
const toolSet = createToolSet({ tools, order: 'stable' })
// list_orders drops out once it has been called this run
.deactivateWhen('list_orders', ({ steps }) =>
steps?.some((s) => s.staticToolCalls.some((c) => c.toolName === 'list_orders')),
);
await generateText({
model,
tools,
stopWhen: stepCountIs(10),
prepareStep: ({ steps }) => {
// activeTools + toolOrder recomputed per step; the static prefix never shifts
const { activeTools, toolOrder } = toolSet.inferTools({ steps });
return { activeTools, toolOrder };
},
});For a single call, spread the whole result including tools, activeTools, and toolOrder directly:
await generateText({ model, ...toolSet.inferTools({ toolSetContext }), messages });Use the order parameter of createToolSet() or the .order() method to change the tool order strategy:
'stable'(default), static tools first, conditionally-activated tools to the tail'insertion', as declared in thetoolsrecord (resolves to notoolOrder)Array<string>, an explicit order; names not listed keep insertion order after the listed ones- a comparator
(a, b) => numberover{ toolName, tool, dynamic, index }
Ordering follows last-call wins:
const toolSet = createToolSet({ tools, order: 'insertion' }); // opt out — keep the declared order
toolSet.order(['search', 'get_weather']); // pin a few, the rest follow insertion order
toolSet.order((a, b) => a.toolName.localeCompare(b.toolName)); // comparator over { toolName, tool, dynamic, index }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:
// Global scope: created once, shared across requests
const toolSet = createToolSet({ tools }).deactivate(['list_order', 'cancel_order']);
export async function POST(req: Request) {
const { messages } = await req.json();
// Activate list_orders only for this request
// myToolSet !== toolSet, original toolSet is unchanged for next request
const myToolSet = toolSet.activate(['list_orders']);
const result = await generateText({
model,
...myToolSet.inferTools({ messages }),
messages,
});
}Use createToolSet({ 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:
export async function POST(req: Request) {
const { messages } = await req.json();
// Local scope: created and mutated per request
const toolSet = createToolSet({ tools, mutable: true })
.deactivate(['list_order', 'cancel_order'])
.activate(['list_orders']);
const result = await generateText({
model,
...toolSet.inferTools({ messages }),
messages,
});
}Use .clone({ mutable?: boolean }) to convert between immutable and mutable, preserving all activation entries:
// Convert an immutable toolset to mutable
const mutableToolSet = toolSet.clone({ mutable: true });
// Convert a mutable toolset back to immutable
const immutableToolSet = mutableToolSet.clone();This is useful when you want to create a base tool set in the global scope and clone it per request to add request-specific activation:
// Global scope: base tool set
const baseToolSet = createToolSet({ tools }).deactivate(['list_order', 'cancel_order']);
export async function POST(req: Request) {
const { messages } = await req.json();
// Clone the base tool set into a mutable instance for this request
const toolSet = baseToolSet.clone({ mutable: true });
// Activate list_orders only for this request
toolSet.activate(['list_orders']);
const result = await generateText({
model,
...toolSet.inferTools({ messages }),
messages,
});
}Use InferUIToolSet to get fully typed UI messages from your tool set:
import type { UIMessage } from 'ai';
import type { InferUIToolSet } from 'ai-tool-set';
const tools = { search, list_orders, cancel_order };
const toolSet = createToolSet({ tools });
// From the tools record
type MyToolSet = InferUIToolSet<typeof tools>;
// Or from the ToolSet instance
type MyToolSet = InferUIToolSet<typeof toolSet>;
// Use MyToolSet in your UIMessage type for type-safe access to tool invocation parts:
type MyUIMessage = UIMessage<unknown, any, MyToolSet>;The second generic MESSAGE of createToolSet() defaults to UIMessage and types the messages passed to predicates and inferTools(). If you already have a custom UIMessage type, you can pass it to make messages fully typed:
import { myTools } from './my-tools.js';
import { MyUIMessage } from './my-ui-message.js';
const toolSet = createToolSet<typeof myTools, MyUIMessage>({ tools: myTools }).activateWhen(
'cancel_order',
({ messages }) => hasUnfulfilledOrders(messages),
// ~~~~~~~~
// Messages are now typed as Array<MyUIMessage> | undefined
);
const { tools, activeTools } = toolSet.inferTools({ messages });If you rather work with model messages than UI messages, you can pass ModelMessage instead. This is useful if you want to call inferTools() inside the prepareStep() callback, where you only have access to model messages:
import { myTools } from './my-tools.js';
import { ModelMessage } from 'ai';
const toolSet = createToolSet<typeof myTools, ModelMessage>({ tools: myTools }).activateWhen(
'cancel_order',
({ messages }) => hasUnfulfilledOrders(messages),
// ~~~~~~~~
// Messages are now typed as Array<ModelMessage> | undefined
);
const result = await generateText({
model,
messages: await convertToModelMessages(messages),
prepareStep: ({ messages }) => {
// ~~~~~~~~
// The available model messages for each step
const { activeTools } = toolSet.inferTools({ messages });
return { activeTools };
},
});The AI SDK has its own runtimeContext and toolsContext that flow through generation and are available at tool-call time. toolSetContext is this library's own context, evaluated at .inferTools() time. It can share the shape of your AI SDK runtimeContext, or be something completely different.
Pass a TOOLSET_CONTEXT generic to createToolSet() to type the toolSetContext field in predicates, approval resolvers, and inferTools:
import { myTools } from './my-tools.js';
import { MyUIMessage } from './my-ui-message.js';
type MyToolSetContext = { userId: string; isAdmin: boolean };
const toolSet = createToolSet<typeof myTools, MyUIMessage, MyToolSetContext>({ tools: myTools }).activateWhen(
'cancel_order',
({ toolSetContext }) => toolSetContext?.isAdmin,
// ~~~~~~~~~~~~~~
// toolSetContext is typed as MyToolSetContext | undefined
);
const { tools, activeTools } = toolSet.inferTools({
messages,
toolSetContext: { userId: '1', isAdmin: true },
});A fourth RUNTIME_CONTEXT generic types the AI SDK runtimeContext (defaults to Record<string, unknown>). It types both the runtimeContext a deferred approval function receives at tool-call time and the runtimeContext on each step in predicates. It is separate from toolSetContext and is passed to generateText() as a plain value:
type RuntimeContext = { region: string };
const toolSet = createToolSet<typeof myTools, MyUIMessage, MyToolSetContext, RuntimeContext>({
tools: myTools,
}).approval(
'cancel_order',
() =>
(input, { runtimeContext }) =>
runtimeContext.region === 'eu' ? 'user-approval' : 'approved',
// ~~~~~~~~~~~~~~
// runtimeContext is typed as RuntimeContext
);
// Pass the matching runtimeContext to generateText — the AI SDK forwards it to the deferred function
const result = await generateText({
model,
...toolSet.inferTools(),
runtimeContext: { region: 'eu' },
prompt: 'Cancel order eu-1001',
});options.tools, a plainRecord<string, Tool>of AI SDK toolsoptions.mutable(optional), set totruefor a mutable tool set (default:false)options.order(optional), the ordering strategy resolved intotoolOrder(default:'stable'), see.order(order)
Returns a ToolSet instance. All tools are active by default.
const toolSet = createToolSet({ tools: { search, list_orders, cancel_order } });
// Mutable mode — methods mutate in-place and return `this`
const toolSet = createToolSet({ tools: { search, list_orders, cancel_order }, mutable: true });All tools as a standard AI SDK tool record, regardless of activation state.
const { tools } = toolSet;Statically activate tools by name. Returns a new instance (immutable) or this (mutable).
toolSet.activate(['cancel_order']);Statically deactivate tools by name. Returns a new instance (immutable) or this (mutable).
toolSet.deactivate(['search']);Conditionally activate tools. The predicate receives { messages, steps, toolSetContext } and returns true to activate. All fields can be undefined if not provided to inferTools. Returning undefined is treated as false.
toolSet.activateWhen('cancel_order', ({ messages }) => messages?.some((m) => hasOrders(m)));
toolSet.activateWhen({
cancel_order: ({ messages }) => messages?.some((m) => hasOrders(m)),
list_orders: ({ toolSetContext }) => toolSetContext?.isAuthenticated,
});Conditionally deactivate tools. The predicate receives { messages, steps, toolSetContext } and returns true to deactivate. All fields can be undefined if not provided to inferTools. Returning undefined is treated as false (tool stays active).
toolSet.deactivateWhen('search', ({ messages }) => messages && messages.length > 10);Statically approve tools by name (status 'approved'). Returns a new instance (immutable) or this (mutable).
toolSet.approve(['list_orders']);Statically deny tools by name (status 'denied'). Returns a new instance (immutable) or this (mutable).
toolSet.deny(['cancel_order']);Set a tool's approval to a constant ToolApprovalStatus or a resolver. A resolver receives { messages, steps, toolSetContext } and returns either a final status or a SingleToolApprovalFunction that the AI SDK calls at tool-call time. Last-call wins.
// Constant status
toolSet.approval('cancel_order', 'user-approval');
// Resolver — decide from the toolset context
toolSet.approval('cancel_order', ({ toolSetContext }) => (toolSetContext?.isAdmin ? 'approved' : 'user-approval'));
// Resolver — defer to the AI SDK with the tool input + runtime context
toolSet.approval(
'cancel_order',
() =>
(input, { runtimeContext }) =>
input.orderId ? 'approved' : 'denied',
);
// Multiple tools at once
toolSet.approval({
search: 'approved',
cancel_order: ({ toolSetContext }) => (toolSetContext?.isAdmin ? 'approved' : 'denied'),
});Set the toolset's toolChoice to a constant ToolChoice ('auto', 'none', 'required', or { type: 'tool', toolName }) or a resolver. A resolver receives { messages, steps, toolSetContext } and returns a ToolChoice, or undefined to leave it unconstrained (omitted). Last-call wins. The resolved toolChoice is passed through as-is; keep a forced tool active so the AI SDK does not filter it out.
// Constant
toolSet.choice('required');
toolSet.choice({ type: 'tool', toolName: 'search' });
// Resolver — force a tool on the first step, then let the model decide
toolSet.choice(({ steps }) => (steps?.length === 0 ? { type: 'tool', toolName: 'search' } : 'auto'));Set how tools are ordered for the provider. .inferTools() resolves this into a toolOrder value (the AI SDK's own toolOrder parameter), so it applies without reordering the tools record and flows through prepareStep. Returns a new instance (immutable) or this (mutable). Last-call wins. order is one of:
'stable'(default), static tools first (in insertion order), conditionally-activated tools to the tail — keeps the prompt's static tool prefix stable for provider prompt caching'insertion', as declared in thetoolsrecord (resolves to notoolOrder)Array<string>, an explicit order; names not listed keep insertion order after the listed ones- a comparator
(a, b) => numberover{ toolName, tool, dynamic, index }, matchingArray.prototype.sort
toolSet.order('stable');
toolSet.order(['search', 'get_weather']);
toolSet.order((a, b) => a.toolName.localeCompare(b.toolName));Evaluate all predicates, approval resolvers, the tool-choice entry, and the order strategy and return { tools, activeTools, toolApproval, toolChoice, toolOrder }, directly spreadable into generateText(), streamText(), or a prepareStep return. The input is optional; all fields are optional. Predicates and resolvers receive undefined for fields not provided.
input(optional):messages(optional), the current conversation messagessteps(optional), the completed steps of the current run (theStepResultarray fromprepareStep)toolSetContext(optional), arbitrary values passed to predicates, approval resolvers, and the tool-choice resolver
The result includes toolChoice when set via .choice() (otherwise undefined), and toolOrder when .order() is anything other than 'insertion' (otherwise undefined).
// Spread the whole result into generateText / streamText
const result = await generateText({ model, ...toolSet.inferTools({ messages, toolSetContext }), messages });
// Or destructure the parts you need
const { tools, activeTools, toolApproval, toolChoice, toolOrder } = toolSet.inferTools({ messages });
// Inside prepareStep — activeTools + toolOrder recomputed per step
const result = await generateText({
model,
tools,
prepareStep: ({ steps }) => {
const { activeTools, toolOrder } = toolSet.inferTools({ steps });
return { activeTools, toolOrder };
},
});Clone the toolset, preserving all activation entries. Pass { mutable: true } to get a mutable clone, or omit for an immutable clone. Defaults to immutable.
const mutableClone = toolSet.clone({ mutable: true });
const immutableClone = toolSet.clone();Input passed to inferTools(), activation predicates, and approval resolvers. Generic over TOOLS, MESSAGE, TOOLSET_CONTEXT, and RUNTIME_CONTEXT. All fields are optional since they may not be provided to inferTools. steps is inferred from TOOLS (and carries RUNTIME_CONTEXT on each step):
import type { InferToolsInput } from 'ai-tool-set';
type MyInput = InferToolsInput<typeof tools, MyUIMessage, { isAdmin: boolean }>;
// {
// messages?: Array<MyUIMessage>;
// steps?: Array<StepResult<typeof tools>>;
// toolSetContext?: { isAdmin: boolean };
// }Parameter type that accepts both immutable and mutable variants of an existing tool set. Use it for helpers that should work regardless of which flavor the caller is holding:
import { createToolSet, type ToolSet } from 'ai-tool-set';
const toolSet = createToolSet({ tools }).deactivate(['cancel_order']);
type MyToolSet = ToolSet<typeof toolSet>;
// Accepts the immutable toolset AND the cloned mutable instance
function activateTools(toolSet: MyToolSet) {
toolSet.activate(['cancel_order']);
}
activateTools(toolSet);
const mutableToolSet = toolSet.clone({ mutable: true });
activateTools(mutableToolSet);Extract the raw tool record from a tool record or ToolSet instance:
import type { InferToolSet } from 'ai-tool-set';
type Tools = InferToolSet<typeof toolSet>;
// { search: Tool<...>, list_orders: Tool<...>, cancel_order: Tool<...> }Derive typed UI tool parts from a tool record or ToolSet instance. Use with UIMessage for type-safe access to tool invocation parts:
import type { UIMessage } from 'ai';
import type { InferUIToolSet } from 'ai-tool-set';
type MyUIMessage = UIMessage<unknown, any, InferUIToolSet<typeof toolSet>>;
// Parts are now typed per tool:
// message.parts[0].type === 'tool-search'
// message.parts[0].output // typed as search tool's return typeExtract the tool names tracked as active from an immutable ToolSet instance. Tracks tools from .activate() and .deactivateWhen().
Note
InferActiveTools returns never for mutable toolsets, since TypeScript cannot track type changes on the same reference across method calls.
import type { InferActiveTools } from 'ai-tool-set';
const toolSet = createToolSet({ tools }).deactivate(['cancel_order']);
type Active = InferActiveTools<typeof toolSet>;
// 'search' | 'list_orders'Extract the tool names tracked as inactive from an immutable ToolSet instance. Tracks tools from .deactivate() and .activateWhen().
Note
InferInactiveTools returns never for mutable toolsets, since TypeScript cannot track type changes on the same reference across method calls.
import type { InferInactiveTools } from 'ai-tool-set';
const toolSet = createToolSet({ tools }).deactivate(['cancel_order']);
type Inactive = InferInactiveTools<typeof toolSet>;
// 'cancel_order'Extract all tool names from a ToolSet instance, regardless of activation state. Works for both immutable and mutable toolsets since the tool record is statically known.
import type { InferAllTools } from 'ai-tool-set';
const toolSet = createToolSet({ tools }).deactivate(['cancel_order']);
type All = InferAllTools<typeof toolSet>;
// 'search' | 'list_orders' | 'cancel_order'MIT