Tools API overhauled; TaskGroup and WarmTransferTask now stable
@livekit/agents@1.5.0
Minor Changes
-
Port async tool execution semantics from Python: tools can release their turn with
ctx.update(), - #1674 (@toubatbrian)AsyncToolsetcontrols session/activity scope, and cancellable tools expose task-management helpers. -
BREAKING:
Agent({ tools })andagent.updateTools()now accept a flat list(FunctionTool | ProviderTool | Toolset)[]instead of aRecord<string, FunctionTool>map, andllm.tool({ ... })requires anamefield.ToolContextis now a Python-parity class withfunctionTools/providerTools/toolsetsaccessors, plusflatten(),hasTool(id),getFunctionTool(id),updateTools(),copy(), andequals(). To match the Python reference, registering two different function-tool instances under the samenamenow throwsduplicate function name: <name>instead of silently overriding the earlier entry; passing the same instance twice is a no-op.agent.toolCtxreturns a defensive copy so callers can no longer mutate the agent's internal state.LLM.chat({ toolCtx })accepts either aToolContextinstance or a raw(FunctionTool | ProviderTool | Toolset)[]array (ToolCtxInput) and normalizes it internally, so callers don't have to construct aToolContextthemselves. - #1674 (@toubatbrian)Tools also expose an
id: stringfield on the baseToolinterface (parity with Python'sTool.idproperty): forFunctionToolit mirrorsname, forProviderToolit is the provider tool id.ToolContextkeys and equality now usetool.idconsistently.BREAKING: Provider tools are now modeled to match Python's
ProviderTool:ProviderDefinedToolis renamed toProviderTool, andisProviderDefinedToolis renamed toisProviderTool.ProviderToolis now an abstract class (Python parity). Plugins must subclass it (class WebSearch extends ProviderTool { ... }) to attach provider-specific fields and serializers; barenew ProviderTool(...)is rejected at compile time.- The
tool({ id })factory overload is removed;tool({ ... })only creates function tools now. Construct provider tools by instantiating aProviderToolsubclass. - The
ToolTypeliteral for provider tools is renamed from'provider-defined'to'provider'.
Toolsetnow carries aTOOLSET_SYMBOLmarker and is detected via a newisToolset()guard (consistent withisFunctionTool/isProviderTool). Existinginstanceof Toolsetchecks still work, but symbol-based detection is preferred for cross-realm safety. -
Add beta EndCallTool for ending calls from agent tools - #1674 (@toubatbrian)
-
Promote the
TaskGroupworkflow out of beta. It now lives in the stableworkflowsnamespace — import it asworkflows.TaskGroup(withworkflows.TaskCompletedEvent,workflows.TaskGroupOptions, andworkflows.TaskGroupResult) instead ofbeta.TaskGroup. Thebetanamespace continues to re-export these symbols as deprecated aliases for backward compatibility; they will be removed in a future release. This is an intentional Node.js-only change; in PythonTaskGroupremains underbeta.workflows. - #1674 (@toubatbrian) -
Promote the
WarmTransferTaskworkflow out of beta. It now lives in the stableworkflowsnamespace — import it asworkflows.WarmTransferTask(withworkflows.WarmTransferResult,workflows.WarmTransferTaskOptions, andworkflows.InstructionParts) instead ofbeta.WarmTransferTask. Thebetanamespace continues to re-export these symbols as deprecated aliases for backward compatibility; they will be removed in a future release. - #1674 (@toubatbrian)
Patch Changes
-
Add
Agent.updateInstructions()to update an agent's instructions mid-session (parity with Python). The change propagates the new instructions through the activeAgentActivity, records anAgentConfigUpdatein the chat and session history, and syncs the realtime/stateless contexts. For OpenAI realtime, per-response instructions now preserve the session-level instructions instead of replacing them. - #1674 (@toubatbrian) -
Adaptive interruption detection now omits the threshold from
session.createunless the user explicitly overrides it, letting the gateway apply its fetched default (surfaced viadefault_thresholdonsession.created). The HTTP transport has been dropped — detection always connects over WebSocket and always requires LiveKit credentials, and its base URL now defaults fromLIVEKIT_INFERENCE_URLinstead ofLIVEKIT_REMOTE_EOT_URL. Inference requests also send anX-LiveKit-Worker-Tokenheader whenLIVEKIT_WORKER_TOKENis set (hosted agents); a token supplied via the--worker-tokenCLI flag is now re-exported into the environment so forked job subprocesses inherit it and include the header. TheX-LiveKit-Agent-Idheader is now only attached once the room is connected to avoid leaking an unset local-participant SID. The interruption WebSocket is now closed deterministically on stream teardown (including error and cancel paths) instead of only on graceful completion — previously an orphaned socket leaked per session/activity and accumulated for the worker's lifetime. Mid-session threshold/duration changes viaupdateOptionsnow reconnect the WebSocket in place rather than closing it and letting the next send error the stream — so option changes no longer consume a failover retry (previously enough updates in a session could exhaust the retry budget and stop interruption detection). - #1674 (@toubatbrian) -
Add Agent.create method - #1674 (@toubatbrian)
-
Add scoped filler support to RunContext - #1674 (@toubatbrian)
-
Don't retain recorded events when recording is disabled - #1674 (@toubatbrian)
-
Add Inworld
delivery_modeto inference TTS model options. - #1674 (@toubatbrian) -
List available tools in unknown-function error output. - #1674 (@toubatbrian)
-
Add
LLMStream.collect()for awaiting the full response of a chat stream as a single object (text, tool calls, usage, extra). - #1674 (@toubatbrian) -
feat(testing): add
mockToolsutility to override an Agent's tool implementations within an async context, mirroring the Pythonmock_toolsAPI - #1674 (@toubatbrian) -
Add object-map tool syntax compatibility. - #1674 (@toubatbrian)
-
Fix interrupted assistant speech being dropped from chat history when the audio output provides no playback-aligned transcript. On a mid-playout interruption,
forwardedTextFor(and the realtime reply path) returnedsynchronizedTranscript ?? '', so an interrupted-but-heard reply produced noconversation_item_addedand was missing fromchatCtx— common with avatar outputs that don't emit a synchronized transcript. The commit now falls back to the forwarded generation text (textOut.text), matching the Python SDK's_ForwardOutput.forwarded_textbehavior. - #1916 (@ShayneP) -
Adds base
Toolsetsupport: a stateful container for a group of tools withsetup()/aclose()lifecycle hooks. Toolsets can be passed directly intoAgent({ tools: [...] })alongside individual function tools; their tools are flattened into the agent'sToolContextand the runtime drivessetup()on activity start,aclose()on close, and a setup/close diff whenagent.updateTools()adds or removes Toolsets mid-session. Per-toolsetsetup()errors are logged but do not abort the activity. TheIGNORE_ON_ENTERflag is also respected for function tools nested inside a Toolset. Every LLM and realtime plugin tool builder iteratesToolContext.flatten()so toolset-contributed tools are correctly advertised. Also exportsToolCalledEvent/ToolCompletedEventpayload types. - #1674 (@toubatbrian) -
fix(voice): scope forwardAudio's playback-started listener to its own segment - #1674 (@toubatbrian)
When a speech is interrupted, the scheduling loop immediately authorizes the next speech, so the new segment's
forwardAudioregisters itsplayback_startedlistener on the shared audio output while the interrupted segment is still emitting events during teardown. The stray event resolved the new segment'sfirstFrameFutbefore its first frame was captured, which skipped resampler creation and pushed an unresampled frame straight to theAudioSource(RtcError: sample_rate and num_channels don't match) and corrupted playback bookkeeping. The listener now only resolvesfirstFrameFutafter the segment has captured its own first frame. -
chore(deps): update livekit dependency - #1674 (@toubatbrian)
-
Increase the default worker drain timeout to one hour. - #1930 (@rosetta-livekit-bot)
-
fix(eot): disable retries for eot connections and errors - #1674 (@toubatbrian)
-
Pipeline nodes (
ttsNode,sttNode,transcriptionNode,realtimeAudioOutputNode) now accept an async generator/iterable as their stream input end-to-end. This includes the staticAgent.default.*helpers and theAgent.create()/AgentTask.create()hook overrides, so overrides can pass a generator directly tovoice.Agent.default.<node>(ctx.agent, generator, settings)without wrapping it intoStream()first. Also fixes agetReader is not a functioncrash when anAgent.create/AgentTask.createstream hook received a plainAsyncIterable. - #1674 (@toubatbrian) -
fix(warm-transfer): capture job context for post-merge caller-room cleanup - #1674 (@toubatbrian)
WarmTransferTask's post-mergeRoomEvent.ParticipantDisconnectedlistener calledgetJobContext()to build aRoomServiceClientand delete the caller room. That handler runs from a native rtc-node FFI callback whoseAsyncLocalStoragecontext is pinned toFfiClient-singleton creation, not to the job's context — sogetJobContext()read an empty (or stale) store and threw, surfacing as an unhandled promise rejection and leaving the 2-party SIP room undeleted when a participant hung up after the bridge. The task now captures theJobContexteagerly inonEnter()(while the live context is available) and usesjobCtx.deleteRoom()in the late handler, which also passes the job's API credentials instead of relying on environment variables.
Fetched July 2, 2026

