Skip to content
Console

Agents

Tools

Copy page

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).

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. It has no URL and no secret (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).

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."
}'
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.

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

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 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.

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.

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.

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); 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).
  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.

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.

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). So there is always a person’s message or tap between the ask and the run.

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), which the SDKs send for you from the handlers you give them (the client SDK and its React hooks). 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) 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.

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.
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.

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) — 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.

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.

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
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", ""))
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" } }'
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.

curl
curl "https://api.lumavox.ai/v1/tools/tl_4fHq9Zk2LmP0aXbC/calls?limit=20" \
-H "Authorization: Bearer $API_KEY"
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.

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), 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), 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).