# Tool Calling

> Give the voice agent tools it can call mid-conversation — HTTP endpoints, remote MCP servers, or your own SDK handlers.

Give your voice agent the ability to _act_ during a live call. It can look up
an order, check an appointment slot, file a ticket, or fetch a balance. You
do not rebuild the runtime. You declare a set of tools on the agent. The
model sees each tool's name, description, and argument schema. The model
decides when to call a tool. FirstByte Voice executes the tool and feeds
the result back, so the model can keep talking.

This is the same tool model that the
[Agent DAG](/modalities/voice/runtime/overview) uses under the hood. An `llm`
(or `realtime`) node advertises tools by name, and the runtime resolves and
runs them. On this page, you configure the tools themselves.

## How a tool call flows through a turn

  1. **Advertise**
On the first audio frame of a new session, the runtime hydrates the
    agent's tool list. It advertises each tool's `name`, `description`, and
    parameter schema to the model.
  2. **Call**
The model emits a structured `tool_call` with JSON arguments. The runtime
    validates those arguments against the tool's JSON Schema _before_
    dispatch. Thus a malformed call fails fast and does not hit your
    endpoint.
  3. **Execute**
The runtime runs the tool. This is an outbound HTTP request, an MCP
    `tools/call`, or a push to your SDK. The runtime captures the result as a
    JSON string.
  4. **Feed back**
The result returns to the model as a `tool_result`. The model reasons
    over the result. It then answers the caller or calls another tool. Mark a
    tool `silent` to hand the result back without a prompt for a new spoken
    response.

> **WARNING:**
> Tool invokes are **synchronous within the model turn**. A slow upstream eats
> the turn budget, and the caller hears dead air. Keep each tool fast.
> Sub-second is ideal. Chain multiple short LLM turns instead of several tool
> calls inside one turn.

## Where tools live

You declare tools **on the agent**. They are not baked into the runtime. The
control plane persists them under the agent's config
(`agent:<agent_id>:config.tools[]`). The runtime hydrates them per session.
Thus edits in the [Voice AI console](https://voiceai.firstbyte.fast) apply to the
next call with no redeploy.

Each session builds its tool set as a union. It unions your operator-defined
tools with a built-in **telephony toolset** that the runtime advertises
automatically. The toolset includes transfer, hold/unhold, DTMF, disconnect,
supervisor request, and more. See below and
[AI ↔ human handoffs](/modalities/voice/transport-telephony/ai-human-handoff).
On a name collision, the operator tool wins. Thus a declared tool named
`transfer_call` overrides the built-in.

> **NOTE:**
> If a tool's spec fails to parse, the runtime skips the tool at registration.
> The runtime does not register a broken tool. Thus a typo in one tool's spec
> cannot shadow a built-in of the same name, and cannot wedge the session. If a
> tool you expected never reaches the model, check the Voice AI console's
> validation banner.

## Built-in telephony tools

Every session ships with a built-in telephony toolset. The model can call
these tools with **no operator config**. They drive the live call itself. The
model can move the caller, place them on hold, dial a human, hang up, punch
DTMF into a downstream IVR, or pull in a supervisor. These are the same
operations that ISV apps get as CSTA verbs on
[CTI call-control](/modalities/voice/cti/call-control). Here they are tools
that the LLM calls _itself_, in the middle of the conversation, when it
decides that a human or a different destination should take over.

> **NOTE:**
> Every transfer passes the realm's **transfer-rule policy**. A destination can
> be disallowed, for example a blocked cross-realm hop. Then the tool returns a
> **structured error** instead of a connection. Thus the model can react
> in-turn: apologize, try `route_to_skill`, or read the reason back to the
> caller. The tool does not fail silently.

### Picking a transfer style

The model has three ways to hand a caller off. The right way depends on two
questions. Does a human need a **brief**? Should the **caller hear** that
brief?

| Style | Tool(s) | Does the AI stay? | Does the caller hear the brief? | Reach for it when |
| ----- | ------- | ----------------- | ------------------------------- | ----------------- |
| **Blind** | `transfer_call` | No — leaves at once | No brief happens | You already know the exact destination, and no handoff brief is needed. |
| **Warm / attended** | `warm_transfer` → `complete_transfer` | Yes, until it steps out | **Yes** — everyone is in one conference | You want to brief the human out loud, with the caller present, before you drop. |
| **Private consult** | `consult` → `consult_transfer` \| `consult_cancel` | Yes, speaking privately | **No** — caller is on hold | You need to check with a third party privately, and you may decide _not_ to transfer. |

### Blind transfer — `transfer_call`

This is a one-step blind transfer (SIP REFER). The runtime hands the caller
to the destination. The **AI leaves the call** immediately. There is no
briefing phase. Use it when you know the destination and no context needs to
pass by voice.

- **`destination`** (`string`, required) — Where to send the caller. Accepts an E.164 number (`+15551234567`), a SIP   URI (`sip:queue@pbx.example.com`), or a `tel:` URI.
- **`referred_by`** (`string`) — Optional `Referred-By` identity presented on the REFER. If you omit it, the   default is the trunk's identity.

### Warm transfer — `warm_transfer` then `complete_transfer`

This is an attended transfer where the caller **hears the handoff**. The AI
dials the destination and joins everyone into a conference, so it can _speak_
the brief. The caller is live throughout and hears the whole exchange.

  1. **warm_transfer { destination }**
The AI dials `destination` and joins a **conference** with the caller.
    All three parties are connected. The AI delivers the handoff brief out
    loud while the caller listens.

    - **`destination`** (`string`, required) — A skill name, extension, SIP URI, or PSTN number (E.164) to bring into       the conference.
  2. **complete_transfer { }**
When the brief is done, the AI steps out of the conference. The caller
    and the destination stay connected. Only the AI leg stops. There are no
    parameters.

### Private consult — `consult` then `consult_transfer` or `consult_cancel`

This is an attended, one-to-one consult where the caller does **not** hear
anything. The AI dials a third party and speaks **privately** while the
caller waits on hold. It then either connects them or returns to the caller.

  1. **consult { destination }**
The runtime effectively places the caller on hold. The AI dials
    `destination` on a private leg. The AI and the consulted party can talk,
    and the caller does not hear them.

    - **`destination`** (`string`, required) — A skill name, extension, SIP URI, or PSTN number (E.164) to consult       privately.
  2. **Then resolve it one of two ways**

      
        Connects the caller to the consulted party. The **AI drops out**. The
        two parties are left in conversation. There are no parameters.
      
      
        Hangs up on the consulted party and **returns the AI to the caller**.
        It takes the caller off hold. Use it when the consult did not pan
        out. There are no parameters.
      
    

### The rest of the toolset

The remaining built-in tools control the caller's own leg or pull in a human.
They take effect the moment the model calls them.

| Tool | Params | What it does |
| ---- | ------ | ------------ |
| `route_to_skill` | `skill?` | Queues the caller to a **human skill / queue** (ACD), and the AI steps out. Use it for "get me a human" when there is no specific number. Omit `skill` to use the agent's default queue. |
| `hold_call` | — | Places the caller on hold. The caller hears the trunk's hold treatment. |
| `unhold_call` | — | Takes the caller back off hold. |
| `disconnect_call` | `reason?` | Hangs up the call. The CDR records `reason`. |
| `send_dtmf` | `digit` (`0-9*#A-D`), `duration_ms?` (default `100`), `mode` (`rfc2833` \| `inband`) | Punches a DTMF digit down the line to navigate a downstream IVR. |
| `request_supervisor` | `reason`, `urgency` (`low` \| `medium` \| `high`) | Signals a supervisor to join / assist. The caller is not transferred away. |

> **TIP:**
> You can override any of these tools. Declare an operator tool of the same
> name; the operator tool wins the union. You can also pull a tool from a
> tenant entirely. See [Hiding tools per tenant](#hiding-tools-per-tenant)
> below.

## Per-tool spec

Every entry in `tools[]` shares the same envelope. The `kind`-specific fields
go inside `spec`:

- **`kind`** (`string`, required) — `http`, `mcp`, or `client`. Selects which executor runs the tool.
- **`name`** (`string`, required) — Unique within the agent. This is the function name that the model sees and   calls.
- **`description`** (`string`, required) — The human-readable purpose, advertised to the model. Write it for the   model. It is the primary signal for _when_ to call the tool.
- **`silent`** (`boolean`, default `false`) — When `true`, the runtime hands the tool result back and does not ask the   model to generate a fresh spoken turn. This is useful for fire-and-forget   side effects (log a disposition, tag the call), where you do not want the   agent to narrate the action.
- **`spec`** (`object`, required) — Kind-specific configuration. Every kind accepts a `parameters` field, which   is a standard **JSON Schema** for the tool's arguments. If you omit it on   an `http` tool, the runtime synthesizes one from the `{{placeholder}}`   tokens in your URL and body.

## Tool kinds

| Kind     | Status       | What it does |
| -------- | ------------ | ------------ |
| `http`   | **shipped**  | Calls an HTTP(S) endpoint you own. URL, headers, and body templates substitute `{{argument}}` from the model's tool arguments. Auth: `bearer`, `basic`, or custom header. 10s default timeout. |
| `mcp`    | **shipped**  | JSON-RPC 2.0 `tools/call` against a remote [MCP](https://modelcontextprotocol.io) server over Streamable HTTP. You declare each tool explicitly (no `tools/list` auto-discovery yet). Same auth shapes as `http`. |
| `client` | scaffolded   | The runtime advertises the tool and parses the call, but **pushes** it to your SDK to fulfil. See the status note below. |

  
    ```json
    {
      "kind": "http",
      "name": "lookup_order",
      "description": "Look up an order by its ID and return its status.",
      "silent": false,
      "spec": {
        "method": "GET",
        "url": "https://api.example.com/orders/{{order_id}}",
        "headers": { "X-Tenant": "acme" },
        "auth": { "type": "bearer", "token": "sk_live_..." },
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": { "type": "string", "description": "Order id to look up." }
          },
          "required": ["order_id"]
        },
        "timeout_ms": 10000
      }
    }
    ```

    The runtime replaces `{{name}}` tokens in `url`, `headers`, and `body`
    with the matching top-level property from the model's arguments. **URL**
    substitutions are percent-encoded. **Body** substitutions are inserted
    verbatim. Thus, if your endpoint expects JSON, write JSON in the body
    template. `auth` accepts `{ "type": "bearer", "token": ... }`,
    `{ "type": "basic", "user": ..., "pass": ... }`, or
    `{ "type": "header", "name": ..., "value": ... }`. The runtime returns
    the response body to the model as the tool result.
  
  
    ```json
    {
      "kind": "mcp",
      "name": "search_orders",
      "description": "Search a customer's recent orders.",
      "silent": false,
      "spec": {
        "server_url": "https://mcp.example.com/messages",
        "tool_name": "search_orders",
        "transport": "http",
        "auth": { "type": "bearer", "token": "..." },
        "parameters": {
          "type": "object",
          "properties": {
            "query": { "type": "string" }
          },
          "required": ["query"]
        },
        "timeout_ms": 10000
      }
    }
    ```

    The runtime forwards each call as a JSON-RPC 2.0 `tools/call` to
    `server_url`, with `params.name = tool_name` and `params.arguments` = the
    model's arguments. The tool result is the concatenated text from the
    response's `result.content[*].text`. On failure, it is a structured error
    envelope. The model gets a JSON string either way.

    > **NOTE:**
> You declare each remote MCP tool explicitly. Dynamic discovery via
>     `tools/list` (connect → enumerate → auto-register) is not wired yet.
>     Rich content types (images, embeddings) collapse to their text parts for
>     now. A declaration by name keeps the tool set available in the model's
>     very first turn, without a per-session handshake.
  
  
    ```json
    {
      "kind": "client",
      "name": "approve_refund",
      "description": "Show the operator an approval dialog and report the choice.",
      "silent": false,
      "spec": {
        "timeout_ms": 10000,
        "parameters": {
          "type": "object",
          "properties": {
            "amount": { "type": "number" }
          },
          "required": ["amount"]
        }
      }
    }
    ```

    A `client` tool flips the direction. The runtime does not call out.
    Instead, the runtime **pushes** the tool call to your SDK and waits for
    the SDK to return a result. Use it for actions that only the caller's
    environment can perform: read local state, show an approval dialog, or
    run code in the user's sandbox. You register a handler on the SDK side
    (see the [JavaScript SDK](/modalities/voice/sdk/javascript)).

    > **WARNING:**
> `client` tools are **scaffolded, not live**. You can configure them
>     today, and the model will call them. But `invoke` currently returns a
>     `not_yet_plumbed` error. The runtime→SDK envelope ships alongside the
>     move of the supervise channel onto media-over-QUIC. Do not put a
>     `client` tool on the critical path of a production agent yet.
  

## Hiding tools per tenant

You can pull a built-in telephony tool out of a tenant's agents without an
edit to each agent. Write the tool names as a JSON array to
`FirstByte:tenant:<tenant_id>:telephony_disabled_tools`. The runtime
filters out disabled tools before the tool set reaches the model. It
re-checks the list at invoke time. Thus a stale provider session cannot slip
a call through for a name you disabled mid-call.

## Native tools

Some tools need privileged access to the live call: realm-policy evaluation,
telephony dispatch, recording control, and supervisor signalling. For these,
the built-in telephony toolset is implemented as first-class runtime code,
not as `http`/`mcp` entries. That path is for platform developers who build
into the runtime itself. Tenants and operators should use the `http`, `mcp`,
and (soon) `client` kinds above. Those kinds need no rebuild.

## Related

  - **[Runtime overview](/modalities/voice/runtime/overview)** — Learn how agents, pipelines, and providers fit together.
  - **[Session lifecycle](/modalities/voice/runtime/session-lifecycle)** — Learn when tools hydrate and how the turn budget is spent.
  - **[AI ↔ human handoffs](/modalities/voice/transport-telephony/ai-human-handoff)** — See the built-in transfer / supervisor toolset in context.
  - **[CTI call-control verbs](/modalities/voice/cti/call-control)** — The same operations as CSTA verbs, for ISV apps to drive over the CTI
    leg.
  - **[BYO ASR / LLM / TTS](/modalities/voice/runtime/byo-asr-llm-tts)** — Learn which providers surface tool calling in a cascaded pipeline.
