diff --git a/fern/assistants/client-tool-calls.mdx b/fern/assistants/client-tool-calls.mdx new file mode 100644 index 000000000..2b1fd7665 --- /dev/null +++ b/fern/assistants/client-tool-calls.mdx @@ -0,0 +1,243 @@ +--- +title: Client tool calls +subtitle: Handle tools on the client using the Web SDK +slug: assistants/client-tool-calls +--- + +## Overview + +Run tools directly in your web app. When a tool in your assistant has no `server.url`, Vapi sends a `tool-calls` message to your client. Your app executes the action locally (e.g., update UI, access browser APIs) and can optionally send results back to the assistant. + +**In this guide, you'll learn to:** +- Configure a tool to run on the client +- Enable client messages for tool calling +- Handle `tool-calls` in the Web SDK +- Decide when to return results vs fire-and-forget + +## Prerequisites +- A Vapi assistant with at least one function tool +- Web SDK installed and initialized +- Familiarity with [Web Quickstart](mdc:fern/quickstart/web) + +## Configuration + + + + Omit the `server` property so calls are sent to your client: + + ```json title="Tool (client-executed)" + { + "type": "function", + "async": false, + "function": { + "name": "updateUI", + "description": "Updates UI with new data", + "parameters": { + "type": "object", + "properties": { + "action": { "type": "string" }, + "data": { "type": "object" } + }, + "required": ["action"] + } + }, + "messages": [ + { "type": "request-start", "content": "Let me update that for you..." }, + { "type": "request-complete", "content": "Done!" } + ] + } + ``` + + + + Make sure your assistant sends tool events to the client. You can include the defaults or explicitly list the ones you need: + + ```json title="Assistant (excerpt)" + { + "monitorPlan": { + "clientMessages": [ + "conversation-update", + "function-call", + "model-output", + "speech-update", + "status-update", + "tool-calls", + "tool-calls-result", + "transfer-update", + "transcript", + "user-interrupted" + ] + }, + "model": { + "tools": [ + { + "type": "function", + "function": { "name": "updateUI", "parameters": { "type": "object" } } + } + ] + } + } + ``` + + + `tool-calls` notifies your client when the model wants to run a tool. `tool-calls-result` forwards results back to clients (useful when tools execute elsewhere). + + + + +## Handle tool calls in the Web SDK + + +```tsx title="React / TypeScript" +import React, { useEffect, useState } from 'react'; +import Vapi from '@vapi-ai/web'; + +function App() { + const [vapi, setVapi] = useState(null); + + useEffect(() => { + const client = new Vapi('YOUR_PUBLIC_API_KEY'); + setVapi(client); + + client.on('message', async (message: any) => { + if (message.type === 'tool-calls') { + // message.toolCallList: minimal list of calls + // message.toolWithToolCallList: tools with full definitions + calls + for (const call of message.toolCallList) { + const fn = call.function?.name; + const args = safeParse(call.function?.arguments); + + if (fn === 'updateUI') { + await handleUIUpdate(args?.action, args?.data); + // For sync tools (async: false), optionally add a function result to context + client.send({ + type: 'add-message', + message: { + role: 'function', + name: 'updateUI', + content: JSON.stringify({ ok: true }) + } + }); + } + } + } else if (message.type === 'tool-calls-result') { + // Optional: consume results produced by non-client tools + console.log('Tool result:', message.toolCallResult); + } + }); + + return () => client.stop(); + }, []); + + return ( + + ); +} + +function safeParse(v: unknown) { + if (typeof v === 'string') { + try { return JSON.parse(v); } catch { return undefined; } + } + return v ?? undefined; +} + +async function handleUIUpdate(action?: string, data?: Record) { + if (!action) return; + switch (action) { + case 'show_notification': + // implement your UI behavior + break; + case 'navigate': + // router.push(data?.href as string) + break; + default: + break; + } +} + +export default App; +``` + +```ts title="Vanilla TypeScript" +import Vapi from '@vapi-ai/web'; + +const vapi = new Vapi('YOUR_PUBLIC_API_KEY'); + +evapi.on('message', async (message: any) => { + if (message.type === 'tool-calls') { + for (const call of message.toolCallList) { + const fn = call.function?.name; + const args = typeof call.function?.arguments === 'string' + ? JSON.parse(call.function.arguments) + : call.function?.arguments; + + if (fn === 'showNotification') { + showToast(args?.title, args?.message, args?.variant); + // fire-and-forget example: if tool is async, you can skip returning a result + } + } + } +}); + +function showToast(title?: string, message?: string, variant: 'info'|'success'|'warning'|'error' = 'info') { + console.log(`[${variant}] ${title ?? ''} ${message ?? ''}`); +} +``` + + + +Results can be reflected back to the model by adding a function message via `vapi.send({ type: "add-message", message: { role: "function", name, content } })`. Additionally, when other systems execute tools, your client can receive their outcomes via the `tool-calls-result` client message. + + +## When to respond vs not respond + +Use these rules to decide whether to send a result back after handling a tool on the client: + +- **Respond (send a result) when:** + - **Tool is sync (`async: false`)** and the model should incorporate the output into its next turn + - You want the transcript/history to include a structured function result + - The assistant prompted with `request-complete`/`request-failed` messaging that depends on outcome data + +- **Do not respond (fire-and-forget) when:** + - **Tool is async (`async: true`)** and UI-side effects are enough (e.g., open modal, play sound) + - The result is purely visual and doesn’t change the conversation + - Returning data would be redundant or overly large + + +If a tool is configured as sync but your client never returns a result (e.g., via a function message), the assistant may wait or proceed with limited context. Prefer `async: true` for one-way UI effects. + + +## Example: client-side notification tool + +```json +{ + "type": "function", + "async": true, + "function": { + "name": "showNotification", + "description": "Shows a notification to the user in the browser", + "parameters": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "message": { "type": "string" }, + "variant": { "type": "string", "enum": ["info", "success", "warning", "error"] } + }, + "required": ["message"] + } + }, + "messages": [ + { "type": "request-start", "content": "I'll show you that notification now." } + ] +} +``` + +## Troubleshooting +- **Not receiving `tool-calls` on the client?** Ensure `monitorPlan.clientMessages` includes `tool-calls` and you started the call with the Web SDK. +- **Parse errors on arguments?** Some providers stream arguments as strings—safely `JSON.parse` when needed. +- **Assistant waiting on results?** Either return a function message result or set the tool to `async: true`. + +## Next steps +- **Web Quickstart:** Build and test calls with the [Web SDK](mdc:fern/quickstart/web) +- **Custom tools:** Learn server-executed tools and formats in [Custom Tools](mdc:fern/tools/custom-tools) +- **Server events:** Compare with server-side `tool-calls` in [Server events](mdc:fern/server-url/events)