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