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.
ToolContextis now a class instead of a plain object that maps tool names to tools.ProviderDefinedToolrenamed toProviderTool. This change only affects provider tool authors.
Migration summary
| Change | Migration impact | Required action |
|---|---|---|
| Tools can be passed as a flat array | Optional | The object map form still works for function tools. See Tools accept a flat array. |
tool() requires a name | None | Only 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/agents | Optional | The voice and llm namespaces still work. See Simplified imports. |
Agent.create() & AgentTask.create() | Optional | A new alternative to subclassing. See Building agents with Agent.create. |
beta.TaskGroup & beta.WarmTransferTask | Deprecated | Both 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 class | Breaking | Update code that reads toolCtx as a plain object. See ToolContext is now a class. |
ProviderDefinedTool renamed to ProviderTool | Breaking | Affects 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),}),},});
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
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.
ProviderDefinedToolis renamed toProviderTool, and it's now an abstract class. Construct a provider tool by instantiating a subclass;new ProviderTool(...)is rejected.isProviderDefinedToolis renamed toisProviderTool.- The
ToolTypeliteral for provider tools changes from'provider-defined'to'provider'.
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:
| Accessor | Returns |
|---|---|
toolCtx.tools | The tools as passed, with toolsets not expanded: (FunctionTool | ProviderTool | Toolset)[]. |
toolCtx.functionTools | All function tools as a legacy-style name → tool map, including those nested in toolsets: Record<string, FunctionTool>. |
toolCtx.providerTools | All provider tools, including those nested in toolsets: ProviderTool[]. |
toolCtx.toolsets | The 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 nameagent.toolCtx.getFunctionTool('getWeather');// List tool namesObject.keys(agent.toolCtx.functionTools);// Check whether a tool existsagent.toolCtx.hasTool('getWeather');// Iterate toolsagent.toolCtx.tools; // (includes toolsets and provider tools)
// Look up a tool by nameagent.toolCtx['getWeather'];// List tool namesObject.keys(agent.toolCtx);// Check whether a tool exists'getWeather' in agent.toolCtx;// Iterate toolsObject.values(agent.toolCtx);
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 toolawait agent.updateTools([...agent.toolCtx.tools, toolA]);// Remove a toolawait agent.updateTools(agent.toolCtx.tools.filter((t) => t.id !== 'toolA'));// Replace all toolsawait 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 downstreamyield 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
GoogleSearchandGoogleMapsfor Gemini, orWebSearchandCodeInterpreterfor 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.