@uix-ai/adapter-a2ui 0.0.1

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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @uix-ai/adapter-a2ui
2
+
3
+ Adapter to convert [Google A2UI](https://github.com/google/A2UI) protocol payloads to UIX Lucid IR format.
4
+
5
+ > **Experimental.** A2UI is an early-stage protocol and its specification may change. This adapter tracks the latest available version (`v0.10`).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @uix-ai/adapter-a2ui
11
+ ```
12
+
13
+ Peer dependency: `@uix-ai/core`
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { fromA2UIPayload, toA2UIPayload } from '@uix-ai/adapter-a2ui'
19
+
20
+ // A2UI -> UIX Lucid IR
21
+ const conversation = fromA2UIPayload(a2uiPayload)
22
+
23
+ // UIX Lucid IR -> A2UI
24
+ const payload = toA2UIPayload(conversation)
25
+ ```
26
+
27
+ ## `fromA2UIPayload(payload, options?)`
28
+
29
+ Converts an A2UI `updateComponents` payload into a `LucidConversation`. The A2UI component tree is flattened into a linear sequence of `LucidBlock` items. Container components (Row, Column, Card, etc.) are traversed but not emitted as blocks.
30
+
31
+ Returns `null` if the payload does not contain `updateComponents`.
32
+
33
+ ```typescript
34
+ import { fromA2UIPayload } from '@uix-ai/adapter-a2ui'
35
+ import type { A2UIPayload } from '@uix-ai/adapter-a2ui'
36
+
37
+ const payload: A2UIPayload = {
38
+ version: 'v0.10',
39
+ updateComponents: {
40
+ surfaceId: 'form_1',
41
+ components: [
42
+ { id: 'root', component: 'Column', children: ['title', 'img'] },
43
+ { id: 'title', component: 'Text', text: 'Hello World' },
44
+ { id: 'img', component: 'Image', url: 'https://example.com/photo.png' },
45
+ ],
46
+ },
47
+ }
48
+
49
+ const conversation = fromA2UIPayload(payload)
50
+ // conversation.blocks[0] -> text block "Hello World"
51
+ // conversation.blocks[1] -> image block
52
+ ```
53
+
54
+ ### Batch conversion
55
+
56
+ ```typescript
57
+ import { fromA2UIPayloads } from '@uix-ai/adapter-a2ui'
58
+
59
+ const conversations = fromA2UIPayloads(payloadArray)
60
+ ```
61
+
62
+ ## `toA2UIPayload(conversation, surfaceId?, version?)`
63
+
64
+ Converts a `LucidConversation` back to an A2UI `updateComponents` payload. All blocks are wrapped in a root `Column` layout.
65
+
66
+ ```typescript
67
+ import { toA2UIPayload } from '@uix-ai/adapter-a2ui'
68
+
69
+ const payload = toA2UIPayload(conversation)
70
+ // payload.updateComponents.components -> [Column root, Text, Image, ...]
71
+ ```
72
+
73
+ ## Supported Component Types
74
+
75
+ ### Display Components
76
+
77
+ | A2UI Component | UIX Block Type | Notes |
78
+ |----------------|----------------|-------|
79
+ | `Text` | `text` | Direct mapping |
80
+ | `Image` | `image` | Direct mapping |
81
+ | `Icon` | `text` | Rendered as `[Icon: name]` |
82
+ | `Video` | `text` | Rendered as `[Video: url]` |
83
+ | `AudioPlayer` | `text` | Rendered as `[Audio: url]` |
84
+
85
+ ### Container Components (traversed, not emitted)
86
+
87
+ `Row`, `Column`, `Card`, `List`, `Tabs`, `Modal`
88
+
89
+ Containers are walked to collect child components but do not produce blocks themselves. `Tabs` children are emitted with bold tab titles.
90
+
91
+ ### Input Components
92
+
93
+ | A2UI Component | UIX Block Type | Notes |
94
+ |----------------|----------------|-------|
95
+ | `TextField` | `text` | `[TextField: label]` |
96
+ | `CheckBox` | `text` | `[CheckBox: label]` |
97
+ | `DateTimeInput` | `text` | `[DateTimeInput: label]` |
98
+ | `ChoicePicker` | `text` | `[ChoicePicker: label] options=[...]` |
99
+ | `Slider` | `text` | `[Slider: label] range=[min, max]` |
100
+
101
+ ### Interactive Components
102
+
103
+ | A2UI Component | UIX Block Type | Notes |
104
+ |----------------|----------------|-------|
105
+ | `Button` | `text` | `[Button: label] (action:name)` |
106
+ | `Divider` | `text` | Rendered as `---` |
107
+
108
+ ## Conversion Options
109
+
110
+ ```typescript
111
+ import type { A2UIConversionOptions } from '@uix-ai/adapter-a2ui'
112
+
113
+ const options: A2UIConversionOptions = {
114
+ generateConversationId: (surfaceId) => `my-${surfaceId}`,
115
+ generateBlockId: (componentId) => `blk-${componentId}`,
116
+ resolveValue: (dynamic) => {
117
+ // Resolve data-binding expressions against your data model
118
+ if (typeof dynamic === 'object' && 'path' in dynamic) {
119
+ return lookupPath(dynamic.path)
120
+ }
121
+ return String(dynamic)
122
+ },
123
+ }
124
+ ```
125
+
126
+ ## Dynamic Values
127
+
128
+ A2UI supports dynamic data bindings. Without a `resolveValue` function, bindings are rendered as placeholders:
129
+
130
+ - Path binding: `{ path: "user.name" }` renders as `{{user.name}}`
131
+ - Function call: `{ call: "formatDate", args: {...} }` renders as `{{formatDate(...)}}`
132
+
133
+ Provide a `resolveValue` callback to resolve these against your application's data model.
134
+
135
+ ## Links
136
+
137
+ - [UIX Repository](https://github.com/Deepractice/UIX)
138
+ - [Google A2UI Protocol](https://github.com/google/A2UI)
@@ -0,0 +1,334 @@
1
+ import { LucidConversation } from '@uix-ai/core';
2
+
3
+ /**
4
+ * @uix-ai/adapter-a2ui
5
+ *
6
+ * Adapter to convert Google A2UI protocol payloads to UIX Lucid IR format.
7
+ *
8
+ * A2UI is a declarative UI protocol where agents generate JSON payloads
9
+ * describing UI components. Unlike AG-UI (event streaming), A2UI is
10
+ * snapshot-based - the agent sends a complete UI description as JSON.
11
+ *
12
+ * This adapter converts A2UI's component tree into UIX LucidConversation
13
+ * and LucidBlock structures for rendering with UIX components.
14
+ *
15
+ * @see https://github.com/google/A2UI
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { fromA2UIPayload, toA2UIPayload } from '@uix-ai/adapter-a2ui'
20
+ *
21
+ * // Convert A2UI payload to UIX Lucid IR
22
+ * const conversation = fromA2UIPayload(a2uiPayload)
23
+ *
24
+ * // Convert UIX Lucid IR back to A2UI payload
25
+ * const payload = toA2UIPayload(conversation)
26
+ * ```
27
+ */
28
+
29
+ /**
30
+ * A2UI protocol version
31
+ */
32
+ type A2UIVersion = 'v0.8' | 'v0.9' | 'v0.10';
33
+ /**
34
+ * A2UI display component types
35
+ */
36
+ type A2UIDisplayComponent = 'Text' | 'Image' | 'Icon' | 'Video' | 'AudioPlayer';
37
+ /**
38
+ * A2UI container component types
39
+ */
40
+ type A2UIContainerComponent = 'Row' | 'Column' | 'Card' | 'List' | 'Tabs' | 'Modal';
41
+ /**
42
+ * A2UI input component types
43
+ */
44
+ type A2UIInputComponent = 'TextField' | 'CheckBox' | 'DateTimeInput' | 'ChoicePicker' | 'Slider';
45
+ /**
46
+ * A2UI interactive component types
47
+ */
48
+ type A2UIInteractiveComponent = 'Button' | 'Divider';
49
+ /**
50
+ * Union of all A2UI component type names
51
+ */
52
+ type A2UIComponentType = A2UIDisplayComponent | A2UIContainerComponent | A2UIInputComponent | A2UIInteractiveComponent;
53
+ /**
54
+ * A2UI dynamic string - either a literal or a data-binding reference
55
+ */
56
+ type A2UIDynamicString = string | {
57
+ path: string;
58
+ } | {
59
+ call: string;
60
+ args: Record<string, unknown>;
61
+ };
62
+ /**
63
+ * A2UI dynamic number
64
+ */
65
+ type A2UIDynamicNumber = number | {
66
+ path: string;
67
+ } | {
68
+ call: string;
69
+ args: Record<string, unknown>;
70
+ };
71
+ /**
72
+ * A2UI dynamic boolean
73
+ */
74
+ type A2UIDynamicBoolean = boolean | {
75
+ path: string;
76
+ } | {
77
+ call: string;
78
+ args: Record<string, unknown>;
79
+ };
80
+ /**
81
+ * A2UI child list - either an array of IDs or a template binding
82
+ */
83
+ type A2UIChildList = string[] | {
84
+ path: string;
85
+ componentId: string;
86
+ };
87
+ /**
88
+ * A2UI validation check
89
+ */
90
+ interface A2UICheck {
91
+ call: string;
92
+ args: Record<string, unknown>;
93
+ message: string;
94
+ }
95
+ /**
96
+ * A2UI event action
97
+ */
98
+ interface A2UIEventAction {
99
+ name: string;
100
+ context?: Record<string, unknown>;
101
+ }
102
+ /**
103
+ * A2UI function call action
104
+ */
105
+ interface A2UIFunctionCallAction {
106
+ name: string;
107
+ args?: Record<string, unknown>;
108
+ }
109
+ /**
110
+ * A2UI action - either an event or a function call
111
+ */
112
+ interface A2UIAction {
113
+ event?: A2UIEventAction;
114
+ functionCall?: A2UIFunctionCallAction;
115
+ }
116
+ /**
117
+ * A2UI component object in the flat adjacency list
118
+ *
119
+ * Components are stored as a flat array and reference each other by ID.
120
+ * The tree structure is built via `children` and `child` properties.
121
+ */
122
+ interface A2UIComponent {
123
+ /** Unique identifier for this component */
124
+ id: string;
125
+ /** Component type name */
126
+ component: A2UIComponentType | string;
127
+ /** Child component IDs (for container components like Row, Column, List) */
128
+ children?: A2UIChildList;
129
+ /** Single child component ID (for Card, Modal) */
130
+ child?: string;
131
+ /** Text content (for Text, Button) */
132
+ text?: A2UIDynamicString;
133
+ /** Label (for input components) */
134
+ label?: A2UIDynamicString;
135
+ /** URL (for Image, Video, AudioPlayer) */
136
+ url?: A2UIDynamicString;
137
+ /** Icon name (for Icon) */
138
+ name?: A2UIDynamicString;
139
+ /** Value binding (for input components) */
140
+ value?: A2UIDynamicString | A2UIDynamicNumber | A2UIDynamicBoolean;
141
+ /** Action on interaction (for Button) */
142
+ action?: A2UIAction;
143
+ /** Validation checks (for TextField, Button) */
144
+ checks?: A2UICheck[];
145
+ /** Button variant */
146
+ variant?: string;
147
+ /** Layout alignment */
148
+ justify?: string;
149
+ /** Layout cross-axis alignment */
150
+ align?: string;
151
+ /** Axis for Divider */
152
+ axis?: string;
153
+ /** Tabs configuration */
154
+ tabs?: Array<{
155
+ title: A2UIDynamicString;
156
+ child: string;
157
+ }>;
158
+ /** Slider min value */
159
+ min?: number;
160
+ /** Slider max value */
161
+ max?: number;
162
+ /** Choice picker options */
163
+ options?: Array<{
164
+ label: A2UIDynamicString;
165
+ value: string;
166
+ }>;
167
+ /** Any additional properties from custom or extended components */
168
+ [key: string]: unknown;
169
+ }
170
+ /**
171
+ * A2UI surface theme configuration
172
+ */
173
+ interface A2UITheme {
174
+ primaryColor?: string;
175
+ [key: string]: unknown;
176
+ }
177
+ /**
178
+ * createSurface message - initializes a new UI surface
179
+ */
180
+ interface A2UICreateSurface {
181
+ surfaceId: string;
182
+ catalogId?: string;
183
+ theme?: A2UITheme;
184
+ sendDataModel?: boolean;
185
+ }
186
+ /**
187
+ * updateComponents message - adds or updates UI components
188
+ */
189
+ interface A2UIUpdateComponents {
190
+ surfaceId: string;
191
+ components: A2UIComponent[];
192
+ }
193
+ /**
194
+ * updateDataModel message - modifies data model via JSON pointer
195
+ */
196
+ interface A2UIUpdateDataModel {
197
+ surfaceId: string;
198
+ path?: string;
199
+ value?: unknown;
200
+ }
201
+ /**
202
+ * deleteSurface message - removes a surface
203
+ */
204
+ interface A2UIDeleteSurface {
205
+ surfaceId: string;
206
+ }
207
+ /**
208
+ * Top-level A2UI message payload.
209
+ *
210
+ * Each message contains exactly one of the four message types:
211
+ * - `createSurface`: Initialize a new UI surface
212
+ * - `updateComponents`: Add or update components on a surface
213
+ * - `updateDataModel`: Modify the surface data model
214
+ * - `deleteSurface`: Remove a surface
215
+ */
216
+ interface A2UIPayload {
217
+ /** A2UI protocol version */
218
+ version: string;
219
+ /** Create a new surface */
220
+ createSurface?: A2UICreateSurface;
221
+ /** Update components on a surface */
222
+ updateComponents?: A2UIUpdateComponents;
223
+ /** Update the data model */
224
+ updateDataModel?: A2UIUpdateDataModel;
225
+ /** Delete a surface */
226
+ deleteSurface?: A2UIDeleteSurface;
227
+ }
228
+ /**
229
+ * Options for A2UI to Lucid IR conversion
230
+ */
231
+ interface A2UIConversionOptions {
232
+ /**
233
+ * Custom ID generator for conversations
234
+ * @default () => `a2ui-conv-${surfaceId}`
235
+ */
236
+ generateConversationId?: (surfaceId: string) => string;
237
+ /**
238
+ * Custom ID generator for blocks
239
+ * @default () => `a2ui-block-${componentId}`
240
+ */
241
+ generateBlockId?: (componentId: string) => string;
242
+ /**
243
+ * Resolve dynamic string values against a data model.
244
+ * If not provided, dynamic bindings are serialized as-is.
245
+ */
246
+ resolveValue?: (dynamic: A2UIDynamicString | A2UIDynamicNumber | A2UIDynamicBoolean) => string;
247
+ }
248
+ /**
249
+ * Convert an A2UI payload (containing updateComponents) to a UIX LucidConversation.
250
+ *
251
+ * The A2UI component tree is flattened into a linear sequence of LucidBlocks:
252
+ * - `Text` components become text blocks
253
+ * - `Image` components become image blocks
254
+ * - Input and interactive components become text blocks with structured descriptions
255
+ * - Container components (Row, Column, Card, etc.) are traversed but not emitted
256
+ *
257
+ * @param payload - An A2UI payload message (must contain updateComponents)
258
+ * @param options - Conversion options
259
+ * @returns A LucidConversation representing the A2UI surface, or null if the payload
260
+ * does not contain updateComponents
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const payload: A2UIPayload = {
265
+ * version: 'v0.10',
266
+ * updateComponents: {
267
+ * surfaceId: 'form_1',
268
+ * components: [
269
+ * { id: 'root', component: 'Column', children: ['title', 'img'] },
270
+ * { id: 'title', component: 'Text', text: 'Hello World' },
271
+ * { id: 'img', component: 'Image', url: 'https://example.com/photo.png' }
272
+ * ]
273
+ * }
274
+ * }
275
+ *
276
+ * const conversation = fromA2UIPayload(payload)
277
+ * // conversation.blocks[0] -> text block "Hello World"
278
+ * // conversation.blocks[1] -> image block
279
+ * ```
280
+ */
281
+ declare function fromA2UIPayload(payload: A2UIPayload, options?: A2UIConversionOptions): LucidConversation | null;
282
+ /**
283
+ * Convert multiple A2UI payloads (a stream of messages) to LucidConversations.
284
+ *
285
+ * Processes an array of A2UI messages and returns one LucidConversation per
286
+ * updateComponents message encountered.
287
+ *
288
+ * @param payloads - Array of A2UI payload messages
289
+ * @param options - Conversion options
290
+ * @returns Array of LucidConversations
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * const messages: A2UIPayload[] = [
295
+ * { version: 'v0.10', createSurface: { surfaceId: 's1', catalogId: '...' } },
296
+ * { version: 'v0.10', updateComponents: { surfaceId: 's1', components: [...] } },
297
+ * ]
298
+ *
299
+ * const conversations = fromA2UIPayloads(messages)
300
+ * ```
301
+ */
302
+ declare function fromA2UIPayloads(payloads: A2UIPayload[], options?: A2UIConversionOptions): LucidConversation[];
303
+ /**
304
+ * Convert a UIX LucidConversation back to an A2UI payload.
305
+ *
306
+ * Maps LucidBlocks back to A2UI components wrapped in a Column layout:
307
+ * - text blocks -> Text components
308
+ * - image blocks -> Image components
309
+ * - Other block types -> Text components with type annotation
310
+ *
311
+ * @param conversation - A UIX LucidConversation
312
+ * @param surfaceId - Optional surface ID (defaults to conversation.id)
313
+ * @param version - A2UI protocol version (defaults to 'v0.10')
314
+ * @returns An A2UI payload with updateComponents
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * const conversation: LucidConversation = {
319
+ * id: 'conv-1',
320
+ * role: 'assistant',
321
+ * status: 'completed',
322
+ * blocks: [
323
+ * { id: 'b1', type: 'text', status: 'completed', content: { text: 'Hello' } },
324
+ * { id: 'b2', type: 'image', status: 'completed', content: { url: 'https://...' } }
325
+ * ],
326
+ * timestamp: Date.now()
327
+ * }
328
+ *
329
+ * const payload = toA2UIPayload(conversation)
330
+ * ```
331
+ */
332
+ declare function toA2UIPayload(conversation: LucidConversation, surfaceId?: string, version?: string): A2UIPayload;
333
+
334
+ export { type A2UIAction, type A2UICheck, type A2UIChildList, type A2UIComponent, type A2UIComponentType, type A2UIContainerComponent, type A2UIConversionOptions, type A2UICreateSurface, type A2UIDeleteSurface, type A2UIDisplayComponent, type A2UIDynamicBoolean, type A2UIDynamicNumber, type A2UIDynamicString, type A2UIEventAction, type A2UIFunctionCallAction, type A2UIInputComponent, type A2UIInteractiveComponent, type A2UIPayload, type A2UITheme, type A2UIUpdateComponents, type A2UIUpdateDataModel, type A2UIVersion, fromA2UIPayload, fromA2UIPayloads, toA2UIPayload };
package/dist/index.js ADDED
@@ -0,0 +1,350 @@
1
+ // src/index.ts
2
+ var blockIdCounter = 0;
3
+ function defaultConversationId(surfaceId) {
4
+ return `a2ui-conv-${surfaceId}`;
5
+ }
6
+ function defaultBlockId(componentId) {
7
+ return `a2ui-block-${componentId}-${++blockIdCounter}`;
8
+ }
9
+ function resolveDynamic(value, resolver) {
10
+ if (value === void 0 || value === null) return "";
11
+ if (typeof value === "string") return value;
12
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
13
+ if (resolver) return resolver(value);
14
+ if ("path" in value) return `{{${value.path}}}`;
15
+ if ("call" in value) return `{{${value.call}(...)}}`;
16
+ return JSON.stringify(value);
17
+ }
18
+ function buildComponentMap(components) {
19
+ const map = /* @__PURE__ */ new Map();
20
+ for (const comp of components) {
21
+ map.set(comp.id, comp);
22
+ }
23
+ return map;
24
+ }
25
+ function findRoot(components) {
26
+ return components.find((c) => c.id === "root");
27
+ }
28
+ function getChildIds(component) {
29
+ if (component.child) return [component.child];
30
+ if (Array.isArray(component.children)) return component.children;
31
+ return [];
32
+ }
33
+ function componentToBlock(component, options = {}) {
34
+ const genBlockId = options.generateBlockId ?? defaultBlockId;
35
+ const blockId = genBlockId(component.id);
36
+ const resolve = (v) => resolveDynamic(v, options.resolveValue);
37
+ switch (component.component) {
38
+ case "Text":
39
+ return {
40
+ id: blockId,
41
+ type: "text",
42
+ status: "completed",
43
+ content: {
44
+ text: resolve(component.text)
45
+ }
46
+ };
47
+ case "Image":
48
+ return {
49
+ id: blockId,
50
+ type: "image",
51
+ status: "completed",
52
+ content: {
53
+ url: resolve(component.url),
54
+ alt: component.id
55
+ }
56
+ };
57
+ case "Button": {
58
+ const label = resolve(component.text) || component.id;
59
+ const actionDesc = component.action?.event ? `action:${component.action.event.name}` : component.action?.functionCall ? `call:${component.action.functionCall.name}` : "";
60
+ return {
61
+ id: blockId,
62
+ type: "text",
63
+ status: "completed",
64
+ content: {
65
+ text: `[Button: ${label}]${actionDesc ? ` (${actionDesc})` : ""}`
66
+ }
67
+ };
68
+ }
69
+ case "TextField": {
70
+ const label = resolve(component.label) || component.id;
71
+ const val = resolve(component.value);
72
+ return {
73
+ id: blockId,
74
+ type: "text",
75
+ status: "completed",
76
+ content: {
77
+ text: `[TextField: ${label}]${val ? ` value="${val}"` : ""}`
78
+ }
79
+ };
80
+ }
81
+ case "CheckBox": {
82
+ const label = resolve(component.label) || component.id;
83
+ return {
84
+ id: blockId,
85
+ type: "text",
86
+ status: "completed",
87
+ content: {
88
+ text: `[CheckBox: ${label}]`
89
+ }
90
+ };
91
+ }
92
+ case "DateTimeInput": {
93
+ const label = resolve(component.label) || component.id;
94
+ return {
95
+ id: blockId,
96
+ type: "text",
97
+ status: "completed",
98
+ content: {
99
+ text: `[DateTimeInput: ${label}]`
100
+ }
101
+ };
102
+ }
103
+ case "ChoicePicker": {
104
+ const label = resolve(component.label) || component.id;
105
+ const optionLabels = component.options ? component.options.map((o) => resolve(o.label)).join(", ") : "";
106
+ return {
107
+ id: blockId,
108
+ type: "text",
109
+ status: "completed",
110
+ content: {
111
+ text: `[ChoicePicker: ${label}]${optionLabels ? ` options=[${optionLabels}]` : ""}`
112
+ }
113
+ };
114
+ }
115
+ case "Slider": {
116
+ const label = resolve(component.label) || component.id;
117
+ const min = component.min ?? 0;
118
+ const max = component.max ?? 100;
119
+ return {
120
+ id: blockId,
121
+ type: "text",
122
+ status: "completed",
123
+ content: {
124
+ text: `[Slider: ${label}] range=[${min}, ${max}]`
125
+ }
126
+ };
127
+ }
128
+ case "Video":
129
+ return {
130
+ id: blockId,
131
+ type: "text",
132
+ status: "completed",
133
+ content: {
134
+ text: `[Video: ${resolve(component.url)}]`
135
+ }
136
+ };
137
+ case "AudioPlayer":
138
+ return {
139
+ id: blockId,
140
+ type: "text",
141
+ status: "completed",
142
+ content: {
143
+ text: `[Audio: ${resolve(component.url)}]`
144
+ }
145
+ };
146
+ case "Icon":
147
+ return {
148
+ id: blockId,
149
+ type: "text",
150
+ status: "completed",
151
+ content: {
152
+ text: `[Icon: ${resolve(component.name)}]`
153
+ }
154
+ };
155
+ case "Divider":
156
+ return {
157
+ id: blockId,
158
+ type: "text",
159
+ status: "completed",
160
+ content: {
161
+ text: "---"
162
+ }
163
+ };
164
+ // Container components (Row, Column, Card, List, Tabs, Modal)
165
+ // are flattened - their children are emitted as separate blocks.
166
+ // A container itself does not produce a block.
167
+ // This default handles any unknown/custom component types.
168
+ default:
169
+ return {
170
+ id: blockId,
171
+ type: "text",
172
+ status: "completed",
173
+ content: {
174
+ text: `[${component.component}: ${component.id}]`
175
+ }
176
+ };
177
+ }
178
+ }
179
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["Row", "Column", "Card", "List", "Tabs", "Modal"]);
180
+ function flattenComponents(rootId, componentMap, options = {}, visited = /* @__PURE__ */ new Set()) {
181
+ if (visited.has(rootId)) return [];
182
+ visited.add(rootId);
183
+ const component = componentMap.get(rootId);
184
+ if (!component) return [];
185
+ const isContainer = CONTAINER_TYPES.has(component.component);
186
+ const childIds = getChildIds(component);
187
+ if (component.component === "Tabs" && component.tabs) {
188
+ const blocks2 = [];
189
+ for (const tab of component.tabs) {
190
+ const title = resolveDynamic(tab.title, options.resolveValue);
191
+ const genBlockId = options.generateBlockId ?? defaultBlockId;
192
+ blocks2.push({
193
+ id: genBlockId(`${component.id}-tab-${tab.child}`),
194
+ type: "text",
195
+ status: "completed",
196
+ content: { text: `**${title}**` }
197
+ });
198
+ blocks2.push(...flattenComponents(tab.child, componentMap, options, visited));
199
+ }
200
+ return blocks2;
201
+ }
202
+ if (isContainer) {
203
+ const blocks2 = [];
204
+ for (const childId of childIds) {
205
+ blocks2.push(...flattenComponents(childId, componentMap, options, visited));
206
+ }
207
+ return blocks2;
208
+ }
209
+ const blocks = [componentToBlock(component, options)];
210
+ for (const childId of childIds) {
211
+ blocks.push(...flattenComponents(childId, componentMap, options, visited));
212
+ }
213
+ return blocks;
214
+ }
215
+ function fromA2UIPayload(payload, options = {}) {
216
+ const update = payload.updateComponents;
217
+ if (!update) return null;
218
+ const { surfaceId, components } = update;
219
+ const genConvId = options.generateConversationId ?? defaultConversationId;
220
+ const componentMap = buildComponentMap(components);
221
+ const root = findRoot(components);
222
+ let blocks;
223
+ if (root) {
224
+ blocks = flattenComponents(root.id, componentMap, options);
225
+ } else {
226
+ blocks = components.filter((c) => !CONTAINER_TYPES.has(c.component)).map((c) => componentToBlock(c, options));
227
+ }
228
+ return {
229
+ id: genConvId(surfaceId),
230
+ role: "assistant",
231
+ status: "completed",
232
+ blocks,
233
+ timestamp: Date.now()
234
+ };
235
+ }
236
+ function fromA2UIPayloads(payloads, options = {}) {
237
+ const results = [];
238
+ for (const payload of payloads) {
239
+ const conv = fromA2UIPayload(payload, options);
240
+ if (conv) results.push(conv);
241
+ }
242
+ return results;
243
+ }
244
+ function toA2UIPayload(conversation, surfaceId, version = "v0.10") {
245
+ const sid = surfaceId ?? conversation.id;
246
+ const components = [];
247
+ const childIds = [];
248
+ for (const block of conversation.blocks) {
249
+ const compId = `comp-${block.id}`;
250
+ childIds.push(compId);
251
+ switch (block.type) {
252
+ case "text": {
253
+ const content = block.content;
254
+ components.push({
255
+ id: compId,
256
+ component: "Text",
257
+ text: content.text
258
+ });
259
+ break;
260
+ }
261
+ case "image": {
262
+ const content = block.content;
263
+ components.push({
264
+ id: compId,
265
+ component: "Image",
266
+ url: content.url
267
+ });
268
+ break;
269
+ }
270
+ case "thinking": {
271
+ const content = block.content;
272
+ components.push({
273
+ id: compId,
274
+ component: "Text",
275
+ text: `*Thinking: ${content.reasoning}*`
276
+ });
277
+ break;
278
+ }
279
+ case "tool": {
280
+ const content = block.content;
281
+ const parts = [`**Tool: ${content.name}**`];
282
+ if (content.input) parts.push(`Input: \`${JSON.stringify(content.input)}\``);
283
+ if (content.output) parts.push(`Output: \`${JSON.stringify(content.output)}\``);
284
+ parts.push(`Status: ${content.status}`);
285
+ components.push({
286
+ id: compId,
287
+ component: "Text",
288
+ text: parts.join("\n\n")
289
+ });
290
+ break;
291
+ }
292
+ case "error": {
293
+ const content = block.content;
294
+ components.push({
295
+ id: compId,
296
+ component: "Text",
297
+ text: `**Error [${content.code}]:** ${content.message}`
298
+ });
299
+ break;
300
+ }
301
+ case "file": {
302
+ const content = block.content;
303
+ components.push({
304
+ id: compId,
305
+ component: "Text",
306
+ text: `[File: ${content.name}](${content.url})`
307
+ });
308
+ break;
309
+ }
310
+ case "source": {
311
+ const content = block.content;
312
+ const text = content.url ? `[Source: ${content.title}](${content.url})` : `Source: ${content.title}`;
313
+ components.push({
314
+ id: compId,
315
+ component: "Text",
316
+ text: content.excerpt ? `${text}
317
+
318
+ > ${content.excerpt}` : text
319
+ });
320
+ break;
321
+ }
322
+ default: {
323
+ components.push({
324
+ id: compId,
325
+ component: "Text",
326
+ text: `[${block.type}: ${block.id}]`
327
+ });
328
+ break;
329
+ }
330
+ }
331
+ }
332
+ components.unshift({
333
+ id: "root",
334
+ component: "Column",
335
+ children: childIds
336
+ });
337
+ return {
338
+ version,
339
+ updateComponents: {
340
+ surfaceId: sid,
341
+ components
342
+ }
343
+ };
344
+ }
345
+ export {
346
+ fromA2UIPayload,
347
+ fromA2UIPayloads,
348
+ toA2UIPayload
349
+ };
350
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @uix-ai/adapter-a2ui\n *\n * Adapter to convert Google A2UI protocol payloads to UIX Lucid IR format.\n *\n * A2UI is a declarative UI protocol where agents generate JSON payloads\n * describing UI components. Unlike AG-UI (event streaming), A2UI is\n * snapshot-based - the agent sends a complete UI description as JSON.\n *\n * This adapter converts A2UI's component tree into UIX LucidConversation\n * and LucidBlock structures for rendering with UIX components.\n *\n * @see https://github.com/google/A2UI\n *\n * @example\n * ```typescript\n * import { fromA2UIPayload, toA2UIPayload } from '@uix-ai/adapter-a2ui'\n *\n * // Convert A2UI payload to UIX Lucid IR\n * const conversation = fromA2UIPayload(a2uiPayload)\n *\n * // Convert UIX Lucid IR back to A2UI payload\n * const payload = toA2UIPayload(conversation)\n * ```\n */\n\nimport type {\n LucidConversation,\n LucidBlock,\n ContentStatus,\n BlockType,\n TextBlockContent,\n ImageBlockContent,\n ErrorBlockContent,\n} from '@uix-ai/core'\n\n// ============================================================================\n// A2UI Protocol Types\n// ============================================================================\n\n/**\n * A2UI protocol version\n */\nexport type A2UIVersion = 'v0.8' | 'v0.9' | 'v0.10'\n\n/**\n * A2UI display component types\n */\nexport type A2UIDisplayComponent = 'Text' | 'Image' | 'Icon' | 'Video' | 'AudioPlayer'\n\n/**\n * A2UI container component types\n */\nexport type A2UIContainerComponent = 'Row' | 'Column' | 'Card' | 'List' | 'Tabs' | 'Modal'\n\n/**\n * A2UI input component types\n */\nexport type A2UIInputComponent = 'TextField' | 'CheckBox' | 'DateTimeInput' | 'ChoicePicker' | 'Slider'\n\n/**\n * A2UI interactive component types\n */\nexport type A2UIInteractiveComponent = 'Button' | 'Divider'\n\n/**\n * Union of all A2UI component type names\n */\nexport type A2UIComponentType =\n | A2UIDisplayComponent\n | A2UIContainerComponent\n | A2UIInputComponent\n | A2UIInteractiveComponent\n\n// ============================================================================\n// A2UI Dynamic Value Types\n// ============================================================================\n\n/**\n * A2UI dynamic string - either a literal or a data-binding reference\n */\nexport type A2UIDynamicString = string | { path: string } | { call: string; args: Record<string, unknown> }\n\n/**\n * A2UI dynamic number\n */\nexport type A2UIDynamicNumber = number | { path: string } | { call: string; args: Record<string, unknown> }\n\n/**\n * A2UI dynamic boolean\n */\nexport type A2UIDynamicBoolean = boolean | { path: string } | { call: string; args: Record<string, unknown> }\n\n/**\n * A2UI child list - either an array of IDs or a template binding\n */\nexport type A2UIChildList = string[] | { path: string; componentId: string }\n\n// ============================================================================\n// A2UI Validation & Actions\n// ============================================================================\n\n/**\n * A2UI validation check\n */\nexport interface A2UICheck {\n call: string\n args: Record<string, unknown>\n message: string\n}\n\n/**\n * A2UI event action\n */\nexport interface A2UIEventAction {\n name: string\n context?: Record<string, unknown>\n}\n\n/**\n * A2UI function call action\n */\nexport interface A2UIFunctionCallAction {\n name: string\n args?: Record<string, unknown>\n}\n\n/**\n * A2UI action - either an event or a function call\n */\nexport interface A2UIAction {\n event?: A2UIEventAction\n functionCall?: A2UIFunctionCallAction\n}\n\n// ============================================================================\n// A2UI Component Definition\n// ============================================================================\n\n/**\n * A2UI component object in the flat adjacency list\n *\n * Components are stored as a flat array and reference each other by ID.\n * The tree structure is built via `children` and `child` properties.\n */\nexport interface A2UIComponent {\n /** Unique identifier for this component */\n id: string\n /** Component type name */\n component: A2UIComponentType | string\n /** Child component IDs (for container components like Row, Column, List) */\n children?: A2UIChildList\n /** Single child component ID (for Card, Modal) */\n child?: string\n /** Text content (for Text, Button) */\n text?: A2UIDynamicString\n /** Label (for input components) */\n label?: A2UIDynamicString\n /** URL (for Image, Video, AudioPlayer) */\n url?: A2UIDynamicString\n /** Icon name (for Icon) */\n name?: A2UIDynamicString\n /** Value binding (for input components) */\n value?: A2UIDynamicString | A2UIDynamicNumber | A2UIDynamicBoolean\n /** Action on interaction (for Button) */\n action?: A2UIAction\n /** Validation checks (for TextField, Button) */\n checks?: A2UICheck[]\n /** Button variant */\n variant?: string\n /** Layout alignment */\n justify?: string\n /** Layout cross-axis alignment */\n align?: string\n /** Axis for Divider */\n axis?: string\n /** Tabs configuration */\n tabs?: Array<{ title: A2UIDynamicString; child: string }>\n /** Slider min value */\n min?: number\n /** Slider max value */\n max?: number\n /** Choice picker options */\n options?: Array<{ label: A2UIDynamicString; value: string }>\n /** Any additional properties from custom or extended components */\n [key: string]: unknown\n}\n\n// ============================================================================\n// A2UI Theme\n// ============================================================================\n\n/**\n * A2UI surface theme configuration\n */\nexport interface A2UITheme {\n primaryColor?: string\n [key: string]: unknown\n}\n\n// ============================================================================\n// A2UI Message Types\n// ============================================================================\n\n/**\n * createSurface message - initializes a new UI surface\n */\nexport interface A2UICreateSurface {\n surfaceId: string\n catalogId?: string\n theme?: A2UITheme\n sendDataModel?: boolean\n}\n\n/**\n * updateComponents message - adds or updates UI components\n */\nexport interface A2UIUpdateComponents {\n surfaceId: string\n components: A2UIComponent[]\n}\n\n/**\n * updateDataModel message - modifies data model via JSON pointer\n */\nexport interface A2UIUpdateDataModel {\n surfaceId: string\n path?: string\n value?: unknown\n}\n\n/**\n * deleteSurface message - removes a surface\n */\nexport interface A2UIDeleteSurface {\n surfaceId: string\n}\n\n// ============================================================================\n// A2UI Payload (Top-Level Message)\n// ============================================================================\n\n/**\n * Top-level A2UI message payload.\n *\n * Each message contains exactly one of the four message types:\n * - `createSurface`: Initialize a new UI surface\n * - `updateComponents`: Add or update components on a surface\n * - `updateDataModel`: Modify the surface data model\n * - `deleteSurface`: Remove a surface\n */\nexport interface A2UIPayload {\n /** A2UI protocol version */\n version: string\n /** Create a new surface */\n createSurface?: A2UICreateSurface\n /** Update components on a surface */\n updateComponents?: A2UIUpdateComponents\n /** Update the data model */\n updateDataModel?: A2UIUpdateDataModel\n /** Delete a surface */\n deleteSurface?: A2UIDeleteSurface\n}\n\n// ============================================================================\n// Conversion Options\n// ============================================================================\n\n/**\n * Options for A2UI to Lucid IR conversion\n */\nexport interface A2UIConversionOptions {\n /**\n * Custom ID generator for conversations\n * @default () => `a2ui-conv-${surfaceId}`\n */\n generateConversationId?: (surfaceId: string) => string\n\n /**\n * Custom ID generator for blocks\n * @default () => `a2ui-block-${componentId}`\n */\n generateBlockId?: (componentId: string) => string\n\n /**\n * Resolve dynamic string values against a data model.\n * If not provided, dynamic bindings are serialized as-is.\n */\n resolveValue?: (dynamic: A2UIDynamicString | A2UIDynamicNumber | A2UIDynamicBoolean) => string\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\nlet blockIdCounter = 0\n\nfunction defaultConversationId(surfaceId: string): string {\n return `a2ui-conv-${surfaceId}`\n}\n\nfunction defaultBlockId(componentId: string): string {\n return `a2ui-block-${componentId}-${++blockIdCounter}`\n}\n\n/**\n * Resolve a dynamic value to a plain string.\n * Literal values are returned directly; bindings are serialized for display.\n */\nfunction resolveDynamic(\n value: A2UIDynamicString | A2UIDynamicNumber | A2UIDynamicBoolean | undefined,\n resolver?: (v: A2UIDynamicString | A2UIDynamicNumber | A2UIDynamicBoolean) => string\n): string {\n if (value === undefined || value === null) return ''\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (resolver) return resolver(value)\n // Fallback: serialize binding as a readable placeholder\n if ('path' in value) return `{{${value.path}}}`\n if ('call' in value) return `{{${value.call}(...)}}`\n return JSON.stringify(value)\n}\n\n/**\n * Build a lookup map from component array\n */\nfunction buildComponentMap(components: A2UIComponent[]): Map<string, A2UIComponent> {\n const map = new Map<string, A2UIComponent>()\n for (const comp of components) {\n map.set(comp.id, comp)\n }\n return map\n}\n\n/**\n * Find the root component (id === 'root') in the component list\n */\nfunction findRoot(components: A2UIComponent[]): A2UIComponent | undefined {\n return components.find((c) => c.id === 'root')\n}\n\n/**\n * Get direct children IDs from a component\n */\nfunction getChildIds(component: A2UIComponent): string[] {\n if (component.child) return [component.child]\n if (Array.isArray(component.children)) return component.children\n // Template children (data-bound) cannot be resolved without a data model\n return []\n}\n\n// ============================================================================\n// A2UI Component to LucidBlock Mapping\n// ============================================================================\n\n/**\n * Map an A2UI component type to a UIX BlockType.\n *\n * Direct mappings:\n * - Text -> 'text'\n * - Image -> 'image'\n *\n * Components without a direct mapping are represented as 'text' blocks\n * with a structured description of the component.\n */\nfunction mapComponentType(componentType: string): BlockType {\n switch (componentType) {\n case 'Text':\n return 'text'\n case 'Image':\n return 'image'\n default:\n return 'text'\n }\n}\n\n/**\n * Convert a single A2UI component to a LucidBlock.\n *\n * For components that map directly to UIX block types (Text, Image),\n * the content is converted to the matching block content type.\n *\n * For other components (Button, TextField, Card, etc.), a text block\n * is produced with a structured markdown representation.\n */\nfunction componentToBlock(\n component: A2UIComponent,\n options: A2UIConversionOptions = {}\n): LucidBlock {\n const genBlockId = options.generateBlockId ?? defaultBlockId\n const blockId = genBlockId(component.id)\n const resolve = (v: A2UIDynamicString | A2UIDynamicNumber | A2UIDynamicBoolean | undefined) =>\n resolveDynamic(v, options.resolveValue)\n\n switch (component.component) {\n case 'Text':\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: resolve(component.text),\n } as TextBlockContent,\n }\n\n case 'Image':\n return {\n id: blockId,\n type: 'image',\n status: 'completed' as ContentStatus,\n content: {\n url: resolve(component.url),\n alt: component.id,\n } as ImageBlockContent,\n }\n\n case 'Button': {\n const label = resolve(component.text) || component.id\n const actionDesc = component.action?.event\n ? `action:${component.action.event.name}`\n : component.action?.functionCall\n ? `call:${component.action.functionCall.name}`\n : ''\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[Button: ${label}]${actionDesc ? ` (${actionDesc})` : ''}`,\n } as TextBlockContent,\n }\n }\n\n case 'TextField': {\n const label = resolve(component.label) || component.id\n const val = resolve(component.value)\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[TextField: ${label}]${val ? ` value=\"${val}\"` : ''}`,\n } as TextBlockContent,\n }\n }\n\n case 'CheckBox': {\n const label = resolve(component.label) || component.id\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[CheckBox: ${label}]`,\n } as TextBlockContent,\n }\n }\n\n case 'DateTimeInput': {\n const label = resolve(component.label) || component.id\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[DateTimeInput: ${label}]`,\n } as TextBlockContent,\n }\n }\n\n case 'ChoicePicker': {\n const label = resolve(component.label) || component.id\n const optionLabels = component.options\n ? component.options.map((o) => resolve(o.label)).join(', ')\n : ''\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[ChoicePicker: ${label}]${optionLabels ? ` options=[${optionLabels}]` : ''}`,\n } as TextBlockContent,\n }\n }\n\n case 'Slider': {\n const label = resolve(component.label) || component.id\n const min = component.min ?? 0\n const max = component.max ?? 100\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[Slider: ${label}] range=[${min}, ${max}]`,\n } as TextBlockContent,\n }\n }\n\n case 'Video':\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[Video: ${resolve(component.url)}]`,\n } as TextBlockContent,\n }\n\n case 'AudioPlayer':\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[Audio: ${resolve(component.url)}]`,\n } as TextBlockContent,\n }\n\n case 'Icon':\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[Icon: ${resolve(component.name)}]`,\n } as TextBlockContent,\n }\n\n case 'Divider':\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: '---',\n } as TextBlockContent,\n }\n\n // Container components (Row, Column, Card, List, Tabs, Modal)\n // are flattened - their children are emitted as separate blocks.\n // A container itself does not produce a block.\n // This default handles any unknown/custom component types.\n default:\n return {\n id: blockId,\n type: 'text',\n status: 'completed' as ContentStatus,\n content: {\n text: `[${component.component}: ${component.id}]`,\n } as TextBlockContent,\n }\n }\n}\n\n/**\n * Container component types whose children should be traversed\n * but which do not produce blocks themselves.\n */\nconst CONTAINER_TYPES = new Set(['Row', 'Column', 'Card', 'List', 'Tabs', 'Modal'])\n\n/**\n * Recursively flatten the A2UI component tree into LucidBlocks.\n *\n * Container components are traversed but not emitted as blocks.\n * Leaf/display/input/interactive components are converted to blocks.\n */\nfunction flattenComponents(\n rootId: string,\n componentMap: Map<string, A2UIComponent>,\n options: A2UIConversionOptions = {},\n visited: Set<string> = new Set()\n): LucidBlock[] {\n if (visited.has(rootId)) return []\n visited.add(rootId)\n\n const component = componentMap.get(rootId)\n if (!component) return []\n\n const isContainer = CONTAINER_TYPES.has(component.component)\n const childIds = getChildIds(component)\n\n // For Tabs, children are defined differently\n if (component.component === 'Tabs' && component.tabs) {\n const blocks: LucidBlock[] = []\n for (const tab of component.tabs) {\n const title = resolveDynamic(tab.title, options.resolveValue)\n const genBlockId = options.generateBlockId ?? defaultBlockId\n blocks.push({\n id: genBlockId(`${component.id}-tab-${tab.child}`),\n type: 'text',\n status: 'completed' as ContentStatus,\n content: { text: `**${title}**` } as TextBlockContent,\n })\n blocks.push(...flattenComponents(tab.child, componentMap, options, visited))\n }\n return blocks\n }\n\n if (isContainer) {\n // Recurse into children, skip the container itself\n const blocks: LucidBlock[] = []\n for (const childId of childIds) {\n blocks.push(...flattenComponents(childId, componentMap, options, visited))\n }\n return blocks\n }\n\n // Leaf component - convert to block, then also traverse any children\n const blocks: LucidBlock[] = [componentToBlock(component, options)]\n for (const childId of childIds) {\n blocks.push(...flattenComponents(childId, componentMap, options, visited))\n }\n return blocks\n}\n\n// ============================================================================\n// Public Conversion Functions\n// ============================================================================\n\n/**\n * Convert an A2UI payload (containing updateComponents) to a UIX LucidConversation.\n *\n * The A2UI component tree is flattened into a linear sequence of LucidBlocks:\n * - `Text` components become text blocks\n * - `Image` components become image blocks\n * - Input and interactive components become text blocks with structured descriptions\n * - Container components (Row, Column, Card, etc.) are traversed but not emitted\n *\n * @param payload - An A2UI payload message (must contain updateComponents)\n * @param options - Conversion options\n * @returns A LucidConversation representing the A2UI surface, or null if the payload\n * does not contain updateComponents\n *\n * @example\n * ```typescript\n * const payload: A2UIPayload = {\n * version: 'v0.10',\n * updateComponents: {\n * surfaceId: 'form_1',\n * components: [\n * { id: 'root', component: 'Column', children: ['title', 'img'] },\n * { id: 'title', component: 'Text', text: 'Hello World' },\n * { id: 'img', component: 'Image', url: 'https://example.com/photo.png' }\n * ]\n * }\n * }\n *\n * const conversation = fromA2UIPayload(payload)\n * // conversation.blocks[0] -> text block \"Hello World\"\n * // conversation.blocks[1] -> image block\n * ```\n */\nexport function fromA2UIPayload(\n payload: A2UIPayload,\n options: A2UIConversionOptions = {}\n): LucidConversation | null {\n const update = payload.updateComponents\n if (!update) return null\n\n const { surfaceId, components } = update\n const genConvId = options.generateConversationId ?? defaultConversationId\n\n const componentMap = buildComponentMap(components)\n const root = findRoot(components)\n\n let blocks: LucidBlock[]\n if (root) {\n blocks = flattenComponents(root.id, componentMap, options)\n } else {\n // No root found - convert all components linearly\n blocks = components\n .filter((c) => !CONTAINER_TYPES.has(c.component))\n .map((c) => componentToBlock(c, options))\n }\n\n return {\n id: genConvId(surfaceId),\n role: 'assistant',\n status: 'completed',\n blocks,\n timestamp: Date.now(),\n }\n}\n\n/**\n * Convert multiple A2UI payloads (a stream of messages) to LucidConversations.\n *\n * Processes an array of A2UI messages and returns one LucidConversation per\n * updateComponents message encountered.\n *\n * @param payloads - Array of A2UI payload messages\n * @param options - Conversion options\n * @returns Array of LucidConversations\n *\n * @example\n * ```typescript\n * const messages: A2UIPayload[] = [\n * { version: 'v0.10', createSurface: { surfaceId: 's1', catalogId: '...' } },\n * { version: 'v0.10', updateComponents: { surfaceId: 's1', components: [...] } },\n * ]\n *\n * const conversations = fromA2UIPayloads(messages)\n * ```\n */\nexport function fromA2UIPayloads(\n payloads: A2UIPayload[],\n options: A2UIConversionOptions = {}\n): LucidConversation[] {\n const results: LucidConversation[] = []\n for (const payload of payloads) {\n const conv = fromA2UIPayload(payload, options)\n if (conv) results.push(conv)\n }\n return results\n}\n\n/**\n * Convert a UIX LucidConversation back to an A2UI payload.\n *\n * Maps LucidBlocks back to A2UI components wrapped in a Column layout:\n * - text blocks -> Text components\n * - image blocks -> Image components\n * - Other block types -> Text components with type annotation\n *\n * @param conversation - A UIX LucidConversation\n * @param surfaceId - Optional surface ID (defaults to conversation.id)\n * @param version - A2UI protocol version (defaults to 'v0.10')\n * @returns An A2UI payload with updateComponents\n *\n * @example\n * ```typescript\n * const conversation: LucidConversation = {\n * id: 'conv-1',\n * role: 'assistant',\n * status: 'completed',\n * blocks: [\n * { id: 'b1', type: 'text', status: 'completed', content: { text: 'Hello' } },\n * { id: 'b2', type: 'image', status: 'completed', content: { url: 'https://...' } }\n * ],\n * timestamp: Date.now()\n * }\n *\n * const payload = toA2UIPayload(conversation)\n * ```\n */\nexport function toA2UIPayload(\n conversation: LucidConversation,\n surfaceId?: string,\n version: string = 'v0.10'\n): A2UIPayload {\n const sid = surfaceId ?? conversation.id\n const components: A2UIComponent[] = []\n const childIds: string[] = []\n\n for (const block of conversation.blocks) {\n const compId = `comp-${block.id}`\n childIds.push(compId)\n\n switch (block.type) {\n case 'text': {\n const content = block.content as TextBlockContent\n components.push({\n id: compId,\n component: 'Text',\n text: content.text,\n })\n break\n }\n\n case 'image': {\n const content = block.content as ImageBlockContent\n components.push({\n id: compId,\n component: 'Image',\n url: content.url,\n })\n break\n }\n\n case 'thinking': {\n const content = block.content as { reasoning: string }\n components.push({\n id: compId,\n component: 'Text',\n text: `*Thinking: ${content.reasoning}*`,\n })\n break\n }\n\n case 'tool': {\n const content = block.content as { name: string; input: unknown; output?: unknown; status: string }\n const parts = [`**Tool: ${content.name}**`]\n if (content.input) parts.push(`Input: \\`${JSON.stringify(content.input)}\\``)\n if (content.output) parts.push(`Output: \\`${JSON.stringify(content.output)}\\``)\n parts.push(`Status: ${content.status}`)\n components.push({\n id: compId,\n component: 'Text',\n text: parts.join('\\n\\n'),\n })\n break\n }\n\n case 'error': {\n const content = block.content as ErrorBlockContent\n components.push({\n id: compId,\n component: 'Text',\n text: `**Error [${content.code}]:** ${content.message}`,\n })\n break\n }\n\n case 'file': {\n const content = block.content as { name: string; url: string; type: string }\n components.push({\n id: compId,\n component: 'Text',\n text: `[File: ${content.name}](${content.url})`,\n })\n break\n }\n\n case 'source': {\n const content = block.content as { title: string; url?: string; excerpt?: string }\n const text = content.url\n ? `[Source: ${content.title}](${content.url})`\n : `Source: ${content.title}`\n components.push({\n id: compId,\n component: 'Text',\n text: content.excerpt ? `${text}\\n\\n> ${content.excerpt}` : text,\n })\n break\n }\n\n default: {\n components.push({\n id: compId,\n component: 'Text',\n text: `[${block.type}: ${block.id}]`,\n })\n break\n }\n }\n }\n\n // Wrap all components in a root Column\n components.unshift({\n id: 'root',\n component: 'Column',\n children: childIds,\n })\n\n return {\n version,\n updateComponents: {\n surfaceId: sid,\n components,\n },\n }\n}\n"],"mappings":"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","names":["blocks"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@uix-ai/adapter-a2ui",
3
+ "version": "0.0.1",
4
+ "description": "Adapter to convert Google A2UI protocol payloads to UIX Lucid IR format",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "test": "vitest run",
22
+ "clean": "rm -rf dist"
23
+ },
24
+ "dependencies": {
25
+ "@uix-ai/core": "workspace:*"
26
+ },
27
+ "devDependencies": {
28
+ "tsup": "^8.0.0",
29
+ "typescript": "^5.3.0",
30
+ "vitest": "^3.2.4"
31
+ },
32
+ "keywords": [
33
+ "uix",
34
+ "a2ui",
35
+ "agent-ui",
36
+ "adapter",
37
+ "protocol",
38
+ "converter",
39
+ "google"
40
+ ],
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/Deepractice/UIX.git",
45
+ "directory": "packages/adapter-a2ui"
46
+ }
47
+ }