@urbicon-ui/blocks 6.40.4 → 6.43.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/components/Chat/A2UIView/A2UINode.svelte +20 -39
- package/dist/components/Chat/A2UIView/A2UIView.svelte +47 -50
- package/dist/components/Chat/A2UIView/a2ui-basic-catalog.d.ts +17 -0
- package/dist/components/Chat/A2UIView/a2ui-basic-catalog.js +69 -0
- package/dist/components/Chat/A2UIView/a2ui-catalog.d.ts +95 -0
- package/dist/components/Chat/A2UIView/a2ui-catalog.js +87 -0
- package/dist/components/Chat/A2UIView/a2ui-node-common.d.ts +40 -0
- package/dist/components/Chat/A2UIView/a2ui-node-common.js +72 -0
- package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +21 -10
- package/dist/components/Chat/A2UIView/a2ui-prompt.js +170 -24
- package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +1 -1
- package/dist/components/Chat/A2UIView/a2ui-registry.js +2 -1
- package/dist/components/Chat/A2UIView/a2ui-render.d.ts +30 -0
- package/dist/components/Chat/A2UIView/a2ui-render.js +61 -23
- package/dist/components/Chat/A2UIView/a2ui-router.d.ts +136 -0
- package/dist/components/Chat/A2UIView/a2ui-router.js +264 -0
- package/dist/components/Chat/A2UIView/a2ui-schema.d.ts +45 -0
- package/dist/components/Chat/A2UIView/a2ui-schema.js +133 -0
- package/dist/components/Chat/A2UIView/a2ui-stream.d.ts +120 -0
- package/dist/components/Chat/A2UIView/a2ui-stream.js +406 -0
- package/dist/components/Chat/A2UIView/a2ui-transcript.d.ts +65 -0
- package/dist/components/Chat/A2UIView/a2ui-transcript.js +101 -0
- package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +33 -1
- package/dist/components/Chat/A2UIView/a2ui-validate.js +167 -63
- package/dist/components/Chat/A2UIView/a2ui.types.d.ts +50 -0
- package/dist/components/Chat/A2UIView/a2ui.types.js +38 -1
- package/dist/components/Chat/A2UIView/index.d.ts +35 -2
- package/dist/components/Chat/A2UIView/index.js +13 -0
- package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte +831 -0
- package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte.d.ts +4 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.d.ts +14 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.js +87 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.d.ts +78 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.js +783 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.d.ts +43 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.js +33 -0
- package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.d.ts +7 -0
- package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.js +86 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +19 -19
- package/dist/components/FileUpload/fileUpload.variants.js +15 -4
- package/dist/components/PinInput/PinInput.svelte +1 -0
- package/dist/components/PinInput/pin-input.variants.js +14 -1
- package/dist/components/TimeInput/time-input.variants.js +16 -1
- package/dist/internal/field-chrome.d.ts +18 -1
- package/dist/internal/field-chrome.js +18 -1
- package/dist/primitives/Avatar/avatar.variants.js +4 -1
- package/dist/primitives/Badge/badge.variants.js +11 -2
- package/dist/primitives/Button/button.variants.js +4 -1
- package/dist/primitives/Card/card.variants.js +4 -1
- package/dist/primitives/Checkbox/checkbox.variants.js +7 -1
- package/dist/primitives/Combobox/Combobox.svelte +9 -1
- package/dist/primitives/Combobox/combobox.variants.d.ts +25 -0
- package/dist/primitives/Combobox/combobox.variants.js +32 -3
- package/dist/primitives/Combobox/index.d.ts +8 -2
- package/dist/primitives/Dialog/dialog.variants.js +4 -1
- package/dist/primitives/Drawer/drawer.variants.js +4 -1
- package/dist/primitives/Input/input.variants.js +17 -1
- package/dist/primitives/Menu/menu.variants.js +5 -1
- package/dist/primitives/Progress/progress.variants.js +5 -2
- package/dist/primitives/RadioGroup/radioGroup.variants.js +68 -9
- package/dist/primitives/Select/select.variants.js +13 -4
- package/dist/primitives/Slider/Slider.svelte +4 -1
- package/dist/primitives/Slider/slider.variants.js +9 -2
- package/dist/primitives/Textarea/textarea.variants.js +15 -1
- package/dist/primitives/Toast/toast.variants.js +5 -1
- package/dist/primitives/Toggle/toggle.variants.js +92 -18
- package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
- package/dist/primitives/Tooltip/tooltip.variants.js +5 -1
- package/dist/utils/persistent-state.svelte.d.ts +25 -1
- package/dist/utils/persistent-state.svelte.js +98 -10
- package/docs/MIGRATION-v5.md +29 -0
- package/package.json +3 -3
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface routing across message boundaries.
|
|
3
|
+
*
|
|
4
|
+
* `A2UIView` is payload-driven: one view renders one envelope array, and its
|
|
5
|
+
* processor lives with that view. In a chat that means one view per assistant
|
|
6
|
+
* message — so a surface dies with the message that created it. The A2UI spec
|
|
7
|
+
* says the opposite: a `surfaceId` is "globally unique for the renderer's
|
|
8
|
+
* lifetime", `deleteSurface` is the only way to retire one, and
|
|
9
|
+
* `updateComponents`/`updateDataModel` are patches an agent may send at any
|
|
10
|
+
* later point. A model that patches a form it built two turns ago is reading
|
|
11
|
+
* the protocol correctly; the renderer just had nowhere to put the envelope.
|
|
12
|
+
*
|
|
13
|
+
* This router closes that gap WITHOUT giving the engine a second state
|
|
14
|
+
* lifetime. It is a pure bookkeeping layer over the transcript: it remembers
|
|
15
|
+
* which source (message + a2ui part) owns which surface, and splits each
|
|
16
|
+
* source's envelopes into the ones that stay put and the ones that belong to an
|
|
17
|
+
* earlier source's payload. The consumer appends those to that payload
|
|
18
|
+
* (`[...payload, ...patch]`, keeping element identity) and `A2UIView` consumes
|
|
19
|
+
* them incrementally — two-way edits the user already made survive, because the
|
|
20
|
+
* view never rebuilds.
|
|
21
|
+
*
|
|
22
|
+
* The transcript stays the single source of truth: every envelope still lives
|
|
23
|
+
* in exactly one payload, so undo (`revoke`) and replay are plain array work.
|
|
24
|
+
*
|
|
25
|
+
* Zero-dependency and Svelte-free — usable from a server-side transcript
|
|
26
|
+
* reducer, not just from the browser.
|
|
27
|
+
*/
|
|
28
|
+
import { A2UI_ISSUE_CODES } from './a2ui.types.js';
|
|
29
|
+
/** The four operations an envelope may carry, in the order the spec lists them. */
|
|
30
|
+
const OP_KEYS = ['createSurface', 'updateComponents', 'updateDataModel', 'deleteSurface'];
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read `(operation, surfaceId)` off an envelope — tolerantly, and without
|
|
36
|
+
* duplicating the validator. Anything that is not exactly one known operation
|
|
37
|
+
* with a non-empty string `surfaceId` returns `undefined`: the router then
|
|
38
|
+
* leaves the envelope where it is and the processor reports the real fault.
|
|
39
|
+
*/
|
|
40
|
+
function readTarget(envelope) {
|
|
41
|
+
if (!isPlainObject(envelope))
|
|
42
|
+
return null;
|
|
43
|
+
const ops = OP_KEYS.filter((key) => envelope[key] !== undefined);
|
|
44
|
+
if (ops.length !== 1)
|
|
45
|
+
return null;
|
|
46
|
+
const op = ops[0];
|
|
47
|
+
const body = envelope[op];
|
|
48
|
+
if (!isPlainObject(body))
|
|
49
|
+
return null;
|
|
50
|
+
const surfaceId = body.surfaceId;
|
|
51
|
+
if (typeof surfaceId !== 'string' || surfaceId === '')
|
|
52
|
+
return null;
|
|
53
|
+
return { op, surfaceId };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Tracks surface ownership across sources and routes envelopes to the payload
|
|
57
|
+
* that owns their surface.
|
|
58
|
+
*
|
|
59
|
+
* A "source" is one A2UI payload in the transcript — in a chat app, one `a2ui`
|
|
60
|
+
* part of one message. The key is the consumer's to choose; it only has to be
|
|
61
|
+
* stable for that payload's lifetime (e.g. `` `${messageId}#${partIndex}` ``).
|
|
62
|
+
*
|
|
63
|
+
* Ownership rules:
|
|
64
|
+
* - `createSurface` for an unknown id → the calling source owns it.
|
|
65
|
+
* - `createSurface` for an id owned by ANOTHER source → ownership transfers to
|
|
66
|
+
* the caller (last writer wins, so the new surface actually renders) and a
|
|
67
|
+
* `SURFACE_RECREATED` issue is raised for the agent.
|
|
68
|
+
* - Any operation on a surface owned by another source → routed to that owner.
|
|
69
|
+
* - Any operation on an UNKNOWN surface stays with the caller, so the
|
|
70
|
+
* processor's `NO_SURFACE` error surfaces instead of vanishing here.
|
|
71
|
+
* - `deleteSurface` routes to the owner and then frees the id for re-use.
|
|
72
|
+
*
|
|
73
|
+
* @example One assistant turn, wired into the transcript
|
|
74
|
+
* ```ts
|
|
75
|
+
* const result = router.route(`${messageId}#0`, part.payload);
|
|
76
|
+
* part.payload = result.own;
|
|
77
|
+
* for (const patch of result.patches) appendToPayload(patch.targetKey, patch.envelopes);
|
|
78
|
+
* for (const surfaceId of result.promoted) markLongLived(surfaceId);
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export class A2uiSurfaceRouter {
|
|
82
|
+
/** surfaceId → owning sourceKey. */
|
|
83
|
+
#owners = new Map();
|
|
84
|
+
#sources = new Map();
|
|
85
|
+
/**
|
|
86
|
+
* surfaceId → the sources that patched it from the outside. Counting the
|
|
87
|
+
* patchers (rather than flagging a boolean) keeps the promotion signal honest
|
|
88
|
+
* when a turn is regenerated: take the last outside patch away and the
|
|
89
|
+
* surface stops being long-lived, so a later patch promotes it afresh.
|
|
90
|
+
*/
|
|
91
|
+
#patchedBy = new Map();
|
|
92
|
+
/**
|
|
93
|
+
* Detach a source: release the surfaces it owns, undo its share of every
|
|
94
|
+
* promotion, and report the envelopes it had pushed elsewhere.
|
|
95
|
+
*/
|
|
96
|
+
#detach(sourceKey, source) {
|
|
97
|
+
for (const [surfaceId, owner] of this.#owners) {
|
|
98
|
+
if (owner === sourceKey) {
|
|
99
|
+
this.#owners.delete(surfaceId);
|
|
100
|
+
this.#patchedBy.delete(surfaceId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const surfaceId of source.patched) {
|
|
104
|
+
const patchers = this.#patchedBy.get(surfaceId);
|
|
105
|
+
if (!patchers)
|
|
106
|
+
continue;
|
|
107
|
+
patchers.delete(sourceKey);
|
|
108
|
+
if (patchers.size === 0)
|
|
109
|
+
this.#patchedBy.delete(surfaceId);
|
|
110
|
+
}
|
|
111
|
+
const patches = [];
|
|
112
|
+
for (const [targetKey, envelopes] of source.sent) {
|
|
113
|
+
if (envelopes.length > 0)
|
|
114
|
+
patches.push({ targetKey, envelopes });
|
|
115
|
+
}
|
|
116
|
+
return patches;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Route one source's envelope list.
|
|
120
|
+
*
|
|
121
|
+
* Call it as often as you like — on every streamed token, if that is when the
|
|
122
|
+
* payload grows. Only envelopes appended since the last call are examined; an
|
|
123
|
+
* unchanged prefix is never re-routed, so patches are never duplicated. If
|
|
124
|
+
* the prefix DID change (a regenerated turn), the source is rebuilt from
|
|
125
|
+
* scratch and its previous patches come back in `revoked`.
|
|
126
|
+
*
|
|
127
|
+
* @param sourceKey Stable id of the payload being routed.
|
|
128
|
+
* @param envelopes The payload's accumulated envelopes, in stream order.
|
|
129
|
+
*/
|
|
130
|
+
route(sourceKey, envelopes) {
|
|
131
|
+
const previous = this.#sources.get(sourceKey);
|
|
132
|
+
const isAppend = previous !== undefined &&
|
|
133
|
+
envelopes.length >= previous.seen.length &&
|
|
134
|
+
previous.seen.every((envelope, index) => envelope === envelopes[index]);
|
|
135
|
+
const revoked = [];
|
|
136
|
+
let source;
|
|
137
|
+
let startIndex;
|
|
138
|
+
if (isAppend) {
|
|
139
|
+
source = previous;
|
|
140
|
+
startIndex = previous.seen.length;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Rebuild: hand back whatever this source had pushed into other payloads
|
|
144
|
+
// and drop the surfaces it owned, so re-routing re-establishes them.
|
|
145
|
+
if (previous)
|
|
146
|
+
revoked.push(...this.#detach(sourceKey, previous));
|
|
147
|
+
source = { seen: [], own: [], sent: new Map(), patched: new Set() };
|
|
148
|
+
this.#sources.set(sourceKey, source);
|
|
149
|
+
startIndex = 0;
|
|
150
|
+
}
|
|
151
|
+
const patches = new Map();
|
|
152
|
+
const promoted = [];
|
|
153
|
+
const issues = [];
|
|
154
|
+
for (let index = startIndex; index < envelopes.length; index++) {
|
|
155
|
+
const envelope = envelopes[index];
|
|
156
|
+
source.seen.push(envelope);
|
|
157
|
+
const target = readTarget(envelope);
|
|
158
|
+
if (!target) {
|
|
159
|
+
source.own.push(envelope);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const { op, surfaceId } = target;
|
|
163
|
+
const owner = this.#owners.get(surfaceId);
|
|
164
|
+
if (op === 'createSurface') {
|
|
165
|
+
if (owner !== undefined && owner !== sourceKey) {
|
|
166
|
+
issues.push({
|
|
167
|
+
severity: 'warning',
|
|
168
|
+
code: A2UI_ISSUE_CODES.SURFACE_RECREATED,
|
|
169
|
+
surfaceId,
|
|
170
|
+
message: `Surface "${surfaceId}" was already created earlier in this conversation. ` +
|
|
171
|
+
"A surfaceId must stay unique for the renderer's lifetime — to change an " +
|
|
172
|
+
'existing surface send updateComponents/updateDataModel for it instead of ' +
|
|
173
|
+
'createSurface, or pick a new id.'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Last writer wins either way: the newest source owns the id, so its
|
|
177
|
+
// surface renders and later patches follow it. A recreated surface is
|
|
178
|
+
// a fresh one — its promotion history goes with the old incarnation.
|
|
179
|
+
this.#owners.set(surfaceId, sourceKey);
|
|
180
|
+
this.#patchedBy.delete(surfaceId);
|
|
181
|
+
source.own.push(envelope);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// An unknown surface stays here on purpose — fail loud through the
|
|
185
|
+
// processor rather than silently dropping the envelope.
|
|
186
|
+
if (owner === undefined || owner === sourceKey) {
|
|
187
|
+
source.own.push(envelope);
|
|
188
|
+
if (op === 'deleteSurface' && owner === sourceKey)
|
|
189
|
+
this.#owners.delete(surfaceId);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// The surface lives in an earlier payload — send the patch there.
|
|
193
|
+
const bucket = patches.get(owner) ?? [];
|
|
194
|
+
bucket.push(envelope);
|
|
195
|
+
patches.set(owner, bucket);
|
|
196
|
+
const sent = source.sent.get(owner) ?? [];
|
|
197
|
+
sent.push(envelope);
|
|
198
|
+
source.sent.set(owner, sent);
|
|
199
|
+
const patchers = this.#patchedBy.get(surfaceId) ?? new Set();
|
|
200
|
+
const wasLongLived = patchers.size > 0;
|
|
201
|
+
patchers.add(sourceKey);
|
|
202
|
+
this.#patchedBy.set(surfaceId, patchers);
|
|
203
|
+
source.patched.add(surfaceId);
|
|
204
|
+
if (!wasLongLived)
|
|
205
|
+
promoted.push(surfaceId);
|
|
206
|
+
if (op === 'deleteSurface') {
|
|
207
|
+
this.#owners.delete(surfaceId);
|
|
208
|
+
this.#patchedBy.delete(surfaceId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
own: source.own.slice(),
|
|
213
|
+
patches: [...patches].map(([targetKey, list]) => ({ targetKey, envelopes: list })),
|
|
214
|
+
revoked,
|
|
215
|
+
promoted,
|
|
216
|
+
issues
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Forget a source entirely — call it when a message leaves the transcript
|
|
221
|
+
* (regenerate, delete, branch switch). The returned patches are the envelopes
|
|
222
|
+
* this source had pushed into other payloads; remove them by reference so no
|
|
223
|
+
* orphaned patch survives its author.
|
|
224
|
+
*/
|
|
225
|
+
revoke(sourceKey) {
|
|
226
|
+
const source = this.#sources.get(sourceKey);
|
|
227
|
+
if (!source)
|
|
228
|
+
return [];
|
|
229
|
+
this.#sources.delete(sourceKey);
|
|
230
|
+
return this.#detach(sourceKey, source);
|
|
231
|
+
}
|
|
232
|
+
/** The source whose payload currently holds `surfaceId`, if any. */
|
|
233
|
+
ownerOf(surfaceId) {
|
|
234
|
+
return this.#owners.get(surfaceId);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Whether a surface has been patched from a later source — the promotion
|
|
238
|
+
* predicate, for clients that need to ask after the fact rather than react to
|
|
239
|
+
* {@link A2uiRouteResult.promoted}.
|
|
240
|
+
*/
|
|
241
|
+
isLongLived(surfaceId) {
|
|
242
|
+
return (this.#patchedBy.get(surfaceId)?.size ?? 0) > 0;
|
|
243
|
+
}
|
|
244
|
+
/** Every surface currently owned by a source, in creation order. */
|
|
245
|
+
surfaceIds() {
|
|
246
|
+
return [...this.#owners.keys()];
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Every source key routed so far, in first-seen order.
|
|
250
|
+
*
|
|
251
|
+
* Needed to retire a source whose payload no longer shows it: a message whose
|
|
252
|
+
* envelopes ALL travelled to earlier surfaces keeps no a2ui part of its own,
|
|
253
|
+
* so the caller cannot reconstruct its keys by counting parts.
|
|
254
|
+
*/
|
|
255
|
+
sourceKeys() {
|
|
256
|
+
return [...this.#sources.keys()];
|
|
257
|
+
}
|
|
258
|
+
/** Drop all bookkeeping (new conversation). */
|
|
259
|
+
reset() {
|
|
260
|
+
this.#owners.clear();
|
|
261
|
+
this.#sources.clear();
|
|
262
|
+
this.#patchedBy.clear();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional per-surface DATA schema for the A2UI engine — the data-model side of
|
|
3
|
+
* the registry's prop typing (findings §4.3). A schema declares the shape of the
|
|
4
|
+
* fields a surface's data model may hold (JSON Pointer → type + optional
|
|
5
|
+
* enum/format/description). It does two things:
|
|
6
|
+
* - documents the declared fields in the system prompt (`a2uiDataSchemaSection`),
|
|
7
|
+
* so the agent knows which paths exist and what they hold;
|
|
8
|
+
* - validates `updateDataModel` writes against those declarations
|
|
9
|
+
* (`validateSchemaWrite`), turning a type mismatch on a declared pointer into a
|
|
10
|
+
* `SCHEMA_TYPE_MISMATCH` error and a write to an undeclared top-level branch
|
|
11
|
+
* into a `SCHEMA_UNDECLARED_PATH` warning — both relayed through the same
|
|
12
|
+
* `onValidationError` feedback loop the agent already consumes.
|
|
13
|
+
*
|
|
14
|
+
* Pure TS, no Svelte. Deliberately MINIMAL (v1): exact-pointer type/enum checks
|
|
15
|
+
* and a top-level "did you mean to write here?" warning. Deferred: required
|
|
16
|
+
* fields, nested object schemas, cross-field constraints, and transporting the
|
|
17
|
+
* schema inside the envelope.
|
|
18
|
+
*/
|
|
19
|
+
import { type A2uiValidationIssue } from './a2ui.types.js';
|
|
20
|
+
/** The JSON primitive/shape a declared field holds. */
|
|
21
|
+
export type A2uiSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
|
|
22
|
+
/** One declared field: its type plus optional enum/format/description for the prompt. */
|
|
23
|
+
export interface A2uiSchemaField {
|
|
24
|
+
type: A2uiSchemaType;
|
|
25
|
+
/** Agent-facing description; emitted into the prompt verbatim. */
|
|
26
|
+
description?: string;
|
|
27
|
+
/** Allowed literal values (documented + enforced for string/number fields). */
|
|
28
|
+
enum?: readonly (string | number)[];
|
|
29
|
+
/** A format hint (e.g. `date`, `time`, `email`) — documented, not enforced in v1. */
|
|
30
|
+
format?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Surface data schema: absolute JSON Pointer → field declaration. */
|
|
33
|
+
export type A2uiDataSchema = Readonly<Record<string, A2uiSchemaField>>;
|
|
34
|
+
/**
|
|
35
|
+
* Validate one `updateDataModel` write against the schema. `pointer` is the
|
|
36
|
+
* write target (`''`/`'/'`/`undefined` = whole model); `value` is the written
|
|
37
|
+
* value (`undefined` for a delete — never flagged). Returns any schema issues.
|
|
38
|
+
*/
|
|
39
|
+
export declare function validateSchemaWrite(schema: A2uiDataSchema, pointer: string | undefined, value: unknown, surfaceId?: string): A2uiValidationIssue[];
|
|
40
|
+
/**
|
|
41
|
+
* Render the schema as a prompt section listing every declared field, its type
|
|
42
|
+
* and any enum/format/description. Appended by the app after `a2uiSystemPrompt`
|
|
43
|
+
* (like the transport section), NOT baked into the generator.
|
|
44
|
+
*/
|
|
45
|
+
export declare function a2uiDataSchemaSection(schema: A2uiDataSchema): string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional per-surface DATA schema for the A2UI engine — the data-model side of
|
|
3
|
+
* the registry's prop typing (findings §4.3). A schema declares the shape of the
|
|
4
|
+
* fields a surface's data model may hold (JSON Pointer → type + optional
|
|
5
|
+
* enum/format/description). It does two things:
|
|
6
|
+
* - documents the declared fields in the system prompt (`a2uiDataSchemaSection`),
|
|
7
|
+
* so the agent knows which paths exist and what they hold;
|
|
8
|
+
* - validates `updateDataModel` writes against those declarations
|
|
9
|
+
* (`validateSchemaWrite`), turning a type mismatch on a declared pointer into a
|
|
10
|
+
* `SCHEMA_TYPE_MISMATCH` error and a write to an undeclared top-level branch
|
|
11
|
+
* into a `SCHEMA_UNDECLARED_PATH` warning — both relayed through the same
|
|
12
|
+
* `onValidationError` feedback loop the agent already consumes.
|
|
13
|
+
*
|
|
14
|
+
* Pure TS, no Svelte. Deliberately MINIMAL (v1): exact-pointer type/enum checks
|
|
15
|
+
* and a top-level "did you mean to write here?" warning. Deferred: required
|
|
16
|
+
* fields, nested object schemas, cross-field constraints, and transporting the
|
|
17
|
+
* schema inside the envelope.
|
|
18
|
+
*/
|
|
19
|
+
import { A2UI_ISSUE_CODES } from './a2ui.types.js';
|
|
20
|
+
import { getAtPointer } from './a2ui-data.js';
|
|
21
|
+
function typeMatches(type, value) {
|
|
22
|
+
switch (type) {
|
|
23
|
+
case 'string':
|
|
24
|
+
return typeof value === 'string';
|
|
25
|
+
case 'number':
|
|
26
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
27
|
+
case 'integer':
|
|
28
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
29
|
+
case 'boolean':
|
|
30
|
+
return typeof value === 'boolean';
|
|
31
|
+
case 'array':
|
|
32
|
+
return Array.isArray(value);
|
|
33
|
+
case 'object':
|
|
34
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** The first pointer segment (`/booking/date` → `booking`); `''` for the root. */
|
|
38
|
+
function topSegment(pointer) {
|
|
39
|
+
const trimmed = pointer.startsWith('/') ? pointer.slice(1) : pointer;
|
|
40
|
+
return trimmed.split('/')[0] ?? '';
|
|
41
|
+
}
|
|
42
|
+
function fieldIssues(field, pointer, value, surfaceId) {
|
|
43
|
+
if (!typeMatches(field.type, value)) {
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
severity: 'error',
|
|
47
|
+
code: A2UI_ISSUE_CODES.SCHEMA_TYPE_MISMATCH,
|
|
48
|
+
surfaceId,
|
|
49
|
+
path: pointer,
|
|
50
|
+
message: `Data at "${pointer}" must be a ${field.type} per the surface schema`
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
if (field.enum &&
|
|
55
|
+
(typeof value === 'string' || typeof value === 'number') &&
|
|
56
|
+
!field.enum.includes(value)) {
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
severity: 'error',
|
|
60
|
+
code: A2UI_ISSUE_CODES.SCHEMA_TYPE_MISMATCH,
|
|
61
|
+
surfaceId,
|
|
62
|
+
path: pointer,
|
|
63
|
+
message: `Data at "${pointer}" must be one of: ${field.enum.join(', ')}`
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate one `updateDataModel` write against the schema. `pointer` is the
|
|
71
|
+
* write target (`''`/`'/'`/`undefined` = whole model); `value` is the written
|
|
72
|
+
* value (`undefined` for a delete — never flagged). Returns any schema issues.
|
|
73
|
+
*/
|
|
74
|
+
export function validateSchemaWrite(schema, pointer, value, surfaceId) {
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
return []; // a delete never violates a type
|
|
77
|
+
const whole = pointer === undefined || pointer === '' || pointer === '/';
|
|
78
|
+
if (whole) {
|
|
79
|
+
// Whole-model write: check every declared field against the new model.
|
|
80
|
+
const issues = [];
|
|
81
|
+
for (const [declaredPointer, field] of Object.entries(schema)) {
|
|
82
|
+
const at = getAtPointer(value, declaredPointer);
|
|
83
|
+
if (at !== undefined)
|
|
84
|
+
issues.push(...fieldIssues(field, declaredPointer, at, surfaceId));
|
|
85
|
+
}
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
// `Object.hasOwn`, not a bare lookup: a pointer like `/toString` or `/__proto__`
|
|
89
|
+
// would otherwise resolve an inherited Object.prototype member and mis-report a
|
|
90
|
+
// type mismatch on a field that was never declared.
|
|
91
|
+
const field = Object.hasOwn(schema, pointer) ? schema[pointer] : undefined;
|
|
92
|
+
if (field)
|
|
93
|
+
return fieldIssues(field, pointer, value, surfaceId);
|
|
94
|
+
// Undeclared pointer: warn only when its whole top-level branch is unknown, so
|
|
95
|
+
// a write into a declared object (a deeper path we don't model yet) stays quiet.
|
|
96
|
+
const top = topSegment(pointer);
|
|
97
|
+
const declared = Object.keys(schema).some((declaredPointer) => topSegment(declaredPointer) === top);
|
|
98
|
+
if (!declared) {
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
severity: 'warning',
|
|
102
|
+
code: A2UI_ISSUE_CODES.SCHEMA_UNDECLARED_PATH,
|
|
103
|
+
surfaceId,
|
|
104
|
+
path: pointer,
|
|
105
|
+
message: `Data path "${pointer}" is not declared in the surface schema`
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Render the schema as a prompt section listing every declared field, its type
|
|
113
|
+
* and any enum/format/description. Appended by the app after `a2uiSystemPrompt`
|
|
114
|
+
* (like the transport section), NOT baked into the generator.
|
|
115
|
+
*/
|
|
116
|
+
export function a2uiDataSchemaSection(schema) {
|
|
117
|
+
const lines = [
|
|
118
|
+
'## Data schema',
|
|
119
|
+
'',
|
|
120
|
+
'The surface data model holds these fields (bind inputs to them with { path }',
|
|
121
|
+
'and initialize them via updateDataModel). Write only the declared types:'
|
|
122
|
+
];
|
|
123
|
+
for (const [pointer, field] of Object.entries(schema)) {
|
|
124
|
+
const bits = [field.type];
|
|
125
|
+
if (field.enum)
|
|
126
|
+
bits.push(`one of: ${field.enum.join(' | ')}`);
|
|
127
|
+
if (field.format)
|
|
128
|
+
bits.push(`format ${field.format}`);
|
|
129
|
+
const suffix = field.description ? ` — ${field.description}` : '';
|
|
130
|
+
lines.push(`- ${pointer} (${bits.join('; ')})${suffix}`);
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** A streamed text segment. Structurally assignable to blocks' `ChatMessagePart` (`text`). */
|
|
2
|
+
export interface A2uiStreamTextPart {
|
|
3
|
+
type: 'text';
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
/** A streamed A2UI segment. `payload` is the accumulated envelope array (assignable to `ChatMessagePart` `a2ui`). */
|
|
7
|
+
export interface A2uiStreamUiPart {
|
|
8
|
+
type: 'a2ui';
|
|
9
|
+
payload: unknown[];
|
|
10
|
+
}
|
|
11
|
+
export type A2uiStreamPart = A2uiStreamTextPart | A2uiStreamUiPart;
|
|
12
|
+
/** A non-fatal finding from the split (a malformed JSONL line, an unterminated fence). */
|
|
13
|
+
export interface A2uiStreamIssue {
|
|
14
|
+
code: 'UNPARSEABLE_LINE' | 'UNTERMINATED_FENCE';
|
|
15
|
+
message: string;
|
|
16
|
+
/** The offending source line, when applicable. */
|
|
17
|
+
line?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The info string that marks a fence as A2UI. Shared by the parser and by
|
|
21
|
+
* `a2uiFencedTransportSection()` so the prompt can never name a different tag
|
|
22
|
+
* than the splitter accepts.
|
|
23
|
+
*/
|
|
24
|
+
export declare const A2UI_FENCE_TAG = "a2ui";
|
|
25
|
+
export declare class A2uiStreamSplitter {
|
|
26
|
+
/** Committed parts (immutable; replaced wholesale on every mutation). */
|
|
27
|
+
private committed;
|
|
28
|
+
private mode;
|
|
29
|
+
/**
|
|
30
|
+
* When in text mode inside a *regular* (non-a2ui) markdown code fence, the
|
|
31
|
+
* marker that opened it. A `` ```a2ui `` line inside such a block is literal
|
|
32
|
+
* code, NOT a surface — this is what stops a quoted example from executing.
|
|
33
|
+
*/
|
|
34
|
+
private codeFence;
|
|
35
|
+
/** Buffered trailing line that has not yet seen its newline. */
|
|
36
|
+
private pending;
|
|
37
|
+
private rawText;
|
|
38
|
+
private readonly issueList;
|
|
39
|
+
/**
|
|
40
|
+
* Raw lines of an in-progress multi-line envelope (a pretty-printed object
|
|
41
|
+
* spanning several fence lines), plus the depth-scanner state that tracks
|
|
42
|
+
* whether the object has closed. Empty/zeroed while envelopes arrive as
|
|
43
|
+
* proper single-line JSONL.
|
|
44
|
+
*/
|
|
45
|
+
private objLines;
|
|
46
|
+
private objDepth;
|
|
47
|
+
private objInString;
|
|
48
|
+
private objEscape;
|
|
49
|
+
/** Feed the next token / chunk of model output. */
|
|
50
|
+
push(chunk: string): void;
|
|
51
|
+
/** Signal end of stream: flush the trailing partial line and settle state. */
|
|
52
|
+
end(): void;
|
|
53
|
+
private asText;
|
|
54
|
+
private resetObjBuffer;
|
|
55
|
+
/**
|
|
56
|
+
* Advance the depth scanner across one line: brace/bracket depth outside of
|
|
57
|
+
* strings, with `\"`-escape handling inside them. Returns `false` when the
|
|
58
|
+
* buffer can never become valid JSON — depth underflow, or a string left
|
|
59
|
+
* open at end-of-line (raw newlines are illegal inside JSON strings).
|
|
60
|
+
*/
|
|
61
|
+
private scanDepth;
|
|
62
|
+
/**
|
|
63
|
+
* Abandon the current a2ui fence: drop an empty surface, record an issue and
|
|
64
|
+
* re-emit the buffered lines (plus the offending one, if any) as text inside
|
|
65
|
+
* a synthetic ``` code block. `codeFence` is armed so the fence's real
|
|
66
|
+
* closing ``` closes that block instead of opening a stray one downstream.
|
|
67
|
+
*/
|
|
68
|
+
private fenceFallback;
|
|
69
|
+
private processLine;
|
|
70
|
+
/**
|
|
71
|
+
* The current parts, including the buffered trailing text tail for a
|
|
72
|
+
* responsive stream. A partial line that might still become a fence marker is
|
|
73
|
+
* held back (never flashed). a2ui parts are carried by reference so A2UIView
|
|
74
|
+
* consumes them incrementally.
|
|
75
|
+
*/
|
|
76
|
+
snapshot(): A2uiStreamPart[];
|
|
77
|
+
/** The raw model output fed so far (fences included) — the faithful wire form. */
|
|
78
|
+
get raw(): string;
|
|
79
|
+
/** Accumulated non-fatal issues (malformed JSONL, unterminated fence). */
|
|
80
|
+
get issues(): A2uiStreamIssue[];
|
|
81
|
+
}
|
|
82
|
+
/** Options for {@link a2uiFencedTransportSection}. */
|
|
83
|
+
export interface A2uiTransportSectionOptions {
|
|
84
|
+
/**
|
|
85
|
+
* Prefix your client puts on a user turn that reports an action, followed by
|
|
86
|
+
* the JSON `A2uiActionEvent`. Set to `false` if actions reach the agent some
|
|
87
|
+
* other way (a tool result, a structured field) — the round-channel paragraph
|
|
88
|
+
* is then omitted. @default '[ui-action]'
|
|
89
|
+
*/
|
|
90
|
+
actionPrefix?: string | false;
|
|
91
|
+
/**
|
|
92
|
+
* Prefix for a turn that reports validation issues as JSON, so the agent can
|
|
93
|
+
* repair the surface. `false` omits it. @default '[ui-error]'
|
|
94
|
+
*/
|
|
95
|
+
errorPrefix?: string | false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The transport half of the fenced-JSONL contract: the prompt section that makes
|
|
99
|
+
* an agent emit exactly what {@link A2uiStreamSplitter} parses.
|
|
100
|
+
*
|
|
101
|
+
* `a2uiSystemPrompt()` deliberately says nothing about transport, because how
|
|
102
|
+
* envelopes travel is the app's business. This is the transport for apps that
|
|
103
|
+
* take the model's ordinary text stream and let it open a ` ```a2ui ` fence —
|
|
104
|
+
* the setup the splitter exists for. Append it after `a2uiSystemPrompt()` (and
|
|
105
|
+
* after `a2uiDataSchemaSection()`, if you use one).
|
|
106
|
+
*
|
|
107
|
+
* Anything domain-specific — which tools to call, what never to invent — stays
|
|
108
|
+
* yours to append; this covers only the wire format and the return path.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const system = [
|
|
113
|
+
* a2uiSystemPrompt({ catalog: urbiconA2uiCatalogSpec }),
|
|
114
|
+
* a2uiDataSchemaSection(MY_SCHEMA),
|
|
115
|
+
* a2uiFencedTransportSection(),
|
|
116
|
+
* MY_DOMAIN_RULES
|
|
117
|
+
* ].join('\n\n');
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export declare function a2uiFencedTransportSection(options?: A2uiTransportSectionOptions): string;
|