LangGraph adapter
Render LangGraph agents with chatcn: SDK messages and run streams mapped to parts, with human-in-the-loop interrupts as approvals.
On this page
Installation
This installs the hook and the chatcn-langgraph mapping it builds on. Install only chatcn-langgraph to convert thread state or drive your own stream.
Usage
"use client"
import { Client } from "@langchain/langgraph-sdk"
import { useMemo } from "react"
import { Chatbot } from "@/components/chatcn/chatbot/chatbot"
import { useChatcnLangGraph } from "@/hooks/use-chatcn-langgraph"
export function GraphChat() {
const client = useMemo(() => new Client({ apiUrl: process.env.NEXT_PUBLIC_LANGGRAPH_URL }), [])
const { messages, status, error, actions } = useChatcnLangGraph({ client, assistantId: "agent" })
return (
<Chatbot
messages={messages}
actions={actions}
status={status === "running" ? "streaming" : status === "error" ? "error" : "ready"}
error={error ? new Error(error.message) : null}
/>
)
}The hook creates a thread on the first message (or uses threadId), streams with streamMode: ["values", "messages-tuple", "updates"] and sends { messages: [message] } as input. Change it with input, and point messagesKey at your state key if it is not messages.
Thread history
import { fromLangGraphMessages } from "@/lib/chatcn-langgraph"
const thread = await client.threads.get(threadId)
const messages = fromLangGraphMessages(thread.values.messages)Human in the loop
Interrupts land on the tool call they review. approve(id) and deny(id, reason) record the answer; once every request has one, the hook resumes with command: { resume } in the shape the interrupt asked for.
| Interrupt | Resume value |
|---|---|
LangChain humanInTheLoopMiddleware (action_requests): each request on its tool call | { decisions: [{ type: "approve" }, { type: "reject", message }] } |
Agent Inbox (action_request): each request on its tool call | [{ type: "accept" | "response" | "ignore", args }] |
Anything else: its own approval part, titled from question, message or description | { approved, reason } |
Several interrupts resume as a map keyed by interrupt id. Pass resume: (state) => ... to build the value yourself.
Message mapping
- Everything between two human messages folds into one assistant message, so a multi-step agent reads as one reply with its tool trace.
- Reasoning comes from Anthropic
thinkingblocks, OpenAIreasoningsummaries andadditional_kwargs.reasoning_content. - While a message streams,
tool_call_chunksparse as partial JSON (input-streaming);tool_callsfrom the finished message replace them. - A
ToolMessagelands on its call:output-available, oroutput-errorwhenstatusiserror.invalid_tool_callsrender as errors. - Subgraph events (
messages|ns) stream into the same transcript;valuesfrom subgraphs are ignored.
Notes
stop()aborts the stream and callsclient.runs.cancelwhen the client has it.regenerate,editMessageandsubmitToolOutputare left out: in LangGraph they mean forking a checkpoint, which your app decides. Blocks hide those controls.- Prefer the AI SDK?
@ai-sdk/langchainturns LangGraph streams into UI message streams that the AI SDK adapter renders as is.