Skip to content
kitn AI/UI

LangGraph

LangGraph runs the agent loop — model calls, tool calls, state — and emits it all as a stream. Point a server route at that stream, pull out the assistant text, and forward it to <kai-chat> as SSE.

Stream the graph with streamMode: "messages"

Section titled “Stream the graph with streamMode: "messages"”

Compile your graph, then call graph.stream(input, { streamMode: "messages" }). That mode yields [messageChunk, metadata] tuples: messageChunk.content is the token delta, and metadata tells you which node produced it. The other modes — updates, values, custom, debug — give you state, not tokens, so messages is the one for chat.

// POST /api/chat — stream a compiled LangGraph agent to the browser
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
const getWeather = tool(
async ({ city }) => `It's 18°C and clear in ${city}.`,
{
name: 'get_weather',
description: 'Get the current weather for a city.',
schema: z.object({ city: z.string() }),
},
);
const agent = createReactAgent({
llm: new ChatOpenAI({ model: 'gpt-4o' }),
tools: [getWeather],
});
export async function POST(req: Request) {
const { messages } = await req.json();
const stream = await agent.stream({ messages }, { streamMode: 'messages' });
const encoder = new TextEncoder();
const body = new ReadableStream({
async start(controller) {
const send = (obj: unknown) =>
controller.enqueue(encoder.encode(`data: ${JSON.stringify(obj)}\n\n`));
for await (const [chunk] of stream) {
if (typeof chunk.content === 'string' && chunk.content) {
send({ choices: [{ delta: { content: chunk.content } }] });
}
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
},
});
return new Response(body, { headers: { 'Content-Type': 'text/event-stream' } });
}

The browser side is the OpenAI-format SSE reader from the Streaming recipe — text deltas land in the assistant message. You don’t write that loop again here.

The loop above renders the answer. To show the agent’s work — the tool calls and reasoning LangGraph emits — map them onto the message contract and forward extra frames a richer browser reader picks up. A <kai-chat> message carries a tools[] array, each part moving through a state:

LangGraph pieceMessage contract field
messageChunk.content (string)assistant content
chunk.tool_call_chunks[].name / .argstool part input (input-streaminginput-available)
ToolMessage.contenttool part output (output-available)
additional_kwargs.reasoning_contentreasoning.text

Open a tool part with state: "input-streaming" and a toolCallId when the first chunk arrives, fill input as the arguments stream, then set output and flip to output-available once the ToolMessage lands. A failed tool becomes output-error with errorText. Render the result with a tool card and reasoning with a reasoning part.

If your front end already speaks Vercel AI SDK message streams, skip the hand-rolled loop — there’s an adapter (@ai-sdk/langchain) that turns a LangGraph run into AI SDK UI messages, tool calls and all. Check the LangGraph streaming docs for the current adapter before wiring it in.