@copilotkit/sdk-js 1.59.2 → 1.59.3-alpha.2

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.
@@ -3,8 +3,58 @@ const require_header_propagation = require('../header-propagation.cjs');
3
3
  let langchain = require("langchain");
4
4
  let zod = require("zod");
5
5
  zod = require_runtime.__toESM(zod);
6
+ let _ag_ui_langgraph = require("@ag-ui/langgraph");
6
7
 
7
8
  //#region src/langgraph/middleware.ts
9
+ const a2uiToolsByThread = /* @__PURE__ */ new Map();
10
+ const A2UI_DEFAULT_THREAD_KEY = "__copilotkit_a2ui_default__";
11
+ const a2uiThreadKey = (state) => state?.thread_id || A2UI_DEFAULT_THREAD_KEY;
12
+ /**
13
+ * Find the frontend-registered A2UI catalog wherever it was passed. Returns
14
+ * `{ compositionGuide?, catalogId? }` when a catalog is present, else `null`
15
+ * (so the tool is never advertised when the client can't render A2UI). Two
16
+ * delivery paths, depending on how the agent is served:
17
+ * - AG-UI native endpoint → `state["ag-ui"].a2ui_schema` (JSON
18
+ * `{ catalogId, components }`); the toolkit reads it from state itself.
19
+ * - CopilotKit runtime proxy → a `state.copilotkit.context` entry describing
20
+ * the A2UI catalog (catalog id + component schemas as text), passed to the
21
+ * subagent via `compositionGuide`.
22
+ * `catalogId` binds generated surfaces to the frontend's catalog so BYOC
23
+ * custom catalogs render their own components (not the basic one).
24
+ */
25
+ const resolveA2uiCatalog = (state) => {
26
+ const a2uiSchema = state?.["ag-ui"]?.a2ui_schema;
27
+ if (a2uiSchema) {
28
+ let catalogId;
29
+ try {
30
+ catalogId = (typeof a2uiSchema === "string" ? JSON.parse(a2uiSchema) : a2uiSchema)?.catalogId;
31
+ } catch {}
32
+ return { catalogId };
33
+ }
34
+ const context = state?.copilotkit?.context;
35
+ for (const entry of Array.isArray(context) ? context : []) {
36
+ const description = entry?.description ?? "";
37
+ const value = entry?.value ?? "";
38
+ if (!description.includes("A2UI catalog") || !value) continue;
39
+ return {
40
+ compositionGuide: value,
41
+ catalogId: /^\s*-\s+(\S+)/m.exec(value)?.[1]
42
+ };
43
+ }
44
+ return null;
45
+ };
46
+ /**
47
+ * The runtime's `injectA2UITool` decision, forwarded as
48
+ * `state.copilotkit.a2ui = { injectTool: boolean | string }` whenever
49
+ * CopilotRuntime is configured with an `a2ui` option. Returns `undefined` when
50
+ * there is no runtime signal (AG-UI native path / no A2UI config), in which
51
+ * case the middleware falls back to its catalog-gated default. A falsy value
52
+ * is the host explicitly opting out.
53
+ */
54
+ const a2uiInjectDecision = (state) => {
55
+ const a2ui = state?.copilotkit?.a2ui;
56
+ if (a2ui && typeof a2ui === "object" && "injectTool" in a2ui) return a2ui.injectTool;
57
+ };
8
58
  /**
9
59
  * Augment a Standard-Schema–compatible schema (e.g. Zod) with a
10
60
  * `~standard.jsonSchema.input` hook so LangGraph's
@@ -168,6 +218,7 @@ const createAppContextBeforeAgent = (state, runtime) => {
168
218
  const copilotKitStateSchema = zod.object({ copilotkit: zodState(zod.object({
169
219
  actions: zod.array(zod.any()),
170
220
  context: zod.any().optional(),
221
+ a2ui: zod.any().optional(),
171
222
  interceptedToolCalls: zod.array(zod.any()).optional(),
172
223
  originalAIMessageId: zod.string().optional()
173
224
  }).optional()) });
@@ -191,16 +242,46 @@ const buildMiddlewareInput = (exposeState) => ({
191
242
  }
192
243
  };
193
244
  }
194
- const frontendTools = request.state["copilotkit"]?.actions ?? [];
195
- if (frontendTools.length === 0) return handler(request);
196
- const mergedTools = [...request.tools || [], ...frontendTools];
245
+ let a2uiTool = null;
246
+ const decision = a2uiInjectDecision(request.state);
247
+ const a2uiCatalog = typeof _ag_ui_langgraph.getA2UITools === "function" && !(decision !== void 0 && !decision) ? resolveA2uiCatalog(request.state) : null;
248
+ if (a2uiCatalog) {
249
+ const opts = {};
250
+ if (a2uiCatalog.catalogId) opts.defaultCatalogId = a2uiCatalog.catalogId;
251
+ if (a2uiCatalog.compositionGuide) opts.compositionGuide = a2uiCatalog.compositionGuide;
252
+ const candidate = (0, _ag_ui_langgraph.getA2UITools)(request.model, opts);
253
+ if (!new Set((request.tools || []).map((t) => t?.name)).has(candidate.name)) {
254
+ a2uiTool = candidate;
255
+ a2uiToolsByThread.set(a2uiThreadKey(request.state), a2uiTool);
256
+ }
257
+ }
258
+ let frontendTools = request.state["copilotkit"]?.actions ?? [];
259
+ if (a2uiTool) {
260
+ const drop = typeof decision === "string" ? decision : "render_a2ui";
261
+ frontendTools = frontendTools.filter((t) => (t?.function?.name ?? t?.name) !== drop);
262
+ }
263
+ if (frontendTools.length === 0 && !a2uiTool) return handler(request);
264
+ const mergedTools = [
265
+ ...request.tools || [],
266
+ ...a2uiTool ? [a2uiTool] : [],
267
+ ...frontendTools
268
+ ];
197
269
  return handler({
198
270
  ...request,
199
271
  tools: mergedTools
200
272
  });
201
273
  },
274
+ wrapToolCall: async (request, handler) => {
275
+ const tool = a2uiToolsByThread.get(a2uiThreadKey(request.state));
276
+ if (tool && !request.tool && request.toolCall?.name === tool.name) return handler({
277
+ ...request,
278
+ tool
279
+ });
280
+ return handler(request);
281
+ },
202
282
  beforeAgent: createAppContextBeforeAgent,
203
283
  afterAgent: (state) => {
284
+ a2uiToolsByThread.delete(a2uiThreadKey(state));
204
285
  const interceptedToolCalls = state["copilotkit"]?.interceptedToolCalls;
205
286
  const originalMessageId = state["copilotkit"]?.originalAIMessageId;
206
287
  if (!interceptedToolCalls?.length || !originalMessageId) return;
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.cjs","names":["z","SystemMessage","getForwardedHeaders","AIMessage"],"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 { getForwardedHeaders } from \"../header-propagation\";\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 existing = request.systemPrompt;\n if (existing == null) {\n return { ...request, systemPrompt: new SystemMessage({ content: 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: new SystemMessage({ content: `${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 appContext = state[\"copilotkit\"]?.context ?? 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.object({\n copilotkit: zodState(\n z\n .object({\n actions: z.array(z.any()),\n context: z.any().optional(),\n interceptedToolCalls: z.array(z.any()).optional(),\n originalAIMessageId: z.string().optional(),\n })\n .optional(),\n ),\n});\n\nconst buildMiddlewareInput = (exposeState: ExposeStateOption) => ({\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 const frontendTools = request.state[\"copilotkit\"]?.actions ?? [];\n\n if (frontendTools.length === 0) {\n return handler(request);\n }\n\n const existingTools = request.tools || [];\n const mergedTools = [...existingTools, ...frontendTools];\n\n return handler({\n ...request,\n tools: mergedTools,\n });\n },\n\n beforeAgent: createAppContextBeforeAgent,\n\n // Restore frontend tool calls to AIMessage before agent exits\n afterAgent: (state) => {\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 new AIMessage({\n content: msg.content,\n tool_calls: [...existingToolCalls, ...interceptedToolCalls],\n id: msg.id,\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 frontendTools = state[\"copilotkit\"]?.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 = new AIMessage({\n content: lastMessage.content,\n tool_calls: backendToolCalls,\n id: lastMessage.id,\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).\n *\n * @example\n * ```typescript\n * import { createCopilotkitMiddleware } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const middleware = createCopilotkitMiddleware({\n * exposeState: [\"liked\", \"todos\"],\n * });\n * ```\n */\nexport const createCopilotkitMiddleware = (\n options: { exposeState?: ExposeStateOption } = {},\n) => {\n const exposeState = options.exposeState ?? false;\n return createMiddleware(buildMiddlewareInput(exposeState) as any);\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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"}
1
+ {"version":3,"file":"middleware.cjs","names":["z","SystemMessage","getForwardedHeaders","getA2UITools","AIMessage"],"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 { 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 * Find the frontend-registered A2UI catalog wherever it was passed. Returns\n * `{ compositionGuide?, catalogId? }` when a catalog is present, else `null`\n * (so the tool is never advertised when the client can't render A2UI). 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 * - CopilotKit runtime proxy → a `state.copilotkit.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 a2uiSchema = state?.[\"ag-ui\"]?.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 = state?.copilotkit?.context;\n for (const entry of Array.isArray(context) ? context : []) {\n const description = entry?.description ?? \"\";\n const value = entry?.value ?? \"\";\n if (!description.includes(\"A2UI catalog\") || !value) 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 runtime's `injectA2UITool` decision, forwarded as\n * `state.copilotkit.a2ui = { injectTool: boolean | string }` whenever\n * CopilotRuntime is configured with an `a2ui` option. Returns `undefined` when\n * there is no runtime signal (AG-UI native path / no A2UI config), in which\n * case the middleware falls back to its catalog-gated default. A falsy value\n * is the host explicitly opting out.\n */\nconst a2uiInjectDecision = (state: any): boolean | string | undefined => {\n const a2ui = state?.copilotkit?.a2ui;\n if (a2ui && typeof a2ui === \"object\" && \"injectTool\" in a2ui) {\n return a2ui.injectTool;\n }\n return undefined;\n};\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 existing = request.systemPrompt;\n if (existing == null) {\n return { ...request, systemPrompt: new SystemMessage({ content: 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: new SystemMessage({ content: `${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 appContext = state[\"copilotkit\"]?.context ?? 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.object({\n copilotkit: zodState(\n z\n .object({\n actions: z.array(z.any()),\n context: z.any().optional(),\n // A2UI decision forwarded from the runtime ({ injectTool: bool | str }).\n // Declared so the state schema preserves it instead of stripping it as\n // an unknown key. Present only when CopilotRuntime has an `a2ui` config.\n a2ui: z.any().optional(),\n interceptedToolCalls: z.array(z.any()).optional(),\n originalAIMessageId: z.string().optional(),\n })\n .optional(),\n ),\n});\n\nconst buildMiddlewareInput = (exposeState: ExposeStateOption) => ({\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 // Auto-inject generate_a2ui when the frontend has registered an A2UI\n // catalog — sourced wherever the FE passed it (CopilotKit runtime proxy via\n // copilotkit.context, or AG-UI native via ag-ui.a2ui_schema). The catalog's\n // presence is the signal that the client can render A2UI surfaces, so this\n // never advertises a tool that would render nowhere while staying fully\n // zero-config. The model is inferred from request.model; the catalog id\n // binds surfaces to the FE's catalog; the built tool is stashed for\n // wrapToolCall to execute.\n // Gate auto-injection of generate_a2ui, in order:\n // (1) honor an explicit runtime opt-out (injectA2UITool: false);\n // (2) require a frontend-registered catalog (the client can render A2UI);\n // (3) don't double-inject if the agent already defines this tool.\n // When no runtime signal is present (AG-UI native path), only (2)–(3)\n // apply, keeping that path zero-config.\n let a2uiTool: any = null;\n const decision = a2uiInjectDecision(request.state);\n const optedOut = decision !== undefined && !decision;\n const a2uiCatalog =\n typeof getA2UITools === \"function\" && !optedOut\n ? resolveA2uiCatalog(request.state)\n : null;\n if (a2uiCatalog) {\n const opts: { defaultCatalogId?: string; compositionGuide?: string } = {};\n if (a2uiCatalog.catalogId) opts.defaultCatalogId = a2uiCatalog.catalogId;\n if (a2uiCatalog.compositionGuide)\n opts.compositionGuide = a2uiCatalog.compositionGuide;\n const candidate = getA2UITools(request.model, opts);\n const existingNames = new Set(\n (request.tools || []).map((t: any) => t?.name),\n );\n if (!existingNames.has(candidate.name)) {\n a2uiTool = candidate;\n a2uiToolsByThread.set(a2uiThreadKey(request.state), a2uiTool);\n }\n }\n\n let frontendTools = request.state[\"copilotkit\"]?.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 new AIMessage({\n content: msg.content,\n tool_calls: [...existingToolCalls, ...interceptedToolCalls],\n id: msg.id,\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 frontendTools = state[\"copilotkit\"]?.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 = new AIMessage({\n content: lastMessage.content,\n tool_calls: backendToolCalls,\n id: lastMessage.id,\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).\n *\n * @example\n * ```typescript\n * import { createCopilotkitMiddleware } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const middleware = createCopilotkitMiddleware({\n * exposeState: [\"liked\", \"todos\"],\n * });\n * ```\n */\nexport const createCopilotkitMiddleware = (\n options: { exposeState?: ExposeStateOption } = {},\n) => {\n const exposeState = options.exposeState ?? false;\n return createMiddleware(buildMiddlewareInput(exposeState) as any);\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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.cts","names":[],"sources":["../../src/langgraph/middleware.ts"],"mappings":";;;;;KASK,cAAA,MAAoB,CAAA;EAAY,WAAA;AAAA,IACjC,IAAA,CAAK,CAAA;EACH,WAAA,EAAa,CAAA,GACX,oBAAA,CAAqB,KAAA,CACnB,CAAA,SAAU,gBAAA,CAAiB,KAAA,iBAAsB,CAAA,YACjD,CAAA,SAAU,gBAAA,CAAiB,KAAA,iBAAsB,CAAA;AAAA,IAGvD,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;iBA0BY,QAAA,kBAAA,CAA2B,MAAA,EAAQ,CAAA,GAAI,cAAA,CAAe,CAAA;;;;;;;;;;;AAAtE;;KA2DY,iBAAA;;;;;;;;;;;;;AAAZ;;;cAmVa,0BAAA,GACX,OAAA;EAAW,WAAA,GAAc,iBAAA;AAAA,MAAwB,SAAA,CAAA,eAAA,0CAAP,sBAAA,CAAO,UAAA,GAAA,sBAAA,CAAA,UAAA;;;;;;cAWtC,oBAAA,EAAoB,SAAA,CAAA,eAAA,0CAA+B,sBAAA,CAA/B,UAAA,GAAA,sBAAA,CAAA,UAAA"}
1
+ {"version":3,"file":"middleware.d.cts","names":[],"sources":["../../src/langgraph/middleware.ts"],"mappings":";;;;;KA4EK,cAAA,MAAoB,CAAA;EAAY,WAAA;AAAA,IACjC,IAAA,CAAK,CAAA;EACH,WAAA,EAAa,CAAA,GACX,oBAAA,CAAqB,KAAA,CACnB,CAAA,SAAU,gBAAA,CAAiB,KAAA,iBAAsB,CAAA,YACjD,CAAA,SAAU,gBAAA,CAAiB,KAAA,iBAAsB,CAAA;AAAA,IAGvD,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;iBA0BY,QAAA,kBAAA,CAA2B,MAAA,EAAQ,CAAA,GAAI,cAAA,CAAe,CAAA;;;;;;;;;;;AAAtE;;KA2DY,iBAAA;;;;;;;;;;;;;AAAZ;;;cAwZa,0BAAA,GACX,OAAA;EAAW,WAAA,GAAc,iBAAA;AAAA,MAAwB,SAAA,CAAA,eAAA,0CAAP,sBAAA,CAAO,UAAA,GAAA,sBAAA,CAAA,UAAA;;;;;;cAWtC,oBAAA,EAAoB,SAAA,CAAA,eAAA,0CAA+B,sBAAA,CAA/B,UAAA,GAAA,sBAAA,CAAA,UAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/langgraph/middleware.ts"],"mappings":";;;;;KASK,cAAA,MAAoB,CAAA;EAAY,WAAA;AAAA,IACjC,IAAA,CAAK,CAAA;EACH,WAAA,EAAa,CAAA,GACX,oBAAA,CAAqB,KAAA,CACnB,CAAA,SAAU,gBAAA,CAAiB,KAAA,iBAAsB,CAAA,YACjD,CAAA,SAAU,gBAAA,CAAiB,KAAA,iBAAsB,CAAA;AAAA,IAGvD,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;iBA0BY,QAAA,kBAAA,CAA2B,MAAA,EAAQ,CAAA,GAAI,cAAA,CAAe,CAAA;;;;;;;;;;;AAAtE;;KA2DY,iBAAA;;;;;;;;;;;;;AAAZ;;;cAmVa,0BAAA,GACX,OAAA;EAAW,WAAA,GAAc,iBAAA;AAAA,MAAwB,SAAA,CAAA,eAAA,0CAAP,sBAAA,CAAO,UAAA,GAAA,sBAAA,CAAA,UAAA;;;;;;cAWtC,oBAAA,EAAoB,SAAA,CAAA,eAAA,0CAA+B,sBAAA,CAA/B,UAAA,GAAA,sBAAA,CAAA,UAAA"}
1
+ {"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/langgraph/middleware.ts"],"mappings":";;;;;KA4EK,cAAA,MAAoB,CAAA;EAAY,WAAA;AAAA,IACjC,IAAA,CAAK,CAAA;EACH,WAAA,EAAa,CAAA,GACX,oBAAA,CAAqB,KAAA,CACnB,CAAA,SAAU,gBAAA,CAAiB,KAAA,iBAAsB,CAAA,YACjD,CAAA,SAAU,gBAAA,CAAiB,KAAA,iBAAsB,CAAA;AAAA,IAGvD,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;iBA0BY,QAAA,kBAAA,CAA2B,MAAA,EAAQ,CAAA,GAAI,cAAA,CAAe,CAAA;;;;;;;;;;;AAAtE;;KA2DY,iBAAA;;;;;;;;;;;;;AAAZ;;;cAwZa,0BAAA,GACX,OAAA;EAAW,WAAA,GAAc,iBAAA;AAAA,MAAwB,SAAA,CAAA,eAAA,0CAAP,sBAAA,CAAO,UAAA,GAAA,sBAAA,CAAA,UAAA;;;;;;cAWtC,oBAAA,EAAoB,SAAA,CAAA,eAAA,0CAA+B,sBAAA,CAA/B,UAAA,GAAA,sBAAA,CAAA,UAAA"}
@@ -1,8 +1,58 @@
1
1
  import { getForwardedHeaders } from "../header-propagation.mjs";
2
2
  import { AIMessage, SystemMessage, createMiddleware } from "langchain";
3
3
  import * as z from "zod";
4
+ import { getA2UITools } from "@ag-ui/langgraph";
4
5
 
5
6
  //#region src/langgraph/middleware.ts
7
+ const a2uiToolsByThread = /* @__PURE__ */ new Map();
8
+ const A2UI_DEFAULT_THREAD_KEY = "__copilotkit_a2ui_default__";
9
+ const a2uiThreadKey = (state) => state?.thread_id || A2UI_DEFAULT_THREAD_KEY;
10
+ /**
11
+ * Find the frontend-registered A2UI catalog wherever it was passed. Returns
12
+ * `{ compositionGuide?, catalogId? }` when a catalog is present, else `null`
13
+ * (so the tool is never advertised when the client can't render A2UI). Two
14
+ * delivery paths, depending on how the agent is served:
15
+ * - AG-UI native endpoint → `state["ag-ui"].a2ui_schema` (JSON
16
+ * `{ catalogId, components }`); the toolkit reads it from state itself.
17
+ * - CopilotKit runtime proxy → a `state.copilotkit.context` entry describing
18
+ * the A2UI catalog (catalog id + component schemas as text), passed to the
19
+ * subagent via `compositionGuide`.
20
+ * `catalogId` binds generated surfaces to the frontend's catalog so BYOC
21
+ * custom catalogs render their own components (not the basic one).
22
+ */
23
+ const resolveA2uiCatalog = (state) => {
24
+ const a2uiSchema = state?.["ag-ui"]?.a2ui_schema;
25
+ if (a2uiSchema) {
26
+ let catalogId;
27
+ try {
28
+ catalogId = (typeof a2uiSchema === "string" ? JSON.parse(a2uiSchema) : a2uiSchema)?.catalogId;
29
+ } catch {}
30
+ return { catalogId };
31
+ }
32
+ const context = state?.copilotkit?.context;
33
+ for (const entry of Array.isArray(context) ? context : []) {
34
+ const description = entry?.description ?? "";
35
+ const value = entry?.value ?? "";
36
+ if (!description.includes("A2UI catalog") || !value) continue;
37
+ return {
38
+ compositionGuide: value,
39
+ catalogId: /^\s*-\s+(\S+)/m.exec(value)?.[1]
40
+ };
41
+ }
42
+ return null;
43
+ };
44
+ /**
45
+ * The runtime's `injectA2UITool` decision, forwarded as
46
+ * `state.copilotkit.a2ui = { injectTool: boolean | string }` whenever
47
+ * CopilotRuntime is configured with an `a2ui` option. Returns `undefined` when
48
+ * there is no runtime signal (AG-UI native path / no A2UI config), in which
49
+ * case the middleware falls back to its catalog-gated default. A falsy value
50
+ * is the host explicitly opting out.
51
+ */
52
+ const a2uiInjectDecision = (state) => {
53
+ const a2ui = state?.copilotkit?.a2ui;
54
+ if (a2ui && typeof a2ui === "object" && "injectTool" in a2ui) return a2ui.injectTool;
55
+ };
6
56
  /**
7
57
  * Augment a Standard-Schema–compatible schema (e.g. Zod) with a
8
58
  * `~standard.jsonSchema.input` hook so LangGraph's
@@ -166,6 +216,7 @@ const createAppContextBeforeAgent = (state, runtime) => {
166
216
  const copilotKitStateSchema = z.object({ copilotkit: zodState(z.object({
167
217
  actions: z.array(z.any()),
168
218
  context: z.any().optional(),
219
+ a2ui: z.any().optional(),
169
220
  interceptedToolCalls: z.array(z.any()).optional(),
170
221
  originalAIMessageId: z.string().optional()
171
222
  }).optional()) });
@@ -189,16 +240,46 @@ const buildMiddlewareInput = (exposeState) => ({
189
240
  }
190
241
  };
191
242
  }
192
- const frontendTools = request.state["copilotkit"]?.actions ?? [];
193
- if (frontendTools.length === 0) return handler(request);
194
- const mergedTools = [...request.tools || [], ...frontendTools];
243
+ let a2uiTool = null;
244
+ const decision = a2uiInjectDecision(request.state);
245
+ const a2uiCatalog = typeof getA2UITools === "function" && !(decision !== void 0 && !decision) ? resolveA2uiCatalog(request.state) : null;
246
+ if (a2uiCatalog) {
247
+ const opts = {};
248
+ if (a2uiCatalog.catalogId) opts.defaultCatalogId = a2uiCatalog.catalogId;
249
+ if (a2uiCatalog.compositionGuide) opts.compositionGuide = a2uiCatalog.compositionGuide;
250
+ const candidate = getA2UITools(request.model, opts);
251
+ if (!new Set((request.tools || []).map((t) => t?.name)).has(candidate.name)) {
252
+ a2uiTool = candidate;
253
+ a2uiToolsByThread.set(a2uiThreadKey(request.state), a2uiTool);
254
+ }
255
+ }
256
+ let frontendTools = request.state["copilotkit"]?.actions ?? [];
257
+ if (a2uiTool) {
258
+ const drop = typeof decision === "string" ? decision : "render_a2ui";
259
+ frontendTools = frontendTools.filter((t) => (t?.function?.name ?? t?.name) !== drop);
260
+ }
261
+ if (frontendTools.length === 0 && !a2uiTool) return handler(request);
262
+ const mergedTools = [
263
+ ...request.tools || [],
264
+ ...a2uiTool ? [a2uiTool] : [],
265
+ ...frontendTools
266
+ ];
195
267
  return handler({
196
268
  ...request,
197
269
  tools: mergedTools
198
270
  });
199
271
  },
272
+ wrapToolCall: async (request, handler) => {
273
+ const tool = a2uiToolsByThread.get(a2uiThreadKey(request.state));
274
+ if (tool && !request.tool && request.toolCall?.name === tool.name) return handler({
275
+ ...request,
276
+ tool
277
+ });
278
+ return handler(request);
279
+ },
200
280
  beforeAgent: createAppContextBeforeAgent,
201
281
  afterAgent: (state) => {
282
+ a2uiToolsByThread.delete(a2uiThreadKey(state));
202
283
  const interceptedToolCalls = state["copilotkit"]?.interceptedToolCalls;
203
284
  const originalMessageId = state["copilotkit"]?.originalAIMessageId;
204
285
  if (!interceptedToolCalls?.length || !originalMessageId) return;
@@ -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 { getForwardedHeaders } from \"../header-propagation\";\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 existing = request.systemPrompt;\n if (existing == null) {\n return { ...request, systemPrompt: new SystemMessage({ content: 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: new SystemMessage({ content: `${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 appContext = state[\"copilotkit\"]?.context ?? 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.object({\n copilotkit: zodState(\n z\n .object({\n actions: z.array(z.any()),\n context: z.any().optional(),\n interceptedToolCalls: z.array(z.any()).optional(),\n originalAIMessageId: z.string().optional(),\n })\n .optional(),\n ),\n});\n\nconst buildMiddlewareInput = (exposeState: ExposeStateOption) => ({\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 const frontendTools = request.state[\"copilotkit\"]?.actions ?? [];\n\n if (frontendTools.length === 0) {\n return handler(request);\n }\n\n const existingTools = request.tools || [];\n const mergedTools = [...existingTools, ...frontendTools];\n\n return handler({\n ...request,\n tools: mergedTools,\n });\n },\n\n beforeAgent: createAppContextBeforeAgent,\n\n // Restore frontend tool calls to AIMessage before agent exits\n afterAgent: (state) => {\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 new AIMessage({\n content: msg.content,\n tool_calls: [...existingToolCalls, ...interceptedToolCalls],\n id: msg.id,\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 frontendTools = state[\"copilotkit\"]?.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 = new AIMessage({\n content: lastMessage.content,\n tool_calls: backendToolCalls,\n id: lastMessage.id,\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).\n *\n * @example\n * ```typescript\n * import { createCopilotkitMiddleware } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const middleware = createCopilotkitMiddleware({\n * exposeState: [\"liked\", \"todos\"],\n * });\n * ```\n */\nexport const createCopilotkitMiddleware = (\n options: { exposeState?: ExposeStateOption } = {},\n) => {\n const exposeState = options.exposeState ?? false;\n return createMiddleware(buildMiddlewareInput(exposeState) as any);\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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"}
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 { 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 * Find the frontend-registered A2UI catalog wherever it was passed. Returns\n * `{ compositionGuide?, catalogId? }` when a catalog is present, else `null`\n * (so the tool is never advertised when the client can't render A2UI). 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 * - CopilotKit runtime proxy → a `state.copilotkit.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 a2uiSchema = state?.[\"ag-ui\"]?.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 = state?.copilotkit?.context;\n for (const entry of Array.isArray(context) ? context : []) {\n const description = entry?.description ?? \"\";\n const value = entry?.value ?? \"\";\n if (!description.includes(\"A2UI catalog\") || !value) 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 runtime's `injectA2UITool` decision, forwarded as\n * `state.copilotkit.a2ui = { injectTool: boolean | string }` whenever\n * CopilotRuntime is configured with an `a2ui` option. Returns `undefined` when\n * there is no runtime signal (AG-UI native path / no A2UI config), in which\n * case the middleware falls back to its catalog-gated default. A falsy value\n * is the host explicitly opting out.\n */\nconst a2uiInjectDecision = (state: any): boolean | string | undefined => {\n const a2ui = state?.copilotkit?.a2ui;\n if (a2ui && typeof a2ui === \"object\" && \"injectTool\" in a2ui) {\n return a2ui.injectTool;\n }\n return undefined;\n};\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 existing = request.systemPrompt;\n if (existing == null) {\n return { ...request, systemPrompt: new SystemMessage({ content: 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: new SystemMessage({ content: `${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 appContext = state[\"copilotkit\"]?.context ?? 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.object({\n copilotkit: zodState(\n z\n .object({\n actions: z.array(z.any()),\n context: z.any().optional(),\n // A2UI decision forwarded from the runtime ({ injectTool: bool | str }).\n // Declared so the state schema preserves it instead of stripping it as\n // an unknown key. Present only when CopilotRuntime has an `a2ui` config.\n a2ui: z.any().optional(),\n interceptedToolCalls: z.array(z.any()).optional(),\n originalAIMessageId: z.string().optional(),\n })\n .optional(),\n ),\n});\n\nconst buildMiddlewareInput = (exposeState: ExposeStateOption) => ({\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 // Auto-inject generate_a2ui when the frontend has registered an A2UI\n // catalog — sourced wherever the FE passed it (CopilotKit runtime proxy via\n // copilotkit.context, or AG-UI native via ag-ui.a2ui_schema). The catalog's\n // presence is the signal that the client can render A2UI surfaces, so this\n // never advertises a tool that would render nowhere while staying fully\n // zero-config. The model is inferred from request.model; the catalog id\n // binds surfaces to the FE's catalog; the built tool is stashed for\n // wrapToolCall to execute.\n // Gate auto-injection of generate_a2ui, in order:\n // (1) honor an explicit runtime opt-out (injectA2UITool: false);\n // (2) require a frontend-registered catalog (the client can render A2UI);\n // (3) don't double-inject if the agent already defines this tool.\n // When no runtime signal is present (AG-UI native path), only (2)–(3)\n // apply, keeping that path zero-config.\n let a2uiTool: any = null;\n const decision = a2uiInjectDecision(request.state);\n const optedOut = decision !== undefined && !decision;\n const a2uiCatalog =\n typeof getA2UITools === \"function\" && !optedOut\n ? resolveA2uiCatalog(request.state)\n : null;\n if (a2uiCatalog) {\n const opts: { defaultCatalogId?: string; compositionGuide?: string } = {};\n if (a2uiCatalog.catalogId) opts.defaultCatalogId = a2uiCatalog.catalogId;\n if (a2uiCatalog.compositionGuide)\n opts.compositionGuide = a2uiCatalog.compositionGuide;\n const candidate = getA2UITools(request.model, opts);\n const existingNames = new Set(\n (request.tools || []).map((t: any) => t?.name),\n );\n if (!existingNames.has(candidate.name)) {\n a2uiTool = candidate;\n a2uiToolsByThread.set(a2uiThreadKey(request.state), a2uiTool);\n }\n }\n\n let frontendTools = request.state[\"copilotkit\"]?.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 new AIMessage({\n content: msg.content,\n tool_calls: [...existingToolCalls, ...interceptedToolCalls],\n id: msg.id,\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 frontendTools = state[\"copilotkit\"]?.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 = new AIMessage({\n content: lastMessage.content,\n tool_calls: backendToolCalls,\n id: lastMessage.id,\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).\n *\n * @example\n * ```typescript\n * import { createCopilotkitMiddleware } from \"@copilotkit/sdk-js/langgraph\";\n *\n * const middleware = createCopilotkitMiddleware({\n * exposeState: [\"liked\", \"todos\"],\n * });\n * ```\n */\nexport const createCopilotkitMiddleware = (\n options: { exposeState?: ExposeStateOption } = {},\n) => {\n const exposeState = options.exposeState ?? false;\n return createMiddleware(buildMiddlewareInput(exposeState) as any);\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.59.2",
3
+ "version": "1.59.3-alpha.2",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -48,7 +48,7 @@
48
48
  "access": "public"
49
49
  },
50
50
  "dependencies": {
51
- "@ag-ui/langgraph": "0.0.34",
51
+ "@ag-ui/langgraph": "0.0.35",
52
52
  "@copilotkit/shared": "1.59.2"
53
53
  },
54
54
  "devDependencies": {
@@ -498,6 +498,216 @@ describe("afterAgent", () => {
498
498
  });
499
499
  });
500
500
 
501
+ // ---------------------------------------------------------------------------
502
+ // Auto-A2UI — middleware injects + executes generate_a2ui when the frontend
503
+ // registered a catalog (surfaced into state["ag-ui"].a2ui_schema)
504
+ // ---------------------------------------------------------------------------
505
+ //
506
+ // Contract: the developer passes nothing — using the middleware is enough.
507
+ // generate_a2ui is advertised to the model only when an A2UI catalog is
508
+ // present, is built from the agent's own (inferred) model, and is executed by
509
+ // the middleware itself (it is never in the agent's static tool registry).
510
+
511
+ async function runWrapTool(middleware: any, request: any) {
512
+ let received: any = null;
513
+ const handler = async (req: any) => {
514
+ received = req;
515
+ return { content: "tool-ok" } as any;
516
+ };
517
+ await middleware.wrapToolCall(request, handler);
518
+ return received;
519
+ }
520
+
521
+ describe("auto-A2UI injection", () => {
522
+ it("does NOT advertise generate_a2ui when there is no A2UI catalog", async () => {
523
+ const request = makeRequest({
524
+ state: { messages: [], thread_id: "a2ui-off" },
525
+ tools: [{ name: "backend" }],
526
+ });
527
+
528
+ const { received } = await runWrap(copilotkitMiddleware, request);
529
+
530
+ expect(received.tools.map((t: any) => t.name)).toEqual(["backend"]);
531
+ });
532
+
533
+ it("advertises generate_a2ui (alongside existing tools) when a catalog is present", async () => {
534
+ const request = makeRequest({
535
+ state: {
536
+ messages: [],
537
+ thread_id: "a2ui-on",
538
+ "ag-ui": { a2ui_schema: "<components/>" },
539
+ },
540
+ tools: [{ name: "backend" }],
541
+ });
542
+
543
+ const { received } = await runWrap(copilotkitMiddleware, request);
544
+
545
+ const names = received.tools.map((t: any) => t.name);
546
+ expect(names).toContain("backend");
547
+ expect(names).toContain("generate_a2ui");
548
+ });
549
+
550
+ it("advertises generate_a2ui when the catalog arrives via copilotkit.context (runtime-proxy path)", async () => {
551
+ const request = makeRequest({
552
+ state: {
553
+ messages: [],
554
+ thread_id: "a2ui-ctx",
555
+ copilotkit: {
556
+ context: [
557
+ {
558
+ description:
559
+ "A2UI catalog capabilities: available catalog IDs and custom component definitions.",
560
+ value:
561
+ "Available A2UI catalog:\n- declarative-gen-ui-catalog\n - Card: {...}\n - Metric: {...}",
562
+ },
563
+ ],
564
+ },
565
+ },
566
+ tools: [{ name: "backend" }],
567
+ });
568
+
569
+ const { received } = await runWrap(copilotkitMiddleware, request);
570
+
571
+ const names = received.tools.map((t: any) => t.name);
572
+ expect(names).toContain("backend");
573
+ expect(names).toContain("generate_a2ui");
574
+ });
575
+
576
+ it("executes generate_a2ui via wrapToolCall using the inferred model", async () => {
577
+ const state = {
578
+ messages: [],
579
+ thread_id: "a2ui-exec",
580
+ "ag-ui": { a2ui_schema: "<components/>" },
581
+ };
582
+ // First the model call infers the model + stashes the built tool.
583
+ await runWrap(copilotkitMiddleware, makeRequest({ state, tools: [] }));
584
+
585
+ const received = await runWrapTool(copilotkitMiddleware, {
586
+ toolCall: { name: "generate_a2ui", id: "1", args: {} },
587
+ tool: undefined,
588
+ state,
589
+ runtime: {},
590
+ });
591
+
592
+ expect(received.tool).toBeDefined();
593
+ expect(received.tool.name).toBe("generate_a2ui");
594
+ });
595
+
596
+ it("leaves non-A2UI tool calls untouched", async () => {
597
+ const state = {
598
+ messages: [],
599
+ thread_id: "a2ui-other",
600
+ "ag-ui": { a2ui_schema: "<components/>" },
601
+ };
602
+ await runWrap(copilotkitMiddleware, makeRequest({ state, tools: [] }));
603
+
604
+ const backendTool = { name: "backend" };
605
+ const received = await runWrapTool(copilotkitMiddleware, {
606
+ toolCall: { name: "backend", id: "1", args: {} },
607
+ tool: backendTool,
608
+ state,
609
+ runtime: {},
610
+ });
611
+
612
+ expect(received.tool).toBe(backendTool);
613
+ });
614
+
615
+ it("stops executing generate_a2ui after the run ends (afterAgent clears the bridge)", async () => {
616
+ const state = {
617
+ messages: [],
618
+ thread_id: "a2ui-clean",
619
+ "ag-ui": { a2ui_schema: "<components/>" },
620
+ };
621
+ await runWrap(copilotkitMiddleware, makeRequest({ state, tools: [] }));
622
+ copilotkitMiddleware.afterAgent(state, {} as any);
623
+
624
+ const received = await runWrapTool(copilotkitMiddleware, {
625
+ toolCall: { name: "generate_a2ui", id: "1", args: {} },
626
+ tool: undefined,
627
+ state,
628
+ runtime: {},
629
+ });
630
+
631
+ expect(received.tool).toBeUndefined();
632
+ });
633
+
634
+ // --- runtime injectA2UITool flag (state.copilotkit.a2ui) -----------------
635
+
636
+ it("does NOT advertise generate_a2ui when the runtime set injectA2UITool=false", async () => {
637
+ const request = makeRequest({
638
+ state: {
639
+ messages: [],
640
+ thread_id: "a2ui-optout",
641
+ "ag-ui": { a2ui_schema: "<components/>" },
642
+ copilotkit: { a2ui: { injectTool: false } },
643
+ },
644
+ tools: [{ name: "backend" }],
645
+ });
646
+
647
+ const { received } = await runWrap(copilotkitMiddleware, request);
648
+
649
+ const names = received.tools.map((t: any) => t.name);
650
+ expect(names).not.toContain("generate_a2ui");
651
+ expect(names).toContain("backend");
652
+ });
653
+
654
+ it("advertises generate_a2ui when the runtime set injectA2UITool=true", async () => {
655
+ const request = makeRequest({
656
+ state: {
657
+ messages: [],
658
+ thread_id: "a2ui-optin",
659
+ "ag-ui": { a2ui_schema: "<components/>" },
660
+ copilotkit: { a2ui: { injectTool: true } },
661
+ },
662
+ tools: [{ name: "backend" }],
663
+ });
664
+
665
+ const { received } = await runWrap(copilotkitMiddleware, request);
666
+
667
+ expect(received.tools.map((t: any) => t.name)).toContain("generate_a2ui");
668
+ });
669
+
670
+ it("drops the runtime's render_a2ui when injecting our generate_a2ui", async () => {
671
+ const request = makeRequest({
672
+ state: {
673
+ messages: [],
674
+ thread_id: "a2ui-drop",
675
+ "ag-ui": { a2ui_schema: "<components/>" },
676
+ copilotkit: {
677
+ a2ui: { injectTool: true },
678
+ actions: [{ name: "render_a2ui" }, { name: "fe_tool" }],
679
+ },
680
+ },
681
+ tools: [],
682
+ });
683
+
684
+ const { received } = await runWrap(copilotkitMiddleware, request);
685
+
686
+ const names = received.tools.map((t: any) => t.name);
687
+ expect(names).toContain("generate_a2ui");
688
+ expect(names).toContain("fe_tool");
689
+ expect(names).not.toContain("render_a2ui");
690
+ });
691
+
692
+ it("does NOT double-inject when the agent already defines generate_a2ui", async () => {
693
+ const request = makeRequest({
694
+ state: {
695
+ messages: [],
696
+ thread_id: "a2ui-dup",
697
+ "ag-ui": { a2ui_schema: "<components/>" },
698
+ },
699
+ tools: [{ name: "generate_a2ui" }],
700
+ });
701
+
702
+ const { received } = await runWrap(copilotkitMiddleware, request);
703
+
704
+ const count = received.tools.filter(
705
+ (t: any) => t.name === "generate_a2ui",
706
+ ).length;
707
+ expect(count).toBe(1);
708
+ });
709
+ });
710
+
501
711
  // ---------------------------------------------------------------------------
502
712
  // zodState — Standard-Schema JSON-schema augmentation
503
713
  // ---------------------------------------------------------------------------
@@ -5,8 +5,75 @@ import type {
5
5
  StandardSchemaV1,
6
6
  } from "@standard-schema/spec";
7
7
  import * as z from "zod";
8
+ import { getA2UITools } from "@ag-ui/langgraph";
8
9
  import { getForwardedHeaders } from "../header-propagation";
9
10
 
11
+ // ---------------------------------------------------------------------------
12
+ // Auto-A2UI: bridge the inferred model's generate_a2ui tool from wrapModelCall
13
+ // (the only hook that exposes the bound model) to wrapToolCall (where the tool
14
+ // actually executes but the model is absent). Keyed by the run's thread id so
15
+ // concurrent runs don't clobber each other.
16
+ // ---------------------------------------------------------------------------
17
+ const a2uiToolsByThread = new Map<string, any>();
18
+ const A2UI_DEFAULT_THREAD_KEY = "__copilotkit_a2ui_default__";
19
+ const a2uiThreadKey = (state: any): string =>
20
+ (state?.thread_id as string) || A2UI_DEFAULT_THREAD_KEY;
21
+
22
+ /**
23
+ * Find the frontend-registered A2UI catalog wherever it was passed. Returns
24
+ * `{ compositionGuide?, catalogId? }` when a catalog is present, else `null`
25
+ * (so the tool is never advertised when the client can't render A2UI). Two
26
+ * delivery paths, depending on how the agent is served:
27
+ * - AG-UI native endpoint → `state["ag-ui"].a2ui_schema` (JSON
28
+ * `{ catalogId, components }`); the toolkit reads it from state itself.
29
+ * - CopilotKit runtime proxy → a `state.copilotkit.context` entry describing
30
+ * the A2UI catalog (catalog id + component schemas as text), passed to the
31
+ * subagent via `compositionGuide`.
32
+ * `catalogId` binds generated surfaces to the frontend's catalog so BYOC
33
+ * custom catalogs render their own components (not the basic one).
34
+ */
35
+ const resolveA2uiCatalog = (
36
+ state: any,
37
+ ): { compositionGuide?: string; catalogId?: string } | null => {
38
+ const a2uiSchema = state?.["ag-ui"]?.a2ui_schema;
39
+ if (a2uiSchema) {
40
+ let catalogId: string | undefined;
41
+ try {
42
+ const parsed =
43
+ typeof a2uiSchema === "string" ? JSON.parse(a2uiSchema) : a2uiSchema;
44
+ catalogId = parsed?.catalogId;
45
+ } catch {
46
+ // non-JSON schema — fall back to the toolkit's basic catalog
47
+ }
48
+ return { catalogId };
49
+ }
50
+ const context = state?.copilotkit?.context;
51
+ for (const entry of Array.isArray(context) ? context : []) {
52
+ const description = entry?.description ?? "";
53
+ const value = entry?.value ?? "";
54
+ if (!description.includes("A2UI catalog") || !value) continue;
55
+ const match = /^\s*-\s+(\S+)/m.exec(value);
56
+ return { compositionGuide: value, catalogId: match?.[1] };
57
+ }
58
+ return null;
59
+ };
60
+
61
+ /**
62
+ * The runtime's `injectA2UITool` decision, forwarded as
63
+ * `state.copilotkit.a2ui = { injectTool: boolean | string }` whenever
64
+ * CopilotRuntime is configured with an `a2ui` option. Returns `undefined` when
65
+ * there is no runtime signal (AG-UI native path / no A2UI config), in which
66
+ * case the middleware falls back to its catalog-gated default. A falsy value
67
+ * is the host explicitly opting out.
68
+ */
69
+ const a2uiInjectDecision = (state: any): boolean | string | undefined => {
70
+ const a2ui = state?.copilotkit?.a2ui;
71
+ if (a2ui && typeof a2ui === "object" && "injectTool" in a2ui) {
72
+ return a2ui.injectTool;
73
+ }
74
+ return undefined;
75
+ };
76
+
10
77
  type WithJsonSchema<T> = T extends { "~standard": infer S }
11
78
  ? Omit<T, "~standard"> & {
12
79
  "~standard": S &
@@ -291,6 +358,10 @@ const copilotKitStateSchema = z.object({
291
358
  .object({
292
359
  actions: z.array(z.any()),
293
360
  context: z.any().optional(),
361
+ // A2UI decision forwarded from the runtime ({ injectTool: bool | str }).
362
+ // Declared so the state schema preserves it instead of stripping it as
363
+ // an unknown key. Present only when CopilotRuntime has an `a2ui` config.
364
+ a2ui: z.any().optional(),
294
365
  interceptedToolCalls: z.array(z.any()).optional(),
295
366
  originalAIMessageId: z.string().optional(),
296
367
  })
@@ -322,14 +393,62 @@ const buildMiddlewareInput = (exposeState: ExposeStateOption) => ({
322
393
  };
323
394
  }
324
395
 
325
- const frontendTools = request.state["copilotkit"]?.actions ?? [];
396
+ // Auto-inject generate_a2ui when the frontend has registered an A2UI
397
+ // catalog — sourced wherever the FE passed it (CopilotKit runtime proxy via
398
+ // copilotkit.context, or AG-UI native via ag-ui.a2ui_schema). The catalog's
399
+ // presence is the signal that the client can render A2UI surfaces, so this
400
+ // never advertises a tool that would render nowhere while staying fully
401
+ // zero-config. The model is inferred from request.model; the catalog id
402
+ // binds surfaces to the FE's catalog; the built tool is stashed for
403
+ // wrapToolCall to execute.
404
+ // Gate auto-injection of generate_a2ui, in order:
405
+ // (1) honor an explicit runtime opt-out (injectA2UITool: false);
406
+ // (2) require a frontend-registered catalog (the client can render A2UI);
407
+ // (3) don't double-inject if the agent already defines this tool.
408
+ // When no runtime signal is present (AG-UI native path), only (2)–(3)
409
+ // apply, keeping that path zero-config.
410
+ let a2uiTool: any = null;
411
+ const decision = a2uiInjectDecision(request.state);
412
+ const optedOut = decision !== undefined && !decision;
413
+ const a2uiCatalog =
414
+ typeof getA2UITools === "function" && !optedOut
415
+ ? resolveA2uiCatalog(request.state)
416
+ : null;
417
+ if (a2uiCatalog) {
418
+ const opts: { defaultCatalogId?: string; compositionGuide?: string } = {};
419
+ if (a2uiCatalog.catalogId) opts.defaultCatalogId = a2uiCatalog.catalogId;
420
+ if (a2uiCatalog.compositionGuide)
421
+ opts.compositionGuide = a2uiCatalog.compositionGuide;
422
+ const candidate = getA2UITools(request.model, opts);
423
+ const existingNames = new Set(
424
+ (request.tools || []).map((t: any) => t?.name),
425
+ );
426
+ if (!existingNames.has(candidate.name)) {
427
+ a2uiTool = candidate;
428
+ a2uiToolsByThread.set(a2uiThreadKey(request.state), a2uiTool);
429
+ }
430
+ }
431
+
432
+ let frontendTools = request.state["copilotkit"]?.actions ?? [];
433
+ if (a2uiTool) {
434
+ // Our generate_a2ui replaces the runtime's render tool — don't advertise
435
+ // both. Drop the render tool the A2UI middleware injected.
436
+ const drop = typeof decision === "string" ? decision : "render_a2ui";
437
+ frontendTools = frontendTools.filter(
438
+ (t: any) => (t?.function?.name ?? t?.name) !== drop,
439
+ );
440
+ }
326
441
 
327
- if (frontendTools.length === 0) {
442
+ if (frontendTools.length === 0 && !a2uiTool) {
328
443
  return handler(request);
329
444
  }
330
445
 
331
446
  const existingTools = request.tools || [];
332
- const mergedTools = [...existingTools, ...frontendTools];
447
+ const mergedTools = [
448
+ ...existingTools,
449
+ ...(a2uiTool ? [a2uiTool] : []),
450
+ ...frontendTools,
451
+ ];
333
452
 
334
453
  return handler({
335
454
  ...request,
@@ -337,10 +456,27 @@ const buildMiddlewareInput = (exposeState: ExposeStateOption) => ({
337
456
  });
338
457
  },
339
458
 
459
+ // Execute the dynamically-advertised generate_a2ui tool. It is not in the
460
+ // agent's static tool registry, so the tool node cannot run it on its own;
461
+ // we supply the implementation (built with the inferred model) for that one
462
+ // tool. This hook's presence also disables createAgent's "unknown tool"
463
+ // guard for dynamically-advertised tools.
464
+ wrapToolCall: async (request: any, handler: (req: any) => Promise<any>) => {
465
+ const tool = a2uiToolsByThread.get(a2uiThreadKey(request.state));
466
+ if (tool && !request.tool && request.toolCall?.name === tool.name) {
467
+ return handler({ ...request, tool });
468
+ }
469
+ return handler(request);
470
+ },
471
+
340
472
  beforeAgent: createAppContextBeforeAgent,
341
473
 
342
474
  // Restore frontend tool calls to AIMessage before agent exits
343
475
  afterAgent: (state) => {
476
+ // Drop the bridged A2UI tool for this run — all tool calls for the turn
477
+ // have executed by now; the next model call re-stashes if needed.
478
+ a2uiToolsByThread.delete(a2uiThreadKey(state));
479
+
344
480
  const interceptedToolCalls = state["copilotkit"]?.interceptedToolCalls;
345
481
  const originalMessageId = state["copilotkit"]?.originalAIMessageId;
346
482