@jigs.design/server 0.0.0-stage → 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/dist/chat.js ADDED
@@ -0,0 +1,711 @@
1
+ import { MARKDOWN_ELEMENTS } from "./compile.js";
2
+ import { belowPrefix, pragmaLine, splitPragma, withPragma } from "./pragma.js";
3
+ import { reorderDeclarations } from "./rewrite.js";
4
+ import { applyChecked, editResult, runSession } from "./session.js";
5
+ import { applyEdits, isEditReply, parseEdits } from "@jigs.design/compiler";
6
+ import { editRepairText, needsRepair, repairText } from "@jigs.design/prompt";
7
+ //#region src/chat.ts
8
+ /**
9
+ * The most blocks one reply shows. Each is a sandboxed program in the
10
+ * browser, so a reply asked for dozens of UIs stops here; later blocks are
11
+ * done with an error and no program.
12
+ */
13
+ const MAX_BLOCKS = 8;
14
+ /**
15
+ * The most blocks of one reply that get a repair, each a model call (or
16
+ * `maxRepairs` of them). Later blocks with errors are done as they are.
17
+ */
18
+ const MAX_BLOCK_REPAIRS = 3;
19
+ /**
20
+ * Lines after unfenced Pickle that wait to see whether more Pickle follows
21
+ * (a heading or a blank line inside a document), before they show as prose.
22
+ */
23
+ const MAX_HELD_LINES = 6;
24
+ /**
25
+ * Tags Markdown and HTML also use, so a prose line may start with one, as in
26
+ * "<code>npm i</code> installs it." Such a line is Pickle only when it is all
27
+ * markup, ending in a tag.
28
+ */
29
+ const PROSE_TAGS = /* @__PURE__ */ new Set([
30
+ ...MARKDOWN_ELEMENTS,
31
+ "a",
32
+ "abbr",
33
+ "b",
34
+ "br",
35
+ "button",
36
+ "del",
37
+ "details",
38
+ "em",
39
+ "i",
40
+ "img",
41
+ "input",
42
+ "kbd",
43
+ "label",
44
+ "mark",
45
+ "small",
46
+ "span",
47
+ "strong",
48
+ "sub",
49
+ "summary",
50
+ "sup",
51
+ "table",
52
+ "u"
53
+ ]);
54
+ /** A line that opens a pickle block: ```pickle, then an optional id. */
55
+ const PICKLE_OPEN = /^( {0,3})`{3}[ \t]*pickle(?:[ \t]+([^`]*?))?[ \t]*$/i;
56
+ /** A line that closes a pickle block. */
57
+ const BLOCK_CLOSE = /^ {0,3}`{3,}[ \t]*$/;
58
+ /**
59
+ * A fence with text after it, inside a block. Pickle never holds a fence, so
60
+ * the model closed the block and ran its prose onto the same line.
61
+ */
62
+ const BLOCK_CLOSE_RUN_ON = /^ {0,3}`{3,}(.+)$/;
63
+ /** Tag names of tool-call markup, held back until the line shows what it is. */
64
+ const TOOL_TAGS = [
65
+ "function_calls",
66
+ "invoke",
67
+ "parameter",
68
+ "tool_call",
69
+ "tool_use"
70
+ ];
71
+ const TOOL_NAME = String.raw`(?:${TOOL_TAGS.join("|")})\b`;
72
+ /** One tool tag, or an opening and a closing tag with only text between them. */
73
+ const TOOL_PIECE = String.raw`(?:<\/?${TOOL_NAME}[^>]*>|<${TOOL_NAME}[^>]*>[^<]*<\/${TOOL_NAME}[^>]*>)`;
74
+ /**
75
+ * A prose line that is only tool-call markup, which some models leak into
76
+ * their text instead of calling a tool. It would show as text. Text outside
77
+ * a tag pair makes it prose: "`<invoke>` wraps each call."
78
+ */
79
+ const TOOL_MARKUP = new RegExp(String.raw`^\s*${TOOL_PIECE}(?:\s*${TOOL_PIECE})*\s*$`);
80
+ /** A line that opens any other fenced code block. */
81
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
82
+ /** A partial line that may still turn into a fence line. */
83
+ const MAYBE_FENCE = /^ {0,3}(?:`{0,2}|~{0,2})$/;
84
+ /** A partial line that is a fence line, whose end decides what it opens. */
85
+ const FENCE_START = /^ {0,3}(?:`{3}|~{3})/;
86
+ const AUTO_ID = /^pickle-(\d+)$/;
87
+ const VALID_ID = /^[a-z][a-z0-9-]*$/;
88
+ /**
89
+ * A block as the model wrote it, for the model's history: a fenced `pickle`
90
+ * block with its id, and its source without the pragma line.
91
+ */
92
+ function chatBlockText(block) {
93
+ const body = splitPragma(block.source).body.replace(/\n+$/, "");
94
+ return `\`\`\`pickle ${block.id}\n${body}\n\`\`\``;
95
+ }
96
+ /** The block's id from the fence's info string, made kebab-case. */
97
+ function slug(info) {
98
+ const joined = info.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word !== "").join("-");
99
+ if (joined === "") return "";
100
+ return /^[a-z]/.test(joined) ? joined : `pickle-${joined}`;
101
+ }
102
+ /** Whether `line` closes a code fence opened with `fence`. */
103
+ function closesFence(line, fence) {
104
+ const trimmed = line.trimStart();
105
+ if (line.length - trimmed.length > 3) return false;
106
+ let run = 0;
107
+ while (run < trimmed.length && trimmed[run] === fence[0]) run++;
108
+ return run >= fence.length && trimmed.slice(run).trim() === "";
109
+ }
110
+ /** `lines` without trailing blank lines, joined. */
111
+ function bodyOf(lines) {
112
+ let end = lines.length;
113
+ while (end > 0 && (lines[end - 1] ?? "").trim() === "") end--;
114
+ return lines.slice(0, end).join("\n");
115
+ }
116
+ const TAG_LINE = /^ {0,3}<\/?([A-Za-z][\w-]*)/;
117
+ const BODY_LINE = /^ {0,3}\{@body\b/;
118
+ const BODY_PREFIX = "{@body";
119
+ /** How a complete line changes the open-tag depth of a block with no fence. */
120
+ function depthChange(line) {
121
+ const trimmed = line.trim();
122
+ const tag = TAG_LINE.exec(trimmed);
123
+ if (tag === null) return 0;
124
+ if (trimmed.startsWith("</")) return -1;
125
+ const name = tag[1] ?? "";
126
+ return trimmed.endsWith("/>") || trimmed.includes(`</${name}>`) ? 0 : 1;
127
+ }
128
+ /**
129
+ * Splits a chat reply into prose and pickle blocks as it streams. Pure and
130
+ * synchronous: `push` takes any slice of the reply and returns what it
131
+ * completed. Prose comes out as soon as a partial line can no longer be a
132
+ * fence; a block compiles at each committed line and is sent again whenever
133
+ * its program changes.
134
+ */
135
+ function createReplyParser(deps, history) {
136
+ /** Latest body (no pragma) by id. */
137
+ const latest = /* @__PURE__ */ new Map();
138
+ /**
139
+ * The body as this reply wrote it, by id, for a block whose `{@body}`
140
+ * lines the server moved: an edit later in the same reply was written
141
+ * against it, not against the moved source.
142
+ */
143
+ const asWritten = /* @__PURE__ */ new Map();
144
+ /** The version `edits` apply to: the latest, or the one the reply wrote. */
145
+ const baseFor = (id, edits) => {
146
+ const base = latest.get(id);
147
+ const written = asWritten.get(id);
148
+ return base !== void 0 && written !== void 0 && !applyEdits(base, edits).ok && applyEdits(written, edits).ok ? written : base;
149
+ };
150
+ const versions = /* @__PURE__ */ new Map();
151
+ /** By block key, so a copy of the block (spread, cloned, serialized) finds its plan. */
152
+ const plans = /* @__PURE__ */ new Map();
153
+ /** By block key, in the order the blocks closed. */
154
+ const results = /* @__PURE__ */ new Map();
155
+ let autoId = 0;
156
+ let opened = 0;
157
+ let repairsLeft = MAX_BLOCK_REPAIRS;
158
+ for (const entry of history) {
159
+ if (!VALID_ID.test(entry.id)) continue;
160
+ latest.set(entry.id, splitPragma(entry.source).body);
161
+ versions.set(entry.id, (versions.get(entry.id) ?? 0) + 1);
162
+ const auto = AUTO_ID.exec(entry.id);
163
+ if (auto !== null) autoId = Math.max(autoId, Number(auto[1]));
164
+ }
165
+ const empty = deps.compile("");
166
+ let mode = { kind: "prose" };
167
+ /** The current prose line, while it may still be a fence line. */
168
+ let held = "";
169
+ /** Whether the current prose line is already known to be text. */
170
+ let lineIsText = false;
171
+ let out = [];
172
+ /** Whether a complete prose line is Pickle the model forgot to fence. */
173
+ const strayLine = (value) => {
174
+ if (BODY_LINE.test(value)) return true;
175
+ const name = TAG_LINE.exec(value)?.[1] ?? "";
176
+ if (!deps.tags.has(name)) return false;
177
+ return !PROSE_TAGS.has(name) || value.trimEnd().endsWith(">");
178
+ };
179
+ /** Whether a line keeps unfenced Pickle going: a stray line or a closing tag. */
180
+ const pickleLine = (value) => value.trim() !== "" && (strayLine(value) || /^\s*<\//.test(value));
181
+ /** Whether a partial prose line may still become a stray line. */
182
+ const mayBeStray = (partial) => {
183
+ const trimmed = partial.trimStart();
184
+ if (partial.length - trimmed.length > 3) return false;
185
+ if (trimmed === "") return false;
186
+ if (trimmed === "<" || trimmed === "</") return true;
187
+ if (trimmed.startsWith("{")) return BODY_PREFIX.startsWith(trimmed) || BODY_LINE.test(trimmed);
188
+ const tag = TAG_LINE.exec(trimmed);
189
+ if (tag === null) return false;
190
+ const name = tag[1] ?? "";
191
+ if (tag[0].length === trimmed.length) {
192
+ for (const known of [...deps.tags, ...TOOL_TAGS]) if (known.startsWith(name)) return true;
193
+ return false;
194
+ }
195
+ return deps.tags.has(name) || TOOL_TAGS.includes(name);
196
+ };
197
+ const text = (value) => {
198
+ if (value === "") return;
199
+ const last = out.at(-1);
200
+ if (last?.type === "text") out[out.length - 1] = {
201
+ text: last.text + value,
202
+ type: "text"
203
+ };
204
+ else out.push({
205
+ text: value,
206
+ type: "text"
207
+ });
208
+ };
209
+ const blockOf = (open, body, compiled, status, error) => {
210
+ const block = {
211
+ diagnostics: belowPrefix(compiled.diagnostics, pragmaLine(deps.catalogVersion)),
212
+ id: open.id,
213
+ key: `${open.id}@${open.version}`,
214
+ program: compiled.program,
215
+ source: withPragma(body, deps.catalogVersion),
216
+ status,
217
+ version: open.version
218
+ };
219
+ if (error !== void 0) block.error = error;
220
+ return block;
221
+ };
222
+ const emit = (block) => {
223
+ out.push({
224
+ block,
225
+ type: "block"
226
+ });
227
+ };
228
+ /**
229
+ * Emits a block that closed, and records what it came to. A block whose
230
+ * edit did not apply shows the previous version, whose diagnostics are
231
+ * not the reply's, so its result has none. `advice` is numbered by the
232
+ * body's lines, as the compile reported it.
233
+ */
234
+ const settle = (open, block, edits, advice = [], rewrite) => {
235
+ const result = {
236
+ advice: belowPrefix(advice, pragmaLine(deps.catalogVersion)),
237
+ diagnostics: edits?.applied === false ? [] : block.diagnostics,
238
+ fenced: !open.implicit,
239
+ id: block.id,
240
+ key: block.key,
241
+ repairs: [],
242
+ status: block.status === "repairing" ? "repairing" : "done"
243
+ };
244
+ if (edits !== void 0) result.edits = edits;
245
+ if (block.error !== void 0) result.error = block.error;
246
+ if (rewrite !== void 0) result.rewrites = [rewrite];
247
+ results.set(block.key, result);
248
+ emit(block);
249
+ };
250
+ const open = (info, indent, implicit = false) => {
251
+ const named = slug(info ?? "");
252
+ const id = named === "" ? `pickle-${++autoId}` : named;
253
+ const version = (versions.get(id) ?? 0) + 1;
254
+ versions.set(id, version);
255
+ opened++;
256
+ const block = {
257
+ baseShown: false,
258
+ depth: 0,
259
+ dirty: false,
260
+ editsShown: 0,
261
+ id,
262
+ implicit,
263
+ indent,
264
+ lastKey: JSON.stringify(empty.program),
265
+ lastSentAt: Number.NEGATIVE_INFINITY,
266
+ lines: [],
267
+ over: opened > MAX_BLOCKS,
268
+ partial: "",
269
+ version,
270
+ waiting: []
271
+ };
272
+ mode = {
273
+ block,
274
+ kind: "block"
275
+ };
276
+ if (!block.over) emit(blockOf(block, "", empty, "streaming"));
277
+ };
278
+ /**
279
+ * Compiles the committed lines. An edit block shows its base version,
280
+ * then each finished edit over it; edits that do not apply wait for the
281
+ * block to close, which reports them.
282
+ */
283
+ const refresh = (block) => {
284
+ if (block.over) return;
285
+ let body = bodyOf(block.lines);
286
+ const editing = isEditReply(body);
287
+ if (editing === true) {
288
+ const edits = parseEdits(body);
289
+ const base = baseFor(block.id, edits);
290
+ if (base === void 0) return;
291
+ if (!block.baseShown) {
292
+ block.baseShown = true;
293
+ const compiled = deps.compile(base);
294
+ block.lastKey = JSON.stringify(compiled.program);
295
+ emit(blockOf(block, base, compiled, "streaming"));
296
+ }
297
+ const applied = applyEdits(base, edits);
298
+ if (edits.length === block.editsShown || !applied.ok) return;
299
+ block.editsShown = edits.length;
300
+ body = applied.text;
301
+ } else if (editing === null) return;
302
+ const compiled = deps.compile(body);
303
+ const key = JSON.stringify(compiled.program);
304
+ const now = deps.now();
305
+ if (key !== block.lastKey && now - block.lastSentAt >= deps.programIntervalMs) {
306
+ block.lastKey = key;
307
+ block.lastSentAt = now;
308
+ emit(blockOf(block, body, compiled, "streaming"));
309
+ }
310
+ };
311
+ /** Whether a block that closed with a problem gets a repair round. */
312
+ const repairable = () => {
313
+ if (!deps.repairs || repairsLeft === 0) return false;
314
+ repairsLeft--;
315
+ return true;
316
+ };
317
+ const close = (block) => {
318
+ mode = { kind: "prose" };
319
+ if (block.over) {
320
+ settle(block, blockOf(block, "", empty, "done", {
321
+ code: "RATE_LIMITED",
322
+ message: `A reply shows at most ${MAX_BLOCKS} pickles, so this one was left out.`
323
+ }));
324
+ return;
325
+ }
326
+ const written = bodyOf(block.lines);
327
+ let body = written;
328
+ let edits;
329
+ if (isEditReply(written) === true) {
330
+ const parsed = parseEdits(written);
331
+ const base = baseFor(block.id, parsed);
332
+ if (base === void 0) {
333
+ settle(block, blockOf(block, "", empty, "done", {
334
+ code: "NOT_FOUND",
335
+ message: `There is no earlier pickle "${block.id}" to change.`
336
+ }), {
337
+ applied: false,
338
+ blocks: parsed.length
339
+ });
340
+ return;
341
+ }
342
+ const applied = applyChecked(base, parsed, deps.compile);
343
+ edits = editResult(parsed.length, applied);
344
+ if (!applied.ok) {
345
+ const previous = deps.compile(base);
346
+ if (!repairable()) {
347
+ settle(block, blockOf(block, base, previous, "done", {
348
+ code: "INTERNAL",
349
+ message: "The model's edit could not be applied."
350
+ }), edits);
351
+ return;
352
+ }
353
+ const pending = blockOf(block, base, previous, "repairing");
354
+ const { ok: _ok, ...failure } = applied;
355
+ plans.set(pending.key, {
356
+ base,
357
+ failure,
358
+ kind: "edit"
359
+ });
360
+ settle(block, pending, edits);
361
+ return;
362
+ }
363
+ body = applied.text;
364
+ }
365
+ let compiled = deps.compile(body);
366
+ const reordered = reorderDeclarations(body, compiled, deps.compile);
367
+ if (reordered === null) asWritten.delete(block.id);
368
+ else {
369
+ asWritten.set(block.id, body);
370
+ ({body, compiled} = reordered);
371
+ }
372
+ latest.set(block.id, body);
373
+ const advice = compiled.advise("chat");
374
+ const chosen = advice.filter((entry) => deps.designRepair.has(entry.code));
375
+ if ((needsRepair(compiled.diagnostics) || chosen.length > 0) && repairable()) {
376
+ const pending = blockOf(block, body, compiled, "repairing");
377
+ plans.set(pending.key, {
378
+ advice: chosen,
379
+ body,
380
+ diagnostics: compiled.diagnostics,
381
+ kind: "errors"
382
+ });
383
+ settle(block, pending, edits, advice, reordered?.rewrite);
384
+ return;
385
+ }
386
+ settle(block, blockOf(block, body, compiled, "done"), edits, advice, reordered?.rewrite);
387
+ };
388
+ /** One complete line, without its newline. */
389
+ const line = (raw) => {
390
+ const value = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
391
+ if (mode.kind === "block") {
392
+ const { block } = mode;
393
+ if (block.implicit) {
394
+ if (block.depth > 0 || pickleLine(value)) {
395
+ block.lines.push(...block.waiting);
396
+ block.waiting = [];
397
+ block.depth = Math.max(0, block.depth + depthChange(value));
398
+ block.lines.push(value);
399
+ block.dirty = true;
400
+ return;
401
+ }
402
+ if (FENCE_OPEN.exec(value) === null && block.waiting.length < MAX_HELD_LINES) {
403
+ block.waiting.push(value);
404
+ return;
405
+ }
406
+ finishImplicit(block);
407
+ line(raw);
408
+ return;
409
+ }
410
+ if (BLOCK_CLOSE.test(value)) {
411
+ close(block);
412
+ return;
413
+ }
414
+ const runOn = BLOCK_CLOSE_RUN_ON.exec(value);
415
+ if (runOn !== null) {
416
+ close(block);
417
+ line(PICKLE_OPEN.test(value) ? value : (runOn[1] ?? "").trimStart());
418
+ return;
419
+ }
420
+ let cut = 0;
421
+ while (cut < block.indent && value[cut] === " ") cut++;
422
+ block.lines.push(value.slice(cut));
423
+ block.dirty = true;
424
+ return;
425
+ }
426
+ if (lineIsText) {
427
+ text("\n");
428
+ return;
429
+ }
430
+ if (mode.kind === "code") {
431
+ if (closesFence(value, mode.fence)) mode = { kind: "prose" };
432
+ text(`${raw}\n`);
433
+ return;
434
+ }
435
+ const pickle = PICKLE_OPEN.exec(value);
436
+ if (pickle !== null) {
437
+ open(pickle[2], pickle[1]?.length ?? 0);
438
+ return;
439
+ }
440
+ if (TOOL_MARKUP.test(value)) return;
441
+ if (strayLine(value)) {
442
+ open(void 0, 0, true);
443
+ line(raw);
444
+ return;
445
+ }
446
+ const fence = FENCE_OPEN.exec(value);
447
+ if (fence !== null && !(fence[1]?.startsWith("`") && fence[2]?.includes("`"))) mode = {
448
+ fence: fence[1] ?? "```",
449
+ kind: "code"
450
+ };
451
+ text(`${raw}\n`);
452
+ };
453
+ /**
454
+ * Closes a block; for one with no fence, the lines still waiting after
455
+ * its Pickle are prose again.
456
+ */
457
+ function finishImplicit(block) {
458
+ const waiting = block.waiting;
459
+ block.waiting = [];
460
+ close(block);
461
+ for (const value of waiting) line(value);
462
+ }
463
+ const push = (delta) => {
464
+ out = [];
465
+ let rest = delta;
466
+ while (rest !== "") {
467
+ const newline = rest.indexOf("\n");
468
+ const piece = newline === -1 ? rest : rest.slice(0, newline);
469
+ rest = newline === -1 ? "" : rest.slice(newline + 1);
470
+ if (mode.kind === "block") {
471
+ const { block } = mode;
472
+ block.partial += piece;
473
+ if (newline !== -1) {
474
+ const whole = block.partial;
475
+ block.partial = "";
476
+ line(whole);
477
+ }
478
+ } else if (lineIsText) {
479
+ text(piece);
480
+ if (newline !== -1) {
481
+ line("");
482
+ lineIsText = false;
483
+ }
484
+ } else {
485
+ held += piece;
486
+ if (newline !== -1) {
487
+ const whole = held;
488
+ held = "";
489
+ line(whole);
490
+ } else if (!MAYBE_FENCE.test(held) && !FENCE_START.test(held) && !mayBeStray(held)) {
491
+ lineIsText = true;
492
+ text(held);
493
+ held = "";
494
+ }
495
+ }
496
+ }
497
+ if (mode.kind === "block" && mode.block.dirty) {
498
+ mode.block.dirty = false;
499
+ refresh(mode.block);
500
+ }
501
+ const events = out;
502
+ out = [];
503
+ return events;
504
+ };
505
+ /** Closes the block `line` may have opened; a function, so `mode` is read fresh. */
506
+ const closeOpen = () => {
507
+ if (mode.kind === "block") finishImplicit(mode.block);
508
+ };
509
+ const end = () => {
510
+ out = [];
511
+ if (mode.kind === "block") {
512
+ const { block } = mode;
513
+ const last = block.partial;
514
+ block.partial = "";
515
+ if (!block.implicit) {
516
+ if (last !== "") line(last);
517
+ closeOpen();
518
+ } else if (last !== "" && (block.depth > 0 || pickleLine(last))) {
519
+ line(last);
520
+ closeOpen();
521
+ } else {
522
+ finishImplicit(block);
523
+ if (PICKLE_OPEN.exec(last) === null) text(last);
524
+ }
525
+ } else if (lineIsText) lineIsText = false;
526
+ else if (held !== "") {
527
+ const last = held;
528
+ held = "";
529
+ if (mode.kind !== "code" && strayLine(last)) {
530
+ line(last);
531
+ closeOpen();
532
+ } else if (mode.kind === "code" || PICKLE_OPEN.exec(last) === null) text(last);
533
+ }
534
+ const events = out;
535
+ out = [];
536
+ return events;
537
+ };
538
+ return {
539
+ claim: (block) => {
540
+ const plan = plans.get(block.key);
541
+ plans.delete(block.key);
542
+ return plan;
543
+ },
544
+ end,
545
+ plan: (block) => plans.get(block.key),
546
+ push,
547
+ remember: (id, body) => {
548
+ asWritten.delete(id);
549
+ latest.set(id, body);
550
+ },
551
+ result: (key) => results.get(key),
552
+ results: () => [...results.values()].map((result) => ({ ...result }))
553
+ };
554
+ }
555
+ /**
556
+ * Repairs one block with a document-mode session turn: the user's message,
557
+ * the block's source as the assistant's answer, and the follow-up that asks
558
+ * for the fix.
559
+ */
560
+ async function* repairBlock(deps, block) {
561
+ if (block.status !== "repairing") {
562
+ yield block;
563
+ return;
564
+ }
565
+ const plan = deps.parser.claim(block);
566
+ if (plan === void 0) {
567
+ yield {
568
+ ...block,
569
+ error: {
570
+ code: "NOT_FOUND",
571
+ message: `No repair is waiting for "${block.key}": it was repaired already, or comes from another turn.`
572
+ },
573
+ status: "done"
574
+ };
575
+ return;
576
+ }
577
+ const result = deps.parser.result(block.key);
578
+ const base = plan.kind === "edit" ? plan.base : plan.body;
579
+ const ask = `Write the "${block.id}" UI.`;
580
+ const messages = [
581
+ {
582
+ content: deps.prompt === void 0 ? ask : `${deps.prompt}\n\n${ask}`,
583
+ role: "user"
584
+ },
585
+ {
586
+ content: base,
587
+ role: "assistant"
588
+ },
589
+ {
590
+ content: plan.kind === "edit" ? editRepairText(plan.failure, plan.base) : repairText(plan.diagnostics, plan.advice),
591
+ role: "user"
592
+ }
593
+ ];
594
+ let program = null;
595
+ let failure = {
596
+ code: "INTERNAL",
597
+ message: "The repair stopped before it finished."
598
+ };
599
+ const forward = deps.session.onTurn;
600
+ const session = {
601
+ ...deps.session,
602
+ designRepair: plan.kind === "errors" && plan.advice.length > 0 ? /* @__PURE__ */ new Set() : deps.session.designRepair,
603
+ editFailures: plan.kind === "edit" ? 1 : 0,
604
+ onTurn: async (turn) => {
605
+ if (result !== void 0) result.repairs = turn.rounds;
606
+ await forward?.(turn);
607
+ }
608
+ };
609
+ for await (const event of runSession(session, messages, deps.options, deps.call)) if (event.type === "program") program = event.program;
610
+ else if (event.type === "done") {
611
+ deps.parser.remember(block.id, splitPragma(event.source).body);
612
+ if (result !== void 0) result.status = "done";
613
+ yield {
614
+ ...block,
615
+ diagnostics: event.diagnostics,
616
+ program: program ?? block.program,
617
+ source: event.source,
618
+ status: "done"
619
+ };
620
+ return;
621
+ } else failure = event.error;
622
+ if (result !== void 0) {
623
+ result.error = failure;
624
+ result.status = "done";
625
+ }
626
+ yield {
627
+ ...block,
628
+ error: failure,
629
+ status: "done"
630
+ };
631
+ }
632
+ /**
633
+ * Runs `push`, `end` and `repair` over a reply's text. Repairs start as
634
+ * their block closes and run while the rest of the reply streams; the
635
+ * iterable ends once the text has ended and every repair has settled.
636
+ */
637
+ async function* streamChat(turn, reply, signal) {
638
+ const ready = [];
639
+ let wake = null;
640
+ let running = 0;
641
+ const outcome = {};
642
+ const stop = new AbortController();
643
+ const stopped = () => stop.signal.aborted || signal?.aborted === true;
644
+ const notify = () => {
645
+ const resume = wake;
646
+ wake = null;
647
+ resume?.();
648
+ };
649
+ const repair = async (block) => {
650
+ try {
651
+ for await (const next of turn.repair(block)) {
652
+ ready.push({
653
+ block: next,
654
+ type: "block"
655
+ });
656
+ notify();
657
+ }
658
+ } finally {
659
+ running--;
660
+ notify();
661
+ }
662
+ };
663
+ const take = (events) => {
664
+ for (const event of events) {
665
+ ready.push(event);
666
+ if (event.type === "block" && event.block.status === "repairing") {
667
+ running++;
668
+ repair(event.block);
669
+ }
670
+ }
671
+ notify();
672
+ };
673
+ const pump = async () => {
674
+ const iterator = reply[Symbol.asyncIterator]();
675
+ try {
676
+ for (;;) {
677
+ if (stopped()) return;
678
+ const step = await iterator.next();
679
+ if (step.done === true) break;
680
+ take(turn.push(step.value));
681
+ }
682
+ take(turn.end());
683
+ } catch (error) {
684
+ outcome.failure = { error };
685
+ } finally {
686
+ if (stopped()) iterator.return?.();
687
+ running--;
688
+ notify();
689
+ }
690
+ };
691
+ running++;
692
+ pump();
693
+ try {
694
+ for (;;) {
695
+ const next = ready.shift();
696
+ if (next !== void 0) {
697
+ yield next;
698
+ continue;
699
+ }
700
+ if (running === 0) break;
701
+ await new Promise((resolve) => {
702
+ wake = resolve;
703
+ });
704
+ }
705
+ if (outcome.failure !== void 0) throw outcome.failure.error;
706
+ } finally {
707
+ stop.abort();
708
+ }
709
+ }
710
+ //#endregion
711
+ export { chatBlockText, createReplyParser, repairBlock, streamChat };
@@ -0,0 +1,3 @@
1
+ import "./design.js";
2
+ import { Catalog, Host } from "@jigs.design/host";
3
+ import { Diagnostic, Program } from "@jigs.design/runtime";