@copilotkit/sdk-js 1.56.4 → 1.56.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,175 @@
1
1
  import { createMiddleware, AIMessage, SystemMessage } from "langchain";
2
2
  import type { InteropZodObject } from "@langchain/core/utils/types";
3
+ import type {
4
+ StandardJSONSchemaV1,
5
+ StandardSchemaV1,
6
+ } from "@standard-schema/spec";
3
7
  import * as z from "zod";
4
8
 
9
+ type WithJsonSchema<T> = T extends { "~standard": infer S }
10
+ ? Omit<T, "~standard"> & {
11
+ "~standard": S &
12
+ StandardJSONSchemaV1.Props<
13
+ S extends StandardSchemaV1.Props<infer I, any> ? I : unknown,
14
+ S extends StandardSchemaV1.Props<any, infer O> ? O : unknown
15
+ >;
16
+ }
17
+ : T;
18
+
19
+ /**
20
+ * Augment a Standard-Schema–compatible schema (e.g. Zod) with a
21
+ * `~standard.jsonSchema.input` hook so LangGraph's
22
+ * `getJsonSchemaFromSchema` (called from `StateSchema.getJsonSchema`)
23
+ * can serialize the field.
24
+ *
25
+ * Without this, Zod v4 fields carry `~standard.validate` + `vendor` only,
26
+ * and `isStandardJSONSchema()` returns false, so the field is silently
27
+ * dropped from the graph's `output_schema`. That makes AG-UI
28
+ * `STATE_SNAPSHOT` events filter the field out of the payload sent to
29
+ * the frontend even though the underlying thread state has the value.
30
+ *
31
+ * Use this on any custom state field you want visible to the frontend
32
+ * via `useAgent().state.*`.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { zodState } from "@copilotkit/sdk-js/langgraph";
37
+ *
38
+ * const stateSchema = z.object({
39
+ * todos: zodState(z.array(TodoSchema).default(() => [])),
40
+ * });
41
+ * ```
42
+ */
43
+ export function zodState<T extends object>(schema: T): WithJsonSchema<T> {
44
+ const std = (schema as { "~standard"?: { jsonSchema?: unknown } })[
45
+ "~standard"
46
+ ];
47
+ if (std && typeof std === "object" && !("jsonSchema" in std)) {
48
+ let cached: Record<string, unknown> | undefined;
49
+ std.jsonSchema = {
50
+ input: () => {
51
+ if (cached) return cached;
52
+ // Prefer zod-v4's native `toJSONSchema` when available. Falls back to
53
+ // an empty object, which is sufficient for the field to appear in the
54
+ // graph's output_schema (langgraph-api treats it as an opaque field).
55
+ try {
56
+ const maybeV4ToJsonSchema = (
57
+ z as unknown as {
58
+ toJSONSchema?: (s: unknown) => Record<string, unknown>;
59
+ }
60
+ ).toJSONSchema;
61
+ cached =
62
+ typeof maybeV4ToJsonSchema === "function"
63
+ ? maybeV4ToJsonSchema(schema)
64
+ : {};
65
+ } catch {
66
+ cached = {};
67
+ }
68
+ return cached;
69
+ },
70
+ };
71
+ }
72
+ return schema as WithJsonSchema<T>;
73
+ }
74
+
75
+ /**
76
+ * Internal/framework state keys that should never be auto-surfaced to the
77
+ * LLM as user-facing state. These are reducer-managed message buckets,
78
+ * CopilotKit/AG-UI plumbing, or graph-internal scaffolding.
79
+ */
80
+ const RESERVED_STATE_KEYS: ReadonlySet<string> = new Set([
81
+ "messages",
82
+ "copilotkit",
83
+ "ag-ui",
84
+ "tools",
85
+ "structured_response",
86
+ "thread_id",
87
+ "remaining_steps",
88
+ ]);
89
+
90
+ /**
91
+ * Controls how user-defined state keys are surfaced into the LLM prompt
92
+ * on every model call. Off by default to avoid leaking arbitrary state
93
+ * into prompts; opt in explicitly.
94
+ *
95
+ * - `false` (default) — never surface state.
96
+ * - `true` — every state key not in the reserved internal set and not
97
+ * prefixed with `_` is JSON-serialized into a "Current agent state:"
98
+ * note appended to the system prompt.
99
+ * - `string[]` — only surface the named keys (use this when you want
100
+ * explicit control over what the LLM sees, e.g. `["liked", "todos"]`).
101
+ */
102
+ export type ExposeStateOption = boolean | readonly string[];
103
+
104
+ const buildStateNote = (
105
+ state: Record<string, unknown>,
106
+ expose: ExposeStateOption,
107
+ ): string | null => {
108
+ if (expose === false) return null;
109
+
110
+ const allow: ReadonlySet<string> | null = Array.isArray(expose)
111
+ ? new Set(expose)
112
+ : null;
113
+
114
+ const snapshot: Record<string, unknown> = {};
115
+ for (const key of Object.keys(state)) {
116
+ if (
117
+ allow
118
+ ? !allow.has(key)
119
+ : RESERVED_STATE_KEYS.has(key) || key.startsWith("_")
120
+ ) {
121
+ continue;
122
+ }
123
+ const value = state[key];
124
+ if (
125
+ value === undefined ||
126
+ value === null ||
127
+ value === "" ||
128
+ (Array.isArray(value) && value.length === 0) ||
129
+ (typeof value === "object" &&
130
+ !Array.isArray(value) &&
131
+ Object.keys(value as Record<string, unknown>).length === 0)
132
+ ) {
133
+ continue;
134
+ }
135
+ snapshot[key] = value;
136
+ }
137
+
138
+ if (Object.keys(snapshot).length === 0) return null;
139
+
140
+ let body: string;
141
+ try {
142
+ body = JSON.stringify(snapshot, null, 2);
143
+ } catch {
144
+ body = String(snapshot);
145
+ }
146
+ return `Current agent state:\n${body}`;
147
+ };
148
+
149
+ const applyStateNote = (request: any, expose: ExposeStateOption): any => {
150
+ const note = buildStateNote(
151
+ (request.state ?? {}) as Record<string, unknown>,
152
+ expose,
153
+ );
154
+ if (!note) return request;
155
+
156
+ const existing = request.systemPrompt;
157
+ if (existing == null) {
158
+ return { ...request, systemPrompt: new SystemMessage({ content: note }) };
159
+ }
160
+ // existing may be a string OR a SystemMessage
161
+ const baseText =
162
+ typeof existing === "string"
163
+ ? existing
164
+ : typeof existing.content === "string"
165
+ ? existing.content
166
+ : String(existing.content);
167
+ return {
168
+ ...request,
169
+ systemPrompt: new SystemMessage({ content: `${baseText}\n\n${note}` }),
170
+ };
171
+ };
172
+
5
173
  const createAppContextBeforeAgent = (state, runtime) => {
6
174
  const messages = state.messages;
7
175
 
@@ -117,23 +285,26 @@ const createAppContextBeforeAgent = (state, runtime) => {
117
285
  * ```
118
286
  */
119
287
  const copilotKitStateSchema = z.object({
120
- copilotkit: z
121
- .object({
122
- actions: z.array(z.any()),
123
- context: z.any().optional(),
124
- interceptedToolCalls: z.array(z.any()).optional(),
125
- originalAIMessageId: z.string().optional(),
126
- })
127
- .optional(),
288
+ copilotkit: zodState(
289
+ z
290
+ .object({
291
+ actions: z.array(z.any()),
292
+ context: z.any().optional(),
293
+ interceptedToolCalls: z.array(z.any()).optional(),
294
+ originalAIMessageId: z.string().optional(),
295
+ })
296
+ .optional(),
297
+ ),
128
298
  });
129
299
 
130
- const middlewareInput = {
300
+ const buildMiddlewareInput = (exposeState: ExposeStateOption) => ({
131
301
  name: "CopilotKitMiddleware",
132
302
 
133
303
  stateSchema: copilotKitStateSchema as unknown as InteropZodObject,
134
304
 
135
- // Inject frontend tools before model call
136
- wrapModelCall: async (request, handler) => {
305
+ // Inject frontend tools and surface user state before model call
306
+ wrapModelCall: async (request: any, handler: (req: any) => Promise<any>) => {
307
+ request = applyStateNote(request, exposeState);
137
308
  const frontendTools = request.state["copilotkit"]?.actions ?? [];
138
309
 
139
310
  if (frontendTools.length === 0) {
@@ -234,9 +405,33 @@ const middlewareInput = {
234
405
  },
235
406
  };
236
407
  },
237
- } as any;
238
- const createCopilotKitMiddleware = () => {
239
- return createMiddleware(middlewareInput);
408
+ });
409
+
410
+ /**
411
+ * Build a CopilotKit middleware instance with custom options.
412
+ *
413
+ * Use this when you want to override the default state-exposure behavior
414
+ * (for example to hide a sensitive key, or to use an explicit allowlist).
415
+ *
416
+ * @example
417
+ * ```typescript
418
+ * import { createCopilotkitMiddleware } from "@copilotkit/sdk-js/langgraph";
419
+ *
420
+ * const middleware = createCopilotkitMiddleware({
421
+ * exposeState: ["liked", "todos"],
422
+ * });
423
+ * ```
424
+ */
425
+ export const createCopilotkitMiddleware = (
426
+ options: { exposeState?: ExposeStateOption } = {},
427
+ ) => {
428
+ const exposeState = options.exposeState ?? false;
429
+ return createMiddleware(buildMiddlewareInput(exposeState) as any);
240
430
  };
241
431
 
242
- export const copilotkitMiddleware = createCopilotKitMiddleware();
432
+ /**
433
+ * Default CopilotKit middleware singleton — does NOT surface user state
434
+ * to the LLM. Pass `exposeState: true` (or an allowlist) to
435
+ * {@link createCopilotkitMiddleware} to opt in.
436
+ */
437
+ export const copilotkitMiddleware = createCopilotkitMiddleware();