# Tools and functions

A knowledge base lets the agent *know* something. A function lets it *do* something — look up this caller's order, book the slot, hand the call to a person. The model decides when; you decide what is possible.

## What is available

`GET /v1/functions`

**Shell**

```bash
curl -s https://voice.sphoro.com/v1/functions \
  -H "Authorization: Bearer $SPHORO_API_KEY"
```

This lists what your account can actually use, which is the only list worth trusting — naming a function on an agent that this endpoint does not return is refused.

## The three built in

| Tool | Does | Given to the agent by |
| --- | --- | --- |
| `search_knowledge_base` | Searches the documents attached to this agent, mid-turn, before it answers. | Attaching at least one base in `knowledge_base_ids`. Naming it in `tools` does nothing. |
| `transfer_call` | Hands the caller to a person. | Setting `transfer_number` on the agent. That is the **only** number it can ever dial — the model chooses when, never where. |
| `end_call` | Ends the call, with a reason in the model's own words that is written onto the call record. | Available to an agentic agent; switchable under `autonomy: "custom"`. |

> **Two of the three are enabled by configuring the thing they act on, not by listing them.** This trips people up constantly: an agent with `"tools": ["transfer_call"]` and no `transfer_number` has no transfer tool, and the model never mentions transferring because it cannot see one.

## Functions that call your own API

The registry also runs tools backed by **your** REST endpoints: the model calls `get_order_status`, we validate the arguments, call your URL, and hand the response back to the model to speak from.

A tool like that is described by four things:

**JSON**

```json
{
  "name": "get_order_status",
  "description": "Look up the status of an order by its reference. Ask the caller to read the reference back before calling this.",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "pattern": "^AC-[0-9]{5}$" }
    },
    "required": ["order_id"],
    "additionalProperties": false
  },
  "url": "https://api.acme.example/orders/{{order_id}}",
  "method": "GET"
}
```

> **You do not have to send us that.** An agent carries its own tools in `tool_configs`, and the endpoint above lists what the *deployment* registered — the shared, code-defined ones. Most agents have both.

## Tools an agent carries itself

`tool_configs` on an agent is a list of tools defined by whoever built the agent, each carrying the configuration that makes it mean something: which calendar, which person, which endpoint. It replaces the whole list on every write, so a tool left out of it is removed.

There are four kinds.

| `kind` | Does | Configured by |
| --- | --- | --- |
| `calendar_availability` | Reads the open slots, so the agent offers times that are actually free. | `calendar`: a Cal.com `api_key`, an `event_type_id` and a `timezone`. |
| `book_appointment` | Writes the agreed time in, and reads the reference back. | The same `calendar` block. Give the agent both: one to offer times, one to take one. |
| `transfer_call` | Hands the call to a person, at its own destination. | `transfer`: a `destination` in E.164 or a `sip:` URI, and an optional `webhook_url` told before the call moves. |
| `custom_function` | Calls any endpoint of yours. | `request`: a `method`, a `url`, `headers`, and a table of `parameters`. |

**JSON**

```json
{
  "tool_configs": [
    {
      "kind": "custom_function",
      "name": "get_order_status",
      "description": "Look up an order by its reference. Ask the caller to read the reference back before calling this — a wrong reference returns somebody else's order.",
      "pre_tool_message": { "en-IN": "Let me pull that order up." },
      "request": {
        "method": "GET",
        "url": "https://api.acme.example/orders/{{order_id}}",
        "headers": { "Accept": "application/json" },
        "secret_header_values": { "Authorization": "Bearer sk_live_…" },
        "parameters": [
          { "name": "order_id", "type": "string", "in": "path", "required": true,
            "description": "The order reference the caller read out, like AC-10293." }
        ],
        "timeout_seconds": 8
      }
    },
    {
      "kind": "transfer_call",
      "name": "escalate_to_billing",
      "description": "Put the caller through to billing when they dispute a charge.",
      "transfer": { "destination": "+15559998888" }
    }
  ]
}
```

### Credentials go one way

A calendar key travels as `{"api_key": {"value": "cal_live_…"}}` and comes back as `{"api_key": {"hint": "••••1234"}}`; sealed headers go out under `secret_header_values` and come back as `secret_header_names` alone. Leaving either out on an update keeps what is stored — which is what lets you change a description without resending the key.

### The pre-tool message

A tool can take seconds, and silence on a phone reads as a dropped line. `pre_tool_message` is what the agent says while it runs, keyed by language code, and it is per tool because the wait is not the same wait: "let me check the diary" and "let me put you through" describe different things happening. An agent's own `filler_phrase` covers any tool that has none.

### Where else the work could go

| Instead of a live tool | Use | Difference |
| --- | --- | --- |
| Work that can happen *after* the call | A [workflow](https://voice.sphoro.com/docs/workflows)'s `trigger_webhook` step | Runs once the conversation is over. The caller never waits for it. |
| Work between calls in a sequence | A [journey](https://voice.sphoro.com/docs/journeys)'s `api` node | Calls your endpoint between calls and branches on the answer. |

Most things people reach for a live tool to do turn out not to need to be live. "Write the outcome to our CRM" does not have to happen while the caller is on the line; "tell the caller their balance" does.

## Writing the description

**The description is a prompt, not documentation.** It is the only thing the model reads when deciding whether to call this tool, and rewriting it is the highest-leverage change you can make to a tool that fires at the wrong times.

| Instead of | Write |
| --- | --- |
| "Books an appointment." | "Reserve a slot on the clinic calendar. Confirm the date and time with the caller first, and never call this twice for one caller." |
| "Gets order status." | "Look up an order by its reference. Ask the caller to read the reference back before calling this — a wrong reference returns somebody else's order." |

Say *when* to call it, *what to do first*, and *when not to*. Those three sentences do more than any schema change.

## Arguments are validated before your endpoint sees them

A model will, reliably and forever, invent argument names, omit required fields and pass strings where numbers belong. Every call is checked against the schema first, and every problem is reported at once.

| Supported |  |
| --- | --- |
| Types | `object`, `array`, `string`, `number`, `integer`, `boolean`, `null` |
| Presence | `required`, `additionalProperties` |
| Strings | `minLength`, `maxLength`, `pattern`, `enum` |
| Numbers | `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum` |
| Arrays | `items`, `minItems`, `maxItems` |
| Objects | `properties`, nested to any depth |
| Combinators | `allOf`, `anyOf`, `oneOf` |
| Formats | `email`, `date-time`, `uri`, `phone` |

> **Always set `"additionalProperties": false`.** Without it, a model that invents an extra argument has it silently accepted and passed to your handler. With it, the invention is caught and the model is told, on the same turn, so it corrects itself.

### Coercion is deliberately narrow

Models routinely emit `"3"` where a number belongs. Only the unambiguous cases are converted; anything else is left alone so validation still catches it.

```
"3"      ──▶ 3        (integer)
"0.7"    ──▶ 0.7      (number)
"true"   ──▶ true     (boolean)
"maybe"  ──▶ rejected
```

### A validation failure is text, not an error

When arguments do not validate, the model is handed the failure *as the tool result* rather than the call being aborted. It then corrects itself on the next turn, usually within the same breath. That feedback loop is most of what makes tool calling reliable on a live call, and it is why a badly-specified schema shows up as a slow call rather than a broken one.

## What the caller hears while it runs

A tool call is a real network round-trip inside a live conversation. Two things to set:

| Setting | Does |
| --- | --- |
| `filler_phrase` on the agent | Something said while the tool runs — "let me check that for you". Makes a three-second lookup sound like thinking rather than a dropped line. |
| `max_tool_rounds` in `capabilities` | How many times one turn may call a tool and generate again. 1–10, default 3. Every round is a full model call plus your API, so a bigger number is a longer silence rather than a more thorough answer. |

## Watching tool calls

`call.tool.called` fires on every invocation, carrying the tool name — on [webhooks](https://voice.sphoro.com/docs/webhooks) and on the per-call event stream. It is the first thing to look at when a call has an unexplained pause in the middle: if the pause lines up with a tool call, your endpoint is the latency, not the model.

`GET /v1/calls/{id}/events`

**Shell**

```bash
curl -sN https://voice.sphoro.com/v1/calls/$CALL_ID/events \
  -H "Authorization: Bearer $SPHORO_API_KEY"
```

## Restricting tools to part of a conversation

On a [flow](https://voice.sphoro.com/docs/flows), each node can narrow or force the tools available while the call is on it — `tools` restricts what the model can see, and `tool` forces one to be called. A transfer node that must transfer, a booking node that must book. That is usually a better answer than writing "only book once you have confirmed" into a prompt and hoping.
