@jigs.design/server 0.3.5 → 0.3.6

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 CHANGED
@@ -624,7 +624,7 @@ async function* repairBlock(deps, block) {
624
624
  source: event.source,
625
625
  status: "done"
626
626
  };
627
- const compiled = deps.session.checker.compile(body);
627
+ const compiled = deps.session.compile(body);
628
628
  const { ref } = compiled;
629
629
  if (ref !== null) repaired.ref = ref;
630
630
  if (compiled.toolKeys !== null) repaired.toolKeys = compiled.toolKeys;
@@ -0,0 +1,2 @@
1
+ import "./design.js";
2
+ import { Diagnostic, Program } from "@jigs.design/protocol";
@@ -2,10 +2,11 @@
2
2
  /** The default bound: 32 MB of programs, as JSON. */
3
3
  const DEFAULT_MAX_BYTES = 33554432;
4
4
  /**
5
- * A cache bounded by the size of its programs, least recently used out
6
- * first. A program larger than the whole bound is not kept.
5
+ * A cache bounded by the sum of `sizeOf` over its values, and by
6
+ * `maxEntries`, least recently used out first. A value larger than the
7
+ * whole bound is not kept.
7
8
  */
8
- function memoryCompileCache(maxBytes = DEFAULT_MAX_BYTES) {
9
+ function boundedCache(maxBytes, sizeOf, maxEntries = Number.POSITIVE_INFINITY) {
9
10
  const entries = /* @__PURE__ */ new Map();
10
11
  let bytes = 0;
11
12
  const drop = (key) => {
@@ -28,7 +29,7 @@ function memoryCompileCache(maxBytes = DEFAULT_MAX_BYTES) {
28
29
  },
29
30
  set: (key, value) => {
30
31
  drop(key);
31
- const size = JSON.stringify(value.program).length;
32
+ const size = sizeOf(value);
32
33
  if (size > maxBytes) return;
33
34
  entries.set(key, {
34
35
  size,
@@ -36,7 +37,7 @@ function memoryCompileCache(maxBytes = DEFAULT_MAX_BYTES) {
36
37
  });
37
38
  bytes += size;
38
39
  for (const oldest of entries.keys()) {
39
- if (bytes <= maxBytes) break;
40
+ if (bytes <= maxBytes && entries.size <= maxEntries) break;
40
41
  drop(oldest);
41
42
  }
42
43
  },
@@ -46,6 +47,13 @@ function memoryCompileCache(maxBytes = DEFAULT_MAX_BYTES) {
46
47
  };
47
48
  }
48
49
  /**
50
+ * A cache bounded by the size of its programs, least recently used out
51
+ * first. A program larger than the whole bound is not kept.
52
+ */
53
+ function memoryCompileCache(maxBytes = DEFAULT_MAX_BYTES) {
54
+ return boundedCache(maxBytes, (value) => JSON.stringify(value.program).length);
55
+ }
56
+ /**
49
57
  * The one cache every server in this process shares. Keys start with a
50
58
  * hash of what the server compiles against, so two hosts never read each
51
59
  * other's entries.
@@ -57,4 +65,4 @@ async function sha256(text) {
57
65
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
58
66
  }
59
67
  //#endregion
60
- export { memoryCompileCache, processCompileCache, sha256 };
68
+ export { boundedCache, memoryCompileCache, processCompileCache, sha256 };
package/dist/compile.js CHANGED
@@ -3,7 +3,7 @@ import { JigsCompileError, JigsError, fatalDiagnostics } from "./errors.js";
3
3
  import { inputDiagnostics } from "./inputs.js";
4
4
  import { outputDiagnostics } from "./outputs.js";
5
5
  import { splitPragma, versionDiagnostics } from "./pragma.js";
6
- import { compileJigs, compileLibrary, nearest, spanIn } from "@jigs.design/compiler";
6
+ import { compileJigs, compileLibrary, extendLibrary, nearest, parseJigs, spanIn } from "@jigs.design/compiler";
7
7
  import { TOOL_RESULTS } from "@jigs.design/prompt";
8
8
  import { componentSchema } from "@jigs.design/host";
9
9
  import { PROGRAM_FORMAT } from "@jigs.design/protocol";
@@ -198,6 +198,40 @@ function refOf(tree, pages) {
198
198
  props: sortedProps(props)
199
199
  };
200
200
  }
201
+ /** The element names `source` writes, whether or not the library has them. */
202
+ function writtenNames(source) {
203
+ const names = /* @__PURE__ */ new Set();
204
+ const visit = (nodes) => {
205
+ for (const node of nodes) if (node.type === "element") {
206
+ names.add(node.name);
207
+ visit(node.children);
208
+ }
209
+ };
210
+ visit(parseJigs(source).children);
211
+ return names;
212
+ }
213
+ /**
214
+ * `defines` ordered so each comes after the others it writes, in list
215
+ * order where the order is free; the members of a cycle, and what waits on
216
+ * them, last, in list order. Read from the parse rather than a compile: a
217
+ * compile drops an element its library lacks, so a define compiled before
218
+ * the one it writes would not show it.
219
+ */
220
+ function dependencyOrder(defines) {
221
+ const names = new Set(defines.map((entry) => entry.name));
222
+ const needs = new Map(defines.map((entry) => [entry.name, [...writtenNames(entry.body)].filter((name) => name !== entry.name && names.has(name))]));
223
+ const ordered = [];
224
+ const placed = /* @__PURE__ */ new Set();
225
+ for (let progressed = true; progressed;) {
226
+ progressed = false;
227
+ for (const entry of defines) if (!placed.has(entry.name) && (needs.get(entry.name) ?? []).every((name) => placed.has(name))) {
228
+ ordered.push(entry);
229
+ placed.add(entry.name);
230
+ progressed = true;
231
+ }
232
+ }
233
+ return [...ordered, ...defines.filter((entry) => !placed.has(entry.name))];
234
+ }
201
235
  /**
202
236
  * The host, ready to check sources: its catalog in the compiler's form and
203
237
  * its define jigs compiled into two libraries, one of the tags the model
@@ -298,6 +332,35 @@ function createChecker(host) {
298
332
  };
299
333
  };
300
334
  const compile = (body) => compileWith(body, tagLibrary, tagFacts);
335
+ const extend = (saved) => {
336
+ const extended = extendLibrary(tagLibrary, dependencyOrder(saved).map((entry) => ({
337
+ name: entry.name,
338
+ source: entry.body
339
+ })), catalog);
340
+ const facts = new Map(tagFacts);
341
+ const diagnostics = /* @__PURE__ */ new Map();
342
+ const kept = /* @__PURE__ */ new Set();
343
+ for (const entry of saved) {
344
+ const found = [...extended.diagnostics.get(entry.name) ?? []];
345
+ const define = extended.library.get(entry.name);
346
+ if (define !== void 0 && !tagLibrary.has(entry.name)) {
347
+ const uses = [...define.procedures];
348
+ found.push(...procedureDiagnostics(host, entry.body, uses), ...inputDiagnostics(host, entry.body));
349
+ facts.set(entry.name, {
350
+ tags: define.tags,
351
+ uses
352
+ });
353
+ kept.add(entry.name);
354
+ }
355
+ diagnostics.set(entry.name, found.toSorted((a, b) => a.span.line - b.span.line));
356
+ }
357
+ return {
358
+ compile: (body) => compileWith(body, extended.library, facts),
359
+ diagnostics,
360
+ kept,
361
+ tags: /* @__PURE__ */ new Set([...tags, ...kept])
362
+ };
363
+ };
301
364
  const definePage = (name) => {
302
365
  const define = fullLibrary.get(name);
303
366
  if (define === void 0 || !pages.has(name)) throw new Error(`"${name}" is not a define page on this host`);
@@ -333,6 +396,7 @@ function createChecker(host) {
333
396
  }
334
397
  return {
335
398
  compile,
399
+ extend,
336
400
  needsProps: (name) => definePage(name).props.some((prop) => prop.required),
337
401
  page: (name, props) => compileWith(pageBody(name, props), fullLibrary, allFacts),
338
402
  pageBody,
package/dist/core.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import "./compile.js";
2
1
  import { ContextFunction, JigsServer, JigsServerOptions, RunFunctions } from "./types.js";
3
2
  import "./session.js";
4
3
  import "./handler.js";
4
+ import "./scope.js";
5
5
  import { Catalog, Host, JigDefinition, PageName, Procedures } from "@jigs.design/host";
6
6
  import { JigsErrorData } from "@jigs.design/protocol";
7
7
  //#region src/core.d.ts
package/dist/core.js CHANGED
@@ -8,8 +8,10 @@ import { processCompileCache, sha256 } from "./compile-cache.js";
8
8
  import { eventStream } from "./events.js";
9
9
  import { invalidationsOf } from "./invalidation.js";
10
10
  import { withToolResults } from "./tool-results.js";
11
+ import { checkedTag, listHashInput, processScopeCache, promptTag, savedTagListSchema, savedTagSchema, scanTag, scopeHashInput, tagDiagnostics, withTail } from "./scope.js";
11
12
  import { createModelServer } from "./model-server.js";
12
- import { systemPromptParts } from "@jigs.design/prompt";
13
+ import { RESERVED_TAGS } from "@jigs.design/compiler";
14
+ import { savedTagsSection, systemPromptParts } from "@jigs.design/prompt";
13
15
  import { isCallPolicy } from "@jigs.design/host";
14
16
  import { PROGRAM_FORMAT } from "@jigs.design/protocol";
15
17
  import { z } from "zod";
@@ -380,10 +382,19 @@ function createJigsServer(options) {
380
382
  reportCall(report);
381
383
  return outcome;
382
384
  };
383
- /** A stored source: its pragma's versions are checked, then its body compiled. */
384
- const compileStored = (source, placement = "document") => {
385
+ /** What a request without saved tags compiles and prompts with: the host alone. */
386
+ const hostScope = {
387
+ compile: checker.compile,
388
+ hash: null,
389
+ rows: [],
390
+ saved: [],
391
+ tags: checker.tags,
392
+ tail: ""
393
+ };
394
+ /** A stored source: its pragma's versions are checked, then its body compiled in `scope`. */
395
+ const compileStored = (source, placement = "document", scope = hostScope) => {
385
396
  const stored = splitPragma(source);
386
- const compiled = checker.compile(stored.body);
397
+ const compiled = scope.compile(stored.body);
387
398
  const prefix = source.slice(0, source.length - stored.body.length);
388
399
  const result = {
389
400
  advice: belowPrefix(compiled.advise(placement), prefix),
@@ -395,19 +406,21 @@ function createJigsServer(options) {
395
406
  };
396
407
  /**
397
408
  * `compile` through the process's cache, keyed by what this server
398
- * compiles against, the placement (or "page", for a define page's body,
399
- * which compiles against every define jig) and the body's hash. Only
400
- * final artifacts come here: a reply's partial sources stay on `checker`.
409
+ * compiles against, the scope's hash when the caller has saved tags, the
410
+ * placement (or "page", for a define page's body, which compiles against
411
+ * every define jig and never a scope) and the body's hash. Only final
412
+ * artifacts come here: a reply's partial sources stay on `checker`.
401
413
  */
402
- const cached = async (placement, body, compile) => {
403
- const key = `${await namespace}.${placement}.${await sha256(body)}`;
414
+ const cached = async (placement, body, compile, scope = hostScope) => {
415
+ const hash = await sha256(body);
416
+ const key = scope.hash === null ? `${await namespace}.${placement}.${hash}` : `${await namespace}.${scope.hash}.${placement}.${hash}`;
404
417
  const hit = processCompileCache.get(key);
405
418
  if (hit !== void 0) return hit;
406
419
  const compiled = compile();
407
420
  processCompileCache.set(key, compiled);
408
421
  return compiled;
409
422
  };
410
- const compileCached = (source, placement = "document") => cached(placement, source, () => compileStored(source, placement));
423
+ const compileCached = (source, placement = "document", scope = hostScope) => cached(placement, source, () => compileStored(source, placement, scope), scope);
411
424
  const jigsByName = new Map(host.jigs.map((entry) => [entry.name, entry]));
412
425
  /** A define page's one-tag body compiled; it throws as `pageBody` does. */
413
426
  const compileDefine = (name, props) => {
@@ -488,7 +501,8 @@ function createJigsServer(options) {
488
501
  const savedPage = async (name, props, lookup) => {
489
502
  const saved = await savedSource(name, props, lookup);
490
503
  if (saved === null) return null;
491
- const compiled = await compileCached(saved.source);
504
+ const scope = await scopeFor(lookup);
505
+ const compiled = await compileCached(saved.source, "document", scope);
492
506
  return {
493
507
  ...lookup.strict === true ? strictly(`Page "${name}"`, compiled) : compiled,
494
508
  data: saved.data,
@@ -503,10 +517,12 @@ function createJigsServer(options) {
503
517
  return entry === void 0 ? savedPage(name, props, lookup) : null;
504
518
  };
505
519
  const page = resolve;
506
- const compile = async (source, compileOptions) => {
507
- const compiled = await compileCached(source, compileOptions?.placement);
520
+ /** `compile` in `scope`. Async so a strict failure is a rejection. */
521
+ const compileIn = async (scope, source, compileOptions) => {
522
+ const compiled = await compileCached(source, compileOptions?.placement, scope);
508
523
  return compileOptions?.strict === false ? compiled : strictly("The source", compiled);
509
524
  };
525
+ const compile = (source, compileOptions) => compileIn(hostScope, source, compileOptions);
510
526
  /** Whether `pagePolicy` lets this `ctx` read a shipped page; it fails closed. */
511
527
  const pageAllowed = (ctx, name, props) => {
512
528
  if (options.pagePolicy === void 0) return true;
@@ -594,7 +610,8 @@ function createJigsServer(options) {
594
610
  }
595
611
  return checker.pageDocument(name, props);
596
612
  };
597
- const prepareSession = async (body, context) => {
613
+ /** The session's turn, its named page resolved for `ctx`. */
614
+ const sessionTurn = async (body, context) => {
598
615
  const input = parseSessionInput(body);
599
616
  if ("error" in input) return input;
600
617
  const document = input.document;
@@ -621,6 +638,155 @@ function createJigsServer(options) {
621
638
  messages: input.messages
622
639
  };
623
640
  };
641
+ const prepareSession = async (body, context) => {
642
+ const turn = await sessionTurn(body, context);
643
+ if ("error" in turn) return turn;
644
+ try {
645
+ return {
646
+ scope: await scopeFor(context),
647
+ turn
648
+ };
649
+ } catch (error) {
650
+ if (!isJigsError(error)) onError(error, { type: "tag" });
651
+ return { error: errorData(isJigsError(error) ? error : null) };
652
+ }
653
+ };
654
+ /**
655
+ * Names a saved tag may not take: the catalog's components, the
656
+ * language's own tags and every jig of the host, tag, page-only define
657
+ * or document page. The compiler keeps a define that shadows its library
658
+ * (`shadows_library` is info), so the scope refuses these itself.
659
+ */
660
+ const reservedNames = /* @__PURE__ */ new Set([
661
+ ...host.components.components.map((component) => component.name),
662
+ ...RESERVED_TAGS,
663
+ ...jigsByName.keys()
664
+ ]);
665
+ /** A scanned tag's diagnostics, compiled among `others`, which it replaces one of by name. */
666
+ const checkAmong = (others, tag) => {
667
+ const extended = checker.extend([...others.filter((entry) => entry.name !== tag.name), tag].map((entry) => ({
668
+ body: entry.body,
669
+ name: entry.name
670
+ })));
671
+ return tagDiagnostics(tag, extended.diagnostics.get(tag.name) ?? [], host.components.version);
672
+ };
673
+ /** `checkTag` among a scope's rows: the host's, or a caller's. */
674
+ const checkTagIn = (rows, row, checkOptions) => {
675
+ const parsed = savedTagSchema.safeParse(row);
676
+ if (!parsed.success) throw new JigsError("BAD_INPUT", `A saved tag is { description, example?, source }: ${issuesText(parsed.error)}`);
677
+ const scanned = scanTag(parsed.data, reservedNames);
678
+ if ("message" in scanned) throw new JigsError("BAD_INPUT", scanned.message);
679
+ const diagnostics = checkAmong(rows, scanned);
680
+ if (checkOptions?.strict !== false) {
681
+ const fatal = fatalDiagnostics(diagnostics);
682
+ if (fatal.length > 0) throw new JigsCompileError(`Tag "${scanned.name}"`, fatal);
683
+ }
684
+ return checkedTag(scanned, diagnostics);
685
+ };
686
+ /**
687
+ * The scope for rows the resolver returned, each checked as `checkTag`
688
+ * would and dropped with `onError` when it fails: the host extended by
689
+ * the rest, in dependency order. The host's scope when none is kept.
690
+ */
691
+ const buildScope = async (rows) => {
692
+ const kept = [];
693
+ const names = /* @__PURE__ */ new Set();
694
+ let total = 0;
695
+ for (const [index, row] of rows.entries()) {
696
+ if (row === null) {
697
+ onError(new JigsError("BAD_INPUT", "resolveTags returned an entry that is not { description, example?, source }"), { type: "tag" });
698
+ continue;
699
+ }
700
+ const scanned = scanTag(row, reservedNames);
701
+ if ("message" in scanned) {
702
+ onError(new JigsError("BAD_INPUT", scanned.message), scanned.name === void 0 ? { type: "tag" } : {
703
+ name: scanned.name,
704
+ type: "tag"
705
+ });
706
+ continue;
707
+ }
708
+ if (names.has(scanned.name)) {
709
+ onError(new JigsError("BAD_INPUT", `resolveTags returned <${scanned.name}> twice; the first was kept`), {
710
+ name: scanned.name,
711
+ type: "tag"
712
+ });
713
+ continue;
714
+ }
715
+ if (kept.length >= 50 || total + scanned.source.length > 2e5) {
716
+ onError(/* @__PURE__ */ new Error(`resolveTags returned more than 50 tags or ${MAX_CONTENT} characters of source; <${scanned.name}> and the ${rows.length - index - 1} after it were dropped`), { type: "tag" });
717
+ break;
718
+ }
719
+ kept.push(scanned);
720
+ names.add(scanned.name);
721
+ total += scanned.source.length;
722
+ }
723
+ if (kept.length === 0) return hostScope;
724
+ const extended = checker.extend(kept.map((tag) => ({
725
+ body: tag.body,
726
+ name: tag.name
727
+ })));
728
+ const saved = [];
729
+ for (const tag of kept) {
730
+ const diagnostics = tagDiagnostics(tag, extended.diagnostics.get(tag.name) ?? [], host.components.version);
731
+ if (extended.kept.has(tag.name)) saved.push(checkedTag(tag, diagnostics));
732
+ else onError(new JigsCompileError(`Tag "${tag.name}"`, fatalDiagnostics(diagnostics)), {
733
+ name: tag.name,
734
+ type: "tag"
735
+ });
736
+ }
737
+ if (saved.length === 0) return hostScope;
738
+ return {
739
+ compile: extended.compile,
740
+ hash: await sha256(scopeHashInput(kept)),
741
+ rows: kept,
742
+ saved,
743
+ tags: extended.tags,
744
+ tail: savedTagsSection(kept.filter((tag) => extended.kept.has(tag.name)).map((tag) => promptTag(tag)))
745
+ };
746
+ };
747
+ /**
748
+ * The scope for one request, after `context`: the host's without
749
+ * `resolveTags` or when it returns none, else the host extended by the
750
+ * caller's tags, from the process's scope cache when the same list was
751
+ * built before. A `JigsError` the resolver throws passes through; any
752
+ * other throw, or a value that is not an array, is `INTERNAL`.
753
+ */
754
+ const scopeFor = async (lookup) => {
755
+ const resolveTags = options.resolveTags;
756
+ if (resolveTags === void 0) return hostScope;
757
+ let listed;
758
+ try {
759
+ listed = await resolveTags(lookup.signal === void 0 ? { ctx: lookup.ctx } : {
760
+ ctx: lookup.ctx,
761
+ signal: lookup.signal
762
+ });
763
+ } catch (error) {
764
+ if (isJigsError(error)) throw error;
765
+ onError(error, { type: "tag" });
766
+ throw new JigsError("INTERNAL", INTERNAL_MESSAGE);
767
+ }
768
+ const list = savedTagListSchema.safeParse(listed);
769
+ if (!list.success) {
770
+ onError(/* @__PURE__ */ new Error(`resolveTags returned ${String(listed)}; return an array of { description, example?, source }`), { type: "tag" });
771
+ throw new JigsError("INTERNAL", INTERNAL_MESSAGE);
772
+ }
773
+ const rows = list.data;
774
+ if (rows.length === 0) return hostScope;
775
+ const key = `${await namespace}.${await sha256(listHashInput(rows))}`;
776
+ const hit = processScopeCache.get(key);
777
+ if (hit !== void 0) return hit;
778
+ const scope = await buildScope(rows);
779
+ processScopeCache.set(key, scope);
780
+ return scope;
781
+ };
782
+ /** What `forUser` hands out for a scope. */
783
+ const userScope = (scope) => ({
784
+ checkTag: async (row, checkOptions) => checkTagIn(scope.rows, row, checkOptions),
785
+ compile: async (source, compileOptions) => compileIn(scope, source, compileOptions),
786
+ prompt: (mode) => withTail(mode === "chat" ? chatSystem : system, scope.tail),
787
+ promptPrefix: (mode) => mode === "chat" ? chatParts.stable : documentParts.stable,
788
+ tags: scope.saved
789
+ });
624
790
  const http = createHttp({
625
791
  allowedOrigins,
626
792
  ownOrigin,
@@ -665,13 +831,13 @@ function createJigsServer(options) {
665
831
  const restoreOnly = async (body, context) => {
666
832
  const prepared = await prepareSession(body, context);
667
833
  if ("error" in prepared) return prepared;
668
- const document = prepared.document;
669
- if (document === null || asksModel(prepared.messages)) return { error: {
834
+ const document = prepared.turn.document;
835
+ if (document === null || asksModel(prepared.turn.messages)) return { error: {
670
836
  code: "NOT_FOUND",
671
837
  message: "This server has no model, so it cannot answer a turn that asks one."
672
838
  } };
673
839
  return { async *turn() {
674
- yield* restoreTurn(checker, host.components.version, document, true);
840
+ yield* restoreTurn(prepared.scope.compile, host.components.version, document, true);
675
841
  } };
676
842
  };
677
843
  const hostEvents = options.events;
@@ -731,9 +897,11 @@ function createJigsServer(options) {
731
897
  };
732
898
  const core = {
733
899
  call,
900
+ checkTag: async (row, checkOptions) => checkTagIn(hostScope.rows, row, checkOptions),
734
901
  compile,
735
902
  data,
736
903
  events,
904
+ forUser: async (lookup) => userScope(await scopeFor(lookup)),
737
905
  frame,
738
906
  handler: createHandler({
739
907
  ...handlerDeps,
@@ -745,12 +913,12 @@ function createJigsServer(options) {
745
913
  promptPrefix: (mode) => mode === "chat" ? chatParts.stable : documentParts.stable,
746
914
  version,
747
915
  withModel: (modelOptions) => createModelServer({
748
- checker,
749
916
  context: options.context,
750
917
  handlerDeps,
751
918
  host,
752
919
  onError,
753
920
  prepareSession,
921
+ scopeFor,
754
922
  toolPrompt: toolParts === null ? null : {
755
923
  system: `${toolParts.stable}\n\n${toolParts.variable}`,
756
924
  systemPrefix: toolParts.stable
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { Advice, DesignCheckCode, Placement } from "./design.js";
2
2
  import { ConfirmationStore, memoryConfirmations } from "./confirm.js";
3
- import { CallContext, CallOutcome, CallReport, CallReportOutcome, CallRequest, ChatBlockResult, ChatBlockSource, ChatEvent, ChatOptions, ChatTurn, CompileOptions, ContextFunction, EditFailureReason, ErrorInfo, EventSubscriber, EventsFunction, JigsModelServer, JigsServer, JigsServerOptions, ModelFunction, ModelOptions, ModelRequest, ModelRequestKind, PageCall, PageFunction, PagePolicy, PageResolver, PolicyCall, PushInvalidate, PushInvalidatePages, PushInvalidation, PushPage, PushSend, RequiresAction, ResolvedPage, RunFunctions, SavedPage, SessionRequest, Turn, TurnEdits, TurnRewrite, TurnRound, Usage } from "./types.js";
3
+ import { CallContext, CallOutcome, CallReport, CallReportOutcome, CallRequest, ChatBlockResult, ChatBlockSource, ChatEvent, ChatOptions, ChatTurn, CheckTagOptions, CheckedTag, CompileOptions, ContextFunction, EditFailureReason, ErrorInfo, EventSubscriber, EventsFunction, JigsModelServer, JigsServer, JigsServerOptions, ModelFunction, ModelOptions, ModelRequest, ModelRequestKind, PageCall, PageFunction, PagePolicy, PageResolver, PolicyCall, PushInvalidate, PushInvalidatePages, PushInvalidation, PushPage, PushSend, RequiresAction, ResolvedPage, RunFunctions, SavedPage, SavedTag, SessionRequest, TagResolver, Turn, TurnEdits, TurnRewrite, TurnRound, Usage, UserScope } from "./types.js";
4
4
  import { chatBlockText } from "./chat.js";
5
5
  import { JigsCompileError, JigsError, ThrownCode } from "./errors.js";
6
6
  import { createJigsServer, defineRun } from "./core.js";
7
7
  import { addUsage } from "./usage.js";
8
8
  import { PromptMode, askOf, withDocument } from "@jigs.design/prompt";
9
9
  import { ChatBlock, ChatBlockStatus, Diagnostic, JigsErrorCode, JigsErrorData, Message, PageProps, PageRef, Program, SessionDocument, SessionEvent, Severity, Span } from "@jigs.design/protocol";
10
- export { type Advice, type CallContext, type CallOutcome, type CallReport, type CallReportOutcome, type CallRequest, type ChatBlock, type ChatBlockResult, type ChatBlockSource, type ChatBlockStatus, type ChatEvent, type ChatOptions, type ChatTurn, type CompileOptions, type ConfirmationStore, type ContextFunction, type DesignCheckCode, type Diagnostic, type EditFailureReason, type ErrorInfo, type EventSubscriber, type EventsFunction, JigsCompileError, JigsError, type JigsErrorCode, type JigsErrorData, type JigsModelServer, type JigsServer, type JigsServerOptions, type Message, type ModelFunction, type ModelOptions, type ModelRequest, type ModelRequestKind, type PageCall, type PageFunction, type PagePolicy, type PageProps, type PageRef, type PageResolver, type Placement, type PolicyCall, type Program, type PromptMode, type PushInvalidate, type PushInvalidatePages, type PushInvalidation, type PushPage, type PushSend, type RequiresAction, type ResolvedPage, type RunFunctions, type SavedPage, type SessionDocument, type SessionEvent, type SessionRequest, type Severity, type Span, type ThrownCode, type Turn, type TurnEdits, type TurnRewrite, type TurnRound, type Usage, addUsage, askOf, chatBlockText, createJigsServer, defineRun, memoryConfirmations, withDocument };
10
+ export { type Advice, type CallContext, type CallOutcome, type CallReport, type CallReportOutcome, type CallRequest, type ChatBlock, type ChatBlockResult, type ChatBlockSource, type ChatBlockStatus, type ChatEvent, type ChatOptions, type ChatTurn, type CheckTagOptions, type CheckedTag, type CompileOptions, type ConfirmationStore, type ContextFunction, type DesignCheckCode, type Diagnostic, type EditFailureReason, type ErrorInfo, type EventSubscriber, type EventsFunction, JigsCompileError, JigsError, type JigsErrorCode, type JigsErrorData, type JigsModelServer, type JigsServer, type JigsServerOptions, type Message, type ModelFunction, type ModelOptions, type ModelRequest, type ModelRequestKind, type PageCall, type PageFunction, type PagePolicy, type PageProps, type PageRef, type PageResolver, type Placement, type PolicyCall, type Program, type PromptMode, type PushInvalidate, type PushInvalidatePages, type PushInvalidation, type PushPage, type PushSend, type RequiresAction, type ResolvedPage, type RunFunctions, type SavedPage, type SavedTag, type SessionDocument, type SessionEvent, type SessionRequest, type Severity, type Span, type TagResolver, type ThrownCode, type Turn, type TurnEdits, type TurnRewrite, type TurnRound, type Usage, type UserScope, addUsage, askOf, chatBlockText, createJigsServer, defineRun, memoryConfirmations, withDocument };
@@ -3,6 +3,7 @@ import { splitPragma } from "./pragma.js";
3
3
  import { createHandler, issuesText, parseBounded } from "./handler.js";
4
4
  import { MAX_CONTENT, runSession } from "./session.js";
5
5
  import { createReplyParser, repairBlock, streamChat } from "./chat.js";
6
+ import { withTail } from "./scope.js";
6
7
  import { TOOL_RESULTS } from "@jigs.design/prompt";
7
8
  import { z } from "zod";
8
9
  //#region src/model-server.ts
@@ -25,7 +26,10 @@ function createModelServer(internals, core, options) {
25
26
  const maxRepairs = options.maxRepairs ?? 1;
26
27
  const programIntervalMs = options.programIntervalMs ?? 250;
27
28
  const designRepair = new Set(options.designRepair ?? []);
28
- /** A session request checked, its options parsed, and a named document resolved for `ctx`. */
29
+ /**
30
+ * A session request checked, its options parsed, a named document
31
+ * resolved for `ctx`, and the scope `ctx` compiles and prompts in.
32
+ */
29
33
  const prepareSession = async (body, call) => {
30
34
  let turnOptions;
31
35
  if (options.options === void 0) turnOptions = void 0;
@@ -37,33 +41,47 @@ function createModelServer(internals, core, options) {
37
41
  } };
38
42
  turnOptions = parsed.data;
39
43
  }
40
- const turn = await internals.prepareSession(body, call);
41
- return "error" in turn ? turn : {
44
+ const prepared = await internals.prepareSession(body, call);
45
+ return "error" in prepared ? prepared : {
42
46
  options: turnOptions,
43
- turn
47
+ scope: prepared.scope,
48
+ turn: prepared.turn
44
49
  };
45
50
  };
46
- const sessionDeps = {
47
- checker: internals.checker,
48
- designRepair,
49
- host: internals.host,
50
- kind: "session",
51
- maxRepairs,
52
- model: options.model,
53
- onError: internals.onError,
54
- onTurn: options.onTurn,
55
- programIntervalMs,
51
+ const toolPrompt = internals.toolPrompt;
52
+ const documentPrompt = {
56
53
  system,
57
54
  systemPrefix: core.promptPrefix("document")
58
55
  };
59
- const toolPrompt = internals.toolPrompt;
60
- const toolSessionDeps = toolPrompt === null ? sessionDeps : {
61
- ...sessionDeps,
62
- system: toolPrompt.system,
63
- systemPrefix: toolPrompt.systemPrefix
56
+ /**
57
+ * A turn's deps in `scope`: its compile, and the document prompt (the
58
+ * slot's when `tools` and the server has one) with the scope's saved
59
+ * tags at the end.
60
+ */
61
+ const sessionDeps = (scope, tools) => {
62
+ const prompt = tools && toolPrompt !== null ? toolPrompt : documentPrompt;
63
+ return {
64
+ compile: scope.compile,
65
+ designRepair,
66
+ host: internals.host,
67
+ kind: "session",
68
+ maxRepairs,
69
+ model: options.model,
70
+ onError: internals.onError,
71
+ onTurn: options.onTurn,
72
+ programIntervalMs,
73
+ savedTags: scope.saved.map((tag) => tag.name),
74
+ system: withTail(prompt.system, scope.tail),
75
+ systemPrefix: prompt.systemPrefix,
76
+ systemTail: scope.tail
77
+ };
64
78
  };
65
- /** A session's deps: the slot's prompt when its document reads `toolResults`. */
66
- const depsFor = (turn) => toolPrompt !== null && turn.document !== null && Object.hasOwn(internals.checker.compile(splitPragma(turn.document).body).program.procedures, TOOL_RESULTS) ? toolSessionDeps : sessionDeps;
79
+ /**
80
+ * A session's deps: the slot's prompt when its document reads
81
+ * `toolResults`, compiled in the request's scope, so a document that
82
+ * reads it only through a saved tag gets it too.
83
+ */
84
+ const depsFor = (turn, scope) => sessionDeps(scope, toolPrompt !== null && turn.document !== null && Object.hasOwn(scope.compile(splitPragma(turn.document).body).program.procedures, TOOL_RESULTS));
67
85
  const chat = async (request, chatOptions) => {
68
86
  const refused = internals.handlerDeps.http.refusal(request);
69
87
  if (refused !== null) throw new JigsError(refused.code, refused.message);
@@ -92,15 +110,16 @@ function createModelServer(internals, core, options) {
92
110
  ctx,
93
111
  signal: chatOptions?.signal ?? new AbortController().signal
94
112
  };
113
+ const scope = await internals.scopeFor(turnCall);
95
114
  const repairRounds = Math.max(0, maxRepairs - 1);
96
115
  const parser = createReplyParser({
97
116
  catalogVersion: internals.host.components.version,
98
- compile: (body) => internals.checker.compile(body),
117
+ compile: (body) => scope.compile(body),
99
118
  designRepair,
100
119
  now: () => performance.now(),
101
120
  programIntervalMs,
102
121
  repairs: maxRepairs > 0,
103
- tags: internals.checker.tags
122
+ tags: scope.tags
104
123
  }, input.data.blocks ?? []);
105
124
  const turnDeps = {
106
125
  call: turnCall,
@@ -108,7 +127,7 @@ function createModelServer(internals, core, options) {
108
127
  parser,
109
128
  prompt: input.data.prompt,
110
129
  session: {
111
- ...toolSessionDeps,
130
+ ...sessionDeps(scope, true),
112
131
  kind: "chat-repair",
113
132
  maxRepairs: repairRounds
114
133
  }
@@ -124,23 +143,26 @@ function createModelServer(internals, core, options) {
124
143
  push: parser.push,
125
144
  repair
126
145
  }, reply, chatOptions?.signal),
127
- system: chatSystem,
128
- systemPrefix: core.promptPrefix("chat")
146
+ system: withTail(chatSystem, scope.tail),
147
+ systemPrefix: core.promptPrefix("chat"),
148
+ systemTail: scope.tail
129
149
  };
130
150
  };
131
151
  return {
132
152
  call: core.call,
133
153
  chat,
154
+ checkTag: core.checkTag,
134
155
  compile: core.compile,
135
156
  data: core.data,
136
157
  events: core.events,
158
+ forUser: core.forUser,
137
159
  frame: core.frame,
138
160
  handler: createHandler({
139
161
  ...internals.handlerDeps,
140
162
  session: async (body, call) => {
141
163
  const prepared = await prepareSession(body, call);
142
164
  if ("error" in prepared) return prepared;
143
- return { turn: (turnCall) => runSession(depsFor(prepared.turn), prepared.turn, prepared.options, turnCall) };
165
+ return { turn: (turnCall) => runSession(depsFor(prepared.turn, prepared.scope), prepared.turn, prepared.options, turnCall) };
144
166
  }
145
167
  }),
146
168
  maxBodyBytes: core.maxBodyBytes,
@@ -156,7 +178,7 @@ function createModelServer(internals, core, options) {
156
178
  };
157
179
  return;
158
180
  }
159
- yield* runSession(depsFor(prepared.turn), prepared.turn, prepared.options, context);
181
+ yield* runSession(depsFor(prepared.turn, prepared.scope), prepared.turn, prepared.options, context);
160
182
  },
161
183
  version: core.version
162
184
  };
@@ -0,0 +1 @@
1
+ import { Diagnostic, Span } from "@jigs.design/protocol";
@@ -0,0 +1,7 @@
1
+ import "./compile.js";
2
+ import "./types.js";
3
+ import "./compile-cache.js";
4
+ import "./pragma.js";
5
+ import "@jigs.design/prompt";
6
+ import { Diagnostic } from "@jigs.design/protocol";
7
+ import { z } from "zod";
package/dist/scope.js ADDED
@@ -0,0 +1,150 @@
1
+ import { belowPrefix, splitPragma, versionDiagnostics } from "./pragma.js";
2
+ import { MAX_CONTENT } from "./session.js";
3
+ import { boundedCache } from "./compile-cache.js";
4
+ import { jigSignature } from "@jigs.design/compiler";
5
+ import { scanDefine } from "@jigs.design/host";
6
+ import { z } from "zod";
7
+ //#region src/scope.ts
8
+ /** The longest description a saved tag may have, in characters. */
9
+ const MAX_DESCRIPTION = 300;
10
+ /** The longest example a saved tag may have, in characters. */
11
+ const MAX_EXAMPLE = 300;
12
+ /** The longest signature a saved tag may render to, in characters. */
13
+ const MAX_SIGNATURE = 500;
14
+ /** What the scope cache holds at most: 32 MB of sources and tails. */
15
+ const MAX_SCOPE_BYTES = 33554432;
16
+ /** How many scopes the scope cache holds at most. */
17
+ const MAX_SCOPES = 256;
18
+ /** A line terminator, which a description, an example or a signature may not hold. */
19
+ const LINE_BREAK = /[\r\n\u2028\u2029]/;
20
+ /** What `resolveTags` must return each entry as, checked, since a plain-JavaScript one may not. */
21
+ const savedTagSchema = z.object({
22
+ description: z.string(),
23
+ example: z.string().exactOptional(),
24
+ source: z.string()
25
+ });
26
+ /**
27
+ * What `resolveTags` must return, checked: an array, each entry a tag or,
28
+ * when it is not one, null, so the scope drops that entry and keeps the rest.
29
+ */
30
+ const savedTagListSchema = z.array(savedTagSchema.or(z.unknown().transform(() => null)));
31
+ /**
32
+ * The scan of one saved tag before anything compiles: one `<define>` with a
33
+ * tag name that is not `reserved`, and a description, example and rendered
34
+ * signature on one line each and within their limits.
35
+ */
36
+ function scanTag(row, reserved) {
37
+ if (row.source.length > 2e5) return { message: `A saved tag's source is at most ${MAX_CONTENT} characters` };
38
+ const scanned = scanDefine(row.source);
39
+ if ("reason" in scanned) {
40
+ if (scanned.reason === "document") return { message: "A saved tag's source must be one <define name=\"…\"> and nothing else" };
41
+ return scanned.name === void 0 ? { message: "A saved tag's <define> needs a literal name, e.g. <define name=\"groceryRun\">" } : {
42
+ message: `Tag name "${scanned.name}" must start with a lowercase letter and use letters and digits only`,
43
+ name: scanned.name
44
+ };
45
+ }
46
+ const { name } = scanned;
47
+ if (reserved.has(name)) return {
48
+ message: `"${name}" is taken by the app: a component, a language tag or one of its jigs. Name the tag something else`,
49
+ name
50
+ };
51
+ const lines = [[
52
+ "description",
53
+ row.description,
54
+ MAX_DESCRIPTION
55
+ ], [
56
+ "example",
57
+ row.example,
58
+ MAX_EXAMPLE
59
+ ]];
60
+ const split = splitPragma(row.source);
61
+ const signature = jigSignature({
62
+ name,
63
+ source: split.body
64
+ });
65
+ lines.push([
66
+ "signature",
67
+ signature,
68
+ MAX_SIGNATURE
69
+ ]);
70
+ for (const [field, text, limit] of lines) {
71
+ if (text === void 0) continue;
72
+ if (LINE_BREAK.test(text)) return {
73
+ message: `<${name}>'s ${field} must be one line`,
74
+ name
75
+ };
76
+ if (text.length > limit) return {
77
+ message: `<${name}>'s ${field} is longer than ${limit} characters`,
78
+ name
79
+ };
80
+ }
81
+ const tag = {
82
+ body: split.body,
83
+ description: row.description,
84
+ name,
85
+ prefix: row.source.slice(0, row.source.length - split.body.length),
86
+ signature,
87
+ source: row.source,
88
+ versions: split.versions
89
+ };
90
+ if (row.example !== void 0) tag.example = row.example;
91
+ return tag;
92
+ }
93
+ /**
94
+ * A scanned tag's diagnostics in its row's source: its pragma's against
95
+ * `catalogVersion`, then `found`, numbered by its body, moved below the pragma.
96
+ */
97
+ function tagDiagnostics(tag, found, catalogVersion) {
98
+ return [...versionDiagnostics(tag.versions, catalogVersion), ...belowPrefix(found, tag.prefix)];
99
+ }
100
+ /** A scanned tag as `checkTag` and `UserScope.tags` show it. */
101
+ function checkedTag(tag, diagnostics) {
102
+ const checked = {
103
+ description: tag.description,
104
+ diagnostics,
105
+ name: tag.name,
106
+ signature: tag.signature
107
+ };
108
+ if (tag.example !== void 0) checked.example = tag.example;
109
+ return checked;
110
+ }
111
+ /** A kept tag as the prompt lists it: its signature read from its body. */
112
+ function promptTag(tag) {
113
+ const listed = {
114
+ description: tag.description,
115
+ name: tag.name,
116
+ source: tag.body
117
+ };
118
+ if (tag.example !== void 0) listed.example = tag.example;
119
+ return listed;
120
+ }
121
+ /** The kept rows as tuples, so no two lists hash alike by concatenation. */
122
+ function scopeHashInput(tags) {
123
+ return JSON.stringify(tags.map((tag) => [
124
+ tag.name,
125
+ tag.source,
126
+ tag.description,
127
+ tag.example ?? null
128
+ ]));
129
+ }
130
+ /** A resolved list as tuples, an entry that is not a `SavedTag` as null: the scope cache's key. */
131
+ function listHashInput(rows) {
132
+ return JSON.stringify(rows.map((row) => row === null ? null : [
133
+ row.source,
134
+ row.description,
135
+ row.example ?? null
136
+ ]));
137
+ }
138
+ /**
139
+ * Scopes by `${namespace}.${list hash}`, shared by every server in this
140
+ * process, as the compile cache is: bounded by the kept sources and the
141
+ * tail, in characters, and by count. On a hit nothing compiles and nothing
142
+ * reaches `onError` again.
143
+ */
144
+ const processScopeCache = boundedCache(MAX_SCOPE_BYTES, (scope) => scope.tail.length + scope.rows.reduce((sum, tag) => sum + tag.source.length, 0), MAX_SCOPES);
145
+ /** `system` with the caller's saved tags after it, or as it is with none. */
146
+ function withTail(system, tail) {
147
+ return tail === "" ? system : `${system}\n\n${tail}`;
148
+ }
149
+ //#endregion
150
+ export { checkedTag, listHashInput, processScopeCache, promptTag, savedTagListSchema, savedTagSchema, scanTag, scopeHashInput, tagDiagnostics, withTail };
package/dist/session.js CHANGED
@@ -107,9 +107,9 @@ function applyChecked(base, edits, compile) {
107
107
  * A turn that starts from a document and asks nothing: its source is
108
108
  * checked and compiled again, with no model call.
109
109
  */
110
- function* restoreTurn(checker, catalogVersion, content, fresh) {
110
+ function* restoreTurn(compile, catalogVersion, content, fresh) {
111
111
  const restored = splitPragma(content);
112
- const compiled = checker.compile(restored.body);
112
+ const compiled = compile(restored.body);
113
113
  const diagnostics = [...versionDiagnostics(restored.versions, catalogVersion), ...belowPrefix(compiled.diagnostics, restored.versions === null ? pragmaLine(catalogVersion) : content.slice(0, content.length - restored.body.length))];
114
114
  yield {
115
115
  diagnostics,
@@ -151,13 +151,13 @@ async function* runSession(deps, turn, options, call) {
151
151
  if (deps.kind === "chat-repair") return "chat-repair";
152
152
  return round === 0 ? "session" : "repair";
153
153
  };
154
- const compile = (body) => deps.checker.compile(body);
154
+ const compile = (body) => deps.compile(body);
155
155
  const placement = deps.kind === "session" ? "document" : "chat";
156
156
  /** Whether a round already asked for the design changes `designRepair` names. */
157
157
  let designRepaired = false;
158
158
  const { messages } = turn;
159
159
  if (turn.document !== null && !asksModel(messages)) {
160
- yield* restoreTurn(deps.checker, catalogVersion, turn.document, true);
160
+ yield* restoreTurn(deps.compile, catalogVersion, turn.document, true);
161
161
  return;
162
162
  }
163
163
  const conversation = messages.map((message) => message.role === "assistant" ? {
@@ -226,7 +226,8 @@ async function* runSession(deps, turn, options, call) {
226
226
  options,
227
227
  signal: call.signal,
228
228
  system: deps.system,
229
- systemPrefix: deps.systemPrefix
229
+ systemPrefix: deps.systemPrefix,
230
+ systemTail: deps.systemTail
230
231
  })[Symbol.asyncIterator]();
231
232
  try {
232
233
  for (;;) {
@@ -404,6 +405,7 @@ async function* runSession(deps, turn, options, call) {
404
405
  options,
405
406
  repairs,
406
407
  rounds,
408
+ savedTags: deps.savedTags,
407
409
  source: finalSource,
408
410
  usage
409
411
  });
package/dist/types.d.ts CHANGED
@@ -33,7 +33,11 @@ export interface ModelRequest<TContext, TOptions> {
33
33
  messages: ReadonlyArray<Message>;
34
34
  options: TOptions;
35
35
  signal: AbortSignal;
36
- /** The document prompt rendered from the host, with the server's `instructions`. */
36
+ /**
37
+ * The document prompt for this request: the host's, with the server's
38
+ * `instructions`, then the caller's saved tags (`systemTail`) when
39
+ * `resolveTags` returned any.
40
+ */
37
41
  system: string;
38
42
  /**
39
43
  * The start of `system` that is the same on every turn: the language and
@@ -41,6 +45,13 @@ export interface ModelRequest<TContext, TOptions> {
41
45
  * provider's prompt cache, as `aiSdkModel` does.
42
46
  */
43
47
  systemPrefix: string;
48
+ /**
49
+ * The end of `system` that differs per user: the `## Saved tags` section
50
+ * for the caller's saved tags, or "" with none. Mark it as a third part
51
+ * (`cachedInstructions(system, systemPrefix, systemTail)`), so the host's
52
+ * part between stays cached for every user.
53
+ */
54
+ systemTail: string;
44
55
  }
45
56
  /**
46
57
  * The product's call to its LLM. Yield text as it arrives. A generator may
@@ -142,6 +153,11 @@ export interface Turn<TContext, TOptions> {
142
153
  * a stop or a failure is not a round.
143
154
  */
144
155
  rounds: ReadonlyArray<TurnRound>;
156
+ /**
157
+ * The names of the caller's saved tags the prompt listed, in its order;
158
+ * empty without `resolveTags`, or when it returned none.
159
+ */
160
+ savedTags: ReadonlyArray<string>;
145
161
  /** The checked source sent with `done`, with its pragma; null when the turn did not finish. */
146
162
  source: string | null;
147
163
  usage: Usage;
@@ -281,13 +297,17 @@ export interface EventSubscriber<TProcedures extends Procedures, TContext, TPage
281
297
  */
282
298
  export type EventsFunction<TProcedures extends Procedures, TContext, TPageName extends string = string> = (subscriber: EventSubscriber<TProcedures, TContext, TPageName>) => (() => void) | Promise<() => void>;
283
299
  export interface ErrorInfo {
284
- /** The procedure, for a call; the page, for a page. */
300
+ /**
301
+ * The procedure, for a call; the page, for a page; the saved tag, for a
302
+ * tag, when the entry had a name to read.
303
+ */
285
304
  name?: string;
286
305
  /**
287
306
  * The request it came from: a call, a session turn, a chat turn with its
288
- * repairs, the push stream, or a page (`jigs.page` or its wire request).
307
+ * repairs, the push stream, a page (`jigs.page` or its wire request), or
308
+ * a saved tag (`resolveTags`, or one entry it returned that was dropped).
289
309
  */
290
- type: "call" | "chat" | "events" | "page" | "session";
310
+ type: "call" | "chat" | "events" | "page" | "session" | "tag";
291
311
  }
292
312
  /** A page `resolvePage` found: the source as the app stored it, and what it reads. */
293
313
  export interface SavedPage {
@@ -311,6 +331,89 @@ export type PageResolver<TContext> = (name: string, call: {
311
331
  ctx: TContext;
312
332
  signal?: AbortSignal;
313
333
  }) => Promise<SavedPage | null> | SavedPage | null;
334
+ /**
335
+ * A tag a user saved, as the app stores it: one `<define>`, which names
336
+ * it, and what the model is told about it. A saved tag is never a page.
337
+ */
338
+ export interface SavedTag {
339
+ /**
340
+ * One line for the model: when to reach for this tag. At most 300
341
+ * characters, with no line break.
342
+ */
343
+ description: string;
344
+ /** A one-line use, shown to the model. At most 300 characters. */
345
+ example?: string;
346
+ /**
347
+ * One `<define>` and nothing else, with or without a pragma line; at
348
+ * most 200,000 characters.
349
+ */
350
+ source: string;
351
+ }
352
+ /**
353
+ * The caller's saved tags, for the `ctx` that `context` returned, in the
354
+ * order the prompt lists them. Runs on every chat turn, session turn,
355
+ * saved-page request and `forUser`; cache in it if the store is slow.
356
+ * Throw a `JigsError` to refuse the request.
357
+ */
358
+ export type TagResolver<TContext> = (call: {
359
+ ctx: TContext;
360
+ signal?: AbortSignal;
361
+ }) => Promise<ReadonlyArray<SavedTag>> | ReadonlyArray<SavedTag>;
362
+ /** A saved tag checked: what the prompt shows for it, and what the checks found. */
363
+ export interface CheckedTag {
364
+ description: string;
365
+ /**
366
+ * What the checks found in its body, numbered by the lines of its
367
+ * source, pragma included: the compiler's diagnostics, a procedure the
368
+ * host lacks, a wrong literal input, a pragma for another catalog.
369
+ */
370
+ diagnostics: ReadonlyArray<Diagnostic>;
371
+ example?: string;
372
+ /** The name its `<define>` gives it. */
373
+ name: string;
374
+ /** The heading the prompt shows, such as `<groceryRun week>`. */
375
+ signature: string;
376
+ }
377
+ export interface CheckTagOptions {
378
+ /**
379
+ * Reject with a `JigsCompileError` when the tag's body has an error or
380
+ * warning diagnostic. Default true; pass false to get the diagnostics
381
+ * instead.
382
+ */
383
+ strict?: boolean;
384
+ }
385
+ /**
386
+ * The server for one caller: the host extended by the tags `resolveTags`
387
+ * returned for their `ctx`, resolved once. Pass it where a server is taken
388
+ * for compiling, such as `reviveJigs`.
389
+ */
390
+ export interface UserScope {
391
+ /**
392
+ * Checks `tag` as the scope would list it, against the host and this
393
+ * caller's other saved tags, a saved tag of its name replaced by it.
394
+ * Rejects with `JigsError("BAD_INPUT")` for what the scope would drop
395
+ * (not one `<define>`, a name that is not a tag name or is the catalog's,
396
+ * the language's or the host's, a description, example or signature
397
+ * with a line break or over its limit, a source over 200,000
398
+ * characters), and, when `strict`, with a `JigsCompileError` for an error
399
+ * or warning diagnostic.
400
+ */
401
+ checkTag: (tag: SavedTag, options?: CheckTagOptions) => Promise<CheckedTag>;
402
+ /**
403
+ * `jigs.compile` in this caller's scope: their saved tags compile into the
404
+ * programs that use them. Strict unless told otherwise.
405
+ */
406
+ compile: JigsServer<unknown>["compile"];
407
+ /** `jigs.prompt(mode)` with this caller's `## Saved tags` at the end. */
408
+ prompt: (mode?: PromptMode) => string;
409
+ /** `jigs.promptPrefix(mode)`: the same for every caller. */
410
+ promptPrefix: (mode?: PromptMode) => string;
411
+ /**
412
+ * The saved tags the scope kept, in the order the prompt lists them,
413
+ * with their lenient diagnostics.
414
+ */
415
+ tags: ReadonlyArray<CheckedTag>;
416
+ }
314
417
  /**
315
418
  * Whether a shipped page may be served on the wire to the caller `ctx`
316
419
  * names. Saved pages are scoped by `resolvePage` instead.
@@ -392,6 +495,16 @@ export interface JigsServerOptions<TProcedures extends Procedures, TCatalog exte
392
495
  * call says `strict`. See `PageResolver`.
393
496
  */
394
497
  resolvePage?: PageResolver<NoInfer<TContext>>;
498
+ /**
499
+ * The caller's saved tags, on every request that compiles or prompts:
500
+ * chat and session turns, saved pages and `forUser`. Each request builds
501
+ * its scope from them: the host's tags extended by the user's, compiled
502
+ * leniently, listed as `## Saved tags` at the end of the prompt, and
503
+ * part of the compile cache's key. A bad entry is dropped and reported
504
+ * to `onError` with `type: "tag"`; past 50 tags or 200,000 characters of
505
+ * source, the rest are. Shipped pages never see them. See `TagResolver`.
506
+ */
507
+ resolveTags?: TagResolver<NoInfer<TContext>>;
395
508
  run: RunFunctions<NoInfer<TProcedures>, NoInfer<TContext>>;
396
509
  /**
397
510
  * Signs confirmation tokens, at least 32 characters; keep it out of the
@@ -601,10 +714,15 @@ export interface ChatTurn {
601
714
  results: () => Array<ChatBlockResult>;
602
715
  /** `push`, `end` and `repair` together; ends after every repair settles. */
603
716
  stream: (reply: AsyncIterable<string>) => AsyncIterable<ChatEvent>;
604
- /** The chat-mode prompt, `prompt("chat")`, with the server's `instructions`. */
717
+ /**
718
+ * The chat-mode prompt for this request: `prompt("chat")`, with the
719
+ * server's `instructions`, then the caller's saved tags (`systemTail`).
720
+ */
605
721
  system: string;
606
722
  /** The start of `system` that is the same on every turn, as on `ModelRequest`. */
607
723
  systemPrefix: string;
724
+ /** The end of `system` that differs per user, or "", as on `ModelRequest`. */
725
+ systemTail: string;
608
726
  }
609
727
  /** A page, compiled, and what the checks said about it. */
610
728
  export interface ResolvedPage {
@@ -665,6 +783,11 @@ export interface PageFunction<TContext, TPageName extends string> {
665
783
  }
666
784
  export interface JigsServer<TContext, TPageName extends string = never> {
667
785
  call: (request: CallRequest, context: CallContext<TContext>) => Promise<CallOutcome>;
786
+ /**
787
+ * Checks a saved tag against the host alone, as `UserScope.checkTag`
788
+ * does in a caller's scope.
789
+ */
790
+ checkTag: (tag: SavedTag, options?: CheckTagOptions) => Promise<CheckedTag>;
668
791
  /** Checks and compiles a source with no model. Strict unless told otherwise. */
669
792
  compile: (source: string, options?: CompileOptions) => Promise<{
670
793
  /** The design checks' advice, numbered like `diagnostics`. */
@@ -692,6 +815,17 @@ export interface JigsServer<TContext, TPageName extends string = never> {
692
815
  * accepts `text/event-stream`, after `context`.
693
816
  */
694
817
  events: (request: Request, context: CallContext<TContext>) => Promise<Response>;
818
+ /**
819
+ * The scope for one caller, for server-side code that has their `ctx`:
820
+ * `resolveTags` runs once, and the scope compiles, checks tags and
821
+ * renders prompts with their saved tags. Without `resolveTags`, the host
822
+ * alone. Rejects as `resolveTags` fails: a `JigsError` it throws, or
823
+ * `INTERNAL`.
824
+ */
825
+ forUser: (call: {
826
+ ctx: TContext;
827
+ signal?: AbortSignal;
828
+ }) => Promise<UserScope>;
695
829
  /** The sandbox frame document, with its own content security policy. */
696
830
  frame: (request?: Request) => Response;
697
831
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jigs.design/server",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "createJigsServer: checks and streams programs, runs procedures, and serves the sandbox frame.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,11 +29,11 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@jigs.design/compiler": "0.3.5",
33
- "@jigs.design/host": "0.3.5",
34
- "@jigs.design/prompt": "0.3.5",
35
- "@jigs.design/protocol": "0.3.5",
36
- "@jigs.design/runtime": "0.3.5"
32
+ "@jigs.design/compiler": "0.3.6",
33
+ "@jigs.design/host": "0.3.6",
34
+ "@jigs.design/prompt": "0.3.6",
35
+ "@jigs.design/protocol": "0.3.6",
36
+ "@jigs.design/runtime": "0.3.6"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "zod": "^4.0.0"