@adia-ai/a2ui 0.8.37

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +1073 -0
  2. package/README.md +99 -0
  3. package/a2ui.schema.d.ts +192 -0
  4. package/controllers/accordion.js +73 -0
  5. package/controllers/base.js +68 -0
  6. package/controllers/data-stream.js +281 -0
  7. package/controllers/form.js +81 -0
  8. package/controllers/index.js +6 -0
  9. package/controllers/selection.js +82 -0
  10. package/controllers/state-machine.js +135 -0
  11. package/controllers/toggle.js +40 -0
  12. package/dockables/action.d.ts +55 -0
  13. package/dockables/action.js +152 -0
  14. package/dockables/base.d.ts +26 -0
  15. package/dockables/base.js +30 -0
  16. package/dockables/controller.d.ts +35 -0
  17. package/dockables/controller.js +97 -0
  18. package/dockables/data-source.d.ts +35 -0
  19. package/dockables/data-source.js +103 -0
  20. package/dockables/index.d.ts +21 -0
  21. package/dockables/index.js +6 -0
  22. package/dockables/lifecycle.d.ts +38 -0
  23. package/dockables/lifecycle.js +84 -0
  24. package/dockables/provider.d.ts +28 -0
  25. package/dockables/provider.js +59 -0
  26. package/index.d.ts +64 -0
  27. package/index.js +54 -0
  28. package/package.json +89 -0
  29. package/prop-apply.d.ts +13 -0
  30. package/prop-apply.js +113 -0
  31. package/registry.d.ts +17 -0
  32. package/registry.js +418 -0
  33. package/renderer.d.ts +67 -0
  34. package/renderer.js +715 -0
  35. package/stream.d.ts +62 -0
  36. package/stream.js +521 -0
  37. package/surface-manifest.d.ts +73 -0
  38. package/surface-manifest.js +294 -0
  39. package/surface.d.ts +72 -0
  40. package/surface.js +222 -0
  41. package/types.d.ts +26 -0
  42. package/validate/CHANGELOG.md +1005 -0
  43. package/validate/README.md +146 -0
  44. package/validate/index.d.ts +4 -0
  45. package/validate/index.js +12 -0
  46. package/validate/validator.d.ts +4 -0
  47. package/validate/validator.js +1232 -0
  48. package/wire-factory.d.ts +15 -0
  49. package/wire-factory.js +134 -0
  50. package/wiring-engine.d.ts +61 -0
  51. package/wiring-engine.js +209 -0
  52. package/wiring-registry.d.ts +80 -0
  53. package/wiring-registry.js +342 -0
package/stream.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * A2UI Stream Adapters — connect to various transports and yield A2UI messages.
3
+ */
4
+
5
+ /** Options shared by stream adapters that accept a catalog and/or AbortSignal. */
6
+ export interface StreamOptions {
7
+ /** Component type catalog — limits types advertised to the server. */
8
+ catalog?: Map<string, unknown> | Record<string, unknown>;
9
+ /** AbortSignal to cancel the stream. */
10
+ signal?: AbortSignal;
11
+ }
12
+
13
+ /** Options for mcpStream — also accepts an action callback. */
14
+ export interface McpStreamOptions extends StreamOptions {
15
+ /** Called when the MCP server emits an a2ui/action RPC method. */
16
+ onAction?: (name: string, args: unknown) => Promise<unknown>;
17
+ }
18
+
19
+ /**
20
+ * SSE (Server-Sent Events) stream adapter.
21
+ * Returns an AsyncIterable that yields parsed A2UI messages.
22
+ */
23
+ export declare function sseStream(
24
+ url: string,
25
+ options?: StreamOptions,
26
+ ): AsyncIterable<unknown>;
27
+
28
+ /**
29
+ * WebSocket stream adapter.
30
+ * Returns an AsyncIterable that yields parsed A2UI messages.
31
+ */
32
+ export declare function wsStream(
33
+ url: string,
34
+ options?: StreamOptions,
35
+ ): AsyncIterable<unknown>;
36
+
37
+ /**
38
+ * Array / mock stream adapter — for testing.
39
+ * Yields each message in the array, optionally with a delay between items.
40
+ */
41
+ export declare function mockStream(
42
+ messages: readonly unknown[],
43
+ delay?: number,
44
+ ): AsyncIterable<unknown>;
45
+
46
+ /**
47
+ * MCP (Model Context Protocol) stream adapter over WebSocket.
48
+ * Yields A2UI messages extracted from MCP JSON-RPC tool results.
49
+ */
50
+ export declare function mcpStream(
51
+ url: string,
52
+ options?: McpStreamOptions,
53
+ ): AsyncIterable<unknown>;
54
+
55
+ /**
56
+ * JSONL (newline-delimited JSON) stream adapter via fetch.
57
+ * Yields each parsed line from the response body.
58
+ */
59
+ export declare function jsonlStream(
60
+ url: string,
61
+ options?: StreamOptions,
62
+ ): AsyncIterable<unknown>;
package/stream.js ADDED
@@ -0,0 +1,521 @@
1
+ /**
2
+ * A2UI Stream Adapters — connect to various transports and yield A2UI messages.
3
+ *
4
+ * Each adapter returns an AsyncIterable<A2UIMessage>.
5
+ *
6
+ * Usage:
7
+ * const stream = sseStream('/api/agent');
8
+ * for await (const message of stream) { renderer.process(message); }
9
+ */
10
+
11
+ /**
12
+ * SSE (Server-Sent Events) stream adapter.
13
+ */
14
+ export function sseStream(url, options = {}) {
15
+ let finalUrl = url;
16
+ if (options.catalog) {
17
+ const types = options.catalog instanceof Map ? [...options.catalog.keys()] : Object.keys(options.catalog);
18
+ const sep = url.includes('?') ? '&' : '?';
19
+ finalUrl = `${url}${sep}a2ui_catalog=${encodeURIComponent(types.join(','))}`;
20
+ }
21
+
22
+ return {
23
+ [Symbol.asyncIterator]() {
24
+ const eventSource = new EventSource(finalUrl);
25
+ const queue = [];
26
+ let resolve = null;
27
+ let done = false;
28
+
29
+ eventSource.onmessage = (e) => {
30
+ try {
31
+ const message = JSON.parse(e.data);
32
+ if (resolve) { const r = resolve; resolve = null; r({ value: message, done: false }); }
33
+ else queue.push(message);
34
+ } catch { console.warn('A2UI SSE: invalid JSON', e.data); }
35
+ };
36
+
37
+ eventSource.onerror = () => {
38
+ done = true;
39
+ eventSource.close();
40
+ if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); }
41
+ };
42
+
43
+ if (options.signal) {
44
+ options.signal.addEventListener('abort', () => {
45
+ done = true;
46
+ eventSource.close();
47
+ if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); }
48
+ });
49
+ }
50
+
51
+ return {
52
+ next() {
53
+ if (queue.length > 0) return Promise.resolve({ value: queue.shift(), done: false });
54
+ if (done) return Promise.resolve({ value: undefined, done: true });
55
+ return new Promise(r => { resolve = r; });
56
+ },
57
+ return() {
58
+ done = true;
59
+ eventSource.close();
60
+ return Promise.resolve({ value: undefined, done: true });
61
+ },
62
+ };
63
+ },
64
+ };
65
+ }
66
+
67
+ /**
68
+ * WebSocket stream adapter.
69
+ */
70
+ export function wsStream(url, options = {}) {
71
+ return {
72
+ [Symbol.asyncIterator]() {
73
+ const ws = new WebSocket(url);
74
+ const queue = [];
75
+ let resolve = null;
76
+ let done = false;
77
+
78
+ ws.onopen = () => {
79
+ if (options.catalog) {
80
+ const types = options.catalog instanceof Map ? [...options.catalog.keys()] : Object.keys(options.catalog);
81
+ ws.send(JSON.stringify({ type: 'a2ui:catalog', supportedTypes: types }));
82
+ }
83
+ };
84
+
85
+ ws.onmessage = (e) => {
86
+ try {
87
+ const message = JSON.parse(e.data);
88
+ if (resolve) { const r = resolve; resolve = null; r({ value: message, done: false }); }
89
+ else queue.push(message);
90
+ } catch { console.warn('A2UI WS: invalid JSON', e.data); }
91
+ };
92
+
93
+ ws.onclose = () => {
94
+ done = true;
95
+ if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); }
96
+ };
97
+
98
+ ws.onerror = () => { done = true; ws.close(); };
99
+
100
+ return {
101
+ next() {
102
+ if (queue.length > 0) return Promise.resolve({ value: queue.shift(), done: false });
103
+ if (done) return Promise.resolve({ value: undefined, done: true });
104
+ return new Promise(r => { resolve = r; });
105
+ },
106
+ return() { done = true; ws.close(); return Promise.resolve({ value: undefined, done: true }); },
107
+ };
108
+ },
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Array/mock stream adapter — for testing.
114
+ */
115
+ export function mockStream(messages, delay = 0) {
116
+ return {
117
+ async *[Symbol.asyncIterator]() {
118
+ for (const msg of messages) {
119
+ if (delay > 0) await new Promise(r => setTimeout(r, delay));
120
+ yield msg;
121
+ }
122
+ },
123
+ };
124
+ }
125
+
126
+ /**
127
+ * MCP stream adapter — connects to MCP server, yields A2UI messages.
128
+ *
129
+ * Dispatches by URL scheme:
130
+ * ws://, wss:// -> WebSocket transport (legacy / custom MCP-over-WS)
131
+ * http://, https:// -> MCP Streamable HTTP (spec 2025-11-25)
132
+ * relative path -> assume same-origin Streamable HTTP
133
+ */
134
+ export function mcpStream(url, options = {}) {
135
+ const scheme = (url.split(':')[0] || '').toLowerCase();
136
+ if (scheme === 'ws' || scheme === 'wss') {
137
+ return mcpStreamWebSocket(url, options);
138
+ }
139
+ if (scheme === 'http' || scheme === 'https') {
140
+ return mcpStreamHttp(url, options);
141
+ }
142
+ // Default: relative path -> same-origin HTTP
143
+ return mcpStreamHttp(url, options);
144
+ }
145
+
146
+ /**
147
+ * Helper: extract A2UI messages from a JSON-RPC `result.content` array
148
+ * and push them into a queue/resolver pair.
149
+ */
150
+ function _extractA2UIFromRpc(rpc, push) {
151
+ if (rpc.result?.content) {
152
+ for (const block of rpc.result.content) {
153
+ if (block.type === 'resource' && block.resource?.mimeType === 'application/json+a2ui') {
154
+ try {
155
+ const messages = JSON.parse(block.resource.text);
156
+ for (const msg of (Array.isArray(messages) ? messages : [messages])) push(msg);
157
+ } catch { /* malformed payload */ }
158
+ } else if (block.type === 'text') {
159
+ try {
160
+ const msg = JSON.parse(block.text);
161
+ if (msg.type && (msg.type.startsWith('create') || msg.type.startsWith('update') || msg.type.startsWith('delete'))) {
162
+ push(msg);
163
+ }
164
+ } catch { /* not A2UI */ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * MCP over WebSocket — original implementation, unchanged behavior.
172
+ */
173
+ function mcpStreamWebSocket(url, options) {
174
+ return {
175
+ async *[Symbol.asyncIterator]() {
176
+ const { signal, catalog, onAction } = options;
177
+
178
+ const ws = new WebSocket(url);
179
+ await new Promise((resolve, reject) => {
180
+ ws.onopen = resolve;
181
+ ws.onerror = reject;
182
+ if (signal) signal.addEventListener('abort', () => {
183
+ ws.close();
184
+ reject(new DOMException('Aborted', 'AbortError'));
185
+ }, { once: true });
186
+ });
187
+
188
+ if (catalog) {
189
+ const types = catalog instanceof Map ? [...catalog.keys()] : Object.keys(catalog);
190
+ ws.send(JSON.stringify({
191
+ jsonrpc: '2.0',
192
+ method: 'a2ui/catalog',
193
+ params: { supportedTypes: types },
194
+ }));
195
+ }
196
+
197
+ const queue = [];
198
+ let resolve = null;
199
+ let done = false;
200
+ const push = (msg) => {
201
+ if (resolve) { const r = resolve; resolve = null; r({ value: msg, done: false }); }
202
+ else queue.push(msg);
203
+ };
204
+
205
+ ws.onmessage = (e) => {
206
+ try {
207
+ const rpc = JSON.parse(e.data);
208
+ _extractA2UIFromRpc(rpc, push);
209
+
210
+ if (rpc.method === 'a2ui/action' && onAction) {
211
+ onAction(rpc.params?.name, rpc.params?.arguments).then(result => {
212
+ ws.send(JSON.stringify({ jsonrpc: '2.0', id: rpc.id, result }));
213
+ }).catch(err => {
214
+ ws.send(JSON.stringify({ jsonrpc: '2.0', id: rpc.id, error: { code: -1, message: err.message } }));
215
+ });
216
+ }
217
+ } catch { console.warn('A2UI MCP: invalid message', e.data); }
218
+ };
219
+
220
+ ws.onclose = () => { done = true; if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); } };
221
+ ws.onerror = () => { done = true; ws.close(); };
222
+ if (signal) signal.addEventListener('abort', () => { done = true; ws.close(); if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); } });
223
+
224
+ while (!done || queue.length > 0) {
225
+ if (queue.length > 0) { yield queue.shift(); }
226
+ else if (done) { break; }
227
+ else {
228
+ const v = await new Promise(r => { resolve = r; });
229
+ if (v.done) break;
230
+ yield v.value;
231
+ }
232
+ }
233
+ },
234
+ };
235
+ }
236
+
237
+ /**
238
+ * MCP over Streamable HTTP — per MCP spec 2025-11-25.
239
+ *
240
+ * Protocol:
241
+ * 1. POST <url> with JSON-RPC `initialize` request. Server replies with
242
+ * `mcp-session-id` header and either JSON or an SSE stream.
243
+ * 2. POST <url> with `notifications/initialized` (header set) to complete handshake.
244
+ * 3. GET <url> with `mcp-session-id` header (Accept: text/event-stream) opens
245
+ * a long-lived SSE stream of server->client JSON-RPC messages.
246
+ * 4. Each SSE `data:` event is a JSON-RPC message. We extract A2UI messages
247
+ * from `result.content` blocks the same way as the WS path.
248
+ * 5. If the server emits an `a2ui/action` request method, we POST a JSON-RPC
249
+ * response back to <url> with the session header.
250
+ *
251
+ * Note: this is a minimal client. Tools must be invoked separately by the
252
+ * consumer (this adapter is the *transport*, not a tool-call orchestrator).
253
+ * If the consumer needs feature-complete MCP semantics (resumability, request
254
+ * batching, cancellation tokens), use `@modelcontextprotocol/sdk`'s
255
+ * `StreamableHTTPClientTransport` directly.
256
+ */
257
+ function mcpStreamHttp(url, options) {
258
+ return {
259
+ async *[Symbol.asyncIterator]() {
260
+ const { signal, catalog, onAction } = options;
261
+
262
+ const queue = [];
263
+ let resolve = null;
264
+ let done = false;
265
+ const push = (msg) => {
266
+ if (resolve) { const r = resolve; resolve = null; r({ value: msg, done: false }); }
267
+ else queue.push(msg);
268
+ };
269
+ const finish = () => {
270
+ done = true;
271
+ if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); }
272
+ };
273
+
274
+ // --- 1. initialize handshake ---
275
+ const initBody = {
276
+ jsonrpc: '2.0',
277
+ id: 1,
278
+ method: 'initialize',
279
+ params: {
280
+ protocolVersion: '2025-11-25',
281
+ capabilities: {},
282
+ clientInfo: { name: 'a2ui', version: '0.0.0' },
283
+ },
284
+ };
285
+
286
+ let initResp;
287
+ try {
288
+ initResp = await fetch(url, {
289
+ method: 'POST',
290
+ headers: {
291
+ 'Content-Type': 'application/json',
292
+ 'Accept': 'application/json, text/event-stream',
293
+ },
294
+ body: JSON.stringify(initBody),
295
+ signal,
296
+ });
297
+ } catch (err) {
298
+ console.warn('A2UI MCP HTTP: initialize failed', err);
299
+ return;
300
+ }
301
+
302
+ if (!initResp.ok) {
303
+ console.warn(`A2UI MCP HTTP: initialize returned ${initResp.status}`);
304
+ return;
305
+ }
306
+
307
+ const sessionId = initResp.headers.get('mcp-session-id');
308
+ if (!sessionId) {
309
+ console.warn('A2UI MCP HTTP: server did not return mcp-session-id header');
310
+ // Try to drain body so the connection can be reused
311
+ try { await initResp.text(); } catch { /* ignore */ }
312
+ return;
313
+ }
314
+
315
+ // Drain initialize response body (we don't need its result for streaming)
316
+ try {
317
+ const ct = initResp.headers.get('content-type') || '';
318
+ if (ct.includes('text/event-stream')) {
319
+ // Read & discard
320
+ const reader = initResp.body.getReader();
321
+ // Read just enough to consume the initialize response then move on
322
+ const decoder = new TextDecoder();
323
+ let buf = '';
324
+ // eslint-disable-next-line no-constant-condition
325
+ while (true) {
326
+ const { done: rd, value } = await reader.read();
327
+ if (rd) break;
328
+ buf += decoder.decode(value, { stream: true });
329
+ if (buf.includes('\n\n')) break; // got at least one SSE event
330
+ }
331
+ try { reader.cancel(); } catch { /* ignore */ }
332
+ } else {
333
+ await initResp.json().catch(() => null);
334
+ }
335
+ } catch { /* ignore */ }
336
+
337
+ // --- 2. send `notifications/initialized` ---
338
+ try {
339
+ await fetch(url, {
340
+ method: 'POST',
341
+ headers: {
342
+ 'Content-Type': 'application/json',
343
+ 'Accept': 'application/json, text/event-stream',
344
+ 'mcp-session-id': sessionId,
345
+ },
346
+ body: JSON.stringify({
347
+ jsonrpc: '2.0',
348
+ method: 'notifications/initialized',
349
+ }),
350
+ signal,
351
+ }).then(r => r.text()).catch(() => null);
352
+ } catch { /* ignore */ }
353
+
354
+ // --- 3. optional: advertise catalog as a custom notification ---
355
+ if (catalog) {
356
+ const types = catalog instanceof Map ? [...catalog.keys()] : Object.keys(catalog);
357
+ try {
358
+ await fetch(url, {
359
+ method: 'POST',
360
+ headers: {
361
+ 'Content-Type': 'application/json',
362
+ 'Accept': 'application/json, text/event-stream',
363
+ 'mcp-session-id': sessionId,
364
+ },
365
+ body: JSON.stringify({
366
+ jsonrpc: '2.0',
367
+ method: 'a2ui/catalog',
368
+ params: { supportedTypes: types },
369
+ }),
370
+ signal,
371
+ }).then(r => r.text()).catch(() => null);
372
+ } catch { /* ignore */ }
373
+ }
374
+
375
+ // Helper to POST a JSON-RPC response (for server->client requests)
376
+ const postRpc = (msg) => {
377
+ return fetch(url, {
378
+ method: 'POST',
379
+ headers: {
380
+ 'Content-Type': 'application/json',
381
+ 'Accept': 'application/json, text/event-stream',
382
+ 'mcp-session-id': sessionId,
383
+ },
384
+ body: JSON.stringify(msg),
385
+ signal,
386
+ }).then(r => r.text()).catch(() => null);
387
+ };
388
+
389
+ // Handle an incoming JSON-RPC message
390
+ const handleRpc = (rpc) => {
391
+ _extractA2UIFromRpc(rpc, push);
392
+ if (rpc.method === 'a2ui/action' && onAction) {
393
+ Promise.resolve()
394
+ .then(() => onAction(rpc.params?.name, rpc.params?.arguments))
395
+ .then(result => postRpc({ jsonrpc: '2.0', id: rpc.id, result }))
396
+ .catch(err => postRpc({ jsonrpc: '2.0', id: rpc.id, error: { code: -1, message: err.message } }));
397
+ }
398
+ };
399
+
400
+ // --- 4. open long-lived GET SSE stream ---
401
+ let getResp;
402
+ try {
403
+ getResp = await fetch(url, {
404
+ method: 'GET',
405
+ headers: {
406
+ 'Accept': 'text/event-stream',
407
+ 'mcp-session-id': sessionId,
408
+ },
409
+ signal,
410
+ });
411
+ } catch (err) {
412
+ if (err?.name !== 'AbortError') console.warn('A2UI MCP HTTP: GET stream failed', err);
413
+ return;
414
+ }
415
+
416
+ if (!getResp.ok || !getResp.body) {
417
+ console.warn(`A2UI MCP HTTP: GET stream returned ${getResp.status}`);
418
+ return;
419
+ }
420
+
421
+ // Wire abort
422
+ if (signal) {
423
+ signal.addEventListener('abort', finish, { once: true });
424
+ }
425
+
426
+ // Best-effort: DELETE session on stream close to free server-side state
427
+ const closeSession = () => {
428
+ try {
429
+ fetch(url, {
430
+ method: 'DELETE',
431
+ headers: { 'mcp-session-id': sessionId },
432
+ }).catch(() => null);
433
+ } catch { /* ignore */ }
434
+ };
435
+
436
+ // SSE parser loop (running concurrently with the yield loop)
437
+ (async () => {
438
+ const reader = getResp.body.getReader();
439
+ const decoder = new TextDecoder();
440
+ let buffer = '';
441
+ try {
442
+ while (!done) {
443
+ const { done: rd, value } = await reader.read();
444
+ if (rd) break;
445
+ buffer += decoder.decode(value, { stream: true });
446
+ let idx;
447
+ // SSE events separated by blank lines
448
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
449
+ const rawEvent = buffer.slice(0, idx);
450
+ buffer = buffer.slice(idx + 2);
451
+ const dataLines = [];
452
+ for (const line of rawEvent.split('\n')) {
453
+ if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart());
454
+ }
455
+ if (dataLines.length === 0) continue;
456
+ const data = dataLines.join('\n');
457
+ try {
458
+ const rpc = JSON.parse(data);
459
+ handleRpc(rpc);
460
+ } catch {
461
+ console.warn('A2UI MCP HTTP: invalid JSON-RPC SSE payload', data);
462
+ }
463
+ }
464
+ }
465
+ } catch (err) {
466
+ if (err?.name !== 'AbortError') console.warn('A2UI MCP HTTP: stream read error', err);
467
+ } finally {
468
+ finish();
469
+ closeSession();
470
+ }
471
+ })();
472
+
473
+ while (!done || queue.length > 0) {
474
+ if (queue.length > 0) { yield queue.shift(); }
475
+ else if (done) { break; }
476
+ else {
477
+ const v = await new Promise(r => { resolve = r; });
478
+ if (v.done) break;
479
+ yield v.value;
480
+ }
481
+ }
482
+ },
483
+ };
484
+ }
485
+
486
+ /**
487
+ * JSONL (newline-delimited JSON) stream adapter via fetch.
488
+ */
489
+ export function jsonlStream(url, options = {}) {
490
+ return {
491
+ async *[Symbol.asyncIterator]() {
492
+ const headers = {};
493
+ if (options.catalog) {
494
+ const types = options.catalog instanceof Map ? [...options.catalog.keys()] : Object.keys(options.catalog);
495
+ headers['X-A2UI-Catalog'] = types.join(',');
496
+ }
497
+ const response = await fetch(url, { signal: options.signal, headers });
498
+ const reader = response.body.getReader();
499
+ const decoder = new TextDecoder();
500
+ let buffer = '';
501
+
502
+ while (true) {
503
+ const { done, value } = await reader.read();
504
+ if (done) break;
505
+ buffer += decoder.decode(value, { stream: true });
506
+ const lines = buffer.split('\n');
507
+ buffer = lines.pop();
508
+ for (const line of lines) {
509
+ const trimmed = line.trim();
510
+ if (!trimmed) continue;
511
+ try { yield JSON.parse(trimmed); }
512
+ catch { console.warn('A2UI JSONL: invalid JSON', trimmed); }
513
+ }
514
+ }
515
+ if (buffer.trim()) {
516
+ try { yield JSON.parse(buffer.trim()); }
517
+ catch { /* ignore */ }
518
+ }
519
+ },
520
+ };
521
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Surface Manifest — Multi-surface relationship document (A008).
3
+ */
4
+
5
+ export interface SurfaceDescriptor {
6
+ name: string;
7
+ route?: string;
8
+ entryPoint?: boolean;
9
+ requiredParams?: string[];
10
+ produces?: Record<string, object>;
11
+ consumes?: Record<string, { keys: string[] }>;
12
+ status?: 'generated' | 'manual' | 'template' | 'placeholder';
13
+ generatedBy?: string;
14
+ tags?: string[];
15
+ }
16
+
17
+ export type AssociationType =
18
+ | 'routes-to'
19
+ | 'feeds'
20
+ | 'shares-context'
21
+ | 'depends-on'
22
+ | 'triggers'
23
+ | 'contains'
24
+ | 'slots-into';
25
+
26
+ export interface Association {
27
+ type: AssociationType;
28
+ from: string;
29
+ to: string;
30
+ trigger?: string;
31
+ params?: Record<string, object>;
32
+ mapping?: object;
33
+ context?: string;
34
+ condition?: string;
35
+ fallback?: string;
36
+ effect?: string;
37
+ slot?: string;
38
+ position?: number;
39
+ meta?: object;
40
+ }
41
+
42
+ export interface ManifestValidationResult {
43
+ valid: boolean;
44
+ issues: Array<{ severity: 'error' | 'warning'; message: string }>;
45
+ }
46
+
47
+ export interface ManifestOptions {
48
+ id: string;
49
+ name: string;
50
+ version?: string;
51
+ }
52
+
53
+ export declare class SurfaceManifest {
54
+ constructor(opts: ManifestOptions);
55
+
56
+ readonly surfaceIds: string[];
57
+
58
+ addSurface(surfaceId: string, descriptor: SurfaceDescriptor): void;
59
+ removeSurface(surfaceId: string): void;
60
+ getSurface(surfaceId: string): SurfaceDescriptor | null;
61
+
62
+ addAssociation(association: Association): void;
63
+ getAssociationsFrom(surfaceId: string, type?: AssociationType): Association[];
64
+ getAssociationsTo(surfaceId: string, type?: AssociationType): Association[];
65
+ getSharedContextPeers(surfaceId: string): Array<{ surfaceId: string; context: string | undefined }>;
66
+
67
+ defineSharedContext(name: string, config: object): void;
68
+ getSharedContext(name: string): object | null;
69
+
70
+ validate(): ManifestValidationResult;
71
+ toJSON(): object;
72
+ static fromJSON(json: object): SurfaceManifest;
73
+ }