Skip to main content

Agents 1.5 migration - Node.js

Migrate your Node.js agents from Agents 1.x to 1.5.

Overview

Agents 1.5 refines the Node.js API for defining agents and tools. The most significant change is a new functional API for building agents: Agent.create() and AgentTask.create(). These let you create custom agents without subclassing. Most other changes are additive or are backward-compatible.

Behavior change: Agents 1.5 breaking changes
  • ToolContext is now a class instead of a plain object that maps tool names to tools.
  • ProviderDefinedTool renamed to ProviderTool. This change only affects provider tool authors.

Migration summary

ChangeMigration impactRequired action
Tools can be passed as a flat arrayOptionalThe object map form still works for function tools. See Tools accept a flat array.
tool() requires a nameNoneOnly add name when you use the new array form; the object key still supplies it in the object form. See Tools accept a flat array.
Direct imports from @livekit/agentsOptionalThe voice and llm namespaces still work. See Simplified imports.
Agent.create() & AgentTask.create()OptionalA new alternative to subclassing. See Building agents with Agent.create.
beta.TaskGroup & beta.WarmTransferTaskDeprecatedBoth moved to the stable workflows namespace: workflows.TaskGroup, workflows.WarmTransferTask. The beta aliases still work but are deprecated and will be removed in a future release.
ToolContext is now a classBreakingUpdate code that reads toolCtx as a plain object. See ToolContext is now a class.
ProviderDefinedTool renamed to ProviderToolBreakingAffects provider tool authors only. Rename to ProviderTool / isProviderTool. See Provider tool API rename.

Simplified imports

Agent, AgentTask, AgentSession, RunContext, tool, ToolContext, ChatContext, and the other voice and LLM exports are now available directly from @livekit/agents. You can drop the voice. and llm. namespaces.

// Direct imports (simpler)
import { Agent, tool } from '@livekit/agents';
const agent = Agent.create({ instructions: '...', tools: [tool({ name: 'x', /* ... */ })] });
// Namespaced (supported for backward compatibility in 1.5)
import { llm, voice } from '@livekit/agents';
const agent = voice.Agent.create({ instructions: '...', tools: [llm.tool({ name: 'x', /* ... */ })] });

Tools accept a flat array

Tools can be passed to Agent, AgentTask, and AgentSession as a flat array of FunctionTool | ProviderTool | Toolset, where tool() takes a name.

import { Agent, tool } from '@livekit/agents';
const agent = Agent.create({
instructions: 'You are a helpful assistant.',
tools: [
tool({
name: 'getWeather',
description: 'Look up weather information for a given location.',
execute: async ({ location }) => getWeather(location),
}),
],
});
import { llm, voice } from '@livekit/agents';
const agent = new voice.Agent({
instructions: 'You are a helpful assistant.',
tools: {
getWeather: llm.tool({
description: 'Look up weather information for a given location.',
execute: async ({ location }) => getWeather(location),
}),
},
});
Backward compatibility

The legacy object form (tools: { getWeather: tool({ ... }) }) is still supported for function tools: the object key supplies the tool name internally, so tool() does not require a name in that form. Use the array form when you want to include Toolsets or provider tools, which the object form can't express.

In the array form, registering two different function-tool instances under the same name now throws duplicate function name: <name>. Before 1.5, tools were a name → tool map, so a later entry silently overrode an earlier one with the same name. Passing the same instance twice is a no-op.

Provider tool API rename

Behavior change: ProviderDefinedTool renamed to ProviderTool

This is the breaking change if your code references the provider tool base class or its typed guard directly.

This is a breaking change if, for example, you author your own provider tools. If you only consume the built-in provider tools (such as GoogleSearch or WebSearch), no change is required.

  • ProviderDefinedTool is renamed to ProviderTool, and it's now an abstract class. Construct a provider tool by instantiating a subclass; new ProviderTool(...) is rejected.
  • isProviderDefinedTool is renamed to isProviderTool.
  • The ToolType literal for provider tools changes from 'provider-defined' to 'provider'.

ToolContext is now a class

Behavior change: ToolContext is now a class

This is the breaking change most agent code hits in 1.5. The other breaking change, the ProviderTool API rename, affects only code that authors provider tools.

Before 1.5, ToolContext was a plain Record<string, FunctionTool>, a name → tool map. The agent.toolCtx property returned that object, so you could index it, call Object.keys() on it, spread it, or mutate it in place.

In 1.5, a tool can also be a Toolset, which a flat name → tool map can't represent. So ToolContext is now a dedicated class (this also lines up with the Python SDK), and agent.toolCtx returns a defensive copy: you can no longer mutate it in place. Use agent.updateTools() instead.

Use these accessors instead of treating it as a plain object:

AccessorReturns
toolCtx.toolsThe tools as passed, with toolsets not expanded: (FunctionTool | ProviderTool | Toolset)[].
toolCtx.functionToolsAll function tools as a legacy-style name → tool map, including those nested in toolsets: Record<string, FunctionTool>.
toolCtx.providerToolsAll provider tools, including those nested in toolsets: ProviderTool[].
toolCtx.toolsetsThe registered toolsets: Toolset[].
toolCtx.getFunctionTool(name)The function tool with that name, or undefined if none is registered.
toolCtx.hasTool(name)true if a function or provider tool with that name is registered, otherwise false.
toolCtx.flatten()All function and provider tools, with toolset tools expanded: Tool[].

Migrating toolCtx access

Use ToolContext methods and properties to look up, check, and iterate over tools.

// Look up a tool by name
agent.toolCtx.getFunctionTool('getWeather');
// List tool names
Object.keys(agent.toolCtx.functionTools);
// Check whether a tool exists
agent.toolCtx.hasTool('getWeather');
// Iterate tools
agent.toolCtx.tools; // (includes toolsets and provider tools)
// Look up a tool by name
agent.toolCtx['getWeather'];
// List tool names
Object.keys(agent.toolCtx);
// Check whether a tool exists
'getWeather' in agent.toolCtx;
// Iterate tools
Object.values(agent.toolCtx);
Note

In a custom llmNode override, the toolCtx argument is now a ToolContext instance. Passing it straight through to llm.chat({ toolCtx }) or Agent.default.llmNode(...) still works, since both accept a ToolContext. You only need to make changes if you previously iterated toolCtx as a plain object.

Updating tools at runtime

agent.updateTools() now takes an array. Reading agent.toolCtx returns a snapshot: a copy of the agent's tools at that moment, not a live reference. Changing it in place has no effect on the agent. To update the agent's tools, build a new list from agent.toolCtx.tools and pass it to updateTools():

// Add a tool
await agent.updateTools([...agent.toolCtx.tools, toolA]);
// Remove a tool
await agent.updateTools(agent.toolCtx.tools.filter((t) => t.id !== 'toolA'));
// Replace all tools
await agent.updateTools([toolA, toolB]);

Building agents with Agent.create

In addition to subclassing, you can now build agents and tasks functionally with Agent.create() and AgentTask.create(), passing lifecycle and pipeline hooks instead of overriding methods. Hooks receive a ctx object first.

Subclassing is still supported for backward compatibility.

import { Agent } from '@livekit/agents';
class MyAgent extends Agent {
constructor() {
super({ instructions: 'You are a helpful assistant.' });
}
async onEnter(): Promise<void> {
this.session.generateReply({ instructions: 'Greet the user.' });
}
}
import { Agent } from '@livekit/agents';
const agent = Agent.create({
instructions: 'You are a helpful assistant.',
onEnter(ctx) {
ctx.session.generateReply({ instructions: 'Greet the user.' });
},
});

Available hooks are onEnter, onExit, onUserTurnCompleted, and the pipeline nodes sttNode, llmNode, ttsNode, realtimeAudioOutputNode, and transcriptionNode. Stream node hooks receive and return AsyncIterable values, so you can use async generators instead of constructing a ReadableStream. Less common overrides such as onUserTurnExceeded aren't exposed as create() hooks. To customize those, subclass Agent and override the method.

import { Agent } from '@livekit/agents';
const agent = Agent.create({
instructions: 'You are a helpful assistant.',
async ttsNode(ctx, text, modelSettings) {
async function* replaceWords(input: AsyncIterable<string>) {
for await (const chunk of input) {
yield chunk.replace(/\bLiveKit\b/gi, 'Live Kit');
}
}
return Agent.default.ttsNode(ctx.agent, replaceWords(text), modelSettings);
},
});

A node hook can also be written directly as an async * generator that produces values with yield, instead of returning a stream. This is often the simplest form when post-processing the default node's output:

import { Agent } from '@livekit/agents';
const agent = Agent.create({
instructions: 'You are a helpful assistant.',
async *llmNode(ctx, chatCtx, toolCtx, modelSettings) {
const stream = await Agent.default.llmNode(ctx.agent, chatCtx, toolCtx, modelSettings);
if (!stream) return;
for await (const chunk of stream) {
// inspect or transform chunks here, then yield them downstream
yield chunk;
}
},
});

Subclassing remains fully supported, so existing agents keep working without changes.

New capabilities in 1.5

Agents 1.5 also adds Node.js support for features that were previously Python-only:

  • Toolsets: manage groups of tools that share setup and teardown, such as database connections or websockets.
  • Async tools: keep talking while a long-running tool runs, with progress updates and filler speech.
  • Provider tools: built-in server-side tools that the LLM provider runs within a single API call, such as GoogleSearch and GoogleMaps for Gemini, or WebSearch and CodeInterpreter for OpenAI. In Node.js, provider tools are available for OpenAI and Gemini.
  • LLMStream.collect(): await a full chat response as a single object.
  • Tool mocking: mock tools in tests via withMockTools.