Headless tools
Headless tools let your agent call tools whose real execution must happen in the user's app instead of on the server. The agent still sees a normal tool schema, but the implementation lives in the frontend, where it can access browser APIs like IndexedDB, geolocation, clipboard, canvas, or file pickers.
This pattern is especially useful when data should stay local to the device. The playground example on this page uses a small browser-memory toolkit backed by IndexedDB plus a geolocation tool that runs entirely on the client.
import { PatternEmbed } from "/snippets/pattern-embed.jsx"
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 call interrupt(), then
mirror the same tool names and argument fields in a
frontend tools.ts file.
Each tool interrupts with a structured payload the frontend can handle, then returns the value provided when the run resumes. Mirror the same tool names and schemas on the client so the frontend can attach implementations.
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:
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:
import { useStream } from "@langchain/react";
import { geolocationGet, memoryGet, memoryPut } from "./impl";
import type { AgentState } from "./types";
const AGENT_URL = "http://localhost:2024";
export function Chat() {
const stream = useStream({
apiUrl: AGENT_URL,
assistantId: "headless_tools",
tools: [memoryPut, memoryGet, geolocationGet],
});
return ;
}<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { geolocationGet, memoryGet, memoryPut } from "./impl";
import type { AgentState } from "./types";
const AGENT_URL = "http://localhost:2024";
const stream = useStream({
apiUrl: AGENT_URL,
assistantId: "headless_tools",
tools: [memoryPut, memoryGet, geolocationGet],
});
</script>
<template>
<ChatView
:messages="stream.messages.value"
:tool-calls="stream.toolCalls.value"
/>
</template><script lang="ts">
import { useStream } from "@langchain/svelte";
import { geolocationGet, memoryGet, memoryPut } from "./impl";
import type { AgentState } from "./types";
const AGENT_URL = "http://localhost:2024";
const { messages, toolCalls } = useStream({
apiUrl: AGENT_URL,
assistantId: "headless_tools",
tools: [memoryPut, memoryGet, geolocationGet],
});
</script>
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 in stream.toolCalls back to the AI message that triggered it:
import type { ToolCallWithResult, DefaultToolCall } from "@langchain/react";
function Message({ message, toolCalls }: {
message: AIMessage,
toolCalls: ToolCallWithResult[]
}) {
const messageToolCalls = toolCalls.filter((tc) =>
message.tool_calls?.some((call) => call.id === tc.call.id),
);
return (
<div>
{message.text && <p>{message.text}</p>}
{messageToolCalls.map((tc) => (
))}
</div>
);
}This works especially well with the richer UI patterns from Tool calling (opens in a new tab), where each tool result can render as a specialized card instead of raw JSON.
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 (opens in a new tab).