How headless tools work
At a high level, headless tools split the tool schema from the browser-only implementation.- Register a tool on the agent that immediately calls
interrupt()to defer execution to the frontend. - Mirror the same tool names and argument fields in frontend definitions.
- Implement the matching tools in the frontend with
.implement(...)and pass them touseStream({ tools: [...] }). - When the agent invokes a matching tool, the client handles the action 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 normal tools on the server that immediately callinterrupt(), then
mirror the same tool names and argument fields in a
frontend tools.ts file.
agent.py
tools.ts
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.
Define a TypeScript interface matching your agent’s state schema and pass it as
a type parameter to useStream for type-safe access to state values:
types.ts
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.

