Client tools
Introduction
Section titled “Introduction”Some things only your client knows: which theme the reader picked, what is selected on screen, which form is open. A client tool is a tool your app declares and your app answers. The server never stores it and no agent definition enables it. Use one when the agent needs information from, or wants to show something in, the interface the reader is looking at.
There are two kinds. A blocking tool pauses the run, your handler answers, and the model reads the answer. A one-way tool hands over its arguments and the run carries on, which fits “show this to the reader”.
Declaring a tool the client answers
Section titled “Declaring a tool the client answers”The demo web app declares read_client_appearance so the agent can ask which appearance the reader chose and write its next sentence accordingly.
import type { ClientTool } from '@pilotoo/chat-core';
export function webClientTools(theme: ThemeControl): ClientTool[] { return [ { kind: 'blocking', name: 'read_client_appearance', description: 'Reports the appearance this reader chose for the chat: light, dark, or the system default.', arguments: { type: 'object', properties: {}, additionalProperties: false }, result: { type: 'object', properties: { appearance: { type: 'string', enum: ['system', 'light', 'dark'] } }, required: ['appearance'], }, serve: () => ({ appearance: theme.current() }), }, ];}arguments and result are JSON Schema. The gateway validates what the model sends against arguments and what you answer against result.
Passing the list to the session
Section titled “Passing the list to the session”Give the list to the session provider. It declares the list in the input of every run it opens and every resume that continues one.
const [transport] = useState(() => createSocketIoTransport(apiUrl, { credentials }));const [clientTools] = useState(() => webClientTools(theme));
return ( <CoreChatSessionProvider clientTools={clientTools} transport={transport}> {children} </CoreChatSessionProvider>);Showing something with a one-way tool
Section titled “Showing something with a one-way tool”A one-way tool has no serve. The demo web app declares email_draft: the agent writes the whole email into the arguments, and the app draws a card with copy buttons instead of repeating the text in the reply.
{ kind: 'one-way', name: 'email_draft', description: 'Shows an email draft to the user as a card with copy buttons. ...', arguments: { type: 'object', properties: { subject: { type: 'string' }, body: { type: 'string', description: 'Plain text, not markdown.' }, variables: { type: 'array', items: { /* { name, description? } */ } }, }, required: ['subject', 'body'], },}Register a component under the tool’s name. EmailDraftCard and parseEmailDraft ship with @pilotoo/ui/chat.
import { EmailDraftCard, parseEmailDraft, type ClientToolRegistry } from '@pilotoo/ui/chat';
export const CLIENT_TOOL_VIEWS: ClientToolRegistry = { email_draft: ({ arguments: args }) => <EmailDraftCard {...parseEmailDraft(args)} labels={CHAT_LABELS} />,};The card shows {{Readable label}} placeholders as chips, lists what is left to fill in, and copies the draft or opens a mailto: link.
Letting the reader answer
Section titled “Letting the reader answer”A blocking tool with no serve waits for the reader. It lands as the conversation’s pendingDecision, and you draw it through the same registry.
import { ClientToolView, type ClientToolRegistry } from '@pilotoo/chat-ui';
// `DatePicker` is your component; `call` is the pending client-tool call; `resume` sends the answer.const registry: ClientToolRegistry = { pick_date: DatePicker };
<ClientToolView arguments={call.arguments} name={call.name} registry={registry} onAnswer={resume} />;clientToolCall(pendingDecision.payload) from @pilotoo/chat-core tells whether a pending decision is a client tool’s call or the server’s own, and returns the call above.
To draw a one-way call in the transcript, give TranscriptToolCall the registry. Until the arguments are complete JSON the generic row stays; your component replaces it once they close, live and after a reload.
import { TranscriptToolCall } from '@pilotoo/chat-ui';
<TranscriptToolCall args={call.args} name={call.name} registry={registry} status={call.status} />;Options
Section titled “Options”| Field | Meaning |
|---|---|
kind |
blocking: the run pauses and the answer is what the model reads. one-way: handed over once its arguments are complete; the run carries on. |
name, description |
What the model sees. |
arguments |
JSON Schema for the arguments. |
result |
JSON Schema for the answer (blocking only). |
serve |
Optional handler. |
- A name that collides with a registered server tool is refused rather than shadowing it.
- A schema may not carry
patternorpatternPropertiesat any depth; the run is refused at its start if it does. Check formats inside your handler. - A handler that throws answers the run with a readable refusal instead of leaving it paused.
- Subagents have no client tools.
Next steps
Section titled “Next steps”- Tools for tools that run on the server.
- Connecting a client for the transport the session uses.
- Embedding the web chat to mount the demo web app in a host page.