@copilotkit/shared 1.55.3 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2ui-prompts.cjs +31 -22
- package/dist/a2ui-prompts.cjs.map +1 -1
- package/dist/a2ui-prompts.d.cts +2 -2
- package/dist/a2ui-prompts.d.cts.map +1 -1
- package/dist/a2ui-prompts.d.mts +2 -2
- package/dist/a2ui-prompts.d.mts.map +1 -1
- package/dist/a2ui-prompts.mjs +31 -22
- package/dist/a2ui-prompts.mjs.map +1 -1
- package/dist/debug.cjs +38 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.cts +29 -0
- package/dist/debug.d.cts.map +1 -0
- package/dist/debug.d.mts +29 -0
- package/dist/debug.d.mts.map +1 -0
- package/dist/debug.mjs +37 -0
- package/dist/debug.mjs.map +1 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +133 -28
- package/dist/index.umd.js.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/utils/clipboard.cjs +28 -0
- package/dist/utils/clipboard.cjs.map +1 -0
- package/dist/utils/clipboard.d.cts +14 -0
- package/dist/utils/clipboard.d.cts.map +1 -0
- package/dist/utils/clipboard.d.mts +14 -0
- package/dist/utils/clipboard.d.mts.map +1 -0
- package/dist/utils/clipboard.mjs +27 -0
- package/dist/utils/clipboard.mjs.map +1 -0
- package/dist/utils/index.cjs +1 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.cts +1 -0
- package/dist/utils/index.d.cts.map +1 -1
- package/dist/utils/index.d.mts +1 -0
- package/dist/utils/index.d.mts.map +1 -1
- package/dist/utils/index.mjs +1 -0
- package/dist/utils/index.mjs.map +1 -1
- package/dist/utils/json-schema.cjs +36 -5
- package/dist/utils/json-schema.cjs.map +1 -1
- package/dist/utils/json-schema.d.cts +1 -1
- package/dist/utils/json-schema.d.cts.map +1 -1
- package/dist/utils/json-schema.d.mts +1 -1
- package/dist/utils/json-schema.d.mts.map +1 -1
- package/dist/utils/json-schema.mjs +36 -5
- package/dist/utils/json-schema.mjs.map +1 -1
- package/dist/utils/types.cjs.map +1 -1
- package/dist/utils/types.d.cts +3 -0
- package/dist/utils/types.d.cts.map +1 -1
- package/dist/utils/types.d.mts +3 -0
- package/dist/utils/types.d.mts.map +1 -1
- package/dist/utils/types.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/debug.test.ts +116 -0
- package/src/a2ui-prompts.ts +31 -22
- package/src/debug.ts +55 -0
- package/src/index.ts +1 -0
- package/src/utils/__tests__/clipboard.test.ts +87 -0
- package/src/utils/__tests__/json-schema.test.ts +250 -1
- package/src/utils/clipboard.ts +23 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/json-schema.ts +84 -3
- package/src/utils/types.ts +3 -0
package/src/a2ui-prompts.ts
CHANGED
|
@@ -47,29 +47,38 @@ CRITICAL: Do NOT use "/name" (absolute) inside templates — use "name" (relativ
|
|
|
47
47
|
The container's path ("/items") uses a leading slash (absolute), but all
|
|
48
48
|
components INSIDE the template use paths WITHOUT leading slash.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
COMPONENT VALUES — DEFAULT RULE:
|
|
51
|
+
Use inline literal values for ALL component properties. Pass strings, numbers,
|
|
52
|
+
arrays, and objects directly on the component. Do NOT use { "path": "..." }
|
|
53
|
+
objects unless the property's schema explicitly allows it (see exception below).
|
|
54
|
+
CRITICAL: USING { "path": "..." } ON A PROPERTY THAT DOES NOT DECLARE PATH
|
|
55
|
+
SUPPORT IN ITS SCHEMA WILL CAUSE A RUNTIME CRASH AND BREAK THE ENTIRE UI.
|
|
56
|
+
ALWAYS CHECK THE COMPONENT SCHEMA FIRST — IF THE PROPERTY ONLY ACCEPTS A
|
|
57
|
+
PLAIN TYPE, YOU MUST USE A LITERAL VALUE.
|
|
58
|
+
VERY IMPORTANT: THE APPLICATION WILL BREAK IF YOU DO NOT FOLLOW THIS RULE!
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
You MUST use { "path": "..." } to make inputs editable.
|
|
60
|
+
For example, a chart's "data" must always be an inline array:
|
|
61
|
+
"data": [{"label": "Jan", "value": 100}, {"label": "Feb", "value": 200}]
|
|
62
|
+
A metric's "value" must always be an inline string:
|
|
63
|
+
"value": "$1,200"
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
PATH BINDING EXCEPTION — SCHEMA-DRIVEN:
|
|
66
|
+
A few properties accept { "path": "/some/path" } as an alternative to a literal
|
|
67
|
+
value. You can identify these in the Available Components schema: the property
|
|
68
|
+
will list BOTH a literal type AND an object-with-path option. If a property only
|
|
69
|
+
shows a single type (string, number, array, etc.), it does NOT support path
|
|
70
|
+
binding — use a literal value only.
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
Path binding is typically used for editable form inputs so the client can write
|
|
73
|
+
user input back to the data model. When building forms:
|
|
74
|
+
- Bind input "value" to a data model path: "value": { "path": "/form/name" }
|
|
75
|
+
- Pre-fill via the "data" tool argument: "data": { "form": { "name": "Alice" } }
|
|
76
|
+
- Capture values on submit via button action context:
|
|
77
|
+
"action": { "event": { "name": "submit", "context": { "name": { "path": "/form/name" } } } }
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
REPEATING CONTENT uses a structural children format (not the same as value binding):
|
|
80
|
+
children: { componentId: "card-id", path: "/items" }
|
|
81
|
+
Components inside templates use RELATIVE paths (no leading slash): { "path": "name" }.`;
|
|
73
82
|
|
|
74
83
|
/**
|
|
75
84
|
* Design guidelines — visual design rules, component hierarchy tips,
|
|
@@ -94,8 +103,8 @@ Design principles:
|
|
|
94
103
|
"action": { "event": { "name": "myAction", "context": { "key": "value" } } }
|
|
95
104
|
The "event" key holds an OBJECT with "name" (required) and "context" (optional).
|
|
96
105
|
Do NOT use a flat format like {"event": "name"} — "event" must be an object.
|
|
97
|
-
- For forms:
|
|
98
|
-
|
|
99
|
-
action context
|
|
106
|
+
- For forms: check the component schema — if an input's "value" property
|
|
107
|
+
supports path binding, use it for editable fields. The submit button's
|
|
108
|
+
action context should reference the same paths to capture user input.
|
|
100
109
|
|
|
101
110
|
Use the SAME surfaceId as the main surface. Match action names to button action event names.`;
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Granular debug configuration for CopilotKit runtime and client.
|
|
3
|
+
* Pass `true` to enable events + lifecycle logging (but NOT verbose payloads),
|
|
4
|
+
* or an object for granular control including `verbose: true` for full payloads.
|
|
5
|
+
*/
|
|
6
|
+
export type DebugConfig =
|
|
7
|
+
| boolean
|
|
8
|
+
| {
|
|
9
|
+
/** Log every event emitted/received. Default: true */
|
|
10
|
+
events?: boolean;
|
|
11
|
+
/** Log request/run lifecycle. Default: true */
|
|
12
|
+
lifecycle?: boolean;
|
|
13
|
+
/** Log full event payloads instead of summaries. Default: false — must be explicitly opted in */
|
|
14
|
+
verbose?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Normalized debug configuration — all fields resolved to booleans. */
|
|
18
|
+
export interface ResolvedDebugConfig {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
events: boolean;
|
|
21
|
+
lifecycle: boolean;
|
|
22
|
+
verbose: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The all-off config used when debug is falsy. */
|
|
26
|
+
const DEBUG_OFF: ResolvedDebugConfig = {
|
|
27
|
+
enabled: false,
|
|
28
|
+
events: false,
|
|
29
|
+
lifecycle: false,
|
|
30
|
+
verbose: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes a DebugConfig value into a ResolvedDebugConfig.
|
|
35
|
+
*
|
|
36
|
+
* - `false` / `undefined` → all off
|
|
37
|
+
* - `true` → events + lifecycle on, verbose off (no PII in logs)
|
|
38
|
+
* - object → merges with defaults (events: true, lifecycle: true, verbose: false)
|
|
39
|
+
*/
|
|
40
|
+
export function resolveDebugConfig(
|
|
41
|
+
debug: DebugConfig | undefined,
|
|
42
|
+
): ResolvedDebugConfig {
|
|
43
|
+
if (!debug) return DEBUG_OFF;
|
|
44
|
+
|
|
45
|
+
if (debug === true) {
|
|
46
|
+
return { enabled: true, events: true, lifecycle: true, verbose: false };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const events = debug.events ?? true;
|
|
50
|
+
const lifecycle = debug.lifecycle ?? true;
|
|
51
|
+
const enabled = events || lifecycle;
|
|
52
|
+
const verbose = enabled && (debug.verbose ?? false);
|
|
53
|
+
|
|
54
|
+
return { enabled, events, lifecycle, verbose };
|
|
55
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { copyToClipboard } from "../clipboard";
|
|
3
|
+
|
|
4
|
+
// Mock navigator for Node 20 environments where it doesn't exist
|
|
5
|
+
if (typeof globalThis.navigator === "undefined") {
|
|
6
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
7
|
+
value: { clipboard: { writeText: vi.fn() } },
|
|
8
|
+
writable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("copyToClipboard", () => {
|
|
14
|
+
let originalClipboard: Clipboard;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
originalClipboard = navigator.clipboard;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
22
|
+
value: originalClipboard,
|
|
23
|
+
writable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns true on successful clipboard write", async () => {
|
|
29
|
+
const writeTextMock = vi.fn().mockResolvedValue(undefined);
|
|
30
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
31
|
+
value: { writeText: writeTextMock },
|
|
32
|
+
writable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await copyToClipboard("hello");
|
|
37
|
+
expect(result).toBe(true);
|
|
38
|
+
expect(writeTextMock).toHaveBeenCalledWith("hello");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns false when clipboard API is unavailable", async () => {
|
|
42
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
43
|
+
value: undefined,
|
|
44
|
+
writable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
49
|
+
const result = await copyToClipboard("hello");
|
|
50
|
+
expect(result).toBe(false);
|
|
51
|
+
expect(consoleSpy).toHaveBeenCalledWith("Clipboard API is not available");
|
|
52
|
+
consoleSpy.mockRestore();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns false when writeText is not available", async () => {
|
|
56
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
57
|
+
value: {},
|
|
58
|
+
writable: true,
|
|
59
|
+
configurable: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
63
|
+
const result = await copyToClipboard("hello");
|
|
64
|
+
expect(result).toBe(false);
|
|
65
|
+
consoleSpy.mockRestore();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns false when writeText rejects", async () => {
|
|
69
|
+
const writeTextMock = vi
|
|
70
|
+
.fn()
|
|
71
|
+
.mockRejectedValue(new Error("Permission denied"));
|
|
72
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
73
|
+
value: { writeText: writeTextMock },
|
|
74
|
+
writable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
79
|
+
const result = await copyToClipboard("hello");
|
|
80
|
+
expect(result).toBe(false);
|
|
81
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
82
|
+
"Failed to copy to clipboard:",
|
|
83
|
+
expect.any(Error),
|
|
84
|
+
);
|
|
85
|
+
consoleSpy.mockRestore();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
convertJsonSchemaToZodSchema,
|
|
@@ -191,6 +191,255 @@ describe("convertJsonSchemaToZodSchema", () => {
|
|
|
191
191
|
|
|
192
192
|
expect(resultSchemaJson).toStrictEqual(expectedSchemaJson);
|
|
193
193
|
});
|
|
194
|
+
|
|
195
|
+
it("should resolve non-circular $ref definitions correctly", () => {
|
|
196
|
+
const jsonSchema = {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: {
|
|
199
|
+
address: { $ref: "#/$defs/Address" },
|
|
200
|
+
},
|
|
201
|
+
required: ["address"],
|
|
202
|
+
$defs: {
|
|
203
|
+
Address: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
street: { type: "string", description: "Street name" },
|
|
207
|
+
city: { type: "string", description: "City name" },
|
|
208
|
+
},
|
|
209
|
+
required: ["street", "city"],
|
|
210
|
+
description: "A postal address",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const result = convertJsonSchemaToZodSchema(jsonSchema, true);
|
|
216
|
+
const resultJson = zodToJsonSchema(result);
|
|
217
|
+
|
|
218
|
+
const expectedSchema = z.object({
|
|
219
|
+
address: z
|
|
220
|
+
.object({
|
|
221
|
+
street: z.string().describe("Street name"),
|
|
222
|
+
city: z.string().describe("City name"),
|
|
223
|
+
})
|
|
224
|
+
.describe("A postal address"),
|
|
225
|
+
});
|
|
226
|
+
const expectedJson = zodToJsonSchema(expectedSchema);
|
|
227
|
+
|
|
228
|
+
expect(resultJson).toStrictEqual(expectedJson);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should handle circular $ref without crashing and return z.any()", () => {
|
|
232
|
+
// A schema where Node references itself — this would cause infinite
|
|
233
|
+
// recursion without cycle detection.
|
|
234
|
+
const jsonSchema = {
|
|
235
|
+
type: "object",
|
|
236
|
+
properties: {
|
|
237
|
+
root: { $ref: "#/$defs/Node" },
|
|
238
|
+
},
|
|
239
|
+
required: ["root"],
|
|
240
|
+
$defs: {
|
|
241
|
+
Node: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
value: { type: "string", description: "Node value" },
|
|
245
|
+
child: { $ref: "#/$defs/Node" },
|
|
246
|
+
},
|
|
247
|
+
required: ["value"],
|
|
248
|
+
description: "A tree node",
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
254
|
+
|
|
255
|
+
// Must not throw or hang
|
|
256
|
+
const result = convertJsonSchemaToZodSchema(jsonSchema, true);
|
|
257
|
+
expect(result).toBeDefined();
|
|
258
|
+
|
|
259
|
+
// The circular ref should have produced a console.warn
|
|
260
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
261
|
+
expect.stringContaining("Circular $ref detected"),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// The top-level shape should still have a "root" key that is an object
|
|
265
|
+
const shape = (result as z.ZodObject<any>).shape;
|
|
266
|
+
expect(shape.root).toBeDefined();
|
|
267
|
+
|
|
268
|
+
// Inside root, "value" should be a string and "child" should be z.any()
|
|
269
|
+
// (child is optional since it's not in required[], so unwrap ZodOptional)
|
|
270
|
+
const rootShape = (shape.root as z.ZodObject<any>).shape;
|
|
271
|
+
expect(rootShape.value._def.typeName).toBe("ZodString");
|
|
272
|
+
const childDef = rootShape.child._def;
|
|
273
|
+
if (childDef.typeName === "ZodOptional") {
|
|
274
|
+
expect(childDef.innerType._def.typeName).toBe("ZodAny");
|
|
275
|
+
} else {
|
|
276
|
+
expect(childDef.typeName).toBe("ZodAny");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
warnSpy.mockRestore();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should resolve the same $ref used by multiple sibling properties", () => {
|
|
283
|
+
// Two properties reference the same $def — the visited set must NOT
|
|
284
|
+
// mark the second usage as circular.
|
|
285
|
+
const jsonSchema = {
|
|
286
|
+
type: "object",
|
|
287
|
+
properties: {
|
|
288
|
+
billing: { $ref: "#/$defs/Address" },
|
|
289
|
+
shipping: { $ref: "#/$defs/Address" },
|
|
290
|
+
},
|
|
291
|
+
required: ["billing", "shipping"],
|
|
292
|
+
$defs: {
|
|
293
|
+
Address: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
street: { type: "string", description: "Street" },
|
|
297
|
+
city: { type: "string", description: "City" },
|
|
298
|
+
},
|
|
299
|
+
required: ["street", "city"],
|
|
300
|
+
description: "An address",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
306
|
+
|
|
307
|
+
const result = convertJsonSchemaToZodSchema(jsonSchema, true);
|
|
308
|
+
const shape = (result as z.ZodObject<any>).shape;
|
|
309
|
+
|
|
310
|
+
// Both should be fully resolved objects, NOT z.any()
|
|
311
|
+
expect(shape.billing._def.typeName).toBe("ZodObject");
|
|
312
|
+
expect(shape.shipping._def.typeName).toBe("ZodObject");
|
|
313
|
+
|
|
314
|
+
// No circular-ref warning should have been emitted
|
|
315
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
316
|
+
|
|
317
|
+
warnSpy.mockRestore();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should resolve a shared $ref used in different branches of a $ref chain", () => {
|
|
321
|
+
// Wrapper -> Container (via $ref) which has two children both using $ref to Leaf.
|
|
322
|
+
// Without set cloning, the second Leaf ref in Container would be wrongly flagged
|
|
323
|
+
// as circular because the first Leaf resolution already added it to the set.
|
|
324
|
+
const jsonSchema = {
|
|
325
|
+
type: "object",
|
|
326
|
+
properties: {
|
|
327
|
+
wrapper: { $ref: "#/$defs/Container" },
|
|
328
|
+
},
|
|
329
|
+
required: ["wrapper"],
|
|
330
|
+
$defs: {
|
|
331
|
+
Container: {
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {
|
|
334
|
+
first: { $ref: "#/$defs/Leaf" },
|
|
335
|
+
second: { $ref: "#/$defs/Leaf" },
|
|
336
|
+
},
|
|
337
|
+
required: ["first", "second"],
|
|
338
|
+
description: "A container with two leaves",
|
|
339
|
+
},
|
|
340
|
+
Leaf: {
|
|
341
|
+
type: "object",
|
|
342
|
+
properties: {
|
|
343
|
+
label: { type: "string", description: "Leaf label" },
|
|
344
|
+
},
|
|
345
|
+
required: ["label"],
|
|
346
|
+
description: "A leaf node",
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
352
|
+
|
|
353
|
+
const result = convertJsonSchemaToZodSchema(jsonSchema, true);
|
|
354
|
+
const wrapperShape = (
|
|
355
|
+
(result as z.ZodObject<any>).shape.wrapper as z.ZodObject<any>
|
|
356
|
+
).shape;
|
|
357
|
+
|
|
358
|
+
// Both first and second should be fully resolved Leaf objects
|
|
359
|
+
expect(wrapperShape.first._def.typeName).toBe("ZodObject");
|
|
360
|
+
expect(wrapperShape.second._def.typeName).toBe("ZodObject");
|
|
361
|
+
|
|
362
|
+
// No circular-ref warning should have been emitted
|
|
363
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
364
|
+
|
|
365
|
+
warnSpy.mockRestore();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("should handle anyOf with $ref variants", () => {
|
|
369
|
+
const jsonSchema = {
|
|
370
|
+
type: "object",
|
|
371
|
+
properties: {
|
|
372
|
+
pet: {
|
|
373
|
+
anyOf: [{ $ref: "#/$defs/Cat" }, { $ref: "#/$defs/Dog" }],
|
|
374
|
+
description: "A pet",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
required: ["pet"],
|
|
378
|
+
$defs: {
|
|
379
|
+
Cat: {
|
|
380
|
+
type: "object",
|
|
381
|
+
properties: { meow: { type: "boolean" } },
|
|
382
|
+
required: ["meow"],
|
|
383
|
+
},
|
|
384
|
+
Dog: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: { bark: { type: "boolean" } },
|
|
387
|
+
required: ["bark"],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const result = convertJsonSchemaToZodSchema(jsonSchema, true);
|
|
393
|
+
expect(result).toBeDefined();
|
|
394
|
+
|
|
395
|
+
// Should produce a union inside the "pet" property
|
|
396
|
+
const petSchema = (result as z.ZodObject<any>).shape.pet;
|
|
397
|
+
expect(petSchema._def.typeName).toBe("ZodUnion");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should handle integer type as z.number()", () => {
|
|
401
|
+
const jsonSchema = {
|
|
402
|
+
type: "object",
|
|
403
|
+
properties: {
|
|
404
|
+
count: { type: "integer", description: "A count" },
|
|
405
|
+
},
|
|
406
|
+
required: ["count"],
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const result = convertJsonSchemaToZodSchema(jsonSchema, true);
|
|
410
|
+
const shape = (result as z.ZodObject<any>).shape;
|
|
411
|
+
expect(shape.count._def.typeName).toBe("ZodNumber");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("should handle null type", () => {
|
|
415
|
+
const jsonSchema = {
|
|
416
|
+
type: "object",
|
|
417
|
+
properties: {
|
|
418
|
+
empty: { type: "null", description: "Always null" },
|
|
419
|
+
},
|
|
420
|
+
required: ["empty"],
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const result = convertJsonSchemaToZodSchema(jsonSchema, true);
|
|
424
|
+
const shape = (result as z.ZodObject<any>).shape;
|
|
425
|
+
expect(shape.empty._def.typeName).toBe("ZodNull");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("should warn and return z.any() for unsupported schema types", () => {
|
|
429
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
430
|
+
|
|
431
|
+
const jsonSchema = { type: "custom_unsupported" };
|
|
432
|
+
const result = convertJsonSchemaToZodSchema(jsonSchema, true);
|
|
433
|
+
|
|
434
|
+
expect(result._def.typeName).toBe("ZodAny");
|
|
435
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
436
|
+
expect.stringContaining(
|
|
437
|
+
'Unsupported JSON schema type "custom_unsupported"',
|
|
438
|
+
),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
warnSpy.mockRestore();
|
|
442
|
+
});
|
|
194
443
|
});
|
|
195
444
|
|
|
196
445
|
describe("jsonSchemaToActionParameters", () => {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely copies text to the clipboard.
|
|
3
|
+
*
|
|
4
|
+
* Checks that the Clipboard API is available before attempting the write,
|
|
5
|
+
* and catches any errors (e.g. permission denied, insecure context).
|
|
6
|
+
*
|
|
7
|
+
* @param text - The text to copy to the clipboard.
|
|
8
|
+
* @returns `true` if the text was successfully copied, `false` otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
11
|
+
if (!navigator.clipboard?.writeText) {
|
|
12
|
+
console.error("Clipboard API is not available");
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await navigator.clipboard.writeText(text);
|
|
18
|
+
return true;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error("Failed to copy to clipboard:", err);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/utils/index.ts
CHANGED
package/src/utils/json-schema.ts
CHANGED
|
@@ -241,7 +241,69 @@ function convertAttribute(attribute: Parameter): JSONSchema {
|
|
|
241
241
|
export function convertJsonSchemaToZodSchema(
|
|
242
242
|
jsonSchema: any,
|
|
243
243
|
required: boolean,
|
|
244
|
+
definitions?: Record<string, any>,
|
|
245
|
+
visitedRefs?: Set<string>,
|
|
244
246
|
): z.ZodSchema {
|
|
247
|
+
// Resolve $ref references
|
|
248
|
+
if (jsonSchema.$ref && definitions) {
|
|
249
|
+
const refPath = jsonSchema.$ref.replace(
|
|
250
|
+
/^#\/\$defs\/|^#\/definitions\//,
|
|
251
|
+
"",
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Detect circular $ref cycles
|
|
255
|
+
const refs = visitedRefs ?? new Set<string>();
|
|
256
|
+
if (refs.has(refPath)) {
|
|
257
|
+
console.warn(
|
|
258
|
+
`[CopilotKit] Circular $ref detected for "${refPath}" — falling back to z.any()`,
|
|
259
|
+
);
|
|
260
|
+
let schema = z.any();
|
|
261
|
+
if (jsonSchema.description) {
|
|
262
|
+
schema = schema.describe(jsonSchema.description);
|
|
263
|
+
}
|
|
264
|
+
return required ? schema : schema.optional();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const resolved = definitions[refPath];
|
|
268
|
+
if (resolved) {
|
|
269
|
+
// Clone the set so sibling branches don't see each other's visited refs
|
|
270
|
+
const nextRefs = new Set(refs);
|
|
271
|
+
nextRefs.add(refPath);
|
|
272
|
+
return convertJsonSchemaToZodSchema(
|
|
273
|
+
resolved,
|
|
274
|
+
required,
|
|
275
|
+
definitions,
|
|
276
|
+
nextRefs,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Collect top-level definitions for $ref resolution
|
|
282
|
+
const defs = definitions ?? jsonSchema.$defs ?? jsonSchema.definitions;
|
|
283
|
+
|
|
284
|
+
// Handle anyOf / oneOf as z.union
|
|
285
|
+
const unionVariants = jsonSchema.anyOf ?? jsonSchema.oneOf;
|
|
286
|
+
if (Array.isArray(unionVariants) && unionVariants.length > 0) {
|
|
287
|
+
if (unionVariants.length === 1) {
|
|
288
|
+
return convertJsonSchemaToZodSchema(
|
|
289
|
+
unionVariants[0],
|
|
290
|
+
required,
|
|
291
|
+
defs,
|
|
292
|
+
visitedRefs,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const schemas = unionVariants.map((v: any) =>
|
|
296
|
+
convertJsonSchemaToZodSchema(v, true, defs, visitedRefs),
|
|
297
|
+
);
|
|
298
|
+
let schema = z.union(
|
|
299
|
+
schemas as [z.ZodSchema, z.ZodSchema, ...z.ZodSchema[]],
|
|
300
|
+
);
|
|
301
|
+
if (jsonSchema.description) {
|
|
302
|
+
schema = schema.describe(jsonSchema.description);
|
|
303
|
+
}
|
|
304
|
+
return required ? schema : schema.optional();
|
|
305
|
+
}
|
|
306
|
+
|
|
245
307
|
if (jsonSchema.type === "object") {
|
|
246
308
|
const spec: { [key: string]: z.ZodSchema } = {};
|
|
247
309
|
|
|
@@ -253,6 +315,8 @@ export function convertJsonSchemaToZodSchema(
|
|
|
253
315
|
spec[key] = convertJsonSchemaToZodSchema(
|
|
254
316
|
value,
|
|
255
317
|
jsonSchema.required ? jsonSchema.required.includes(key) : false,
|
|
318
|
+
defs,
|
|
319
|
+
visitedRefs,
|
|
256
320
|
);
|
|
257
321
|
}
|
|
258
322
|
let schema = z.object(spec).describe(jsonSchema.description);
|
|
@@ -266,18 +330,35 @@ export function convertJsonSchemaToZodSchema(
|
|
|
266
330
|
}
|
|
267
331
|
let schema = z.string().describe(jsonSchema.description);
|
|
268
332
|
return required ? schema : schema.optional();
|
|
269
|
-
} else if (jsonSchema.type === "number") {
|
|
333
|
+
} else if (jsonSchema.type === "number" || jsonSchema.type === "integer") {
|
|
270
334
|
let schema = z.number().describe(jsonSchema.description);
|
|
271
335
|
return required ? schema : schema.optional();
|
|
272
336
|
} else if (jsonSchema.type === "boolean") {
|
|
273
337
|
let schema = z.boolean().describe(jsonSchema.description);
|
|
274
338
|
return required ? schema : schema.optional();
|
|
275
339
|
} else if (jsonSchema.type === "array") {
|
|
276
|
-
let itemSchema = convertJsonSchemaToZodSchema(
|
|
340
|
+
let itemSchema = convertJsonSchemaToZodSchema(
|
|
341
|
+
jsonSchema.items,
|
|
342
|
+
true,
|
|
343
|
+
defs,
|
|
344
|
+
visitedRefs,
|
|
345
|
+
);
|
|
277
346
|
let schema = z.array(itemSchema).describe(jsonSchema.description);
|
|
278
347
|
return required ? schema : schema.optional();
|
|
348
|
+
} else if (jsonSchema.type === "null") {
|
|
349
|
+
let schema = z.null().describe(jsonSchema.description);
|
|
350
|
+
return required ? schema : schema.optional();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Fallback: accept any value rather than throwing
|
|
354
|
+
console.warn(
|
|
355
|
+
`[CopilotKit] Unsupported JSON schema type "${jsonSchema.type ?? "unknown"}" — falling back to z.any()`,
|
|
356
|
+
);
|
|
357
|
+
let schema = z.any();
|
|
358
|
+
if (jsonSchema.description) {
|
|
359
|
+
schema = schema.describe(jsonSchema.description);
|
|
279
360
|
}
|
|
280
|
-
|
|
361
|
+
return required ? schema : schema.optional();
|
|
281
362
|
}
|
|
282
363
|
|
|
283
364
|
export function getZodParameters<T extends [] | Parameter[] | undefined>(
|
package/src/utils/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AgentCapabilities } from "@ag-ui/core";
|
|
2
|
+
|
|
1
3
|
export type MaybePromise<T> = T | PromiseLike<T>;
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -17,6 +19,7 @@ export interface AgentDescription {
|
|
|
17
19
|
name: string;
|
|
18
20
|
className: string;
|
|
19
21
|
description: string;
|
|
22
|
+
capabilities?: AgentCapabilities;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export type RuntimeMode = "sse" | "intelligence";
|