How to Use Function Tools with LiveKit Agents: Complete Implementation Guide

LiveKit Agents enable large language models (LLMs) to invoke Python functions automatically by decorating methods with @function_tool, collecting them in the agent's ToolContext, and emitting FunctionToolsExecutedEvent results that are re-inserted into the conversation context.

Function tools transform your voice agents from passive responders into active systems capable of querying databases, calling external APIs, and executing business logic. In the livekit/agents framework, tools are first-class citizens that bridge natural language understanding with programmatic action.

Defining Function Tools with the @function_tool Decorator

The @function_tool decorator in livekit/agents/llm/tool_context.py converts Python callables into FunctionTool objects that LLMs can discover and invoke. When applied to an async method, the decorator introspects type hints and docstrings to generate a JSON Schema automatically.

from livekit.agents.llm import function_tool
from livekit.agents.voice import Agent

class WeatherAgent(Agent):
    @function_tool
    async def get_weather(self, latitude: str, longitude: str):
        """Return current temperature for the given coordinates."""
        # External API call implementation

        return {"temperature": 22, "unit": "C"}

Source: [examples/voice_agents/weather_agent.py](https://github.com/livekit/agents/blob/main/examples/voice_agents/weather_agent.py#L24-L38)

The decorator creates a FunctionTool instance that stores the callable, its name, description, and parameter schema. This metadata allows the LLM provider (OpenAI, Anthropic, Google) to understand when and how to invoke the function during a conversation.

Raw JSON Schema Tools

For complex schemas or OpenAI-compatible definitions, use raw_schema=RawFunctionDescription to bypass automatic introspection. This approach is implemented in the same tool_context.py file and creates a RawFunctionTool instead.

from livekit.agents.llm import function_tool, RawFunctionDescription

@function_tool(
    raw_schema=RawFunctionDescription(
        name="search_web",
        description="Search the web and return snippets.",
        parameters={
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"]
        },
    )
)
async def search_web(query: str):
    # Web search implementation

    return {"results": ["snippet 1", "snippet 2"]}

Source: [examples/voice_agents/raw_function_description.py](https://github.com/livekit/agents/blob/main/examples/voice_agents/raw_function_description.py#L30-L38)

Registering Tools with Your Agent

The base Agent class automatically discovers decorated methods through ToolContext. When you instantiate an agent, the constructor calls self._collect_tools() (defined in livekit/agents/voice/agent.py), which scans the class for @function_tool decorators and populates the internal registry.

class MyAgent(Agent):
    def __init__(self):
        super().__init__(
            instructions="You are a helpful assistant.",
            llm=openai.realtime.RealtimeModel(),
            # Tools are auto-collected from decorated methods

        )

The ToolContext class (in livekit/agents/llm/tool_context.py) maintains a mapping of name → FunctionTool, enabling fast lookup when the LLM issues a call. You can also manually pass a Toolset or list of tools to the constructor if you need to share tools across multiple agent instances.

LLM Integration and Tool Invocation

When the agent creates a ChatContext (in livekit/agents/voice/agent.py), it copies the tool list into ChatContext.tools. This context propagates to the LLM provider via the LLM.chat method in livekit/agents/llm/llm.py, which streams the available functions to the underlying API.

The LLM returns a FunctionToolCall object containing the tool name and JSON-encoded arguments. The framework captures this in the streaming response and prepares for execution without blocking the conversation flow.

Executing Tool Calls and Handling Results

Execution occurs in livekit/agents/llm/utils.py through the execute_function_call function. This utility:

  1. Resolves the tool name against the ToolContext
  2. Deserializes JSON arguments into Python types
  3. Injects the optional RunContext if the function signature requires it
  4. Awaits async results or handles sync returns
  5. Wraps outputs or exceptions into FunctionCallOutput objects

# Pseudocode representing the internal execution flow

output = await execute_function_call(
    tool_context=self._tool_context,
    function_call=function_tool_call,
    run_context=run_context
)

After execution, the FunctionCallOutput is automatically inserted back into the conversation history, allowing the LLM to generate a contextual response based on the tool's data.

Event-Driven Tool Execution Flow

Once tools execute, the agent emits a FunctionToolsExecutedEvent (defined in livekit/agents/voice/events.py). This event carries both the original FunctionToolCall and the resulting FunctionCallOutput, enabling client-side reactions such as displaying tool results or modifying UI state.

@session.on("function_tools_executed")
def on_tool_executed(ev: FunctionToolsExecutedEvent):
    for call, out in ev.zipped():
        print(f"Tool {call.name} returned: {out.output}")
        # Optional: Prevent the agent from speaking the result

        # ev.cancel_tool_reply()

The RemoteSession class in livekit/agents/voice/client_events.py bridges these server-side events to the frontend via RTC data channels, ensuring real-time synchronization between voice agents and client applications.

Summary

  • Define tools using @function_tool for automatic schema generation or raw_schema=RawFunctionDescription for manual control, both located in livekit/agents/llm/tool_context.py.
  • Auto-registration occurs via Agent.__init__ calling _collect_tools(), which populates a ToolContext mapping tool names to callables.
  • LLM propagation happens through ChatContext.tools in livekit/agents/voice/agent.py, sending schemas to providers like OpenAI or Anthropic.
  • Execution is handled by execute_function_call in livekit/agents/llm/utils.py, which handles argument parsing, async invocation, and result wrapping.
  • Event emission via FunctionToolsExecutedEvent allows client-side handling through the function_tools_executed event, with optional reply cancellation via cancel_tool_reply().

Frequently Asked Questions

What is the difference between @function_tool and raw_schema tools?

@function_tool without arguments automatically generates JSON Schema from Python type hints and docstrings, creating a standard FunctionTool object. @function_tool(raw_schema=RawFunctionDescription(...)) creates a RawFunctionTool that uses your exact schema definition, bypassing introspection. Use raw schemas when you need provider-specific parameter formats or complex nested structures that type hints cannot express.

How does the Agent automatically discover function tools?

The base Agent class constructor calls self._collect_tools() (in livekit/agents/voice/agent.py), which scans the instance for methods decorated with @function_tool. These are registered in a ToolContext object that maps function names to their implementations. You do not need to manually list tools unless you are using external functions or sharing a Toolset across agents.

Can I cancel a tool reply after execution?

Yes. The FunctionToolsExecutedEvent emitted after tool execution includes a cancel_tool_reply() method. Call this inside your event handler to prevent the agent from verbally acknowledging the tool result, useful when you want to handle the output silently or trigger a hand-off to another system instead.

How are tool outputs returned to the LLM?

After execute_function_call runs the function (in livekit/agents/llm/utils.py), it wraps the return value in a FunctionCallOutput object. The agent inserts this output into the ChatContext as a function result message, then sends the updated context back to the LLM. The model receives the JSON output and generates its next response based on that data, creating a seamless tool-use conversation loop.

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 →