@jigs.design/server 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/chat.d.ts +12 -0
- package/dist/chat.js +711 -0
- package/dist/compile.d.ts +3 -0
- package/dist/compile.js +249 -0
- package/dist/confirm.d.ts +16 -0
- package/dist/confirm.js +97 -0
- package/dist/design.d.ts +17 -0
- package/dist/design.js +145 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +62 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/inputs.js +224 -0
- package/dist/invalidation.js +39 -0
- package/dist/literal.js +258 -0
- package/dist/node.d.ts +19 -0
- package/dist/node.js +91 -0
- package/dist/outputs.js +120 -0
- package/dist/pragma.js +80 -0
- package/dist/rewrite.js +30 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +786 -0
- package/dist/session.d.ts +7 -0
- package/dist/session.js +352 -0
- package/dist/types.d.ts +481 -0
- package/dist/usage.d.ts +5 -0
- package/dist/usage.js +13 -0
- package/package.json +40 -3
package/dist/session.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { errorData, isPickleError } from "./errors.js";
|
|
2
|
+
import { belowPrefix, pragmaLine, splitPragma, versionDiagnostics, withPragma } from "./pragma.js";
|
|
3
|
+
import { reorderDeclarations } from "./rewrite.js";
|
|
4
|
+
import { addUsage } from "./usage.js";
|
|
5
|
+
import { applyEdits, editBreakage, isEditReply, parseEdits } from "@jigs.design/compiler";
|
|
6
|
+
import { editRepairText, fullDocumentText, needsRepair, repairText } from "@jigs.design/prompt";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
const MAX_CONTENT = 2e5;
|
|
9
|
+
const messagesSchema = z.array(z.object({
|
|
10
|
+
content: z.string().max(MAX_CONTENT),
|
|
11
|
+
role: z.enum(["user", "assistant"])
|
|
12
|
+
})).min(1).max(200);
|
|
13
|
+
const modelResult = z.object({ usage: z.object({
|
|
14
|
+
cacheReadTokens: z.number().nonnegative().exactOptional(),
|
|
15
|
+
cacheWriteTokens: z.number().nonnegative().exactOptional(),
|
|
16
|
+
inputTokens: z.number().nonnegative(),
|
|
17
|
+
outputTokens: z.number().nonnegative()
|
|
18
|
+
}) });
|
|
19
|
+
const FENCE_OPEN = /^\s*`{3}[\w-]*\s*$/;
|
|
20
|
+
const FENCE_CLOSE = /^\s*`{3}\s*$/;
|
|
21
|
+
/** The reply without a code fence around it, which models add despite the prompt. */
|
|
22
|
+
function cleanReply(text) {
|
|
23
|
+
const lines = text.split("\n");
|
|
24
|
+
const fenced = lines.length > 0 && FENCE_OPEN.test(lines[0] ?? "");
|
|
25
|
+
if (fenced) lines.shift();
|
|
26
|
+
while (lines.length > 0 && (lines.at(-1) ?? "").trim() === "") lines.pop();
|
|
27
|
+
if (fenced && lines.length > 0 && FENCE_CLOSE.test(lines.at(-1) ?? "")) lines.pop();
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
}
|
|
30
|
+
/** What an edit reply came to: how many blocks it had and whether they applied. */
|
|
31
|
+
const editResult = (blocks, applied) => applied.ok ? {
|
|
32
|
+
applied: true,
|
|
33
|
+
blocks
|
|
34
|
+
} : {
|
|
35
|
+
applied: false,
|
|
36
|
+
blocks,
|
|
37
|
+
reason: applied.reason
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Applies `edits` to `base`, and refuses the result when it needs parser
|
|
41
|
+
* repairs the base did not: an edit that leaves a tag unclosed compiles,
|
|
42
|
+
* with the tag closed at the end of the document, which the model did not
|
|
43
|
+
* mean.
|
|
44
|
+
*/
|
|
45
|
+
function applyChecked(base, edits, compile) {
|
|
46
|
+
const applied = applyEdits(base, edits);
|
|
47
|
+
if (!applied.ok) return applied;
|
|
48
|
+
const breakage = editBreakage(compile(base).diagnostics, compile(applied.text).diagnostics);
|
|
49
|
+
const failed = edits.at(-1);
|
|
50
|
+
if (breakage.length === 0 || failed === void 0) return applied;
|
|
51
|
+
return {
|
|
52
|
+
breakage: breakage.map((repair) => ({
|
|
53
|
+
line: repair.span.line,
|
|
54
|
+
message: repair.message
|
|
55
|
+
})),
|
|
56
|
+
failed,
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: "broken"
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const lastAssistantBody = (messages) => {
|
|
62
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
63
|
+
const message = messages[index];
|
|
64
|
+
if (message?.role === "assistant") return splitPragma(message.content).body;
|
|
65
|
+
}
|
|
66
|
+
return "";
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* One turn. A conversation that ends with an assistant message calls no
|
|
70
|
+
* model: its source is checked and compiled again. Otherwise the model's
|
|
71
|
+
* reply is compiled at each committed line, edits are applied to the last
|
|
72
|
+
* checked source, and errors get up to `maxRepairs` repair rounds.
|
|
73
|
+
*/
|
|
74
|
+
async function* runSession(deps, messages, options, call) {
|
|
75
|
+
const catalogVersion = deps.host.components.version;
|
|
76
|
+
/**
|
|
77
|
+
* Diagnostics as the browser sees them: spans in the sent source, which
|
|
78
|
+
* starts with the pragma line. The model reads the body without it.
|
|
79
|
+
*/
|
|
80
|
+
const belowPragma = (diagnostics) => belowPrefix(diagnostics, pragmaLine(catalogVersion));
|
|
81
|
+
const errorType = deps.kind === "session" ? "session" : "chat";
|
|
82
|
+
const started = performance.now();
|
|
83
|
+
/** What `model` is told: the first answer, or a round that repairs one. */
|
|
84
|
+
const kindOf = (round) => {
|
|
85
|
+
if (deps.kind === "chat-repair") return "chat-repair";
|
|
86
|
+
return round === 0 ? "session" : "repair";
|
|
87
|
+
};
|
|
88
|
+
const compile = (body) => deps.checker.compile(body);
|
|
89
|
+
const placement = deps.kind === "session" ? "document" : "chat";
|
|
90
|
+
/** Whether a round already asked for the design changes `designRepair` names. */
|
|
91
|
+
let designRepaired = false;
|
|
92
|
+
const last = messages.at(-1);
|
|
93
|
+
if (last?.role === "assistant") {
|
|
94
|
+
const restored = splitPragma(last.content);
|
|
95
|
+
const compiled = compile(restored.body);
|
|
96
|
+
const diagnostics = [...versionDiagnostics(restored.versions, catalogVersion), ...restored.versions === null ? belowPragma(compiled.diagnostics) : belowPrefix(compiled.diagnostics, last.content.slice(0, last.content.length - restored.body.length))];
|
|
97
|
+
yield {
|
|
98
|
+
diagnostics,
|
|
99
|
+
fresh: deps.kind === "session",
|
|
100
|
+
program: compiled.program,
|
|
101
|
+
revision: 1,
|
|
102
|
+
type: "program"
|
|
103
|
+
};
|
|
104
|
+
yield {
|
|
105
|
+
diagnostics,
|
|
106
|
+
revision: 1,
|
|
107
|
+
source: restored.versions === null ? withPragma(restored.body, catalogVersion) : last.content,
|
|
108
|
+
type: "done"
|
|
109
|
+
};
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (deps.model === void 0) {
|
|
113
|
+
yield {
|
|
114
|
+
error: {
|
|
115
|
+
code: "INTERNAL",
|
|
116
|
+
message: "This server has no model."
|
|
117
|
+
},
|
|
118
|
+
type: "error"
|
|
119
|
+
};
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const conversation = messages.map((message) => message.role === "assistant" ? {
|
|
123
|
+
content: splitPragma(message.content).body,
|
|
124
|
+
role: "assistant"
|
|
125
|
+
} : message);
|
|
126
|
+
let current = lastAssistantBody(messages);
|
|
127
|
+
let revision = 0;
|
|
128
|
+
/**
|
|
129
|
+
* Whether the next program starts a new UI: the first one of a session
|
|
130
|
+
* turn whose answer is a whole document rather than edits. A repair round
|
|
131
|
+
* fixes the UI the turn started, so it never starts another.
|
|
132
|
+
*/
|
|
133
|
+
let startsDocument = deps.kind === "session";
|
|
134
|
+
let repairs = 0;
|
|
135
|
+
let editFailures = deps.editFailures ?? 0;
|
|
136
|
+
let usage = {
|
|
137
|
+
inputTokens: 0,
|
|
138
|
+
outputTokens: 0
|
|
139
|
+
};
|
|
140
|
+
const rounds = [];
|
|
141
|
+
let firstProgramMs = null;
|
|
142
|
+
/**
|
|
143
|
+
* Notes the first program the browser receives. A chat repair's programs
|
|
144
|
+
* stay on the server until it is done, so for one that is `done`.
|
|
145
|
+
*/
|
|
146
|
+
const shown = (event) => {
|
|
147
|
+
if (firstProgramMs === null && (event === "done" || deps.kind === "session")) firstProgramMs = Math.round(performance.now() - started);
|
|
148
|
+
};
|
|
149
|
+
let finalSource = null;
|
|
150
|
+
let finalDiagnostics = [];
|
|
151
|
+
let finalAdvice = [];
|
|
152
|
+
try {
|
|
153
|
+
for (let round = 0;; round++) {
|
|
154
|
+
let text = "";
|
|
155
|
+
let committed = 0;
|
|
156
|
+
/** The last program sent, and when; one compiled since waits for the interval. */
|
|
157
|
+
let lastKey = null;
|
|
158
|
+
let lastSentAt = Number.NEGATIVE_INFINITY;
|
|
159
|
+
/** Complete edit blocks already drawn, for an edit reply. */
|
|
160
|
+
let editsShown = 0;
|
|
161
|
+
let roundUsage = {
|
|
162
|
+
inputTokens: 0,
|
|
163
|
+
outputTokens: 0
|
|
164
|
+
};
|
|
165
|
+
const roundKind = round === 0 && deps.kind === "session" ? "answer" : "repair";
|
|
166
|
+
const iterator = deps.model({
|
|
167
|
+
ctx: call.ctx,
|
|
168
|
+
kind: kindOf(round),
|
|
169
|
+
messages: conversation,
|
|
170
|
+
options,
|
|
171
|
+
signal: call.signal,
|
|
172
|
+
system: deps.system,
|
|
173
|
+
systemPrefix: deps.systemPrefix
|
|
174
|
+
})[Symbol.asyncIterator]();
|
|
175
|
+
try {
|
|
176
|
+
for (;;) {
|
|
177
|
+
const step = await iterator.next();
|
|
178
|
+
if (step.done === true) {
|
|
179
|
+
const result = modelResult.safeParse(step.value);
|
|
180
|
+
if (result.success) {
|
|
181
|
+
roundUsage = result.data.usage;
|
|
182
|
+
usage = addUsage(usage, roundUsage);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
text += step.value;
|
|
187
|
+
const cut = text.lastIndexOf("\n");
|
|
188
|
+
const editing = isEditReply(text);
|
|
189
|
+
if (round > 0 || editing === null || cut <= committed) continue;
|
|
190
|
+
committed = cut;
|
|
191
|
+
let body = cleanReply(text.slice(0, cut));
|
|
192
|
+
if (editing) {
|
|
193
|
+
const edits = parseEdits(text.slice(0, cut));
|
|
194
|
+
const applied = applyEdits(current, edits);
|
|
195
|
+
if (edits.length === editsShown || !applied.ok) continue;
|
|
196
|
+
editsShown = edits.length;
|
|
197
|
+
startsDocument = false;
|
|
198
|
+
body = applied.text;
|
|
199
|
+
}
|
|
200
|
+
const compiled = compile(body);
|
|
201
|
+
const key = JSON.stringify(compiled.program);
|
|
202
|
+
const now = performance.now();
|
|
203
|
+
if (key !== lastKey && now - lastSentAt >= deps.programIntervalMs) {
|
|
204
|
+
lastSentAt = now;
|
|
205
|
+
lastKey = key;
|
|
206
|
+
revision++;
|
|
207
|
+
shown("program");
|
|
208
|
+
yield {
|
|
209
|
+
diagnostics: belowPragma(compiled.diagnostics),
|
|
210
|
+
fresh: startsDocument,
|
|
211
|
+
program: compiled.program,
|
|
212
|
+
revision,
|
|
213
|
+
type: "program"
|
|
214
|
+
};
|
|
215
|
+
startsDocument = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} finally {
|
|
219
|
+
iterator.return?.();
|
|
220
|
+
}
|
|
221
|
+
if (call.signal.aborted) return;
|
|
222
|
+
let source;
|
|
223
|
+
let edits;
|
|
224
|
+
if (isEditReply(text) === true) {
|
|
225
|
+
startsDocument = false;
|
|
226
|
+
const parsed = parseEdits(text);
|
|
227
|
+
const applied = applyChecked(current, parsed, compile);
|
|
228
|
+
edits = editResult(parsed.length, applied);
|
|
229
|
+
if (!applied.ok) {
|
|
230
|
+
rounds.push({
|
|
231
|
+
advice: [],
|
|
232
|
+
diagnostics: [],
|
|
233
|
+
edits,
|
|
234
|
+
kind: roundKind,
|
|
235
|
+
usage: roundUsage
|
|
236
|
+
});
|
|
237
|
+
if (round < deps.maxRepairs) {
|
|
238
|
+
editFailures++;
|
|
239
|
+
conversation.push({
|
|
240
|
+
content: text,
|
|
241
|
+
role: "assistant"
|
|
242
|
+
}, {
|
|
243
|
+
content: editFailures === 1 ? editRepairText(applied, current) : fullDocumentText(applied, current),
|
|
244
|
+
role: "user"
|
|
245
|
+
});
|
|
246
|
+
repairs++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
yield {
|
|
250
|
+
error: {
|
|
251
|
+
code: "INTERNAL",
|
|
252
|
+
message: "The model's edit could not be applied."
|
|
253
|
+
},
|
|
254
|
+
type: "error"
|
|
255
|
+
};
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
source = applied.text;
|
|
259
|
+
} else source = cleanReply(text);
|
|
260
|
+
let compiled = compile(source);
|
|
261
|
+
const reordered = reorderDeclarations(source, compiled, compile);
|
|
262
|
+
if (reordered !== null) {
|
|
263
|
+
source = reordered.body;
|
|
264
|
+
compiled = reordered.compiled;
|
|
265
|
+
}
|
|
266
|
+
const diagnostics = belowPragma(compiled.diagnostics);
|
|
267
|
+
const advice = compiled.advise(placement);
|
|
268
|
+
const finished = {
|
|
269
|
+
advice: belowPragma(advice),
|
|
270
|
+
diagnostics,
|
|
271
|
+
kind: roundKind,
|
|
272
|
+
usage: roundUsage
|
|
273
|
+
};
|
|
274
|
+
if (edits !== void 0) finished.edits = edits;
|
|
275
|
+
if (reordered !== null) finished.rewrites = [reordered.rewrite];
|
|
276
|
+
rounds.push(finished);
|
|
277
|
+
if (JSON.stringify(compiled.program) !== lastKey) {
|
|
278
|
+
revision++;
|
|
279
|
+
shown("program");
|
|
280
|
+
yield {
|
|
281
|
+
diagnostics,
|
|
282
|
+
fresh: startsDocument,
|
|
283
|
+
program: compiled.program,
|
|
284
|
+
revision,
|
|
285
|
+
type: "program"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
startsDocument = false;
|
|
289
|
+
const chosen = designRepaired ? [] : advice.filter((entry) => deps.designRepair.has(entry.code));
|
|
290
|
+
if ((needsRepair(compiled.diagnostics) || chosen.length > 0) && round < deps.maxRepairs) {
|
|
291
|
+
designRepaired ||= chosen.length > 0;
|
|
292
|
+
conversation.push({
|
|
293
|
+
content: source,
|
|
294
|
+
role: "assistant"
|
|
295
|
+
}, {
|
|
296
|
+
content: repairText(compiled.diagnostics, chosen),
|
|
297
|
+
role: "user"
|
|
298
|
+
});
|
|
299
|
+
current = source;
|
|
300
|
+
repairs++;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
finalSource = withPragma(source, catalogVersion);
|
|
304
|
+
finalDiagnostics = diagnostics;
|
|
305
|
+
finalAdvice = finished.advice;
|
|
306
|
+
shown("done");
|
|
307
|
+
yield {
|
|
308
|
+
diagnostics: finalDiagnostics,
|
|
309
|
+
revision,
|
|
310
|
+
source: finalSource,
|
|
311
|
+
type: "done"
|
|
312
|
+
};
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (isPickleError(error)) yield {
|
|
317
|
+
error: errorData(error),
|
|
318
|
+
type: "error"
|
|
319
|
+
};
|
|
320
|
+
else if (!call.signal.aborted) {
|
|
321
|
+
deps.onError(error, { type: errorType });
|
|
322
|
+
yield {
|
|
323
|
+
error: {
|
|
324
|
+
code: "INTERNAL",
|
|
325
|
+
message: "The model call failed."
|
|
326
|
+
},
|
|
327
|
+
type: "error"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
} finally {
|
|
331
|
+
if (deps.onTurn !== void 0) try {
|
|
332
|
+
await deps.onTurn({
|
|
333
|
+
advice: finalAdvice,
|
|
334
|
+
completed: finalSource !== null,
|
|
335
|
+
ctx: call.ctx,
|
|
336
|
+
diagnostics: finalDiagnostics,
|
|
337
|
+
firstProgramMs,
|
|
338
|
+
kind: deps.kind,
|
|
339
|
+
messages,
|
|
340
|
+
options,
|
|
341
|
+
repairs,
|
|
342
|
+
rounds,
|
|
343
|
+
source: finalSource,
|
|
344
|
+
usage
|
|
345
|
+
});
|
|
346
|
+
} catch (error) {
|
|
347
|
+
deps.onError(error, { type: errorType });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
export { MAX_CONTENT, applyChecked, editResult, messagesSchema, runSession };
|