Skip to content

Commit 3142f88

Browse files
feat(langchain): headless tools
1 parent e6e9954 commit 3142f88

File tree

1 file changed

+175
-0
lines changed

1 file changed

+175
-0
lines changed

src/oss/langchain/tools.mdx

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,181 @@ tool_node = ToolNode([get_message_count])
828828

829829
For more details on accessing state, context, and long-term memory from tools, see [Access context](#access-context).
830830

831+
## Headless tools
832+
833+
Some tools should run **where your user's app runs** (typically the browser), not inside the process. **Headless tools** are tool definitions, e.g. name, description, and argument schema, that you register on the **server** with your agent, while the **implementation** is registered only on the **client** and executed after a short interrupt/resume handshake.
834+
835+
This is different from ordinary tools whose function body runs on the server, and from [server-side tool use](#server-side-tool-use) where the model provider executes built-in tools remotely.
836+
837+
### When to use headless tools
838+
839+
Use them when the work depends on **environment, device, or UI** that only exists on the client:
840+
841+
- **Browser APIs:** Geolocation, IndexedDB, Clipboard, Canvas 2D, file pickers, Battery API, etc.
842+
- **Privacy and locality:** Data stays on the device (for example local “memory” in IndexedDB).
843+
- **Latency:** No extra server round trip for purely local operations.
844+
- **Structured, safe effects:** Prefer many small, typed tools (for example one tool per canvas primitive) instead of sending arbitrary code to `eval`.
845+
846+
### How the pattern works
847+
848+
1. **Define** the tool with `tool({ name, description, schema })` from `langchain`, metadata and validation only, no server-side runner.
849+
2. **Implement** the real behavior with `.implement(async (args) => { ... })`, which returns a **headless tool implementation** (definition + `execute` function).
850+
851+
:::python
852+
<Info>
853+
There is no `.implement()` API for this pattern in Python. If your agent is defined in Python, you must **redefine** each headless tool on the **frontend**.
854+
</Info>
855+
:::
856+
:::js
857+
<Info>
858+
Put **tool definitions** (`tool({ name, description, schema })`) and **implementations** (`.implement(...)`) in **separate modules**. Import the shared definition file from your server agent and from your frontend so names and schemas stay aligned; keep client-only execute logic in implementation modules the server never loads.
859+
</Info>
860+
:::
861+
862+
3. **Server agent:** Pass the **definitions** (the objects from step 1) to `createAgent` / your graph so the model sees the tools in its usual tool-calling loop.
863+
4. **Client:** Pass the **implementations** from step 2 to your streaming hook's `tools` option (for example `useStream` from `@langchain/react`, `@langchain/svelte`, `@langchain/vue` or `@langchain/angular`).
864+
865+
When the model issues a tool call for one of these tools, the run **interrupts** with a small payload the SDK recognizes. The client finds the matching implementation, runs it (for example calling `navigator.geolocation` or drawing on a canvas), then **resumes** the graph with the tool result so the agent can continue. You do not need to wire that resume manually when using the supported SDK hooks—they detect headless-tool interrupts, execute the client code, and submit the resume command for you.
866+
867+
Use the optional **`onTool`** callback to observe lifecycle events (`start`, `success`, `error`) for UI feedback such as spinners or toasts.
868+
869+
### Example
870+
871+
Split **definitions** (shared between server and UI) from **implementations** (client-only). The server agent registers the tool so the model can call it; `useStream` registers `.implement(...)` handlers that run when the headless-tool interrupt fires.
872+
873+
<CodeGroup>
874+
875+
:::python
876+
```python agent.py
877+
from langchain.agents import create_agent
878+
from langchain.tools import tool
879+
from langchain_openai import ChatOpenAI
880+
from langgraph.checkpoint.memory import MemorySaver
881+
882+
883+
@tool
884+
def save_note(key: str, text: str) -> str:
885+
"""Persist a short note in the user's browser (local only)."""
886+
# Body is not used when the client handles the tool call; keep name, description,
887+
# and parameters aligned with `tools.ts`.
888+
return ""
889+
890+
891+
agent = create_agent(
892+
model=ChatOpenAI(model="gpt-4o-mini"),
893+
tools=[save_note],
894+
checkpointer=MemorySaver(),
895+
)
896+
```
897+
898+
```ts tools.ts
899+
import * as z from "zod";
900+
import { tool } from "langchain";
901+
902+
/** Mirror the Python tool’s name, description, and fields for the client SDK. */
903+
export const saveNote = tool({
904+
name: "save_note",
905+
description: "Persist a short note in the user's browser (local only).",
906+
schema: z.object({
907+
key: z.string(),
908+
text: z.string(),
909+
}),
910+
});
911+
```
912+
913+
```tsx App.tsx
914+
import { useStream } from "@langchain/langgraph-sdk/react";
915+
import { saveNote } from "./tools";
916+
917+
export function App() {
918+
const stream = useStream({
919+
assistantId: "my-agent",
920+
apiUrl: "http://localhost:2024",
921+
tools: [
922+
saveNote.implement(async ({ key, text }) => {
923+
localStorage.setItem(key, text);
924+
return { ok: true as const, key };
925+
}),
926+
],
927+
onTool: (event) => {
928+
if (event.phase === "error") {
929+
console.error(event.name, event.error);
930+
}
931+
},
932+
});
933+
934+
return null;
935+
}
936+
```
937+
938+
:::
939+
940+
:::js
941+
```ts agent.ts
942+
import { createAgent } from "langchain";
943+
import { ChatOpenAI } from "@langchain/openai";
944+
import { MemorySaver } from "@langchain/langgraph";
945+
946+
import { saveNote } from "./tools";
947+
948+
export const agent = createAgent({
949+
model: new ChatOpenAI({ model: "gpt-4o-mini" }),
950+
tools: [saveNote],
951+
checkpointer: new MemorySaver(),
952+
});
953+
```
954+
955+
```ts tools.ts
956+
import * as z from "zod";
957+
import { tool } from "langchain";
958+
959+
export const saveNote = tool({
960+
name: "save_note",
961+
description: "Persist a short note in the user's browser (local only).",
962+
schema: z.object({
963+
key: z.string(),
964+
text: z.string(),
965+
}),
966+
});
967+
```
968+
969+
```tsx App.tsx
970+
import { useStream } from "@langchain/langgraph-sdk/react";
971+
import type { agent } from "./agent";
972+
import { saveNote } from "./tools";
973+
974+
export function App() {
975+
const stream = useStream<typeof agent>({
976+
assistantId: "my-agent",
977+
apiUrl: "http://localhost:2024",
978+
tools: [
979+
saveNote.implement(async ({ key, text }) => {
980+
localStorage.setItem(key, text);
981+
return { ok: true as const, key };
982+
}),
983+
],
984+
onTool: (event) => {
985+
if (event.phase === "error") {
986+
console.error(event.name, event.error);
987+
}
988+
},
989+
});
990+
991+
return null;
992+
}
993+
```
994+
995+
:::
996+
997+
</CodeGroup>
998+
999+
<Info>
1000+
End-to-end demos in the LangGraph.js repo show two contrasting styles:
1001+
1002+
- **[Browser tools](https://github.com/langchain-ai/langgraphjs/tree/main/examples/ui-react/src/examples/browser-tools)** — IndexedDB-backed memory tools plus geolocation; geolocation is wrapped with human-in-the-loop while other tools run automatically on the client.
1003+
- **[Canvas drawing](https://github.com/langchain-ai/langgraphjs/tree/main/examples/ui-react/src/examples/canvas-drawing)** — Many small canvas tools mapping to safe Canvas 2D operations (no arbitrary code execution).
1004+
</Info>
1005+
8311006
## Prebuilt tools
8321007

8331008
LangChain provides a large collection of prebuilt tools and toolkits for common tasks like web search, code interpretation, database access, and more. These ready-to-use tools can be directly integrated into your agents without writing custom code.

0 commit comments

Comments
 (0)