From 1ce257f8a36f4809ed0ee16e4ceaff16ebfbd28c Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Thu, 13 Aug 2026 16:40:13 +0530 Subject: [PATCH] feat(cli): add OpenUI Cloud LangGraph and Vercel AI SDK backends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the two backend overlays for the Cloud template. In both, the framework owns orchestration and application tools while OpenUI Cloud is attached as the Responses model provider and conversation store; reports, presentations, web search, image search, and MCP stay provider-executed. - LangGraph: `src/agent/agent.ts` builds the agent with `createAgent()` and a middleware that points the model at Cloud, forwards only the newest message (Cloud holds the history), and strips Cloud-owned tool calls from graph state so ToolNode runs only app tools. `/api/chat` proxies to the Agent Server via `@openuidev/langchain`. - Vercel AI SDK: a `streamText()` route with middleware that marks Cloud-executed tools as provider-executed so the SDK streams them without dispatching them locally, and a UI-chunk transform that makes completed Cloud tools display-only in the browser. - Each overlay ships its own `cloud-chat.tsx` wired to the transport its route speaks — `agUIAdapter()` for LangGraph, `vercelAIAdapter()` plus `vercelAIMessageFormat` for the AI SDK. The base template keeps its `openAIResponsesAdapter` version untouched, so there is no transport map or `backend` prop to keep in sync as backends are added. - Ignore `.langgraph_api`, written by the local Agent Server. Migrated from PR #785 (visharad/th-2051-route-replacement). Co-Authored-By: Visharad Kashyap <154831195+vishxrad@users.noreply.github.com> --- .../backends/langgraph/langgraph.json | 8 + .../backends/langgraph/manifest.json | 24 ++ .../backends/langgraph/src/agent/agent.ts | 120 +++++++++ .../langgraph/src/app/api/chat/route.ts | 37 +++ .../langgraph/src/components/cloud-chat.tsx | 92 +++++++ .../backends/vercel-ai-sdk/manifest.json | 13 + .../vercel-ai-sdk/src/app/api/chat/route.ts | 235 ++++++++++++++++++ .../src/components/cloud-chat.tsx | 94 +++++++ .../src/templates/openui-cloud/gitignore | 3 + 9 files changed, 626 insertions(+) create mode 100644 packages/openui-cli/src/templates/openui-cloud/backends/langgraph/langgraph.json create mode 100644 packages/openui-cli/src/templates/openui-cloud/backends/langgraph/manifest.json create mode 100644 packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/agent/agent.ts create mode 100644 packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/app/api/chat/route.ts create mode 100644 packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/components/cloud-chat.tsx create mode 100644 packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/manifest.json create mode 100644 packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/src/app/api/chat/route.ts create mode 100644 packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/src/components/cloud-chat.tsx diff --git a/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/langgraph.json b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/langgraph.json new file mode 100644 index 000000000..fa46b9f8f --- /dev/null +++ b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/langgraph.json @@ -0,0 +1,8 @@ +{ + "node_version": "20", + "dependencies": ["."], + "graphs": { + "agent": "./src/agent/agent.ts:graph" + }, + "env": ".env" +} diff --git a/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/manifest.json b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/manifest.json new file mode 100644 index 000000000..aa16abe5c --- /dev/null +++ b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/manifest.json @@ -0,0 +1,24 @@ +{ + "packageJson": { + "dependencies": { + "@langchain/core": "^1.2.5", + "@langchain/langgraph": "^1.4.9", + "@langchain/openai": "^1.5.6", + "@openuidev/langchain": "latest", + "langchain": "^1.5.5" + }, + "devDependencies": { + "@langchain/langgraph-cli": "^1.4.4", + "npm-run-all2": "^9.0.2" + }, + "scripts": { + "dev": "run-p dev:langgraph dev:next", + "dev:langgraph": "langgraphjs dev", + "dev:next": "next dev" + } + }, + "files": { + "remove": ["src/lib/tool-loop.ts"] + }, + "gettingStarted": "The generated LangGraph agent uses OpenUI Cloud as its Responses provider. `{{packageManager}} run dev` starts both the Agent Server and Next.js. Deploy the Next.js frontend to Vercel and point LANGGRAPH_API_URL at wherever the Agent Server runs.\nAsk \"What's the weather in Berlin?\" to exercise the included LangGraph tool." +} diff --git a/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/agent/agent.ts b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/agent/agent.ts new file mode 100644 index 000000000..5a3721cda --- /dev/null +++ b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/agent/agent.ts @@ -0,0 +1,120 @@ +import { AIMessage } from "@langchain/core/messages"; +import { type ServerTool, tool } from "@langchain/core/tools"; +import { StateSchema } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { openUIStreamTransformer } from "@openuidev/langchain/transformer"; +import { artifactTool, generateSystemPrompt } from "@openuidev/thesys-server"; +import { createAgent, createMiddleware } from "langchain"; +import { z } from "zod"; + +import { requiredEnv } from "../lib/env"; +import { DEFAULT_MODEL } from "../lib/models"; +import { executeGetWeather, getWeatherTool } from "../lib/tools/get-weather"; + +const getWeather = tool( + async ({ location }, config) => + executeGetWeather(JSON.stringify({ location }), { signal: config.signal }), + { + name: "get_weather", + description: getWeatherTool.description, + schema: z.object({ + location: z.string().trim().min(1).describe("City or place name, e.g. Berlin."), + }), + }, +); + +const appTools = [getWeather]; +const appToolNames = new Set(appTools.map(({ name }) => name)); +const TOOL_CALL_BLOCK_TYPES = new Set(["tool_call", "tool_call_chunk", "tool_use"]); + +function keepAppToolCallBlocks(block: unknown) { + if (typeof block !== "object" || block === null) return true; + const { type, name } = block as { type?: unknown; name?: unknown }; + if (typeof type !== "string" || !TOOL_CALL_BLOCK_TYPES.has(type)) return true; + return typeof name === "string" && appToolNames.has(name); +} + +// These are provider-executed tools. LangGraph sends their declarations to +// OpenUI Cloud, while Cloud runs them and stores their outputs/artifacts. +const cloudTools = [ + artifactTool({ artifacts: ["slides", "report"] }), + { type: "web_search" }, + { type: "image_search" }, + // Add provider-executed MCP servers here, for example: + // { type: "mcp", server_label: "deepwiki", server_url: "https://mcp.deepwiki.com/mcp" }, +] as ServerTool[]; + +const CloudAgentState = new StateSchema({ + conversationId: z.string(), + model: z.string().default(DEFAULT_MODEL), +}); + +function cloudModel(model: string, conversationId?: string) { + return new ChatOpenAI({ + model, + apiKey: requiredEnv("THESYS_API_KEY"), + streaming: true, + useResponsesApi: true, + configuration: { baseURL: "https://api.thesys.dev/v1/embed" }, + modelKwargs: { + store: true, + ...(conversationId ? { conversation: conversationId } : {}), + }, + }); +} + +const cloudConversation = createMiddleware({ + name: "OpenUICloudConversation", + stateSchema: CloudAgentState, + wrapModelCall: async (request, handler) => { + const { conversationId, model } = request.state as unknown as { + conversationId: string; + model: string; + }; + + const response = await handler({ + ...request, + model: cloudModel(model, conversationId), + // Cloud has the earlier turns. Within a LangGraph run this becomes the + // latest user message first, then each locally produced ToolMessage. + messages: request.messages.slice(-1), + }); + + // Cloud has already executed its provider tools. Keep only app-owned + // calls in graph state so LangGraph's ToolNode executes exactly those. + // ChatOpenAI also derives tool_calls from standard content blocks, so + // remove Cloud-owned call blocks as well as filtering response.tool_calls. + const localToolCalls = response.tool_calls?.filter(({ name }) => appToolNames.has(name)); + const localContent = Array.isArray(response.content) + ? response.content.filter(keepAppToolCallBlocks) + : response.content; + const contentChanged = + Array.isArray(response.content) && localContent.length !== response.content.length; + if (localToolCalls?.length === response.tool_calls?.length && !contentChanged) { + return response; + } + + return new AIMessage({ + id: response.id, + content: localContent, + additional_kwargs: response.additional_kwargs, + response_metadata: response.response_metadata, + tool_calls: localToolCalls, + invalid_tool_calls: response.invalid_tool_calls, + usage_metadata: response.usage_metadata, + }); + }, +}); + +/** + * A normal LangGraph agent: LangGraph owns orchestration and local + * tool execution; OpenUI Cloud is the attached Responses provider. + */ +export const graph = createAgent({ + model: cloudModel(DEFAULT_MODEL), + tools: [...cloudTools, ...appTools], + systemPrompt: generateSystemPrompt(), + stateSchema: CloudAgentState, + middleware: [cloudConversation], + streamTransformers: [openUIStreamTransformer], +}); diff --git a/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/app/api/chat/route.ts b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/app/api/chat/route.ts new file mode 100644 index 000000000..0bbc59341 --- /dev/null +++ b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/app/api/chat/route.ts @@ -0,0 +1,37 @@ +import { resolveRequestedModel } from "@/lib/models"; +import { createLangChainStreamResponse } from "@openuidev/langchain"; + +export const runtime = "nodejs"; + +const API_URL = process.env.LANGGRAPH_API_URL || "http://localhost:2024"; +const ASSISTANT_ID = process.env.LANGGRAPH_ASSISTANT_ID || "agent"; + +/** + * Browser-to-Agent-Server proxy. The agent itself lives in src/agent/agent.ts + * and can be run locally or deployed independently. + */ +export async function POST(request: Request) { + return createLangChainStreamResponse(request, { + apiUrl: API_URL, + assistantId: ASSISTANT_ID, + apiKey: process.env.LANGSMITH_API_KEY, + debug: process.env.NODE_ENV !== "production", + prepareInput: ({ messages, requestBody }) => { + const conversationId = requestBody.threadId; + if (typeof conversationId !== "string" || !conversationId) { + throw new Error("threadId is required — create the conversation first"); + } + + const model = resolveRequestedModel(requestBody.model); + if (!model) throw new Error("model is not available in this agent"); + + return { + // OpenUI Cloud stores prior turns. LangGraph owns the current run and + // local tool loop, while only new inputs are sent to the conversation. + messages: messages.slice(-1), + conversationId, + model, + }; + }, + }); +} diff --git a/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/components/cloud-chat.tsx b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/components/cloud-chat.tsx new file mode 100644 index 000000000..09e37aba2 --- /dev/null +++ b/packages/openui-cli/src/templates/openui-cloud/backends/langgraph/src/components/cloud-chat.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { usePersistedModel } from "@/hooks/use-persisted-model"; +import { MODEL_OPTIONS } from "@/lib/models"; +import { OPENUI_LOGOS, PROMPT_TEMPLATES, STARTERS } from "@/lib/starters"; +import { + AgentInterface, + ModelSwitcher, + agUIAdapter, + defineArtifactCategories, + fetchLLM, + useSystemThemeMode, +} from "@openuidev/react-ui"; +import { + chatLibrary, + presentationArtifactRenderer, + reportArtifactRenderer, + useOpenuiCloudStorage, +} from "@openuidev/thesys"; +import { FileText, Presentation } from "lucide-react"; + +const { artifactRenderers, artifactCategories } = defineArtifactCategories([ + { + name: "Presentations", + renderers: [presentationArtifactRenderer], + icon: , + }, + { + name: "Reports", + renderers: [reportArtifactRenderer], + icon: , + }, +]); + +export default function CloudChat() { + const mode = useSystemThemeMode(); + const [selectedModel, setSelectedModel] = usePersistedModel(); + // The LangGraph proxy emits AG-UI events, so no message format is needed: + // the Agent Server receives the conversation as graph input instead. + const llm = fetchLLM({ + url: "/api/chat", + streamAdapter: agUIAdapter(), + body: { model: selectedModel }, + }); + + const storage = useOpenuiCloudStorage({ + token: "/api/frontend-token", + apiBaseUrl: "https://api.thesys.dev", + features: { artifact: true }, + }); + + const logoPath = mode === "dark" ? OPENUI_LOGOS.DARK : OPENUI_LOGOS.LIGHT; + + return ( +
+ + + } + /> + + + + + +
+ ); +} diff --git a/packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/manifest.json b/packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/manifest.json new file mode 100644 index 000000000..467b58fa3 --- /dev/null +++ b/packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/manifest.json @@ -0,0 +1,13 @@ +{ + "packageJson": { + "dependencies": { + "@ai-sdk/openai": "^3.0.91", + "ai": "^6.0.246", + "zod": "^4.4.3" + } + }, + "files": { + "remove": ["src/lib/tool-loop.ts"] + }, + "gettingStarted": "The generated Vercel AI SDK route uses OpenUI Cloud as its Responses provider and is deployable as a normal Next.js app on Vercel.\nAsk \"What's the weather in Berlin?\" to exercise the included AI SDK tool." +} diff --git a/packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/src/app/api/chat/route.ts b/packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/src/app/api/chat/route.ts new file mode 100644 index 000000000..b4aba8fce --- /dev/null +++ b/packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/src/app/api/chat/route.ts @@ -0,0 +1,235 @@ +import { requiredEnv } from "@/lib/env"; +import { resolveRequestedModel } from "@/lib/models"; +import { executeGetWeather, getWeatherTool } from "@/lib/tools/get-weather"; +import { createOpenAI } from "@ai-sdk/openai"; +import { artifactTool, generateSystemPrompt } from "@openuidev/thesys-server"; +import { + convertToModelMessages, + createUIMessageStreamResponse, + stepCountIs, + streamText, + tool, + wrapLanguageModel, + type LanguageModelMiddleware, + type UIMessage, + type UIMessageChunk, +} from "ai"; +import { z } from "zod"; + +export const runtime = "nodejs"; + +const appTools = { + get_weather: tool({ + description: getWeatherTool.description, + inputSchema: z.object({ + location: z.string().trim().min(1).describe("City or place name, e.g. Berlin."), + }), + execute: ({ location }, { abortSignal }) => + executeGetWeather(JSON.stringify({ location }), { signal: abortSignal }), + }), +}; + +const appToolNames = new Set(Object.keys(appTools)); +for (const appToolName of appToolNames) { + if (appToolName.startsWith("thesys_")) { + throw new Error(`App tool names cannot use the reserved thesys_ prefix: ${appToolName}`); + } +} + +type WrapStreamArgs = Parameters>[0]; +type ModelStreamResult = Awaited>; +type ModelStreamPart = + ModelStreamResult["stream"] extends ReadableStream ? Part : never; + +type CloudFunctionCallOutput = { + type: "response.output_item.done"; + item: { + type: "function_call_output"; + call_id: string; + output: string; + }; +}; + +function isCloudFunctionCallOutput(value: unknown): value is CloudFunctionCallOutput { + if (typeof value !== "object" || value === null) return false; + const event = value as { type?: unknown; item?: unknown }; + if (event.type !== "response.output_item.done") return false; + if (typeof event.item !== "object" || event.item === null) return false; + const item = event.item as { type?: unknown; call_id?: unknown; output?: unknown }; + return ( + item.type === "function_call_output" && + typeof item.call_id === "string" && + typeof item.output === "string" + ); +} + +/** + * OpenUI Cloud runs artifacts, search, and MCP calls itself. Mark those calls + * as provider-executed dynamic tools so the AI SDK includes them in its stream + * without dispatching them through the local appTools executor. + */ +const cloudToolsAsProviderExecuted: LanguageModelMiddleware = { + specificationVersion: "v3", + wrapStream: async ({ doStream }) => { + const result = await doStream(); + const cloudToolCallIds = new Set(); + const cloudToolNames = new Map(); + + return { + ...result, + stream: result.stream.pipeThrough( + new TransformStream({ + transform(part, controller) { + if (part.type === "raw") { + if ( + isCloudFunctionCallOutput(part.rawValue) && + cloudToolCallIds.has(part.rawValue.item.call_id) + ) { + const toolCallId = part.rawValue.item.call_id; + const toolName = cloudToolNames.get(toolCallId); + if (toolName) { + controller.enqueue({ + type: "tool-result", + toolCallId, + toolName, + result: part.rawValue.item.output, + dynamic: true, + }); + } + } + return; + } + + if (part.type === "tool-input-start") { + if (part.providerExecuted === true || !appToolNames.has(part.toolName)) { + cloudToolCallIds.add(part.id); + cloudToolNames.set(part.id, part.toolName); + controller.enqueue({ ...part, providerExecuted: true, dynamic: true }); + return; + } + } + + if (part.type === "tool-call") { + if ( + part.providerExecuted === true || + cloudToolCallIds.has(part.toolCallId) || + !appToolNames.has(part.toolName) + ) { + cloudToolCallIds.add(part.toolCallId); + cloudToolNames.set(part.toolCallId, part.toolName); + controller.enqueue({ ...part, providerExecuted: true, dynamic: true }); + return; + } + } + + if (part.type === "tool-result" && cloudToolCallIds.has(part.toolCallId)) { + controller.enqueue({ ...part, dynamic: true }); + return; + } + + controller.enqueue(part); + }, + }), + ), + }; + }, +}; + +/** + * `providerExecuted` controls the AI SDK's backend tool loop. Once a completed + * Cloud tool reaches the browser it is display-only; removing that flag from + * the outgoing UI chunk lets OpenUI render the activity while the generic + * adapter keeps rejecting unscoped provider-executed streams by default. + */ +function displayOnlyProviderTools() { + return new TransformStream({ + transform(chunk, controller) { + if ("providerExecuted" in chunk && chunk.providerExecuted === true) { + const { providerExecuted: _providerExecuted, ...displayChunk } = chunk; + controller.enqueue(displayChunk as UIMessageChunk); + return; + } + + controller.enqueue(chunk); + }, + }); +} + +/** Add Cloud-managed tool declarations after the AI SDK prepares its request. */ +const cloudFetch: typeof fetch = async (input, init) => { + const url = input instanceof Request ? input.url : String(input); + if (!url.endsWith("/responses") || typeof init?.body !== "string") { + return fetch(input, init); + } + + const body = JSON.parse(init.body) as { tools?: unknown[] }; + body.tools = [ + artifactTool({ artifacts: ["slides", "report"] }), + { type: "image_search" }, + // Add provider-executed MCP servers here, for example: + // { type: "mcp", server_label: "deepwiki", server_url: "https://mcp.deepwiki.com/mcp" }, + ...(body.tools ?? []), + ]; + + return fetch(input, { ...init, body: JSON.stringify(body) }); +}; + +export async function POST(req: Request) { + const { + threadId, + messages, + model: requestedModel, + } = (await req.json()) as { + threadId?: string; + messages?: UIMessage[]; + model?: unknown; + }; + + if (!threadId) return badRequest("threadId is required — create the conversation first"); + if (!Array.isArray(messages) || messages.length === 0) { + return badRequest("messages must be a non-empty UIMessage[]"); + } + const model = resolveRequestedModel(requestedModel); + if (!model) return badRequest("model is not available in this agent"); + + const openai = createOpenAI({ + baseURL: "https://api.thesys.dev/v1/embed", + apiKey: requiredEnv("THESYS_API_KEY"), + fetch: cloudFetch, + }); + const cloudModel = wrapLanguageModel({ + model: openai.responses(model), + middleware: cloudToolsAsProviderExecuted, + }); + + const result = streamText({ + model: cloudModel, + messages: await convertToModelMessages(messages.slice(-1)), + tools: { + ...appTools, + web_search: openai.tools.webSearch({}), + }, + stopWhen: stepCountIs(5), + prepareStep: ({ messages: stepMessages }) => ({ messages: stepMessages.slice(-1) }), + providerOptions: { + openai: { + conversation: threadId, + store: true, + instructions: generateSystemPrompt(), + }, + }, + abortSignal: req.signal, + // Cloud-specific function_call_output items are currently exposed by the + // OpenAI provider as raw chunks, which the middleware maps to tool results. + includeRawChunks: true, + }); + + // The AI SDK owns UIMessage SSE encoding for this variant. + return createUIMessageStreamResponse({ + stream: result.toUIMessageStream().pipeThrough(displayOnlyProviderTools()), + }); +} + +function badRequest(message: string): Response { + return Response.json({ error: { message } }, { status: 400 }); +} diff --git a/packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/src/components/cloud-chat.tsx b/packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/src/components/cloud-chat.tsx new file mode 100644 index 000000000..aafa3f5c5 --- /dev/null +++ b/packages/openui-cli/src/templates/openui-cloud/backends/vercel-ai-sdk/src/components/cloud-chat.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { usePersistedModel } from "@/hooks/use-persisted-model"; +import { MODEL_OPTIONS } from "@/lib/models"; +import { OPENUI_LOGOS, PROMPT_TEMPLATES, STARTERS } from "@/lib/starters"; +import { + AgentInterface, + ModelSwitcher, + defineArtifactCategories, + fetchLLM, + useSystemThemeMode, + vercelAIAdapter, + vercelAIMessageFormat, +} from "@openuidev/react-ui"; +import { + chatLibrary, + presentationArtifactRenderer, + reportArtifactRenderer, + useOpenuiCloudStorage, +} from "@openuidev/thesys"; +import { FileText, Presentation } from "lucide-react"; + +const { artifactRenderers, artifactCategories } = defineArtifactCategories([ + { + name: "Presentations", + renderers: [presentationArtifactRenderer], + icon: , + }, + { + name: "Reports", + renderers: [reportArtifactRenderer], + icon: , + }, +]); + +export default function CloudChat() { + const mode = useSystemThemeMode(); + const [selectedModel, setSelectedModel] = usePersistedModel(); + // The route returns the AI SDK's native UIMessage stream, so the browser + // decodes it with the SDK's own adapter and message format. + const llm = fetchLLM({ + url: "/api/chat", + streamAdapter: vercelAIAdapter(), + messageFormat: vercelAIMessageFormat, + body: { model: selectedModel }, + }); + + const storage = useOpenuiCloudStorage({ + token: "/api/frontend-token", + apiBaseUrl: "https://api.thesys.dev", + features: { artifact: true }, + }); + + const logoPath = mode === "dark" ? OPENUI_LOGOS.DARK : OPENUI_LOGOS.LIGHT; + + return ( +
+ + + } + /> + + + + + +
+ ); +} diff --git a/packages/openui-cli/src/templates/openui-cloud/gitignore b/packages/openui-cli/src/templates/openui-cloud/gitignore index 3cceb2b4c..07b0ee2c1 100644 --- a/packages/openui-cli/src/templates/openui-cloud/gitignore +++ b/packages/openui-cli/src/templates/openui-cloud/gitignore @@ -36,6 +36,9 @@ yarn-error.log* # vercel .vercel +# local LangGraph Agent Server +.langgraph_api + # local thread index (created at runtime) /.data/