# Client Tools

> Tools executed by your code via a handler function.

**URL:** https://inference.sh/docs/api/agent/client-tools
**Last updated:** 2026-10-12

---

Tools executed by your code via a handler function.

---

## Basic Usage

<div data-type="codegroup">

```python Python
from inferencesh import tool, string

greet = (
    tool("greet")
    .describe("Greet a user by name")
    .param("name", string("User's name"))
    .handler(lambda args: f"Hello, {args['name']}!")
    .build()
)
```

```typescript JavaScript
import { tool, string } from '@inferencesh/sdk';

const greet = tool('greet')
  .describe('Greet a user by name')
  .param('name', string('User name'))
  .handler(args => `Hello, ${args.name}!`)
  .build();
```

</div>

---

## Builder Methods

| Method | Description |
|--------|-------------|
| `tool(name)` | Create a client tool |
| `.describe(text)` | Set description for LLM |
| `.display(name)` | Human-readable name |
| `.param(name, schema)` | Add a parameter |
| `.handler(fn)` | Function to execute |
| `.requireApproval()` | Require user approval |
| `.build()` | Build the tool |

---

## Sync Handlers

<div data-type="codegroup">

```python Python
def get_weather(args):
    city = args["city"]
    # Call weather API...
    return f"Weather in {city}: 72°F, Sunny"

weather = (
    tool("get_weather")
    .describe("Get current weather for a city")
    .param("city", string("City name"))
    .handler(get_weather)
    .build()
)
```

```typescript JavaScript
const getWeather = tool('get_weather')
  .describe('Get current weather for a city')
  .param('city', string('City name'))
  .handler(args => {
    // Call weather API...
    return `Weather in ${args.city}: 72°F, Sunny`;
  })
  .build();
```

</div>

---

## Async Handlers

<div data-type="codegroup">

```python Python
import aiohttp

async def fetch_data(args):
    async with aiohttp.ClientSession() as session:
        async with session.get(args["url"]) as response:
            return await response.text()

fetcher = (
    tool("fetch")
    .describe("Fetch data from URL")
    .param("url", string("URL to fetch"))
    .handler(fetch_data)
    .build()
)
```

```typescript JavaScript
const fetchData = tool('fetch')
  .describe('Fetch data from URL')
  .param('url', string('URL to fetch'))
  .handler(async (args) => {
    const res = await fetch(args.url);
    return res.text();
  })
  .build();
```

</div>

---

## Returning Files

A client tool's result can carry files along with its text. Upload each file first with `POST /files` (`client.files.upload` in JavaScript), then return the file refs as `attachments`. A result carries at most 20 attachments, and each needs a `uri`.

The tool message keeps the text and the files. Images reach the model as image input when the model accepts images; other files are listed on the tool message.

In JavaScript (`@inferencesh/sdk` 0.24.0 and later), a handler in `clientToolHandlers` returns either a string or `{ result, attachments }` (type `ClientToolResult`). The tool builder's `.handler()` is typed to return a string.

```tsx
import { inference, type FileRef } from '@inferencesh/sdk';
import { AgentChatProvider, type ClientToolHandlerFn } from '@inferencesh/sdk/agent';

const client = inference({ apiKey: 'inf_your_key' });

const screenshot: ClientToolHandlerFn = async () => {
  const png: Blob = await captureVisibleTab();     // your capture code
  const ref: FileRef = await client.files.upload(png);
  return {
    result: JSON.stringify({ note: 'The screenshot of the visible tab is attached.' }),
    attachments: [ref],
  };
};

<AgentChatProvider
  client={client}
  agentConfig={{ agent: 'my-org/browser-agent@latest' }}
  clientToolHandlers={new Map([['screenshot', screenshot]])}
>
  {/* chat UI */}
</AgentChatProvider>
```

When you submit results yourself, pass the refs as the third argument: `agent.submitToolResult(call.id, result, [ref])`. Over REST, send `attachments` in the body of [`POST /tools/{id}`](/docs/api/rest/agents#submit-tool-result).

### How each provider receives a tool's images

Each model app sends a tool's images in the form its provider takes, and downsizes images to the provider's limits (it never upscales).

| Provider | Where a tool's images go |
|----------|--------------------------|
| Claude | Image blocks inside the `tool_result` |
| OpenAI, xAI (Responses API) | `function_call_output` output as a list of `input_text` and `input_image` items |
| Gemini 3 | `FunctionResponse.parts` |
| Gemini 2.5 | A user turn after the function responses |
| OpenRouter, Melious, Opper (Chat Completions) | Image parts inside the role `tool` message |

---

## Multiple Parameters

<div data-type="codegroup">

```python Python
from inferencesh import tool, string, number, boolean

search = (
    tool("search")
    .describe("Search the database")
    .param("query", string("Search query"))
    .param("limit", number("Max results"))
    .param("include_archived", boolean("Include archived items"))
    .handler(lambda args: do_search(**args))
    .build()
)
```

```typescript JavaScript
import { tool, string, number, boolean } from '@inferencesh/sdk';

const search = tool('search')
  .describe('Search the database')
  .param('query', string('Search query'))
  .param('limit', number('Max results'))
  .param('includeArchived', boolean('Include archived'))
  .handler(args => doSearch(args))
  .build();
```

</div>

---

## Complex Parameters

<div data-type="codegroup">

```python Python
from inferencesh import tool, obj, string, array

create_order = (
    tool("create_order")
    .describe("Create a new order")
    .param("product", string("Product name"))
    .param("shipping", obj({
        "address": string("Street address"),
        "city": string("City"),
        "zip": string("ZIP code")
    }, "Shipping details"))
    .param("tags", array(string(), "Order tags"))
    .handler(process_order)
    .build()
)
```

```typescript JavaScript
import { tool, obj, string, array } from '@inferencesh/sdk';

const createOrder = tool('create_order')
  .describe('Create a new order')
  .param('product', string('Product name'))
  .param('shipping', obj({
    address: string('Street address'),
    city: string('City'),
    zip: string('ZIP code')
  }, 'Shipping details'))
  .param('tags', array(string(), 'Order tags'))
  .handler(processOrder)
  .build();
```

</div>

---

## Automatic handler dispatch

When you attach a `.handler()` to a client tool, the JavaScript SDK runs it automatically as soon as the invocation reaches `in_progress`. You do not need to poll for `awaiting_input` on the chat — client tools keep the chat in `busy` while they run.

The SDK also accepts `awaiting_input` for backwards compatibility. Interactive widgets use `awaiting_input` on the invocation and typically move the chat to `awaiting_input` as well.

If a handler throws, the SDK submits a JSON error result. If no handler is registered for a tool name, it submits `{ "status": "not_available" }`.

The SDK retries delivering a tool result on transient failures (no response, a 5xx, a 408 or a 429) with backoff, for up to about ten minutes. A refused result (another 4xx) is not retried.

→ [Tool invocation status](/docs/api/rest/agents#tool-invocation-status) · [Polling transport](/docs/api/sdk/polling)

---

## Tools Without Handler

If you omit the handler, the tool becomes "pending" — your code must handle it manually:

<div data-type="codegroup">

```python Python
calculator = (
    tool("calculator")
    .describe("Calculate math")
    .param("expression", string("Math expression"))
    .build()  # No handler!
)

# Later, check for pending tool calls:
response = agent.send_message("What's 42 * 17?")
if response.pending_tool_calls:
    for tc in response.pending_tool_calls:
        result = eval(tc.arguments["expression"])
        agent.submit_tool_result(tc.id, str(result))
```

```typescript JavaScript
const calculator = tool('calculator')
  .describe('Calculate math')
  .param('expression', string('Math expression'))
  .build();  // No handler!

// Later:
const response = await agent.sendMessage("What's 42 * 17?");
if (response.pendingToolCalls) {
  for (const tc of response.pendingToolCalls) {
    const result = eval(tc.arguments.expression);
    await agent.submitToolResult(tc.id, String(result));
  }
}
```

</div>
