Skip to content
AdaptersLangGraph adapterMenu

LangGraph adapter

Render LangGraph agents with chatcn: SDK messages and run streams mapped to parts, with human-in-the-loop interrupts as approvals.

A recorded runs.stream session: streamed thinking, a tool call and a review interrupt.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/use-chatcn-langgraph.json

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

tsx
"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

ts
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.

InterruptResume 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 thinking blocks, OpenAI reasoning summaries and additional_kwargs.reasoning_content.
  • While a message streams, tool_call_chunks parse as partial JSON (input-streaming); tool_calls from the finished message replace them.
  • A ToolMessage lands on its call: output-available, or output-error when status is error. invalid_tool_calls render as errors.
  • Subgraph events (messages|ns) stream into the same transcript; values from subgraphs are ignored.

Notes

  • stop() aborts the stream and calls client.runs.cancel when the client has it.
  • regenerate, editMessage and submitToolOutput are left out: in LangGraph they mean forking a checkpoint, which your app decides. Blocks hide those controls.
  • Prefer the AI SDK? @ai-sdk/langchain turns LangGraph streams into UI message streams that the AI SDK adapter renders as is.