# Tools

> Give your agents tools to call while they reply — your server, your app, and two of Gemini's own — turned on per agent.

A **tool** is something an agent's model may call while it replies: look an order up, book a table, check stock. You keep your tools in one library for your organisation and turn each on for the agents that should have it, up to the platform's limit an agent (`limits.agent.tools` on [Limits](https://docs.lumavox.ai/limits.md)).

A tool is one of two kinds:

| `kind` | Who runs it |
|---|---|
| `webhook` | Your server. It is sent a signed `POST` with the call's arguments, and what it answers is what the model reads. |
| `app` | The person's app, over the agent's [WebSocket](https://docs.lumavox.ai/agents/websocket.md). It has no URL and no secret ([App tools](#app-tools)). |

Beside your own, an agent can read web pages and run code with two of Gemini's tools, each a switch on the agent ([Built-in tools](#built-in-tools)).

```bash title="curl"
curl https://api.lumavox.ai/v1/tools \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "lookup_order",
    "description": "Look an order up by the number on the customer'\''s receipt.",
    "kind": "webhook",
    "url": "https://shop.example.com/tools/lookup-order",
    "parameters": {
      "type": "object",
      "properties": { "order_number": { "type": "string", "description": "As printed on the receipt." } },
      "required": ["order_number"]
    },
    "headers": { "Authorization": "Bearer sk_shop_…" },
    "filler": "Let me check that for you."
  }'
```

```json title="Response — 201"
{
  "id": "tl_4fHq9Zk2LmP0aXbC",
  "name": "lookup_order",
  "description": "Look an order up by the number on the customer's receipt.",
  "kind": "webhook",
  "parameters": {
    "type": "object",
    "properties": { "order_number": { "type": "string", "description": "As printed on the receipt." } },
    "required": ["order_number"]
  },
  "url": "https://shop.example.com/tools/lookup-order",
  "timeout_ms": 10000,
  "ask_first": false,
  "action_only": false,
  "filler": "Let me check that for you.",
  "header_names": ["Authorization"],
  "secret_hint": "x9Qa",
  "agent_ids": [],
  "version": 1,
  "created_at": "2026-10-30T09:55:02.000Z",
  "updated_at": "2026-10-30T09:55:02.000Z",
  "secret": "whsec_8sKq…x9Qa"
}
```

**`secret` is shown this once.** Every call to your server is signed with it; keep it with your server. `POST /v1/tools/{tool_id}/secret` makes a new one, shown once in the same way.

## Scopes

Reading and listing tools need `agent`. Making, editing, deleting, rotating a secret, testing and turning tools on for an agent need `agent:write`.

## Make a tool

`POST /v1/tools`:

| Field | What it is |
|---|---|
| `name` | Required. What the model calls the tool by: 1 to 64 lower-case letters, digits and underscores, starting with a letter. Unique in your organisation. `search_knowledge`, `remember_relation` and `forget_fact` are the platform's own. |
| `description` | Required, up to 1,000 characters. What the tool does and when to call it — the model reads this to decide. |
| `kind` | Required: `webhook` or `app`. Fixed once made. |
| `parameters` | Its inputs, as a JSON Schema object (below). Left out: it takes none. |
| `url` | A webhook's address: `https://`, a public host, never this platform's. |
| `timeout_ms` | How long a call may take: 1,000 to 30,000, default 10,000. Past it the call has failed, and the model is told so. |
| `headers` | A webhook's own headers, sent with every call — your API token. At most 10, stored encrypted and never shown again: a tool answers their names in `header_names`. `Content-Type`, `User-Agent`, `X-Tool-*` and HTTP's own are the platform's. |
| `ask_first` | The agent asks the person before the tool runs, and it runs only after they say yes. |
| `action_only` | An app's tool only: the app is told to act and the reply carries on, with no answer awaited. |
| `filler` | A line the agent says before the call, while it waits. Up to 200 characters. |

### Parameters

`parameters` is the part of JSON Schema the model takes: an object of `properties`, each `string` (with an optional `enum`), `number`, `integer`, `boolean`, `array` (with `items`) or `object` (with its own `properties`), each with an optional `description`, and `required` naming the ones the model must give. **Nothing else is taken**: a `minimum`, a `format` or a `pattern` is refused with a 422 rather than passed on unenforced. Check what your server is sent; the model's arguments are only ever its best effort.

**Every tool on an agent rides on every model call**, used or not: each costs about 100 to 250 input tokens a call, a long description and many parameters more.

### Edit and delete

`PATCH /v1/tools/{tool_id}` changes what you send; an explicit `null` clears a field, and `headers` replaces them all. Send `expected_version` to be refused with 409 if somebody else changed the tool since you read it. Conversations already open pick a change up within about a minute. `DELETE /v1/tools/{tool_id}` turns it off for every agent that had it.

## Turn tools on for an agent

```bash title="curl"
curl -X PUT https://api.lumavox.ai/v1/agents/0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07/tools \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tool_ids": ["tl_4fHq9Zk2LmP0aXbC"] }'
```

The list is the complete set: an empty one turns every tool off. Ids that are not yours come back in `skipped`. `GET /v1/agents/{agent_id}/tools` lists what an agent has.

## How a call runs

When the model calls one of your tools while it replies:

1. **The call goes out at once**: a webhook's signed, from near the person; an app's down its connection. Several calls in one reply run at the same time, at most the platform's `agents.tools.calls_per_round` ([Limits](https://docs.lumavox.ai/limits.md)); more are answered "not run".
2. **The person hears the tool's `filler`**, if it has one and the turn speaks, once per round. A typed turn shows `tool.start` instead.
3. **The model reads what your server or app answered** and carries on with the reply: one more model call, which is billed like any reply. Calling your server is free. The model may call again from there, **the platform's `agents.tools.max_rounds` rounds at most** in one reply ([Limits](https://docs.lumavox.ai/limits.md)).
4. **The call is kept with the reply**: its arguments, what it answered, whether it failed and how long it took — in `turn.done`'s `tool_calls`, and in the transcript (`tool_calls` on every message of `GET /v1/agent/sessions/{session_id}` and the console).

A call that fails — a status other than 2xx, past its timeout, a server that cannot be reached — fails the call, not the reply: the model is told what failed and answers around it. The socket says each call with `tool.start` and `tool.end` (`ok`, `ms`); see [The WebSocket](https://docs.lumavox.ai/agents/websocket.md).

**A reply with tools takes longer**: one more model call per round, about half a second to two, plus your slowest call in it. At three rounds, three of 10-second tools is a reply of about 35 seconds at worst.

### Ask first

A tool marked `ask_first` **never runs on the model's word alone**. When the model calls it, it does not run: the model is told to ask the person in its own words, and your app is sent `tool.confirm` with the call's `call_id`, `name` and `arguments`. It runs only if:

- your app answers `tool.confirmed` with `yes: true` — a tap, which counts as the person saying "Yes." — or
- in the person's next turn, the model calls the same tool with the same arguments: their message between was their answer.

A no, other arguments, a later turn or the platform's `agents.tools.confirm_for_minutes` passing drops it ([Limits](https://docs.lumavox.ai/limits.md)). So there is always a person's message or tap between the ask and the run.

### App tools

An app's tool runs in the person's app: open a page, start the camera, read something only the device knows.

1. **The app names its tools when it connects**: the `auth` frame's `tools` ([The WebSocket](https://docs.lumavox.ai/agents/websocket.md)), which the SDKs send for you from the handlers you give them ([the client SDK](https://docs.lumavox.ai/sdks/client.md) and [its React hooks](https://docs.lumavox.ai/sdks/react.md)). The model is offered the agent's app tools named there and no others, so a version of your app without a tool is never asked for it.
2. **The app is sent `tool.call`**, between the call's `tool.start` and `tool.end`: `call_id`, `name`, `arguments`, `action_only` and `timeout_ms`.
3. **It answers `tool.result`** with the same `call_id`: `result`, any JSON, or `error`, what went wrong. No answer within the tool's `timeout_ms`, or the connection closing first, fails the call, and the model is told so. An answer longer than the platform's `agents.tools.result_max_bytes` ([Limits](https://docs.lumavox.ai/limits.md)) is cut there, as a webhook's is.

**An action-only tool** (`action_only: true`) is not waited for: the app is told to act, the model is told it was done, and a reply the model already wrote stands, with no second model call. Use it for what has nothing to say back, such as opening a page.

## Built-in tools

Two of Gemini's own tools, each a switch on the agent, off until you turn it on:

| Switch | What the agent can do |
|---|---|
| `url_context` | Read a web page by its address, when the person gives one or its instructions name one. Google fetches the page. |
| `code_execution` | Write and run Python while it replies — a sum, a unit conversion, a date — and answer from what it printed. It runs at Google, with no network and nothing of yours. |

```bash title="curl"
curl -X PATCH https://api.lumavox.ai/v1/agents/0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07 \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url_context": true, "code_execution": true }'
```

**Neither has a fee; both are billed as model tokens.** A page's text, and the code with its output, count as the reply's input and are charged as any reply is: a short page is a few hundred tokens, a long one thousands. Neither is offered to the agent's opening greeting.

**The pages a reply read are its `sources`**, each with its `url` and whether it could be `retrieved`: on the socket's `turn.done` and on every message of a conversation's history. A page behind a login, or one its site refuses, is listed with `retrieved: false`, and the agent answers without it.

**A page is untrusted input**: what it says reaches the model. The most it could steer the agent towards is the agent's own tools, and a tool marked `ask_first` still needs the person.

## What your server is sent

```http title="Request"
POST /tools/lookup-order HTTP/1.1
Host: shop.example.com
Content-Type: application/json
User-Agent: Agent-Tools/1
X-Tool-Name: lookup_order
X-Tool-Call: call_8f2c…
X-Tool-Signature: t=1793354102,v1=5d0338b4…
Authorization: Bearer sk_shop_…

{
  "tool": "lookup_order",
  "call_id": "call_8f2c…",
  "arguments": { "order_number": "A-1042" },
  "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07",
  "session_id": "0193b2f0-…",
  "turn_id": "0193b2f1-…",
  "end_user": { "id": "eu_7GqT2mXcV9pLs4Hd", "external_id": "cust-77" },
  "test": false,
  "sent_at": "2026-10-30T09:55:02.000Z"
}
```

`end_user` is the person the agent is talking to: their `eu_…` id and your own id for them. A call the person said yes to keeps the `call_id` your app was sent with `tool.confirm`. A call from the test button says `test: true` and carries no agent, conversation or person.

**Answer with any JSON, or text, within the tool's timeout.** What you answer is what the model reads, cut at the platform's `agents.tools.result_max_bytes` ([Limits](https://docs.lumavox.ai/limits.md)) — the model is told when it was cut. A status other than 2xx, a timeout or a body that cannot be read is a failed call, and the model is told what failed. Redirects are not followed.

### Check the signature

`X-Tool-Signature` is `t=<unix seconds>,v1=<hex>`: the HMAC-SHA256 of `<t>.<the raw body>`, keyed with your tool's secret as text. Check it against the raw body, before parsing it, and refuse a `t` more than five minutes from now: that stops a call being replayed.

```ts title="TypeScript"
import { createHmac, timingSafeEqual } from "node:crypto";

export function verified(secret: string, header: string | null, body: string): boolean {
  const parts = Object.fromEntries(
    (header ?? "").split(",").map((part) => part.split("=", 2) as [string, string]),
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "utf8");
  return given.length === expected.length && timingSafeEqual(given, Buffer.from(expected, "utf8"));
}
```

```python title="Python"
import hashlib, hmac, time

def verified(secret: str, header: str | None, body: bytes) -> bool:
    parts = dict(part.split("=", 1) for part in (header or "").split(",") if "=" in part)
    try:
        t = int(parts.get("t", ""))
    except ValueError:
        return False
    if abs(time.time() - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

## Test a tool

```bash title="curl"
curl https://api.lumavox.ai/v1/tools/tl_4fHq9Zk2LmP0aXbC/test \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "arguments": { "order_number": "A-1042" } }'
```

```json title="Response — 200"
{
  "ok": true,
  "status": 200,
  "duration_ms": 184,
  "result": { "status": "shipped", "eta": "2026-11-02" },
  "truncated": false,
  "error": null
}
```

The test sends your server one call, signed and with your headers, exactly as a conversation would, and answers what came back as the model would read it. A webhook tool only; nothing is charged.

## See its calls

```bash title="curl"
curl "https://api.lumavox.ai/v1/tools/tl_4fHq9Zk2LmP0aXbC/calls?limit=20" \
  -H "Authorization: Bearer $API_KEY"
```

```json title="Response — 200"
{
  "data": [
    {
      "at": "2026-10-30T09:15:02.000Z",
      "call_id": "call_duOGWQLfDfemyNUV",
      "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07",
      "session_id": "0193b2f0-6a1c-7e4d-8b2a-1c3d5e7f9a0b",
      "end_user_id": "eu_7GqT2mXcV9pLs4Hd",
      "ok": false,
      "duration_ms": 10004,
      "error": "The tool did not answer within 10 s."
    }
  ],
  "next_cursor": null
}
```

The call log holds a tool's most recent calls in your agents' conversations, newest first, from the past seven days: when, in which conversation and for whom, how long each took, and what failed. It keeps no inputs or answers; those are in each conversation's history, under `tool_calls`. A call shows within about a minute. When a tool is called thousands of times an hour, the log keeps a sample of its calls rather than all of them, so don't count calls from it. Ask for up to 100 with `limit`.

## When a tool keeps failing

A call fails when your server misses the tool's timeout, answers with an error, or can't be reached; an app's tool fails when the app misses the deadline or answers an `error`. The model is told what failed and answers the person anyway. Each tool shows its failures:

- `recent_failures`: failed calls within the platform's `agents.tools.failure_window_minutes` ([Limits](https://docs.lumavox.ai/limits.md)), or 0 once that passes without one.
- `last_failed_at` and `last_error`: when the last failure happened, and what the model was told.

Each conversation reports its failures at most once in the platform's `agents.tools.report_every_minutes` ([Limits](https://docs.lumavox.ai/limits.md)), so these can be that far behind.

**At the platform's `agents.tools.failures_to_mail` failures within that window, your organisation's admins get one email** naming the tool and its last error. A tool that keeps failing sends at most one email in each `agents.tools.mail_every_hours` ([Limits](https://docs.lumavox.ai/limits.md)).
