The short version: If your agent already runs on LangGraph, you do not have to rebuild it in the Vercel AI SDK to get a good streaming chat UI. The rewritten @ai-sdk/langchain adapter bridges the two: toBaseMessages() turns the client's UIMessage array into LangChain input, and toUIMessageStream() turns your graph's event stream into an AI SDK UIMessage stream that the useChat hook renders natively — text deltas, streaming tool calls, reasoning, and typed custom data included. You keep LangGraph's checkpoints and interrupts; you get the React chat for free. The bridge is a couple of calls in one route handler.
This is the pragmatic answer to the recurring "LangGraph vs the AI SDK" question: for a lot of teams it is not either/or. Use the graph where orchestration and durable state matter, and the SDK where the UI matters.
The problem it removes#
Before the rewrite, wiring a LangGraph agent to an AI SDK frontend meant hand-writing a createUIMessageStream loop: subscribe to the graph with streamMode: ['custom', 'messages', 'updates'], iterate the events, and manually map each one to the SDK's start-text / delta / tool-call events. It worked, but it was glue you owned and debugged every time the event shapes shifted.
The rewritten adapter absorbs that mapping. You hand it the graph's stream; it emits a spec-correct UIMessage stream.
1. The server route: two calls, in and out#
The route handler does exactly two translations — incoming messages into the graph, outgoing events into the UI stream.
// app/api/chat/route.ts
import { toBaseMessages, toUIMessageStream } from '@ai-sdk/langchain';
import { createUIMessageStreamResponse, type UIMessage } from 'ai';
import { agentGraph } from '@/lib/agent'; // your compiled LangGraph
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
// IN: UIMessages from the client → LangChain BaseMessages the graph expects.
const graphStream = await agentGraph.stream(
{ messages: toBaseMessages(messages) },
{ streamMode: ['messages', 'updates', 'custom'] },
);
// OUT: the graph's event stream → an AI SDK UIMessage stream the UI renders.
return createUIMessageStreamResponse({
stream: toUIMessageStream(graphStream),
});
}
That is the entire bridge. toBaseMessages() handles the direction the SDK's own docs call out — client UIMessages carry parts (text, tool calls, files) that must become LangChain BaseMessages — and toUIMessageStream() handles the return trip. Everything the graph emits under those streamMode values flows through.
2. The client: it is just useChat#
Because the response is a standard AI SDK UIMessage stream, the frontend is the ordinary hook — nothing LangGraph-specific leaks into the UI.
'use client';
import { useChat } from '@ai-sdk/react';
export function Chat() {
const { messages, sendMessage, status } = useChat();
return (
<div>
{messages.map((m) => (
<div key={m.id}>
{m.parts.map((part, i) => {
if (part.type === 'text') return <span key={i}>{part.text}</span>;
// tool-call chips render as their input streams in:
if (part.type.startsWith('tool-')) return <ToolChip key={i} part={part} />;
return null;
})}
</div>
))}
<PromptBox onSend={sendMessage} busy={status !== 'ready'} />
</div>
);
}
Gotcha: the value of the adapter is that it streams more than text. It forwards tool calls with partial input streaming, tool results, reasoning blocks, and multimodal content — so a tool chip can fill in as its arguments arrive, and a thinking panel can render live. If you only read part.type === 'text', you are throwing away the parts that make an agent UI feel alive. Branch on the part types you care about.
3. Custom node data → typed UI events#
Anything a node writes as custom stream data comes through as a typed data-{type} event, so a graph node can drive a bespoke piece of UI — a progress step, a retrieved-source card, a partial plan.
# inside a LangGraph node (Python), emit custom data via the stream writer
def research_node(state, config):
config["writer"]({"type": "source", "url": "https://example.com", "title": "..."})
return {"messages": [...]}
On the client that surfaces as a data-source part you can render into a citations rail. This is the same typed-data channel the AI SDK uses for its own custom parts, so your LangGraph-specific signals become first-class UI without a side channel.
4. Human-in-the-Loop rides the same stream#
A LangGraph interrupt — the graph pausing to ask for approval before a risky tool runs — surfaces through the adapter as a UIMessage event, which maps directly onto the AI SDK's tool-approval pattern. The server pauses the graph; the client shows an approve/reject affordance; the user's answer resumes the graph from the interrupt. You get durable, resumable HITL from LangGraph's checkpointer and a clean approval UI from the SDK, joined by the same bridge — not two competing state machines.
When to reach for LangSmithDeploymentTransport#
If your graph already runs as a LangSmith deployment, you can skip the route handler entirely. LangSmithDeploymentTransport lets the browser stream straight from the hosted graph:
import { useChat } from '@ai-sdk/react';
import { LangSmithDeploymentTransport } from '@ai-sdk/langchain';
const { messages, sendMessage } = useChat({
transport: new LangSmithDeploymentTransport({ /* deployment url + config */ }),
});
Use it when the deployment is the source of truth and you just want a UI on it. Reach for the route-handler pattern (Steps 1–2) when you need to inject auth, rate limits, or your own pre/post-processing between the browser and the graph — which, for most founder products shipping to real users, you do.
The takeaway: the "pick one framework" framing is a false choice for the UI layer. Keep LangGraph for the orchestration and durable state it is good at, and let the rewritten @ai-sdk/langchain adapter hand its stream to the AI SDK's UI. The integration is small, it is officially maintained, and it streams the tool calls and reasoning that make an agent feel like more than a chatbot.



