@jigs.design/server 0.2.0 → 0.3.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/README.md +19 -13
- package/dist/chat.d.ts +2 -2
- package/dist/chat.js +46 -34
- package/dist/compile-cache.js +60 -0
- package/dist/compile.d.ts +1 -1
- package/dist/compile.js +167 -35
- package/dist/{server.d.ts → core.d.ts} +7 -3
- package/dist/{server.js → core.js} +309 -395
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +3 -3
- package/dist/events.js +129 -0
- package/dist/handler.d.ts +4 -0
- package/dist/handler.js +344 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -2
- package/dist/model-server.js +154 -0
- package/dist/session.d.ts +1 -2
- package/dist/session.js +116 -52
- package/dist/types.d.ts +277 -51
- package/package.json +6 -6
package/dist/session.d.ts
CHANGED
|
@@ -3,5 +3,4 @@ import "./compile.js";
|
|
|
3
3
|
import "./types.js";
|
|
4
4
|
import "@jigs.design/compiler";
|
|
5
5
|
import { Host } from "@jigs.design/host";
|
|
6
|
-
import { Diagnostic, Message, SessionEvent } from "@jigs.design/protocol";
|
|
7
|
-
import { z } from "zod";
|
|
6
|
+
import { Diagnostic, JigsErrorData, Message, SessionEvent } from "@jigs.design/protocol";
|
package/dist/session.js
CHANGED
|
@@ -1,15 +1,60 @@
|
|
|
1
1
|
import { errorData, isJigsError } from "./errors.js";
|
|
2
2
|
import { belowPrefix, pragmaLine, splitPragma, versionDiagnostics, withPragma } from "./pragma.js";
|
|
3
3
|
import { reorderDeclarations } from "./rewrite.js";
|
|
4
|
+
import { issuesText } from "./handler.js";
|
|
4
5
|
import { addUsage } from "./usage.js";
|
|
5
6
|
import { applyEdits, editBreakage, isEditReply, parseEdits } from "@jigs.design/compiler";
|
|
6
|
-
import { editRepairText, fullDocumentText, needsRepair, repairText } from "@jigs.design/prompt";
|
|
7
|
+
import { editRepairText, fullDocumentText, needsRepair, repairText, withDocument } from "@jigs.design/prompt";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
const MAX_CONTENT = 2e5;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const documentSchema = z.union([z.strictObject({ source: z.string().min(1).max(MAX_CONTENT) }), z.strictObject({
|
|
11
|
+
name: z.string().min(1).max(200),
|
|
12
|
+
props: z.record(z.string(), z.union([
|
|
13
|
+
z.boolean(),
|
|
14
|
+
z.number(),
|
|
15
|
+
z.string()
|
|
16
|
+
])).exactOptional()
|
|
17
|
+
})]);
|
|
18
|
+
const sessionSchema = z.object({
|
|
19
|
+
document: documentSchema.optional(),
|
|
20
|
+
messages: z.array(z.object({
|
|
21
|
+
content: z.string().max(MAX_CONTENT),
|
|
22
|
+
role: z.enum(["user", "assistant"])
|
|
23
|
+
})).max(200)
|
|
24
|
+
});
|
|
25
|
+
/** Whether a turn calls the model: its conversation ends with an ask. */
|
|
26
|
+
const asksModel = (messages) => messages.at(-1)?.role === "user";
|
|
27
|
+
/**
|
|
28
|
+
* Checks a session request's `document` and `messages`: their shape and
|
|
29
|
+
* limits, then that the turn has something to do. A reply with no document
|
|
30
|
+
* is history the model could not act on, so it is refused.
|
|
31
|
+
*/
|
|
32
|
+
function parseSessionInput(body) {
|
|
33
|
+
const parsed = sessionSchema.safeParse({
|
|
34
|
+
document: body.document,
|
|
35
|
+
messages: body.messages
|
|
36
|
+
});
|
|
37
|
+
if (!parsed.success) return { error: {
|
|
38
|
+
code: "BAD_INPUT",
|
|
39
|
+
message: issuesText(parsed.error)
|
|
40
|
+
} };
|
|
41
|
+
const document = parsed.data.document ?? null;
|
|
42
|
+
const messages = parsed.data.messages;
|
|
43
|
+
if (document === null) {
|
|
44
|
+
if (messages.length === 0) return { error: {
|
|
45
|
+
code: "BAD_INPUT",
|
|
46
|
+
message: "Send a document, a message, or both."
|
|
47
|
+
} };
|
|
48
|
+
if (messages.some((message) => message.role === "assistant")) return { error: {
|
|
49
|
+
code: "BAD_INPUT",
|
|
50
|
+
message: "messages holds a reply, so send the document it produced as `document`."
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
document,
|
|
55
|
+
messages
|
|
56
|
+
};
|
|
57
|
+
}
|
|
13
58
|
const modelResult = z.object({ usage: z.object({
|
|
14
59
|
cacheReadTokens: z.number().nonnegative().exactOptional(),
|
|
15
60
|
cacheWriteTokens: z.number().nonnegative().exactOptional(),
|
|
@@ -58,20 +103,41 @@ function applyChecked(base, edits, compile) {
|
|
|
58
103
|
reason: "broken"
|
|
59
104
|
};
|
|
60
105
|
}
|
|
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
106
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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.
|
|
107
|
+
* A turn that starts from a document and asks nothing: its source is
|
|
108
|
+
* checked and compiled again, with no model call.
|
|
73
109
|
*/
|
|
74
|
-
|
|
110
|
+
function* restoreTurn(checker, catalogVersion, content, fresh) {
|
|
111
|
+
const restored = splitPragma(content);
|
|
112
|
+
const compiled = checker.compile(restored.body);
|
|
113
|
+
const diagnostics = [...versionDiagnostics(restored.versions, catalogVersion), ...belowPrefix(compiled.diagnostics, restored.versions === null ? pragmaLine(catalogVersion) : content.slice(0, content.length - restored.body.length))];
|
|
114
|
+
yield {
|
|
115
|
+
diagnostics,
|
|
116
|
+
fresh,
|
|
117
|
+
program: compiled.program,
|
|
118
|
+
revision: 1,
|
|
119
|
+
type: "program"
|
|
120
|
+
};
|
|
121
|
+
yield {
|
|
122
|
+
diagnostics,
|
|
123
|
+
revision: 1,
|
|
124
|
+
source: restored.versions === null ? withPragma(restored.body, catalogVersion) : content,
|
|
125
|
+
type: "done"
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* One turn. A document with a conversation that does not end with an ask
|
|
130
|
+
* calls no model: its source is checked and compiled again. Otherwise the
|
|
131
|
+
* model's reply is compiled at each committed line, edits are applied to
|
|
132
|
+
* the document (or to an empty one, for a new UI), and errors get up to
|
|
133
|
+
* `maxRepairs` repair rounds.
|
|
134
|
+
*
|
|
135
|
+
* In a session turn, every model call sees the current UI in its last user
|
|
136
|
+
* message (`withDocument`), and each round's reply goes into the
|
|
137
|
+
* conversation as written. A chat repair shows no document: its assistant
|
|
138
|
+
* turns hold the checked source its repair texts are numbered by.
|
|
139
|
+
*/
|
|
140
|
+
async function* runSession(deps, turn, options, call) {
|
|
75
141
|
const catalogVersion = deps.host.components.version;
|
|
76
142
|
/**
|
|
77
143
|
* Diagnostics as the browser sees them: spans in the sent source, which
|
|
@@ -89,41 +155,31 @@ async function* runSession(deps, messages, options, call) {
|
|
|
89
155
|
const placement = deps.kind === "session" ? "document" : "chat";
|
|
90
156
|
/** Whether a round already asked for the design changes `designRepair` names. */
|
|
91
157
|
let designRepaired = false;
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
|
|
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
|
-
};
|
|
158
|
+
const { messages } = turn;
|
|
159
|
+
if (turn.document !== null && !asksModel(messages)) {
|
|
160
|
+
yield* restoreTurn(deps.checker, catalogVersion, turn.document, true);
|
|
120
161
|
return;
|
|
121
162
|
}
|
|
122
163
|
const conversation = messages.map((message) => message.role === "assistant" ? {
|
|
123
164
|
content: splitPragma(message.content).body,
|
|
124
165
|
role: "assistant"
|
|
125
166
|
} : message);
|
|
126
|
-
|
|
167
|
+
/** The UI the turn edits; empty for a new one. */
|
|
168
|
+
let current = turn.document === null ? "" : splitPragma(turn.document).body;
|
|
169
|
+
/** Whether model calls show `current`: from the start with a document, after the first round without. */
|
|
170
|
+
let showsDocument = deps.kind === "session" && turn.document !== null;
|
|
171
|
+
/**
|
|
172
|
+
* The conversation as `model` gets it, a copy the turn does not grow:
|
|
173
|
+
* the last user message framed with `current` when the call shows it.
|
|
174
|
+
*/
|
|
175
|
+
const request = () => {
|
|
176
|
+
const ask = conversation.at(-1);
|
|
177
|
+
if (!showsDocument || ask?.role !== "user") return [...conversation];
|
|
178
|
+
return [...conversation.slice(0, -1), {
|
|
179
|
+
content: withDocument(ask.content, current),
|
|
180
|
+
role: "user"
|
|
181
|
+
}];
|
|
182
|
+
};
|
|
127
183
|
let revision = 0;
|
|
128
184
|
/**
|
|
129
185
|
* Whether the next program starts a new UI: the first one of a session
|
|
@@ -166,7 +222,7 @@ async function* runSession(deps, messages, options, call) {
|
|
|
166
222
|
const iterator = deps.model({
|
|
167
223
|
ctx: call.ctx,
|
|
168
224
|
kind: kindOf(round),
|
|
169
|
-
messages:
|
|
225
|
+
messages: request(),
|
|
170
226
|
options,
|
|
171
227
|
signal: call.signal,
|
|
172
228
|
system: deps.system,
|
|
@@ -219,6 +275,8 @@ async function* runSession(deps, messages, options, call) {
|
|
|
219
275
|
iterator.return?.();
|
|
220
276
|
}
|
|
221
277
|
if (call.signal.aborted) return;
|
|
278
|
+
/** The reply as the model wrote it, without a fence around it. */
|
|
279
|
+
const written = cleanReply(text);
|
|
222
280
|
let source;
|
|
223
281
|
let edits;
|
|
224
282
|
if (isEditReply(text) === true) {
|
|
@@ -237,7 +295,7 @@ async function* runSession(deps, messages, options, call) {
|
|
|
237
295
|
if (round < deps.maxRepairs) {
|
|
238
296
|
editFailures++;
|
|
239
297
|
conversation.push({
|
|
240
|
-
content:
|
|
298
|
+
content: written,
|
|
241
299
|
role: "assistant"
|
|
242
300
|
}, {
|
|
243
301
|
content: editFailures === 1 ? editRepairText(applied, current) : fullDocumentText(applied, current),
|
|
@@ -256,7 +314,7 @@ async function* runSession(deps, messages, options, call) {
|
|
|
256
314
|
return;
|
|
257
315
|
}
|
|
258
316
|
source = applied.text;
|
|
259
|
-
} else source =
|
|
317
|
+
} else source = written;
|
|
260
318
|
let compiled = compile(source);
|
|
261
319
|
const reordered = reorderDeclarations(source, compiled, compile);
|
|
262
320
|
if (reordered !== null) {
|
|
@@ -289,14 +347,18 @@ async function* runSession(deps, messages, options, call) {
|
|
|
289
347
|
const chosen = designRepaired ? [] : advice.filter((entry) => deps.designRepair.has(entry.code));
|
|
290
348
|
if ((needsRepair(compiled.diagnostics) || chosen.length > 0) && round < deps.maxRepairs) {
|
|
291
349
|
designRepaired ||= chosen.length > 0;
|
|
292
|
-
conversation.push({
|
|
350
|
+
conversation.push(deps.kind === "session" ? {
|
|
351
|
+
content: written,
|
|
352
|
+
role: "assistant"
|
|
353
|
+
} : {
|
|
293
354
|
content: source,
|
|
294
355
|
role: "assistant"
|
|
295
356
|
}, {
|
|
296
|
-
content: repairText(compiled.diagnostics, chosen),
|
|
357
|
+
content: repairText(compiled.diagnostics, chosen, deps.kind === "session" ? "shown" : "answer"),
|
|
297
358
|
role: "user"
|
|
298
359
|
});
|
|
299
360
|
current = source;
|
|
361
|
+
showsDocument = deps.kind === "session";
|
|
300
362
|
repairs++;
|
|
301
363
|
continue;
|
|
302
364
|
}
|
|
@@ -306,6 +368,7 @@ async function* runSession(deps, messages, options, call) {
|
|
|
306
368
|
shown("done");
|
|
307
369
|
yield {
|
|
308
370
|
diagnostics: finalDiagnostics,
|
|
371
|
+
reply: round === 0 && reordered === null ? written : source,
|
|
309
372
|
revision,
|
|
310
373
|
source: finalSource,
|
|
311
374
|
type: "done"
|
|
@@ -334,6 +397,7 @@ async function* runSession(deps, messages, options, call) {
|
|
|
334
397
|
completed: finalSource !== null,
|
|
335
398
|
ctx: call.ctx,
|
|
336
399
|
diagnostics: finalDiagnostics,
|
|
400
|
+
document: turn.document,
|
|
337
401
|
firstProgramMs,
|
|
338
402
|
kind: deps.kind,
|
|
339
403
|
messages,
|
|
@@ -349,4 +413,4 @@ async function* runSession(deps, messages, options, call) {
|
|
|
349
413
|
}
|
|
350
414
|
}
|
|
351
415
|
//#endregion
|
|
352
|
-
export { MAX_CONTENT, applyChecked, editResult,
|
|
416
|
+
export { MAX_CONTENT, applyChecked, asksModel, editResult, parseSessionInput, restoreTurn, runSession };
|