Tools that run in the browser
Introduction
Section titled “Introduction”You will declare two tools in the browser: one that answers the agent with the reader’s chosen theme, and one that shows an email draft as a card. Both are declared by the client for each run, so no server code or agent file changes.
What you need
Section titled “What you need”- A chat from Add chat to a React app, or any
ChatProvider.
1. Declare the tools
Section titled “1. Declare the tools”A blocking tool pauses the run until serve answers, and the answer is what the model reads. A one-way tool is handed over once its arguments are complete, and the run carries on.
import type { ClientTool } from '@pilotoo/ui/chat';
export const READ_CLIENT_APPEARANCE = 'read_client_appearance';export const EMAIL_DRAFT = 'email_draft';
export function appClientTools(currentTheme: () => 'system' | 'light' | 'dark'): 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: currentTheme() }), }, { kind: 'one-way', name: EMAIL_DRAFT, description: 'Shows an email draft to the user as a card. Call it whenever you write an email for the user, with the whole email in the arguments. Put a {{Readable label}} placeholder where something is unknown, and list each one in `variables`.', arguments: { type: 'object', properties: { subject: { type: 'string' }, body: { type: 'string', description: 'Plain text, not markdown.' }, variables: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, description: { type: 'string' } }, required: ['name'], }, }, }, required: ['subject', 'body'], }, }, ];}2. Draw the one-way tool as a card
Section titled “2. Draw the one-way tool as a card”The registry maps a tool name to a component. EmailDraftCard and parseEmailDraft ship with @pilotoo/ui/chat.
import { EmailDraftCard, parseEmailDraft, type ClientToolRegistry } from '@pilotoo/ui/chat';
import { EMAIL_DRAFT } from './client-tools';
export const CLIENT_TOOL_VIEWS: ClientToolRegistry = { [EMAIL_DRAFT]: ({ arguments: args }) => <EmailDraftCard {...parseEmailDraft(args)} />,};3. Pass both to the chat
Section titled “3. Pass both to the chat”Build the list once. ChatProvider reads clientTools at mount and declares it in every run it opens.
import { useState } from 'react';import { ChatPanel, ChatProvider } from '@pilotoo/ui/chat';
import { appClientTools } from '../chat/client-tools';import { CLIENT_TOOL_VIEWS } from '../chat/client-tool-views';import { currentTheme } from '../theme';
export function AssistantPage() { const [clientTools] = useState(() => appClientTools(currentTheme));
return ( <ChatProvider baseUrl={import.meta.env.VITE_ASSISTANT_URL} clientTools={clientTools}> <ChatPanel clientToolViews={CLIENT_TOOL_VIEWS} /> </ChatProvider> );}4. Let the reader answer a blocking tool
Section titled “4. Let the reader answer a blocking tool”A blocking tool with no serve stops the run and waits for the reader. Register a component for it in the same registry; it receives onAnswer, and the panel shows it in place of the composer.
import type { ClientToolCallProps } from '@pilotoo/ui/chat';
function DatePicker({ onAnswer, disabled }: ClientToolCallProps) { return ( <input type="date" disabled={disabled} onChange={(event) => onAnswer?.({ date: event.target.value })} /> );}Add pick_date: DatePicker to CLIENT_TOOL_VIEWS, and declare pick_date as a blocking tool without serve.
How it works
Section titled “How it works”- The list travels in the input of every run and is stored nowhere on the server. No agent definition has to enable it, and the next run may declare a different list.
- The server validates both ways: the arguments the model produces against
arguments, and your answer againstresult. - A tool whose name collides with a registered server tool is refused rather than shadowing it.
- A name with no view renders as readable text, never as a silent drop.
Next steps
Section titled “Next steps”- Client tools: the full field list and guarantees.
- Tools: tools that run on the server.
- Add chat to a React app: the surrounding setup.