Skip to content

Tools

Tools registered on an agent are offered to the realtime model and execute on your backend. The session validates arguments, applies retries, runs tools concurrently, returns results to the provider, and records ordinary tool-call messages for later handoff. Capability hooks around tool calls are covered in Capabilities and hooks.

Function tools

When a model calls a tool, the session emits FunctionToolCallEvent, runs the tool, returns the result, and emits FunctionToolResultEvent. Parse failures and ModelRetry produce a RetryPromptPart, matching a standard agent run. Other tool exceptions end the session and propagate from iteration.

Tool return values reach the model exactly as in a standard run: the model receives the string rendering of the return value — plus, where the provider supports it, multimodal content attached via ToolReturn’s content — while local history keeps the full structured ToolReturnPart with its return_value, content, and metadata. Attached content is delivered for real or refused loudly — never silently degraded: OpenAI and Azure OpenAI deliver text and images as a follow-up user message; Gemini Live’s tool results are JSON-only, so text is folded into the result and any binary attachment raises UserError (#7362); media a provider can’t carry (audio and documents everywhere; images also on xAI) likewise raises before anything is sent. If the provider cancels an in-flight call, Pydantic AI cancels the task and records a synthetic cancellation result locally without sending that result back to the provider.

Concurrent tool execution

Every tool runs in the background, so a slow tool does not block session events, other tools, or turn tracking. all_messages() keeps each result adjacent to its call even when calls finish out of order.

Whether the model continues speaking while it waits is provider-specific. Inspect the supports_async_tool_calls profile flag. OpenAI and Azure models generally fill the gap; Gemini pauses unless the google_async_tool_calls setting — which declares the tools NON_BLOCKING to the Live API — is enabled on a supported model.

Native tools

Provider-native tools execute server-side. Add them through high-level capabilities such as WebSearch and WebFetch, or through NativeTool. Each model’s supported_native_tools profile is the source of truth.

from pydantic_ai import Agent
from pydantic_ai.capabilities import WebSearch
from pydantic_ai.messages import NativeToolReturnPart, PartEndEvent

agent = Agent(instructions='Answer questions, searching the web when useful.')


async def main():
    async with agent.realtime(
        'google:gemini-2.5-flash-native-audio-latest',
        capabilities=[WebSearch()],
    ).session() as session:
        await session.send("What's the latest Pydantic AI release?")
        async for event in session:
            if isinstance(event, PartEndEvent) and isinstance(event.part, NativeToolReturnPart):
                print(event.part.content)

An unsupported native tool with a configured local fallback is replaced before connection. Without a fallback, opening the session raises UserError. Provider and model-specific combinations—including Gemini grounding, URL context, and function-tool restrictions—are canonical on the Gemini provider page.

Deferred and approval-required tools

Approval-gated tools need a HandleDeferredToolCalls handler; without one the call is refused every time. A standard run can end with a DeferredToolRequests output and resume once a human answers (see Deferred Tools), but a live conversation has nowhere to pause: with no handler, the model is told the tool cannot complete during a realtime session, and the tool never runs.

The handler resolves each call inline: approve it (the tool then runs and returns normally), deny it (recorded with outcome='denied'), substitute a result, or request a retry. This handler approves small refunds from policy and denies the rest:

from pydantic_ai import Agent, DeferredToolRequests, DeferredToolResults, ToolDenied
from pydantic_ai.capabilities import HandleDeferredToolCalls
from pydantic_ai.tools import RunContext

agent = Agent(instructions='You are a customer support voice assistant.')


@agent.tool_plain(requires_approval=True)
def issue_refund(order_id: str, amount: float) -> str:
    return f'Refunded ${amount:.2f} for order {order_id}.'


async def refund_policy(
    ctx: RunContext[None], requests: DeferredToolRequests
) -> DeferredToolResults:
    results = DeferredToolResults()
    for call in requests.approvals:
        if call.args_as_dict().get('amount', 0) <= 100:
            results.approvals[call.tool_call_id] = True
        else:
            results.approvals[call.tool_call_id] = ToolDenied(
                'Refunds over $100 need a human; offer to connect one.'
            )
    return results


async def main():
    async with agent.realtime(
        'openai:gpt-realtime',
        capabilities=[HandleDeferredToolCalls(handler=refund_policy)],
    ).session():
        ...

This applies to both ways a call is deferred — raising ApprovalRequired or CallDeferred from the tool, and declaring it up front with requires_approval=True or an external toolset. An approval-gated tool is still advertised to the model, exactly as in a standard run; calling it opens the approval flow rather than running the tool.

Asking a human mid-call and resuming on their answer is not supported yet: a realtime session cannot pause and return a DeferredToolRequests output for an out-of-band result. Resolve the request during the call, or move that workflow to a standard agent run.

DeferredToolRequestsEvent on a session is informational for the same reason: it is emitted when the handler has resolved the calls, so a consumer can observe what was asked and decided. It is not a hook to respond to — unlike the same event in a standard run, nothing waits for the consumer, and no event is emitted when no handler is installed and the call is refused.

Tools registered with defer_loading=True are rejected in a realtime session for a related reason; see Deferred capability loading.

Enqueuing prompts from tools

RunContext.enqueue() — the same mechanism as injecting follow-up messages from a tool in a standard run — accepts one plain-text prompt per call from a realtime tool. The default priority='asap' sends it when no response is active; priority='when_idle' waits until the provider reports its current response complete. Neither priority interrupts assistant speech. Delivered prompts become ordinary user turns in history, as in injecting messages mid-run.

Multimodal content and prebuilt message/part sequences are rejected because the realtime live-input channel cannot preserve their standard-run semantics.

Delegating work during a call

Realtime models do not provide structured output and can be weaker at complex reasoning than a frontier text model. Expose a tool that delegates the hard work to a standard Agent with an output_type:

from pydantic import BaseModel

from pydantic_ai import Agent
from pydantic_ai.realtime import RealtimeTurnCompleteEvent


class Answer(BaseModel):
    summary: str
    confidence: float


supervisor = Agent('openai:gpt-5', output_type=Answer)
voice = Agent(instructions='Answer using the `consult` tool, then read the summary aloud.')


@voice.tool_plain
async def consult(question: str) -> str:
    result = await supervisor.run(question)
    return result.output.summary


async def main():
    async with voice.realtime('openai:gpt-realtime').session() as session:
        await session.send(
            'Which of our three shipping options is cheapest for a 4 kg parcel to Berlin?'
        )
        async for event in session:
            if isinstance(event, RealtimeTurnCompleteEvent):
                break

The delegated run executes concurrently, so providers with asynchronous tool calls can keep talking while analysis runs. To continue the entire conversation after the voice session, see History and handoff.

Edge cases

  • A tool finishing does not necessarily finish the turn; see the turn boundary.
  • Short tools can make asynchronous Gemini tool calling counterproductive: the result may interrupt a reply that barely started. Enable it for tools whose latency would otherwise create dead air.
  • Native-tool behavior is model-specific. Check the profile and provider page rather than assuming every model from a provider supports the same tools.