How to Implement Dynamic Tool Creation in LiveKit Agents

LiveKit Agents enables dynamic tool creation through three distinct patterns: initializing tools at construction time with external data, updating the toolset at runtime via update_tools(), or injecting temporal tools inside llm_node() for single-generation use.

LiveKit Agents is an open-source framework for building voice and multimodal AI agents. While agents typically expose static functions to LLMs, dynamic tool creation allows you to modify the available toolset at runtime—loading capabilities from external databases, responding to user-driven requests, or creating temporary helpers for specific conversation turns.

The Three Patterns for Dynamic Tool Creation

The framework supports dynamic tool injection at three distinct lifecycle moments, each serving different architectural needs.

1. Construction-Time Tool Loading

Initialize tools when instantiating the agent, ideal for loading external configurations or database-driven capabilities before the session begins.

In livekit-agents/livekit/agents/voice/agent.py (lines 55-64), the Agent constructor builds the initial ToolContext from the tools argument and any methods decorated with @function_tool on your subclass:

self._tools = [*tools, *find_function_tools(self)]
self._chat_ctx = chat_ctx.copy(tools=self._tools) if chat_ctx else ChatContext.empty()

Example: Loading course listings from a database to generate enum-backed tools:

from enum import Enum
from pydantic import BaseModel
from livekit.agents import Agent, function_tool

# Load external data

courses = await _get_course_list_from_db()
CourseType = Enum("CourseType", {c.replace(" ", "_"): c for c in courses})

class CourseInfo(BaseModel):
    course: CourseType
    location: Literal["online", "in-person"]

async def _get_course_info(info: CourseInfo) -> str:
    return f"Course details for {info.course} at {info.location}"

# Inject at construction

agent = MyAgent(
    instructions="You are a course assistant.",
    tools=[function_tool(_get_course_info, name="get_course_info")]
)

2. Runtime Tool Updates

Add or modify tools after the agent is already running using Agent.update_tools(). This method validates new tools, rebuilds the internal ToolContext, and propagates changes to active real-time sessions.

According to livekit-agents/livekit/agents/voice/agent.py (lines 49-78), update_tools() performs deduplication by tool ID and refreshes the chat context:

await agent.update_tools(agent.tools + [new_tool])

Use case: Enabling a "random number generator" only after the user specifically requests it:

async def _random_number() -> int:
    import random
    return random.randint(1, 100)

# Add capability mid-session

await agent.update_tools(
    agent.tools + [function_tool(_random_number, name="random_number")]
)

3. Temporal Tools for Single Calls

Inject tools that exist only for the current LLM generation by modifying the mutable tools list inside a custom llm_node() implementation. These temporal tools do not persist to subsequent turns unless explicitly added again.

from livekit.agents import Agent, llm, function_tool

class WeatherAgent(Agent):
    async def llm_node(
        self,
        chat_ctx: llm.ChatContext,
        tools: list[llm.Tool | llm.Toolset],
        model_settings: llm.ModelSettings,
    ):
        # Create a one-off weather lookup for this turn only

        async def _get_weather(location: str) -> str:
            return f"The weather in {location} is sunny."
        
        tools.append(function_tool(
            _get_weather, 
            name="get_weather", 
            description="Lookup current weather"
        ))
        
        # Continue with the extended tool list

        return Agent.default.llm_node(self, chat_ctx, tools, model_settings)

How Dynamic Tool Creation Works

Understanding the underlying mechanism ensures you implement updates correctly without side effects.

The function_tool Decorator

The @function_tool decorator (or direct function call) wraps Python callables into FunctionTool or RawFunctionTool instances that LLM providers can invoke. Defined in livekit-agents/livekit/agents/llm/tool_context.py (lines 49-80), the decorator attaches metadata including the function name, description, and parameter schemas via the __livekit_tool_info attribute.

@function_tool
def my_tool(param: str) -> str:
    return f"Result: {param}"

ToolContext Validation and Deduplication

The ToolContext class acts as a stateless container that indexes all available function tools. When you call update_tools(), the framework validates that each item is a FunctionTool, RawFunctionTool, or Toolset, then builds a deduplicated dictionary keyed by tool ID to prevent conflicts.

Real-Time Session Propagation

If your agent has an active real-time session when update_tools() is called, the framework automatically forwards the new tool list to the LLM provider via self._activity.update_tools. This ensures the remote model immediately sees the updated capabilities without requiring session reinitialization.

Complete Working Example

Below is a self-contained implementation demonstrating all three patterns in the repository's dynamic tool creation example (examples/voice_agents/dynamic_tool_creation.py):

from livekit.agents import Agent, function_tool, llm
from typing import Literal
from pydantic import BaseModel
from enum import Enum

class FlexibleAgent(Agent):
    async def llm_node(
        self,
        chat_ctx: llm.ChatContext,
        tools: list[llm.Tool | llm.Toolset],
        model_settings: llm.ModelSettings,
    ):
        # Pattern 3: Temporal tool for this turn only

        async def _hello(name: str) -> str:
            return f"👋 Hello, {name}!"
        
        tools.append(function_tool(
            _hello, 
            name="say_hello", 
            description="Greet a user by name"
        ))
        
        return Agent.default.llm_node(self, chat_ctx, tools, model_settings)

# Pattern 1: Construction-time injection

agent = FlexibleAgent(instructions="You are a helpful assistant.")

# Pattern 2: Runtime update

async def _double(x: int) -> int:
    return x * 2

await agent.update_tools(
    agent.tools + [function_tool(_double, name="double", description="Double a number")]
)

Summary

LiveKit Agents provides robust dynamic tool creation capabilities through these key mechanisms:

  • Construction-time injection passes tools to Agent.__init__() for static initialization with external data
  • Runtime updates via await agent.update_tools() persistently modify the toolset and propagate to active real-time sessions
  • Temporal injection inside llm_node() allows single-turn tool availability without persistent side effects
  • The framework handles validation, deduplication by tool ID, and automatic context rebuilding in livekit-agents/livekit/agents/voice/agent.py

Frequently Asked Questions

What is the difference between update_tools() and llm_node() tool injection?

update_tools() permanently modifies the agent's toolset for the remainder of the session, updating the internal ToolContext and propagating changes to real-time providers. Injecting tools inside llm_node() only affects the current LLM generation turn; the tool disappears on subsequent calls unless you append it again.

Can I remove tools dynamically after adding them?

Yes. Call update_tools() with a filtered list containing only the tools you want to keep. The framework replaces the entire internal tool list with your new collection, effectively removing any tools you omit from the argument.

Does dynamic tool creation work during active real-time sessions?

Yes. When update_tools() detects an active session (via self._activity), it automatically pushes the updated tool definitions to the LLM provider. This happens in the background without interrupting the conversation flow.

How does the framework handle duplicate tool definitions?

The update_tools() implementation in agent.py builds a dictionary keyed by tool ID to deduplicate entries. If you pass multiple tools with the same identifier, the last one in the list takes precedence, ensuring the LLM receives a clean, conflict-free tool schema.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →