
Langgraph Multiagent
- 6 installs
- 5 repo stars
- Updated August 5, 2026
- bjornmelin/dev-skills
langgraph-multiagent is a skill that builds, audits, and migrates LangGraph/LangChain multi-agent systems with version-accurate APIs.
About
This skill builds, audits, and migrates multi-agent systems using LangGraph and LangChain. Developers use it to design supervisor and orchestrator-worker topologies, wire tools, memory, guardrails, and MCP integration, and add observability. It stays version-accurate by resolving current APIs from docs and installed versions and helps migrate off deprecated patterns.
- Architect-level development, audit, and migration of LangGraph/LangChain multi-agent systems
- Covers supervisor/subagent topologies, memory, guardrails, and MCP tool integration
- Migrates off deprecated patterns like create_react_agent and langgraph-supervisor
Langgraph Multiagent by the numbers
- 6 all-time installs (skills.sh)
- Ranked #12,756 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
langgraph-multiagent capabilities & compatibility
- Capabilities
- orchestration
- Works with
- anthropic · openai
- Use cases
- orchestration · research · refactoring
What langgraph-multiagent says it does
Build, review, and modernize production-grade multi-agent systems with LangGraph/LangChain while staying version-accurate by default
**Supervisor + subagents (tool-calling)**: a main “supervisor” calls subagents as tools for context isolation.
Treat all LangGraph/LangChain APIs as **versioned**; never “code from memory” for any API surface that might have changed.
npx skills add https://github.com/bjornmelin/dev-skills --skill langgraph-multiagentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 5, 2026 |
| Repository | bjornmelin/dev-skills ↗ |
What it does
Building or migrating a LangGraph/LangChain multi-agent system with supervisor topologies, memory, guardrails, and MCP tools.
Who is it for?
Developers building or modernizing LangGraph/LangChain multi-agent systems.
Skip if: Non-LangGraph agent frameworks where those libraries own the implementation.
When should I use this skill?
Building, refactoring, or migrating LangGraph/LangChain supervisor/subagent or orchestrator-worker systems.
By the numbers
- Targets LangGraph v1+ and LangChain v1+
- Three workflow decision-tree branches: build, audit/migrate, performance/scale
Files
LangGraph Multi-Agent
What this skill does
Build, review, and modernize production-grade multi-agent systems with LangGraph/LangChain while staying version-accurate by default: always resolve the current APIs from docs + installed versions, and fall back to opensrc/ source snapshots for under-the-hood edge cases.
Default operating rules (do these every time)
1. Treat all LangGraph/LangChain APIs as versioned; never “code from memory” for any API surface that might have changed. 2. Establish ground truth before coding:
- Determine installed/pinned versions (repo lockfiles +
importlib.metadata). - Query official docs via
langchain-docs.SearchDocsByLangChain. - Use Context7 for canonical API references and examples.
3. When docs are ambiguous or behavior is subtle, inspect dependency internals via opensrc/ (read-only):
- Run
npx opensrc pypi:<package>@<version> --modify=false - Check
opensrc/sources.jsonand cite exactopensrc/...paths + versions in your writeup.
Use references/research_playbook.md when you need a rigorous doc-sweep, including llms.txt-driven crawling of LangGraph docs.
Workflow decision tree
- Build something new → follow Build workflow.
- Audit/deprecations/migration → follow Audit & migrate workflow.
- Latency/cost/scale issues → follow Performance & scale workflow.
Quick start (repo-aware)
1. Generate a deprecations + framework audit report:
- Run
python scripts/audit_repo_agents.py --root .(from the skill folder) - Optional:
python scripts/audit_repo_agents.py --root . --json agent_audit.json - Optional:
python scripts/generate_migration_plan.py --audit-json agent_audit.json --out migration_plan.md
2. If you need “latest docs” for a specific area, start with:
langchain-docs.SearchDocsByLangChainusing queries fromreferences/docs_index.md
3. If you hit a behavior edge case, snapshot internals:
- Run
python scripts/opensrc_snapshot.py --packages langgraph langchain langchain-core(from the skill folder)
4. If you need a doc sitemap for LangGraph (for agentic RAG or doc crawling):
- Run
python scripts/fetch_llms_txt_urls.py --print --unique(from the skill folder)
5. If you need to build an offline docs cache (bounded crawl):
- Use seeds in
references/doc_crawl_targets.md - Run
python scripts/crawl_docs.py --llms-txt https://langchain-ai.github.io/langgraph/llms.txt --allow-prefixes https://langchain-ai.github.io/langgraph/ --out-dir docs_cache_langgraph(from the skill folder)
Reference map (use these, don’t guess)
references/langchain_create_agent_middleware.md:create_agent+ middleware hooks + migration mappings.references/langchain_multiagent_handoffs.md: choosing between subagents vs handoffs vs multi-node subgraphs.references/langgraph_graph_api_primitives.md: reducers, Send API, subgraphs, and common error codes.references/memory_and_context_engineering.md: state vs store vs runtime context; memory design.references/mcp_integration_patterns.md: MCP client/interceptors and ToolRuntime-driven auth/DI.references/testing_evaluation.md: tests + eval strategy for safe migrations.references/deployment_agent_server.md:langgraph.jsonand Agent Server deployment basics.references/security_threat_model.md: threat model + mitigations for tool calling systems.references/upgrades_and_versioning.md: repeatable upgrade process.references/audit_and_migration_methodology.md: end-to-end audit→plan→execute methodology.references/api_map_python.md: import-path cheat sheet (verify for your version).references/ui_nextjs_rsc.md: Next.js App Router (RSC) + React UI integration (Agent Server +useStream).references/ui_nextjs_ai_sdk.md: Next.js App Router using AI SDK v6 (useChat) + Streamdown (alternative UI stack).references/ui_fastapi_backend.md: FastAPI backend patterns for in-process agents or Agent Server BFF/proxy.references/ui_streaming_protocol.md: simple SSE event protocol for custom UI backends.references/ui_streamlit.md: Streamlit UI integration patterns (streaming + HITL).
Build workflow (LangChain v1 + LangGraph v1+)
1. Select the right multi-agent topology (start simple):
- Supervisor + subagents (tool-calling): a main “supervisor” calls subagents as tools for context isolation.
- Orchestrator-worker (fan-out): use LangGraph’s worker primitives (e.g., Send-style fanout) for parallelizable tasks; use reducers to avoid concurrent state-update errors.
- Deterministic workflow + agent nodes: keep control-flow explicit; use agents only where needed.
2. Define strict tool schemas and failure behavior:
- Make tools idempotent where possible; add timeouts and retries; “fail open” on non-critical model/rerank steps.
3. Do context engineering explicitly:
- Use typed state for dynamic runtime context; keep “store” for long-term memory; use runtime context injection for user/org-scoped dependencies.
4. Add safety controls early:
- Use middleware guardrails and human-in-the-loop for high-stakes tools (payments, outbound emails, destructive actions).
5. Add observability before scaling:
- Enable tracing (LangSmith/OpenTelemetry), record tool latency/cost, and add regression evaluations.
Use references/patterns.md for design templates and “gotchas” per topology.
Audit & migrate workflow (deprecated patterns → modern stack)
1. Inventory current architecture (dependencies + imports + runtime behavior):
- Run
scripts/audit_repo_agents.pyand expand it with repo-specific patterns if needed.
2. Identify deprecated patterns and their replacements using official docs:
- Prioritize the LangGraph v1 and LangChain v1 migration guides.
- Verify whether
langgraph.prebuilt.create_react_agentusage should move tolangchain.agents.create_agent.
3. Plan the migration in slices (keep it shippable):
- Preserve external behavior; migrate internals; then upgrade prompts/tools; finally tighten types and add tests.
4. Execute migration and harden:
- Replace legacy agent frameworks (LlamaIndex agents, CrewAI, Agno, OpenAI Agents) with LangGraph/LangChain equivalents.
- Add offline tests with fixtures/mocks, and add trace-based regression checks when possible.
Use references/migration_supervisor.md and references/migration_other_frameworks.md for playbooks.
Performance & scale workflow
1. Add measurement first:
- Capture per-node latency, token usage, tool call counts, cache hit rates, and error taxonomy.
2. Apply the “cheapest win” stack in order:
- Reduce context size (summaries, retrieval, subagent isolation).
- Cache tool results (deterministic and keyed); add request coalescing.
- Parallelize only where state updates are safe (reducers, fanout collection keys).
- Use smaller/faster models for routing/validation; reserve larger models for synthesis.
3. Constrain worst-case behavior:
- Recursion limits, max steps, max tool calls, and circuit breakers for flaky dependencies.
Use references/performance_cost.md for checklists and tactics.
Bundled resources in this skill
scripts/: deterministic helpers (audits,llms.txtextraction,opensrcsnapshot runner)references/: playbooks + templates for docs research, patterns, migrations, safety, observability, and performanceassets/: copy/paste templates for Python + UI integrations (Next.js, FastAPI, Streamlit)
Start with references/docs_index.md and references/research_playbook.md.
interface:
display_name: "LangGraph Multiagent"
short_description: "Design LangGraph multi-agent systems"
default_prompt: "Use $langgraph-multiagent to plan or review a LangGraph multi-agent architecture."
policy:
allow_implicit_invocation: false
from __future__ import annotations
import asyncio
import json
import uuid
from dataclasses import asdict, is_dataclass
from typing import Any, AsyncIterator, Literal
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
# NOTE:
# - This template demonstrates in-process LangGraph/LangChain streaming behind FastAPI.
# - For most production UIs, prefer LangGraph Agent Server + useStream() (see references/ui_nextjs_rsc.md).
class ChatMessage(BaseModel):
role: str
content: str
class ChatStreamRequest(BaseModel):
thread_id: str | None = Field(default=None, description="Thread ID for persistence.")
messages: list[ChatMessage] | None = Field(
default=None, description="New messages to append (normal run)."
)
resume: dict[str, Any] | None = Field(
default=None,
description=(
"Resume payload for Command(resume=...). Must match the interrupt schema you received."
),
)
stream_modes: list[Literal["messages", "updates", "custom"]] = Field(
default_factory=lambda: ["messages", "updates"],
description="Which LangGraph stream modes to consume.",
)
def _jsonable(value: Any) -> Any:
if is_dataclass(value):
return asdict(value)
if isinstance(value, (str, int, float, bool)) or value is None:
return value
if isinstance(value, dict):
return {str(k): _jsonable(v) for k, v in value.items()}
if isinstance(value, list):
return [_jsonable(v) for v in value]
if isinstance(value, tuple):
return [_jsonable(v) for v in value]
return str(value)
def _sse(event: dict[str, Any], *, event_id: int | None = None, name: str | None = None) -> str:
lines: list[str] = []
if event_id is not None:
lines.append(f"id: {event_id}")
if name is not None:
lines.append(f"event: {name}")
lines.append(
"data: " + json.dumps(event, separators=(",", ":"), default=str) # type: ignore[arg-type]
)
lines.append("")
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# Agent wiring (replace with your real multi-agent runtime)
# ---------------------------------------------------------------------------
def build_agent() -> object:
"""
Replace this with your real agent/graph import.
For example:
from src.agent import agent
return agent
"""
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
@tool
def search(query: str) -> str:
return f"[search results] {query}"
model = init_chat_model("gpt-4o-mini", temperature=0)
return create_agent(
model=model,
tools=[search],
system_prompt="You are a helpful assistant. Use tools when useful.",
checkpointer=InMemorySaver(),
)
AGENT = build_agent()
app = FastAPI(title="LangGraph SSE (in-process)")
@app.post("/chat/stream")
async def chat_stream(req: ChatStreamRequest) -> StreamingResponse:
thread_id = req.thread_id or str(uuid.uuid4())
async def gen() -> AsyncIterator[str]:
event_id = 0
yield _sse({"type": "meta", "threadId": thread_id}, event_id=event_id, name="meta")
event_id += 1
config = {"configurable": {"thread_id": thread_id}}
stream_modes = req.stream_modes
# If resuming after HITL, pass a Command(resume=...) object into the agent.
if req.resume is not None:
from langgraph.types import Command
agent_input: Any = Command(resume=req.resume)
else:
if not req.messages:
yield _sse(
{"type": "error", "message": "messages is required when resume is not provided"},
event_id=event_id,
name="error",
)
return
agent_input = {
"messages": [m.model_dump() for m in req.messages],
}
try:
# Multi-mode streaming yields (mode, chunk) tuples.
async for mode, chunk in AGENT.astream( # type: ignore[attr-defined]
agent_input,
config=config,
stream_mode=stream_modes,
):
if mode == "messages":
token, metadata = chunk
content = getattr(token, "content", None)
if content:
meta_json = _jsonable(metadata)
yield _sse(
{
"type": "token",
"content": content,
"metadata": meta_json,
"node": meta_json.get("langgraph_node") if isinstance(meta_json, dict) else None,
},
event_id=event_id,
name="token",
)
event_id += 1
elif mode == "updates":
data = _jsonable(chunk)
yield _sse({"type": "update", "data": data}, event_id=event_id, name="update")
event_id += 1
# Interrupts often appear under a __interrupt__ key during updates.
if isinstance(data, dict) and "__interrupt__" in data:
yield _sse(
{"type": "interrupt", "interrupt": data["__interrupt__"]},
event_id=event_id,
name="interrupt",
)
event_id += 1
elif mode == "custom":
yield _sse(
{"type": "custom", "event": _jsonable(chunk)},
event_id=event_id,
name="custom",
)
event_id += 1
else:
yield _sse(
{"type": "custom", "event": {"mode": mode, "chunk": _jsonable(chunk)}},
event_id=event_id,
name="custom",
)
event_id += 1
await asyncio.sleep(0)
except Exception as e: # noqa: BLE001
yield _sse({"type": "error", "message": str(e)}, event_id=event_id, name="error")
return
yield _sse({"type": "done"}, event_id=event_id, name="done")
headers = {
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
return StreamingResponse(gen(), media_type="text/event-stream", headers=headers)
# Run with:
# uvicorn fastapi_sse_multiagent:app --reload --port 8000
{
"dependencies": ["."],
"graphs": {
"agent": "./src/agent.py:agent"
},
"env": ".env"
}
import { convertToModelMessages, streamText, type UIMessage } from "ai";
import { openai } from "@ai-sdk/openai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = (await req.json()) as { messages: UIMessage[] };
const result = streamText({
// Swap this for your provider/model.
model: openai("gpt-4o-mini"),
system:
"You are a helpful assistant. Use markdown for clarity when it helps.",
// v6: convertToModelMessages is async.
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
onError: (error) => {
if (error instanceof Error) return error.message;
return "Unknown error";
},
});
}
"use client";
import { useState } from "react";
import { DefaultChatTransport } from "ai";
import { useChat } from "@ai-sdk/react";
import Response from "./Response";
type Props = {
api?: string;
};
function safeJson(value: unknown): string {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
export default function ChatClient({ api = "/api/chat" }: Props) {
const { messages, sendMessage, status, error, stop, regenerate } = useChat({
transport: new DefaultChatTransport({ api }),
});
const [input, setInput] = useState("");
return (
<div className="mx-auto flex h-dvh max-w-4xl flex-col gap-4 p-4">
<header className="flex items-center justify-between gap-2">
<div className="flex flex-col">
<div className="text-sm font-medium">AI SDK Chat</div>
<div className="text-xs text-muted-foreground">
transport: <span className="font-mono">{api}</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm"
onClick={() => regenerate()}
disabled={!(status === "ready" || status === "error")}
>
Regenerate
</button>
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm"
onClick={() => stop()}
disabled={status !== "streaming"}
>
Stop
</button>
</div>
</header>
<main className="flex-1 overflow-auto rounded-lg border bg-background p-3">
<div className="flex flex-col gap-3">
{messages.map((message) => (
<div key={message.id} className="flex flex-col gap-2">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{message.role}
</div>
<div className="rounded-md border bg-background p-2">
{message.parts.map((part, idx) => {
switch (part.type) {
case "text":
return (
<Response
key={idx}
isAnimating={status === "streaming"}
>
{part.text}
</Response>
);
case "tool-invocation":
return (
<pre
key={idx}
className="mt-2 overflow-auto rounded bg-muted p-2 font-mono text-[11px]"
>
{safeJson(part)}
</pre>
);
case "tool-result":
return (
<pre
key={idx}
className="mt-2 overflow-auto rounded bg-muted p-2 font-mono text-[11px]"
>
{safeJson(part)}
</pre>
);
case "file":
return (
<pre
key={idx}
className="mt-2 overflow-auto rounded bg-muted p-2 font-mono text-[11px]"
>
{safeJson(part)}
</pre>
);
default:
return (
<pre
key={idx}
className="mt-2 overflow-auto rounded bg-muted p-2 font-mono text-[11px]"
>
{safeJson(part)}
</pre>
);
}
})}
</div>
</div>
))}
{status === "submitted" || status === "streaming" ? (
<div className="text-sm text-muted-foreground">Thinking…</div>
) : null}
{error ? (
<div className="rounded-md border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-500">
{error.message}
</div>
) : null}
</div>
</main>
<footer className="flex items-end gap-2">
<textarea
className="h-20 flex-1 resize-none rounded-md border bg-background p-2 text-sm"
placeholder="Type a message…"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
void sendMessage({ text: input });
setInput("");
}
}}
/>
<button
type="button"
className="rounded-md border bg-background px-3 py-2 text-sm"
disabled={status !== "ready"}
onClick={() => {
void sendMessage({ text: input });
setInput("");
}}
>
Send
</button>
</footer>
<div className="text-xs text-muted-foreground">
Tip: press <span className="font-mono">Ctrl+Enter</span> to send.
</div>
</div>
);
}
import ChatClient from "./ChatClient";
export default function Page() {
return <ChatClient api="/api/chat" />;
}
"use client";
import { memo, type ComponentProps } from "react";
import { Streamdown } from "streamdown";
type Props = ComponentProps<typeof Streamdown>;
function Response({ className, ...props }: Props) {
const base = "prose prose-sm dark:prose-invert max-w-none";
return (
<Streamdown
className={className ? `${base} ${className}` : base}
{...props}
/>
);
}
export default memo(Response);
"use client";
import { useEffect, useMemo, useState } from "react";
import { useStream } from "@langchain/langgraph-sdk/react";
import type { Message } from "@langchain/langgraph-sdk";
import type { AgentState, HITLRequest, HITLResponse } from "./types";
import { HITLRequestSchema } from "./types";
import Response from "./Response";
type Props = {
apiUrl: string;
assistantId: string;
};
type DecisionType = "approve" | "reject" | "edit";
type StreamRef = ReturnType<typeof useStream>;
const THREAD_ID_STORAGE_KEY = "langgraph:thread_id";
function safeStringify(value: unknown): string {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function renderContent(content: unknown): string {
if (typeof content === "string") return content;
if (content == null) return "";
return safeStringify(content);
}
export default function ChatClient({ apiUrl, assistantId }: Props) {
const [threadId, setThreadId] = useState<string | null>(null);
const [input, setInput] = useState("");
useEffect(() => {
setThreadId(window.localStorage.getItem(THREAD_ID_STORAGE_KEY));
}, []);
useEffect(() => {
if (!threadId) return;
window.localStorage.setItem(THREAD_ID_STORAGE_KEY, threadId);
}, [threadId]);
const stream = useStream<AgentState, { InterruptType: HITLRequest }>({
apiUrl,
assistantId,
threadId,
onThreadId: setThreadId,
reconnectOnMount: true,
});
const [hitlUiError, setHitlUiError] = useState<string | null>(null);
const [decisionModeByIndex, setDecisionModeByIndex] = useState<
Record<number, DecisionType>
>({});
const [editedArgsJsonByIndex, setEditedArgsJsonByIndex] = useState<
Record<number, string>
>({});
const [rejectReason, setRejectReason] = useState("User rejected");
const hitlRequest = useMemo(() => {
const candidate = stream.interrupt?.value;
const parsed = HITLRequestSchema.safeParse(candidate);
return parsed.success ? parsed.data : undefined;
}, [stream.interrupt]);
useEffect(() => {
if (!hitlRequest) return;
const nextDecisionModes: Record<number, DecisionType> = {};
const nextArgs: Record<number, string> = {};
hitlRequest.actionRequests.forEach((action, idx) => {
nextDecisionModes[idx] = "approve";
nextArgs[idx] = safeStringify(action.args ?? action.arguments ?? {});
});
setDecisionModeByIndex(nextDecisionModes);
setEditedArgsJsonByIndex(nextArgs);
setHitlUiError(null);
}, [hitlRequest]);
const handleSubmit = async () => {
const text = input.trim();
if (!text) return;
setInput("");
await stream.submit({
messages: [{ type: "human", content: text }],
});
};
const handleNewThread = () => {
window.localStorage.removeItem(THREAD_ID_STORAGE_KEY);
setThreadId(null);
stream.stop();
};
const handleResumeFromInterrupt = async () => {
if (!hitlRequest) return;
setHitlUiError(null);
const decisions: HITLResponse["decisions"] = hitlRequest.actionRequests.map(
(action, idx) => {
const mode = decisionModeByIndex[idx] ?? "approve";
if (mode === "approve") return { type: "approve" };
if (mode === "reject") return { type: "reject", message: rejectReason };
const raw = editedArgsJsonByIndex[idx] ?? "{}";
try {
const args = JSON.parse(raw) as Record<string, unknown>;
const editedAction =
action.arguments !== undefined
? { name: action.name, arguments: args }
: { name: action.name, args };
return {
type: "edit",
editedAction,
};
} catch {
return { type: "reject", message: "Invalid edited JSON args" };
}
}
);
const resume: HITLResponse = { decisions };
await stream.submit(null, {
command: { resume },
});
};
return (
<div className="mx-auto flex h-dvh max-w-4xl flex-col gap-4 p-4">
<header className="flex items-center justify-between gap-2">
<div className="flex flex-col">
<div className="text-sm font-medium">LangGraph Chat</div>
<div className="text-xs text-muted-foreground">
assistant: <span className="font-mono">{assistantId}</span> · thread:{" "}
<span className="font-mono">{threadId ?? "—"}</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm"
onClick={handleNewThread}
>
New thread
</button>
</div>
</header>
<main className="flex-1 overflow-auto rounded-lg border bg-background p-3">
<div className="flex flex-col gap-3">
{stream.messages.map((message, idx) => (
<MessageRow
key={(message.id ?? idx) as string}
message={message as Message}
stream={stream}
/>
))}
{stream.isLoading ? (
<div className="text-sm text-muted-foreground">Thinking…</div>
) : null}
{stream.error ? (
<div className="rounded-md border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-500">
{stream.error.message}
</div>
) : null}
{hitlRequest && hitlRequest.actionRequests.length > 0 ? (
<section className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-3">
<div className="mb-2 text-sm font-semibold text-amber-600">
Human approval required
</div>
<div className="mb-3 text-xs text-muted-foreground">
Decide for each action, then resume the run.
</div>
<div className="flex flex-col gap-3">
{hitlRequest.actionRequests.map((action, idx) => (
<div
key={idx}
className="rounded-md border bg-background p-3"
>
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-sm font-mono">{action.name}</div>
<select
className="rounded-md border bg-background px-2 py-1 text-xs"
value={decisionModeByIndex[idx] ?? "approve"}
onChange={(e) =>
setDecisionModeByIndex((prev) => ({
...prev,
[idx]: e.target.value as DecisionType,
}))
}
>
<option value="approve">approve</option>
<option value="reject">reject</option>
<option value="edit">edit args</option>
</select>
</div>
<div className="mb-2 text-xs text-muted-foreground">
{action.description ?? "Tool call pending review."}
</div>
{(decisionModeByIndex[idx] ?? "approve") === "edit" ? (
<textarea
className="h-28 w-full rounded-md border bg-background p-2 font-mono text-xs"
value={editedArgsJsonByIndex[idx] ?? "{}"}
onChange={(e) =>
setEditedArgsJsonByIndex((prev) => ({
...prev,
[idx]: e.target.value,
}))
}
/>
) : (
<pre className="overflow-auto rounded-md border bg-muted p-2 text-xs">
{safeStringify(action.args ?? action.arguments ?? {})}
</pre>
)}
</div>
))}
</div>
<div className="mt-3 flex flex-col gap-2">
<div className="flex items-center gap-2">
<input
className="w-full rounded-md border bg-background px-2 py-1 text-xs"
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="Reject reason (used for any reject decisions)"
/>
<button
type="button"
className="rounded-md border bg-background px-3 py-1.5 text-xs"
onClick={handleResumeFromInterrupt}
>
Resume
</button>
</div>
{hitlUiError ? (
<div className="text-xs text-red-500">{hitlUiError}</div>
) : null}
</div>
</section>
) : null}
</div>
</main>
<footer className="flex items-end gap-2">
<textarea
className="h-20 flex-1 resize-none rounded-md border bg-background p-2 text-sm"
placeholder="Type a message…"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
handleSubmit();
}
}}
/>
<button
type="button"
className="rounded-md border bg-background px-3 py-2 text-sm"
disabled={stream.isLoading}
onClick={handleSubmit}
>
Send
</button>
</footer>
<div className="text-xs text-muted-foreground">
Tip: press <span className="font-mono">Ctrl+Enter</span> to send.
</div>
</div>
);
}
function MessageRow({
message,
stream,
}: {
message: Message;
stream: StreamRef;
}) {
const meta = stream.getMessageMetadata(message);
const node = (meta as { langgraph_node?: string } | undefined)?.langgraph_node;
const role = message.type;
const toolCalls =
role === "ai" ? (stream.getToolCalls(message) as unknown[]) : [];
const content = renderContent(message.content);
return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{role}
{node ? <span className="ml-2 font-mono lowercase">{node}</span> : null}
</div>
</div>
{toolCalls.length > 0 ? (
<div className="flex flex-col gap-2">
{toolCalls.map((tc, idx) => (
<ToolCallCard key={idx} toolCall={tc} />
))}
</div>
) : null}
{role === "ai" && typeof message.content === "string" ? (
<div className="rounded-md border bg-background p-2">
<Response isAnimating={stream.isLoading}>{content}</Response>
</div>
) : (
<pre className="whitespace-pre-wrap rounded-md border bg-muted p-2 text-sm">
{content}
</pre>
)}
</div>
);
}
function ToolCallCard({ toolCall }: { toolCall: unknown }) {
const tc = toolCall as {
id?: string;
state?: string;
call?: { name?: string; args?: unknown };
result?: unknown;
error?: unknown;
};
return (
<div className="rounded-md border bg-background p-2 text-xs">
<div className="flex items-center justify-between gap-2">
<div className="font-mono">
{tc.call?.name ?? "tool_call"}
{tc.id ? <span className="text-muted-foreground"> · {tc.id}</span> : null}
</div>
{tc.state ? (
<div className="rounded bg-muted px-2 py-0.5 font-mono text-[10px]">
{tc.state}
</div>
) : null}
</div>
<pre className="mt-2 overflow-auto rounded bg-muted p-2 font-mono text-[11px]">
{safeStringify(tc.call?.args ?? {})}
</pre>
{tc.result != null ? (
<pre className="mt-2 overflow-auto rounded bg-muted p-2 font-mono text-[11px]">
{safeStringify(tc.result)}
</pre>
) : null}
{tc.error != null ? (
<pre className="mt-2 overflow-auto rounded bg-red-500/10 p-2 font-mono text-[11px] text-red-500">
{safeStringify(tc.error)}
</pre>
) : null}
</div>
);
}
import ChatClient from "./ChatClient";
export default function Page() {
const apiUrl =
process.env.NEXT_PUBLIC_LANGGRAPH_API_URL ?? "http://localhost:2024";
const assistantId = process.env.NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID ?? "agent";
return <ChatClient apiUrl={apiUrl} assistantId={assistantId} />;
}
"use client";
import { memo, type ComponentProps } from "react";
import { Streamdown } from "streamdown";
type Props = ComponentProps<typeof Streamdown>;
function Response({ className, ...props }: Props) {
const base = "prose prose-sm dark:prose-invert max-w-none";
return (
<Streamdown
className={className ? `${base} ${className}` : base}
{...props}
/>
);
}
export default memo(Response);
import { z } from "zod";
import type { Message } from "@langchain/langgraph-sdk";
// ---------------------------------------------------------------------------
// Tool-call typing (fill this in per app)
// ---------------------------------------------------------------------------
export const ToolCallSchema = z.discriminatedUnion("name", [
z.object({
name: z.literal("search"),
args: z.object({ query: z.string() }),
id: z.string().optional(),
}),
z.object({
name: z.literal("calculate"),
args: z.object({ expression: z.string() }),
id: z.string().optional(),
}),
]);
export type ToolCall = z.infer<typeof ToolCallSchema>;
export type AgentMessage = Message<ToolCall>;
export interface AgentState {
// Embed tool-call types in messages for end-to-end type safety in useStream().
messages: AgentMessage[];
// Optional: keep custom UI messages in graph state for Generative UI.
ui?: unknown[];
}
// ---------------------------------------------------------------------------
// HITL typing (runtime value is versioned; validate defensively)
// ---------------------------------------------------------------------------
export const HITLActionRequestSchema = z.object({
name: z.string(),
// JS docs typically use `args`, but some examples show `arguments`.
args: z.record(z.unknown()).optional(),
arguments: z.record(z.unknown()).optional(),
description: z.string().optional(),
});
export const HITLRequestSchema = z.preprocess(
(value) => {
if (value == null || typeof value !== "object") return value;
const obj = value as Record<string, unknown>;
return {
actionRequests: (obj.actionRequests ?? obj.action_requests) as unknown,
reviewConfigs: (obj.reviewConfigs ?? obj.review_configs) as unknown,
};
},
z.object({
actionRequests: z.array(HITLActionRequestSchema).default([]),
reviewConfigs: z.array(z.unknown()).optional(),
})
);
export type HITLRequest = z.infer<typeof HITLRequestSchema>;
const HITLEditedActionSchema = z.object({
name: z.string(),
args: z.record(z.unknown()).optional(),
arguments: z.record(z.unknown()).optional(),
});
export const HITLDecisionSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("approve") }),
z.object({ type: z.literal("reject"), message: z.string().optional() }),
z.object({
type: z.literal("edit"),
editedAction: HITLEditedActionSchema,
}),
]);
export const HITLResponseSchema = z.object({
decisions: z.array(HITLDecisionSchema),
});
export type HITLResponse = z.infer<typeof HITLResponseSchema>;
from __future__ import annotations
import urllib.request
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
ALLOWED_PREFIXES = (
"https://langchain-ai.github.io/langgraph/",
"https://docs.langchain.com/oss/python/",
)
@tool
def fetch_documentation(url: str) -> str:
"""Fetch documentation from an allowlisted URL (SSRF-safe by prefix allowlist)."""
if not any(url.startswith(p) for p in ALLOWED_PREFIXES):
return f"Error: URL not allowed. Must start with one of: {', '.join(ALLOWED_PREFIXES)}"
req = urllib.request.Request(url, headers={"User-Agent": "langgraph-multiagent-skill/1.0"})
with urllib.request.urlopen(req, timeout=20) as resp:
return resp.read().decode("utf-8", errors="replace")
def build_docs_agent() -> object:
model = init_chat_model("gpt-4o-mini", temperature=0)
system_prompt = (
"You are a docs-grounded assistant for LangGraph/LangChain.\n"
"If the question involves API details, you MUST call fetch_documentation\n"
"on the relevant official docs URL before answering."
)
return create_agent(
model=model,
tools=[fetch_documentation],
system_prompt=system_prompt,
)
from __future__ import annotations
from typing import Literal
from langchain.agents import AgentState, create_agent
from langchain.messages import AIMessage, ToolMessage
from langchain.tools import tool, ToolRuntime
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command
from typing_extensions import NotRequired
class MultiAgentState(AgentState):
active_agent: NotRequired[str]
@tool
def transfer_to_sales(runtime: ToolRuntime) -> Command:
"""Transfer to the sales agent."""
last_ai_message = next(msg for msg in reversed(runtime.state["messages"]) if isinstance(msg, AIMessage))
transfer_message = ToolMessage(
content="Transferred to sales agent from support agent",
tool_call_id=runtime.tool_call_id,
)
return Command(
goto="sales_agent",
update={"active_agent": "sales_agent", "messages": [last_ai_message, transfer_message]},
graph=Command.PARENT,
)
@tool
def transfer_to_support(runtime: ToolRuntime) -> Command:
"""Transfer to the support agent."""
last_ai_message = next(msg for msg in reversed(runtime.state["messages"]) if isinstance(msg, AIMessage))
transfer_message = ToolMessage(
content="Transferred to support agent from sales agent",
tool_call_id=runtime.tool_call_id,
)
return Command(
goto="support_agent",
update={"active_agent": "support_agent", "messages": [last_ai_message, transfer_message]},
graph=Command.PARENT,
)
sales_agent = create_agent(
model="anthropic:claude-sonnet-4-20250514",
tools=[transfer_to_support],
system_prompt="You are a sales agent. If asked about support, transfer to support.",
)
support_agent = create_agent(
model="anthropic:claude-sonnet-4-20250514",
tools=[transfer_to_sales],
system_prompt="You are a support agent. If asked about sales, transfer to sales.",
)
def call_sales(state: MultiAgentState) -> Command:
return sales_agent.invoke(state)
def call_support(state: MultiAgentState) -> Command:
return support_agent.invoke(state)
def route_after_agent(state: MultiAgentState) -> Literal["sales_agent", "support_agent", END]:
messages = state.get("messages", [])
if messages:
last = messages[-1]
if isinstance(last, AIMessage) and not last.tool_calls:
return END
return state.get("active_agent") or "sales_agent"
def route_initial(state: MultiAgentState) -> Literal["sales_agent", "support_agent"]:
return state.get("active_agent") or "sales_agent"
def build_graph() -> object:
builder = StateGraph(MultiAgentState)
builder.add_node("sales_agent", call_sales)
builder.add_node("support_agent", call_support)
builder.add_conditional_edges(START, route_initial, ["sales_agent", "support_agent"])
builder.add_conditional_edges("sales_agent", route_after_agent, ["sales_agent", "support_agent", END])
builder.add_conditional_edges("support_agent", route_after_agent, ["sales_agent", "support_agent", END])
return builder.compile()
from __future__ import annotations
from typing import Literal, TypedDict
from langchain.chat_models import init_chat_model
from langgraph.graph import END, START, MessagesState, StateGraph
class State(MessagesState):
# Keep additional aggregation state here (lists/dicts) and ensure reducers if parallelizing.
pass
def build_graph() -> object:
model = init_chat_model("gpt-4o-mini", temperature=0)
def plan(state: State) -> dict:
# Replace with planning logic; keep it small and structured.
response = model.invoke(state["messages"])
return {"messages": [response]}
def should_continue(state: State) -> Literal[END, "plan"]:
last = state["messages"][-1]
return END if not getattr(last, "tool_calls", None) else "plan"
builder = StateGraph(State)
builder.add_node("plan", plan)
builder.add_edge(START, "plan")
builder.add_conditional_edges("plan", should_continue)
return builder.compile()
from __future__ import annotations
from typing import TypedDict
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
class Output(TypedDict):
text: str
@tool
def calendar_tool(request: str) -> str:
"""Placeholder tool: replace with a real calendar integration."""
return f"[calendar] {request}"
@tool
def email_tool(request: str) -> str:
"""Placeholder tool: replace with a real email integration."""
return f"[email] {request}"
def build_supervisor_agent() -> object:
model = init_chat_model("gpt-4o-mini", temperature=0)
calendar_agent = create_agent(
model,
tools=[calendar_tool],
system_prompt="You are a calendar assistant. Be precise.",
)
email_agent = create_agent(
model,
tools=[email_tool],
system_prompt="You are an email assistant. Be concise and professional.",
)
@tool
def schedule_event(request: str) -> str:
result = calendar_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].text
@tool
def manage_email(request: str) -> str:
result = email_agent.invoke({"messages": [{"role": "user", "content": request}]})
return result["messages"][-1].text
supervisor = create_agent(
model,
tools=[schedule_event, manage_email],
system_prompt=(
"You are a supervisor. Delegate work to tools. "
"Use multiple tools in sequence when needed."
),
checkpointer=InMemorySaver(),
)
return supervisor
from __future__ import annotations
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware, PIIMiddleware, SummarizationMiddleware
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
@tool
def read_data(query: str) -> str:
return f"[data] {query}"
@tool
def send_email(to: str, subject: str, body: str) -> str:
return f"Sent email to {to} with subject {subject}"
def build_agent() -> object:
model = init_chat_model("gpt-4o-mini", temperature=0)
agent = create_agent(
model=model,
tools=[read_data, send_email],
system_prompt="You are a helpful assistant. Use tools when appropriate.",
middleware=[
# Redact emails in user input before sending to model
PIIMiddleware("email", strategy="redact", apply_to_input=True),
# Keep long conversations bounded
SummarizationMiddleware(model=model, trigger={"tokens": 1200}),
# Require approval for side effects
HumanInTheLoopMiddleware(interrupt_on={"send_email": True}),
],
# Required for interrupts / HITL
checkpointer=InMemorySaver(),
)
return agent
from __future__ import annotations
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.messages import ToolMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
@dataclass(frozen=True)
class Context:
user_id: str
api_key: str
async def require_authentication(request: MCPToolCallRequest, handler):
runtime = request.runtime
if not runtime.state.get("authenticated", False) and request.name in {"delete_file", "export_data"}:
return ToolMessage(
content="Authentication required.",
tool_call_id=runtime.tool_call_id,
)
return await handler(request)
async def inject_user_context(request: MCPToolCallRequest, handler):
runtime = request.runtime
modified = request.override(args={**request.args, "user_id": runtime.context.user_id})
return await handler(modified)
async def build_agent() -> object:
client = MultiServerMCPClient(
{
"internal": {"url": "http://localhost:8000/mcp"},
},
tool_interceptors=[require_authentication, inject_user_context],
)
tools = await client.get_tools()
return create_agent(
model="gpt-4o-mini",
tools=tools,
context_schema=Context,
)
from __future__ import annotations
from dataclasses import dataclass
from langchain.tools import ToolRuntime, tool
@dataclass(frozen=True)
class RuntimeContext:
user_id: str
@tool
def example_tool(runtime: ToolRuntime[RuntimeContext]) -> str:
# Use runtime.context for DI (user IDs, db clients, etc).
return f"hello {runtime.context.user_id}"
from __future__ import annotations
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import START, StateGraph
class State(TypedDict):
# Append-only list reducer allows parallel nodes to safely write updates.
completed_sections: Annotated[list[str], operator.add]
def worker_a(state: State) -> dict:
return {"completed_sections": ["a"]}
def worker_b(state: State) -> dict:
return {"completed_sections": ["b"]}
def build_graph() -> object:
g = StateGraph(State)
g.add_node("a", worker_a)
g.add_node("b", worker_b)
g.add_edge(START, "a")
g.add_edge(START, "b")
return g.compile()
from __future__ import annotations
from graph_supervisor_subagents import build_supervisor_agent
def test_supervisor_smoke() -> None:
agent = build_supervisor_agent()
out = agent.invoke({"messages": [{"role": "user", "content": "Draft an email to Bob about lunch."}]})
assert out["messages"]
from __future__ import annotations
import json
import os
import uuid
from dataclasses import asdict, is_dataclass
from typing import Any
import streamlit as st
def _jsonable(value: Any) -> Any:
if is_dataclass(value):
return asdict(value)
if isinstance(value, (str, int, float, bool)) or value is None:
return value
if isinstance(value, dict):
return {str(k): _jsonable(v) for k, v in value.items()}
if isinstance(value, list):
return [_jsonable(v) for v in value]
if isinstance(value, tuple):
return [_jsonable(v) for v in value]
return str(value)
@st.cache_resource
def build_agent() -> object:
"""
Replace with your real multi-agent system.
For example:
from src.agent import agent
return agent
"""
if os.environ.get("LANGGRAPH_UI_TEST_MODE") == "1":
class _Token:
def __init__(self, content: str) -> None:
self.content = content
class _StubAgent:
def stream(self, agent_input: Any, *_args: Any, **_kwargs: Any): # type: ignore[no-untyped-def]
user_text = ""
if isinstance(agent_input, dict):
messages = agent_input.get("messages") or []
if messages:
last = messages[-1]
if isinstance(last, dict):
user_text = str(last.get("content") or "")
response = f"Stub response (set LANGGRAPH_UI_TEST_MODE=0 for real LLM): {user_text}"
for token in response.split(" "):
yield ("messages", (_Token(token + " "), {}))
return _StubAgent()
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
@tool
def search(query: str) -> str:
return f"[search results] {query}"
model = init_chat_model("gpt-4o-mini", temperature=0)
return create_agent(
model=model,
tools=[search],
system_prompt="You are a helpful assistant. Use tools when useful.",
middleware=[
# Example: require approvals for a specific tool (update to match your tools).
HumanInTheLoopMiddleware(
interrupt_on={"search": True},
description_prefix="Tool execution pending approval",
),
],
checkpointer=InMemorySaver(),
)
AGENT = build_agent()
st.set_page_config(page_title="LangGraph Multi-Agent (Streamlit)", layout="wide")
def init_state() -> None:
st.session_state.setdefault("thread_id", str(uuid.uuid4()))
st.session_state.setdefault("messages", [])
st.session_state.setdefault("pending_interrupts", None)
st.session_state.setdefault("reject_reason", "User rejected")
def reset_thread() -> None:
st.session_state.thread_id = str(uuid.uuid4())
st.session_state.messages = []
st.session_state.pending_interrupts = None
def extract_action_requests(interrupt_value: Any) -> list[dict[str, Any]]:
if isinstance(interrupt_value, dict):
if "action_requests" in interrupt_value:
return interrupt_value.get("action_requests") or []
if "actionRequests" in interrupt_value:
return interrupt_value.get("actionRequests") or []
return []
def stream_run(agent_input: Any) -> None:
config = {"configurable": {"thread_id": st.session_state.thread_id}}
assistant_container = st.chat_message("assistant")
text_placeholder = assistant_container.empty()
token_buffer = ""
interrupts: Any = None
for mode, chunk in AGENT.stream( # type: ignore[attr-defined]
agent_input,
config=config,
stream_mode=["messages", "updates"],
):
if mode == "messages":
token, _meta = chunk
content = getattr(token, "content", None)
if content:
token_buffer += content
text_placeholder.markdown(token_buffer)
elif mode == "updates":
data = _jsonable(chunk)
if isinstance(data, dict) and "__interrupt__" in data:
interrupts = data["__interrupt__"]
if token_buffer.strip():
st.session_state.messages.append({"role": "assistant", "content": token_buffer})
st.session_state.pending_interrupts = interrupts
def resume_from_interrupts(decisions: dict[str, Any]) -> None:
from langgraph.types import Command
stream_run(Command(resume=decisions))
init_state()
st.sidebar.header("Session")
st.sidebar.caption(f"Streamlit {st.__version__}")
st.sidebar.caption("Thread IDs are required for persistence, interrupts, and resume.")
st.sidebar.code(st.session_state.thread_id, language="text")
if st.sidebar.button("New thread", use_container_width=True):
reset_thread()
st.rerun()
st.title("LangGraph Multi-Agent (Streamlit)")
st.caption("Streams tokens + handles interrupts (HITL) using thread-scoped persistence.")
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
user_text = st.chat_input("Ask something…")
if user_text:
st.session_state.messages.append({"role": "user", "content": user_text})
with st.chat_message("user"):
st.markdown(user_text)
stream_run({"messages": [{"role": "user", "content": user_text}]})
pending = st.session_state.pending_interrupts
if pending:
st.divider()
st.subheader("Human approval required")
# In Python, interrupts are commonly a list of Interrupt objects with {id, value}.
pending_json = _jsonable(pending)
st.caption("Review each tool call. Decisions must match the action order.")
reject_reason = st.text_input(
"Reject reason (used for any reject decisions)",
value=st.session_state.reject_reason,
key="hitl_reject_reason",
)
st.session_state.reject_reason = reject_reason
interrupts_list = pending_json if isinstance(pending_json, list) else [pending_json]
for item in interrupts_list:
interrupt_id = (item.get("id") if isinstance(item, dict) else None) or str(uuid.uuid4())
value = item.get("value") if isinstance(item, dict) else item
action_requests = extract_action_requests(value)
st.markdown(f"**Interrupt** `{interrupt_id}`")
st.code(json.dumps(_jsonable(value), indent=2), language="json")
for idx, action in enumerate(action_requests):
name = action.get("name") if isinstance(action, dict) else "tool_call"
args = {}
if isinstance(action, dict):
args = action.get("args") or action.get("arguments") or {}
st.markdown(f"**{name}**")
decision_key = f"hitl_decision::{interrupt_id}::{idx}"
mode = st.selectbox(
"Decision",
options=["approve", "reject", "edit"],
index=0,
key=decision_key,
)
if mode == "edit":
st.text_area(
"Edited args (JSON)",
value=json.dumps(args, indent=2),
key=f"hitl_edit_args::{interrupt_id}::{idx}",
height=120,
)
else:
st.code(json.dumps(args, indent=2), language="json")
st.divider()
if st.button("Resume", type="primary"):
resume: dict[str, Any] = {}
errors: list[str] = []
for item in interrupts_list:
interrupt_id = (item.get("id") if isinstance(item, dict) else None) or str(uuid.uuid4())
value = item.get("value") if isinstance(item, dict) else item
action_requests = extract_action_requests(value)
decisions: list[dict[str, Any]] = []
for idx, action in enumerate(action_requests):
name = action.get("name") if isinstance(action, dict) else "tool_call"
mode = st.session_state.get(f"hitl_decision::{interrupt_id}::{idx}", "approve")
if mode == "approve":
decisions.append({"type": "approve"})
elif mode == "reject":
decisions.append({"type": "reject", "message": st.session_state.reject_reason})
else:
raw = st.session_state.get(f"hitl_edit_args::{interrupt_id}::{idx}", "{}")
try:
edited_args = json.loads(raw)
except Exception:
errors.append(f"Invalid JSON for edited args: interrupt={interrupt_id} action={idx}")
edited_args = {}
decisions.append(
{
"type": "edit",
# Python uses snake_case for decision payload keys.
"edited_action": {"name": name, "args": edited_args},
}
)
resume[interrupt_id] = {"decisions": decisions}
if errors:
st.error("\n".join(errors))
else:
st.session_state.pending_interrupts = None
resume_from_interrupts(resume)
st.rerun()
import os
from streamlit.testing.v1 import AppTest
def test_streamlit_app_loads_smoke() -> None:
# Keep tests offline; the template supports a stub runtime in test mode.
os.environ["LANGGRAPH_UI_TEST_MODE"] = "1"
# NOTE: Adjust the path if you rename/move the Streamlit entrypoint.
AppTest.from_file("streamlit_chat_app.py").run()
Python API map (LangChain v1 + LangGraph v1+)
This is a cheat sheet; always verify with docs for your pinned versions.
Agents + middleware
from langchain.agents import create_agentfrom langchain.agents import AgentState(for typed state extension; confirm export in your version)from langchain.agents.middleware import HumanInTheLoopMiddleware, PIIMiddleware, SummarizationMiddlewarefrom langchain.agents.middleware import before_model, after_model, wrap_model_call, wrap_tool_call, dynamic_prompt
Tools + runtime access
from langchain.tools import tool, ToolRuntimefrom langchain.messages import ToolMessage, AIMessage(handoffs and tool error shaping)
LangGraph primitives
from langgraph.graph import StateGraph, START, END, MessagesStatefrom langgraph.types import Command, Send, interruptfrom langgraph.checkpoint.memory import InMemorySaver(dev only; use persistent checkpointer in prod)from langgraph.store.memory import InMemoryStore(dev; use DB-backed store in prod)
Common patterns to look up in docs
- reducers:
add_messages,operator.add, per-field reducer annotations - time travel: state history + checkpoint IDs
- persistence: thread_id config + checkpointer backends
- multi-agent handoffs:
Command.PARENTand message pairing rules
Audit + migration methodology (architect-grade)
Use this to perform deep reviews and produce actionable migration plans.
Phase 1: Inventory
1. Identify entrypoints:
- agent constructors, graph builders, tool registries, UI bindings.
2. Capture dependencies and versions. 3. Run automated scan:
python scripts/audit_repo_agents.py --root . --out agent_audit_report.md --json agent_audit.json
Phase 2: Classify architecture
For each agent workflow, classify:
- topology: single-agent / supervisor-subagents / handoffs / orchestrator-worker
- memory: none / thread checkpointer / store long-term memory
- safety: guardrails / HITL / permissions
- observability: tracing + metrics + evaluation
Phase 3: Define target architecture (keep it minimal)
Pick the simplest that meets requirements:
1) single create_agent + middleware (best default) 2) supervisor + subagents (tool-calling) 3) graph-native orchestration (LangGraph StateGraph)
Phase 4: Migration plan (ship in slices)
Generate a draft plan automatically:
python scripts/generate_migration_plan.py --audit-json agent_audit.json --out migration_plan.md
Then refine:
- add concrete file-level tasks
- add tests for each migration slice
- add rollout plan and monitoring
Phase 5: Execute + harden
- migrate wiring with minimal behavior changes
- add middleware guardrails + HITL for side effects
- add evaluation/regression suite for drift
Deployment: LangGraph CLI / Agent Server (langgraph.json)
Use this when packaging a LangGraph/LangChain agent as a deployable service with durable execution.
Minimal application structure
A deployable app typically includes:
- one or more compiled graphs (LangChain
create_agentreturns a compiled graph) langgraph.json(configuration)- dependency spec (
pyproject.tomlorpackage.json) - optional
.env
Minimal langgraph.json (Python)
The smallest useful config maps graph IDs to Python module objects:
{
"dependencies": ["."],
"graphs": {
"agent": "./src/agent.py:agent"
},
"env": ".env"
}Keep graphs stable: treat graph IDs as API contracts.
HTTP headers → runtime config (multi-tenant safety)
Agent Server can map selected HTTP headers into the runtime config (for user/org IDs, feature flags, budgets). For safety, explicitly allowlist which headers are passed through:
{
"http": {
"configurable_headers": {
"includes": ["x-user-id", "x-organization-id", "my-prefix-*"],
"excludes": ["authorization", "x-api-key"]
}
}
}Related: you can also opt-in to logging specific headers for correlation/debugging (keep PII redacted by default).
Streaming + resumability (runs/threads)
- Agent Server streams outputs via SSE for run endpoints.
- If a run is created with
stream_resumable=true, clients can reconnect usingLast-Event-IDto resume from the last seen event ID (see the Agent Server APIJoin Run Streamendpoint). - For UX robustness (page refresh, flaky networks), prefer resumable streaming and store
run_idwhile a run is active.
Operational checklist
- Add auth + rate limiting at the service boundary.
- Use persistent checkpointing in production (not in-memory).
- Configure store/semantic search carefully for multi-tenant isolation.
- Redact logs/spans by default (PII).
- Configure cancellation behavior (
cancel_on_disconnect) for long streams when appropriate. - Review Agent Server scale guidance if you expect high read/write or many concurrent runs.
Where to look next
- LangGraph “application structure” + config reference (schema evolves; always consult latest docs).
- MCP endpoint support and auth middleware if exposing tools/agents via MCP.
Deployment notes (LangGraph/LangChain)
Deployment options (typical)
- Local dev: run graphs directly in-process.
- Service deployment: expose graphs behind an API service; add auth, rate limits, and auditing.
- Durable execution: enable checkpointing/persistence and thread IDs.
What to decide early
- Where checkpoints live (in-memory vs DB-backed).
- Multi-tenant boundaries (namespace design for long-term memory stores).
- Secret management for tools (never ship secrets to the model; inject at runtime context).
“Don’t ship without”
- tests around core tool calls
- traceability (run IDs, thread IDs)
- safe timeouts + retries
- least-privilege tool credentials
Doc crawl targets (seeds)
This file exists to make “crawl everything relevant” repeatable without guessing where to start.
LangGraph sitemap
https://langchain-ai.github.io/langgraph/llms.txt- (optional, larger)
https://langchain-ai.github.io/langgraph/llms-full.txt
LangChain OSS Python (high-signal entrypoints)
https://docs.langchain.com/oss/python/langchain/multi-agenthttps://docs.langchain.com/oss/python/langchain/multi-agent/subagents-personal-assistanthttps://docs.langchain.com/oss/python/langchain/guardrailshttps://docs.langchain.com/oss/python/langchain/runtimehttps://docs.langchain.com/oss/python/langchain/context-engineeringhttps://docs.langchain.com/oss/python/langchain/mcphttps://docs.langchain.com/oss/python/langchain/human-in-the-loophttps://docs.langchain.com/oss/python/langchain/retrievalhttps://docs.langchain.com/oss/python/langchain/long-term-memory
LangChain OSS JavaScript (UI + streaming)
https://docs.langchain.com/oss/javascript/langchain/streaming/frontendhttps://docs.langchain.com/oss/javascript/langchain/uihttps://docs.langchain.com/oss/javascript/langchain/human-in-the-loop
LangGraph OSS Python (high-signal entrypoints)
https://docs.langchain.com/oss/python/langgraph/agentic-raghttps://docs.langchain.com/oss/python/langgraph/sql-agenthttps://docs.langchain.com/oss/python/langgraph/workflows-agentshttps://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph
LangSmith / Agent Server (UI + API)
https://docs.langchain.com/langsmith/agent-serverhttps://docs.langchain.com/langsmith/server-api-refhttps://docs.langchain.com/langsmith/configurable-headershttps://docs.langchain.com/langsmith/configurable-logshttps://docs.langchain.com/langsmith/agent-server-scalehttps://docs.langchain.com/langsmith/generative-ui-reacthttps://docs.langchain.com/langsmith/agent-server-api/thread-runs/create-run-stream-outputhttps://docs.langchain.com/langsmith/agent-server-api/thread-runs/join-run-streamhttps://docs.langchain.com/langsmith/agent-server-api/threads/join-thread-stream
How to crawl (bounded)
Use the deterministic crawler:
python scripts/crawl_docs.py --llms-txt https://langchain-ai.github.io/langgraph/llms.txt --allow-prefixes https://langchain-ai.github.io/langgraph/ --max-pages 500 --out-dir docs_cache_langgraph(from the skill folder)- For LangChain pages, pass as explicit seeds and constrain prefixes:
python scripts/crawl_docs.py --seeds https://docs.langchain.com/oss/python/langchain/multi-agent https://docs.langchain.com/oss/python/langchain/guardrails --allow-prefixes https://docs.langchain.com/oss/python/langchain/ --max-pages 300 --out-dir docs_cache_langchain(from the skill folder)
If you need richer extraction (markdown/plaintext), prefer MCP tools (langchain-docs.SearchDocsByLangChain + Exa crawling) over raw HTML crawling.
Docs index (always fetch the latest)
This skill is intentionally docs-driven: do not rely on memory for APIs. Use langchain-docs.SearchDocsByLangChain first, then Context7 for API-level details.
Primary doc entrypoints (Python)
- Multi-agent overview: search
langchain multi-agent(LangChain OSS Python). - Subagents / supervisor pattern tutorial (migration target for supervisor libs): search
subagents-personal-assistant. - Handoffs (state-driven routing): search
handoffsandCommand.PARENT. - Guardrails: search
langchain guardrails(middleware, PII/prompt-injection checks, HITL). - Middleware (core of create_agent): search
decorator-based middlewareandcustom middleware. - Context engineering: search
context overview(runtime context, state, store). - MCP integration: search
langchain mcp(adapters, ToolRuntime context). - Human-in-the-loop: search
human-in-the-loopandinterrupt. - Retrieval + agentic RAG: search
langgraph agentic ragandlangchain retrieval. - Long-term memory: search
langchain long-term memory(stores, namespaces, search). - LangGraph Graph API + reducers: search
MessagesStateandProcess state updates with reducers. - LangGraph patterns: search
thinking in langgraphandworkflows and agentsandorchestrator-worker. - LangGraph persistence: search
persistenceandtime travel. - LangGraph app config/deployment: search
langgraph.jsonandLangGraph CLIandapplication structure. - Releases & migrations:
- search
langgraph v1(what’s new) - search
langchain v1(what’s new) - search
migrate langgraph v1(e.g.,create_react_agent → create_agent) - search
migrate langchain v1(migration table + behavior changes)
UI + frontend streaming (JS/TS + Next.js)
When integrating LangGraph/LangChain into Next.js/React UIs, prioritize these docs:
useStreamhook + frontend streaming guide:- search
streaming frontend useStream - search
useStream return values(interrupts, toolCalls, branching, resume) - Thread management patterns:
- search
Thread managementandOptimistic thread creation - search
Resume after page refresh reconnectOnMount - Tool call rendering:
- search
Rendering tool calls getToolCalls - Generative UI (React UI components from the graph):
- search
generative-ui-react - search
LoadExternalComponent react-ui - search
uiMessageReducer onCustomEvent - Agent Server + SDK streaming semantics:
- search
Create Run, Stream Output - search
Join Run Stream Last-Event-ID - search
Join Thread Stream - Security/gov for multi-tenant UIs:
- search
configurable headers - search
logging headers - search
agent server scale
Alternative UI stack (Node/TS runtime):
- AI SDK v6 (
useChat,streamText, UI stream protocol): - see
references/ui_nextjs_ai_sdk.md - Context7:
/vercel/ai - Streamdown (streaming-safe markdown renderer):
- load
$streamdownwhen available
LangGraph doc sitemap (llms.txt)
Use llms.txt as the authoritative index of LangGraph documentation pages (agentic RAG friendly).
- Extract URLs: run
python scripts/fetch_llms_txt_urls.py --print --unique(from the skill folder) - Then crawl selectively (don’t blindly fetch everything):
- pick relevant URLs based on the task
- fetch content with
mcp__exa__crawling_exaorweb.run - synthesize a “current best practice” summary tied to the installed versions
“Must use docs” triggers
Always consult docs before implementing if any of these are true:
- You’re touching agent creation APIs (
create_agent, deprecated prebuilt agents, middleware). - You’re implementing supervisor/subagent handoffs, parallel fanout, or reducers.
- You’re using
interrupt, checkpointing, stores, or persistence. - You’re dealing with runtime context injection (
ToolRuntime,context,store). - You’re implementing MCP integrations or tool adapters.
- You’re migrating from
langgraph-supervisor(-py)orcreate_react_agent. - You’re building a UI that depends on streaming (
useStream, SSE, resume/branching, tool-call UIs).
Guardrails + human-in-the-loop (HITL)
Design goal
Minimize irreversible failures and unsafe behavior by combining:
- deterministic checks (fast, cheap, predictable)
- model-based checks (semantic, slower)
- human approvals for high-stakes actions
Where to apply guardrails
- Before model calls (prompt shaping, policy constraints, context filtering)
- After model calls (output validation, structured extraction checks)
- Around tool calls (argument validation, allowlists, approval gates, rate limits)
HITL rules of thumb
Require approval for:
- destructive writes (delete/update production data)
- outbound communication (email, SMS, Slack, social)
- payments/transfers
- privileged system actions (shell, infra changes)
Prefer middleware-based HITL when available (clean separation of policy from business logic). Use graph interrupt when you need explicit pause/resume at a specific node boundary.
LangChain v1 create_agent + middleware (Python)
Use this as the canonical playbook for building production agents in LangChain v1 (built on LangGraph) while staying version-correct.
Ground rules
- Treat all APIs as versioned: verify with
langchain-docs.SearchDocsByLangChain+ Context7 before coding. - Prefer middleware for policy and cross-cutting concerns (context engineering, guardrails, retries, logging) instead of embedding that logic inside tools/nodes.
The modern surface area (what matters)
Agent creation
- Build agents with
langchain.agents.create_agent. - Prefer small, typed tool schemas and deterministic tool behavior.
- Add
checkpointer=when you need durability features like interrupts / human-in-the-loop and thread persistence.
Middleware hooks (Python)
LangChain supports both decorator-based middleware (simple, single-hook) and custom middleware implementations (complex, configurable). Common hooks:
@before_agent: once, before agent starts@before_model: before each model call@after_model: after each model response@after_agent: once, after agent completes@wrap_model_call: wrap each model call (modify request/response, retries)@wrap_tool_call: wrap each tool call (retries, error shaping, auth gates)@dynamic_prompt: generate dynamic system prompts
Use references/memory_and_context_engineering.md for “what to inject” and how to keep context small.
Built-in middleware to know (Python)
These are the “default toolkit” for production agents:
- Human-in-the-loop:
HumanInTheLoopMiddleware(interrupt_on={...}) - PII:
PIIMiddleware(pii_type, strategy=..., apply_to_input=..., apply_to_output=..., apply_to_tool_results=...) - Summarization:
SummarizationMiddleware(model=..., trigger={...}) - Tool emulation (useful for tests / dry-runs):
LLMToolEmulator(...)
Names/params are versioned; confirm via docs for your pinned versions.
Patterns (battle-tested)
1) Dynamic tool exposure (accuracy + cost)
Goal: reduce prompt/tool-choice entropy by presenting only a relevant subset of tools per turn.
- Implement as
@wrap_model_calland overriderequest.tools(or equivalent override API). - Keep the global tool registry stable; select a subset dynamically.
- Apply permissions here too (user/org allowlists).
2) Tool error shaping (don’t crash the run, but don’t hide real bugs)
Goal: treat user-caused/runtime tool failures as tool feedback, while surfacing genuine implementation bugs.
- Implement as
@wrap_tool_call. - Catch expected “runtime input errors” (e.g., invalid SQL syntax) and return a
ToolMessagewith a helpful error. - Do not swallow:
- network/system outages (use retry middleware + circuit breakers)
- programming errors (let them bubble up)
- schema validation errors (the framework handles these)
3) Human-in-the-loop for side effects
Goal: force review for irreversible operations.
- Gate: outbound comms, destructive writes, payments, privileged ops.
- HITL requires a
checkpointer=...and athread_idin config so the run can pause/resume.
4) Context injection as policy (not prompt sprawl)
Goal: keep LLM context small and relevant.
- Load memory/preferences in
@before_modeland inject a short structured snippet. - Use long-term store for facts/preferences, not transcripts.
- For large retrieved context, summarize or attach as artifacts rather than stuffing into the model prompt.
Migration notes: create_react_agent → create_agent
When migrating legacy LangGraph prebuilt agents:
prompt→system_prompt(dynamic prompts → middleware)- pre/post hooks → middleware (
before_model/after_model) - tool error handling → middleware (
wrap_tool_call) - runtime context injection →
context=argument (thread state still uses config/thread_id) - custom state: TypedDict only (no Pydantic/dataclass state schemas in
create_agent)
Use references/upgrades_and_versioning.md and the official migration guides to confirm details.
Multi-agent patterns: supervisor/subagents vs handoffs vs subgraphs
This reference is about multi-agent orchestration patterns in the LangChain/LangGraph ecosystem and how to choose the simplest one that meets requirements.
Default recommendation
Start with supervisor + subagents via tool-calling (LangChain multi-agent “subagents” pattern) unless you have a strong reason to do more.
Why:
- Subagents are stateless and run in a clean context window → less context bloat.
- The supervisor keeps the conversation state and can combine results deterministically.
- Fits well with middleware guardrails and HITL.
Pattern A: Supervisor + subagents (tool-calling)
Structure:
1. Define low-level tools (API calls). 2. Build specialized subagents with domain toolsets. 3. Wrap each subagent behind a single “high-level” tool for the supervisor. 4. Supervisor uses those wrapper tools to route work.
Best for:
- specialization across domains
- clean context isolation
- simple routing via prompt/tool descriptions
Key best practices:
- Make wrapper tools accept one explicit request string or a small structured schema.
- Pass only relevant context to subagents; do not dump the full message history by default.
- Prefer sequential tool calls for dependent operations; parallelize only when safe.
Pattern B: Handoffs (state-driven behavior changes)
Core idea:
- A tool updates a persistent state variable (e.g.,
active_agent/current_step). - A router (or middleware) uses that state to change behavior: prompts, tools, or next node.
Two main implementations:
1) Single agent + middleware (recommended for most handoff use cases)
- One agent; middleware switches prompt/tools based on state.
- Simpler than multiple graph nodes.
2) Multiple agent nodes/subgraphs (only when you need truly distinct agent implementations)
- Each agent is its own node/subgraph.
- Handoff tools return
Command(goto=..., update=..., graph=Command.PARENT).
Handoff correctness invariants (multi-node/subgraph approach)
If you hand off via a tool call, the receiving agent must see a valid “tool call → tool result” pair in history:
- The
AIMessagecontaining the tool call that triggered the handoff - A synthetic
ToolMessageacknowledging the handoff (the tool “result”)
Do not forward entire subagent transcripts by default. Prefer:
- (a) a short summary in the ToolMessage, plus
- (b) structured artifacts stored in state/store for retrieval if needed
Pattern C: Orchestrator-worker (fanout + aggregation)
Core idea:
- Orchestrator generates tasks.
- Workers run in parallel.
- Results aggregate into a shared state key with reducers.
Use when:
- tasks are independent (parallelizable)
- you need explicit aggregation logic
Key requirement:
- never let parallel nodes update the same state key unless it has a reducer (see
references/langgraph_graph_api_primitives.md).
LangGraph Graph API primitives (Python)
Use this when you need to drop below create_agent and build custom workflows/agents directly in LangGraph.
Mental model
- A graph is nodes (functions) + edges (control flow) over a shared state.
- Nodes read state, do work, and return partial state updates (dict).
- Conditional edges encode routing decisions explicitly.
- Durable execution comes from checkpointing.
State schemas
Messages-first state (chat apps)
- Use
MessagesStatefor message history, because it includes the right reducer semantics for chat message lists. - Prefer structured fields for important state (IDs, flags, plans), not only free-form text.
Reducers (required for parallelism)
Parallel fanout requires deterministic merge semantics.
- For append-only lists: use a reducer like
operator.addon a list field. - For message lists: use LangGraph’s
add_messagesreducer (built in).
If you ignore this, you’ll hit INVALID_CONCURRENT_GRAPH_UPDATE.
Common nodes/edges patterns
- Single-pass workflow:
START → step1 → step2 → END - Agent loop: conditional edge checks “has tool calls?” to continue
- Router: conditional edges based on state flag or last message content
- Fanout: orchestrator returns a list of
Send(...)instructions to spawn workers
Interrupts and human-in-the-loop
- Use
interrupt()to pause a graph at a node boundary for human input. - Interrupts require a checkpointer; without one you’ll hit
MISSING_CHECKPOINTER.
Subgraphs
Use subgraphs to:
- encapsulate complex agent behavior behind a stable interface
- isolate per-agent/private message histories
Two ways:
1) Invoke a compiled graph from inside a node (schemas can differ) 2) Add a compiled graph directly as a node (shares state keys)
Known gotcha:
- checkpointing + multiple subgraphs invoked in the same node can trigger
MULTIPLE_SUBGRAPHSunless structured carefully.
Troubleshooting: high-frequency error codes
See the common errors reference; the most common in multi-agent systems:
INVALID_CONCURRENT_GRAPH_UPDATE: parallel updates without reducersINVALID_GRAPH_NODE_RETURN_VALUE: node returned non-dictGRAPH_RECURSION_LIMIT: unintended infinite loopsINVALID_CHAT_HISTORY: malformed message sequence (often from bad handoff history)
When to use LangChain vs LangGraph directly
- Use LangChain
create_agentwhen the agent loop is standard and you want middleware. - Use LangGraph directly when you need deterministic control-flow, bespoke multi-agent routing, or custom durability boundaries.
MCP integration patterns (LangChain/LangGraph)
MCP (Model Context Protocol) standardizes how servers expose tools and context to LLM apps.
Default posture
Treat MCP tools as untrusted until proven otherwise:
- validate args at boundaries
- enforce allowlists/permissions
- add timeouts/retries
- trace everything
Multi-server tools (Python)
Use langchain-mcp-adapters to load tools from multiple MCP servers and expose them to a LangChain agent.
Key concepts:
MultiServerMCPClient: aggregates tools from many servers- tool interceptors: wrap tool calls (auth gates, argument injection, logging)
- ToolRuntime context: access
state,config,store, and usercontext
High-value interceptor patterns
1) Auth gating
- Block sensitive tools unless
runtime.stateindicates the user is authenticated. - Return a
ToolMessageerror instead of executing the tool.
2) Dependency injection
- Inject user-scoped credentials or identifiers from
runtime.contextinto tool args. - Never allow the model to see secrets; inject them server-side.
3) Tool allowlists + domain restrictions
- For URL fetch tools: allow only specific domains/prefixes to prevent SSRF/data exfil.
- For DB tools: enforce parameterized queries and least-privilege DB users.
When MCP is a great fit
- enterprise tool ecosystems (auth, governance, central tool registry)
- multi-tenant apps where tools must be user-scoped
- heterogeneous tool stacks (mixing internal + third-party services)
Context engineering + memory (state, store, runtime context)
This is the “operating manual” for keeping multi-agent systems scalable and reliable.
The 3-layer context model
1) Static runtime context (per run)
Immutable dependencies injected at invocation time (dependency injection):
- user/org IDs
- DB clients
- API credentials (never show to model)
- feature flags / permissions
In LangChain v1 this is typically provided via the context= argument (shaped by context_schema=).
2) Dynamic runtime context (state)
Mutable data during a run:
- messages
- intermediate results
- routing flags (
active_agent,next_step) - extracted entities (IDs, dates, etc.)
3) Dynamic cross-conversation context (store)
Long-term memory, shared across threads/sessions:
- user preferences (tone, verbosity)
- stable facts (timezone, role)
- task-specific durable artifacts (when safe)
Use namespaces (often (user_id, app_context) or (org_id, user_id, domain)).
Short-term vs long-term memory
- Short-term memory is state persisted via checkpointer (thread-scoped).
- Long-term memory is a store (cross-thread) with optional semantic search.
Practical playbook
Keep messages small
- Use summarization middleware or explicit summary fields.
- Delete stale messages when safe (requires message reducers).
- Store raw artifacts outside the LLM prompt; only reference them via IDs + short summaries.
Store the right things
Good long-term memories:
- preferences (“user likes short answers”)
- stable identifiers (“account_id”, “timezone”)
- rules/instructions (“always confirm before sending email”)
Bad long-term memories:
- full transcripts
- secrets / API keys
- large raw documents (store pointers/IDs instead)
Multi-agent memory strategy
- Keep subagents stateless; the supervisor owns memory.
- If agents must hand off, pass only the minimum validated message/tool pair plus a compact summary.
ToolRuntime (why it matters)
Modern LangChain/LangGraph stacks provide a runtime object to tools/middleware/interceptors that can expose:
state(conversation state)config(thread IDs, run config)store(long-term memory)tool_call_id(for ToolMessage correctness)
Use this to:
- enforce auth/permissions
- implement per-user namespaces
- write progress events / telemetry
Migration playbook: other agent frameworks → LangGraph/LangChain
Use this when a repo uses mixed stacks (LlamaIndex agents, CrewAI, Agno, OpenAI Agents). The strategy is always:
1) stabilize behavior with tests, 2) extract tools, 3) rebuild orchestration, 4) add observability, 5) delete legacy.
LlamaIndex agents → LangGraph/LangChain
Typical mappings:
- LI “agent” orchestration → LangGraph graph + LangChain
create_agent - LI tools → LangChain tools (
@tool/ BaseTool) - LI retrieval/indexing → either:
- keep LI indexing where it’s uniquely valuable, but call it from LangChain tools, or
- migrate to LangChain retrievers/vector stores if alignment/simplicity is preferred
Key risk: duplicated memory / state / tracing layers.
CrewAI → supervisor/subagents
CrewAI concepts:
- “crew/roles” → subagents
- “tasks” → tool calls or deterministic workflow nodes
- “manager” → supervisor agent
Migration steps:
1. Define per-role toolsets. 2. Convert each role to a subagent. 3. Implement a supervisor that delegates via tool calls. 4. Add HITL for side effects; add thread-level memory/checkpointing.
Agno → graph-native orchestration
Treat Agno’s orchestration as “workflow + tools”. Rebuild the orchestration as a graph:
- explicit nodes for state changes
- conditional edges for routing decisions
- middleware for policy enforcement
OpenAI Agents / Responses API → LangChain create_agent
Strategy:
1. Extract tools into provider-agnostic LangChain tools. 2. Replace agent loop with create_agent. 3. Implement middleware guardrails for:
- prompt injection checks
- PII filtering
- tool approvals
4. Keep the model provider swappable; validate tool-call JSON schemas end-to-end.
Migration: langgraph-supervisor(-py) → modern LangChain/LangGraph
Why migrate
Current guidance generally recommends building the supervisor pattern directly via tools/subagents, using:
- LangChain v1
create_agent(middleware, runtime context) - LangGraph v1+ primitives for durable execution and checkpoints
Keep langgraph-supervisor only when it is clearly net-positive for your codebase and matches your pinned versions.
Migration outline (safe, staged)
1. Inventory current behavior
- Identify supervisor creation (
create_supervisor, handoff tools, routing rules). - Add tests around the externally observable behavior (tool calls, outputs, error handling).
2. Extract tool layer
- Move business operations to standalone LangChain tools with strict schemas.
- Ensure side-effectful tools are idempotent or protected by HITL.
3. Port subagents
- For each worker agent: create
create_agent(model, tools=..., system_prompt=..., middleware=...). - Wrap subagents as tools callable by the supervisor (tool-calling pattern).
4. Port supervisor
- Implement supervisor as
create_agentwith the subagent-wrapper tools. - If you need strict ordering: encode it in the supervisor system prompt and/or deterministic graph edges.
5. Add durability + HITL
- Add checkpointer for pause/resume and thread memory.
- Add HITL middleware or graph-level
interruptfor sensitive tools.
6. Remove legacy wiring
- Delete supervisor-lib-specific adapters only after parity tests pass.
What to validate explicitly
- Message/state handoff semantics (what context is passed to subagents).
- Streaming behavior (node naming, step boundaries).
- Error handling: tool exceptions, retry behavior, and “fail open” decisions.
- Any hook replacements:
- pre/post model hooks → middleware (
before_model/after_model) - tool error handling → middleware wrappers
Observability (LangSmith + OpenTelemetry)
Minimum viable observability
Record these per run:
- node-level latency + errors
- tool call count + latency + error taxonomy
- token usage (prompt/completion) and estimated cost
- cache hit rate (retrieval/tool/memory)
- stop reasons (max steps, recursion limit, human interrupt, tool failure)
Tracing strategy
1. Use LangSmith traces for rapid debugging and UX iteration (when available). 2. Use OpenTelemetry for org-standard metrics/logs/traces pipelines. 3. Ensure propagation of:
- thread/run identifiers
- user/org identifiers (redacted or hashed)
- model/provider metadata
Production hardening checklist
- Add sampling for high-throughput endpoints.
- Redact PII from spans/logs by default.
- Add “golden path” regression traces to detect behavior drift after upgrades.
Pattern library (multi-agent in LangGraph/LangChain)
0) Default recommendation
Prefer supervisor + subagents via tool-calling unless you have a specific need for:
- strict deterministic control-flow (workflow-heavy)
- parallel fanout (orchestrator-worker)
- deeply customized state transitions or checkpoints
1) Supervisor + subagents (tool-calling)
Core idea:
- Each subagent is a focused
create_agent(...)with its own tools + prompt. - The supervisor calls subagents as tools to get context isolation and prevent context bloat.
- Human-in-the-loop and guardrails are implemented via middleware (preferred) or graph interrupts.
Checklist:
- Keep subagents stateless; the supervisor owns memory/state.
- Use strict tool schemas; validate inputs before tool side effects.
- For sensitive tools, require approval (HITL middleware).
- Make supervisor prompts explicit about when to delegate.
2) Orchestrator-worker (fanout + aggregation)
Core idea:
- Orchestrator emits a dynamic set of worker tasks.
- Workers run independently and write results into a dedicated state key.
- Orchestrator aggregates results and decides whether to continue.
Checklist:
- Never let multiple nodes write to the same state key without a reducer.
- Design aggregation state so it can be merged deterministically.
- Bound concurrency; handle timeouts and partial results.
3) Hybrid workflow + agents
Core idea:
- Keep control-flow deterministic where possible.
- Use LLM decisions only at “routing” seams.
Checklist:
- Separate policy (routing) from mechanics (tools, IO).
- Make errors part of the flow: retries, loop-backs with context, pause for human, or bubble up.
4) Context + memory (three layers)
Use the correct layer for the correct job:
- Static runtime context: per-run dependencies (user ID, DB client, API clients).
- Dynamic runtime context (state): mutable per-run state transitions.
- Dynamic cross-conversation context (store): long-term memory / preferences / user profile.
5) Safety primitives
- Guardrails: deterministic checks (regex, allowlists) + model-based evaluations when needed.
- HITL: approvals for irreversible actions.
- Tool design: idempotency, timeouts, explicit side-effects, least-privilege credentials.
Performance + cost optimization
1) Context discipline
- Prefer retrieval over large static prompts.
- Summarize aggressively at stable checkpoints.
- Use subagents as context isolation boundaries.
2) Cache the right things
- Deterministic tool results: cache by (tool name, args hash, user scope).
- Retrieval: cache embeddings + vector results when acceptable.
- Long-term memory: store normalized facts/preferences, not transcripts.
3) Model routing
- Small model for routing/validation; large model for synthesis.
- Enforce max tool calls / max steps / recursion limits.
4) Parallelism safety
- Parallelize only when state updates can be merged deterministically.
- Use reducers to avoid concurrent update errors.
- Prefer “fanout results key” + aggregation.
5) Reliability tactics
- Timeouts + retries with jitter for flaky dependencies.
- Circuit breakers for repeated tool failures.
- “Fail open” only for non-critical features (e.g., optional rerank).
Research playbook (LangGraph/LangChain multi-agent)
Use this when you need high confidence on behavior, APIs, or migrations. The goal is to always use the latest docs for the repo’s versions.
1) Establish version ground truth
1. Read dependency constraints (preferred order):
pyproject.toml,uv.lock,poetry.lock,requirements*.txt,pip-toolsfiles.
2. Confirm what is actually installed (runtime truth):
python -c "from importlib import metadata; print(metadata.version('langgraph'))"(repeat forlangchain,langchain-core)
If installed versions don’t match constraints, treat all behavior as UNVERIFIED until reconciled.
2) Find the right docs pages (search, don’t guess)
Use langchain-docs.SearchDocsByLangChain as the first stop for:
- release notes + migrations
- “how-to” guides and patterns
- official examples
Technique:
1. Search by concept, not just API:
create_agent middleware before_model after_modelsubagents supervisor tool callinginterrupt checkpointer InMemorySaverstore long-term memory namespace search
2. Open 3–5 top pages and extract the relevant section(s) with Exa crawling or web.run.
3) Lock down API references (Context7)
After you identify the concept/page, use Context7 for API-level details and code snippets:
- Resolve library IDs:
mcp__context7__resolve-library-id - Query docs with the exact API names you’re implementing:
- “
create_agentmiddlewareHumanInTheLoopMiddleware” - “
StateGraphMessagesStatereducersSend”
4) When docs disagree or edge cases appear: use opensrc/
Use opensrc/ for under-the-hood truth (read-only):
1. Snapshot exact versions:
python scripts/opensrc_snapshot.py --packages langgraph langchain langchain-core(from the skill folder)
2. Inspect:
opensrc/sources.json(source-of-truth for versions)- internal implementations for the relevant APIs
3. In writeups (ADRs/specs/PRs): cite exact opensrc/... paths + version strings.
5) Crawl strategy for “all relevant docs” without context bloat
Do not dump entire docs into the prompt. Instead:
1. Use llms.txt as sitemap for LangGraph:
python scripts/fetch_llms_txt_urls.py --print --unique(from the skill folder)
2. Filter URLs by task keywords (e.g. multi_agent, checkpoint, store, interrupt, mcp, deployment). 3. Crawl only those URLs; summarize into a short, version-tagged memo:
- “What changed”, “Recommended pattern”, “Migration steps”, “Gotchas”.
6) Evidence checklist (what “research backed” means)
- Every non-trivial API usage is supported by either:
- official docs snippet (LangChain docs + Context7), or
opensrc/inspection for the installed version- For migrations: include exact “from → to” mappings and identify behavior changes (middleware, streaming, state schema).
- For production changes: include tests and observability hooks.
Retrieval + Agentic RAG
Baseline
Start with a retrieval tool and let the agent decide when to call it (agentic RAG). Keep tool outputs structured:
- “content” for model consumption
- “artifacts” for your application (raw docs/metadata)
Advanced controls
- Query rewriting and relevance checks as separate nodes.
- Reranking with timeouts (fail open).
- Source attribution: keep doc IDs/URLs outside the LLM context when possible, and attach them as artifacts.
Docs-driven RAG for LangGraph
Use llms.txt as the doc URL index, and implement an allowlist-based fetch tool that only loads those URLs.
Security threat model (multi-agent + tool calling)
Use this when designing production agent systems or migrating a legacy stack.
Primary threats
Prompt injection / data exfiltration
- Malicious content attempts to override instructions and trigger unsafe tools.
- Retrieval sources can contain adversarial strings that look like tool directives.
Mitigations:
- strict tool allowlists, permissions, and argument validation
- isolate retrieval content; never treat it as instructions
- use middleware guardrails + HITL for sensitive actions
SSRF / untrusted network access
- “fetch URL” tools can leak internal network data.
Mitigations:
- domain/prefix allowlists
- block loopback/private IP ranges
- enforce HTTPS and size limits
Secrets leakage
- The model should never receive raw secrets; tool outputs may contain secrets.
Mitigations:
- inject secrets via runtime context (server-side DI)
- redact tool results before returning to model
Destructive writes
- Agents can perform irreversible actions quickly.
Mitigations:
- human-in-the-loop for side effects
- least privilege credentials
- idempotent APIs + dry-run modes
Security-by-default checklist
- Every tool has: schema, timeout, retries, and explicit side-effect declaration.
- Every side-effectful tool has: auth gating + HITL (unless explicitly exempt).
- Logs/traces: redaction + sampling + per-tenant isolation.
Testing + evaluation for multi-agent systems
Goal: keep migrations safe and prevent regressions as models/versions change.
Test pyramid
1) Tool unit tests (fast, offline)
- Validate schemas and edge cases.
- Ensure idempotency where required.
- Mock external IO; record deterministic fixtures.
2) Graph/agent integration tests (offline-first)
- Run the compiled graph with a stub model (or mocked model interface).
- Assert:
- correct tool selection / ordering
- correct state transitions
- correct handling of tool failures
- correct interrupt behavior (HITL)
3) End-to-end tests (selective)
- Only for critical paths.
- Capture traces and verify invariants (latency, tool count, stop reason).
Regression evaluation (LangSmith-style)
Recommended:
- maintain a dataset of canonical prompts and expected tool actions
- run evaluations on every dependency upgrade
- keep “golden traces” for critical flows
Migration safety protocol
1. Add tests before refactoring orchestration. 2. Migrate in slices:
- tools → state → routing → memory → deployment
3. Keep observability on during rollout.
UI integration backend: FastAPI for LangGraph/LangChain agents
This guide focuses on running the agent runtime behind FastAPI and integrating with web UIs.
Choose your backend mode
Mode 1 (recommended when you want the full LangGraph stack): Agent Server
Run LangGraph Agent Server (locally via langgraph dev, or deployed) and have your UI call it directly using the JS SDK (useStream) or REST API.
Use FastAPI only as a “BFF” if you need:
- custom auth/session integration
- request shaping / logging / governance
- cross-service orchestration
- a single domain/origin for the browser (avoid CORS issues)
Mode 2: In-process LangChain/LangGraph inside FastAPI
You own:
- persistence (thread state, checkpoint storage)
- streaming protocol to the browser
- HITL resume endpoints
Use when:
- you need full control and won’t use Agent Server
Streaming transport
For browser UIs, prefer SSE for token streaming:
- works over plain HTTP
- proxies/CDNs generally handle it well
- client reconnection behavior is simpler than WS for “server → client” streams
Use WebSockets only if you need bi-directional real-time channels beyond request/response.
FastAPI endpoint design (recommended)
Keep APIs minimal and explicit:
POST /chat/stream→ SSE stream of events (token/update/custom/interrupt/done)POST /chat/resume→ resume a paused thread with HITL decisions (or fold into the stream endpoint)POST /threads→ create a new thread ID (optional; the stream endpoint can also create one)
Always require a thread_id for:
- long-running conversations
- HITL pause/resume
- time travel/branching (if implemented)
In-process runtime essentials (LangGraph/LangChain)
If you run the agent inside FastAPI:
- Use
create_agent(...)(LangChain v1) or a compiled LangGraphStateGraph. - Provide a checkpointer (required for interrupts / resume and for durable threads).
- Use
config = {"configurable": {"thread_id": thread_id}}for thread scoping. - Stream with
stream_mode=["messages", "updates", "custom"]so the UI can show: - tokens (
messages) - step/state deltas + interrupts (
updates) - progress bars + tool traces (
custom)
For HITL resumption, accept a resume payload and invoke with Command(resume=...) (exact schema is versioned and comes from the interrupt itself).
Security + governance
- Never accept tool args from the client as “trusted”. Tool args must come from the model + schema validation.
- Use auth to gate tool access (per user/org).
- Add rate limits and timeouts per tool call.
- Redact PII from logs/spans by default.
- Prefer least-privilege credentials injected at runtime (server-side), not stored in graph state.
Template shipped with this skill
assets/templates/fastapi/fastapi_sse_multiagent.py:- shows SSE streaming from
agent.astream(...) - emits typed JSON events compatible with a Next.js client
Pair it with references/ui_streaming_protocol.md for the event schema.
UI integration: Next.js App Router + AI SDK v6 + Streamdown (React 19+)
This reference covers building a production-grade chat UI in Next.js App Router using:
- Vercel AI SDK v6 (
ai,@ai-sdk/react) for message/parts streaming and tool UIs - Streamdown for streaming-safe Markdown rendering
- Zod for runtime validation and type inference
- shadcn/ui + Tailwind for UI components
Use this when your agent runtime is in TypeScript/Node (AI SDK core functions or ToolLoopAgent).
If your agent runtime is Python (LangGraph/LangChain in-process), prefer:
- LangGraph Agent Server + `useStream` (see
references/ui_nextjs_rsc.md) to avoid writing a custom “protocol bridge”.
Ground-truth rules (always)
1. Treat API surfaces as versioned; don’t code from memory. 2. Before implementing UI plumbing, verify:
- Installed AI SDK versions (lockfile)
- Current AI SDK stream protocol + hook API (Context7 + official docs)
3. If you must understand under-the-hood behavior, snapshot sources (read-only):
npx opensrc ai@<VERSION> --modify=falsenpx opensrc @ai-sdk/react@<VERSION> --modify=falsenpx opensrc streamdown@<VERSION> --modify=false
Minimal architecture (recommended)
app/chat/page.tsx(RSC): resolves auth/session and renders the client chat componentapp/chat/ChatClient.tsx("use client"):useChatUI state + renderingapp/api/chat/route.ts(server):streamText(...)andtoUIMessageStreamResponse()
Why:
- Aligns with AI SDK’s UI message/parts model (tools, files, metadata)
- Keeps secrets server-side (route handler)
- Easy to add persistence and auth
Server route handler pattern (AI SDK v6)
Core pattern:
- Parse
UIMessage[]from request - Convert to model messages with
await convertToModelMessages(messages) - Stream with
streamText(...) - Return
result.toUIMessageStreamResponse(...)
Authoritative example snippet (AI SDK repo/docs) uses:
convertToModelMessages(async in v6)toUIMessageStreamResponsefor UI streaming responses
Client pattern (AI SDK UI)
Use useChat with a DefaultChatTransport:
- chat surface is a client component
- render
message.parts(text/tool results/files) - use
statusfor loading state and StreamdownisAnimating
Tool calling + UI rendering (Zod-first)
Prefer Zod schemas for tools and (when feasible) for server→client custom events:
- Zod becomes the single source-of-truth for validation and TypeScript types
- Keep tool inputs narrow (principle of least privilege)
For tool UI patterns (invocations, results, approvals), load:
- the Vercel AI SDK skill, when available
- any installed
ai-sdk-uiorai-sdk-coreskill references, when available
Message persistence + stream resumption
AI SDK UI is flexible, but persistence is your responsibility:
- Persist chat transcripts in your DB keyed by session/thread
- Restore messages on load (
initialMessages) or via a fetch on mount - Decide whether to persist tool traces or only user/assistant text
References:
- the Vercel AI SDK skill, when available
- any installed
ai-sdk-uireferences for persistence, backend, and
production concerns
Markdown rendering (Streamdown)
Streamdown is a streaming-optimized replacement for react-markdown.
Best practice:
- Render assistant
textparts with Streamdown - Set
isAnimating={status === "streaming"}during generation - Harden untrusted markdown/HTML with
rehype-hardenwhen you allow links/images
References:
- Streamdown skill: load
$streamdownwhen available - AI SDK integration: load Streamdown's AI SDK reference when available
- Styling/security: load Streamdown's styling/security reference when available
Tailwind integration notes live in the Streamdown skill:
- Tailwind v4:
@source "../node_modules/streamdown/dist/*.js";inglobals.css - Tailwind v3: add
./node_modules/streamdown/dist/*.jstocontent
UI quality bar (shadcn/ui + Tailwind)
For polished, production-grade UI patterns (beyond a basic chat box), load:
$ui-workbenchor the Build Web Apps frontend skill when available
For AI SDK Agents workflow/UI patterns:
- the Vercel AI SDK skill, when available
- any installed
ai-sdk-agentsproduction reference, when available
Bridging to a Python agent runtime (FastAPI / LangGraph)
If your agent runtime is Python, prefer Agent Server + `useStream`.
If you must use AI SDK UI anyway:
- Implement a Next.js “BFF” route that:
1. Calls your Python backend 2. Translates backend streaming events into AI SDK’s UI stream protocol
This is non-trivial and easy to get wrong (interrupts/resume, tool calls, threading).
For custom SSE event schemas, see:
references/ui_streaming_protocol.mdreferences/ui_fastapi_backend.md
Templates shipped with this skill
- AI SDK v6 + Streamdown Next.js templates:
assets/templates/nextjs_ai_sdk/app/api/chat/route.tsassets/templates/nextjs_ai_sdk/app/chat/page.tsxassets/templates/nextjs_ai_sdk/app/chat/ChatClient.tsxassets/templates/nextjs_ai_sdk/app/chat/Response.tsx
UI integration: Next.js App Router (RSC) + React + shadcn/ui + Tailwind + Zod
This guide covers production-grade UI integration for LangGraph/LangChain multi-agent systems in a modern Next.js stack.
Recommended architecture options (pick one)
Option A (recommended for LangGraph-based apps): LangGraph Agent Server + useStream
Use LangGraph Agent Server (local langgraph dev, or LangSmith Deployment) and connect from Next.js using:
@langchain/langgraph-sdk/react→useStream()(messages, interrupts, branching, typed state)- optional
@langchain/langgraph-sdk/react-uifor generative UI components (LoadExternalComponent) - optional “Agent Chat UI” for a ready-made baseline chat surface (then layer custom UI)
Why:
- Built-in thread persistence (thread IDs)
- Standard streaming API + SDKs
- Works well with multi-agent graphs, subgraphs, HITL, and long-term memory store
Option B: Next.js route handlers + AI SDK UI (useChat)
Use this when your backend is not an Agent Server (custom FastAPI, custom infra), or you want AI SDK’s message/parts ecosystem.
Tradeoffs:
- You own thread persistence and HITL protocols unless you integrate with Agent Server.
- More custom glue when your agent runtime is Python.
For an end-to-end AI SDK v6 + Streamdown reference (and templates), see:
references/ui_nextjs_ai_sdk.md
Next.js app/router patterns (RSC-safe)
- Keep the “chat surface” as a client component (
"use client"). - Keep auth/session/user resolution in server components (RSC) and pass only non-sensitive IDs to the client.
- Never ship secrets to the browser. Use server-side tokens (cookies/session) or proxy through your server.
Thread IDs (stateful conversations)
With useStream, you can:
- provide
threadIdto resume an existing thread - use
onThreadIdcallback to persist the created thread ID
Best practice:
- persist
threadIdin URL query params orlocalStorage - include user/org identifiers via safe headers or server-side injection
- for optimistic routing (navigate before the thread exists), generate a UUID and pass it via
submit(..., { threadId })
Example thread management pattern:
const [threadId, setThreadId] = useState<string | null>(null);
const stream = useStream({
apiUrl: process.env.NEXT_PUBLIC_LANGGRAPH_API_URL ?? "http://localhost:2024",
assistantId: "agent",
threadId,
onThreadId: setThreadId,
});Zod: validate UI↔backend contracts
Use Zod for:
- validating tool-call payloads shown in UI
- validating custom streaming events (progress, interrupts)
- validating “resume” decisions for HITL
Keep Zod schemas as the single source-of-truth in the frontend and generate types from them.
Human-in-the-loop UI
Two levels:
1. Simple approvals: show tool name + args + “Approve/Reject”. 2. Editable approvals: allow editing args (restricted fields) when edit is allowed.
Implementation detail:
- LangChain HITL interrupts surface as
__interrupt__in state/updates; resumption uses aCommand(resume={decisions: [...]})pattern on the backend. - In Agent Server +
useStream, usestream.interrupt+stream.submit(null, { command: { resume: { decisions } } }).
Minimal HITL resume sketch:
const hitlRequest = stream.interrupt?.value;
await stream.submit(null, { command: { resume: { decisions: [{ type: "approve" }] } } });Multi-agent UI ergonomics
Multi-agent systems often produce outputs from different nodes/agents.
UI best practices:
- show “agent badges” using message metadata (node/agent name)
- collapse tool-call traces by default (expandable)
- stream progress updates (“custom” stream mode) into a small status bar
To label messages, use stream.getMessageMetadata(message) and read fields like langgraph_node (exact keys are versioned; consult the streaming frontend docs).
shadcn/ui + Tailwind guidelines
- Treat the chat UI as a “log viewer” + “command input”.
- Use monospace blocks for tool arguments and structured data.
- Keep interrupts visually distinct (danger border + explicit action buttons).
Advanced: resume after refresh, branching, and custom transport
The streaming frontend docs cover:
reconnectOnMountfor auto-resuming an in-flight run after refreshonCreated/onFinishcallbacks for manual resumption and run-id persistencestream.switchBranch(...)for navigating conversation forksFetchStreamTransportfor adding auth headers or shaping requests without changing UI code
Use this when you need a Next.js “BFF” route (server-side cookies/session) but still want useStream on the client.
Templates shipped with this skill
- Next.js
useStreamchat page: assets/templates/nextjs/app/chat/page.tsxassets/templates/nextjs/app/chat/ChatClient.tsxassets/templates/nextjs/app/chat/types.tsassets/templates/nextjs/app/chat/Response.tsx(Streamdown renderer; installstreamdownto use)
These templates assume you have shadcn primitives available at @/components/ui/*.
- Next.js AI SDK v6 chat page (alternative stack):
- see
references/ui_nextjs_ai_sdk.md - templates under
assets/templates/nextjs_ai_sdk/
Markdown rendering (recommended): Streamdown
Agent responses often contain Markdown (tables, lists, code blocks). Prefer Streamdown for chat UIs because it is streaming-friendly (handles incomplete Markdown as tokens stream).
Deep-dive references:
- Streamdown: load
$streamdownwhen available - Streamdown + AI SDK patterns: load Streamdown's AI SDK reference when available
- Streamdown security + styling: load Streamdown's styling/security reference when available
- Streamdown API reference: load Streamdown's API reference when available
Cross-skill UI references (load on demand)
If you’re building a Next.js UI and want best-practice guidance for the UI layer beyond LangGraph:
- AI SDK Core/UI/Agents: use the Vercel AI SDK skill or installed AI SDK skills
- Frontend quality + aesthetics: use
$ui-workbenchor the Build Web Apps frontend skill when available
UI streaming protocol (SSE/Web) for multi-agent systems
This file defines a simple, typed event protocol for streaming agent execution to UIs (Next.js, Streamlit, etc.).
Goal: represent tokens, state updates, custom progress, and interrupts consistently.
Event types
All events are JSON objects with a type discriminator.
meta
Sent once at the start.
- includes
threadIdand optional model/assistant identifiers - optionally includes
runIdif the backend creates runs
token
Streaming token chunks from stream_mode="messages".
- fields:
content(string), optionalnode, optionalmetadata
update
State updates from stream_mode="updates".
- fields:
data(object), optionalnode - interrupts may show up as
{"__interrupt__": ...}insidedata
interrupt
Explicit interrupt event (recommended to emit separately).
- fields:
interrupt(object)
custom
User-defined progress events emitted by tools/nodes using a writer.
- fields:
event(object)
done / error
Stream termination.
TypeScript (Zod) schema sketch
import { z } from 'zod';
export const MetaEvent = z.object({
type: z.literal('meta'),
threadId: z.string(),
runId: z.string().optional(),
});
export const TokenEvent = z.object({
type: z.literal('token'),
content: z.string(),
node: z.string().optional(),
metadata: z.unknown().optional(),
});
export const UpdateEvent = z.object({
type: z.literal('update'),
data: z.record(z.unknown()),
});
export const InterruptEvent = z.object({
type: z.literal('interrupt'),
interrupt: z.unknown(),
});
export const CustomEvent = z.object({
type: z.literal('custom'),
event: z.unknown(),
});
export const DoneEvent = z.object({ type: z.literal('done') });
export const ErrorEvent = z.object({ type: z.literal('error'), message: z.string() });
export const StreamEvent = z.discriminatedUnion('type', [
MetaEvent,
TokenEvent,
UpdateEvent,
InterruptEvent,
CustomEvent,
DoneEvent,
ErrorEvent,
]);SSE framing
Each event is sent as (minimal):
data: {"type":"token","content":"..."}
No SSE event name required; the type field drives handling.
Optional: resumable SSE (recommended when supported)
If your infra supports SSE reconnection/resumption, include an event ID:
id: 42
data: {"type":"token","content":"..."}
On reconnect, browsers may send Last-Event-ID. Agent Server supports this for resumable streams when a run was created with stream_resumable=true.
Mapping from LangGraph stream modes (Python)
messages→ emittokeneventsupdates→ emitupdateevents; if interrupt present, also emitinterruptcustom→ emitcustomevents
HITL resume payloads
When HITL interrupts occur, the UI typically collects decisions:
approvereject(with optional explanation message)edit(with edited tool args)
Resumption is backend-specific:
- Agent Server/SDK supports resuming runs/threads via standard APIs.
- In-process FastAPI should expose a resume endpoint or accept a resume payload in the stream endpoint.
UI integration: Streamlit (Python)
This guide complements streamlit-master-architect by focusing specifically on LangGraph/LangChain multi-agent integration patterns inside Streamlit apps.
Load Streamlit Master Architect (recommended)
This skill intentionally stays focused on agent integration; for Streamlit APIs, evergreen upgrade rules, and production hardening, load:
- Streamlit Master Architect skill: load it when available.
- Streamlit Master Architect references: load the matching reference files from that skill when available.
Evergreen “ground truth” loop (don’t guess Streamlit APIs)
1. Detect the project’s Streamlit version:
python3 -c "import streamlit as st; print(st.__version__)"
2. Audit the Streamlit project (deprecations + risky patterns):
python3 "$skill_dir/scripts/audit_streamlit_project.py" --root <PROJECT_ROOT> --format md
3. If needed, sync Streamlit docs index:
python3 "$skill_dir/scripts/sync_streamlit_docs.py" --out /tmp/streamlit-docs
Core architecture
Recommended layering:
1. Pure agent logic (tools, graphs, prompts) in importable modules 2. UI wiring (Streamlit) that only handles input/output and state 3. Persistence via checkpointers/stores (not st.session_state only)
Streamlit state model
Use st.session_state for:
- user-visible chat history (messages to render)
- UI state (selected thread, pending interrupt decisions)
- lightweight identifiers (thread_id)
Use LangGraph persistence for:
- durable short-term memory (thread checkpoints)
- HITL pause/resume (required)
Backend choices for Streamlit
- In-process (simplest): import your agent/graph and call
.stream()/.astream()directly. - Agent Server (most scalable): call a local/deployed Agent Server using the LangGraph SDK; still render streaming in Streamlit.
Streaming tokens into Streamlit
Typical pattern:
- render an assistant chat message container
- update a placeholder as tokens arrive
- keep the final assistant message in
st.session_state.messages
Prefer agent.stream(..., stream_mode=["messages","updates","custom"]) for:
- tokens (
messages) - interrupt detection (
updates) - progress events (
custom)
Human-in-the-loop UI
When interrupts occur:
1. surface the pending actions (tool name + args) 2. let the user approve/edit/reject 3. resume the run with a Command(resume=...) using the same thread_id
Keep the interrupt UI explicit and visually distinct (danger/warning styling).
Performance basics
- Cache heavy resources with
st.cache_resource(models, embeddings, vector clients). - Cache data reads with
st.cache_data. - Avoid import-time IO; initialize lazily in cached functions.
Template shipped with this skill
assets/templates/streamlit/streamlit_chat_app.py:- streaming tokens into
st.chat_message - maintaining a
thread_id - placeholder UI for interrupts
assets/templates/streamlit/tests/test_smoke_apptest.py:- minimal AppTest smoke test (offline-first pattern)
Upgrades + versioning playbook (LangGraph/LangChain)
This is the repeatable process for staying “latest best practice” without breaking production.
0) Establish the truth
1. Identify pinned versions (lockfiles / constraints). 2. Confirm installed versions (runtime truth). 3. Snapshot dependency sources for edge cases:
python scripts/opensrc_snapshot.py --packages langgraph langchain langchain-core
1) Read before you code
- Search release notes + migration guides with
langchain-docs.SearchDocsByLangChain. - Use Context7 for API-level confirmations (signatures, import paths).
2) Run the repo audit
python scripts/audit_repo_agents.py --root . --json agent_audit.json- Generate a migration plan:
python scripts/generate_migration_plan.py --audit-json agent_audit.json --out migration_plan.md
3) Migrate in slices
Recommended order:
1. tool layer (schemas, retries, idempotency) 2. orchestration wiring (agent loop / graph) 3. memory (checkpointer + store) 4. guardrails + HITL (middleware) 5. observability + evaluation
4) Known modernization themes
langgraph.prebuilt.create_react_agent→langchain.agents.create_agent- hooks → middleware (
before_model,after_model,wrap_tool_call,dynamic_prompt) - custom state → TypedDict only in modern agent stacks
Always confirm details against the docs for the exact versions you ship.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
import time
import urllib.parse
import urllib.request
from dataclasses import dataclass
from html.parser import HTMLParser
from pathlib import Path
class _LinkExtractor(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.links: list[str] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag.lower() != "a":
return
for k, v in attrs:
if k.lower() == "href" and v:
self.links.append(v)
def _fetch(url: str, *, timeout_s: int = 30) -> str:
req = urllib.request.Request(url, headers={"User-Agent": "langgraph-multiagent-skill/1.0"})
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
return resp.read().decode("utf-8", errors="replace")
def _normalize_url(base: str, href: str) -> str | None:
href = href.strip()
if not href:
return None
if href.startswith("#"):
return None
if href.startswith(("mailto:", "tel:", "javascript:")):
return None
url = urllib.parse.urljoin(base, href)
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in {"http", "https"}:
return None
# Drop fragments
parsed = parsed._replace(fragment="")
return urllib.parse.urlunparse(parsed)
def _sha256(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
def _safe_filename(url: str) -> str:
parsed = urllib.parse.urlparse(url)
path = parsed.path.strip("/").replace("/", "__") or "index"
if len(path) > 180:
path = path[:180]
return f"{parsed.netloc}__{path}__{_sha256(url)[:10]}.html"
def _extract_links(url: str, html: str) -> list[str]:
parser = _LinkExtractor()
try:
parser.feed(html)
except Exception:
return []
out: list[str] = []
for href in parser.links:
norm = _normalize_url(url, href)
if norm:
out.append(norm)
return out
@dataclass(frozen=True)
class CrawlConfig:
allow_domains: tuple[str, ...]
allow_prefixes: tuple[str, ...]
delay_s: float
max_pages: int
def _allowed(url: str, cfg: CrawlConfig) -> bool:
parsed = urllib.parse.urlparse(url)
if parsed.netloc not in cfg.allow_domains:
return False
if not cfg.allow_prefixes:
return True
return any(url.startswith(p) for p in cfg.allow_prefixes)
LLMS_LINK_RE = re.compile(r"\((https?://[^)\\s]+)\)")
def _extract_llms_urls(llms_txt: str) -> list[str]:
return [m.group(1) for m in LLMS_LINK_RE.finditer(llms_txt)]
def main() -> int:
parser = argparse.ArgumentParser(description="Crawl docs (bounded) and save HTML snapshots for offline inspection.")
parser.add_argument("--out-dir", default="docs_cache", help="Output directory for snapshots (default: docs_cache).")
parser.add_argument(
"--seeds",
nargs="*",
default=[],
help="Seed URLs to crawl (space-separated). If omitted and --llms-txt is set, seeds come from llms.txt.",
)
parser.add_argument(
"--llms-txt",
default="",
help="If set, fetch this llms.txt and use it to seed URLs (e.g. https://langchain-ai.github.io/langgraph/llms.txt).",
)
parser.add_argument(
"--allow-domains",
nargs="+",
default=["langchain-ai.github.io", "docs.langchain.com"],
help="Allowed domains for crawling (default: langchain-ai.github.io docs.langchain.com).",
)
parser.add_argument(
"--allow-prefixes",
nargs="*",
default=[],
help="Optional URL prefixes to constrain crawling further (recommended).",
)
parser.add_argument("--max-pages", type=int, default=200, help="Max pages to fetch (default: 200).")
parser.add_argument("--delay-s", type=float, default=0.2, help="Delay between requests (default: 0.2s).")
args = parser.parse_args()
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
seeds = list(args.seeds)
if args.llms_txt:
print(f"[info] Fetching llms.txt: {args.llms_txt}", file=sys.stderr)
llms = _fetch(args.llms_txt)
seeds.extend(_extract_llms_urls(llms))
# Deduplicate while preserving order
seen: set[str] = set()
queue: list[str] = []
for u in seeds:
if u in seen:
continue
seen.add(u)
queue.append(u)
cfg = CrawlConfig(
allow_domains=tuple(args.allow_domains),
allow_prefixes=tuple(args.allow_prefixes),
delay_s=max(args.delay_s, 0.0),
max_pages=max(args.max_pages, 1),
)
index_path = out_dir / "index.jsonl"
fetched: set[str] = set()
pages = 0
with index_path.open("a", encoding="utf-8") as index_f:
while queue and pages < cfg.max_pages:
url = queue.pop(0)
if url in fetched:
continue
if not _allowed(url, cfg):
continue
try:
html = _fetch(url)
except Exception as e:
index_f.write(json.dumps({"url": url, "error": str(e)}) + "\n")
index_f.flush()
continue
filename = _safe_filename(url)
(out_dir / filename).write_text(html, encoding="utf-8")
index_f.write(json.dumps({"url": url, "file": filename, "sha256": _sha256(html)}) + "\n")
index_f.flush()
fetched.add(url)
pages += 1
for link in _extract_links(url, html):
if link in seen:
continue
seen.add(link)
queue.append(link)
if cfg.delay_s:
time.sleep(cfg.delay_s)
print(f"[done] Fetched {pages} pages into: {out_dir}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import re
import sys
import urllib.request
from pathlib import Path
LINK_RE = re.compile(r"\((https?://[^)\\s]+)\)")
def fetch(url: str, *, timeout_s: int = 20) -> str:
req = urllib.request.Request(url, headers={"User-Agent": "langgraph-multiagent-skill/1.0"})
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
return resp.read().decode("utf-8", errors="replace")
def extract_urls(text: str) -> list[str]:
return [m.group(1) for m in LINK_RE.finditer(text)]
def main() -> int:
parser = argparse.ArgumentParser(description="Fetch LangGraph llms.txt and extract all documentation URLs.")
parser.add_argument(
"--url",
default="https://langchain-ai.github.io/langgraph/llms.txt",
help="llms.txt URL to fetch (default: LangGraph llms.txt).",
)
parser.add_argument("--out", default=None, help="Write URLs to a file (one per line).")
parser.add_argument("--print", dest="do_print", action="store_true", help="Print URLs to stdout.")
parser.add_argument("--unique", action="store_true", help="Deduplicate URLs while preserving order.")
args = parser.parse_args()
raw = fetch(args.url)
urls = extract_urls(raw)
if args.unique:
seen: set[str] = set()
deduped: list[str] = []
for u in urls:
if u in seen:
continue
seen.add(u)
deduped.append(u)
urls = deduped
if args.out:
out_path = Path(args.out)
out_path.write_text("\n".join(urls) + "\n", encoding="utf-8")
print(f"Wrote: {out_path}", file=sys.stderr)
if args.do_print or not args.out:
for u in urls:
print(u)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Related skills
FAQ
Can this skill migrate deprecated agent patterns?
Yes, it migrates off patterns like langgraph.prebuilt.create_react_agent and libraries like langgraph-supervisor, CrewAI, and OpenAI Agents.
How does it stay version-accurate?
It resolves current APIs from docs and installed versions and falls back to opensrc source snapshots for edge cases.