Client Tools

Tools executed by your code via a handler function.


Basic Usage

1from inferencesh import tool, string23greet = (4    tool("greet")5    .describe("Greet a user by name")6    .param("name", string("User's name"))7    .handler(lambda args: f"Hello, {args['name']}!")8    .build()9)

Builder Methods

MethodDescription
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

1def get_weather(args):2    city = args["city"]3    # Call weather API...4    return f"Weather in {city}: 72°F, Sunny"56weather = (7    tool("get_weather")8    .describe("Get current weather for a city")9    .param("city", string("City name"))10    .handler(get_weather)11    .build()12)

Async Handlers

1import aiohttp23async def fetch_data(args):4    async with aiohttp.ClientSession() as session:5        async with session.get(args["url"]) as response:6            return await response.text()78fetcher = (9    tool("fetch")10    .describe("Fetch data from URL")11    .param("url", string("URL to fetch"))12    .handler(fetch_data)13    .build()14)

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
1import { inference, type FileRef } from '@inferencesh/sdk';2import { AgentChatProvider, type ClientToolHandlerFn } from '@inferencesh/sdk/agent';34const client = inference({ apiKey: 'inf_your_key' });56const screenshot: ClientToolHandlerFn = async () => {7  const png: Blob = await captureVisibleTab();     // your capture code8  const ref: FileRef = await client.files.upload(png);9  return {10    result: JSON.stringify({ note: 'The screenshot of the visible tab is attached.' }),11    attachments: [ref],12  };13};1415<AgentChatProvider16  client={client}17  agentConfig={{ agent: 'my-org/browser-agent@latest' }}18  clientToolHandlers={new Map([['screenshot', screenshot]])}19>20  {/* chat UI */}21</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}.

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

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

Multiple Parameters

1from inferencesh import tool, string, number, boolean23search = (4    tool("search")5    .describe("Search the database")6    .param("query", string("Search query"))7    .param("limit", number("Max results"))8    .param("include_archived", boolean("Include archived items"))9    .handler(lambda args: do_search(**args))10    .build()11)

Complex Parameters

1from inferencesh import tool, obj, string, array23create_order = (4    tool("create_order")5    .describe("Create a new order")6    .param("product", string("Product name"))7    .param("shipping", obj({8        "address": string("Street address"),9        "city": string("City"),10        "zip": string("ZIP code")11    }, "Shipping details"))12    .param("tags", array(string(), "Order tags"))13    .handler(process_order)14    .build()15)

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 · Polling transport


Tools Without Handler

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

1calculator = (2    tool("calculator")3    .describe("Calculate math")4    .param("expression", string("Math expression"))5    .build()  # No handler!6)78# Later, check for pending tool calls:9response = agent.send_message("What's 42 * 17?")10if response.pending_tool_calls:11    for tc in response.pending_tool_calls:12        result = eval(tc.arguments["expression"])13        agent.submit_tool_result(tc.id, str(result))

we use cookies

we use cookies to ensure you get the best experience on our website. for more information on how we use cookies, please see our cookie policy.

by clicking "accept", you agree to our use of cookies.
learn more.