@hsb3/carbon-agui-adapter 0.1.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.
@@ -0,0 +1,99 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Runtime type-safety at the two AG-UI/Carbon trust boundaries (issue gmb9).
3
+ //
4
+ // - IN seam: every event the runner yields, validated against the protocol's
5
+ // own schema (@ag-ui/core EventSchemas) before the adapter switches on it.
6
+ // - OUT seam: every CUSTOM-carried Carbon item, validated for a known
7
+ // response_type plus its required top-level fields before it is emitted.
8
+ //
9
+ // Both validators are shallow by design: gross malformation is what crosses a
10
+ // trust boundary, and a shallow check catches it cheaply without duplicating
11
+ // the whole Carbon schema here. Nested items (card.body, carousel.items, grid
12
+ // cells) are NOT recursed — a possible follow-up if deep validation is wanted.
13
+ // ---------------------------------------------------------------------------
14
+ import { EventSchemas } from '@ag-ui/core';
15
+ /**
16
+ * IN seam. Validate a raw runner event against the AG-UI protocol schema.
17
+ * On success the parsed value is a canonical event; the cast to the adapter's
18
+ * local `AgUiEvent` is justified because it was validated against the
19
+ * protocol's own union and the local type is a structural subset the switch
20
+ * handles. On failure, `error` is zod's error summary.
21
+ */
22
+ export function validateAgUiEvent(raw) {
23
+ const result = EventSchemas.safeParse(raw);
24
+ if (result.success)
25
+ return { ok: true, event: result.data };
26
+ return { ok: false, error: result.error.message };
27
+ }
28
+ const isObject = (v) => v !== null && typeof v === 'object';
29
+ const isString = (v) => typeof v === 'string';
30
+ /**
31
+ * OUT seam. Validate a raw CUSTOM-carried Carbon item before the adapter emits
32
+ * it: it must be an object, carry a `response_type` string in `known` (the
33
+ * adapter passes its KNOWN_CARBON_TYPES allowlist so this file needs no coverage
34
+ * import), and satisfy the required top-level fields for that response_type.
35
+ */
36
+ export function validateCarbonItem(raw, known) {
37
+ if (!isObject(raw))
38
+ return { ok: false, reason: 'item is not a non-null object' };
39
+ const rt = raw.response_type;
40
+ if (!isString(rt))
41
+ return { ok: false, reason: 'response_type is not a string' };
42
+ if (!known.has(rt))
43
+ return { ok: false, reason: `unknown response_type: ${rt}` };
44
+ let reason = null;
45
+ switch (rt) {
46
+ case 'text':
47
+ case 'conversational_search':
48
+ if (!isString(raw.text))
49
+ reason = `${rt}: text must be a string`;
50
+ break;
51
+ case 'option':
52
+ if (!Array.isArray(raw.options))
53
+ reason = 'option: options must be an array';
54
+ break;
55
+ case 'image':
56
+ case 'video':
57
+ case 'audio':
58
+ case 'iframe':
59
+ if (!isString(raw.source))
60
+ reason = `${rt}: source must be a string`;
61
+ break;
62
+ case 'carousel':
63
+ if (!Array.isArray(raw.items))
64
+ reason = 'carousel: items must be an array';
65
+ break;
66
+ case 'grid':
67
+ if (!Array.isArray(raw.columns) || !Array.isArray(raw.rows)) {
68
+ reason = 'grid: columns and rows must both be arrays';
69
+ }
70
+ break;
71
+ case 'button':
72
+ if (!isString(raw.button_type))
73
+ reason = 'button: button_type must be a string';
74
+ break;
75
+ case 'system':
76
+ if (!isString(raw.title))
77
+ reason = 'system: title must be a string';
78
+ break;
79
+ case 'preview_card':
80
+ if (!isString(raw.workspace_id))
81
+ reason = 'preview_card: workspace_id must be a string';
82
+ break;
83
+ case 'user_defined':
84
+ if (!isObject(raw.user_defined))
85
+ reason = 'user_defined: user_defined must be a non-null object';
86
+ break;
87
+ case 'card':
88
+ // No strictly-required field, but body/footer are arrays when present.
89
+ if (raw.body !== undefined && !Array.isArray(raw.body))
90
+ reason = 'card: body must be an array when present';
91
+ else if (raw.footer !== undefined && !Array.isArray(raw.footer))
92
+ reason = 'card: footer must be an array when present';
93
+ break;
94
+ // date, pause, inline_error: no required field beyond response_type.
95
+ }
96
+ if (reason)
97
+ return { ok: false, reason };
98
+ return { ok: true, item: raw };
99
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@hsb3/carbon-agui-adapter",
3
+ "version": "0.1.0",
4
+ "description": "Adapter that drives IBM Carbon AI Chat from an AG-UI event stream",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "@hsb3:registry": "https://registry.npmjs.org/"
23
+ },
24
+ "scripts": {
25
+ "typecheck": "tsc -p tsconfig.json",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "check": "bun run typecheck && bun run test",
29
+ "coverage": "bun scripts/coverage.ts",
30
+ "build": "tsc -p tsconfig.build.json"
31
+ },
32
+ "dependencies": {
33
+ "@ag-ui/core": "0.0.58"
34
+ },
35
+ "devDependencies": {
36
+ "@carbon/ai-chat": "1.19.0",
37
+ "@types/node": "^26.2.0",
38
+ "typescript": "^5.6.0",
39
+ "vitest": "^3.2.6"
40
+ }
41
+ }