---
title: Client tools
description: Register your app as a client on a session, declare the tools it offers, and answer the calls the model routes to you.
sidebar:
  order: 2
---

Some tools cannot run anywhere but in your app. Reading the form the user is looking at, highlighting a row in their table, asking them to pick a date — the agent should be able to call these, and no server-side integration can provide them.

A **client** is a participant that registers on a session, declares tools, and answers calls. A browser tab is a client. So is a backend worker.

## Registering

```http
POST /v1/sessions/{session_id}/clients
```

```json
{
  "name": "web",
  "lifetime": { "type": "ttl", "data": { "seconds": 60 } },
  "tools": [
    {
      "name": "highlight_row",
      "description": "Highlight a row in the table the user is looking at.",
      "parameters": {
        "type": "object",
        "properties": { "row_id": { "type": "string" } },
        "required": ["row_id"]
      }
    }
  ]
}
```

| Prop | Type | Default | Description |
| - | - | - | - |
| `name` | `string` | - | What the model sees this client's tools under. If the session has already spent the name, a counter is appended — the assigned name comes back in the `client_registered` event, and it is the one that matters. |
| `lifetime` | `never \| ttl` | - | `never` ends only on an explicit leave. `ttl` ends `seconds` after the last ping, so a closed tab stops offering its tools without having to say so. |
| `tools` | `ClientTool[]` | - | Each tool has a name (1–64 chars, `[A-Za-z0-9_-]`), a description up to 4096 characters, and a JSON Schema object for its arguments. |

Either credential reaches these routes: the session's own token, or a workspace API key holding `session.manage`.

## Choosing a lifetime

**Browser tab**

A tab can close without warning, so give it a TTL and ping while it is
open. When the pings stop, the registration lapses and the model stops
being offered tools nothing can answer.

```http
POST /v1/sessions/{session_id}/clients/{client_id}/ping
```

**Backend worker**

A server-side client that stays up for the life of the session can use
`never` and skip the ping entirely. It leaves when it says it leaves.

```http
DELETE /v1/sessions/{session_id}/clients/{client_id}
```

## Answering a call

Watch the session stream for `client_tool_dispatched` — it names your `client_id`, the `call_id`, the tool as **you** declared it (unnamespaced), and the arguments.

1. **Acknowledge it**

    An ack marks the difference between a client that is working on a call and
    one that never saw it. The two carry different deadlines, so acking buys
    you the longer one.

    ```http
    POST /v1/sessions/{session_id}/clients/{client_id}/calls/{call_id}/ack
    ```

2. **Do the work, then post the result**

    ```http
    POST /v1/sessions/{session_id}/clients/{client_id}/calls/{call_id}/result
    ```

If your client goes away mid-call, the call settles as `client_tool_failed` with a reason, and the model receives an error tool result — so the turn recovers instead of stalling on a client that is not coming back.

## Changing what you offer

A client declares the whole list, every time. There is no partial update: a tool you stop naming is a tool you stopped offering.

```http
PUT /v1/sessions/{session_id}/clients/{client_id}/tools
```

This is how a client that navigates between screens keeps its tools honest — re-`PUT` the list on every screen change, and the model is only ever offered what is actually there.

:::note
Calls belong to the registration they were dispatched to, never to the name it held. A later client that inherits a freed name never inherits its outstanding work.
:::
