LiveKit Agents for Node.js 1.5.0 is coming (Feedback Wanted)

Update: 1.5.0 pre-release is up (0.0.0-next-20260624041820)


Hey everyone — we’re getting close to releasing LiveKit Agents for Node.js 1.5.0, a minor version focused on making the Node.js API more ergonomic. The headline is a new functional way to build agents — Agent.create() / AgentTask.create(), so you can create a custom agent without extending a class — plus a batch of new capabilities (toolsets, async/background tools, provider tools, LLMStream.collect(), and more). It also brings a few things in line with the Python SDK along the way, but it’s primarily a JS-side upgrade.

Because this is a version bump with a few breaking changes around tools and ToolContext, we want to gather feedback before the full release and run a pre-release that folks can try first.

Please skim the changes below and let us know in the thread, specifically, do you read or mutate agent.toolCtx directly (index it, Object.keys, spread it)? See the ToolContext section — that’s the one true breaking change.


1. Tools are now a flat array (object form still works)

tool() now takes a name, and tools is a flat array of FunctionTool | ProviderTool | Toolset.

// 1.5+
import { Agent, tool } from '@livekit/agents';

const agent = Agent.create({
  instructions: 'You are a helpful assistant.',
  tools: [
    tool({
      name: 'getWeather',
      description: 'Look up weather for a location.',
      execute: async ({ location }) => getWeather(location),
    }),
  ],
});

Backward compatible: the legacy object form still works for function tools — the object key supplies the tool name:

tools: {
  getWeather: tool({ description: '...', execute: async ({ location }) => getWeather(location) }),
}

Use the array form when you want to include toolsets or provider tools (the object form can’t express those).

2. ToolContext is now a class (the one breaking change)

Previously, ToolContext was effectively a Record<string, FunctionTool> — a plain name→tool mapping. The toolCtx / tools getters on Agent, AgentSession, and AgentActivity returned that map, so you could index it, Object.keys() it, or spread it.

With toolsets, a flat name → tool mapping no longer captures everything (a toolset is a unit with its own lifecycle), so ToolContext is now a dedicated class (this also lines up with Python):

agent.toolCtx.tools; // readonly (FunctionTool | ProviderTool | Toolset)[]
agent.toolCtx.functionTools; // Record<string, FunctionTool>  (the legacy-style map)
agent.toolCtx.providerTools; // ProviderTool[]
agent.toolCtx.toolsets; // readonly Toolset[]
agent.toolCtx.getFunctionTool('getWeather');
agent.toolCtx.hasTool('getWeather');
agent.toolCtx.flatten(); // Flatten the tool context to a list of tools

Migration:

// Before                                  // 1.5+
agent.toolCtx['getWeather']              → agent.toolCtx.getFunctionTool('getWeather')
Object.keys(agent.toolCtx)               → Object.keys(agent.toolCtx.functionTools)
await agent.updateTools({ ...agent.toolCtx, newTool })
                                         → await agent.updateTools([...agent.toolCtx.tools, newTool])

Good news: llm.chat({ toolCtx }), new Agent({ tools }), and agent.updateTools(...) all still accept a plain name→tool object or an array, so most call sites don’t change. The break is specifically if you consume agent.toolCtx as a plain object.

3. New: build agents without subclassing — Agent.create() / AgentTask.create()

You no longer have to extends Agent to build a custom agent. Pass your instructions, tools, and hooks (onEnter, onExit, onUserTurnCompleted, onUserTurnExceeded, and the pipeline nodes sttNode/llmNode/ttsNode/realtimeAudioOutputNode) directly to Agent.create() — hooks receive a ctx object instead of this. The same applies to tasks via AgentTask.create().

import { Agent, AgentTask, tool } from '@livekit/agents';

// A custom agent — no class needed
const agent = Agent.create({
  instructions: 'You are a helpful assistant.',
  tools: [
    tool({
      name: 'getWeather',
      description: '...',
      execute: async ({ location }) => getWeather(location),
    }),
  ],
  onEnter(ctx) {
    ctx.session.generateReply({ instructions: 'Greet the user.' });
  },
});

// A custom task — also no class needed
const collectName = AgentTask.create<string>({
  instructions: 'Ask the user for their name.',
  onEnter(ctx) {
    ctx.session.generateReply({ instructions: 'Ask the user for their name.' });
  },
});

For now, all pipeline node hooks — such as the STT node (sttNode) and TTS node (ttsNode) — accept the async iterable / async generator pattern. You can iterate the input directly and yield values, instead of constructing a ReadableStream:

import { Agent } from '@livekit/agents';

const agent = Agent.create({
  instructions: 'You are a helpful assistant.',
  // ttsNode written as an async generator
  async *ttsNode(ctx, text, modelSettings) {
    const frames = await Agent.default.ttsNode(ctx.agent, text, modelSettings);

    if (!frames) return;

    for await (const frame of frames) {
      yield postProcess(frame); // post-process audio frames here
    }
  },
});

This is purely additive — subclassing Agent / AgentTask still works exactly as before.

4. New capabilities (previously Python-only)

  • Toolsets — manage groups of tools that share setup and teardown (DB connections, websockets, MCP clients).
  • Async tools — keep talking while a long-running tool runs, with progress updates and filler speech.
  • Provider tools — model-lab server-side tools (OpenAI WebSearch/FileSearch/CodeInterpreter, Gemini GoogleSearch/GoogleMaps/…).
  • LLMStream.collect() — await a full chat response as a single object (text, toolCalls, usage).
  • withMockTools — mock tools in tests.
  • Direct importsAgent, AgentSession, AgentTask, tool, etc. are now exported directly from @livekit/agents, so the voice./llm. prefixes are optional.

Release plan

We’re publishing a pre-release (not @latest), available early next week, so teams can validate their setups and get familiar with the new API before we cut the official 1.5.0. This also avoids anyone on @latest in prod accidentally pulling breaking changes before they’ve migrated.

Drop a comment if: you use ToolContext directly, you hit anything that doesn’t migrate cleanly. Thanks! :folded_hands:

7 Likes

Hi Brian,

Super excited for this release and thank you for the teams hard work. My company Acuity Health builds telephony AI scheduling medical receptionists. Here are some of my inputs for your above questions:

We are not indexing, using Object.keys(), or spreading toolCtx heavily in production yet. But we are moving toward a dynamic tool model where we likely would need those patterns.

Indexing: yes, we’d want to fetch/check specific tools by name based on patient state or workflow state.

Object.keys(): yes, we’d want to inspect which tools are currently active for debugging, validation, and possibly observability.

Spreading: yes, or the equivalent, we’d want to add/remove tools dynamically during a session as the patient moves through states, for example matched patient, new patient, urgent symptom, insurance flow, scheduling flow, refill request, etc.

1 Like

Hi Chase, thanks a lot for the detailed feedback.

In 1.5, those patterns should still be supported, but through the ToolContext APIs instead of treating toolCtx itself as a plain object.

For the cases you mentioned:

// Check/fetch a specific function tool by name
const tool = agent.toolCtx.getFunctionTool('scheduleAppointment');

if (agent.toolCtx.hasTool('scheduleAppointment')) {
  // tool is currently available
}

For debugging / observability:

// Function tools only, legacy-style name -> tool map
const functionToolNames = Object.keys(agent.toolCtx.functionTools);

// Everything currently registered, including provider tools / toolsets
const allTools = agent.toolCtx.flatten();

For dynamically adding/removing tools during a session, the recommended path is to construct the new tool list and call updateTools:

// Add a tool
await agent.updateTools([
  ...agent.toolCtx.tools,
  schedulingTool,
]);

// Remove a function tool by name
await agent.updateTools(
  agent.toolCtx.tools.filter((tool) => tool.name !== 'schedulingTool'),
);
2 Likes

Thank you so much! Looking forward to testing the preview. Is it available now?

Yes, the pre-release is up (0.0.0-next-20260624041820)!

2 Likes

Thank you!

Any change the production release is ready today so I can migrate over the weekend?

Super excited!

1 Like

Hey @chase_fagen, we will going to release the official 1.5.0 early next week!