With LangGraph and LangChain agents, useStream from @langchain/react gathers every tool call into one array, stream.toolCalls. Each entry has the tool's name, its input, its output once it returns, and a status of running, finished or error. Render each as a tool call row, match calls to the AI message that made them by callId, and group one message's calls under a line that counts them.
How LangGraph exposes tool calls#
The frontend Tool calling page explains the merge: the agent emits tool calls inside an AI message, the runtime runs each tool, the result comes back as a ToolMessage, and useStream unifies call and result into one AssembledToolCall. The docs list its fields: name, callId (also id), namespace, input (also args), output (null until the call succeeds), status and error.
To put calls under the right message, the docs filter stream.toolCalls by the IDs in that message's tool_calls. The docs then render each call as a loading card, an error card or a tool-specific card. A row is the quieter default: one line per call, with the tool-specific card below it when a result deserves one.
From AssembledToolCall to row#
AssembledToolCall |
Tool call row |
|---|---|
name |
name, shown in mono |
status: "running" |
status="running", live timer, present-tense summary |
status: "finished" |
status="done", duration, past-tense summary with the count |
status: "error" |
status="error", error in place, Retry |
input |
input, folded JSON with Copy |
output |
output, folded JSON with Copy |
namespace |
Which subgraph ran it; group by it if you show subagents |
The code#
A LangChain agent with one tool. useStream reads it from a LangGraph server.
import { MemorySaver } from "@langchain/langgraph";
import { createAgent, tool } from "langchain";
import { z } from "zod";
const searchOrders = tool(async ({ email }) => JSON.stringify({ email, orders: 3, lastOrder: "NW-20418" }), {
name: "search_orders",
description: "Find a customer's orders by email",
schema: z.object({ email: z.string() }),
});
export const agent = createAgent({ model: "openai:gpt-5.5", tools: [searchOrders], checkpointer: new MemorySaver() });The client renders the calls of one AI message as rows.
"use client";
import { useRef } from "react";
import type { AssembledToolCall } from "@langchain/react";
import { ToolCallRow, ToolCallRowGroup, type ToolCallRowStatus, type ToolCallRowValue } from "@/components/pixilui/tool-call-row";
const json = (value: unknown) => (value == null ? undefined : (JSON.parse(JSON.stringify(value)) as ToolCallRowValue));
const statusOf = (call: AssembledToolCall): ToolCallRowStatus => (call.status === "finished" ? "done" : call.status);
// Present tense while it runs, past tense with the evidence once it is done.
function summary(call: AssembledToolCall) {
if (call.name === "search_orders") {
const found = call.status === "finished" ? (JSON.parse(String(call.output)) as { orders: number }).orders : undefined;
return found === undefined ? "Searching orders" : `Found ${found} orders`;
}
return call.status === "finished" ? `Ran ${call.name}` : `Running ${call.name}`;
}
export function MessageToolCalls({ callIds, toolCalls }: { callIds: string[]; toolCalls: AssembledToolCall[] }) {
// The calls carry no timing: note when each one first shows up and when it settles.
const times = useRef(new Map<string, { start: number; end?: number }>());
const calls = toolCalls.filter((call) => callIds.includes(call.callId));
if (!calls.length) return null;
return (
<ToolCallRowGroup>
{calls.map((call) => {
const time = times.current.get(call.callId) ?? { start: Date.now() };
if (call.status !== "running" && time.end === undefined) time.end = Date.now();
times.current.set(call.callId, time);
return (
<ToolCallRow
key={call.callId}
name={call.name}
status={statusOf(call)}
summary={summary(call)}
startedAt={time.start}
durationMs={time.end !== undefined ? time.end - time.start : undefined}
input={json(call.input)}
output={json(call.output)}
error={call.error}
/>
);
})}
</ToolCallRowGroup>
);
}Use it under each AI message: <MessageToolCalls callIds={message.tool_calls?.map((t) => t.id ?? "") ?? []} toolCalls={stream.toolCalls} />, where stream is useStream<typeof agent>(...). Install the row with npx shadcn@latest add @pixilui/tool-call-row.
What the row adds to the tool call#
- One quiet line per call. The docs' loading and error cards become states of one row, so a message with four calls stays four lines.
- A summary in words. "Found 3 orders" for the person;
search_ordersin mono for the developer. - Time. A live timer while
running, the duration oncefinished. - The raw payload, folded. Input and output as formatted JSON with Copy, closed by default.
When the framework already ships a UI#
LangGraph's docs list no tool call component; they build the cards in plain markup and name AI Elements, assistant-ui, CopilotKit and OpenUI as libraries that can render on top of useStream. If one of those already renders your chat, use its tool UI. LangSmith shows every tool call in a trace for developers; the row is for the people using your product.
When not to use this#
- A tool whose result is the answer. A weather card or chart should render as itself; keep the row only if the call needs a record.
- Hidden plumbing. Retrieval and routing calls can be noise; show the calls that changed the answer.
- Headless tools on the client. Tools that run in the browser (the docs call them headless tools) may need their own UI, such as a permission prompt.
Questions#
How do I show tool calls from a LangGraph agent in React?
Use useStream from @langchain/react and read stream.toolCalls. Each entry joins the call and its result, with name, input, output, status and error. Map the status to a row's running, done or error state.
How do I show only one message's tool calls?
Filter stream.toolCalls by the IDs in that AI message's tool_calls. The LangChain frontend docs match on callId.
Do LangGraph tool calls include timing?
The AssembledToolCall fields the docs list carry no timestamps. Note when a call first appears and when its status leaves running, or return the duration from the tool itself.
Related#
- How to show tool calls and their results in an AI chat: what a tool row needs, framework aside.
- Agent run timeline with LangGraph: the steps the calls belong to.
- Tool call UI with the Vercel AI SDK: the same row from AI SDK tool parts.
- Tool call UI with the OpenAI Agents SDK: the same row from run item events.
Sources#
- LangChain frontend: Tool calling, read 4 November 2026
- LangChain frontend: Overview, read 4 November 2026
- @langchain/langgraph-sdk on npm (version 1.12.8, its
AssembledToolCalltype), read 4 November 2026