Skip to main content

What Are Client-Side Tools?

Client-side tools let your agent run functions on the device. You register a handler, and the SDK does the rest. When the agent asks for the tool, your handler runs, the result goes back to the agent, and the reply continues, all inside the same send(...) call.
Client-side tools match the Custom Actions set up on your agent in the Chatbase Dashboard. The name you register must match the action’s name.

tool

Registers a tool handler. Registering the same name twice replaces the old handler.
String
required
The tool name. Must match a Custom Action on your agent.
@Sendable (JSONValue) async throws -> JSONValue
required
An async closure. It receives the tool’s input as a JSONValue and returns the result as a JSONValue.
Register your tools once, right after creating the client, before the first send:
Tool results can be at most 20 KB of JSON. Anything larger fails with a 400 VALIDATION_INVALID_BODY error. Return only the fields the agent needs. Never return a whole API response.

How the Tool Loop Works

One send call can go back and forth with the server several times:
1

You send a message

client.send("What's the weather in Tokyo?") opens the connection.
2

The agent asks for a tool

The SDK runs your onToolCall callback and looks up your handler.
3

Your handler runs

The SDK waits for it, sends the result to the server, and runs onToolResult.
4

The reply continues

The SDK reconnects to the same conversation. The agent can now see the tool result, and either answers or asks for another tool.
5

Repeat until finished

This continues until the agent finishes without asking for a tool. send then returns the ChatResponse.
All of this happens inside that one await. Text from every round is joined together into response.message.text.

Tool loop limit

There is a limit so a confused agent cannot loop forever. The default is 10 rounds, and you can change it per client:
Going over the limit throws ChatError.toolLoopLimitExceeded(limit:):
Hitting the limit usually means the agent keeps calling a tool because the result does not answer its question. Check that your handler returns what the action’s description promises.

Automatic retries

Sending a tool result is retried up to 3 times, waiting 300 ms, then 600 ms, then 1.2 s. This covers the short gap before the server is ready for the result. You do not need to retry yourself.

When a Tool Fails

Return an object with an error key to tell the agent the tool failed, so it can try something else or explain the problem to the user:
Thrown errors become the same thing. If your handler throws, the SDK sends {"error": "<the error's description>"} instead of failing the whole send call, so one broken tool does not kill the reply.
Because these errors are shown to the agent, they can end up in the conversation. Do not throw errors whose description contains tokens, signed URLs, internal IDs, or stack traces. Return a message you are happy for a user to read.
CancellationError is the one exception. It is passed through, so cancelling the Task cancels the whole reply instead of reporting a failed tool.

Tools with no handler

If the agent asks for a tool you never registered, the SDK sends back {"error": "No handler registered for tool 'name'"}. The agent can then apologize or try something else, instead of hanging.

Tools That Ask the User

Handlers are async, so they can wait for the user and return their answer as the tool result. This is how you build confirmation prompts, pickers, and in-chat forms.
Always call resume exactly once, on every path, including when the user dismisses the sheet. If you never resume it, the tool waits forever and send never returns. When the user cancels, resume with a default value or an error result.

Watching Tools Run

Use onToolCall and onToolResult to show progress:
Both callbacks also run for tools the server handles, such as server-side Custom Actions and integrations, not just your own handlers. That means one piece of UI can show every tool the agent uses. See ToolCallInfo and ToolResultInfo.
ConversationState turns these callbacks into tool cards in the message list, with a running, finished, or failed state, using the error key rule above. Start there if you want tool UI without writing it.

Reading Tool Input

Input arrives as a JSONValue. Read it with the subscript and the typed properties:
For bigger inputs, decode into your own type. JSONValue works with Codable:

Streaming

The send method and its callbacks

JSONValue

Reading and writing tool data

SwiftUI

Tool cards you get for free

Error Handling

Handling errors while tools run