@copilotkit/sdk-js 1.78.0 → 1.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/langgraph/middleware.ts"],"sourcesContent":["import { createMiddleware, AIMessage, SystemMessage } from \"langchain\";\nimport type { InteropZodObject } from \"@langchain/core/utils/types\";\nimport type {\n StandardJSONSchemaV1,\n StandardSchemaV1,\n} from \"@standard-schema/spec\";\nimport * as z from \"zod\";\nimport { getA2UITools } from \"@ag-ui/langgraph\";\nimport type { A2UIToolParams } from \"@ag-ui/langgraph\";\nimport { getForwardedHeaders } from \"../header-propagation\";\n\n// ---------------------------------------------------------------------------\n// Auto-A2UI: bridge the inferred model's generate_a2ui tool from wrapModelCall\n// (the only hook that exposes the bound model) to wrapToolCall (where the tool\n// actually executes but the model is absent). Keyed by the run's thread id so\n// concurrent runs don't clobber each other.\n// ---------------------------------------------------------------------------\nconst a2uiToolsByThread = new Map<string, any>();\nconst A2UI_DEFAULT_THREAD_KEY = \"__copilotkit_a2ui_default__\";\nconst a2uiThreadKey = (state: any): string =>\n (state?.thread_id as string) || A2UI_DEFAULT_THREAD_KEY;\n\n/**\n * Merge AG-UI defaults with CopilotKit overrides. Dictionaries recurse; every\n * conflicting leaf uses CopilotKit, including false, null and empty values.\n * Arrays and serialized strings are atomic: no positional pairing or decoding.\n */\nconst isPropertyBag = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\" && !Array.isArray(value);\n\nconst mergeProperties = (\n base: Record<string, unknown>,\n overrides: Record<string, unknown>,\n): Record<string, unknown> =>\n Object.fromEntries(\n Array.from(new Set([...Object.keys(base), ...Object.keys(overrides)])).map(\n (key) => {\n const value = Object.prototype.hasOwnProperty.call(overrides, key)\n ? isPropertyBag(base[key]) && isPropertyBag(overrides[key])\n ? mergeProperties(base[key], overrides[key])\n : overrides[key]\n : base[key];\n return [key, value];\n },\n ),\n );\n\nconst effectiveProperties = (state: unknown): Record<string, unknown> => {\n const source = isPropertyBag(state) ? state : {};\n return mergeProperties(\n isPropertyBag(source[\"ag-ui\"]) ? source[\"ag-ui\"] : {},\n isPropertyBag(source.copilotkit) ? source.copilotkit : {},\n );\n};\n\nconst decodeCatalogValue = (value: unknown): unknown => {\n if (typeof value !== \"string\") return value;\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n};\n\n/**\n * Find the frontend-registered A2UI catalog wherever it was passed. Returns\n * `{ compositionGuide?, catalogId? }` when a catalog is present, else `null`\n * independently of the explicit tool-injection decision. Two\n * delivery paths, depending on how the agent is served:\n * - AG-UI native endpoint → `state[\"ag-ui\"].a2ui_schema` (JSON\n * `{ catalogId, components }`); the toolkit reads it from state itself.\n * - Runtime proxy → an effective `context` entry describing\n * the A2UI catalog (catalog id + component schemas as text), passed to the\n * subagent via `compositionGuide`.\n * `catalogId` binds generated surfaces to the frontend's catalog so BYOC\n * custom catalogs render their own components (not the basic one).\n */\nconst resolveA2uiCatalog = (\n state: any,\n): { compositionGuide?: string; catalogId?: string } | null => {\n const properties = effectiveProperties(state);\n const a2uiSchema = properties.a2ui_schema;\n if (a2uiSchema) {\n let catalogId: string | undefined;\n try {\n const parsed =\n typeof a2uiSchema === \"string\" ? JSON.parse(a2uiSchema) : a2uiSchema;\n catalogId = parsed?.catalogId;\n } catch {\n // non-JSON schema — fall back to the toolkit's basic catalog\n }\n return { catalogId };\n }\n const context = decodeCatalogValue(properties.context);\n for (const entry of Array.isArray(context) ? context : []) {\n const description = entry?.description;\n const value = decodeCatalogValue(entry?.value);\n if (\n typeof description !== \"string\" ||\n typeof value !== \"string\" ||\n !description.includes(\"A2UI catalog\") ||\n !value\n )\n continue;\n const match = /^\\s*-\\s+(\\S+)/m.exec(value);\n return { compositionGuide: value, catalogId: match?.[1] };\n }\n return null;\n};\n\n/**\n * The A2UI `injectA2UITool` decision. The `@ag-ui/a2ui-middleware` forwards it on\n * `forwardedProps`, which `ag-ui-langgraph` surfaces into agent state at\n * `state[\"ag-ui\"].inject_a2ui_tool` — present only when the host turned the\n * runtime A2UI tool on (truthy or a custom tool-name string). `undefined` means\n * no signal (off, or no A2UI middleware in the pipeline) → no auto-injection.\n */\nconst a2uiInjectDecision = (state: any): boolean | string | undefined =>\n effectiveProperties(state).inject_a2ui_tool as boolean | string | undefined;\n\ntype WithJsonSchema<T> = T extends { \"~standard\": infer S }\n ? Omit<T, \"~standard\"> & {\n \"~standard\": S &\n StandardJSONSchemaV1.Props<\n S extends StandardSchemaV1.Props<infer I, any> ? I : unknown,\n S extends StandardSchemaV1.Props<any, infer O> ? O : unknown\n >;\n }\n : T;\n\n/**\n * Augment a Standard-Schema–compatible schema (e.g. Zod) with a\n * `~standard.jsonSchema.input` hook so LangGraph's\n * `getJsonSchemaFromSchema` (called from `StateSchema.getJsonSchema`)\n * can serialize the field.\n *\n * Without this, Zod v4 fields carry `~standard.validate` + `vendor` only,\n * and `isStandardJSONSchema()` returns false, so the field is silently\n * dropped from the graph's `output_schema`. That makes AG-UI\n * `STATE_SNAPSHOT` events filter the field out of the payload sent to\n * the frontend even though the underlying thread state has the value.\n *\n * Use this on any custom state field you want visible to the frontend\n * via `useAgent().state.*`.\n *\n * @example\n * ```ts\n * import { zodState } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const stateSchema = z.object({\n * todos: zodState(z.array(TodoSchema).default(() => [])),\n * });\n * ```\n */\nexport function zodState<T extends object>(schema: T): WithJsonSchema<T> {\n const std = (schema as { \"~standard\"?: { jsonSchema?: unknown } })[\n \"~standard\"\n ];\n if (std && typeof std === \"object\" && !(\"jsonSchema\" in std)) {\n let cached: Record<string, unknown> | undefined;\n std.jsonSchema = {\n input: () => {\n if (cached) return cached;\n // Prefer zod-v4's native `toJSONSchema` when available. Falls back to\n // an empty object, which is sufficient for the field to appear in the\n // graph's output_schema (langgraph-api treats it as an opaque field).\n try {\n const maybeV4ToJsonSchema = (\n z as unknown as {\n toJSONSchema?: (s: unknown) => Record<string, unknown>;\n }\n ).toJSONSchema;\n cached =\n typeof maybeV4ToJsonSchema === \"function\"\n ? maybeV4ToJsonSchema(schema)\n : {};\n } catch {\n cached = {};\n }\n return cached;\n },\n };\n }\n return schema as WithJsonSchema<T>;\n}\n\n/**\n * Internal/framework state keys that should never be auto-surfaced to the\n * LLM as user-facing state. These are reducer-managed message buckets,\n * CopilotKit/AG-UI plumbing, or graph-internal scaffolding.\n */\nconst RESERVED_STATE_KEYS: ReadonlySet<string> = new Set([\n \"messages\",\n \"copilotkit\",\n \"ag-ui\",\n \"tools\",\n \"structured_response\",\n \"thread_id\",\n \"remaining_steps\",\n]);\n\n/**\n * Controls how user-defined state keys are surfaced into the LLM prompt\n * on every model call. Off by default to avoid leaking arbitrary state\n * into prompts; opt in explicitly.\n *\n * - `false` (default) — never surface state.\n * - `true` — every state key not in the reserved internal set and not\n * prefixed with `_` is JSON-serialized into a \"Current agent state:\"\n * note appended to the system prompt.\n * - `string[]` — only surface the named keys (use this when you want\n * explicit control over what the LLM sees, e.g. `[\"liked\", \"todos\"]`).\n */\nexport type ExposeStateOption = boolean | readonly string[];\n\nconst buildStateNote = (\n state: Record<string, unknown>,\n expose: ExposeStateOption,\n): string | null => {\n if (expose === false) return null;\n\n const allow: ReadonlySet<string> | null = Array.isArray(expose)\n ? new Set(expose)\n : null;\n\n const snapshot: Record<string, unknown> = {};\n for (const key of Object.keys(state)) {\n if (\n allow\n ? !allow.has(key)\n : RESERVED_STATE_KEYS.has(key) || key.startsWith(\"_\")\n ) {\n continue;\n }\n const value = state[key];\n if (\n value === undefined ||\n value === null ||\n value === \"\" ||\n (Array.isArray(value) && value.length === 0) ||\n (typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0)\n ) {\n continue;\n }\n snapshot[key] = value;\n }\n\n if (Object.keys(snapshot).length === 0) return null;\n\n let body: string;\n try {\n body = JSON.stringify(snapshot, null, 2);\n } catch {\n body = String(snapshot);\n }\n return `Current agent state:\\n${body}`;\n};\n\nconst applyStateNote = (request: any, expose: ExposeStateOption): any => {\n const note = buildStateNote(\n (request.state ?? {}) as Record<string, unknown>,\n expose,\n );\n if (!note) return request;\n\n const existingMessage = request.systemMessage;\n if (existingMessage != null) {\n const separator = existingMessage.text === \"\" ? \"\" : \"\\n\\n\";\n return {\n ...request,\n systemMessage: existingMessage.concat(`${separator}${note}`),\n };\n }\n\n const existing = request.systemPrompt;\n if (existing == null) {\n return { ...request, systemPrompt: note };\n }\n // existing may be a string OR a SystemMessage\n const baseText =\n typeof existing === \"string\"\n ? existing\n : typeof existing.content === \"string\"\n ? existing.content\n : String(existing.content);\n return {\n ...request,\n systemPrompt: `${baseText}\\n\\n${note}`,\n };\n};\n\nconst createAppContextBeforeAgent = (state, runtime) => {\n const messages = state.messages;\n\n if (!messages || messages.length === 0) {\n return;\n }\n\n // Get app context from runtime\n const properties = effectiveProperties(state);\n const appContext = Object.prototype.hasOwnProperty.call(properties, \"context\")\n ? properties.context\n : runtime?.context;\n\n // Check if appContext is missing or empty\n const isEmptyContext =\n !appContext ||\n (typeof appContext === \"string\" && appContext.trim() === \"\") ||\n (typeof appContext === \"object\" && Object.keys(appContext).length === 0);\n\n if (isEmptyContext) {\n return;\n }\n\n // Create the context content\n const contextContent =\n typeof appContext === \"string\"\n ? appContext\n : JSON.stringify(appContext, null, 2);\n const contextMessageContent = `App Context:\\n${contextContent}`;\n const contextMessagePrefix = \"App Context:\\n\";\n\n // Helper to get message content as string\n const getContentString = (msg: any): string | null => {\n if (typeof msg.content === \"string\") return msg.content;\n if (Array.isArray(msg.content) && msg.content[0]?.text)\n return msg.content[0].text;\n return null;\n };\n\n // Find the first system/developer message (not our context message) to determine\n // where to insert our context message (right after it)\n let firstSystemIndex = -1;\n\n for (let i = 0; i < messages.length; i++) {\n const msg = messages[i];\n const type = msg._getType?.();\n if (type === \"system\" || type === \"developer\") {\n const content = getContentString(msg);\n // Skip if this is our own context message\n if (content?.startsWith(contextMessagePrefix)) {\n continue;\n }\n firstSystemIndex = i;\n break;\n }\n }\n\n // Check if our context message already exists\n let existingContextIndex = -1;\n for (let i = 0; i < messages.length; i++) {\n const msg = messages[i];\n const type = msg._getType?.();\n if (type === \"system\" || type === \"developer\") {\n const content = getContentString(msg);\n if (content?.startsWith(contextMessagePrefix)) {\n existingContextIndex = i;\n break;\n }\n }\n }\n\n // Create the context message\n const contextMessage = new SystemMessage({ content: contextMessageContent });\n\n let updatedMessages;\n\n if (existingContextIndex !== -1) {\n // Replace existing context message\n updatedMessages = [...messages];\n updatedMessages[existingContextIndex] = contextMessage;\n } else {\n // Insert after the first system message, or at position 0 if no system message\n const insertIndex = firstSystemIndex !== -1 ? firstSystemIndex + 1 : 0;\n updatedMessages = [\n ...messages.slice(0, insertIndex),\n contextMessage,\n ...messages.slice(insertIndex),\n ];\n }\n\n return {\n ...state,\n messages: updatedMessages,\n };\n};\n\n/**\n * CopilotKit Middleware for LangGraph agents.\n *\n * Enables:\n * - Dynamic frontend tools from state.tools\n * - Context provided from CopilotKit useCopilotReadable\n *\n * Works with any agent (prebuilt or custom).\n *\n * @example\n * ```typescript\n * import { createAgent } from \"langchain\";\n * import { copilotkitMiddleware } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const agent = createAgent({\n * model: \"gpt-4o\",\n * tools: [backendTool],\n * middleware: [copilotkitMiddleware],\n * });\n * ```\n */\nconst copilotKitStateSchema = z\n .object({\n \"ag-ui\": zodState(z.record(z.string(), z.unknown()).optional()),\n copilotkit: zodState(\n z\n .object({\n actions: z\n .union([z.array(z.any()), z.literal(false)])\n .nullable()\n .optional(),\n context: z.any().optional(),\n interceptedToolCalls: z.array(z.any()).optional(),\n originalAIMessageId: z.string().optional(),\n })\n .passthrough()\n .optional(),\n ),\n })\n .passthrough();\n\nconst isToolCallContentBlock = (block: unknown) =>\n typeof block === \"object\" &&\n block !== null &&\n \"type\" in block &&\n (block.type === \"tool_call\" || block.type === \"tool_call_chunk\");\n\nconst usesV1ContentBlocks = (responseMetadata: unknown) =>\n typeof responseMetadata === \"object\" &&\n responseMetadata !== null &&\n \"output_version\" in responseMetadata &&\n responseMetadata.output_version === \"v1\";\n\n/**\n * Rebuilds an AIMessage with `toolCalls` as the source of truth while\n * preserving its non-tool content and metadata. For v1 content blocks, old\n * tool blocks must be removed before construction so they cannot duplicate or\n * override the supplied tool calls when AIMessage synchronizes both fields.\n */\nconst rebuildAIMessageWithToolCalls = (\n message: AIMessage,\n toolCalls: AIMessage[\"tool_calls\"],\n) => {\n let content = message.content;\n if (\n usesV1ContentBlocks(message.response_metadata) &&\n Array.isArray(content)\n ) {\n content = content.filter((block) => !isToolCallContentBlock(block));\n }\n\n return new AIMessage({\n content,\n additional_kwargs: message.additional_kwargs,\n response_metadata: message.response_metadata,\n tool_calls: toolCalls,\n invalid_tool_calls: message.invalid_tool_calls,\n usage_metadata: message.usage_metadata,\n id: message.id,\n name: message.name,\n });\n};\n\nconst buildMiddlewareInput = (\n exposeState: ExposeStateOption,\n a2uiParams?: Omit<A2UIToolParams, \"model\">,\n) => ({\n name: \"CopilotKitMiddleware\",\n\n stateSchema: copilotKitStateSchema as unknown as InteropZodObject,\n\n // Inject frontend tools, surface user state, and forward x-aimock-* headers\n wrapModelCall: async (request: any, handler: (req: any) => Promise<any>) => {\n request = applyStateNote(request, exposeState);\n\n // Forward x-aimock-* headers from the incoming AG-UI request\n const forwardedHeaders = getForwardedHeaders();\n if (Object.keys(forwardedHeaders).length > 0) {\n const existingSettings = request.modelSettings ?? {};\n const existingHeaders =\n (existingSettings.headers as Record<string, string>) ?? {};\n request = {\n ...request,\n modelSettings: {\n ...existingSettings,\n headers: { ...existingHeaders, ...forwardedHeaders },\n },\n };\n }\n\n // Opt-in auto-injection of generate_a2ui:\n // (1) only inject when the A2UI injectA2UITool flag is truthy (forwarded by\n // @ag-ui/a2ui-middleware and surfaced at state[\"ag-ui\"].inject_a2ui_tool);\n // (2) don't double-inject if the agent already defines this tool.\n // The catalog (when present) only binds surfaces to the FE's catalog; it is\n // not the gate. The model is inferred from request.model; the built tool is\n // stashed for wrapToolCall to execute.\n let a2uiTool: any = null;\n const decision = a2uiInjectDecision(request.state);\n if (typeof getA2UITools === \"function\" && decision) {\n const catalog = resolveA2uiCatalog(request.state);\n // Shared A2UIToolParams: a single params object owned by the toolkit.\n // Start from the host overrides (guidelines / catalog id / tool name /\n // recovery) so a host can steer the subagent, then layer in only what the\n // host cannot know — the bound model, and the registered catalog id +\n // compositionGuide — without clobbering any host-set value.\n const params: A2UIToolParams = {\n ...a2uiParams,\n model: request.model,\n };\n if (catalog?.catalogId && params.defaultCatalogId == null)\n params.defaultCatalogId = catalog.catalogId;\n // Merge the registered catalog schema into any host `guidelines` bag; a\n // host-set compositionGuide wins, host generation/design overrides stay.\n if (catalog?.compositionGuide) {\n const guidelines = { ...params.guidelines };\n if (guidelines.compositionGuide == null)\n guidelines.compositionGuide = catalog.compositionGuide;\n params.guidelines = guidelines;\n }\n const candidate = getA2UITools(params);\n const existingNames = new Set(\n (request.tools || []).map((t: any) => t?.name),\n );\n if (!existingNames.has(candidate.name)) {\n // ToolNode derives tool runtime state from its config, ignoring an\n // overridden middleware request.state. Adapt the tool's invocation\n // config so the toolkit sees the same effective properties.\n const invoke = candidate.invoke.bind(candidate);\n candidate.invoke = (input, config) => {\n const state =\n config && \"state\" in config && isPropertyBag(config.state)\n ? config.state\n : request.state;\n const properties = effectiveProperties(state);\n const schema = properties.a2ui_schema;\n const context = decodeCatalogValue(properties.context);\n return invoke(input, {\n ...config,\n state: {\n ...state,\n \"ag-ui\": {\n ...properties,\n context: Array.isArray(context) ? context : [],\n // The toolkit interpolates the native schema into its prompt.\n a2ui_schema: isPropertyBag(schema)\n ? JSON.stringify(schema)\n : schema,\n },\n },\n });\n };\n a2uiTool = candidate;\n a2uiToolsByThread.set(a2uiThreadKey(request.state), a2uiTool);\n }\n }\n\n const actions = effectiveProperties(request.state).actions;\n let frontendTools = Array.isArray(actions) ? actions : [];\n if (a2uiTool) {\n // Our generate_a2ui replaces the runtime's render tool — don't advertise\n // both. Drop the render tool the A2UI middleware injected.\n const drop = typeof decision === \"string\" ? decision : \"render_a2ui\";\n frontendTools = frontendTools.filter(\n (t: any) => (t?.function?.name ?? t?.name) !== drop,\n );\n }\n\n if (frontendTools.length === 0 && !a2uiTool) {\n return handler(request);\n }\n\n const existingTools = request.tools || [];\n const mergedTools = [\n ...existingTools,\n ...(a2uiTool ? [a2uiTool] : []),\n ...frontendTools,\n ];\n\n return handler({\n ...request,\n tools: mergedTools,\n });\n },\n\n // Execute the dynamically-advertised generate_a2ui tool. It is not in the\n // agent's static tool registry, so the tool node cannot run it on its own;\n // we supply the implementation (built with the inferred model) for that one\n // tool. This hook's presence also disables createAgent's \"unknown tool\"\n // guard for dynamically-advertised tools.\n wrapToolCall: async (request: any, handler: (req: any) => Promise<any>) => {\n const tool = a2uiToolsByThread.get(a2uiThreadKey(request.state));\n if (tool && !request.tool && request.toolCall?.name === tool.name) {\n return handler({ ...request, tool });\n }\n return handler(request);\n },\n\n beforeAgent: createAppContextBeforeAgent,\n\n // Restore frontend tool calls to AIMessage before agent exits\n afterAgent: (state) => {\n // Drop the bridged A2UI tool for this run — all tool calls for the turn\n // have executed by now; the next model call re-stashes if needed.\n a2uiToolsByThread.delete(a2uiThreadKey(state));\n\n const interceptedToolCalls = state[\"copilotkit\"]?.interceptedToolCalls;\n const originalMessageId = state[\"copilotkit\"]?.originalAIMessageId;\n\n if (!interceptedToolCalls?.length || !originalMessageId) {\n return;\n }\n\n let messageFound = false;\n const updatedMessages = state.messages.map((msg: any) => {\n if (AIMessage.isInstance(msg) && msg.id === originalMessageId) {\n messageFound = true;\n const existingToolCalls = msg.tool_calls || [];\n return rebuildAIMessageWithToolCalls(msg, [\n ...existingToolCalls,\n ...interceptedToolCalls,\n ]);\n }\n return msg;\n });\n\n // Only clear intercepted state if we successfully restored the tool calls\n if (!messageFound) {\n console.warn(\n `CopilotKit: Could not find message with id ${originalMessageId} to restore tool calls`,\n );\n return;\n }\n\n return {\n messages: updatedMessages,\n copilotkit: {\n ...state[\"copilotkit\"],\n interceptedToolCalls: undefined,\n originalAIMessageId: undefined,\n },\n };\n },\n\n // Intercept frontend tool calls after model returns, before ToolNode executes\n afterModel: (state) => {\n const actions = effectiveProperties(state).actions;\n const frontendTools = Array.isArray(actions) ? actions : [];\n if (frontendTools.length === 0) return;\n\n const frontendToolNames = new Set(\n frontendTools.map((t: any) => t.function?.name || t.name),\n );\n\n const lastMessage = state.messages[state.messages.length - 1];\n if (!AIMessage.isInstance(lastMessage) || !lastMessage.tool_calls?.length) {\n return;\n }\n\n const backendToolCalls: any[] = [];\n const frontendToolCalls: any[] = [];\n\n for (const call of lastMessage.tool_calls) {\n if (frontendToolNames.has(call.name)) {\n frontendToolCalls.push(call);\n } else {\n backendToolCalls.push(call);\n }\n }\n\n if (frontendToolCalls.length === 0) return;\n\n const updatedAIMessage = rebuildAIMessageWithToolCalls(\n lastMessage,\n backendToolCalls,\n );\n\n return {\n messages: [...state.messages.slice(0, -1), updatedAIMessage],\n copilotkit: {\n ...state[\"copilotkit\"],\n interceptedToolCalls: frontendToolCalls,\n originalAIMessageId: lastMessage.id,\n },\n };\n },\n});\n\n/**\n * Build a CopilotKit middleware instance with custom options.\n *\n * Use this when you want to override the default state-exposure behavior\n * (for example to hide a sensitive key, or to use an explicit allowlist), or\n * to steer the auto-injected `generate_a2ui` subagent via `a2uiParams`.\n *\n * `a2uiParams` is an `A2UIToolParams` without `model` (the middleware always\n * injects the bound model). Use it to override the subagent guidelines\n * (`generationGuidelines` / `designGuidelines` / `compositionGuide`),\n * `defaultCatalogId`, `toolName`, `recovery`, etc. on the auto-inject path —\n * which otherwise only ever uses the toolkit defaults. The registered catalog\n * is still folded in, but host-set values win.\n *\n * @example\n * ```typescript\n * import { createCopilotkitMiddleware } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const middleware = createCopilotkitMiddleware({\n * exposeState: [\"liked\", \"todos\"],\n * a2uiParams: { guidelines: { designGuidelines: \"...repeating-card layout...\" } },\n * });\n * ```\n */\nexport const createCopilotkitMiddleware = (\n options: {\n exposeState?: ExposeStateOption;\n a2uiParams?: Omit<A2UIToolParams, \"model\">;\n } = {},\n) => {\n const exposeState = options.exposeState ?? false;\n return createMiddleware(\n buildMiddlewareInput(exposeState, options.a2uiParams) as any,\n );\n};\n\n/**\n * Default CopilotKit middleware singleton — does NOT surface user state\n * to the LLM. Pass `exposeState: true` (or an allowlist) to\n * {@link createCopilotkitMiddleware} to opt in.\n */\nexport const copilotkitMiddleware = createCopilotkitMiddleware();\n"],"mappings":";;;;;;AAiBA,MAAM,oCAAoB,IAAI,KAAkB;AAChD,MAAM,0BAA0B;AAChC,MAAM,iBAAiB,UACpB,OAAO,aAAwB;;;;;;AAOlC,MAAM,iBAAiB,UACrB,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM;AAEtE,MAAM,mBACJ,MACA,cAEA,OAAO,YACL,MAAM,KAAK,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,KAAK,EAAE,GAAG,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,KACpE,QAAQ;AAMP,QAAO,CAAC,KALM,OAAO,UAAU,eAAe,KAAK,WAAW,IAAI,GAC9D,cAAc,KAAK,KAAK,IAAI,cAAc,UAAU,KAAK,GACvD,gBAAgB,KAAK,MAAM,UAAU,KAAK,GAC1C,UAAU,OACZ,KAAK,KACU;EAEtB,CACF;AAEH,MAAM,uBAAuB,UAA4C;CACvE,MAAM,SAAS,cAAc,MAAM,GAAG,QAAQ,EAAE;AAChD,QAAO,gBACL,cAAc,OAAO,SAAS,GAAG,OAAO,WAAW,EAAE,EACrD,cAAc,OAAO,WAAW,GAAG,OAAO,aAAa,EAAE,CAC1D;;AAGH,MAAM,sBAAsB,UAA4B;AACtD,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI;AACF,SAAO,KAAK,MAAM,MAAM;SAClB;AACN,SAAO;;;;;;;;;;;;;;;;AAiBX,MAAM,sBACJ,UAC6D;CAC7D,MAAM,aAAa,oBAAoB,MAAM;CAC7C,MAAM,aAAa,WAAW;AAC9B,KAAI,YAAY;EACd,IAAI;AACJ,MAAI;AAGF,gBADE,OAAO,eAAe,WAAW,KAAK,MAAM,WAAW,GAAG,aACxC;UACd;AAGR,SAAO,EAAE,WAAW;;CAEtB,MAAM,UAAU,mBAAmB,WAAW,QAAQ;AACtD,MAAK,MAAM,SAAS,MAAM,QAAQ,QAAQ,GAAG,UAAU,EAAE,EAAE;EACzD,MAAM,cAAc,OAAO;EAC3B,MAAM,QAAQ,mBAAmB,OAAO,MAAM;AAC9C,MACE,OAAO,gBAAgB,YACvB,OAAO,UAAU,YACjB,CAAC,YAAY,SAAS,eAAe,IACrC,CAAC,MAED;AAEF,SAAO;GAAE,kBAAkB;GAAO,WADpB,iBAAiB,KAAK,MAAM,GACW;GAAI;;AAE3D,QAAO;;;;;;;;;AAUT,MAAM,sBAAsB,UAC1B,oBAAoB,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AAoC7B,SAAgB,SAA2B,QAA8B;CACvE,MAAM,MAAO,OACX;AAEF,KAAI,OAAO,OAAO,QAAQ,YAAY,EAAE,gBAAgB,MAAM;EAC5D,IAAI;AACJ,MAAI,aAAa,EACf,aAAa;AACX,OAAI,OAAQ,QAAO;AAInB,OAAI;IACF,MAAM,sBACJ,EAGA;AACF,aACE,OAAO,wBAAwB,aAC3B,oBAAoB,OAAO,GAC3B,EAAE;WACF;AACN,aAAS,EAAE;;AAEb,UAAO;KAEV;;AAEH,QAAO;;;;;;;AAQT,MAAM,sBAA2C,IAAI,IAAI;CACvD;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAgBF,MAAM,kBACJ,OACA,WACkB;AAClB,KAAI,WAAW,MAAO,QAAO;CAE7B,MAAM,QAAoC,MAAM,QAAQ,OAAO,GAC3D,IAAI,IAAI,OAAO,GACf;CAEJ,MAAM,WAAoC,EAAE;AAC5C,MAAK,MAAM,OAAO,OAAO,KAAK,MAAM,EAAE;AACpC,MACE,QACI,CAAC,MAAM,IAAI,IAAI,GACf,oBAAoB,IAAI,IAAI,IAAI,IAAI,WAAW,IAAI,CAEvD;EAEF,MAAM,QAAQ,MAAM;AACpB,MACE,UAAU,UACV,UAAU,QACV,UAAU,MACT,MAAM,QAAQ,MAAM,IAAI,MAAM,WAAW,KACzC,OAAO,UAAU,YAChB,CAAC,MAAM,QAAQ,MAAM,IACrB,OAAO,KAAK,MAAiC,CAAC,WAAW,EAE3D;AAEF,WAAS,OAAO;;AAGlB,KAAI,OAAO,KAAK,SAAS,CAAC,WAAW,EAAG,QAAO;CAE/C,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,UAAU,UAAU,MAAM,EAAE;SAClC;AACN,SAAO,OAAO,SAAS;;AAEzB,QAAO,yBAAyB;;AAGlC,MAAM,kBAAkB,SAAc,WAAmC;CACvE,MAAM,OAAO,eACV,QAAQ,SAAS,EAAE,EACpB,OACD;AACD,KAAI,CAAC,KAAM,QAAO;CAElB,MAAM,kBAAkB,QAAQ;AAChC,KAAI,mBAAmB,MAAM;EAC3B,MAAM,YAAY,gBAAgB,SAAS,KAAK,KAAK;AACrD,SAAO;GACL,GAAG;GACH,eAAe,gBAAgB,OAAO,GAAG,YAAY,OAAO;GAC7D;;CAGH,MAAM,WAAW,QAAQ;AACzB,KAAI,YAAY,KACd,QAAO;EAAE,GAAG;EAAS,cAAc;EAAM;CAG3C,MAAM,WACJ,OAAO,aAAa,WAChB,WACA,OAAO,SAAS,YAAY,WAC1B,SAAS,UACT,OAAO,SAAS,QAAQ;AAChC,QAAO;EACL,GAAG;EACH,cAAc,GAAG,SAAS,MAAM;EACjC;;AAGH,MAAM,+BAA+B,OAAO,YAAY;CACtD,MAAM,WAAW,MAAM;AAEvB,KAAI,CAAC,YAAY,SAAS,WAAW,EACnC;CAIF,MAAM,aAAa,oBAAoB,MAAM;CAC7C,MAAM,aAAa,OAAO,UAAU,eAAe,KAAK,YAAY,UAAU,GAC1E,WAAW,UACX,SAAS;AAQb,KAJE,CAAC,cACA,OAAO,eAAe,YAAY,WAAW,MAAM,KAAK,MACxD,OAAO,eAAe,YAAY,OAAO,KAAK,WAAW,CAAC,WAAW,EAGtE;CAQF,MAAM,wBAAwB,iBAH5B,OAAO,eAAe,WAClB,aACA,KAAK,UAAU,YAAY,MAAM,EAAE;CAEzC,MAAM,uBAAuB;CAG7B,MAAM,oBAAoB,QAA4B;AACpD,MAAI,OAAO,IAAI,YAAY,SAAU,QAAO,IAAI;AAChD,MAAI,MAAM,QAAQ,IAAI,QAAQ,IAAI,IAAI,QAAQ,IAAI,KAChD,QAAO,IAAI,QAAQ,GAAG;AACxB,SAAO;;CAKT,IAAI,mBAAmB;AAEvB,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACxC,MAAM,MAAM,SAAS;EACrB,MAAM,OAAO,IAAI,YAAY;AAC7B,MAAI,SAAS,YAAY,SAAS,aAAa;AAG7C,OAFgB,iBAAiB,IAAI,EAExB,WAAW,qBAAqB,CAC3C;AAEF,sBAAmB;AACnB;;;CAKJ,IAAI,uBAAuB;AAC3B,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACxC,MAAM,MAAM,SAAS;EACrB,MAAM,OAAO,IAAI,YAAY;AAC7B,MAAI,SAAS,YAAY,SAAS,aAEhC;OADgB,iBAAiB,IAAI,EACxB,WAAW,qBAAqB,EAAE;AAC7C,2BAAuB;AACvB;;;;CAMN,MAAM,iBAAiB,IAAI,cAAc,EAAE,SAAS,uBAAuB,CAAC;CAE5E,IAAI;AAEJ,KAAI,yBAAyB,IAAI;AAE/B,oBAAkB,CAAC,GAAG,SAAS;AAC/B,kBAAgB,wBAAwB;QACnC;EAEL,MAAM,cAAc,qBAAqB,KAAK,mBAAmB,IAAI;AACrE,oBAAkB;GAChB,GAAG,SAAS,MAAM,GAAG,YAAY;GACjC;GACA,GAAG,SAAS,MAAM,YAAY;GAC/B;;AAGH,QAAO;EACL,GAAG;EACH,UAAU;EACX;;;;;;;;;;;;;;;;;;;;;;;AAwBH,MAAM,wBAAwB,EAC3B,OAAO;CACN,SAAS,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,SAAS,CAAC,CAAC,UAAU,CAAC;CAC/D,YAAY,SACV,EACG,OAAO;EACN,SAAS,EACN,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,QAAQ,MAAM,CAAC,CAAC,CAC3C,UAAU,CACV,UAAU;EACb,SAAS,EAAE,KAAK,CAAC,UAAU;EAC3B,sBAAsB,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,UAAU;EACjD,qBAAqB,EAAE,QAAQ,CAAC,UAAU;EAC3C,CAAC,CACD,aAAa,CACb,UAAU,CACd;CACF,CAAC,CACD,aAAa;AAEhB,MAAM,0BAA0B,UAC9B,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,UACT,MAAM,SAAS,eAAe,MAAM,SAAS;AAEhD,MAAM,uBAAuB,qBAC3B,OAAO,qBAAqB,YAC5B,qBAAqB,QACrB,oBAAoB,oBACpB,iBAAiB,mBAAmB;;;;;;;AAQtC,MAAM,iCACJ,SACA,cACG;CACH,IAAI,UAAU,QAAQ;AACtB,KACE,oBAAoB,QAAQ,kBAAkB,IAC9C,MAAM,QAAQ,QAAQ,CAEtB,WAAU,QAAQ,QAAQ,UAAU,CAAC,uBAAuB,MAAM,CAAC;AAGrE,QAAO,IAAI,UAAU;EACnB;EACA,mBAAmB,QAAQ;EAC3B,mBAAmB,QAAQ;EAC3B,YAAY;EACZ,oBAAoB,QAAQ;EAC5B,gBAAgB,QAAQ;EACxB,IAAI,QAAQ;EACZ,MAAM,QAAQ;EACf,CAAC;;AAGJ,MAAM,wBACJ,aACA,gBACI;CACJ,MAAM;CAEN,aAAa;CAGb,eAAe,OAAO,SAAc,YAAwC;AAC1E,YAAU,eAAe,SAAS,YAAY;EAG9C,MAAM,mBAAmB,qBAAqB;AAC9C,MAAI,OAAO,KAAK,iBAAiB,CAAC,SAAS,GAAG;GAC5C,MAAM,mBAAmB,QAAQ,iBAAiB,EAAE;GACpD,MAAM,kBACH,iBAAiB,WAAsC,EAAE;AAC5D,aAAU;IACR,GAAG;IACH,eAAe;KACb,GAAG;KACH,SAAS;MAAE,GAAG;MAAiB,GAAG;MAAkB;KACrD;IACF;;EAUH,IAAI,WAAgB;EACpB,MAAM,WAAW,mBAAmB,QAAQ,MAAM;AAClD,MAAI,OAAO,iBAAiB,cAAc,UAAU;GAClD,MAAM,UAAU,mBAAmB,QAAQ,MAAM;GAMjD,MAAM,SAAyB;IAC7B,GAAG;IACH,OAAO,QAAQ;IAChB;AACD,OAAI,SAAS,aAAa,OAAO,oBAAoB,KACnD,QAAO,mBAAmB,QAAQ;AAGpC,OAAI,SAAS,kBAAkB;IAC7B,MAAM,aAAa,EAAE,GAAG,OAAO,YAAY;AAC3C,QAAI,WAAW,oBAAoB,KACjC,YAAW,mBAAmB,QAAQ;AACxC,WAAO,aAAa;;GAEtB,MAAM,YAAY,aAAa,OAAO;AAItC,OAAI,CAHkB,IAAI,KACvB,QAAQ,SAAS,EAAE,EAAE,KAAK,MAAW,GAAG,KAAK,CAC/C,CACkB,IAAI,UAAU,KAAK,EAAE;IAItC,MAAM,SAAS,UAAU,OAAO,KAAK,UAAU;AAC/C,cAAU,UAAU,OAAO,WAAW;KACpC,MAAM,QACJ,UAAU,WAAW,UAAU,cAAc,OAAO,MAAM,GACtD,OAAO,QACP,QAAQ;KACd,MAAM,aAAa,oBAAoB,MAAM;KAC7C,MAAM,SAAS,WAAW;KAC1B,MAAM,UAAU,mBAAmB,WAAW,QAAQ;AACtD,YAAO,OAAO,OAAO;MACnB,GAAG;MACH,OAAO;OACL,GAAG;OACH,SAAS;QACP,GAAG;QACH,SAAS,MAAM,QAAQ,QAAQ,GAAG,UAAU,EAAE;QAE9C,aAAa,cAAc,OAAO,GAC9B,KAAK,UAAU,OAAO,GACtB;QACL;OACF;MACF,CAAC;;AAEJ,eAAW;AACX,sBAAkB,IAAI,cAAc,QAAQ,MAAM,EAAE,SAAS;;;EAIjE,MAAM,UAAU,oBAAoB,QAAQ,MAAM,CAAC;EACnD,IAAI,gBAAgB,MAAM,QAAQ,QAAQ,GAAG,UAAU,EAAE;AACzD,MAAI,UAAU;GAGZ,MAAM,OAAO,OAAO,aAAa,WAAW,WAAW;AACvD,mBAAgB,cAAc,QAC3B,OAAY,GAAG,UAAU,QAAQ,GAAG,UAAU,KAChD;;AAGH,MAAI,cAAc,WAAW,KAAK,CAAC,SACjC,QAAO,QAAQ,QAAQ;EAIzB,MAAM,cAAc;GAClB,GAFoB,QAAQ,SAAS,EAAE;GAGvC,GAAI,WAAW,CAAC,SAAS,GAAG,EAAE;GAC9B,GAAG;GACJ;AAED,SAAO,QAAQ;GACb,GAAG;GACH,OAAO;GACR,CAAC;;CAQJ,cAAc,OAAO,SAAc,YAAwC;EACzE,MAAM,OAAO,kBAAkB,IAAI,cAAc,QAAQ,MAAM,CAAC;AAChE,MAAI,QAAQ,CAAC,QAAQ,QAAQ,QAAQ,UAAU,SAAS,KAAK,KAC3D,QAAO,QAAQ;GAAE,GAAG;GAAS;GAAM,CAAC;AAEtC,SAAO,QAAQ,QAAQ;;CAGzB,aAAa;CAGb,aAAa,UAAU;AAGrB,oBAAkB,OAAO,cAAc,MAAM,CAAC;EAE9C,MAAM,uBAAuB,MAAM,eAAe;EAClD,MAAM,oBAAoB,MAAM,eAAe;AAE/C,MAAI,CAAC,sBAAsB,UAAU,CAAC,kBACpC;EAGF,IAAI,eAAe;EACnB,MAAM,kBAAkB,MAAM,SAAS,KAAK,QAAa;AACvD,OAAI,UAAU,WAAW,IAAI,IAAI,IAAI,OAAO,mBAAmB;AAC7D,mBAAe;AAEf,WAAO,8BAA8B,KAAK,CACxC,GAFwB,IAAI,cAAc,EAAE,EAG5C,GAAG,qBACJ,CAAC;;AAEJ,UAAO;IACP;AAGF,MAAI,CAAC,cAAc;AACjB,WAAQ,KACN,8CAA8C,kBAAkB,wBACjE;AACD;;AAGF,SAAO;GACL,UAAU;GACV,YAAY;IACV,GAAG,MAAM;IACT,sBAAsB;IACtB,qBAAqB;IACtB;GACF;;CAIH,aAAa,UAAU;EACrB,MAAM,UAAU,oBAAoB,MAAM,CAAC;EAC3C,MAAM,gBAAgB,MAAM,QAAQ,QAAQ,GAAG,UAAU,EAAE;AAC3D,MAAI,cAAc,WAAW,EAAG;EAEhC,MAAM,oBAAoB,IAAI,IAC5B,cAAc,KAAK,MAAW,EAAE,UAAU,QAAQ,EAAE,KAAK,CAC1D;EAED,MAAM,cAAc,MAAM,SAAS,MAAM,SAAS,SAAS;AAC3D,MAAI,CAAC,UAAU,WAAW,YAAY,IAAI,CAAC,YAAY,YAAY,OACjE;EAGF,MAAM,mBAA0B,EAAE;EAClC,MAAM,oBAA2B,EAAE;AAEnC,OAAK,MAAM,QAAQ,YAAY,WAC7B,KAAI,kBAAkB,IAAI,KAAK,KAAK,CAClC,mBAAkB,KAAK,KAAK;MAE5B,kBAAiB,KAAK,KAAK;AAI/B,MAAI,kBAAkB,WAAW,EAAG;EAEpC,MAAM,mBAAmB,8BACvB,aACA,iBACD;AAED,SAAO;GACL,UAAU,CAAC,GAAG,MAAM,SAAS,MAAM,GAAG,GAAG,EAAE,iBAAiB;GAC5D,YAAY;IACV,GAAG,MAAM;IACT,sBAAsB;IACtB,qBAAqB,YAAY;IAClC;GACF;;CAEJ;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,MAAa,8BACX,UAGI,EAAE,KACH;AAEH,QAAO,iBACL,qBAFkB,QAAQ,eAAe,OAEP,QAAQ,WAAW,CACtD;;;;;;;AAQH,MAAa,uBAAuB,4BAA4B"}
1
+ {"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/langgraph/middleware.ts"],"sourcesContent":["import { createMiddleware, AIMessage } from \"langchain\";\nimport type { InteropZodObject } from \"@langchain/core/utils/types\";\nimport type {\n StandardJSONSchemaV1,\n StandardSchemaV1,\n} from \"@standard-schema/spec\";\nimport * as z from \"zod\";\nimport { getA2UITools } from \"@ag-ui/langgraph\";\nimport type { A2UIToolParams } from \"@ag-ui/langgraph\";\nimport { getForwardedHeaders } from \"../header-propagation\";\n\n// ---------------------------------------------------------------------------\n// Auto-A2UI: bridge the inferred model's generate_a2ui tool from wrapModelCall\n// (the only hook that exposes the bound model) to wrapToolCall (where the tool\n// actually executes but the model is absent). Keyed by the run's thread id so\n// concurrent runs don't clobber each other.\n// ---------------------------------------------------------------------------\nconst a2uiToolsByThread = new Map<string, any>();\nconst A2UI_DEFAULT_THREAD_KEY = \"__copilotkit_a2ui_default__\";\nconst a2uiThreadKey = (state: any): string =>\n (state?.thread_id as string) || A2UI_DEFAULT_THREAD_KEY;\n\n/**\n * Merge AG-UI defaults with CopilotKit overrides. Dictionaries recurse; every\n * conflicting leaf uses CopilotKit, including false, null and empty values.\n * Arrays and serialized strings are atomic: no positional pairing or decoding.\n */\nconst isPropertyBag = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\" && !Array.isArray(value);\n\nconst mergeProperties = (\n base: Record<string, unknown>,\n overrides: Record<string, unknown>,\n): Record<string, unknown> =>\n Object.fromEntries(\n Array.from(new Set([...Object.keys(base), ...Object.keys(overrides)])).map(\n (key) => {\n const value = Object.prototype.hasOwnProperty.call(overrides, key)\n ? isPropertyBag(base[key]) && isPropertyBag(overrides[key])\n ? mergeProperties(base[key], overrides[key])\n : overrides[key]\n : base[key];\n return [key, value];\n },\n ),\n );\n\nconst effectiveProperties = (state: unknown): Record<string, unknown> => {\n const source = isPropertyBag(state) ? state : {};\n return mergeProperties(\n isPropertyBag(source[\"ag-ui\"]) ? source[\"ag-ui\"] : {},\n isPropertyBag(source.copilotkit) ? source.copilotkit : {},\n );\n};\n\nconst decodeCatalogValue = (value: unknown): unknown => {\n if (typeof value !== \"string\") return value;\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n};\n\n/**\n * Find the frontend-registered A2UI catalog wherever it was passed. Returns\n * `{ compositionGuide?, catalogId? }` when a catalog is present, else `null`\n * independently of the explicit tool-injection decision. Two\n * delivery paths, depending on how the agent is served:\n * - AG-UI native endpoint → `state[\"ag-ui\"].a2ui_schema` (JSON\n * `{ catalogId, components }`); the toolkit reads it from state itself.\n * - Runtime proxy → an effective `context` entry describing\n * the A2UI catalog (catalog id + component schemas as text), passed to the\n * subagent via `compositionGuide`.\n * `catalogId` binds generated surfaces to the frontend's catalog so BYOC\n * custom catalogs render their own components (not the basic one).\n */\nconst resolveA2uiCatalog = (\n state: any,\n): { compositionGuide?: string; catalogId?: string } | null => {\n const properties = effectiveProperties(state);\n const a2uiSchema = properties.a2ui_schema;\n if (a2uiSchema) {\n let catalogId: string | undefined;\n try {\n const parsed =\n typeof a2uiSchema === \"string\" ? JSON.parse(a2uiSchema) : a2uiSchema;\n catalogId = parsed?.catalogId;\n } catch {\n // non-JSON schema — fall back to the toolkit's basic catalog\n }\n return { catalogId };\n }\n const context = decodeCatalogValue(properties.context);\n for (const entry of Array.isArray(context) ? context : []) {\n const description = entry?.description;\n const value = decodeCatalogValue(entry?.value);\n if (\n typeof description !== \"string\" ||\n typeof value !== \"string\" ||\n !description.includes(\"A2UI catalog\") ||\n !value\n )\n continue;\n const match = /^\\s*-\\s+(\\S+)/m.exec(value);\n return { compositionGuide: value, catalogId: match?.[1] };\n }\n return null;\n};\n\n/**\n * The A2UI `injectA2UITool` decision. The `@ag-ui/a2ui-middleware` forwards it on\n * `forwardedProps`, which `ag-ui-langgraph` surfaces into agent state at\n * `state[\"ag-ui\"].inject_a2ui_tool` — present only when the host turned the\n * runtime A2UI tool on (truthy or a custom tool-name string). `undefined` means\n * no signal (off, or no A2UI middleware in the pipeline) → no auto-injection.\n */\nconst a2uiInjectDecision = (state: any): boolean | string | undefined =>\n effectiveProperties(state).inject_a2ui_tool as boolean | string | undefined;\n\ntype WithJsonSchema<T> = T extends { \"~standard\": infer S }\n ? Omit<T, \"~standard\"> & {\n \"~standard\": S &\n StandardJSONSchemaV1.Props<\n S extends StandardSchemaV1.Props<infer I, any> ? I : unknown,\n S extends StandardSchemaV1.Props<any, infer O> ? O : unknown\n >;\n }\n : T;\n\n/**\n * Augment a Standard-Schema–compatible schema (e.g. Zod) with a\n * `~standard.jsonSchema.input` hook so LangGraph's\n * `getJsonSchemaFromSchema` (called from `StateSchema.getJsonSchema`)\n * can serialize the field.\n *\n * Without this, Zod v4 fields carry `~standard.validate` + `vendor` only,\n * and `isStandardJSONSchema()` returns false, so the field is silently\n * dropped from the graph's `output_schema`. That makes AG-UI\n * `STATE_SNAPSHOT` events filter the field out of the payload sent to\n * the frontend even though the underlying thread state has the value.\n *\n * Use this on any custom state field you want visible to the frontend\n * via `useAgent().state.*`.\n *\n * @example\n * ```ts\n * import { zodState } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const stateSchema = z.object({\n * todos: zodState(z.array(TodoSchema).default(() => [])),\n * });\n * ```\n */\nexport function zodState<T extends object>(schema: T): WithJsonSchema<T> {\n const std = (schema as { \"~standard\"?: { jsonSchema?: unknown } })[\n \"~standard\"\n ];\n if (std && typeof std === \"object\" && !(\"jsonSchema\" in std)) {\n let cached: Record<string, unknown> | undefined;\n std.jsonSchema = {\n input: () => {\n if (cached) return cached;\n // Prefer zod-v4's native `toJSONSchema` when available. Falls back to\n // an empty object, which is sufficient for the field to appear in the\n // graph's output_schema (langgraph-api treats it as an opaque field).\n try {\n const maybeV4ToJsonSchema = (\n z as unknown as {\n toJSONSchema?: (s: unknown) => Record<string, unknown>;\n }\n ).toJSONSchema;\n cached =\n typeof maybeV4ToJsonSchema === \"function\"\n ? maybeV4ToJsonSchema(schema)\n : {};\n } catch {\n cached = {};\n }\n return cached;\n },\n };\n }\n return schema as WithJsonSchema<T>;\n}\n\n/**\n * Internal/framework state keys that should never be auto-surfaced to the\n * LLM as user-facing state. These are reducer-managed message buckets,\n * CopilotKit/AG-UI plumbing, or graph-internal scaffolding.\n */\nconst RESERVED_STATE_KEYS: ReadonlySet<string> = new Set([\n \"messages\",\n \"copilotkit\",\n \"ag-ui\",\n \"tools\",\n \"structured_response\",\n \"thread_id\",\n \"remaining_steps\",\n]);\n\n/**\n * Controls how user-defined state keys are surfaced into the LLM prompt\n * on every model call. Off by default to avoid leaking arbitrary state\n * into prompts; opt in explicitly.\n *\n * - `false` (default) — never surface state.\n * - `true` — every state key not in the reserved internal set and not\n * prefixed with `_` is JSON-serialized into a \"Current agent state:\"\n * note appended to the system prompt.\n * - `string[]` — only surface the named keys (use this when you want\n * explicit control over what the LLM sees, e.g. `[\"liked\", \"todos\"]`).\n */\nexport type ExposeStateOption = boolean | readonly string[];\n\nconst buildStateNote = (\n state: Record<string, unknown>,\n expose: ExposeStateOption,\n): string | null => {\n if (expose === false) return null;\n\n const allow: ReadonlySet<string> | null = Array.isArray(expose)\n ? new Set(expose)\n : null;\n\n const snapshot: Record<string, unknown> = {};\n for (const key of Object.keys(state)) {\n if (\n allow\n ? !allow.has(key)\n : RESERVED_STATE_KEYS.has(key) || key.startsWith(\"_\")\n ) {\n continue;\n }\n const value = state[key];\n if (\n value === undefined ||\n value === null ||\n value === \"\" ||\n (Array.isArray(value) && value.length === 0) ||\n (typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0)\n ) {\n continue;\n }\n snapshot[key] = value;\n }\n\n if (Object.keys(snapshot).length === 0) return null;\n\n let body: string;\n try {\n body = JSON.stringify(snapshot, null, 2);\n } catch {\n body = String(snapshot);\n }\n return `Current agent state:\\n${body}`;\n};\n\nconst appendSystemNote = (request: any, note: string): any => {\n const existingMessage = request.systemMessage;\n if (existingMessage != null) {\n const separator = existingMessage.text === \"\" ? \"\" : \"\\n\\n\";\n return {\n ...request,\n systemMessage: existingMessage.concat(`${separator}${note}`),\n };\n }\n\n const existing = request.systemPrompt;\n if (existing == null) {\n return { ...request, systemPrompt: note };\n }\n // existing may be a string OR a SystemMessage\n const baseText =\n typeof existing === \"string\"\n ? existing\n : typeof existing.content === \"string\"\n ? existing.content\n : String(existing.content);\n return {\n ...request,\n systemPrompt: `${baseText}\\n\\n${note}`,\n };\n};\n\nconst applyStateNote = (request: any, expose: ExposeStateOption): any => {\n const note = buildStateNote(\n (request.state ?? {}) as Record<string, unknown>,\n expose,\n );\n return note ? appendSystemNote(request, note) : request;\n};\n\nconst APP_CONTEXT_PREFIX = \"App Context:\\n\";\n\n// LangGraph runtime context carries trusted run configuration (thread, tenant\n// and user ids) that must never reach the model, so only a CopilotKit carrier\n// inside it counts: an \"ag-ui\" or \"copilotkit\" namespace, or a legacy\n// unnamespaced carrier with an actions or context key. See #7077.\nconst runtimeCarrierProperties = (\n carrier: unknown,\n namespace: \"ag-ui\" | \"copilotkit\",\n): Record<string, unknown> => {\n if (!isPropertyBag(carrier)) return {};\n const nested = carrier[namespace];\n if (isPropertyBag(nested) && Object.keys(nested).length > 0) return nested;\n if (\n namespace === \"copilotkit\" &&\n (\"actions\" in carrier || \"context\" in carrier)\n )\n return carrier;\n return {};\n};\n\n// Interception bookkeeping alone does not make a state namespace present.\nconst BOOKKEEPING_KEYS = new Set([\n \"interceptedToolCalls\",\n \"originalAIMessageId\",\n]);\n\nconst definedProperties = (value: unknown): Record<string, unknown> =>\n isPropertyBag(value)\n ? Object.fromEntries(\n Object.entries(value).filter(([, entry]) => entry !== undefined),\n )\n : {};\n\n// Each namespace comes from state, or from its runtime carrier when state has\n// none of it. CopilotKit then merges over AG-UI, as in the Python middleware.\nconst contextNamespace = (\n state: unknown,\n runtimeContext: unknown,\n namespace: \"ag-ui\" | \"copilotkit\",\n): Record<string, unknown> => {\n const fromState = definedProperties(\n isPropertyBag(state) ? state[namespace] : undefined,\n );\n return Object.keys(fromState).some((key) => !BOOKKEEPING_KEYS.has(key))\n ? fromState\n : definedProperties(runtimeCarrierProperties(runtimeContext, namespace));\n};\n\nconst buildAppContextNote = (\n state: unknown,\n runtimeContext: unknown,\n): string | null => {\n const properties = mergeProperties(\n contextNamespace(state, runtimeContext, \"ag-ui\"),\n contextNamespace(state, runtimeContext, \"copilotkit\"),\n );\n // Forwarded request headers are transport data, never model context.\n const appContext = isPropertyBag(properties.context)\n ? Object.fromEntries(\n Object.entries(properties.context).filter(\n ([key]) => key !== \"copilotkit_forwarded_headers\",\n ),\n )\n : properties.context;\n\n const isEmptyContext =\n !appContext ||\n (typeof appContext === \"string\" && appContext.trim() === \"\") ||\n (typeof appContext === \"object\" && Object.keys(appContext).length === 0);\n if (isEmptyContext) return null;\n\n const contextContent =\n typeof appContext === \"string\"\n ? appContext\n : JSON.stringify(appContext, null, 2);\n return `${APP_CONTEXT_PREFIX}${contextContent}`;\n};\n\nconst isAppContextMessage = (msg: any): boolean => {\n const type = msg?._getType?.();\n if (type !== \"system\" && type !== \"developer\") return false;\n const content =\n typeof msg.content === \"string\" ? msg.content : msg.content?.[0]?.text;\n return typeof content === \"string\" && content.startsWith(APP_CONTEXT_PREFIX);\n};\n\n// App context goes into the one leading system message, never a message of its\n// own: Anthropic and Gemini reject a second system message. Older releases\n// wrote an \"App Context:\" SystemMessage into the thread, so drop any such\n// message from the model's input too.\nconst applyAppContextNote = (request: any): any => {\n const messages = request.messages ?? [];\n const keptMessages = messages.filter((msg: any) => !isAppContextMessage(msg));\n if (keptMessages.length !== messages.length) {\n request = { ...request, messages: keptMessages };\n }\n\n const note = buildAppContextNote(request.state, request.runtime?.context);\n return note ? appendSystemNote(request, note) : request;\n};\n\n/**\n * CopilotKit Middleware for LangGraph agents.\n *\n * Enables:\n * - Dynamic frontend tools from state.tools\n * - Context provided from CopilotKit useCopilotReadable\n *\n * Works with any agent (prebuilt or custom).\n *\n * @example\n * ```typescript\n * import { createAgent } from \"langchain\";\n * import { copilotkitMiddleware } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const agent = createAgent({\n * model: \"gpt-4o\",\n * tools: [backendTool],\n * middleware: [copilotkitMiddleware],\n * });\n * ```\n */\nconst copilotKitStateSchema = z\n .object({\n \"ag-ui\": zodState(z.record(z.string(), z.unknown()).optional()),\n copilotkit: zodState(\n z\n .object({\n actions: z\n .union([z.array(z.any()), z.literal(false)])\n .nullable()\n .optional(),\n context: z.any().optional(),\n interceptedToolCalls: z.array(z.any()).optional(),\n originalAIMessageId: z.string().optional(),\n })\n .passthrough()\n .optional(),\n ),\n })\n .passthrough();\n\nconst isToolCallContentBlock = (block: unknown) =>\n typeof block === \"object\" &&\n block !== null &&\n \"type\" in block &&\n (block.type === \"tool_call\" || block.type === \"tool_call_chunk\");\n\nconst usesV1ContentBlocks = (responseMetadata: unknown) =>\n typeof responseMetadata === \"object\" &&\n responseMetadata !== null &&\n \"output_version\" in responseMetadata &&\n responseMetadata.output_version === \"v1\";\n\n/**\n * Rebuilds an AIMessage with `toolCalls` as the source of truth while\n * preserving its non-tool content and metadata. For v1 content blocks, old\n * tool blocks must be removed before construction so they cannot duplicate or\n * override the supplied tool calls when AIMessage synchronizes both fields.\n */\nconst rebuildAIMessageWithToolCalls = (\n message: AIMessage,\n toolCalls: AIMessage[\"tool_calls\"],\n) => {\n let content = message.content;\n if (\n usesV1ContentBlocks(message.response_metadata) &&\n Array.isArray(content)\n ) {\n content = content.filter((block) => !isToolCallContentBlock(block));\n }\n\n return new AIMessage({\n content,\n additional_kwargs: message.additional_kwargs,\n response_metadata: message.response_metadata,\n tool_calls: toolCalls,\n invalid_tool_calls: message.invalid_tool_calls,\n usage_metadata: message.usage_metadata,\n id: message.id,\n name: message.name,\n });\n};\n\nconst buildMiddlewareInput = (\n exposeState: ExposeStateOption,\n a2uiParams?: Omit<A2UIToolParams, \"model\">,\n) => ({\n name: \"CopilotKitMiddleware\",\n\n stateSchema: copilotKitStateSchema as unknown as InteropZodObject,\n\n // Inject frontend tools, surface user state and app context, and forward\n // x-aimock-* headers\n wrapModelCall: async (request: any, handler: (req: any) => Promise<any>) => {\n request = applyStateNote(request, exposeState);\n request = applyAppContextNote(request);\n\n // Forward x-aimock-* headers from the incoming AG-UI request\n const forwardedHeaders = getForwardedHeaders();\n if (Object.keys(forwardedHeaders).length > 0) {\n const existingSettings = request.modelSettings ?? {};\n const existingHeaders =\n (existingSettings.headers as Record<string, string>) ?? {};\n request = {\n ...request,\n modelSettings: {\n ...existingSettings,\n headers: { ...existingHeaders, ...forwardedHeaders },\n },\n };\n }\n\n // Opt-in auto-injection of generate_a2ui:\n // (1) only inject when the A2UI injectA2UITool flag is truthy (forwarded by\n // @ag-ui/a2ui-middleware and surfaced at state[\"ag-ui\"].inject_a2ui_tool);\n // (2) don't double-inject if the agent already defines this tool.\n // The catalog (when present) only binds surfaces to the FE's catalog; it is\n // not the gate. The model is inferred from request.model; the built tool is\n // stashed for wrapToolCall to execute.\n let a2uiTool: any = null;\n const decision = a2uiInjectDecision(request.state);\n if (typeof getA2UITools === \"function\" && decision) {\n const catalog = resolveA2uiCatalog(request.state);\n // Shared A2UIToolParams: a single params object owned by the toolkit.\n // Start from the host overrides (guidelines / catalog id / tool name /\n // recovery) so a host can steer the subagent, then layer in only what the\n // host cannot know — the bound model, and the registered catalog id +\n // compositionGuide — without clobbering any host-set value.\n const params: A2UIToolParams = {\n ...a2uiParams,\n model: request.model,\n };\n if (catalog?.catalogId && params.defaultCatalogId == null)\n params.defaultCatalogId = catalog.catalogId;\n // Merge the registered catalog schema into any host `guidelines` bag; a\n // host-set compositionGuide wins, host generation/design overrides stay.\n if (catalog?.compositionGuide) {\n const guidelines = { ...params.guidelines };\n if (guidelines.compositionGuide == null)\n guidelines.compositionGuide = catalog.compositionGuide;\n params.guidelines = guidelines;\n }\n const candidate = getA2UITools(params);\n const existingNames = new Set(\n (request.tools || []).map((t: any) => t?.name),\n );\n if (!existingNames.has(candidate.name)) {\n // ToolNode derives tool runtime state from its config, ignoring an\n // overridden middleware request.state. Adapt the tool's invocation\n // config so the toolkit sees the same effective properties.\n const invoke = candidate.invoke.bind(candidate);\n candidate.invoke = (input, config) => {\n const state =\n config && \"state\" in config && isPropertyBag(config.state)\n ? config.state\n : request.state;\n const properties = effectiveProperties(state);\n const schema = properties.a2ui_schema;\n const context = decodeCatalogValue(properties.context);\n return invoke(input, {\n ...config,\n state: {\n ...state,\n \"ag-ui\": {\n ...properties,\n context: Array.isArray(context) ? context : [],\n // The toolkit interpolates the native schema into its prompt.\n a2ui_schema: isPropertyBag(schema)\n ? JSON.stringify(schema)\n : schema,\n },\n },\n });\n };\n a2uiTool = candidate;\n a2uiToolsByThread.set(a2uiThreadKey(request.state), a2uiTool);\n }\n }\n\n const actions = effectiveProperties(request.state).actions;\n let frontendTools = Array.isArray(actions) ? actions : [];\n if (a2uiTool) {\n // Our generate_a2ui replaces the runtime's render tool — don't advertise\n // both. Drop the render tool the A2UI middleware injected.\n const drop = typeof decision === \"string\" ? decision : \"render_a2ui\";\n frontendTools = frontendTools.filter(\n (t: any) => (t?.function?.name ?? t?.name) !== drop,\n );\n }\n\n if (frontendTools.length === 0 && !a2uiTool) {\n return handler(request);\n }\n\n const existingTools = request.tools || [];\n const mergedTools = [\n ...existingTools,\n ...(a2uiTool ? [a2uiTool] : []),\n ...frontendTools,\n ];\n\n return handler({\n ...request,\n tools: mergedTools,\n });\n },\n\n // Execute the dynamically-advertised generate_a2ui tool. It is not in the\n // agent's static tool registry, so the tool node cannot run it on its own;\n // we supply the implementation (built with the inferred model) for that one\n // tool. This hook's presence also disables createAgent's \"unknown tool\"\n // guard for dynamically-advertised tools.\n wrapToolCall: async (request: any, handler: (req: any) => Promise<any>) => {\n const tool = a2uiToolsByThread.get(a2uiThreadKey(request.state));\n if (tool && !request.tool && request.toolCall?.name === tool.name) {\n return handler({ ...request, tool });\n }\n return handler(request);\n },\n\n // Restore frontend tool calls to AIMessage before agent exits\n afterAgent: (state) => {\n // Drop the bridged A2UI tool for this run — all tool calls for the turn\n // have executed by now; the next model call re-stashes if needed.\n a2uiToolsByThread.delete(a2uiThreadKey(state));\n\n const interceptedToolCalls = state[\"copilotkit\"]?.interceptedToolCalls;\n const originalMessageId = state[\"copilotkit\"]?.originalAIMessageId;\n\n if (!interceptedToolCalls?.length || !originalMessageId) {\n return;\n }\n\n let messageFound = false;\n const updatedMessages = state.messages.map((msg: any) => {\n if (AIMessage.isInstance(msg) && msg.id === originalMessageId) {\n messageFound = true;\n const existingToolCalls = msg.tool_calls || [];\n return rebuildAIMessageWithToolCalls(msg, [\n ...existingToolCalls,\n ...interceptedToolCalls,\n ]);\n }\n return msg;\n });\n\n // Only clear intercepted state if we successfully restored the tool calls\n if (!messageFound) {\n console.warn(\n `CopilotKit: Could not find message with id ${originalMessageId} to restore tool calls`,\n );\n return;\n }\n\n return {\n messages: updatedMessages,\n copilotkit: {\n ...state[\"copilotkit\"],\n interceptedToolCalls: undefined,\n originalAIMessageId: undefined,\n },\n };\n },\n\n // Intercept frontend tool calls after model returns, before ToolNode executes\n afterModel: (state) => {\n const actions = effectiveProperties(state).actions;\n const frontendTools = Array.isArray(actions) ? actions : [];\n if (frontendTools.length === 0) return;\n\n const frontendToolNames = new Set(\n frontendTools.map((t: any) => t.function?.name || t.name),\n );\n\n const lastMessage = state.messages[state.messages.length - 1];\n if (!AIMessage.isInstance(lastMessage) || !lastMessage.tool_calls?.length) {\n return;\n }\n\n const backendToolCalls: any[] = [];\n const frontendToolCalls: any[] = [];\n\n for (const call of lastMessage.tool_calls) {\n if (frontendToolNames.has(call.name)) {\n frontendToolCalls.push(call);\n } else {\n backendToolCalls.push(call);\n }\n }\n\n if (frontendToolCalls.length === 0) return;\n\n const updatedAIMessage = rebuildAIMessageWithToolCalls(\n lastMessage,\n backendToolCalls,\n );\n\n return {\n messages: [...state.messages.slice(0, -1), updatedAIMessage],\n copilotkit: {\n ...state[\"copilotkit\"],\n interceptedToolCalls: frontendToolCalls,\n originalAIMessageId: lastMessage.id,\n },\n };\n },\n});\n\n/**\n * Build a CopilotKit middleware instance with custom options.\n *\n * Use this when you want to override the default state-exposure behavior\n * (for example to hide a sensitive key, or to use an explicit allowlist), or\n * to steer the auto-injected `generate_a2ui` subagent via `a2uiParams`.\n *\n * `a2uiParams` is an `A2UIToolParams` without `model` (the middleware always\n * injects the bound model). Use it to override the subagent guidelines\n * (`generationGuidelines` / `designGuidelines` / `compositionGuide`),\n * `defaultCatalogId`, `toolName`, `recovery`, etc. on the auto-inject path —\n * which otherwise only ever uses the toolkit defaults. The registered catalog\n * is still folded in, but host-set values win.\n *\n * @example\n * ```typescript\n * import { createCopilotkitMiddleware } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const middleware = createCopilotkitMiddleware({\n * exposeState: [\"liked\", \"todos\"],\n * a2uiParams: { guidelines: { designGuidelines: \"...repeating-card layout...\" } },\n * });\n * ```\n */\nexport const createCopilotkitMiddleware = (\n options: {\n exposeState?: ExposeStateOption;\n a2uiParams?: Omit<A2UIToolParams, \"model\">;\n } = {},\n) => {\n const exposeState = options.exposeState ?? false;\n return createMiddleware(\n buildMiddlewareInput(exposeState, options.a2uiParams) as any,\n );\n};\n\n/**\n * Default CopilotKit middleware singleton — does NOT surface user state\n * to the LLM. Pass `exposeState: true` (or an allowlist) to\n * {@link createCopilotkitMiddleware} to opt in.\n */\nexport const copilotkitMiddleware = createCopilotkitMiddleware();\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/sdk-js",
3
- "version": "1.78.0",
3
+ "version": "1.79.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "@ag-ui/langgraph": "0.0.44",
52
52
  "@standard-schema/spec": "^1.0.0",
53
- "@copilotkit/shared": "1.78.0"
53
+ "@copilotkit/shared": "1.79.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@langchain/core": "^1.1.41",
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { AIMessage, HumanMessage } from "@langchain/core/messages";
2
+ import { AIMessage } from "@langchain/core/messages";
3
3
  import { StateGraph, START, END } from "@langchain/langgraph";
4
4
  import type { A2UIToolParams } from "@ag-ui/langgraph";
5
5
 
@@ -41,11 +41,15 @@ async function wrap(state: object) {
41
41
  });
42
42
  return received;
43
43
  }
44
- function context(state: object, runtime = {}) {
45
- return middleware.beforeAgent(
46
- { messages: [new HumanMessage("hello")], ...state },
47
- runtime,
48
- );
44
+ // The App Context note the model sees, folded into its system prompt.
45
+ async function context(state: object, runtime = {}) {
46
+ const request = { state, model: {}, tools: [], messages: [], runtime };
47
+ let received: { systemPrompt?: string } = request;
48
+ await middleware.wrapModelCall(request, async (req) => {
49
+ received = req;
50
+ return new AIMessage("ok");
51
+ });
52
+ return received.systemPrompt;
49
53
  }
50
54
 
51
55
  beforeEach(() => {
@@ -109,7 +113,7 @@ describe("effective AG-UI and CopilotKit properties", () => {
109
113
  };
110
114
  await wrap(state);
111
115
  expect(captured[0].defaultCatalogId).toBe("serialized");
112
- expect(context(state).messages[0].content).toBe(
116
+ expect(await context(state)).toBe(
113
117
  "App Context:\n" + state["ag-ui"].context,
114
118
  );
115
119
  });
@@ -129,7 +133,7 @@ describe("effective AG-UI and CopilotKit properties", () => {
129
133
  expect(captured[0].defaultCatalogId).toBeUndefined();
130
134
  });
131
135
 
132
- it("merges nested context while preserving conflicting scalar and array leaves", () => {
136
+ it("merges nested context while preserving conflicting scalar and array leaves", async () => {
133
137
  const state = {
134
138
  "ag-ui": {
135
139
  context: {
@@ -145,8 +149,7 @@ describe("effective AG-UI and CopilotKit properties", () => {
145
149
  },
146
150
  };
147
151
  const before = structuredClone(state);
148
- const result = context(state);
149
- expect(result.messages[0].content).toBe(
152
+ expect(await context(state)).toBe(
150
153
  "App Context:\n" +
151
154
  JSON.stringify(
152
155
  {
@@ -167,26 +170,92 @@ describe("effective AG-UI and CopilotKit properties", () => {
167
170
 
168
171
  it.each([null, false, "", []])(
169
172
  "does not resurrect AG-UI or runtime context after an explicit %j override",
170
- (value) => {
173
+ async (value) => {
171
174
  expect(
172
- context(
175
+ await context(
173
176
  { "ag-ui": { context: "base" }, copilotkit: { context: value } },
174
- { context: "runtime" },
177
+ { context: { copilotkit: { context: "runtime" } } },
175
178
  ),
176
179
  ).toBeUndefined();
177
180
  },
178
181
  );
179
182
 
180
- it("keeps serialized context atomic and preserves the runtime fallback", () => {
183
+ // Each namespace comes from state, or from its runtime carrier when state has
184
+ // none, before CopilotKit merges over AG-UI (the Python resolution order).
185
+ it.each([
186
+ [
187
+ "an AG-UI state value loses to a CopilotKit runtime carrier",
188
+ { "ag-ui": { context: "page" } },
189
+ { copilotkit: { context: "fresh" } },
190
+ "fresh",
191
+ ],
192
+ [
193
+ "an undefined state value counts as absent",
194
+ { "ag-ui": { context: "page" }, copilotkit: { context: undefined } },
195
+ { copilotkit: { context: "fresh" } },
196
+ "fresh",
197
+ ],
198
+ [
199
+ "interception bookkeeping alone counts as absent",
200
+ { copilotkit: { interceptedToolCalls: [], originalAIMessageId: "m" } },
201
+ { copilotkit: { context: "fresh" } },
202
+ "fresh",
203
+ ],
204
+ [
205
+ "a CopilotKit state namespace keeps its runtime carrier out",
206
+ { copilotkit: { actions: [] } },
207
+ { copilotkit: { context: "fresh" } },
208
+ undefined,
209
+ ],
210
+ ])(
211
+ "resolves context per namespace: %s",
212
+ async (_name, state, carrier, note) => {
213
+ expect(await context(state, { context: carrier })).toBe(
214
+ note && "App Context:\n" + note,
215
+ );
216
+ },
217
+ );
218
+
219
+ it.each([
220
+ ["state", { copilotkit: { context: "CTX" } }, {}],
221
+ ["a runtime carrier", {}, { context: { copilotkit: { context: "CTX" } } }],
222
+ ])(
223
+ "never renders forwarded headers from %s",
224
+ async (_name, state, runtime) => {
225
+ const value = {
226
+ page: "/dashboard",
227
+ copilotkit_forwarded_headers: { authorization: "Bearer secret" },
228
+ };
229
+ const swap = (carrier: object) =>
230
+ JSON.parse(
231
+ JSON.stringify(carrier).replace('"CTX"', JSON.stringify(value)),
232
+ );
233
+
234
+ expect(await context(swap(state), swap(runtime))).toBe(
235
+ "App Context:\n" + JSON.stringify({ page: "/dashboard" }, null, 2),
236
+ );
237
+ },
238
+ );
239
+
240
+ it("renders nothing when forwarded headers are the only context", async () => {
241
+ expect(
242
+ await context({
243
+ copilotkit: { context: { copilotkit_forwarded_headers: { a: "b" } } },
244
+ }),
245
+ ).toBeUndefined();
246
+ });
247
+
248
+ it("keeps serialized context atomic and reads only a namespaced runtime carrier", async () => {
181
249
  expect(
182
- context({
250
+ await context({
183
251
  "ag-ui": { context: { base: 1 } },
184
252
  copilotkit: { context: '{"override":2}' },
185
- }).messages[0].content,
253
+ }),
186
254
  ).toBe('App Context:\n{"override":2}');
187
- expect(context({}, { context: "runtime" }).messages[0].content).toBe(
188
- "App Context:\nruntime",
189
- );
255
+ expect(
256
+ await context({}, { context: { copilotkit: { context: "runtime" } } }),
257
+ ).toBe("App Context:\nruntime");
258
+ expect(await context({}, { context: "runtime" })).toBeUndefined();
190
259
  });
191
260
 
192
261
  it.each(["ag-ui", "copilotkit"])(
@@ -7,8 +7,10 @@
7
7
  *
8
8
  * - Frontend tools listed in `state.copilotkit.actions` reach the model
9
9
  * alongside the agent's own tools. Empty actions = no change.
10
- * - App context from `state.copilotkit.context` (or runtime.context) becomes
11
- * a SystemMessage `"App Context:\n<json>"`. Idempotent across re-runs.
10
+ * - App context from `state.copilotkit.context` (or a namespaced
11
+ * runtime.context carrier, never the raw runtime.context) is appended to
12
+ * the leading system message as `"App Context:\n<json>"`.
13
+ * The model never sees a second system message.
12
14
  * - `afterModel` peels frontend tool calls off the last AIMessage so the
13
15
  * ToolNode does not execute them; `afterAgent` re-attaches them.
14
16
  * - The opt-in `exposeState` knob surfaces user state into
@@ -25,7 +27,11 @@ import {
25
27
  SystemMessage,
26
28
  } from "@langchain/core/messages";
27
29
  import { FakeListChatModel } from "@langchain/core/utils/testing";
28
- import { createAgent, createMiddleware } from "langchain";
30
+ import {
31
+ createAgent,
32
+ createMiddleware,
33
+ dynamicSystemPromptMiddleware,
34
+ } from "langchain";
29
35
 
30
36
  import {
31
37
  copilotkitMiddleware,
@@ -64,7 +70,7 @@ function systemContents(messages: any[]): string[] {
64
70
  const out: string[] = [];
65
71
  for (const m of messages) {
66
72
  if (m._getType?.() === "system") {
67
- out.push(typeof m.content === "string" ? m.content : String(m.content));
73
+ out.push(typeof m.content === "string" ? m.content : m.text);
68
74
  }
69
75
  }
70
76
  return out;
@@ -406,61 +412,202 @@ describe("exposeState", () => {
406
412
  });
407
413
 
408
414
  // ---------------------------------------------------------------------------
409
- // beforeAgent — App Context injection
415
+ // App Context — folded into the single leading system message
410
416
  // ---------------------------------------------------------------------------
411
417
 
412
- describe("beforeAgent", () => {
413
- it("returns no update when context is empty", () => {
414
- const state = {
415
- messages: [new HumanMessage("hi")],
416
- copilotkit: { context: [] },
417
- };
418
- const result = copilotkitMiddleware.beforeAgent(state, {} as any);
419
- expect(result).toBeUndefined();
418
+ describe("app context in an agent run", () => {
419
+ // Anthropic and Gemini accept one leading system instruction only, so the
420
+ // model must never see a second system message.
421
+ async function runTurns(turns: number, context: unknown) {
422
+ const model = new CapturingFakeListChatModel({
423
+ responses: Array.from({ length: turns }, () => "ok"),
424
+ });
425
+ const agent = createAgent({
426
+ model,
427
+ tools: [],
428
+ systemPrompt: "You are a helpful assistant.",
429
+ middleware: [copilotkitMiddleware],
430
+ });
431
+ let messages: any[] = [];
432
+ for (let turn = 0; turn < turns; turn++) {
433
+ const result = await agent.invoke({
434
+ messages: [...messages, new HumanMessage(`turn ${turn}`)],
435
+ copilotkit: { context },
436
+ } as any);
437
+ messages = result.messages;
438
+ }
439
+ return { model, messages };
440
+ }
441
+
442
+ it("sends exactly one system message, first, carrying prompt and context", async () => {
443
+ const { model } = await runTurns(2, [
444
+ { description: "viewer role", value: "admin" },
445
+ ]);
446
+
447
+ expect(model.receivedMessages).toHaveLength(2);
448
+ for (const received of model.receivedMessages) {
449
+ const types = received.map((m) => m._getType());
450
+ expect(types.filter((t) => t === "system")).toHaveLength(1);
451
+ expect(types[0]).toBe("system");
452
+ const [system] = systemContents(received);
453
+ expect(system.startsWith("You are a helpful assistant.\n\n")).toBe(true);
454
+ expect(system).toContain("App Context:\n");
455
+ expect(system).toContain("admin");
456
+ }
420
457
  });
421
458
 
422
- it("injects an App Context SystemMessage into the message list", () => {
423
- const state = {
459
+ it("puts the state note before the context in the one system message", async () => {
460
+ const model = new CapturingFakeListChatModel({ responses: ["ok"] });
461
+ const agent = createAgent({
462
+ model,
463
+ tools: [],
464
+ systemPrompt: "You are a helpful assistant.",
465
+ stateSchema: z.object({ liked: z.array(z.string()).optional() }),
466
+ middleware: [createCopilotkitMiddleware({ exposeState: ["liked"] })],
467
+ });
468
+
469
+ await agent.invoke({
424
470
  messages: [new HumanMessage("hi")],
425
- copilotkit: {
426
- context: [{ description: "viewer role", value: "admin" }],
427
- },
428
- };
471
+ liked: ["tea"],
472
+ copilotkit: { context: "route=/dashboard" },
473
+ } as any);
474
+
475
+ const [received] = model.receivedMessages;
476
+ expect(received.map((m) => m._getType())).toEqual(["system", "human"]);
477
+ const [system] = systemContents(received);
478
+ expect(system.indexOf("You are a helpful assistant.")).toBe(0);
479
+ expect(system.indexOf("Current agent state:")).toBeGreaterThan(0);
480
+ expect(system.indexOf("App Context:\nroute=/dashboard")).toBeGreaterThan(
481
+ system.indexOf("Current agent state:"),
482
+ );
483
+ });
484
+
485
+ it("returns the request unchanged when context is empty", async () => {
486
+ const request = makeRequest({
487
+ state: { messages: [], copilotkit: { context: [] } },
488
+ });
429
489
 
430
- const result = copilotkitMiddleware.beforeAgent(state, {} as any);
490
+ const { received } = await runWrap(copilotkitMiddleware, request);
431
491
 
432
- expect(result).toBeDefined();
433
- const sys = systemContents(result!.messages);
434
- expect(sys.some((s) => s.startsWith("App Context:"))).toBe(true);
435
- expect(sys.some((s) => s.includes("admin"))).toBe(true);
492
+ expect(received.systemPrompt).toBeUndefined();
436
493
  });
437
494
 
438
- it("uses runtime.context when state.copilotkit.context is missing", () => {
439
- const state = {
440
- messages: [new HumanMessage("hi")],
441
- copilotkit: {},
442
- };
443
- const runtime = { context: "route=/dashboard" };
495
+ it("never puts a raw runtime.context into the prompt", async () => {
496
+ // LangGraph runtime context carries trusted run configuration (thread,
497
+ // tenant and user ids); it must not reach the model. See #7077.
498
+ for (const context of [
499
+ "route=/dashboard",
500
+ { thread_id: "t-1", user_id: "u-1" },
501
+ ]) {
502
+ const request = makeRequest({
503
+ state: { messages: [], copilotkit: {} },
504
+ runtime: { context },
505
+ });
444
506
 
445
- const result = copilotkitMiddleware.beforeAgent(state, runtime as any);
507
+ const { received } = await runWrap(copilotkitMiddleware, request);
446
508
 
447
- const sys = systemContents(result!.messages);
448
- expect(sys.some((s) => s.includes("/dashboard"))).toBe(true);
509
+ expect(received.systemPrompt).toBeUndefined();
510
+ }
449
511
  });
450
512
 
451
- it("does not duplicate the App Context message across re-runs", () => {
452
- const state = {
453
- messages: [new HumanMessage("hi")],
454
- copilotkit: { context: [{ description: "k", value: "v" }] },
455
- };
513
+ it.each([
514
+ ["copilotkit", { copilotkit: { context: "route=/dashboard" } }],
515
+ ["ag-ui", { "ag-ui": { context: "route=/dashboard" } }],
516
+ ["legacy unnamespaced", { context: "route=/dashboard", user_id: "u-1" }],
517
+ ])(
518
+ "reads context from a %s runtime.context carrier when state has none",
519
+ async (_name, context) => {
520
+ const request = makeRequest({
521
+ state: { messages: [] },
522
+ runtime: { context },
523
+ });
456
524
 
457
- const first = copilotkitMiddleware.beforeAgent(state, {} as any) ?? state;
458
- const second = copilotkitMiddleware.beforeAgent(first, {} as any) ?? first;
525
+ const { received } = await runWrap(copilotkitMiddleware, request);
459
526
 
460
- const appContextMessages = systemContents(second.messages).filter((s) =>
461
- s.startsWith("App Context:"),
462
- );
463
- expect(appContextMessages).toHaveLength(1);
527
+ expect(systemPromptText(received)).toBe("App Context:\nroute=/dashboard");
528
+ },
529
+ );
530
+
531
+ it("drops an App Context message that an older release saved in the thread", async () => {
532
+ const request = makeRequest({
533
+ messages: [
534
+ new SystemMessage("App Context:\nstale"),
535
+ new HumanMessage("hi"),
536
+ ],
537
+ state: { messages: [], copilotkit: { context: "fresh" } },
538
+ });
539
+
540
+ const { received } = await runWrap(copilotkitMiddleware, request);
541
+
542
+ expect(systemContents(received.messages)).toEqual([]);
543
+ expect(systemPromptText(received)).toBe("App Context:\nfresh");
544
+ });
545
+
546
+ it("does not write the context into the thread's messages", async () => {
547
+ const { messages } = await runTurns(1, "route=/dashboard");
548
+
549
+ expect(systemContents(messages)).toEqual([]);
550
+ });
551
+
552
+ // The context lives only on request.systemMessage. A middleware listed after
553
+ // copilotkitMiddleware runs inside it, so it decides what reaches the model.
554
+ describe("with a second system-prompt middleware", () => {
555
+ const replaceSystemMessage = createMiddleware({
556
+ name: "ReplaceSystemMessage",
557
+ wrapModelCall: (request: any, handler: any) =>
558
+ handler({
559
+ ...request,
560
+ systemMessage: new SystemMessage("Replaced prompt."),
561
+ }),
562
+ });
563
+
564
+ async function systemSeenByModel(middleware: any[]) {
565
+ const model = new CapturingFakeListChatModel({ responses: ["ok"] });
566
+ const agent = createAgent({
567
+ model,
568
+ tools: [],
569
+ systemPrompt: "You are a helpful assistant.",
570
+ middleware,
571
+ });
572
+ await agent.invoke({
573
+ messages: [new HumanMessage("hi")],
574
+ copilotkit: { context: "route=/dashboard" },
575
+ } as any);
576
+ const [received] = model.receivedMessages;
577
+ expect(systemContents(received)).toHaveLength(1);
578
+ return systemContents(received)[0];
579
+ }
580
+
581
+ it("keeps the context when a later middleware appends to the prompt", async () => {
582
+ const system = await systemSeenByModel([
583
+ copilotkitMiddleware,
584
+ dynamicSystemPromptMiddleware(() => "Answer in French."),
585
+ ]);
586
+
587
+ expect(system).toContain("App Context:\nroute=/dashboard");
588
+ expect(system).toContain("Answer in French.");
589
+ });
590
+
591
+ it("keeps the context when an earlier middleware replaces the prompt", async () => {
592
+ const system = await systemSeenByModel([
593
+ replaceSystemMessage,
594
+ copilotkitMiddleware,
595
+ ]);
596
+
597
+ expect(system).toBe("Replaced prompt.\n\nApp Context:\nroute=/dashboard");
598
+ });
599
+
600
+ it("loses the context when a later middleware replaces the prompt", async () => {
601
+ // Known limit: older releases kept the context in a thread message, so it
602
+ // survived this. List copilotkitMiddleware after any middleware that
603
+ // replaces the system message.
604
+ const system = await systemSeenByModel([
605
+ copilotkitMiddleware,
606
+ replaceSystemMessage,
607
+ ]);
608
+
609
+ expect(system).toBe("Replaced prompt.");
610
+ });
464
611
  });
465
612
  });
466
613