@ai-gui/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Liang Li
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @ai-gui/core
2
+
3
+ The headless streaming engine behind [AIGUI](../../README.md) — framework-agnostic. It parses a streaming LLM response into an AST + patches, runs plugin node renderers, sanitizes HTML, and builds the system prompt. Use it directly, or via an adapter (`@ai-gui/react`, `@ai-gui/vue`, `@ai-gui/vanilla`).
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add @ai-gui/core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { Renderer, CardRegistry, buildSystemPrompt } from "@ai-gui/core"
15
+
16
+ const registry = new CardRegistry()
17
+ registry.register({ type: "weather", description: "Weather summary", example: { city: "Tokyo" } })
18
+
19
+ const renderer = new Renderer({
20
+ registry,
21
+ sanitize: true,
22
+ onPatch: (patches, nodes) => {
23
+ // called as the stream grows; render `nodes` however you like
24
+ },
25
+ })
26
+
27
+ // feed an AsyncIterable<string> or a ReadableStream, or push chunks manually
28
+ await renderer.feed(response.body!)
29
+ // renderer.push("more text"); renderer.reset()
30
+
31
+ // assemble the system-prompt guidance for the model
32
+ const system = buildSystemPrompt({ registry })
33
+ ```
34
+
35
+ ## Exports
36
+
37
+ - `Renderer` — `push(chunk)`, `feed(AsyncIterable | ReadableStream)`, `reset()`; constructor `{ registry?, plugins?, sanitize?, onPatch?(patches, nodes) }`.
38
+ - `StreamRouter` — demultiplex one stream into named channels: `.channel(name, sink)`, `.on(name, cb)`, `.feed(source)`.
39
+ - `CardRegistry` — `register(def)`, `parse(type, rawJson)`, `getRender(type)`, `toPromptSpec()`, `toJSONSchema()`.
40
+ - `buildSystemPrompt({ base?, registry?, plugins? })`.
41
+ - Utilities: `parsePartialJSON`, `repairMarkdown`, `sanitizeHtml`, `createParser`, `diffAst`, `collectNodeRenderers`.
42
+ - Types: `ASTNode`, `Patch`, `RenderOutput` (`html | element | card | mount`), `CardDef`, `AIGuiPlugin`, `NodeRenderer`, `RendererOptions`, `JSONSchema`.
43
+
44
+ See the [root README](../../README.md) for the full picture.
package/dist/index.cjs ADDED
@@ -0,0 +1,645 @@
1
+ "use strict";
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+
24
+ //#endregion
25
+ const dompurify = __toESM(require("dompurify"));
26
+ const markdown_it = __toESM(require("markdown-it"));
27
+
28
+ //#region src/partial-json.ts
29
+ /**
30
+ * Parse a possibly-incomplete JSON string, as produced by a streaming source.
31
+ *
32
+ * If the input is valid JSON, it is returned with `complete: true`. Otherwise
33
+ * the parser attempts to repair the fragment by dropping any trailing
34
+ * incomplete token (a dangling key, colon, or comma) and auto-closing unclosed
35
+ * strings, objects, and arrays, returning whatever data has arrived so far with
36
+ * `complete: false`. Unparseable input yields `{ data: undefined, complete: false }`
37
+ * and never throws.
38
+ */
39
+ function parsePartialJSON(input) {
40
+ const str = input.trim();
41
+ if (str === "") return {
42
+ data: void 0,
43
+ complete: false
44
+ };
45
+ try {
46
+ return {
47
+ data: JSON.parse(str),
48
+ complete: true
49
+ };
50
+ } catch {}
51
+ for (const candidate of repair(str)) try {
52
+ return {
53
+ data: JSON.parse(candidate),
54
+ complete: false
55
+ };
56
+ } catch {}
57
+ return {
58
+ data: void 0,
59
+ complete: false
60
+ };
61
+ }
62
+ const LITERAL_CHAR = /[0-9a-zA-Z+.\-]/;
63
+ const WHITESPACE = /\s/;
64
+ function repair(str) {
65
+ const stack = [];
66
+ let inString = false;
67
+ let escaped = false;
68
+ let stringIsKey = false;
69
+ let inLiteral = false;
70
+ let mode = "value";
71
+ let lastGoodEnd = -1;
72
+ for (let i = 0; i < str.length; i++) {
73
+ const ch = str[i];
74
+ if (inString) {
75
+ if (escaped) escaped = false;
76
+ else if (ch === "\\") escaped = true;
77
+ else if (ch === "\"") {
78
+ inString = false;
79
+ if (stringIsKey) mode = "objColon";
80
+ else {
81
+ mode = "afterValue";
82
+ lastGoodEnd = i + 1;
83
+ }
84
+ }
85
+ continue;
86
+ }
87
+ if (inLiteral) {
88
+ if (LITERAL_CHAR.test(ch)) continue;
89
+ inLiteral = false;
90
+ mode = "afterValue";
91
+ lastGoodEnd = i;
92
+ }
93
+ if (WHITESPACE.test(ch)) continue;
94
+ if (ch === "}" || ch === "]") {
95
+ stack.pop();
96
+ mode = "afterValue";
97
+ lastGoodEnd = i + 1;
98
+ continue;
99
+ }
100
+ switch (mode) {
101
+ case "value":
102
+ if (ch === "{") {
103
+ stack.push("obj");
104
+ mode = "objKey";
105
+ lastGoodEnd = i + 1;
106
+ } else if (ch === "[") {
107
+ stack.push("arr");
108
+ mode = "value";
109
+ lastGoodEnd = i + 1;
110
+ } else if (ch === "\"") {
111
+ inString = true;
112
+ stringIsKey = false;
113
+ } else inLiteral = true;
114
+ break;
115
+ case "objKey":
116
+ if (ch === "\"") {
117
+ inString = true;
118
+ stringIsKey = true;
119
+ }
120
+ break;
121
+ case "objColon":
122
+ if (ch === ":") mode = "value";
123
+ break;
124
+ case "afterValue":
125
+ if (ch === ",") mode = stack[stack.length - 1] === "obj" ? "objKey" : "value";
126
+ break;
127
+ }
128
+ }
129
+ if (inString && !stringIsKey) return [closeContainers(str + "\"", stack)];
130
+ const fallback = lastGoodEnd === -1 ? [] : [closeContainers(str.slice(0, lastGoodEnd), stack)];
131
+ if (inLiteral) return [closeContainers(str, stack), ...fallback];
132
+ return fallback;
133
+ }
134
+ function closeContainers(body, stack) {
135
+ let out = body.replace(/,\s*$/, "");
136
+ for (let i = stack.length - 1; i >= 0; i--) out += stack[i] === "obj" ? "}" : "]";
137
+ return out;
138
+ }
139
+
140
+ //#endregion
141
+ //#region src/repair-markdown.ts
142
+ /**
143
+ * Temporarily repair half-finished markdown so a partial streaming buffer
144
+ * renders smoothly. Operates on a copy of the buffer and returns a new string;
145
+ * it never mutates the input. Pure function, no dependencies.
146
+ *
147
+ * The repairs are intentionally conservative: only unambiguous unclosed
148
+ * inline/fence syntax is completed. Ambiguous constructs (e.g. a dangling
149
+ * link text `[docs`) are left untouched to avoid guessing wrong.
150
+ */
151
+ function repairMarkdown(buffer) {
152
+ let out = buffer;
153
+ const fenceCount = (out.match(/^```/gm) ?? []).length;
154
+ if (fenceCount % 2 === 1) {
155
+ if (!out.endsWith("\n")) out += "\n";
156
+ out += "```";
157
+ return out;
158
+ }
159
+ const tick = (out.match(/`/g) ?? []).length;
160
+ if (tick % 2 === 1) out += "`";
161
+ const bold = (out.match(/\*\*/g) ?? []).length;
162
+ if (bold % 2 === 1) out += "**";
163
+ const strike = (out.match(/~~/g) ?? []).length;
164
+ if (strike % 2 === 1) out += "~~";
165
+ return out;
166
+ }
167
+
168
+ //#endregion
169
+ //#region src/sanitizer.ts
170
+ /** Escape the HTML-significant characters so a string renders as inert text. */
171
+ function escapeHtml(html) {
172
+ return html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
173
+ }
174
+ /**
175
+ * Sanitize an HTML string, stripping scripts and unsafe attributes.
176
+ *
177
+ * When a DOM is available (browser, or Node with jsdom) DOMPurify is used. In a
178
+ * bare Node environment without a global `window`, DOMPurify cannot run, so we
179
+ * fall back to escaping the HTML-significant characters. This never emits raw
180
+ * markup and never throws.
181
+ */
182
+ function sanitizeHtml(html) {
183
+ if (typeof window === "undefined") return escapeHtml(html);
184
+ return dompurify.default.sanitize(html);
185
+ }
186
+
187
+ //#endregion
188
+ //#region src/card-registry.ts
189
+ var CardRegistry = class {
190
+ cards = new Map();
191
+ register(def) {
192
+ this.cards.set(def.type, def);
193
+ }
194
+ has(type) {
195
+ return this.cards.has(type);
196
+ }
197
+ getRender(type) {
198
+ return this.cards.get(type)?.render;
199
+ }
200
+ parse(type, rawJson) {
201
+ const def = this.cards.get(type);
202
+ const { data, complete } = parsePartialJSON(rawJson);
203
+ if (!def) return {
204
+ data,
205
+ complete,
206
+ valid: false
207
+ };
208
+ const valid = complete && this.validate(def, data);
209
+ return {
210
+ data,
211
+ complete,
212
+ valid
213
+ };
214
+ }
215
+ validate(def, data) {
216
+ if (def.validate) return def.validate(data);
217
+ if (def.schema) return validateSchema(def.schema, data);
218
+ return true;
219
+ }
220
+ toPromptSpec() {
221
+ const lines = ["You can output cards. Format: a ```card:<type> fenced block with JSON inside. Available cards:"];
222
+ for (const def of this.cards.values()) {
223
+ lines.push(`- \`card:${def.type}\`: ${def.description}`);
224
+ if (def.schema?.properties) {
225
+ const fields = Object.entries(def.schema.properties).map(([k, v]) => `${k}(${v.type ?? "any"})`).join(", ");
226
+ lines.push(` fields: ${fields}`);
227
+ }
228
+ if (def.example !== void 0) lines.push(` example: ${JSON.stringify(def.example)}`);
229
+ }
230
+ return lines.join("\n");
231
+ }
232
+ toJSONSchema() {
233
+ const properties = {};
234
+ for (const def of this.cards.values()) if (def.schema) properties[def.type] = def.schema;
235
+ return {
236
+ type: "object",
237
+ properties
238
+ };
239
+ }
240
+ };
241
+ /** Minimal JSON Schema validation: covers type / required / properties, enough for cards. */
242
+ function validateSchema(schema, data) {
243
+ if (schema.type === "object") {
244
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
245
+ const obj = data;
246
+ for (const req of schema.required ?? []) if (!(req in obj)) return false;
247
+ for (const [key, sub] of Object.entries(schema.properties ?? {})) if (key in obj && !validateSchema(sub, obj[key])) return false;
248
+ return true;
249
+ }
250
+ if (schema.type === "array") {
251
+ if (!Array.isArray(data)) return false;
252
+ return schema.items ? data.every((d) => validateSchema(schema.items, d)) : true;
253
+ }
254
+ if (schema.type === "string") return typeof data === "string";
255
+ if (schema.type === "number") return typeof data === "number";
256
+ if (schema.type === "boolean") return typeof data === "boolean";
257
+ return true;
258
+ }
259
+
260
+ //#endregion
261
+ //#region src/plugins.ts
262
+ /** Merge every plugin's `nodeRenderers` into a single map (later plugins win). */
263
+ function collectNodeRenderers(plugins = []) {
264
+ const map = {};
265
+ for (const p of plugins) for (const [k, v] of Object.entries(p.nodeRenderers ?? {})) map[k] = v;
266
+ return map;
267
+ }
268
+ /** The set of node types claimed by the given plugins. */
269
+ function pluginNodeTypes(plugins = []) {
270
+ return new Set(Object.keys(collectNodeRenderers(plugins)));
271
+ }
272
+
273
+ //#endregion
274
+ //#region src/parser.ts
275
+ /** Build a parser that turns markdown source into a flat list of ASTNodes. */
276
+ function createParser(options = {}) {
277
+ const md = new markdown_it.default({
278
+ html: true,
279
+ linkify: true
280
+ });
281
+ options.configureMd?.(md);
282
+ for (const plugin of options.plugins ?? []) plugin.extendParser?.(md);
283
+ const pluginTypes = pluginNodeTypes(options.plugins);
284
+ return (src) => {
285
+ const tokens = md.parse(src, {});
286
+ const nodes = [];
287
+ let index = 0;
288
+ for (let i = 0; i < tokens.length; i++) {
289
+ const t = tokens[i];
290
+ if (t.type === "fence") {
291
+ const info = t.info.trim();
292
+ if (info.startsWith("card:") && options.registry) {
293
+ const cardType = info.slice(5);
294
+ const res = options.registry.parse(cardType, t.content);
295
+ nodes.push({
296
+ key: `${index++}:card`,
297
+ type: "card",
298
+ card: {
299
+ type: cardType,
300
+ data: res.data,
301
+ complete: res.complete,
302
+ valid: res.valid
303
+ }
304
+ });
305
+ } else if (pluginTypes.has(info)) nodes.push({
306
+ key: `${index++}:${info}`,
307
+ type: info,
308
+ content: t.content,
309
+ attrs: { info }
310
+ });
311
+ else nodes.push({
312
+ key: `${index++}:code`,
313
+ type: "code",
314
+ tag: "code",
315
+ attrs: info ? { lang: info } : void 0,
316
+ content: t.content
317
+ });
318
+ continue;
319
+ }
320
+ if (t.type === "hr") {
321
+ nodes.push({
322
+ key: `${index++}:hr`,
323
+ type: "hr",
324
+ tag: "hr"
325
+ });
326
+ continue;
327
+ }
328
+ if (t.type === "code_block") {
329
+ nodes.push({
330
+ key: `${index++}:code`,
331
+ type: "code",
332
+ tag: "code",
333
+ content: t.content
334
+ });
335
+ continue;
336
+ }
337
+ if (t.type === "html_block") {
338
+ nodes.push({
339
+ key: `${index++}:html`,
340
+ type: "html",
341
+ content: t.content
342
+ });
343
+ continue;
344
+ }
345
+ if (t.type === "heading_open") {
346
+ const inline = tokens[i + 1];
347
+ const raw = inline?.content ?? "";
348
+ nodes.push({
349
+ key: `${index++}:heading`,
350
+ type: "heading",
351
+ tag: t.tag,
352
+ content: raw,
353
+ html: md.renderInline(raw)
354
+ });
355
+ i += 2;
356
+ continue;
357
+ }
358
+ if (t.type === "paragraph_open") {
359
+ const inline = tokens[i + 1];
360
+ const raw = inline?.content ?? "";
361
+ nodes.push({
362
+ key: `${index++}:paragraph`,
363
+ type: "paragraph",
364
+ tag: "p",
365
+ content: raw,
366
+ html: md.renderInline(raw)
367
+ });
368
+ i += 2;
369
+ continue;
370
+ }
371
+ if (t.type.endsWith("_open") && t.level === 0) {
372
+ const closeType = t.type.replace("_open", "_close");
373
+ let j = i;
374
+ let depth = 0;
375
+ for (; j < tokens.length; j++) {
376
+ if (tokens[j].type === t.type) depth++;
377
+ if (tokens[j].type === closeType) {
378
+ depth--;
379
+ if (depth === 0) break;
380
+ }
381
+ }
382
+ const slice = tokens.slice(i, j + 1);
383
+ nodes.push({
384
+ key: `${index++}:${t.type}`,
385
+ type: "html",
386
+ content: md.renderer.render(slice, md.options, {})
387
+ });
388
+ i = j;
389
+ continue;
390
+ }
391
+ if (t.block && t.level === 0) {
392
+ nodes.push({
393
+ key: `${index++}:${t.type}`,
394
+ type: "html",
395
+ content: md.renderer.render([t], md.options, {})
396
+ });
397
+ continue;
398
+ }
399
+ }
400
+ return nodes;
401
+ };
402
+ }
403
+
404
+ //#endregion
405
+ //#region src/diff.ts
406
+ /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
407
+ function diffAst(prev, next) {
408
+ const patches = [];
409
+ const prevByKey = new Map(prev.map((n) => [n.key, n]));
410
+ const nextKeys = new Set(next.map((n) => n.key));
411
+ next.forEach((node, index) => {
412
+ const old = prevByKey.get(node.key);
413
+ if (!old) patches.push({
414
+ op: "insert",
415
+ index,
416
+ node
417
+ });
418
+ else if (!nodeEqual(old, node)) patches.push({
419
+ op: "update",
420
+ key: node.key,
421
+ node
422
+ });
423
+ });
424
+ for (const node of prev) if (!nextKeys.has(node.key)) patches.push({
425
+ op: "remove",
426
+ key: node.key
427
+ });
428
+ return patches;
429
+ }
430
+ function nodeEqual(a, b) {
431
+ return JSON.stringify(a) === JSON.stringify(b);
432
+ }
433
+
434
+ //#endregion
435
+ //#region src/renderer.ts
436
+ /**
437
+ * Streaming render orchestrator: accumulate incoming markdown chunks, repair the
438
+ * partial buffer, parse it into an AST, diff against the previous AST, and emit
439
+ * the resulting patches via `onPatch`.
440
+ */
441
+ var Renderer = class {
442
+ buffer = "";
443
+ prevAst = [];
444
+ parse;
445
+ options;
446
+ sanitize;
447
+ constructor(options = {}) {
448
+ this.options = options;
449
+ this.sanitize = options.sanitize !== false;
450
+ if (options.registry) for (const plugin of options.plugins ?? []) for (const card of plugin.cards ?? []) options.registry.register(card);
451
+ this.parse = createParser({
452
+ registry: options.registry,
453
+ plugins: options.plugins
454
+ });
455
+ }
456
+ push(chunk) {
457
+ this.buffer += chunk;
458
+ this.render();
459
+ }
460
+ async feed(source) {
461
+ if (Symbol.asyncIterator in source) {
462
+ for await (const chunk of source) this.push(chunk);
463
+ return;
464
+ }
465
+ const reader = source.getReader();
466
+ for (;;) {
467
+ const { done, value } = await reader.read();
468
+ if (done) break;
469
+ if (value != null) this.push(value);
470
+ }
471
+ }
472
+ reset() {
473
+ this.buffer = "";
474
+ this.prevAst = [];
475
+ }
476
+ render() {
477
+ const repaired = repairMarkdown(this.buffer);
478
+ const nextAst = this.parse(repaired);
479
+ if (this.sanitize) sanitizeNodes(nextAst);
480
+ const patches = diffAst(this.prevAst, nextAst);
481
+ this.prevAst = nextAst;
482
+ if (patches.length > 0) this.options.onPatch?.(patches, nextAst);
483
+ }
484
+ };
485
+ /**
486
+ * Recursively sanitize node markup in place: the content of `html` nodes and
487
+ * the rendered inline `html` field carried by any node.
488
+ */
489
+ function sanitizeNodes(nodes) {
490
+ for (const node of nodes) {
491
+ if (node.type === "html" && typeof node.content === "string") node.content = sanitizeHtml(node.content);
492
+ if (node.html) node.html = sanitizeHtml(node.html);
493
+ if (node.children) sanitizeNodes(node.children);
494
+ }
495
+ }
496
+
497
+ //#endregion
498
+ //#region src/stream-router.ts
499
+ const DEFAULT_CHANNEL = "content";
500
+ /**
501
+ * Demultiplexes one incoming stream into multiple named channels so different
502
+ * UI regions can update independently (e.g. `progress` + `content` from a single
503
+ * SSE). Framing is auto-detected per line and supports:
504
+ *
505
+ * 1. JSON envelope: `{"ch":"<channel>","delta":"text"}` (a text delta) or
506
+ * `{"ch":"<channel>","data":<any>}` (a structured value). May appear bare or
507
+ * after a `data: ` prefix.
508
+ * 2. SSE `event: <name>` sets the channel for the following `data:` line(s).
509
+ * 3. A plain `data: <text>` line with no `ch` and no preceding `event:` is a
510
+ * text delta on the default `content` channel.
511
+ */
512
+ var StreamRouter = class {
513
+ sinks = new Map();
514
+ handlers = new Map();
515
+ /** Bind a text-stream sink to a channel: text deltas call `sink.push(delta)`. */
516
+ channel(name, sink) {
517
+ this.sinks.set(name, sink);
518
+ return this;
519
+ }
520
+ /** Bind a handler to a channel: data values (and deltas as strings) call it. */
521
+ on(name, handler) {
522
+ this.handlers.set(name, handler);
523
+ return this;
524
+ }
525
+ /**
526
+ * Consume a stream to completion, dispatching each parsed line to its channel.
527
+ * Accepts an async iterable of strings, or a `ReadableStream` of either strings
528
+ * or `Uint8Array` bytes (decoded as UTF-8).
529
+ */
530
+ async feed(source) {
531
+ let buffer = "";
532
+ let currentEvent;
533
+ const consume = (chunk) => {
534
+ buffer += chunk;
535
+ let index;
536
+ while ((index = buffer.indexOf("\n")) !== -1) {
537
+ const line = buffer.slice(0, index);
538
+ buffer = buffer.slice(index + 1);
539
+ currentEvent = this.processLine(line, currentEvent);
540
+ }
541
+ };
542
+ if ("getReader" in source && typeof source.getReader === "function") {
543
+ const reader = source.getReader();
544
+ const decoder = new TextDecoder();
545
+ for (;;) {
546
+ const { done, value } = await reader.read();
547
+ if (done) break;
548
+ if (value == null) continue;
549
+ consume(typeof value === "string" ? value : decoder.decode(value, { stream: true }));
550
+ }
551
+ const tail = decoder.decode();
552
+ if (tail) consume(tail);
553
+ } else for await (const chunk of source) consume(chunk);
554
+ if (buffer.length > 0) this.processLine(buffer, currentEvent);
555
+ }
556
+ /** Process a single raw line; returns the (possibly updated) current event. */
557
+ processLine(rawLine, currentEvent) {
558
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
559
+ if (line.trim() === "") return void 0;
560
+ if (line.startsWith("event:")) return line.slice(6).trim();
561
+ let payload = line;
562
+ if (payload.startsWith("data:")) {
563
+ payload = payload.slice(5);
564
+ if (payload.startsWith(" ")) payload = payload.slice(1);
565
+ }
566
+ if (payload.startsWith("{")) {
567
+ const parsed = tryParseJson(payload);
568
+ if (parsed !== void 0 && isRecord(parsed) && typeof parsed.ch === "string") {
569
+ if ("delta" in parsed) this.routeDelta(parsed.ch, String(parsed.delta));
570
+ else this.routeData(parsed.ch, parsed.data);
571
+ return currentEvent;
572
+ }
573
+ if (parsed !== void 0) {
574
+ this.routeData(currentEvent ?? DEFAULT_CHANNEL, parsed);
575
+ return currentEvent;
576
+ }
577
+ }
578
+ if (currentEvent !== void 0) {
579
+ const parsed = tryParseJson(payload);
580
+ this.routeData(currentEvent, parsed !== void 0 ? parsed : payload);
581
+ return currentEvent;
582
+ }
583
+ this.routeDelta(DEFAULT_CHANNEL, payload);
584
+ return currentEvent;
585
+ }
586
+ /** Route a text delta: to a bound sink and/or an on() handler (either/both). */
587
+ routeDelta(channel, text) {
588
+ const sink = this.sinks.get(channel);
589
+ const handler = this.handlers.get(channel);
590
+ if (sink) sink.push(text);
591
+ if (handler) handler(text);
592
+ }
593
+ /** Route a structured value: to the handler, or a string value to a lone sink. */
594
+ routeData(channel, value) {
595
+ const handler = this.handlers.get(channel);
596
+ if (handler) {
597
+ handler(value);
598
+ return;
599
+ }
600
+ const sink = this.sinks.get(channel);
601
+ if (sink && typeof value === "string") sink.push(value);
602
+ }
603
+ };
604
+ function tryParseJson(text) {
605
+ try {
606
+ return JSON.parse(text);
607
+ } catch {
608
+ return void 0;
609
+ }
610
+ }
611
+ function isRecord(value) {
612
+ return typeof value === "object" && value !== null;
613
+ }
614
+
615
+ //#endregion
616
+ //#region src/build-system-prompt.ts
617
+ /**
618
+ * Assembles the LLM system-prompt guidance: an optional base, the registered
619
+ * cards' spec, and every plugin's promptSpec. Consumers prepend this to their
620
+ * own system prompt so the model knows which fenced blocks it may emit.
621
+ */
622
+ function buildSystemPrompt(options = {}) {
623
+ const parts = [];
624
+ if (options.base) parts.push(options.base);
625
+ const cardSpec = options.registry?.toPromptSpec();
626
+ if (options.registry && hasCards(options.registry) && cardSpec) parts.push(cardSpec);
627
+ for (const p of options.plugins ?? []) if (p.promptSpec) parts.push(p.promptSpec);
628
+ return parts.join("\n\n");
629
+ }
630
+ function hasCards(registry) {
631
+ return registry.toPromptSpec().includes("card:");
632
+ }
633
+
634
+ //#endregion
635
+ exports.CardRegistry = CardRegistry
636
+ exports.Renderer = Renderer
637
+ exports.StreamRouter = StreamRouter
638
+ exports.buildSystemPrompt = buildSystemPrompt
639
+ exports.collectNodeRenderers = collectNodeRenderers
640
+ exports.createParser = createParser
641
+ exports.diffAst = diffAst
642
+ exports.parsePartialJSON = parsePartialJSON
643
+ exports.pluginNodeTypes = pluginNodeTypes
644
+ exports.repairMarkdown = repairMarkdown
645
+ exports.sanitizeHtml = sanitizeHtml