{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "langgraph-agent",
  "title": "LangGraph Agent",
  "description": "A Next.js AI Elements frontend and proxy route for a separately running Python LangGraph backend.",
  "dependencies": [
    "@ai-sdk/react",
    "@base-ui/react",
    "ai",
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "card",
    "separator",
    "textarea",
    "tooltip",
    "https://elements.ai-sdk.dev/api/registry/conversation.json",
    "https://elements.ai-sdk.dev/api/registry/message.json",
    "https://elements.ai-sdk.dev/api/registry/reasoning.json",
    "https://elements.ai-sdk.dev/api/registry/shimmer.json"
  ],
  "files": [
    {
      "path": "registry/langgraph-agent/app/demos/langgraph-agent/page.tsx",
      "content": "import { LangGraphAgentScreen } from \"@/components/langgraph-agent/langgraph-agent-screen\";\n\nexport const dynamic = \"force-dynamic\";\n\nexport default function LangGraphAgentPage() {\n  return <LangGraphAgentScreen />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/langgraph-agent/page.tsx"
    },
    {
      "path": "registry/langgraph-agent/app/api/demos/langgraph-agent/route.ts",
      "content": "import { handleLangGraphAgentRequest } from \"@/lib/langgraph-agent/server/runtime\";\n\n// This registry item installs the Next.js frontend and proxy route only.\n// Run the Python LangGraph backend separately from:\n// https://github.com/SawanaLabs/agent-demos/tree/main/apps/langgraph-agent-api\n//\n// Local-first path: run that backend on http://localhost:2024, then set\n// LANGGRAPH_AGENT_API_URL=http://localhost:2024,\n// LANGGRAPH_AGENT_ASSISTANT_ID=agent, and LANGGRAPH_AGENT_API_KEY to the same\n// shared service key accepted by the backend.\n//\n// Vercel path: deploy apps/langgraph-agent-api as a FastAPI project using its\n// app.py and vercel.json, then point LANGGRAPH_AGENT_API_URL at that deployment.\n//\n// Official refs:\n// - https://docs.langchain.com/langgraph-platform/local-server\n// - https://docs.langchain.com/langgraph-platform/cli\n// - https://docs.langchain.com/oss/python/langgraph/streaming\n// - https://vercel.com/docs/frameworks/backend/fastapi\n\nexport const runtime = \"nodejs\";\n\nexport async function POST(request: Request) {\n  return handleLangGraphAgentRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/langgraph-agent/route.ts"
    },
    {
      "path": "registry/langgraph-agent/components/langgraph-agent/langgraph-agent-screen.tsx",
      "content": "import { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { DemoWorkspaceShell } from \"@/components/demo-workspace-shell\";\n\nimport { getLangGraphAgentRuntimeState } from \"@/lib/langgraph-agent/server/runtime\";\nimport { LangGraphAgentWorkspace } from \"@/components/langgraph-agent/langgraph-agent-workspace\";\n\nexport function LangGraphAgentScreen() {\n  const runtimeState = getLangGraphAgentRuntimeState();\n\n  return (\n    <TooltipProvider>\n      <DemoWorkspaceShell\n        badges={[\n          runtimeState.statusLabel,\n          runtimeState.modelName,\n          runtimeState.assistantId ?? \"assistant id missing\",\n        ]}\n        breadcrumbClassName=\"font-heading text-xs tracking-[0.16em]\"\n        breadcrumbTitle=\"LangGraph Agent\"\n        headerFrame=\"card\"\n        summary=\"This demo validates the copyable frontend slice for teams that already run Python LangGraph agents and want a full-stack product surface.\"\n        title=\"Official LangGraph thread streaming in a Next.js AI Elements workspace\"\n      >\n        <LangGraphAgentWorkspace\n          assistantId={runtimeState.assistantId}\n          isChatAvailable={runtimeState.isChatAvailable}\n          nodeVersion={runtimeState.nodeVersion}\n          remoteUrl={runtimeState.remoteUrl}\n          setupMessage={runtimeState.setupMessage}\n        />\n      </DemoWorkspaceShell>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/langgraph-agent/langgraph-agent-screen.tsx"
    },
    {
      "path": "registry/langgraph-agent/components/langgraph-agent/langgraph-agent-workspace.tsx",
      "content": "\"use client\";\n\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationEmptyState,\n  ConversationScrollButton,\n} from \"@/components/ai-elements/conversation\";\nimport {\n  Message,\n  MessageContent,\n  MessageResponse,\n} from \"@/components/ai-elements/message\";\nimport {\n  PromptInput,\n  PromptInputBody,\n  PromptInputFooter,\n  PromptInputSubmit,\n  PromptInputTextarea,\n} from \"@/components/ai-elements/prompt-input\";\nimport {\n  Reasoning,\n  ReasoningContent,\n  ReasoningTrigger,\n} from \"@/components/ai-elements/reasoning\";\nimport { Shimmer } from \"@/components/ai-elements/shimmer\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  CircleCheckIcon,\n  CircleDotDashedIcon,\n  GitBranchIcon,\n  PlusIcon,\n  RefreshCwIcon,\n  SquareIcon,\n} from \"lucide-react\";\n\nimport {\n  ConversationErrorMessage,\n  useConversationErrorRetry,\n} from \"@/components/demo-chat/conversation-error-message\";\nimport type { LangGraphProgressData } from \"@/lib/langgraph-agent/server/stream-normalizer\";\nimport { getLangGraphThinkingText } from \"./langgraph-agent-message-parts\";\nimport {\n  type LangGraphAgentMessage,\n  useLangGraphAgent,\n} from \"./use-langgraph-agent\";\n\nconst langGraphAgentSamplePrompts = [\n  \"Plan a safe LangGraph handoff from product research to implementation.\",\n  \"Explain how this thread streams LangGraph node updates into AI SDK UI messages.\",\n  \"Validate the minimum environment setup for running this LangGraph demo locally.\",\n] as const;\n\nfunction getTextContent(message: LangGraphAgentMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\");\n}\n\nfunction getGraphNodeSummary(events: LangGraphProgressData[]) {\n  const latestByNode = new Map<string, LangGraphProgressData>();\n\n  for (const event of events) {\n    latestByNode.set(event.node, event);\n  }\n\n  return Array.from(latestByNode.values());\n}\n\nfunction formatStateSummary(state: unknown) {\n  if (typeof state === \"string\") {\n    return state;\n  }\n\n  if (typeof state === \"object\" && state !== null) {\n    return JSON.stringify(state);\n  }\n\n  return \"Stream event received\";\n}\n\nfunction LangGraphAssistantTrace({\n  events,\n  isLastMessage,\n  isStreaming,\n  message,\n}: {\n  events: LangGraphProgressData[];\n  isLastMessage: boolean;\n  isStreaming: boolean;\n  message: LangGraphAgentMessage;\n}) {\n  const text = getTextContent(message);\n  const thinkingText = isLastMessage ? getLangGraphThinkingText(events) : \"\";\n  const showThinking =\n    thinkingText.length > 0 || (isLastMessage && isStreaming && !text);\n\n  return (\n    <>\n      {showThinking ? (\n        <Reasoning\n          className=\"w-full\"\n          defaultOpen={isLastMessage && isStreaming}\n          isStreaming={isLastMessage && isStreaming}\n        >\n          <ReasoningTrigger\n            getThinkingMessage={(streaming: boolean, duration?: number) => {\n              if (streaming || duration === 0) {\n                return (\n                  <Shimmer duration={1}>Thinking through LangGraph...</Shimmer>\n                );\n              }\n\n              if (duration === undefined) {\n                return <p>LangGraph thinking</p>;\n              }\n\n              return <p>LangGraph thought for {duration} seconds</p>;\n            }}\n          />\n          <ReasoningContent>\n            {thinkingText || \"- Start run: Waiting for LangGraph node updates.\"}\n          </ReasoningContent>\n        </Reasoning>\n      ) : null}\n\n      {text ? (\n        <MessageResponse>{text}</MessageResponse>\n      ) : (\n        <Shimmer className=\"text-sm\">Waiting for LangGraph output.</Shimmer>\n      )}\n    </>\n  );\n}\n\nfunction GraphProgressPanel({\n  events,\n  threadId,\n}: {\n  events: LangGraphProgressData[];\n  threadId: string | null;\n}) {\n  const nodeSummary = getGraphNodeSummary(events);\n  const threadLabel = threadId ?? \"Preparing thread...\";\n\n  return (\n    <div className=\"space-y-4\">\n      <div>\n        <p className=\"font-heading text-muted-foreground text-xs uppercase tracking-[0.16em]\">\n          LangGraph thread\n        </p>\n        <p className=\"mt-1 break-all font-mono text-sm\">{threadLabel}</p>\n      </div>\n\n      <div>\n        <p className=\"font-heading text-muted-foreground text-xs uppercase tracking-[0.16em]\">\n          Graph progress\n        </p>\n        <div className=\"mt-2 space-y-2\">\n          {nodeSummary.length > 0 ? (\n            nodeSummary.map((event) => {\n              const isStreaming = event.status === \"streaming\";\n\n              return (\n                <div\n                  className=\"rounded-md border bg-muted/20 px-3 py-2\"\n                  key={`${event.kind}-${event.node}`}\n                >\n                  <div className=\"flex items-center justify-between gap-3\">\n                    <div className=\"flex min-w-0 items-center gap-2\">\n                      {isStreaming ? (\n                        <CircleDotDashedIcon className=\"size-4 shrink-0 text-emerald-600\" />\n                      ) : (\n                        <CircleCheckIcon className=\"size-4 shrink-0 text-emerald-600\" />\n                      )}\n                      <span className=\"truncate font-medium text-sm\">\n                        {event.node}\n                      </span>\n                    </div>\n                    <Badge className=\"shrink-0\" variant=\"outline\">\n                      {event.source}\n                    </Badge>\n                  </div>\n                  {event.kind === \"node-update\" ? (\n                    <p className=\"mt-2 line-clamp-2 break-words text-muted-foreground text-xs\">\n                      {formatStateSummary(event.state)}\n                    </p>\n                  ) : (\n                    <p className=\"mt-2 text-muted-foreground text-xs\">\n                      Streaming tokens from run {event.runId ?? \"unknown\"}\n                    </p>\n                  )}\n                </div>\n              );\n            })\n          ) : (\n            <p className=\"text-muted-foreground text-sm\">\n              Submit a message to watch LangGraph updates and streamed answer\n              tokens land as AI SDK data parts.\n            </p>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport interface LangGraphAgentWorkspaceProps {\n  assistantId: string | null;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  remoteUrl: string | null;\n  setupMessage: string | null;\n}\n\nexport function LangGraphAgentWorkspace({\n  assistantId,\n  isChatAvailable,\n  nodeVersion,\n  remoteUrl,\n  setupMessage,\n}: LangGraphAgentWorkspaceProps) {\n  const {\n    clearError,\n    error,\n    graphEvents,\n    hasMessages,\n    isBusy,\n    messages,\n    regenerate,\n    sendMessage,\n    startNewThread,\n    status,\n    stop,\n    threadId,\n  } = useLangGraphAgent();\n  const retryConversationError = useConversationErrorRetry({\n    clearError,\n    regenerate,\n  });\n  const canStartChatTurn = isChatAvailable && !isBusy;\n  const emptyStateDescription = isChatAvailable\n    ? \"Ask the remote LangGraph agent to explain, plan, or validate a product-agent integration path.\"\n    : \"Set LANGGRAPH_AGENT_API_URL and LANGGRAPH_AGENT_ASSISTANT_ID before sending a chat turn.\";\n  const emptyStateTitle = isChatAvailable\n    ? \"LangGraph thread is ready\"\n    : \"LangGraph setup is required\";\n\n  function sendChatMessage(text: string) {\n    if (!canStartChatTurn) {\n      return;\n    }\n\n    sendMessage({ text });\n  }\n\n  function regenerateChatTurn() {\n    if (!canStartChatTurn) {\n      return;\n    }\n\n    regenerate();\n  }\n\n  return (\n    <div className=\"grid min-h-[70svh] gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n      <section className=\"flex min-h-[70svh] flex-col overflow-hidden border border-foreground/10 bg-background text-base text-foreground leading-normal lg:h-full lg:min-h-0\">\n        {isChatAvailable ? null : (\n          <>\n            <div className=\"px-4 py-3 text-muted-foreground text-xs/relaxed\">\n              {setupMessage}\n            </div>\n            <Separator />\n          </>\n        )}\n\n        <Conversation>\n          <ConversationContent className=\"mx-auto flex w-full max-w-3xl flex-1 gap-6 px-4 py-6\">\n            {hasMessages || error ? (\n              <>\n                {messages.map((message, index) => {\n                  const text = getTextContent(message);\n                  let messageBody = (\n                    <p className=\"text-muted-foreground text-sm\">\n                      Waiting for LangGraph output.\n                    </p>\n                  );\n\n                  if (message.role === \"assistant\") {\n                    messageBody = (\n                      <LangGraphAssistantTrace\n                        events={graphEvents}\n                        isLastMessage={index === messages.length - 1}\n                        isStreaming={isBusy}\n                        message={message}\n                      />\n                    );\n                  } else if (text) {\n                    messageBody = <MessageResponse>{text}</MessageResponse>;\n                  }\n\n                  return (\n                    <Message from={message.role} key={message.id}>\n                      <MessageContent\n                        className={cn(\n                          \"space-y-4\",\n                          message.role === \"assistant\"\n                            ? \"max-w-3xl\"\n                            : \"max-w-2xl\"\n                        )}\n                      >\n                        {messageBody}\n                      </MessageContent>\n                    </Message>\n                  );\n                })}\n                {error ? (\n                  <ConversationErrorMessage\n                    error={error}\n                    isRetryDisabled={isBusy || !isChatAvailable}\n                    onRetry={retryConversationError}\n                  />\n                ) : null}\n              </>\n            ) : (\n              <ConversationEmptyState\n                description={emptyStateDescription}\n                icon={<GitBranchIcon className=\"size-5\" />}\n                title={emptyStateTitle}\n              />\n            )}\n          </ConversationContent>\n          <ConversationScrollButton />\n        </Conversation>\n\n        <Separator />\n        <div className=\"px-4 py-4\">\n          <div className=\"mx-auto w-full max-w-3xl\">\n            <PromptInput onSubmit={({ text }) => sendChatMessage(text)}>\n              <PromptInputBody>\n                <PromptInputTextarea\n                  disabled={!canStartChatTurn}\n                  placeholder=\"Ask the LangGraph agent to reason through an integration or implementation question.\"\n                />\n              </PromptInputBody>\n              <Separator className=\"mt-3\" />\n              <PromptInputFooter className=\"flex items-center justify-between gap-3 px-3 py-3\">\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <Badge variant=\"outline\">LangGraph</Badge>\n                  <Badge variant=\"outline\">AI SDK stream</Badge>\n                  <Badge variant=\"outline\">thread scoped</Badge>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  <Button\n                    disabled={isBusy}\n                    onClick={startNewThread}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    <PlusIcon className=\"size-3.5\" />\n                    New thread\n                  </Button>\n                  {isBusy ? (\n                    <Button\n                      onClick={stop}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <SquareIcon className=\"size-3.5\" />\n                      Stop\n                    </Button>\n                  ) : null}\n                  {hasMessages ? (\n                    <Button\n                      disabled={!canStartChatTurn}\n                      onClick={regenerateChatTurn}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <RefreshCwIcon className=\"size-3.5\" />\n                      Retry\n                    </Button>\n                  ) : null}\n                  <PromptInputSubmit\n                    disabled={!canStartChatTurn}\n                    status={status}\n                  />\n                </div>\n              </PromptInputFooter>\n            </PromptInput>\n\n            {hasMessages ? null : (\n              <div className=\"mt-3 flex flex-wrap gap-2\">\n                {langGraphAgentSamplePrompts.map((prompt) => (\n                  <Button\n                    disabled={!canStartChatTurn}\n                    key={prompt}\n                    onClick={() => sendChatMessage(prompt)}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    <GitBranchIcon className=\"size-3.5\" />\n                    {prompt}\n                  </Button>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </section>\n\n      <aside className=\"min-h-0 overflow-y-auto border border-foreground/10 bg-background p-4 text-base text-foreground leading-normal\">\n        <div className=\"space-y-5\">\n          <div className=\"space-y-4\">\n            <div>\n              <p className=\"font-heading text-muted-foreground text-xs uppercase tracking-[0.16em]\">\n                Runtime\n              </p>\n              <p className=\"mt-1 font-medium text-sm\">{nodeVersion}</p>\n            </div>\n            <div>\n              <p className=\"font-heading text-muted-foreground text-xs uppercase tracking-[0.16em]\">\n                Remote API\n              </p>\n              <p className=\"mt-1 break-all text-muted-foreground text-sm\">\n                {remoteUrl ?? \"Missing LANGGRAPH_AGENT_API_URL\"}\n              </p>\n            </div>\n            <div>\n              <p className=\"font-heading text-muted-foreground text-xs uppercase tracking-[0.16em]\">\n                Assistant\n              </p>\n              <p className=\"mt-1 text-muted-foreground text-sm\">\n                {assistantId ?? \"Missing LANGGRAPH_AGENT_ASSISTANT_ID\"}\n              </p>\n            </div>\n          </div>\n\n          <Separator />\n\n          <GraphProgressPanel events={graphEvents} threadId={threadId} />\n        </div>\n      </aside>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/langgraph-agent/langgraph-agent-workspace.tsx"
    },
    {
      "path": "registry/langgraph-agent/components/langgraph-agent/langgraph-agent-message-parts.ts",
      "content": "import type { LangGraphProgressData } from \"@/lib/langgraph-agent/server/stream-normalizer\";\n\nconst nodeLabels: Record<string, string> = {\n  answer: \"Stream answer\",\n  plan: \"Plan response\",\n  route: \"Route request\",\n  synthesize: \"Prepare final context\",\n  tool: \"Inspect frontend contract\",\n};\n\nconst maxSummaryLength = 220;\n\nfunction truncateSummary(value: string) {\n  const normalized = value.replace(/\\s+/g, \" \").trim();\n\n  if (normalized.length <= maxSummaryLength) {\n    return normalized;\n  }\n\n  return `${normalized.slice(0, maxSummaryLength - 1)}...`;\n}\n\nfunction readStringField(state: unknown, field: string) {\n  if (typeof state !== \"object\" || state === null || Array.isArray(state)) {\n    return null;\n  }\n\n  const value = (state as Record<string, unknown>)[field];\n\n  return typeof value === \"string\" && value.trim().length > 0 ? value : null;\n}\n\nfunction readFirstObservation(state: unknown) {\n  if (typeof state !== \"object\" || state === null || Array.isArray(state)) {\n    return null;\n  }\n\n  const observations = (state as Record<string, unknown>).observations;\n\n  if (!Array.isArray(observations)) {\n    return null;\n  }\n\n  const firstObservation = observations.find(\n    (observation): observation is string =>\n      typeof observation === \"string\" && observation.trim().length > 0\n  );\n\n  return firstObservation ?? null;\n}\n\nfunction formatNodeUpdateSummary(\n  event: Extract<LangGraphProgressData, { kind: \"node-update\" }>\n) {\n  if (event.node === \"route\") {\n    return readStringField(event.state, \"route\") ?? \"Route selected.\";\n  }\n\n  if (event.node === \"tool\") {\n    return (\n      readFirstObservation(event.state) ?? \"No frontend contract lookup needed.\"\n    );\n  }\n\n  if (event.node === \"answer\") {\n    return \"Final answer completed.\";\n  }\n\n  if (event.node === \"plan\" || event.node === \"synthesize\") {\n    return readStringField(event.state, \"plan\") ?? \"Prepared run context.\";\n  }\n\n  if (typeof event.state === \"string\" && event.state.trim().length > 0) {\n    return event.state;\n  }\n\n  return \"Node completed.\";\n}\n\nfunction getLatestProgressByNode(events: LangGraphProgressData[]) {\n  const latestByNode = new Map<string, LangGraphProgressData>();\n\n  for (const event of events) {\n    latestByNode.set(event.node, event);\n  }\n\n  return Array.from(latestByNode.values());\n}\n\nexport function getLangGraphThinkingText(events: LangGraphProgressData[]) {\n  return getLatestProgressByNode(events)\n    .map((event) => {\n      const label = nodeLabels[event.node] ?? event.node;\n      const summary =\n        event.kind === \"node-token\"\n          ? \"Streaming answer tokens.\"\n          : formatNodeUpdateSummary(event);\n\n      return `- ${label}: ${truncateSummary(summary)}`;\n    })\n    .join(\"\\n\");\n}\n",
      "type": "registry:component",
      "target": "@components/langgraph-agent/langgraph-agent-message-parts.ts"
    },
    {
      "path": "registry/langgraph-agent/components/langgraph-agent/use-langgraph-agent.ts",
      "content": "\"use client\";\n\nimport { Chat, useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport, type UIMessage } from \"ai\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport type {\n  LangGraphAgentDataParts,\n  LangGraphProgressData,\n} from \"@/lib/langgraph-agent/server/stream-normalizer\";\n\nexport type LangGraphAgentMessage = UIMessage<unknown, LangGraphAgentDataParts>;\n\nexport function createLangGraphThreadId() {\n  return crypto.randomUUID();\n}\n\nfunction isGraphProgressDataPart(dataPart: unknown): dataPart is {\n  data: LangGraphProgressData;\n  type: \"data-graph-progress\";\n} {\n  return (\n    typeof dataPart === \"object\" &&\n    dataPart !== null &&\n    (dataPart as { type?: unknown }).type === \"data-graph-progress\"\n  );\n}\n\nexport function useLangGraphAgent() {\n  const threadIdRef = useRef<string | null>(null);\n  const [threadId, setThreadId] = useState<string | null>(null);\n  const ensureThreadId = useCallback(() => {\n    if (threadIdRef.current) {\n      return threadIdRef.current;\n    }\n\n    const nextThreadId = createLangGraphThreadId();\n    threadIdRef.current = nextThreadId;\n    setThreadId(nextThreadId);\n\n    return nextThreadId;\n  }, []);\n  const [graphEvents, setGraphEvents] = useState<LangGraphProgressData[]>([]);\n  /* eslint-disable react-hooks/refs -- Chat transport callbacks run after render and need the current thread id. */\n  const [chat] = useState(\n    () =>\n      new Chat<LangGraphAgentMessage>({\n        onData(dataPart) {\n          if (isGraphProgressDataPart(dataPart)) {\n            setGraphEvents((current) => [...current, dataPart.data]);\n          }\n        },\n        transport: new DefaultChatTransport<LangGraphAgentMessage>({\n          api: \"/api/demos/langgraph-agent\",\n          prepareSendMessagesRequest({ messages }) {\n            return {\n              body: {\n                messages,\n                threadId: ensureThreadId(),\n              },\n            };\n          },\n        }),\n      })\n  );\n  /* eslint-enable react-hooks/refs */\n  const controller = useChat({ chat });\n  const hasMessages = controller.messages.length > 0;\n  const isBusy =\n    controller.status === \"submitted\" || controller.status === \"streaming\";\n\n  useEffect(() => {\n    ensureThreadId();\n  }, [ensureThreadId]);\n\n  return {\n    ...controller,\n    graphEvents,\n    hasMessages,\n    isBusy,\n    startNewThread() {\n      const nextThreadId = createLangGraphThreadId();\n      threadIdRef.current = nextThreadId;\n      setThreadId(nextThreadId);\n      setGraphEvents([]);\n      controller.clearError();\n      controller.setMessages([]);\n    },\n    threadId,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/langgraph-agent/use-langgraph-agent.ts"
    },
    {
      "path": "registry/langgraph-agent/components/demo-chat/conversation-error-message.tsx",
      "content": "\"use client\";\n\nimport {\n  Message,\n  MessageContent,\n} from \"@/components/ai-elements/message\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { RefreshCcwIcon } from \"lucide-react\";\nimport { useCallback } from \"react\";\n\ninterface ConversationErrorMessageProps {\n  className?: string;\n  error: Error | string;\n  isRetryDisabled?: boolean;\n  onRetry?: () => Promise<void> | void;\n  retryLabel?: string;\n  title?: string;\n}\n\ninterface UseConversationErrorRetryInput {\n  clearError: () => void;\n  regenerate: () => Promise<void>;\n}\n\nexport function useConversationErrorRetry({\n  clearError,\n  regenerate,\n}: UseConversationErrorRetryInput) {\n  return useCallback(async () => {\n    clearError();\n    await regenerate();\n  }, [clearError, regenerate]);\n}\n\nexport function ConversationErrorMessage({\n  className,\n  error,\n  isRetryDisabled = false,\n  retryLabel = \"Retry\",\n  onRetry,\n  title = \"Assistant response failed\",\n}: ConversationErrorMessageProps) {\n  const errorMessage = typeof error === \"string\" ? error : error.message;\n\n  return (\n    <Message from=\"assistant\">\n      <MessageContent\n        className={cn(\n          \"max-w-3xl border border-destructive/25 bg-destructive/5 px-4 py-3 text-destructive\",\n          className\n        )}\n      >\n        <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n          <div className=\"min-w-0\">\n            <p className=\"font-medium text-sm\">{title}</p>\n            <p className=\"mt-1 break-words text-xs/relaxed opacity-90\">\n              {errorMessage}\n            </p>\n          </div>\n          {onRetry ? (\n            <Button\n              className=\"self-start\"\n              disabled={isRetryDisabled}\n              onClick={() => {\n                void onRetry();\n              }}\n              size=\"sm\"\n              type=\"button\"\n              variant=\"destructive\"\n            >\n              <RefreshCcwIcon className=\"size-3.5\" />\n              {retryLabel}\n            </Button>\n          ) : null}\n        </div>\n      </MessageContent>\n    </Message>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/demo-chat/conversation-error-message.tsx"
    },
    {
      "path": "registry/langgraph-agent/components/demo-workspace-shell.tsx",
      "content": "import { Badge } from \"@/components/ui/badge\";\nimport { Card } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\nimport type { ReactNode } from \"react\";\n\nimport { DemoBreadcrumb } from \"@/components/demo-breadcrumb\";\n\ntype DemoWorkspaceHeaderFrame = \"header\" | \"card\";\n\ninterface DemoWorkspaceShellProps {\n  badges?: readonly ReactNode[];\n  breadcrumbClassName?: string;\n  breadcrumbTitle?: string;\n  children: ReactNode;\n  contentClassName?: string;\n  headerClassName?: string;\n  headerFrame?: DemoWorkspaceHeaderFrame;\n  maxWidthClassName?: string;\n  summary: ReactNode;\n  summaryClassName?: string;\n  title: string;\n  titleClassName?: string;\n  workspaceClassName?: string | null;\n}\n\nconst defaultHeaderClassName =\n  \"grid gap-4 border border-foreground/10 bg-background px-4 py-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end\";\n\nconst cardHeaderClassName =\n  \"grid gap-4 bg-background px-4 py-5 text-base text-foreground leading-normal md:grid-cols-[minmax(0,1fr)_auto] md:items-end\";\n\nexport function DemoWorkspaceShell({\n  badges = [],\n  breadcrumbClassName,\n  breadcrumbTitle,\n  children,\n  contentClassName,\n  headerClassName,\n  headerFrame = \"header\",\n  maxWidthClassName = \"max-w-7xl\",\n  summary,\n  summaryClassName = \"max-w-3xl\",\n  title,\n  titleClassName = \"max-w-3xl\",\n  workspaceClassName = \"lg:h-svh\",\n}: DemoWorkspaceShellProps) {\n  const headerContent = (\n    <>\n      <div className=\"space-y-2\">\n        <DemoBreadcrumb\n          className={breadcrumbClassName}\n          title={breadcrumbTitle ?? title}\n        />\n        <h1\n          className={cn(\"font-medium text-2xl tracking-tight\", titleClassName)}\n        >\n          {title}\n        </h1>\n        <p\n          className={cn(\n            \"text-muted-foreground text-sm/relaxed\",\n            summaryClassName\n          )}\n        >\n          {summary}\n        </p>\n      </div>\n\n      {badges.length > 0 ? (\n        <div className=\"flex flex-wrap items-center gap-2\">\n          {badges.map((badge, index) => (\n            <Badge key={String(index)} variant=\"outline\">\n              {badge}\n            </Badge>\n          ))}\n        </div>\n      ) : null}\n    </>\n  );\n\n  return (\n    <main className=\"min-h-svh bg-background text-foreground\">\n      <div\n        className={cn(\n          \"mx-auto flex w-full flex-col gap-6 px-4 py-6 md:px-6\",\n          maxWidthClassName,\n          contentClassName\n        )}\n      >\n        {headerFrame === \"card\" ? (\n          <Card className={cn(cardHeaderClassName, headerClassName)}>\n            {headerContent}\n          </Card>\n        ) : (\n          <header className={cn(defaultHeaderClassName, headerClassName)}>\n            {headerContent}\n          </header>\n        )}\n\n        {workspaceClassName ? (\n          <div className={workspaceClassName}>{children}</div>\n        ) : (\n          children\n        )}\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/demo-workspace-shell.tsx"
    },
    {
      "path": "registry/langgraph-agent/components/demo-breadcrumb.tsx",
      "content": "import {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { cn } from \"@/lib/utils\";\nimport { ArrowLeft } from \"lucide-react\";\n\ninterface DemoBreadcrumbProps {\n  className?: string;\n  title: string;\n}\n\nexport function DemoBreadcrumb({ className, title }: DemoBreadcrumbProps) {\n  return (\n    <Breadcrumb>\n      <BreadcrumbList\n        className={cn(\n          \"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\",\n          className\n        )}\n      >\n        <BreadcrumbItem>\n          <BreadcrumbLink\n            aria-label=\"Back to demos\"\n            className=\"-ml-1 inline-flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground\"\n            href=\"/\"\n          >\n            <ArrowLeft aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n            <span>Demo</span>\n          </BreadcrumbLink>\n        </BreadcrumbItem>\n        <BreadcrumbSeparator className=\"text-muted-foreground\">\n          /\n        </BreadcrumbSeparator>\n        <BreadcrumbItem>\n          <BreadcrumbPage className=\"font-normal text-muted-foreground\">\n            {title}\n          </BreadcrumbPage>\n        </BreadcrumbItem>\n      </BreadcrumbList>\n    </Breadcrumb>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/demo-breadcrumb.tsx"
    },
    {
      "path": "registry/langgraph-agent/components/ai-elements/prompt-input.tsx",
      "content": "\"use client\";\n\nimport { CornerDownLeftIcon, LoaderCircleIcon, SquareIcon } from \"lucide-react\";\nimport type {\n  ComponentProps,\n  FormEvent,\n  HTMLAttributes,\n  ReactNode,\n  TextareaHTMLAttributes,\n} from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\ntype PromptStatus = \"error\" | \"ready\" | \"streaming\" | \"submitted\";\n\ninterface PromptInputContextValue {\n  setText: (text: string) => void;\n  text: string;\n}\n\ninterface PromptInputMessage {\n  text: string;\n}\n\ninterface PromptInputProps\n  extends Omit<HTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  children: ReactNode;\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>;\n}\n\ninterface PromptInputSubmitProps\n  extends Omit<ComponentProps<typeof Button>, \"children\" | \"type\"> {\n  onStop?: () => void;\n  status?: PromptStatus;\n}\n\ntype PromptInputClickEvent = Parameters<\n  Exclude<ComponentProps<typeof Button>[\"onClick\"], undefined>\n>[0];\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInputContext() {\n  const context = useContext(PromptInputContext);\n\n  if (!context) {\n    throw new Error(\n      \"PromptInput components must be used inside <PromptInput>.\"\n    );\n  }\n\n  return context;\n}\n\nexport function PromptInput({\n  children,\n  className,\n  onSubmit,\n  ...props\n}: PromptInputProps) {\n  const [text, setText] = useState(\"\");\n\n  const contextValue = useMemo(\n    () => ({\n      setText,\n      text,\n    }),\n    [text]\n  );\n\n  const handleSubmit = useCallback(\n    async (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n\n      const nextText = text.trim();\n      if (!nextText) {\n        return;\n      }\n\n      const result = onSubmit({ text: nextText }, event);\n\n      try {\n        await result;\n        setText(\"\");\n      } catch {\n        // Keep the text for retry after a failed submit.\n      }\n    },\n    [onSubmit, text]\n  );\n\n  return (\n    <PromptInputContext.Provider value={contextValue}>\n      <form\n        className={cn(\"w-full\", className)}\n        onSubmit={handleSubmit}\n        {...props}\n      >\n        {children}\n      </form>\n    </PromptInputContext.Provider>\n  );\n}\n\nexport function PromptInputBody({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nexport function PromptInputFooter({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(className)} {...props} />;\n}\n\nexport function PromptInputTextarea({\n  className,\n  disabled,\n  onChange,\n  ...props\n}: TextareaHTMLAttributes<HTMLTextAreaElement>) {\n  const context = usePromptInputContext();\n\n  return (\n    <Textarea\n      className={cn(\"min-h-16 resize-none\", className)}\n      disabled={disabled}\n      onChange={(event) => {\n        context.setText(event.currentTarget.value);\n        onChange?.(event);\n      }}\n      value={context.text}\n      {...props}\n    />\n  );\n}\n\nexport function PromptInputSubmit({\n  className,\n  disabled,\n  onClick,\n  onStop,\n  size = \"icon\",\n  status = \"ready\",\n  variant = \"default\",\n  ...props\n}: PromptInputSubmitProps) {\n  const { text } = usePromptInputContext();\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n  const isDisabled = Boolean(disabled) || (!isBusy && text.trim().length === 0);\n\n  let icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === \"submitted\") {\n    icon = <LoaderCircleIcon className=\"size-4 animate-spin\" />;\n  } else if (status === \"streaming\") {\n    icon = <SquareIcon className=\"size-4\" />;\n  }\n\n  return (\n    <Button\n      aria-label={isBusy ? \"Stop\" : \"Submit\"}\n      className={cn(className)}\n      disabled={isDisabled}\n      onClick={(event: PromptInputClickEvent) => {\n        if (isBusy && onStop) {\n          event.preventDefault();\n          onStop();\n          return;\n        }\n\n        onClick?.(event);\n      }}\n      size={size}\n      type={isBusy && onStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {icon}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai-elements/prompt-input.tsx"
    },
    {
      "path": "registry/langgraph-agent/components/ai-elements/shimmer.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { CSSProperties } from \"react\";\nimport { memo, useMemo } from \"react\";\nimport { motion } from \"motion/react\";\n\nexport interface TextShimmerProps {\n  children: string;\n  as?: \"div\" | \"p\" | \"span\";\n  className?: string;\n  duration?: number;\n  spread?: number;\n}\n\nfunction getMotionProps({\n  children,\n  className,\n  duration,\n  spread,\n}: Required<Pick<TextShimmerProps, \"children\" | \"duration\" | \"spread\">> & {\n  className?: string;\n}) {\n  return {\n    animate: { backgroundPosition: \"0% center\" },\n    className: cn(\n      \"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent\",\n      \"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]\",\n      className\n    ),\n    initial: { backgroundPosition: \"100% center\" },\n    style: {\n      \"--spread\": `${children.length * spread}px`,\n      backgroundImage:\n        \"var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))\",\n    } as CSSProperties,\n    transition: {\n      duration,\n      ease: \"linear\" as const,\n      repeat: Number.POSITIVE_INFINITY,\n    },\n  };\n}\n\nconst ShimmerComponent = ({\n  children,\n  as = \"p\",\n  className,\n  duration = 2,\n  spread = 2,\n}: TextShimmerProps) => {\n  const motionProps = useMemo(\n    () => getMotionProps({ children, className, duration, spread }),\n    [children, className, duration, spread]\n  );\n\n  if (as === \"span\") {\n    return <motion.span {...motionProps}>{children}</motion.span>;\n  }\n\n  if (as === \"div\") {\n    return <motion.div {...motionProps}>{children}</motion.div>;\n  }\n\n  return <motion.p {...motionProps}>{children}</motion.p>;\n};\n\nexport const Shimmer = memo(ShimmerComponent);\n",
      "type": "registry:component",
      "target": "@components/ai-elements/shimmer.tsx"
    },
    {
      "path": "registry/langgraph-agent/lib/ai-gateway/contract.ts",
      "content": "import { createGateway } from \"ai\";\n\nexport const DEFAULT_GATEWAY_BASE_URL = \"https://ai-gateway.vercel.sh/v3/ai\";\nexport const MINIMUM_NODE_VERSION = \"22.13.0\";\nconst nodeVersionPattern = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$/;\n\nexport type AiGatewayEnvRecord = Record<string, string | undefined>;\n\nexport interface ParsedNodeVersion {\n  major: number;\n  minor: number;\n  patch: number;\n}\n\nexport interface AiGatewayContractConfig {\n  apiKey: string;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayResolvedEnv {\n  apiKey: string | undefined;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewaySetupConfig {\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  config: TConfig;\n  isReady: boolean;\n  issues: string[];\n  nodeVersion: string;\n}\n\nexport interface AiGatewayContractOptions<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  buildConfig?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => TConfig;\n  defaultBaseURL?: string;\n  defaultChatModel: string;\n  getAdditionalIssues?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => string[];\n  missingApiKeyError: string;\n  missingApiKeyIssue?: string;\n}\n\nconst genericMissingApiKeyIssue =\n  \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\";\n\nexport function parseNodeVersion(version: string): ParsedNodeVersion {\n  const match = nodeVersionPattern.exec(version);\n  const major = Number(match?.[1]);\n  const minor = Number(match?.[2]);\n  const patch = Number(match?.[3]);\n\n  if (![major, minor, patch].every(Number.isInteger)) {\n    throw new Error(`Unable to parse Node.js version: \"${version}\".`);\n  }\n\n  return { major, minor, patch };\n}\n\nexport function getNodeMajor(version: string): number {\n  return parseNodeVersion(version).major;\n}\n\nfunction compareNodeVersions(\n  left: ParsedNodeVersion,\n  right: ParsedNodeVersion\n): number {\n  if (left.major !== right.major) {\n    return left.major - right.major;\n  }\n\n  if (left.minor !== right.minor) {\n    return left.minor - right.minor;\n  }\n\n  return left.patch - right.patch;\n}\n\nexport function assertSupportedNodeRuntime(version = process.version): number {\n  const parsedVersion = parseNodeVersion(version);\n  const minimumVersion = parseNodeVersion(MINIMUM_NODE_VERSION);\n\n  if (compareNodeVersions(parsedVersion, minimumVersion) < 0) {\n    throw new Error(\n      `Node.js ${version} is unsupported. This demo workspace requires Node.js >=${MINIMUM_NODE_VERSION}.`\n    );\n  }\n\n  return parsedVersion.major;\n}\n\nexport function resolveAiGatewayContractEnv(\n  env: AiGatewayEnvRecord,\n  options: Pick<AiGatewayContractOptions, \"defaultBaseURL\" | \"defaultChatModel\">\n): AiGatewayResolvedEnv {\n  return {\n    apiKey: env.AI_GATEWAY_API_KEY,\n    baseURL:\n      env.AI_GATEWAY_BASE_URL ||\n      options.defaultBaseURL ||\n      DEFAULT_GATEWAY_BASE_URL,\n    chatModel: env.AI_GATEWAY_CHAT_MODEL || options.defaultChatModel,\n  };\n}\n\nexport function readAiGatewayContractConfig(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): AiGatewayContractConfig {\n  assertSupportedNodeRuntime();\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  if (!resolvedEnv.apiKey) {\n    throw new Error(options.missingApiKeyError);\n  }\n\n  return {\n    apiKey: resolvedEnv.apiKey,\n    baseURL: resolvedEnv.baseURL,\n    chatModel: resolvedEnv.chatModel,\n  };\n}\n\nexport function buildAiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig,\n>(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions<TConfig>\n): AiGatewayContractSetupState<TConfig> {\n  const issues: string[] = [];\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  try {\n    assertSupportedNodeRuntime();\n  } catch (error) {\n    issues.push(\n      error instanceof Error ? error.message : \"Unsupported Node.js runtime.\"\n    );\n  }\n\n  if (!resolvedEnv.apiKey) {\n    issues.push(options.missingApiKeyIssue || genericMissingApiKeyIssue);\n  }\n\n  issues.push(...(options.getAdditionalIssues?.(resolvedEnv, env) ?? []));\n\n  return {\n    config:\n      options.buildConfig?.(resolvedEnv, env) ??\n      ({\n        baseURL: resolvedEnv.baseURL,\n        chatModel: resolvedEnv.chatModel,\n      } as TConfig),\n    isReady: issues.length === 0,\n    issues,\n    nodeVersion: process.version,\n  };\n}\n\nexport function createAiGatewayFromContract(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): ReturnType<typeof createGateway> {\n  const { apiKey, baseURL } = readAiGatewayContractConfig(env, options);\n\n  return createGateway({\n    apiKey,\n    baseURL,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/ai-gateway/contract.ts"
    },
    {
      "path": "registry/langgraph-agent/lib/langgraph-agent/server/env-source.ts",
      "content": "export function getLangGraphAgentAppEnv() {\n  // biome-ignore lint/style/noProcessEnv: Registry source installs into consumer apps without this repo's env wrapper.\n  return process.env;\n}\n",
      "type": "registry:lib",
      "target": "@lib/langgraph-agent/server/env-source.ts"
    },
    {
      "path": "registry/langgraph-agent/lib/langgraph-agent/server/env.ts",
      "content": "import { getLangGraphAgentAppEnv } from \"./env-source\";\n\nconst MINIMUM_NODE_VERSION = \"22.13.0\";\nexport const DEFAULT_LANGGRAPH_AGENT_MODEL = \"openai/gpt-5-mini\";\nconst nodeVersionPattern = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$/;\n\ninterface ParsedNodeVersion {\n  major: number;\n  minor: number;\n  patch: number;\n}\n\nexport type LangGraphAgentEnv = Record<string, string | undefined>;\n\nexport interface LangGraphAgentConfig {\n  apiKey: string;\n  assistantId: string;\n  baseUrl: string;\n  modelName: string;\n}\n\nexport interface LangGraphAgentSetupState {\n  config: Partial<LangGraphAgentConfig>;\n  isReady: boolean;\n  issues: string[];\n  nodeVersion: string;\n}\n\nfunction parseNodeVersion(version: string): ParsedNodeVersion {\n  const match = nodeVersionPattern.exec(version);\n  const major = Number(match?.[1]);\n  const minor = Number(match?.[2]);\n  const patch = Number(match?.[3]);\n\n  if (![major, minor, patch].every(Number.isInteger)) {\n    throw new Error(`Unable to parse Node.js version: \"${version}\".`);\n  }\n\n  return { major, minor, patch };\n}\n\nfunction compareNodeVersions(\n  left: ParsedNodeVersion,\n  right: ParsedNodeVersion\n) {\n  if (left.major !== right.major) {\n    return left.major - right.major;\n  }\n\n  if (left.minor !== right.minor) {\n    return left.minor - right.minor;\n  }\n\n  return left.patch - right.patch;\n}\n\nfunction assertSupportedNodeRuntime(version = process.version) {\n  const parsedVersion = parseNodeVersion(version);\n  const minimumVersion = parseNodeVersion(MINIMUM_NODE_VERSION);\n\n  if (compareNodeVersions(parsedVersion, minimumVersion) < 0) {\n    throw new Error(\n      `Node.js ${version} is unsupported. This demo workspace requires Node.js >=${MINIMUM_NODE_VERSION}.`\n    );\n  }\n}\n\nfunction readRequiredEnv(\n  env: LangGraphAgentEnv,\n  name: keyof LangGraphAgentEnv\n) {\n  const value = env[name];\n\n  if (!value) {\n    throw new Error(\n      `Missing ${name}. Add it to .env.local before using the LangGraph agent demo.`\n    );\n  }\n\n  return value;\n}\n\nexport function getLangGraphAgentEnv(): LangGraphAgentEnv {\n  return getLangGraphAgentAppEnv();\n}\n\nexport function getLangGraphAgentConfig(\n  env: LangGraphAgentEnv = getLangGraphAgentEnv()\n): LangGraphAgentConfig {\n  assertSupportedNodeRuntime();\n\n  return {\n    apiKey: readRequiredEnv(env, \"LANGGRAPH_AGENT_API_KEY\"),\n    assistantId: readRequiredEnv(env, \"LANGGRAPH_AGENT_ASSISTANT_ID\"),\n    baseUrl: readRequiredEnv(env, \"LANGGRAPH_AGENT_API_URL\"),\n    modelName: env.LANGGRAPH_AGENT_MODEL ?? DEFAULT_LANGGRAPH_AGENT_MODEL,\n  };\n}\n\nexport function getLangGraphAgentSetupState(\n  env: LangGraphAgentEnv = getLangGraphAgentEnv()\n): LangGraphAgentSetupState {\n  const issues: string[] = [];\n\n  try {\n    assertSupportedNodeRuntime();\n  } catch (error) {\n    issues.push(\n      error instanceof Error ? error.message : \"Unsupported Node.js runtime.\"\n    );\n  }\n\n  if (!env.LANGGRAPH_AGENT_API_URL) {\n    issues.push(\n      \"LANGGRAPH_AGENT_API_URL is missing. Point it at a LangGraph Agent Server before using this demo.\"\n    );\n  }\n\n  if (!env.LANGGRAPH_AGENT_API_KEY) {\n    issues.push(\n      \"LANGGRAPH_AGENT_API_KEY is missing. Set it to the shared service key used by the LangGraph agent API.\"\n    );\n  }\n\n  if (!env.LANGGRAPH_AGENT_ASSISTANT_ID) {\n    issues.push(\n      \"LANGGRAPH_AGENT_ASSISTANT_ID is missing. Set it to the graph id exposed by langgraph.json or your LangGraph deployment.\"\n    );\n  }\n\n  return {\n    config: {\n      apiKey: env.LANGGRAPH_AGENT_API_KEY,\n      assistantId: env.LANGGRAPH_AGENT_ASSISTANT_ID,\n      baseUrl: env.LANGGRAPH_AGENT_API_URL,\n      modelName: env.LANGGRAPH_AGENT_MODEL ?? DEFAULT_LANGGRAPH_AGENT_MODEL,\n    },\n    isReady: issues.length === 0,\n    issues,\n    nodeVersion: process.version,\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/langgraph-agent/server/env.ts"
    },
    {
      "path": "registry/langgraph-agent/lib/langgraph-agent/server/remote-langgraph-client.ts",
      "content": "import type { UIMessage } from \"ai\";\n\ntype FetchLike = typeof fetch;\n\nexport interface RemoteLangGraphClientOptions {\n  apiKey: string;\n  baseUrl: string;\n  fetch?: FetchLike;\n}\n\nexport interface StreamThreadRunOptions {\n  assistantId: string;\n  messages: UIMessage[];\n  streamMode?: Array<\"updates\" | \"messages-tuple\">;\n  threadId: string;\n}\n\ninterface LangGraphMessage {\n  content: string;\n  role: \"ai\" | \"human\" | \"system\";\n}\n\nconst defaultStreamMode: Array<\"updates\" | \"messages-tuple\"> = [\n  \"updates\",\n  \"messages-tuple\",\n];\nconst trailingSlashPattern = /\\/+$/;\n\nfunction normalizeBaseUrl(baseUrl: string) {\n  const trimmedBaseUrl = baseUrl.trim().replace(trailingSlashPattern, \"\");\n\n  if (!trimmedBaseUrl) {\n    throw new Error(\"LANGGRAPH_AGENT_API_URL is required.\");\n  }\n\n  return trimmedBaseUrl;\n}\n\nfunction requireNonEmpty(value: string, name: string) {\n  if (!value.trim()) {\n    throw new Error(`${name} is required.`);\n  }\n}\n\nfunction toLangGraphRole(role: UIMessage[\"role\"]): LangGraphMessage[\"role\"] {\n  if (role === \"user\") {\n    return \"human\";\n  }\n\n  if (role === \"assistant\") {\n    return \"ai\";\n  }\n\n  if (role === \"system\") {\n    return \"system\";\n  }\n\n  throw new Error(`Unsupported UI message role for LangGraph: ${role}`);\n}\n\nfunction readTextContent(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\");\n}\n\nfunction toLangGraphMessages(messages: UIMessage[]): LangGraphMessage[] {\n  return messages.map((message) => {\n    const content = readTextContent(message);\n\n    if (!content.trim()) {\n      throw new Error(`UI message ${message.id} has no text content.`);\n    }\n\n    return {\n      content,\n      role: toLangGraphRole(message.role),\n    };\n  });\n}\n\nexport function createRemoteLangGraphClient({\n  apiKey,\n  baseUrl,\n  fetch: fetchImpl = fetch,\n}: RemoteLangGraphClientOptions) {\n  const normalizedBaseUrl = normalizeBaseUrl(baseUrl);\n  const createJsonHeaders = () => ({\n    \"content-type\": \"application/json\",\n    \"x-api-key\": apiKey,\n  });\n\n  return {\n    async streamThreadRun({\n      assistantId,\n      messages,\n      streamMode = defaultStreamMode,\n      threadId,\n    }: StreamThreadRunOptions) {\n      requireNonEmpty(assistantId, \"LANGGRAPH_AGENT_ASSISTANT_ID\");\n      requireNonEmpty(threadId, \"threadId\");\n\n      const threadResponse = await fetchImpl(`${normalizedBaseUrl}/threads`, {\n        body: JSON.stringify({\n          if_exists: \"do_nothing\",\n          thread_id: threadId,\n        }),\n        headers: createJsonHeaders(),\n        method: \"POST\",\n      });\n\n      if (!threadResponse.ok) {\n        const responseText = await threadResponse.text();\n        throw new Error(\n          `LangGraph thread creation failed with ${threadResponse.status}: ${responseText}`\n        );\n      }\n\n      const response = await fetchImpl(\n        `${normalizedBaseUrl}/threads/${encodeURIComponent(threadId)}/runs/stream`,\n        {\n          body: JSON.stringify({\n            assistant_id: assistantId,\n            input: {\n              messages: toLangGraphMessages(messages),\n            },\n            stream_mode: streamMode,\n          }),\n          headers: createJsonHeaders(),\n          method: \"POST\",\n        }\n      );\n\n      if (!response.ok) {\n        const responseText = await response.text();\n        throw new Error(\n          `LangGraph thread run failed with ${response.status}: ${responseText}`\n        );\n      }\n\n      return response;\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/langgraph-agent/server/remote-langgraph-client.ts"
    },
    {
      "path": "registry/langgraph-agent/lib/langgraph-agent/server/runtime.ts",
      "content": "import {\n  createUIMessageStream,\n  createUIMessageStreamResponse,\n  type UIMessage,\n  validateUIMessages,\n} from \"ai\";\n\nimport {\n  DEFAULT_LANGGRAPH_AGENT_MODEL,\n  getLangGraphAgentConfig,\n  getLangGraphAgentEnv,\n  getLangGraphAgentSetupState,\n  type LangGraphAgentEnv,\n} from \"./env\";\nimport { createRemoteLangGraphClient } from \"./remote-langgraph-client\";\nimport { parseLangGraphSseStream } from \"./sse-parser\";\nimport { createLangGraphStreamNormalizer } from \"./stream-normalizer\";\n\ninterface LangGraphAgentRequestBody {\n  messages?: UIMessage[];\n  threadId?: string;\n}\n\nexport interface LangGraphAgentRuntimeState {\n  assistantId: string | null;\n  isChatAvailable: boolean;\n  modelName: string;\n  nodeVersion: string;\n  remoteUrl: string | null;\n  setupMessage: string | null;\n  statusLabel: \"Ready\" | \"Setup required\";\n}\n\nexport interface LangGraphAgentStreamInput {\n  messages: UIMessage[];\n  threadId: string;\n}\n\ninterface LangGraphAgentClient {\n  streamThreadRun: (\n    input: Parameters<\n      ReturnType<typeof createRemoteLangGraphClient>[\"streamThreadRun\"]\n    >[0]\n  ) => Promise<Response>;\n}\n\ninterface LangGraphAgentStreamDependencies {\n  client?: LangGraphAgentClient;\n}\n\ninterface LangGraphAgentRequestDependencies {\n  streamLangGraphAgent: (\n    input: LangGraphAgentStreamInput,\n    env: LangGraphAgentEnv\n  ) => Promise<Response> | Response;\n}\n\nconst invalidRequestError =\n  'Expected a JSON body with \"messages\" array and \"threadId\" string.';\nconst invalidUiMessagesError =\n  'Expected each \"messages\" entry to match the UIMessage format.';\n\nfunction readThreadId(body: LangGraphAgentRequestBody) {\n  if (typeof body.threadId !== \"string\" || !body.threadId.trim()) {\n    throw new Error(invalidRequestError);\n  }\n\n  return body.threadId;\n}\n\nasync function readLangGraphAgentInput(\n  body: unknown\n): Promise<LangGraphAgentStreamInput> {\n  const requestBody = (body ?? {}) as LangGraphAgentRequestBody;\n\n  if (!Array.isArray(requestBody.messages)) {\n    throw new Error(invalidRequestError);\n  }\n\n  try {\n    return {\n      messages: await validateUIMessages({\n        messages: requestBody.messages,\n      }),\n      threadId: readThreadId(requestBody),\n    };\n  } catch (error) {\n    if (error instanceof Error && error.message === invalidRequestError) {\n      throw error;\n    }\n\n    throw new Error(invalidUiMessagesError);\n  }\n}\n\nexport function getLangGraphAgentRuntimeState(\n  env: LangGraphAgentEnv = getLangGraphAgentEnv()\n): LangGraphAgentRuntimeState {\n  const setup = getLangGraphAgentSetupState(env);\n\n  return {\n    assistantId: setup.config.assistantId ?? null,\n    isChatAvailable: setup.isReady,\n    modelName: setup.config.modelName ?? DEFAULT_LANGGRAPH_AGENT_MODEL,\n    nodeVersion: setup.nodeVersion,\n    remoteUrl: setup.config.baseUrl ?? null,\n    setupMessage: setup.issues.length > 0 ? setup.issues.join(\" \") : null,\n    statusLabel: setup.isReady ? \"Ready\" : \"Setup required\",\n  };\n}\n\nexport async function streamLangGraphAgent(\n  input: LangGraphAgentStreamInput,\n  env: LangGraphAgentEnv = getLangGraphAgentEnv(),\n  dependencies: LangGraphAgentStreamDependencies = {}\n) {\n  const config = getLangGraphAgentConfig(env);\n  const client =\n    dependencies.client ??\n    createRemoteLangGraphClient({\n      apiKey: config.apiKey,\n      baseUrl: config.baseUrl,\n    });\n  const remoteResponse = await client.streamThreadRun({\n    assistantId: config.assistantId,\n    messages: input.messages,\n    threadId: input.threadId,\n  });\n\n  if (!remoteResponse.body) {\n    throw new Error(\"LangGraph stream response did not include a body.\");\n  }\n\n  const normalizer = createLangGraphStreamNormalizer();\n\n  return createUIMessageStreamResponse({\n    stream: createUIMessageStream({\n      async execute({ writer }) {\n        for await (const event of parseLangGraphSseStream(\n          remoteResponse.body as ReadableStream<Uint8Array>\n        )) {\n          for (const chunk of normalizer.normalize(event)) {\n            writer.write(chunk);\n          }\n        }\n\n        for (const chunk of normalizer.finish()) {\n          writer.write(chunk);\n        }\n      },\n      onError(error) {\n        return error instanceof Error\n          ? error.message\n          : \"LangGraph agent stream failed.\";\n      },\n    }),\n  });\n}\n\nexport async function handleLangGraphAgentRequest(\n  request: Request,\n  env: LangGraphAgentEnv = getLangGraphAgentEnv(),\n  dependencies: LangGraphAgentRequestDependencies = {\n    streamLangGraphAgent,\n  }\n) {\n  const runtimeState = getLangGraphAgentRuntimeState(env);\n\n  if (!runtimeState.isChatAvailable) {\n    return Response.json(\n      {\n        error: runtimeState.setupMessage,\n      },\n      { status: 500 }\n    );\n  }\n\n  try {\n    const input = await readLangGraphAgentInput(await request.json());\n\n    return dependencies.streamLangGraphAgent(input, env);\n  } catch (error) {\n    if (\n      error instanceof Error &&\n      [invalidRequestError, invalidUiMessagesError].includes(error.message)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    throw error;\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/langgraph-agent/server/runtime.ts"
    },
    {
      "path": "registry/langgraph-agent/lib/langgraph-agent/server/sse-parser.ts",
      "content": "import type { LangGraphStreamEvent } from \"./stream-normalizer\";\n\ninterface SseFrameFields {\n  dataLines: string[];\n  event?: string;\n}\n\nfunction readSseLine(line: string, fields: SseFrameFields) {\n  if (!line || line.startsWith(\":\")) {\n    return;\n  }\n\n  const separatorIndex = line.indexOf(\":\");\n  const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);\n  const rawValue = separatorIndex === -1 ? \"\" : line.slice(separatorIndex + 1);\n  const value = rawValue.startsWith(\" \") ? rawValue.slice(1) : rawValue;\n\n  if (field === \"event\") {\n    fields.event = value;\n    return;\n  }\n\n  if (field === \"data\") {\n    fields.dataLines.push(value);\n  }\n}\n\nfunction parseSseFrame(frame: string): LangGraphStreamEvent | null {\n  const fields: SseFrameFields = {\n    dataLines: [],\n  };\n\n  for (const line of frame.split(\"\\n\")) {\n    readSseLine(line, fields);\n  }\n\n  if (!fields.event && fields.dataLines.length === 0) {\n    return null;\n  }\n\n  const dataText = fields.dataLines.join(\"\\n\");\n\n  if (dataText === \"[DONE]\") {\n    return null;\n  }\n\n  if (dataText.length === 0) {\n    return {\n      event: fields.event,\n    };\n  }\n\n  try {\n    return {\n      data: JSON.parse(dataText) as unknown,\n      event: fields.event,\n    };\n  } catch (error) {\n    throw new Error(\n      `Unable to parse LangGraph SSE data as JSON for event \"${fields.event ?? \"message\"}\": ${\n        error instanceof Error ? error.message : \"unknown parse error\"\n      }`\n    );\n  }\n}\n\nexport async function* parseLangGraphSseStream(\n  stream: ReadableStream<Uint8Array>\n): AsyncGenerator<LangGraphStreamEvent> {\n  const decoder = new TextDecoder();\n  const reader = stream.getReader();\n  let buffer = \"\";\n\n  try {\n    while (true) {\n      const { done, value } = await reader.read();\n\n      if (done) {\n        break;\n      }\n\n      buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n/g, \"\\n\");\n\n      while (buffer.includes(\"\\n\\n\")) {\n        const frameEnd = buffer.indexOf(\"\\n\\n\");\n        const frame = buffer.slice(0, frameEnd);\n        buffer = buffer.slice(frameEnd + 2);\n        const event = parseSseFrame(frame);\n\n        if (event) {\n          yield event;\n        }\n      }\n    }\n\n    buffer += decoder.decode().replace(/\\r\\n/g, \"\\n\");\n\n    if (buffer.trim()) {\n      const event = parseSseFrame(buffer);\n\n      if (event) {\n        yield event;\n      }\n    }\n  } finally {\n    reader.releaseLock();\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/langgraph-agent/server/sse-parser.ts"
    },
    {
      "path": "registry/langgraph-agent/lib/langgraph-agent/server/stream-normalizer.ts",
      "content": "import type { UIMessageChunk } from \"ai\";\n\nexport type LangGraphProgressData =\n  | {\n      kind: \"node-token\";\n      node: string;\n      runId?: string;\n      source: \"messages\" | \"messages-tuple\";\n      status: \"streaming\";\n    }\n  | {\n      kind: \"node-update\";\n      node: string;\n      source: \"updates\";\n      state: unknown;\n      status: \"completed\";\n    };\n\nexport interface LangGraphAgentDataParts extends Record<string, unknown> {\n  \"graph-progress\": LangGraphProgressData;\n}\n\nexport type LangGraphAgentMessageChunk = UIMessageChunk<\n  unknown,\n  LangGraphAgentDataParts\n>;\n\nexport interface LangGraphStreamNormalizerOptions {\n  textPartId?: string;\n}\n\nexport interface LangGraphStreamEvent {\n  data?: unknown;\n  event?: string;\n  type?: string;\n}\n\ninterface MessageTupleMetadata {\n  langgraph_node?: unknown;\n  run_id?: unknown;\n}\n\ninterface MessageTupleChunk {\n  content?: unknown;\n}\n\nconst defaultTextPartId = \"langgraph-answer\";\n\nfunction getStreamEventType(event: LangGraphStreamEvent) {\n  return event.event ?? event.type;\n}\n\nfunction readMessageContent(chunk: unknown) {\n  if (typeof chunk === \"string\") {\n    return chunk;\n  }\n\n  if (typeof chunk !== \"object\" || chunk === null) {\n    return \"\";\n  }\n\n  const { content } = chunk as MessageTupleChunk;\n\n  return typeof content === \"string\" ? content : \"\";\n}\n\nfunction readNodeName(metadata: unknown) {\n  if (typeof metadata !== \"object\" || metadata === null) {\n    return \"unknown\";\n  }\n\n  const { langgraph_node } = metadata as MessageTupleMetadata;\n\n  return typeof langgraph_node === \"string\" && langgraph_node.length > 0\n    ? langgraph_node\n    : \"unknown\";\n}\n\nfunction readRunId(metadata: unknown) {\n  if (typeof metadata !== \"object\" || metadata === null) {\n    return;\n  }\n\n  const { run_id } = metadata as MessageTupleMetadata;\n\n  return typeof run_id === \"string\" && run_id.length > 0 ? run_id : undefined;\n}\n\nfunction normalizeMessageTuple(\n  event: LangGraphStreamEvent,\n  eventType: \"messages\" | \"messages-tuple\",\n  textPartId: string,\n  hasStartedText: boolean\n) {\n  if (!Array.isArray(event.data) || event.data.length < 2) {\n    throw new Error(`Expected ${eventType} data to be a message tuple.`);\n  }\n\n  const [messageChunk, metadata] = event.data;\n  const text = readMessageContent(messageChunk);\n  const node = readNodeName(metadata);\n  const chunks: LangGraphAgentMessageChunk[] = [\n    {\n      data: {\n        kind: \"node-token\",\n        node,\n        runId: readRunId(metadata),\n        source: eventType,\n        status: \"streaming\",\n      },\n      id: `node-${node}`,\n      transient: true,\n      type: \"data-graph-progress\",\n    },\n  ];\n\n  if (!text) {\n    return {\n      chunks,\n      hasStartedText,\n    };\n  }\n\n  if (!hasStartedText) {\n    chunks.push({\n      id: textPartId,\n      type: \"text-start\",\n    });\n  }\n\n  chunks.push({\n    delta: text,\n    id: textPartId,\n    type: \"text-delta\",\n  });\n\n  return {\n    chunks,\n    hasStartedText: true,\n  };\n}\n\nfunction normalizeUpdates(event: LangGraphStreamEvent) {\n  if (\n    typeof event.data !== \"object\" ||\n    event.data === null ||\n    Array.isArray(event.data)\n  ) {\n    throw new Error(\"Expected updates data to be an object keyed by node.\");\n  }\n\n  return Object.entries(event.data).map(\n    ([node, state]): LangGraphAgentMessageChunk => ({\n      data: {\n        kind: \"node-update\",\n        node,\n        source: \"updates\",\n        state,\n        status: \"completed\",\n      },\n      id: `node-${node}`,\n      transient: false,\n      type: \"data-graph-progress\",\n    })\n  );\n}\n\nexport function createLangGraphStreamNormalizer({\n  textPartId = defaultTextPartId,\n}: LangGraphStreamNormalizerOptions = {}) {\n  let hasStartedText = false;\n\n  return {\n    finish(): LangGraphAgentMessageChunk[] {\n      if (!hasStartedText) {\n        return [];\n      }\n\n      hasStartedText = false;\n\n      return [\n        {\n          id: textPartId,\n          type: \"text-end\",\n        },\n      ];\n    },\n    normalize(event: LangGraphStreamEvent): LangGraphAgentMessageChunk[] {\n      const eventType = getStreamEventType(event);\n\n      if (eventType === \"updates\") {\n        return normalizeUpdates(event);\n      }\n\n      if (eventType === \"messages\" || eventType === \"messages-tuple\") {\n        const result = normalizeMessageTuple(\n          event,\n          eventType,\n          textPartId,\n          hasStartedText\n        );\n        hasStartedText = result.hasStartedText;\n\n        return result.chunks;\n      }\n\n      if (\n        eventType === \"end\" ||\n        eventType === \"metadata\" ||\n        eventType === \"values\"\n      ) {\n        return [];\n      }\n\n      throw new Error(`Unsupported LangGraph stream event: ${eventType}`);\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/langgraph-agent/server/stream-normalizer.ts"
    }
  ],
  "envVars": {
    "LANGGRAPH_AGENT_API_KEY": "",
    "LANGGRAPH_AGENT_API_URL": "http://localhost:2024",
    "LANGGRAPH_AGENT_ASSISTANT_ID": "agent",
    "LANGGRAPH_AGENT_MODEL": "openai/gpt-5-mini"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item installs the LangGraph Agent frontend and Next.js proxy route only; run a Python LangGraph backend separately from https://github.com/SawanaLabs/agent-demos/tree/main/apps/langgraph-agent-api. Local-first backend setup uses http://localhost:2024 with LANGGRAPH_AGENT_ASSISTANT_ID=agent and a shared LANGGRAPH_AGENT_API_KEY. The same backend folder also includes a Vercel FastAPI entry for deployment.",
  "type": "registry:block"
}
