@jigs.design/server 0.1.2 → 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 +20 -14
- 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 +169 -65
- package/dist/{server.d.ts → core.d.ts} +7 -3
- package/dist/{server.js → core.js} +310 -396
- package/dist/design.d.ts +6 -3
- package/dist/design.js +0 -1
- package/dist/errors.d.ts +3 -3
- 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/outputs.js +2 -7
- package/dist/session.d.ts +1 -2
- package/dist/session.js +116 -52
- package/dist/types.d.ts +277 -51
- package/package.json +6 -5
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { INTERNAL_MESSAGE, JigsCompileError, JigsError, errorData, fatalDiagnostics, isJigsError
|
|
2
|
-
import { checkMarkdownElements, createChecker } from "./compile.js";
|
|
1
|
+
import { INTERNAL_MESSAGE, JigsCompileError, JigsError, errorData, fatalDiagnostics, isJigsError } from "./errors.js";
|
|
3
2
|
import { belowPrefix, splitPragma, versionDiagnostics } from "./pragma.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { checkMarkdownElements, createChecker } from "./compile.js";
|
|
4
|
+
import { VERSION_HEADER, createHandler, createHttp, issuesText, originOf } from "./handler.js";
|
|
5
|
+
import { MAX_CONTENT, asksModel, parseSessionInput, restoreTurn } from "./session.js";
|
|
6
6
|
import { confirmMessage, createConfirmer, memoryConfirmations } from "./confirm.js";
|
|
7
|
+
import { processCompileCache, sha256 } from "./compile-cache.js";
|
|
8
|
+
import { eventStream } from "./events.js";
|
|
7
9
|
import { invalidationsOf } from "./invalidation.js";
|
|
10
|
+
import { createModelServer } from "./model-server.js";
|
|
8
11
|
import { systemPromptParts } from "@jigs.design/prompt";
|
|
9
12
|
import { isCallPolicy } from "@jigs.design/host";
|
|
10
|
-
import { PROGRAM_FORMAT } from "@jigs.design/
|
|
13
|
+
import { PROGRAM_FORMAT } from "@jigs.design/protocol";
|
|
11
14
|
import { z } from "zod";
|
|
12
15
|
import { frameDocument, frameSecurityPolicy } from "@jigs.design/runtime/frame";
|
|
13
|
-
//#region src/
|
|
14
|
-
const VERSION_HEADER = "jigs-version";
|
|
16
|
+
//#region src/core.ts
|
|
15
17
|
const DEFAULT_MAX_BODY_BYTES = 1e6;
|
|
16
|
-
const HEARTBEAT_MS = 15e3;
|
|
17
18
|
/** A small document whose compiled code changes when the compiler's output does. */
|
|
18
19
|
const COMPILER_PROBE = `{@body const [count, setCount] = useState(0)}
|
|
19
20
|
# Probe
|
|
@@ -23,62 +24,18 @@ const COMPILER_PROBE = `{@body const [count, setCount] = useState(0)}
|
|
|
23
24
|
<if test={count > 0}>
|
|
24
25
|
**done**
|
|
25
26
|
</if>`;
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
const chunks = [];
|
|
34
|
-
let size = 0;
|
|
35
|
-
for (;;) {
|
|
36
|
-
const chunk = await reader.read();
|
|
37
|
-
if (chunk.done) break;
|
|
38
|
-
size += chunk.value.byteLength;
|
|
39
|
-
if (size > max) {
|
|
40
|
-
await reader.cancel();
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
chunks.push(chunk.value);
|
|
44
|
-
}
|
|
45
|
-
const bytes = new Uint8Array(size);
|
|
46
|
-
let offset = 0;
|
|
47
|
-
for (const chunk of chunks) {
|
|
48
|
-
bytes.set(chunk, offset);
|
|
49
|
-
offset += chunk.byteLength;
|
|
50
|
-
}
|
|
51
|
-
return new TextDecoder().decode(bytes);
|
|
27
|
+
/** What `resolvePage` must return, checked, since a plain-JavaScript one may not. */
|
|
28
|
+
const savedPageSchema = z.object({ source: z.string() });
|
|
29
|
+
/** The strict rule: an error or warning diagnostic throws. */
|
|
30
|
+
function strictly(subject, compiled) {
|
|
31
|
+
const fatal = fatalDiagnostics(compiled.diagnostics);
|
|
32
|
+
if (fatal.length > 0) throw new JigsCompileError(subject, fatal);
|
|
33
|
+
return compiled;
|
|
52
34
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* parses as a URL whose origin is `"null"`, the `Origin` that sandboxed
|
|
57
|
-
* frames and `file:` pages send.
|
|
58
|
-
*/
|
|
59
|
-
function originOf(entry) {
|
|
60
|
-
let url = null;
|
|
61
|
-
try {
|
|
62
|
-
url = new URL(entry);
|
|
63
|
-
} catch {}
|
|
64
|
-
if (url === null || url.protocol !== "http:" && url.protocol !== "https:" || url.origin === "null") throw new Error(`"${entry}" is not an origin: write its scheme and host, such as https://app.example.com`);
|
|
65
|
-
return url.origin;
|
|
35
|
+
/** A document page, shipped or saved, takes no props. */
|
|
36
|
+
function noProps(name, props) {
|
|
37
|
+
if (Object.keys(props).length > 0) throw new JigsError("BAD_INPUT", `"${name}" is a document page, which takes no props`);
|
|
66
38
|
}
|
|
67
|
-
const bodySchema = z.discriminatedUnion("type", [z.object({
|
|
68
|
-
confirmation: z.string().max(1e3).optional(),
|
|
69
|
-
input: z.json().optional(),
|
|
70
|
-
kind: z.enum([
|
|
71
|
-
"action",
|
|
72
|
-
"mutation",
|
|
73
|
-
"query"
|
|
74
|
-
], { error: "Send the procedure's kind as \"kind\": \"query\", \"mutation\" or \"action\"" }),
|
|
75
|
-
name: z.string().max(200),
|
|
76
|
-
type: z.literal("call")
|
|
77
|
-
}), z.object({
|
|
78
|
-
messages: z.json(),
|
|
79
|
-
options: z.json().optional(),
|
|
80
|
-
type: z.literal("session")
|
|
81
|
-
})]);
|
|
82
39
|
/** A call a policy refused. The procedure stays in the prompt. */
|
|
83
40
|
const denial = (name) => ({ error: {
|
|
84
41
|
code: "FORBIDDEN",
|
|
@@ -136,47 +93,16 @@ function hostFingerprint(host) {
|
|
|
136
93
|
output: procedure.output === void 0 ? null : z.toJSONSchema(procedure.output),
|
|
137
94
|
projection: procedure.kind === "action" || procedure.projection === void 0 ? null : z.toJSONSchema(procedure.projection.schema)
|
|
138
95
|
})),
|
|
139
|
-
|
|
96
|
+
jigs: host.jigs.map((entry) => [
|
|
97
|
+
entry.name,
|
|
98
|
+
splitPragma(entry.source).body,
|
|
99
|
+
entry.kind,
|
|
100
|
+
entry.page,
|
|
101
|
+
entry.tag
|
|
102
|
+
])
|
|
140
103
|
});
|
|
141
104
|
}
|
|
142
105
|
/**
|
|
143
|
-
* What `jigs.chat` reads from the route besides the request. `blocks` and
|
|
144
|
-
* `prompt` come from the browser, so they are checked like a session's
|
|
145
|
-
* messages.
|
|
146
|
-
*/
|
|
147
|
-
const chatInputSchema = z.object({
|
|
148
|
-
blocks: z.array(z.object({
|
|
149
|
-
id: z.string().max(200),
|
|
150
|
-
source: z.string().max(MAX_CONTENT)
|
|
151
|
-
})).max(200).optional(),
|
|
152
|
-
prompt: z.string().max(MAX_CONTENT).optional()
|
|
153
|
-
});
|
|
154
|
-
/**
|
|
155
|
-
* Runs a `safeParse`, with a value nested too deeply to walk reported as a
|
|
156
|
-
* failure instead of a thrown `RangeError`.
|
|
157
|
-
*/
|
|
158
|
-
function parseBounded(parse) {
|
|
159
|
-
let parsed;
|
|
160
|
-
try {
|
|
161
|
-
parsed = parse();
|
|
162
|
-
} catch {
|
|
163
|
-
return {
|
|
164
|
-
message: "The request body is nested too deeply.",
|
|
165
|
-
success: false
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
return parsed.success ? {
|
|
169
|
-
data: parsed.data,
|
|
170
|
-
success: true
|
|
171
|
-
} : {
|
|
172
|
-
message: issuesText(parsed.error),
|
|
173
|
-
success: false
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
function issuesText(error) {
|
|
177
|
-
return error.issues.slice(0, 3).map((issue) => issue.path.length === 0 ? issue.message : `${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
106
|
* Types a `context` and `run` pair outside `createJigsServer`, so two
|
|
181
107
|
* servers (one per platform) can share them. `TContext` comes from `context`
|
|
182
108
|
* alone, as in `createJigsServer`.
|
|
@@ -198,23 +124,20 @@ function createJigsServer(options) {
|
|
|
198
124
|
const chatParts = systemPromptParts(host, promptOptions("chat"));
|
|
199
125
|
const system = `${documentParts.stable}\n\n${documentParts.variable}`;
|
|
200
126
|
const chatSystem = `${chatParts.stable}\n\n${chatParts.variable}`;
|
|
201
|
-
/** Tags that mark unfenced Jigs in a chat reply. */
|
|
202
|
-
const chatTags = /* @__PURE__ */ new Set([...host.components.components.map((component) => component.name), ...host.recipes.map((entry) => entry.name)]);
|
|
203
|
-
const maxRepairs = options.maxRepairs ?? 1;
|
|
204
|
-
const programIntervalMs = options.programIntervalMs ?? 250;
|
|
205
|
-
const designRepair = new Set(options.designRepair ?? []);
|
|
206
127
|
const allowedOrigins = new Set((options.allowedOrigins ?? []).map((entry) => originOf(entry)));
|
|
207
128
|
const ownOrigin = options.origin === void 0 ? null : originOf(options.origin);
|
|
208
129
|
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
209
130
|
const frameHtml = frameDocument();
|
|
210
131
|
const compilerProbe = checker.compile(COMPILER_PROBE).program.code;
|
|
211
|
-
const
|
|
132
|
+
const fingerprintInput = JSON.stringify([
|
|
212
133
|
PROGRAM_FORMAT,
|
|
213
134
|
0,
|
|
214
135
|
frameHtml,
|
|
215
136
|
compilerProbe,
|
|
216
137
|
hostFingerprint(host)
|
|
217
|
-
])
|
|
138
|
+
]);
|
|
139
|
+
const version = fingerprint(fingerprintInput);
|
|
140
|
+
const namespace = sha256(fingerprintInput);
|
|
218
141
|
const onError = (cause, info) => {
|
|
219
142
|
try {
|
|
220
143
|
options.onError?.(cause, info);
|
|
@@ -392,7 +315,7 @@ function createJigsServer(options) {
|
|
|
392
315
|
const failed = (cause) => {
|
|
393
316
|
onError(cause, {
|
|
394
317
|
name: report.name,
|
|
395
|
-
type: "call"
|
|
318
|
+
type: report.kind === "page" ? "page" : "call"
|
|
396
319
|
});
|
|
397
320
|
};
|
|
398
321
|
try {
|
|
@@ -423,26 +346,6 @@ function createJigsServer(options) {
|
|
|
423
346
|
reportCall(report);
|
|
424
347
|
return outcome;
|
|
425
348
|
};
|
|
426
|
-
const parseSession = (body) => {
|
|
427
|
-
const messages = messagesSchema.safeParse(body.messages);
|
|
428
|
-
if (!messages.success) return { error: {
|
|
429
|
-
code: "BAD_INPUT",
|
|
430
|
-
message: `messages: ${issuesText(messages.error)}`
|
|
431
|
-
} };
|
|
432
|
-
if (options.options === void 0) return {
|
|
433
|
-
messages: messages.data,
|
|
434
|
-
options: void 0
|
|
435
|
-
};
|
|
436
|
-
const parsed = options.options.safeParse(body.options ?? {});
|
|
437
|
-
if (!parsed.success) return { error: {
|
|
438
|
-
code: "BAD_INPUT",
|
|
439
|
-
message: `options: ${issuesText(parsed.error)}`
|
|
440
|
-
} };
|
|
441
|
-
return {
|
|
442
|
-
messages: messages.data,
|
|
443
|
-
options: parsed.data
|
|
444
|
-
};
|
|
445
|
-
};
|
|
446
349
|
/** A stored source: its pragma's versions are checked, then its body compiled. */
|
|
447
350
|
const compileStored = (source, placement = "document") => {
|
|
448
351
|
const stored = splitPragma(source);
|
|
@@ -454,157 +357,230 @@ function createJigsServer(options) {
|
|
|
454
357
|
program: compiled.program
|
|
455
358
|
};
|
|
456
359
|
};
|
|
457
|
-
/** The strict rule: an error or warning diagnostic throws. */
|
|
458
|
-
const compileStrict = (subject, source, placement) => {
|
|
459
|
-
const compiled = compileStored(source, placement);
|
|
460
|
-
const fatal = fatalDiagnostics(compiled.diagnostics);
|
|
461
|
-
if (fatal.length > 0) throw new JigsCompileError(subject, fatal);
|
|
462
|
-
return compiled;
|
|
463
|
-
};
|
|
464
|
-
const specs = /* @__PURE__ */ new Map();
|
|
465
|
-
for (const [name, source] of Object.entries(options.specs ?? {})) specs.set(name, compileStrict(`Spec "${name}"`, source).program);
|
|
466
|
-
const spec = (name) => {
|
|
467
|
-
const program = specs.get(name);
|
|
468
|
-
if (program === void 0) throw new Error(`createJigsServer has no spec named "${name}"`);
|
|
469
|
-
return program;
|
|
470
|
-
};
|
|
471
|
-
const compile = async (source, compileOptions) => compileOptions?.strict === false ? compileStored(source, compileOptions.placement) : compileStrict("The source", source, compileOptions?.placement);
|
|
472
|
-
const sessionDeps = {
|
|
473
|
-
checker,
|
|
474
|
-
designRepair,
|
|
475
|
-
host,
|
|
476
|
-
kind: "session",
|
|
477
|
-
maxRepairs,
|
|
478
|
-
model: options.model,
|
|
479
|
-
onError,
|
|
480
|
-
onTurn: options.onTurn,
|
|
481
|
-
programIntervalMs,
|
|
482
|
-
system,
|
|
483
|
-
systemPrefix: documentParts.stable
|
|
484
|
-
};
|
|
485
|
-
const serverOrigin = (request) => ownOrigin ?? new URL(request.url).origin;
|
|
486
360
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
361
|
+
* `compile` through the process's cache, keyed by what this server
|
|
362
|
+
* compiles against, the placement (or "page", for a define page's body,
|
|
363
|
+
* which compiles against every define jig) and the body's hash. Only
|
|
364
|
+
* final artifacts come here: a reply's partial sources stay on `checker`.
|
|
490
365
|
*/
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
366
|
+
const cached = async (placement, body, compile) => {
|
|
367
|
+
const key = `${await namespace}.${placement}.${await sha256(body)}`;
|
|
368
|
+
const hit = processCompileCache.get(key);
|
|
369
|
+
if (hit !== void 0) return hit;
|
|
370
|
+
const compiled = compile();
|
|
371
|
+
processCompileCache.set(key, compiled);
|
|
372
|
+
return compiled;
|
|
373
|
+
};
|
|
374
|
+
const compileCached = (source, placement = "document") => cached(placement, source, () => compileStored(source, placement));
|
|
375
|
+
const jigsByName = new Map(host.jigs.map((entry) => [entry.name, entry]));
|
|
376
|
+
/** A define page's one-tag body compiled; it throws as `pageBody` does. */
|
|
377
|
+
const compileDefine = (name, props) => {
|
|
378
|
+
const compiled = checker.page(name, props);
|
|
379
|
+
return {
|
|
380
|
+
advice: compiled.advise("document"),
|
|
381
|
+
diagnostics: compiled.diagnostics,
|
|
382
|
+
program: compiled.program
|
|
496
383
|
};
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
384
|
+
};
|
|
385
|
+
const startupPages = /* @__PURE__ */ new Map();
|
|
386
|
+
for (const entry of host.jigs) {
|
|
387
|
+
if (!entry.page) continue;
|
|
388
|
+
const subject = `Jig "${entry.name}"`;
|
|
389
|
+
if (entry.kind === "document") startupPages.set(entry.name, {
|
|
390
|
+
...strictly(subject, compileStored(entry.source)),
|
|
391
|
+
origin: "shipped",
|
|
392
|
+
source: entry.source
|
|
393
|
+
});
|
|
394
|
+
else if (!checker.needsProps(entry.name)) startupPages.set(entry.name, {
|
|
395
|
+
...strictly(subject, compileDefine(entry.name, {})),
|
|
396
|
+
origin: "shipped",
|
|
397
|
+
source: checker.pageBody(entry.name, {})
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/** A page the host lists, compiled strictly. */
|
|
401
|
+
const shippedPage = async (entry, props) => {
|
|
402
|
+
const ready = startupPages.get(entry.name);
|
|
403
|
+
if (entry.kind === "document") noProps(entry.name, props);
|
|
404
|
+
if (ready !== void 0 && Object.keys(props).length === 0) return ready;
|
|
405
|
+
const body = checker.pageBody(entry.name, props);
|
|
406
|
+
const compiled = await cached("page", body, () => compileDefine(entry.name, props));
|
|
407
|
+
return {
|
|
408
|
+
...strictly(`Page "${entry.name}"`, compiled),
|
|
409
|
+
origin: "shipped",
|
|
410
|
+
source: body
|
|
500
411
|
};
|
|
501
|
-
return null;
|
|
502
412
|
};
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
413
|
+
const internal = (cause, name) => {
|
|
414
|
+
onError(cause, {
|
|
415
|
+
name,
|
|
416
|
+
type: "page"
|
|
417
|
+
});
|
|
418
|
+
return new JigsError("INTERNAL", INTERNAL_MESSAGE);
|
|
419
|
+
};
|
|
420
|
+
/** The source `resolvePage` finds for `call.ctx`, checked; null when it finds none. */
|
|
421
|
+
const savedSource = async (name, props, lookup) => {
|
|
422
|
+
if (options.resolvePage === void 0) return null;
|
|
423
|
+
let found;
|
|
507
424
|
try {
|
|
508
|
-
|
|
425
|
+
found = await options.resolvePage(name, lookup.signal === void 0 ? { ctx: lookup.ctx } : {
|
|
426
|
+
ctx: lookup.ctx,
|
|
427
|
+
signal: lookup.signal
|
|
428
|
+
});
|
|
509
429
|
} catch (error) {
|
|
510
430
|
if (isJigsError(error)) throw error;
|
|
511
|
-
|
|
512
|
-
throw new JigsError("INTERNAL", INTERNAL_MESSAGE);
|
|
513
|
-
}
|
|
514
|
-
const input = parseBounded(() => chatInputSchema.safeParse({
|
|
515
|
-
blocks: chatOptions?.blocks,
|
|
516
|
-
prompt: chatOptions?.prompt
|
|
517
|
-
}));
|
|
518
|
-
if (!input.success) throw new JigsError("BAD_INPUT", input.message);
|
|
519
|
-
let turnOptions;
|
|
520
|
-
if (options.options === void 0) turnOptions = void 0;
|
|
521
|
-
else {
|
|
522
|
-
const schema = options.options;
|
|
523
|
-
const parsed = parseBounded(() => schema.safeParse(chatOptions?.options ?? {}));
|
|
524
|
-
if (!parsed.success) throw new JigsError("BAD_INPUT", `options: ${parsed.message}`);
|
|
525
|
-
turnOptions = parsed.data;
|
|
431
|
+
throw internal(error, name);
|
|
526
432
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}, input.data.blocks ?? []);
|
|
541
|
-
const turnDeps = {
|
|
542
|
-
call: turnCall,
|
|
543
|
-
options: turnOptions,
|
|
544
|
-
parser,
|
|
545
|
-
prompt: input.data.prompt,
|
|
546
|
-
session: {
|
|
547
|
-
...sessionDeps,
|
|
548
|
-
kind: "chat-repair",
|
|
549
|
-
maxRepairs: repairRounds
|
|
550
|
-
}
|
|
551
|
-
};
|
|
552
|
-
const repair = (block) => repairBlock(turnDeps, block);
|
|
433
|
+
if (found === null) return null;
|
|
434
|
+
const checked = savedPageSchema.safeParse(found);
|
|
435
|
+
if (!checked.success) throw internal(/* @__PURE__ */ new Error(`resolvePage returned ${String(found)} for "${name}"; return { source } or null`), name);
|
|
436
|
+
const source = checked.data.source;
|
|
437
|
+
if (source.length > 2e5) throw internal(/* @__PURE__ */ new Error(`The source resolvePage returned for "${name}" is longer than ${MAX_CONTENT} characters`), name);
|
|
438
|
+
noProps(name, props);
|
|
439
|
+
return source;
|
|
440
|
+
};
|
|
441
|
+
/** A page `resolvePage` finds for `call.ctx`, compiled leniently unless `strict`. */
|
|
442
|
+
const savedPage = async (name, props, lookup) => {
|
|
443
|
+
const source = await savedSource(name, props, lookup);
|
|
444
|
+
if (source === null) return null;
|
|
445
|
+
const compiled = await compileCached(source);
|
|
553
446
|
return {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
results: parser.results,
|
|
558
|
-
stream: (reply) => streamChat({
|
|
559
|
-
end: parser.end,
|
|
560
|
-
push: parser.push,
|
|
561
|
-
repair
|
|
562
|
-
}, reply, chatOptions?.signal),
|
|
563
|
-
system: chatSystem,
|
|
564
|
-
systemPrefix: chatParts.stable
|
|
447
|
+
...lookup.strict === true ? strictly(`Page "${name}"`, compiled) : compiled,
|
|
448
|
+
origin: "saved",
|
|
449
|
+
source
|
|
565
450
|
};
|
|
566
451
|
};
|
|
567
|
-
async
|
|
568
|
-
const
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
452
|
+
const resolve = async (name, props = {}, lookup) => {
|
|
453
|
+
const entry = jigsByName.get(name);
|
|
454
|
+
if (entry?.page === true) return shippedPage(entry, props);
|
|
455
|
+
if (lookup === void 0 || !("ctx" in lookup)) throw new Error(`createJigsServer has no page named "${name}"`);
|
|
456
|
+
return entry === void 0 ? savedPage(name, props, lookup) : null;
|
|
457
|
+
};
|
|
458
|
+
const page = resolve;
|
|
459
|
+
const compile = async (source, compileOptions) => {
|
|
460
|
+
const compiled = await compileCached(source, compileOptions?.placement);
|
|
461
|
+
return compileOptions?.strict === false ? compiled : strictly("The source", compiled);
|
|
462
|
+
};
|
|
463
|
+
/** Whether `pagePolicy` lets this `ctx` read a shipped page; it fails closed. */
|
|
464
|
+
const pageAllowed = (ctx, name, props) => {
|
|
465
|
+
if (options.pagePolicy === void 0) return true;
|
|
466
|
+
try {
|
|
467
|
+
const decided = options.pagePolicy({
|
|
468
|
+
ctx,
|
|
469
|
+
name,
|
|
470
|
+
props
|
|
471
|
+
});
|
|
472
|
+
if (decided !== "allow" && decided !== "deny") throw new Error(`pagePolicy returned ${String(decided)} for "${name}"; return "allow" or "deny", synchronously`);
|
|
473
|
+
return decided === "allow";
|
|
474
|
+
} catch (error) {
|
|
475
|
+
onError(error, {
|
|
476
|
+
name,
|
|
477
|
+
type: "page"
|
|
478
|
+
});
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* A page request from the wire, after `context`: `pagePolicy` for a
|
|
484
|
+
* shipped page, then `page` with `ctx`. A denied page and one no list
|
|
485
|
+
* has answer alike, so a denial does not say the page exists.
|
|
486
|
+
*/
|
|
487
|
+
const pageRequest = async (request, context) => {
|
|
488
|
+
const started = performance.now();
|
|
489
|
+
const { name, props } = request;
|
|
490
|
+
const notFound = {
|
|
491
|
+
code: "NOT_FOUND",
|
|
492
|
+
message: `There is no page named "${name}".`
|
|
493
|
+
};
|
|
494
|
+
let outcome;
|
|
495
|
+
let denied = false;
|
|
496
|
+
const shippedName = name;
|
|
497
|
+
if (jigsByName.get(name)?.page === true && !pageAllowed(context.ctx, shippedName, props)) {
|
|
498
|
+
denied = true;
|
|
499
|
+
outcome = { error: notFound };
|
|
500
|
+
} else try {
|
|
501
|
+
const resolved = await page(name, props, context);
|
|
502
|
+
outcome = resolved === null ? { error: notFound } : {
|
|
503
|
+
diagnostics: resolved.diagnostics,
|
|
504
|
+
program: resolved.program,
|
|
505
|
+
version
|
|
573
506
|
};
|
|
574
|
-
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (!isJigsError(error)) onError(error, {
|
|
509
|
+
name,
|
|
510
|
+
type: "page"
|
|
511
|
+
});
|
|
512
|
+
outcome = { error: errorData(isJigsError(error) ? error : null) };
|
|
575
513
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
headers.set("access-control-allow-credentials", "true");
|
|
588
|
-
headers.set("access-control-expose-headers", VERSION_HEADER);
|
|
589
|
-
headers.append("vary", "Origin");
|
|
514
|
+
const report = {
|
|
515
|
+
ctx: context.ctx,
|
|
516
|
+
durationMs: performance.now() - started,
|
|
517
|
+
input: props,
|
|
518
|
+
kind: "page",
|
|
519
|
+
name,
|
|
520
|
+
outcome: "success"
|
|
521
|
+
};
|
|
522
|
+
if ("error" in outcome) {
|
|
523
|
+
report.error = outcome.error;
|
|
524
|
+
report.outcome = denied ? "denied" : "error";
|
|
590
525
|
}
|
|
591
|
-
|
|
526
|
+
reportCall(report);
|
|
527
|
+
return outcome;
|
|
592
528
|
};
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
529
|
+
/**
|
|
530
|
+
* The source a session opens for a page by name, or null when this
|
|
531
|
+
* `ctx` has no such page: a shipped page `pagePolicy` allows, as a
|
|
532
|
+
* self-contained document for a define page, or a saved one. A tag that
|
|
533
|
+
* is not a page is not one a session may open.
|
|
534
|
+
*/
|
|
535
|
+
const pageSource = async (name, props, context) => {
|
|
536
|
+
const entry = jigsByName.get(name);
|
|
537
|
+
if (entry === void 0) return savedSource(name, props, context);
|
|
538
|
+
const shippedName = name;
|
|
539
|
+
if (!entry.page || !pageAllowed(context.ctx, shippedName, props)) return null;
|
|
540
|
+
if (entry.kind === "document") {
|
|
541
|
+
noProps(name, props);
|
|
542
|
+
return entry.source;
|
|
543
|
+
}
|
|
544
|
+
return checker.pageDocument(name, props);
|
|
601
545
|
};
|
|
602
|
-
const
|
|
546
|
+
const prepareSession = async (body, context) => {
|
|
547
|
+
const input = parseSessionInput(body);
|
|
548
|
+
if ("error" in input) return input;
|
|
549
|
+
const document = input.document;
|
|
550
|
+
if (document === null || "source" in document) return {
|
|
551
|
+
document: document?.source ?? null,
|
|
552
|
+
messages: input.messages
|
|
553
|
+
};
|
|
554
|
+
let source;
|
|
555
|
+
try {
|
|
556
|
+
source = await pageSource(document.name, document.props ?? {}, context);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (!isJigsError(error)) onError(error, {
|
|
559
|
+
name: document.name,
|
|
560
|
+
type: "page"
|
|
561
|
+
});
|
|
562
|
+
return { error: errorData(isJigsError(error) ? error : null) };
|
|
563
|
+
}
|
|
564
|
+
if (source === null) return { error: {
|
|
565
|
+
code: "NOT_FOUND",
|
|
566
|
+
message: `There is no page named "${document.name}".`
|
|
567
|
+
} };
|
|
568
|
+
return {
|
|
569
|
+
document: source,
|
|
570
|
+
messages: input.messages
|
|
571
|
+
};
|
|
572
|
+
};
|
|
573
|
+
const http = createHttp({
|
|
574
|
+
allowedOrigins,
|
|
575
|
+
ownOrigin,
|
|
576
|
+
version
|
|
577
|
+
});
|
|
603
578
|
const frame = (request) => {
|
|
604
579
|
const ancestors = ["'self'", ...allowedOrigins].join(" ");
|
|
605
|
-
const headers = request === void 0 ? new Headers() : corsHeaders(request);
|
|
580
|
+
const headers = request === void 0 ? new Headers() : http.corsHeaders(request);
|
|
606
581
|
if (headers.has("access-control-allow-origin")) headers.set("access-control-expose-headers", `${VERSION_HEADER}, content-security-policy`);
|
|
607
582
|
headers.set("cache-control", "no-cache");
|
|
583
|
+
headers.append("vary", "Accept");
|
|
608
584
|
headers.set("content-security-policy", `${frameSecurityPolicy}; frame-ancestors ${ancestors}`);
|
|
609
585
|
headers.set("content-type", "text/html; charset=utf-8");
|
|
610
586
|
headers.set("etag", `"${version}"`);
|
|
@@ -631,156 +607,94 @@ function createJigsServer(options) {
|
|
|
631
607
|
} };
|
|
632
608
|
}
|
|
633
609
|
};
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
};
|
|
664
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
|
665
|
-
controller.close();
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
if (next.done === true) {
|
|
669
|
-
finish();
|
|
670
|
-
controller.close();
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(next.value)}\n\n`));
|
|
674
|
-
},
|
|
675
|
-
start(controller) {
|
|
676
|
-
heartbeat = setInterval(() => {
|
|
677
|
-
if (!open) return;
|
|
678
|
-
try {
|
|
679
|
-
controller.enqueue(encoder.encode(": keep-alive\n\n"));
|
|
680
|
-
} catch {
|
|
681
|
-
finish();
|
|
682
|
-
}
|
|
683
|
-
}, HEARTBEAT_MS);
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
const headers = corsHeaders(request);
|
|
687
|
-
headers.set("content-type", "text/event-stream; charset=utf-8");
|
|
688
|
-
headers.set("cache-control", "no-store");
|
|
689
|
-
headers.set("x-accel-buffering", "no");
|
|
690
|
-
return new Response(body, {
|
|
610
|
+
/**
|
|
611
|
+
* A session turn with no model: a document that asks nothing is
|
|
612
|
+
* compiled again; a turn that would ask the model is not found here.
|
|
613
|
+
*/
|
|
614
|
+
const restoreOnly = async (body, context) => {
|
|
615
|
+
const prepared = await prepareSession(body, context);
|
|
616
|
+
if ("error" in prepared) return prepared;
|
|
617
|
+
const document = prepared.document;
|
|
618
|
+
if (document === null || asksModel(prepared.messages)) return { error: {
|
|
619
|
+
code: "NOT_FOUND",
|
|
620
|
+
message: "This server has no model, so it cannot answer a turn that asks one."
|
|
621
|
+
} };
|
|
622
|
+
return { async *turn() {
|
|
623
|
+
yield* restoreTurn(checker, host.components.version, document, true);
|
|
624
|
+
} };
|
|
625
|
+
};
|
|
626
|
+
const hostEvents = options.events;
|
|
627
|
+
/**
|
|
628
|
+
* The push stream's answer before `ctx` is needed: a foreign `Origin` is
|
|
629
|
+
* refused, and a server without `events` says so with `204`. Null when
|
|
630
|
+
* the stream may open.
|
|
631
|
+
*/
|
|
632
|
+
const eventsRefusal = (request) => {
|
|
633
|
+
const foreign = http.foreignOrigin(request);
|
|
634
|
+
if (foreign !== null) return http.failure(request, foreign);
|
|
635
|
+
if (hostEvents !== void 0) return null;
|
|
636
|
+
const headers = http.corsHeaders(request);
|
|
637
|
+
headers.append("vary", "Accept");
|
|
638
|
+
return new Response(null, {
|
|
691
639
|
headers,
|
|
692
|
-
status:
|
|
640
|
+
status: 204
|
|
693
641
|
});
|
|
694
642
|
};
|
|
695
|
-
const
|
|
696
|
-
const
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
|
|
711
|
-
const headers = corsHeaders(request);
|
|
712
|
-
headers.set("allow", "GET, POST, OPTIONS");
|
|
713
|
-
return new Response(null, {
|
|
714
|
-
headers,
|
|
715
|
-
status: 405
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
const refused = refusal(request);
|
|
719
|
-
if (refused !== null) return failure(request, refused);
|
|
720
|
-
if (Number(request.headers.get("content-length") ?? "0") > maxBodyBytes) return tooLarge(request);
|
|
721
|
-
let text;
|
|
722
|
-
try {
|
|
723
|
-
text = await readLimited(request, maxBodyBytes);
|
|
724
|
-
} catch {
|
|
725
|
-
return failure(request, {
|
|
726
|
-
code: "BAD_INPUT",
|
|
727
|
-
message: "The request body could not be read."
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
if (text === null) return tooLarge(request);
|
|
731
|
-
let raw;
|
|
732
|
-
try {
|
|
733
|
-
raw = JSON.parse(text);
|
|
734
|
-
} catch {
|
|
735
|
-
return failure(request, {
|
|
736
|
-
code: "BAD_INPUT",
|
|
737
|
-
message: "The request body is not JSON."
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
const body = parseBounded(() => bodySchema.safeParse(raw));
|
|
741
|
-
if (!body.success) return failure(request, {
|
|
742
|
-
code: "BAD_INPUT",
|
|
743
|
-
message: body.message
|
|
744
|
-
});
|
|
745
|
-
const context = await contextFor(request, body.data.type === "call" ? "call" : "session");
|
|
746
|
-
if ("error" in context) return failure(request, context.error);
|
|
747
|
-
if (body.data.type === "call") {
|
|
748
|
-
const callRequest = {
|
|
749
|
-
input: body.data.input,
|
|
750
|
-
kind: body.data.kind,
|
|
751
|
-
name: body.data.name
|
|
752
|
-
};
|
|
753
|
-
if (body.data.confirmation !== void 0) callRequest.confirmation = body.data.confirmation;
|
|
754
|
-
const outcome = await call(callRequest, {
|
|
755
|
-
ctx: context.ctx,
|
|
756
|
-
signal: request.signal
|
|
757
|
-
});
|
|
758
|
-
return json(request, "error" in outcome ? statusOf(outcome.error.code) : 200, outcome);
|
|
759
|
-
}
|
|
760
|
-
const parsed = parseSession(body.data);
|
|
761
|
-
if ("error" in parsed) return failure(request, parsed.error);
|
|
762
|
-
const abort = new AbortController();
|
|
763
|
-
const forward = () => abort.abort();
|
|
764
|
-
if (request.signal.aborted) abort.abort();
|
|
765
|
-
else request.signal.addEventListener("abort", forward, { once: true });
|
|
766
|
-
return stream(request, runSession(sessionDeps, parsed.messages, parsed.options, {
|
|
643
|
+
const events = async (request, context) => {
|
|
644
|
+
const refused = eventsRefusal(request);
|
|
645
|
+
if (refused !== null || hostEvents === void 0) return refused ?? new Response(null, { status: 204 });
|
|
646
|
+
return eventStream({
|
|
647
|
+
events: hostEvents,
|
|
648
|
+
http,
|
|
649
|
+
onError
|
|
650
|
+
}, request, context);
|
|
651
|
+
};
|
|
652
|
+
/** The handler's route for a `GET` that accepts `text/event-stream`. */
|
|
653
|
+
const eventsRoute = async (request) => {
|
|
654
|
+
const refused = eventsRefusal(request);
|
|
655
|
+
if (refused !== null) return refused;
|
|
656
|
+
const context = await contextFor(request, "events");
|
|
657
|
+
if ("error" in context) return http.failure(request, context.error);
|
|
658
|
+
return events(request, {
|
|
767
659
|
ctx: context.ctx,
|
|
768
|
-
signal:
|
|
769
|
-
})
|
|
660
|
+
signal: request.signal
|
|
661
|
+
});
|
|
662
|
+
};
|
|
663
|
+
const handlerDeps = {
|
|
664
|
+
allowedOrigins,
|
|
665
|
+
call,
|
|
666
|
+
contextFor,
|
|
667
|
+
events: eventsRoute,
|
|
668
|
+
frame,
|
|
669
|
+
http,
|
|
670
|
+
maxBodyBytes,
|
|
671
|
+
onError,
|
|
672
|
+
page: pageRequest
|
|
770
673
|
};
|
|
771
|
-
|
|
674
|
+
const core = {
|
|
772
675
|
call,
|
|
773
|
-
chat,
|
|
774
676
|
compile,
|
|
677
|
+
events,
|
|
775
678
|
frame,
|
|
776
|
-
handler
|
|
679
|
+
handler: createHandler({
|
|
680
|
+
...handlerDeps,
|
|
681
|
+
session: restoreOnly
|
|
682
|
+
}),
|
|
777
683
|
maxBodyBytes,
|
|
684
|
+
page,
|
|
778
685
|
prompt: (mode) => mode === "chat" ? chatSystem : system,
|
|
779
686
|
promptPrefix: (mode) => mode === "chat" ? chatParts.stable : documentParts.stable,
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
687
|
+
version,
|
|
688
|
+
withModel: (modelOptions) => createModelServer({
|
|
689
|
+
checker,
|
|
690
|
+
context: options.context,
|
|
691
|
+
handlerDeps,
|
|
692
|
+
host,
|
|
693
|
+
onError,
|
|
694
|
+
prepareSession
|
|
695
|
+
}, core, modelOptions)
|
|
783
696
|
};
|
|
697
|
+
return core;
|
|
784
698
|
}
|
|
785
699
|
//#endregion
|
|
786
700
|
export { createJigsServer, defineRun };
|