How headless tools work
At a high level, headless tools split the tool schema from the browser-only implementation.- Register a schema-only tool definition on the agent.
- Implement the matching tool in the frontend with
.implement(...). - Pass those implementations to
useStream({ tools: [...] }). - When the agent emits a matching tool call, the client runs it and resumes the interrupted run with the tool result.
Register the tool on the agent
The playground defines a small set of client-side tools that follow the same pattern: the agent exposes a tool schema, and the frontend handles the actual execution. Define the tools once in a sharedtools.ts file and use that file from both
the agent and the frontend.
Implement the browser behavior
Put the client-only behavior in a separate module and attach it with.implement(...). The real playground includes a fuller IndexedDB store with
search, listing, expiration, and delete operations. The following example shows
the same shape at a higher level:
impl.ts
Wire the implementations into useStream
Pass the implemented tools to useStream. When the agent emits a matching tool
call, the hook runs the client implementation and resumes the run for you.
The agent state can be inferred from the agent definition:
Render tool activity inline
The playground renders each memory or geolocation operation as its own card and keeps a small memory stats panel near the input. The key step is matching each entry instream.toolCalls back to the AI message that triggered it:
Use cases
Use headless tools when the work depends on APIs or data that only exist in the client:- Local memory in IndexedDB or
localStorage - Device APIs like geolocation, clipboard, camera, or file pickers
- Canvas, audio, or other browser-only rendering primitives
- Privacy-sensitive data that should stay on the user’s device
- UI actions that need direct access to in-memory frontend state
Best practices
- Keep tools small and typed. Prefer many narrow tools over one generic “run arbitrary browser code” tool.
- Return JSON-serializable results. Do not try to return DOM nodes, file handles, or other non-serializable browser objects.
- Share definitions, separate implementations. The agent and client should agree on tool names and schemas, but only the client should load browser APIs.
- Surface tool state in the UI. Use
stream.toolCallsandonToolto show pending, success, and error states. - Add review when needed. For sensitive client-side actions, pair this pattern with Human-in-the-loop.
Connect these docs to Claude, VSCode, and more via MCP for real-time answers.

