Middleware · Guides

LangGraph Client Tools

Client tools are frontend-declared tools that the model can call but the browser executes. The backend only exposes tool schemas to the model and decides whether a turn should continue on the server or end so the client can run a local tool.

#State shape

The middleware reads this state slice:

interface ClientToolsState {
  messages: BaseMessage[];
  tools?: ClientToolSpec[];
  client_tools?: ClientToolSpec[];
}

Each ClientToolSpec is:

interface ClientToolSpec {
  name: string;
  description?: string;
  parameters?: Record<string, unknown>;
}

tools is the primary channel. client_tools is a fallback for raw run input keys.

#Catalog conversion

clientToolSpecs(state) filters out nameless entries and converts each client tool into the OpenAI function-tool shape accepted by LangChain chat models:

{
  type: 'function',
  function: {
    name: spec.name,
    description: spec.description ?? '',
    parameters: spec.parameters ?? {},
  },
}

bindClientTools(llm, serverTools, state) binds server tools first, then appends the client-tool stubs for the current run.

#Routing behavior

clientToolsRouter(serverToolNames) creates a conditional-edge router.

The router sends the graph to the server tool node when the last model message includes a server tool call. A call is treated as server-side when its name appears in serverToolNames or when the name is unknown to the current client catalog.

The router returns END when the last model message has only known client-tool calls or no tool calls.

.addConditionalEdges('agent', clientToolsRouter(['lookupOrder']), ['tools', END])

Use serverToolNames to disambiguate tools you actually execute on the backend.

#Mixed tool calls

When a model message contains both server and client tool calls, server work runs first. This keeps backend-owned side effects on the backend and lets the client tool call surface after the graph resumes.

That behavior comes from hasServerToolCall(): any server or unknown tool call routes to the server tool node.

#Lower-level helpers

Use these helpers when you need custom routing:

import {
  clientToolNames,
  clientToolSpecs,
  hasClientToolCall,
  hasServerToolCall,
  lastMessage,
  routeAfterAgent,
} from '@threadplane/middleware/langgraph';

routeAfterAgent(state, serverToolNames, opts) is the primitive behind clientToolsRouter(). The default destinations are 'tools' and '__end__', but you can override them:

routeAfterAgent(state, ['lookupOrder'], {
  toolsNode: 'serverTools',
  end: '__end__',
});

#Frontend contract

The backend middleware does not execute browser tools. The frontend must:

  1. send the catalog into the run input;
  2. observe the model's tool call;
  3. execute the local tool in the browser;
  4. resume the graph with a ToolMessage containing the result.

The @threadplane/chat client-tools helpers provide the browser-side declaration and execution pieces.