narraeon 0.7.7 → 0.7.9

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.
Files changed (24) hide show
  1. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +14 -1
  2. package/dist/node/runtime/history/HistoryRendering.js +25 -6
  3. package/dist/node/runtime/history/HistoryTools.js +31 -0
  4. package/dist/node/runtime/play/PlayCallChain.js +4 -9
  5. package/dist/node/runtime/play/PlayDocumentTools.js +35 -37
  6. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +33 -17
  7. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +5 -9
  8. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +326 -62
  9. package/dist/node/runtime/setting/SettingImprovementSession.js +40 -3
  10. package/dist/node/runtime/world/FileNativeWorldStore.js +13 -3
  11. package/dist/node/runtime/world/WorldDocumentMaintenance.js +9 -2
  12. package/dist/node/runtime/world/WorldLedgerAuthority.js +18 -2
  13. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +61 -17
  14. package/dist/node/runtime/world-revision/LegacyWorldRevisionEpochUpgrade.js +46 -0
  15. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +83 -30
  16. package/dist/node/runtime/world-revision/WorldRevisionSession.js +64 -4
  17. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +32 -11
  18. package/dist/node/shared/default-setting-improvement-prompt.js +23 -8
  19. package/dist/node/shared/ordered-author-prompts.js +9 -7
  20. package/dist/web/assets/index-B2kbaOGe.js +1408 -0
  21. package/dist/web/assets/{index-BjxBYYQi.css → index-DH2BVsYd.css} +1 -1
  22. package/dist/web/index.html +2 -2
  23. package/package.json +1 -1
  24. package/dist/web/assets/index-Da0tdDij.js +0 -1408
@@ -1,4 +1,15 @@
1
1
  import { isPlayPresetBinding, } from "../play/FileNativePlayPresetStore.js";
2
+ export function isAuthoringPreloadRecord(value) {
3
+ if (typeof value !== "object" || value === null || Array.isArray(value))
4
+ return false;
5
+ const record = value;
6
+ return (Object.keys(record).length === 3 &&
7
+ typeof record.revision === "string" &&
8
+ Array.isArray(record.worldDocumentIds) &&
9
+ record.worldDocumentIds.every((id) => typeof id === "string") &&
10
+ Array.isArray(record.opaquePaths) &&
11
+ record.opaquePaths.every((path) => typeof path === "string"));
12
+ }
2
13
  export function validAuthoringRequests(value, validPrompt, session) {
3
14
  if (!Array.isArray(value) ||
4
15
  value.length === 0 ||
@@ -14,7 +25,9 @@ export function validAuthoringRequests(value, validPrompt, session) {
14
25
  if (typeof raw !== "object" || raw === null || Array.isArray(raw))
15
26
  return false;
16
27
  const item = raw;
17
- if (Object.keys(item).length !== 4 ||
28
+ const keys = Object.keys(item).length;
29
+ if ((keys !== 4 && !(keys === 5 && "preload" in item)) ||
30
+ (item.preload !== undefined && !isAuthoringPreloadRecord(item.preload)) ||
18
31
  typeof item.requestId !== "string" ||
19
32
  !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(item.requestId) ||
20
33
  ids.has(item.requestId) ||
@@ -1,5 +1,15 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { parseDocument } from "yaml";
3
+ /** Scope and total only; ordering and continuation rules live in tool descriptions. */
4
+ function compactHeader(total, locale) {
5
+ const zh = locale === "zh-CN";
6
+ return `Scope: ${zh ? "当前时间线" : "current timeline"}\n${zh ? "本次读取快照消息总数" : "Snapshot total messages"}: ${total}`;
7
+ }
8
+ /** Position, role and ref only; role=x would repeat the localized role label. */
9
+ function compactMessageLabel(message, locale) {
10
+ const zh = locale === "zh-CN";
11
+ return `${zh ? "位置" : "position"} ${message.position} · ${historyRoleLabel(message, locale)}${message.isOpening ? " · isOpening=true" : ""} · ${message.ref} · ${message.length} Unicode code points`;
12
+ }
3
13
  /** Shared metadata keeps injected refs and tool results directly navigable. */
4
14
  export function historyMessageLabel(message, locale) {
5
15
  const zh = locale === "zh-CN";
@@ -58,18 +68,26 @@ export function renderHistoryResult(result, locale = "en", legacyRead = false) {
58
68
  if (result.kind === "list")
59
69
  return {
60
70
  ok: true,
61
- markdown: `# ${zh ? "已提交历史列表" : "Committed history listing"}\n\n${header}\nOrder: oldest_first (${zh ? "页内始终由旧到新" : "every page oldest to newest"})\n${result.messages
71
+ markdown: `# ${zh ? "已提交历史列表" : "Committed history listing"}\n\n${compactHeader(result.total, locale)}\n${result.messages
62
72
  .map((message) => {
63
73
  const preview = Array.from(message.text).slice(0, 120).join("");
64
- return `### ${historyMessageLabel(message, locale)}\n${message.length} Unicode code points\n${renderHistoryBackground(message.background, locale)}[${zh ? "部分原文预览" : "Partial original preview"}]\n${preview}\n[${message.length > 120 ? (zh ? "预览有省略" : "preview omitted remaining text") : zh ? "预览结束" : "preview ends"}]`;
74
+ const omitted = message.length > 120
75
+ ? `\n${zh ? "…(预览省略)" : "…(preview omitted)"}`
76
+ : "";
77
+ return `### ${compactMessageLabel(message, locale)}${renderHistoryBackground(message.background, locale, false)}\n${preview}${omitted}`;
65
78
  })
66
- .join("\n\n") || (zh ? "(空页)" : "(empty page)")}\n\n---\n${zh ? "本页位置/条数" : "Page positions / count"}: ${range(result.messages)} / ${result.messages.length}\n${cursorLine("earlierCursor", result.earlierCursor)}\n${cursorLine("laterCursor", result.laterCursor)}\n${zh ? "续页只传 {cursor}。" : "Continue with only {cursor}."}${result.messages.length === 0 && result.anchor !== null ? `\n${zh ? "可从锚点转向浏览" : "Browse from the anchor in the other direction"}: {before:"${result.anchor}"} / {after:"${result.anchor}"}` : ""}`,
79
+ .join("\n\n") || (zh ? "(空页)" : "(empty page)")}\n\n---\n${zh ? "本页位置/条数" : "Page positions / count"}: ${range(result.messages)} / ${result.messages.length}\n${cursorLine("earlierCursor", result.earlierCursor)}\n${cursorLine("laterCursor", result.laterCursor)}${result.messages.length === 0 && result.anchor !== null ? `\n${zh ? "可从锚点转向浏览" : "Browse from the anchor in the other direction"}: {before:"${result.anchor}"} / {after:"${result.anchor}"}` : ""}`,
67
80
  };
68
81
  if (result.kind === "read")
69
82
  return {
70
83
  ok: true,
71
84
  markdown: `# ${zh ? "完整历史原文" : "Complete history originals"}\n\n${header}\nMode: ${result.mode}${result.center === null ? "" : `\nCenter: ${result.center}`}\nOrder: oldest_first\n${zh ? "实际位置" : "Actual positions"}: ${range(result.messages)}\n${zh ? "每条正文完整,无摘要、无截断" : "Each body is complete, without summary or truncation"}\n\n${result.messages.map((message) => renderHistoryMessage(message, locale)).join("\n\n") || (zh ? "(历史为空)" : "(empty history)")}\n\n---\n${cursorLine("earlier", result.earlier)}\n${cursorLine("later", result.later)}\n${result.earlier === null ? (zh ? "已到历史开头。" : "At the history beginning.") : legacyRead ? `context_read {ref:"${result.earlier}"}` : zh ? "向前不重复读取:history_read {ref:earlier,before:N}。" : "Read earlier without repeating this window: history_read {ref:earlier,before:N}."}\n${result.later === null ? (zh ? "已到历史结尾。" : "At the history end.") : legacyRead ? `context_read {ref:"${result.later}"}` : zh ? "向后不重复读取:history_read {ref:later,after:N}。" : "Read later without repeating this window: history_read {ref:later,after:N}."}\n${legacyRead ? (zh ? "按需逐条读取相邻原文。" : "Read adjacent originals one at a time as needed.") : zh ? "按需小步扩展;21 条消息不代表固定输出体积。" : "Expand in small windows as needed; 21 messages do not imply a fixed output size."}`,
72
85
  };
86
+ if (!result.legacy)
87
+ return {
88
+ ok: true,
89
+ markdown: `# ${zh ? "历史字面搜索" : "History literal search"}\n\n${compactHeader(result.total, locale)}\n${zh ? "搜索位置范围(排除边界)" : "Search position range (boundaries excluded)"}: ${result.range.start === null ? "empty" : `${result.range.start}..${result.range.end}`}\n${zh ? "查询词" : "Queries"}: ${JSON.stringify(result.queries)}\n${zh ? "命中消息总数" : "Total matching messages"}: ${result.matches}\n\n${result.messages.map(({ message, hits, fragments, omittedMatches }) => `### ${compactMessageLabel(message, locale)}${renderHistoryBackground(message.background, locale, false)}${zh ? "逐词命中次数" : "Matches per query"}: ${hits.map(({ query, count }) => `${JSON.stringify(query)}: ${count}`).join("; ")}\n${fragments.map((fragment) => `[${zh ? "部分原文片段" : "Partial original fragment"}: ${fragment.start}..${fragment.end}${fragment.omittedBefore ? "; prefix omitted" : ""}${fragment.omittedAfter ? "; suffix omitted" : ""}]\n${fragment.text}`).join("\n")}${omittedMatches ? `\n${zh ? "另有命中未展示;可完整读取核实。" : "Additional matches omitted; read the complete original to verify."}` : ""}`).join("\n\n") || (zh ? "零字面命中不证明事件没有发生;可改词、从 all 放宽为 any 或读取相邻上下文。" : "Zero literal matches do not prove an event never happened. Try other words, relax all to any, or read nearby context.")}\n\n---\n${cursorLine("nextCursor", result.nextCursor)}`,
90
+ };
73
91
  return {
74
92
  ok: true,
75
93
  markdown: `# ${zh ? "历史字面搜索" : "History literal search"}\n\n${header}${result.within === null ? "" : `\nLegacy selected scope: ${result.within}`}\n${zh ? "搜索位置范围(排除边界)" : "Search position range (boundaries excluded)"}: ${result.range.start === null ? "empty" : `${result.range.start}..${result.range.end}`}\nOrder: ${result.order}\nMatch: ${result.match} (${zh ? "同一消息内" : "within one message"})\nNormalization: NFKC${result.caseSensitive ? " (case sensitive)" : " + case folding (locale und)"}\n${zh ? "查询词" : "Queries"}: ${JSON.stringify(result.queries)}\n${zh ? "命中消息总数" : "Total matching messages"}: ${result.matches}\n\n${result.messages.map(({ message, hits, fragments, omittedMatches }) => `### ${historyMessageLabel(message, locale)}\n${message.length} Unicode code points\n${renderHistoryBackground(message.background, locale)}${zh ? "逐词命中次数" : "Matches per query"}: ${hits.map(({ query, count }) => `${JSON.stringify(query)}: ${count}`).join("; ")}\n${fragments.map((fragment) => `[${zh ? "部分原文片段" : "Partial original fragment"}: ${fragment.start}..${fragment.end}${fragment.omittedBefore ? "; prefix omitted" : ""}${fragment.omittedAfter ? "; suffix omitted" : ""}]\n${fragment.text}\n[${zh ? "片段结束" : "fragment ends"}]`).join("\n")}\n${omittedMatches ? (zh ? "另有命中未展示;可完整读取核实。" : "Additional matches omitted; read the complete original to verify.") : zh ? "全部命中均在所示片段内。" : "All matches are covered by these fragments."}`).join("\n\n") || (result.legacy ? (zh ? "零字面命中不证明事件没有发生;可改用其他 query 或读取相邻原文。" : "Zero literal matches do not prove an event never happened. Try another query or read adjacent originals.") : zh ? "零字面命中不证明事件没有发生;可改词、从 all 放宽为 any 或读取相邻上下文。" : "Zero literal matches do not prove an event never happened. Try other words, relax all to any, or read nearby context.")}\n\n---\n${cursorLine(result.legacy ? "Next-page cursor" : "nextCursor", result.nextCursor)}\n${result.legacy ? (zh ? "续页保留原source=history、query、caseSensitive、within与limit并使用返回cursor。" : "Legacy continuation keeps the original source=history, query, caseSensitive, within and limit with the returned cursor.") : zh ? "续页只传 {cursor};已有原文足以判断时停止检索。all 仅限同一消息;相邻消息可改用 any 后读前后文。" : "Continue with only {cursor}; stop when the originals suffice. all applies to one message; use any then read context for terms in adjacent messages."}`,
@@ -87,7 +105,7 @@ function localizedRecovery(result, zh) {
87
105
  return `参数不符合要求,请按工具定义修正。${result.recovery}`;
88
106
  }
89
107
  /** Every result carries its own annotation; no implicit neighboring inheritance. */
90
- export function renderHistoryBackground(background, locale) {
108
+ export function renderHistoryBackground(background, locale, withNotice = true) {
91
109
  if (background === undefined)
92
110
  return "";
93
111
  const zh = locale === "zh-CN";
@@ -99,10 +117,11 @@ export function renderHistoryBackground(background, locale) {
99
117
  const notice = zh
100
118
  ? "历史背景仅说明采集位置的状态,不是当前世界状态;回忆、梦境或段内局部时间结合原文解释,不认证事实或授予角色知识。"
101
119
  : "Historical background describes the captured state, not current world state; interpret memories, dreams and local times with the original text. It does not certify facts or grant character knowledge.";
120
+ const suffix = withNotice ? `\n${notice}` : "";
102
121
  if (background.kind === "snapshot" ||
103
122
  equalBackground(background.before, background.after))
104
- return `\n${zh ? "历史背景" : "Historical background"}:${node(background.kind === "snapshot" ? background.value : background.before)}\n${notice}\n`;
105
- return `\n${zh ? "推进前背景" : "Background before advancement"}:${node(background.before)}\n${zh ? "推进后背景" : "Background after advancement"}:${node(background.after)}\n${notice}\n`;
123
+ return `\n${zh ? "历史背景" : "Historical background"}:${node(background.kind === "snapshot" ? background.value : background.before)}${suffix}\n`;
124
+ return `\n${zh ? "推进前背景" : "Background before advancement"}:${node(background.before)}\n${zh ? "推进后背景" : "Background after advancement"}:${node(background.after)}${suffix}\n`;
106
125
  }
107
126
  function equalBackground(before, after) {
108
127
  if (before === after)
@@ -0,0 +1,31 @@
1
+ import { HistoryQuery, historyInputs } from "./HistoryQuery.js";
2
+ import { renderHistoryResult } from "./HistoryRendering.js";
3
+ export const historyToolNames = [
4
+ "history_list",
5
+ "history_search",
6
+ "history_read",
7
+ ];
8
+ export function isHistoryToolName(name) {
9
+ return historyToolNames.includes(name);
10
+ }
11
+ export function historyEntries(history, backgrounds = {}) {
12
+ return Object.entries(history).map(([path, contents]) => ({
13
+ path,
14
+ contents,
15
+ ...(backgrounds[path] === undefined
16
+ ? {}
17
+ : { background: structuredClone(backgrounds[path]) }),
18
+ }));
19
+ }
20
+ /** Read-only committed-history navigation shared by play and world revision. */
21
+ export function createHistoryQuery(scope, entries) {
22
+ return new HistoryQuery(scope, historyInputs(entries));
23
+ }
24
+ export function runHistoryTool(query, call, locale) {
25
+ const result = call.name === "history_search"
26
+ ? query.search(call.arguments)
27
+ : call.name === "history_read"
28
+ ? query.read(call.arguments)
29
+ : query.list(call.arguments);
30
+ return renderHistoryResult(result, locale);
31
+ }
@@ -2,6 +2,7 @@ import { projectLegacyModelBindingFailure } from "./LegacyModelBindingFailure.js
2
2
  import { dispatchFailureEnding, emptyResponseEnding, lastEndingCancelled, noContinuationEnding, PlayAdvanceLifecycle, settlementFailureEnding, } from "./PlayAdvanceLifecycle.js";
3
3
  import { isResolutionTool, resolutionMarkdown, resolutionToolNames, resolveTabletop, seededRandom, } from "./TabletopResolution.js";
4
4
  import { captureNarrativeOrigin, snapshotBackground, narrativeBackground, } from "../history/HistoryBackground.js";
5
+ import { historyEntries } from "../history/HistoryTools.js";
5
6
  import { isDeepStrictEqual } from "node:util";
6
7
  import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
7
8
  import { renderToolAliasNote, resolveToolName, } from "../prompt/ToolNameAlias.js";
@@ -2163,6 +2164,9 @@ function duplicatePlayerExchange(session, exchangeId, playerText, context) {
2163
2164
  }
2164
2165
  function mergeChangedDocuments(session, changes) {
2165
2166
  for (const change of changes) {
2167
+ // Play and continuity writes never remove a document; only world-revision apply does.
2168
+ if (change.kind === "delete")
2169
+ continue;
2166
2170
  const projected = {
2167
2171
  kind: change.kind,
2168
2172
  ref: `@${change.stableShortRef}`,
@@ -2346,15 +2350,6 @@ function revisionTimelineGeneration(sourceGeneration, operationId, worldId, cont
2346
2350
  .digest("hex")
2347
2351
  .slice(0, 40)}`;
2348
2352
  }
2349
- function historyEntries(history, backgrounds = {}) {
2350
- return Object.entries(history).map(([path, contents]) => ({
2351
- path,
2352
- contents,
2353
- ...(backgrounds[path] === undefined
2354
- ? {}
2355
- : { background: structuredClone(backgrounds[path]) }),
2356
- }));
2357
- }
2358
2353
  function operationId(chainId, key) {
2359
2354
  return `play-${createHash("sha256")
2360
2355
  .update(`${chainId}\0${key}`)
@@ -671,15 +671,15 @@ function knownStateDirectory(snapshot, directory, declaredDirectories) {
671
671
  return snapshot.files.some(({ path }) => path.startsWith(logicalPrefix));
672
672
  }
673
673
  function executeStateList(snapshot, args, declaredDirectories) {
674
+ // Frozen sessions may still send obsolete cursor/limit; directories are
675
+ // listed in full, so those keys are accepted and ignored.
674
676
  if (!record(args) ||
675
677
  !hasOnlyToolKeys(args, ["parent", "cursor", "limit"]) ||
676
678
  typeof args.parent !== "string" ||
677
- parseStateDirectoryHandle(args.parent) === null ||
678
- (args.limit !== undefined && typeof args.limit !== "number") ||
679
- !validOptionalCursor(args.cursor))
679
+ parseStateDirectoryHandle(args.parent) === null)
680
680
  return {
681
681
  ok: false,
682
- markdown: "# Runtime argument error\n\nstate_list requires an @dir-* parent returned by Runtime and accepts only cursor and limit in addition; use history_list for committed history.",
682
+ markdown: "# Runtime argument error\n\nstate_list requires an @dir-* parent returned by Runtime; use history_list for committed history.",
683
683
  };
684
684
  return executeContextList(snapshot, { ...args, source: "state" }, declaredDirectories);
685
685
  }
@@ -699,19 +699,11 @@ function executeContextList(snapshot, args, declaredDirectories = []) {
699
699
  (args.order !== "newest_first" && args.order !== "oldest_first"));
700
700
  if (!hasOnlyToolKeys(args, allowedKeys) ||
701
701
  invalidStateShape ||
702
- invalidHistoryShape ||
703
- (args.limit !== undefined && typeof args.limit !== "number") ||
704
- !validOptionalCursor(args.cursor))
702
+ invalidHistoryShape)
705
703
  return {
706
704
  ok: false,
707
705
  markdown: "# Runtime argument error\n\nstate must use an @dir-* parent returned by the Runtime and omit order; history must use newest_first or oldest_first order and omit parent.",
708
706
  };
709
- const limit = typeof args.limit === "number" ? args.limit : 20;
710
- if (!Number.isInteger(limit) || limit < 1 || limit > 100)
711
- return {
712
- ok: false,
713
- markdown: "# Runtime argument error\n\nlimit must be between 1 and 100.",
714
- };
715
707
  if (args.source === "state") {
716
708
  const parent = args.parent;
717
709
  const directory = parseStateDirectoryHandle(parent);
@@ -722,32 +714,38 @@ function executeContextList(snapshot, args, declaredDirectories = []) {
722
714
  ok: false,
723
715
  markdown: "# Runtime argument error\n\nThe parent is not a Runtime-known state directory; call state_list and descend only through its returned directory handles.",
724
716
  };
725
- const result = snapshot.query({
726
- kind: "catalog",
727
- directory,
728
- includeRetired: true,
729
- declaredDirectories,
730
- limit,
731
- ...(typeof args.cursor === "string" || args.cursor === null
732
- ? { cursor: args.cursor }
733
- : {}),
734
- });
735
- if (result.kind === "error")
736
- return renderStateQueryFailure(result);
737
- if (result.kind !== "catalog")
738
- return unexpectedStateQueryResult();
739
- const entries = result.entries.map((entry) => {
740
- if (entry.kind === "directory") {
741
- const relative = entry.logicalPath.slice(`${snapshot.logicalRoot}/`.length);
742
- return `- Directory ${stateDirectoryHandle(relative)}`;
743
- }
744
- if (entry.document === undefined)
745
- return "- Damaged document (currently not addressable)";
746
- return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.document.retired ? " · retired" : ""}${entry.status === "damaged" ? " · needs repair" : ""}`;
747
- });
717
+ const entries = [];
718
+ let complete = true;
719
+ let cursor = null;
720
+ do {
721
+ const result = snapshot.query({
722
+ kind: "catalog",
723
+ directory,
724
+ includeRetired: true,
725
+ declaredDirectories,
726
+ limit: 100,
727
+ ...(cursor === null ? {} : { cursor }),
728
+ });
729
+ if (result.kind === "error")
730
+ return renderStateQueryFailure(result);
731
+ if (result.kind !== "catalog")
732
+ return unexpectedStateQueryResult();
733
+ if (result.coverage.status !== "complete")
734
+ complete = false;
735
+ entries.push(...result.entries.map((entry) => {
736
+ if (entry.kind === "directory") {
737
+ const relative = entry.logicalPath.slice(`${snapshot.logicalRoot}/`.length);
738
+ return `- Directory ${stateDirectoryHandle(relative)}`;
739
+ }
740
+ if (entry.document === undefined)
741
+ return "- Damaged document (currently not addressable)";
742
+ return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.document.retired ? " · retired" : ""}${entry.status === "damaged" ? " · needs repair" : ""}`;
743
+ }));
744
+ cursor = result.page.nextCursor;
745
+ } while (cursor !== null);
748
746
  return {
749
747
  ok: true,
750
- markdown: `# Directory listing\n\nScope: state · ${parent}\nCoverage: ${result.coverage.status === "complete" ? "complete" : "partial"}\n${entries.join("\n") || "(empty)"}\n\n---\nThis page: ${result.page.start}..${result.page.end} / ${result.page.total} items\nComplete: ${result.page.complete ? "yes" : "no"}${result.page.nextCursor === null ? "" : `\nNext-page cursor: ${result.page.nextCursor}`}`,
748
+ markdown: `# Directory listing\n\nScope: state · ${parent}\nCoverage: ${complete ? "complete" : "partial"}\n${entries.join("\n") || "(empty)"}\n\n---\nComplete: yes`,
751
749
  };
752
750
  }
753
751
  return toolFailure("Use history_list for committed history.");
@@ -148,15 +148,13 @@ export class FileNativePromptCompiler {
148
148
  playPreset: input.playPreset,
149
149
  modelBinding: input.modelBinding,
150
150
  tools: input.tools,
151
+ ...(input.preload === undefined ? {} : { preload: input.preload }),
151
152
  });
152
153
  }
153
154
  compileWorldRevision(input) {
154
155
  const identity = this.#locale === "zh-CN"
155
156
  ? `# 当前修订世界\n\n${input.worldTitle}`
156
157
  : `# World being revised\n\n${input.worldTitle}`;
157
- const boundary = this.#locale === "zh-CN"
158
- ? "# 世界修订工作树边界\n\n随附工具只读写当前锁定 epoch 的 state/* 与 control/* 工作树。成功工具调用尚未进入世界 Authority;只有玩家点击应用才会整体提交并解锁。"
159
- : "# World-revision worktree boundary\n\nThe attached tools read and write only state/* and control/* in the current locked epoch. Successful tool calls do not enter world Authority until the player applies the complete revision and unlocks it.";
160
158
  return this.#compileAuthoringConversation({
161
159
  identity: {
162
160
  source: "world-revision:title",
@@ -165,10 +163,7 @@ export class FileNativePromptCompiler {
165
163
  ? `\n\nEpoch: ${input.epoch.id}\nBase: ${input.epoch.baseHead}`
166
164
  : ""),
167
165
  },
168
- boundary: {
169
- source: "runtime:world-revision-worktree-boundary",
170
- markdown: boundary,
171
- },
166
+ boundary: null,
172
167
  runtimeSource: "runtime:builtin/world-revision",
173
168
  target: "world-revision",
174
169
  gameplay: input.gameplay,
@@ -177,6 +172,7 @@ export class FileNativePromptCompiler {
177
172
  playPreset: input.playPreset,
178
173
  modelBinding: input.modelBinding,
179
174
  tools: input.tools,
175
+ ...(input.preload === undefined ? {} : { preload: input.preload }),
180
176
  });
181
177
  }
182
178
  #compileAuthoringConversation(input) {
@@ -185,7 +181,7 @@ export class FileNativePromptCompiler {
185
181
  const logicalMessages = [];
186
182
  const ordered = input.playPreset.definition.authorPrompts ??
187
183
  legacyAuthorPrompts(input.playPreset.definition);
188
- const catalog = builtinAuthorPrompts(this.#locale);
184
+ const catalog = builtinAuthorPrompts(this.#locale, input.target);
189
185
  for (const entry of ordered) {
190
186
  if (entry.kind === "world" || !entry.enabled)
191
187
  continue;
@@ -210,7 +206,7 @@ export class FileNativePromptCompiler {
210
206
  if (builtin?.id === "author.target")
211
207
  blocks = [
212
208
  input.identity,
213
- input.boundary,
209
+ ...(input.boundary === null ? [] : [input.boundary]),
214
210
  ...(input.gameplay.mode === "tabletop"
215
211
  ? [
216
212
  tabletopAuthoringRulesBlock(input.gameplay, input.target, this.#locale),
@@ -238,6 +234,20 @@ export class FileNativePromptCompiler {
238
234
  blocks,
239
235
  });
240
236
  }
237
+ // Preloaded material follows every author prompt so the stable prefix stays byte-identical.
238
+ if (input.preload !== undefined && input.preload.length > 0) {
239
+ const preloadBlocks = input.preload.map(({ source, markdown }) => ({
240
+ source,
241
+ markdown,
242
+ }));
243
+ logicalMessages.push({
244
+ messageRole: "system",
245
+ promptId: "runtime.preload",
246
+ role: "world_context",
247
+ markdown: joinBlocks(preloadBlocks),
248
+ blocks: preloadBlocks,
249
+ });
250
+ }
241
251
  const tools = structuredClone(input.tools);
242
252
  const toolStrategy = this.#toolStrategyOverride ??
243
253
  defaultRuntimeToolDefinitionStrategy(input.modelBinding.provider);
@@ -870,9 +880,13 @@ function settingImprovementPresetReference(binding, locale, target, gameplay) {
870
880
  return [
871
881
  {
872
882
  ...worldInstructionPlaceholder,
873
- markdown: locale === "zh-CN"
874
- ? "世界指令在此展开;frame 选定材料仅在全新游玩上下文中作为初始工具结果提供。请通过本轮创作工具检查实际内容。"
875
- : "World instructions expand here; frame-selected material is supplied as an initial tool result only for a fresh play context. Inspect actual content through the current authoring tools.",
883
+ markdown: revision
884
+ ? locale === "zh-CN"
885
+ ? "世界指令在此展开;启用块原文见本请求的 Runtime 预载。"
886
+ : "World instructions expand here; enabled block text appears in this request's Runtime preload."
887
+ : locale === "zh-CN"
888
+ ? "世界指令在此展开;frame 选定材料仅在全新游玩上下文中作为初始工具结果提供。请通过本轮创作工具检查实际内容。"
889
+ : "World instructions expand here; frame-selected material is supplied as an initial tool result only for a fresh play context. Inspect actual content through the current authoring tools.",
876
890
  },
877
891
  ];
878
892
  if (entry.kind === "tail" || !entry.enabled)
@@ -949,11 +963,13 @@ function settingImprovementPresetReference(binding, locale, target, gameplay) {
949
963
  : "Player-visible narrative semantics", locale === "zh-CN"
950
964
  ? "这些块只在未来游玩生成玩家可见正文时生效;不得把它们当作本轮 YAML 或 Markdown 设定正文的范文。"
951
965
  : "These blocks apply only when future play produces player-visible story. Do not use them as prose models for YAML or Markdown setting documents in this conversation.", narrativeBlocks),
952
- settingImprovementReferenceGroup(locale === "zh-CN"
953
- ? "本轮提示(每次发送的尾部)"
954
- : "Turn guide (per-send suffix)", locale === "zh-CN"
955
- ? "这些行在每次游玩发送时追加在玩家输入之后,由已启用块的尾部行按同一顺序拼成;它们是提醒,不是世界事实。"
956
- : "These lines are appended after the player input on every play send, assembled from the enabled blocks' turn-guide lines in the same order. They are reminders, not world facts.", turnGuideBlocks),
966
+ revision
967
+ ? null
968
+ : settingImprovementReferenceGroup(locale === "zh-CN"
969
+ ? "本轮提示(每次发送的尾部)"
970
+ : "Turn guide (per-send suffix)", locale === "zh-CN"
971
+ ? "这些行在每次游玩发送时追加在玩家输入之后,由已启用块的尾部行按同一顺序拼成;它们是提醒,不是世界事实。"
972
+ : "These lines are appended after the player input on every play send, assembled from the enabled blocks' turn-guide lines in the same order. They are reminders, not world facts.", turnGuideBlocks),
957
973
  ].filter((group) => group !== null);
958
974
  return [
959
975
  heading,
@@ -134,11 +134,7 @@ function createToolDefinitions(locale) {
134
134
  ...resolutionDefinitions(locale),
135
135
  state_list: {
136
136
  description: descriptions.state_list,
137
- inputSchema: object({
138
- parent: string,
139
- cursor: { type: ["string", "null"] },
140
- limit: { type: "integer", minimum: 1, maximum: 100 },
141
- }, ["parent"]),
137
+ inputSchema: object({ parent: string }, ["parent"]),
142
138
  },
143
139
  history_list: {
144
140
  description: descriptions.history_list,
@@ -294,8 +290,8 @@ function resolutionDefinitions(locale) {
294
290
  const toolDescriptions = {
295
291
  en: {
296
292
  world_checkpoint: "Register a gap between events and suggest that the player start a fresh context here. Arguments must be an empty object. The effect and replay scope are defined by the tools-and-settlement rules; when to register is defined by the checkpoint policy. Consolidation and maintenance do not require this tool; do not advance the story to register a checkpoint.",
297
- state_list: "List document and state directory handles known to Runtime. parent must be @dir-/ for the state root or an @dir-* handle from the initial directory index or an earlier state_list result; no preliminary listing is needed for an already supplied handle. Catalog directories declared by the frozen world frame remain listed and usable when empty. A cursor is valid only for the same state snapshot, parent, and limit. Pass returned document handles to context_read. This tool never lists committed history.",
298
- history_list: "Browse committed history on the current timeline. Omit anchors for the latest page; use only before or after with a Runtime ref (including refs in injected history) for the immediately adjacent page, excluding the anchor. Every page is oldest to newest. limit defaults to 20 (1–100 messages). Previews are only the first 120 Unicode code points. Use independent earlierCursor/laterCursor with {cursor} alone; no first-query parameters on continuation. Cursors bind world, tool, query and immutable ordered history. After history_changed rerun the query. Opening messages are queryable. Read full originals with history_read; positions are snapshot locations, not identities or turns.",
293
+ state_list: "List document and state directory handles known to Runtime. parent must be @dir-/ for the state root or an @dir-* handle from the initial directory index or an earlier state_list result; no preliminary listing is needed for an already supplied handle. Catalog directories declared by the frozen world frame remain listed and usable when empty. Each call returns the whole directory with Complete: yes. Pass returned document handles to context_read. This tool never lists committed history.",
294
+ history_list: "Browse committed history on the current timeline. Omit anchors for the latest page; use only before or after with a Runtime ref (including refs in injected history) for the immediately adjacent page, excluding the anchor. Every page is oldest to newest in Authority acceptance order, not story dates. limit defaults to 20 (1–100 messages). Previews are only the first 120 Unicode code points. Historical background describes the captured state, not current world state; interpret memories, dreams and local times with the original text, and it does not certify facts or grant character knowledge. Use independent earlierCursor/laterCursor with {cursor} alone; no first-query parameters on continuation. Cursors bind world, tool, query and immutable ordered history. After history_changed rerun the query. Opening messages are queryable. Read full originals with history_read; positions are snapshot locations, not identities or turns.",
299
295
  history_search: "Search committed originals on the current timeline with queries: 1–5 nonempty literal strings (max 256 code points each); no query alias or semantic filters. match defaults to any, or all within one message. NFKC normalization always applies; caseSensitive defaults to false (locale-und case folding), normalized duplicate queries collapse. order defaults to newest_first, or oldest_first. limit defaults to 10 (1–50 matching messages). Optional after/before refs exclude timeline boundaries independently of order; after must precede before. Each result has at most 3 partial original fragments, query occurrence counts and complete-original length. Read a hit and nearby context with history_read. Continue with {cursor} alone; history_changed requires a new query.",
300
296
  history_read: "Read complete committed originals on the current timeline, never summarized, truncated or body-paginated. Use {latest:N} alone (1–21 messages), or {ref,before,after} with defaults before=after=0 and sum at most 20; do not mix modes. Ref may come directly from Runtime prompt injections or tools without listing first. Windows are always oldest to newest, including the anchor. earlier/later are immediately outside this window and can be the next ref with before/after to avoid rereading. Boundaries and empty history are normal. Counts are messages, not turns; 21 long messages can produce large output.",
301
297
  context_search: "Search literal source text in state documents. Use source=state; committed history uses history_search. Zero matches means only that the supplied text had no literal match; it does not prove that a world fact does not exist. Player-visible narrative must not recount the search process. within accepts only an @document or @dir-* handle returned by Runtime.",
@@ -308,8 +304,8 @@ const toolDescriptions = {
308
304
  },
309
305
  "zh-CN": {
310
306
  world_checkpoint: "登记事件间隙,向玩家建议从这里开始全新上下文。参数为空对象。生效条件与补入范围见“工具与响应结算”;何时登记见检查点政策。整理与归并不需要调用本工具;不要为了登记而推进剧情。",
311
- state_list: "列出 Runtime 已知的文档与状态目录句柄。状态根目录的 parent 使用 @dir-/;继续下级时使用初始目录索引或 state_list 返回的 @dir-*;已有句柄无需再次列目录确认。冻结世界 frame 声明的 catalog 目录即使为空,也会继续列出并可用。cursor 只对同一状态快照、parent 和 limit 有效。返回的文档句柄交给 context_read;本工具不列出已提交历史。",
312
- history_list: "浏览当前时间线已提交历史。无锚点时取最新一页;before 或 after 只能选一个,取锚点紧邻前/后的一页且不含锚点。页内始终由旧到新。limit 默认20,整数1–100。预览仅含前120个Unicode码点;完整原文用history_read。earlierCursor/laterCursor独立翻页,续页只传{cursor},不可混用首次参数。游标绑定世界、工具、查询和有序历史快照;history_changed须重新查询。开场白可查询。提示词注入中的ref可直接使用,无需再次列举;位置不是身份或回合数。",
307
+ state_list: "列出 Runtime 已知的文档与状态目录句柄。状态根目录的 parent 使用 @dir-/;继续下级时使用初始目录索引或 state_list 返回的 @dir-*;已有句柄无需再次列目录确认。冻结世界 frame 声明的 catalog 目录即使为空,也会继续列出并可用。每次调用整份返回该目录,末尾 Complete: yes。返回的文档句柄交给 context_read;本工具不列出已提交历史。",
308
+ history_list: "浏览当前时间线已提交历史。无锚点时取最新一页;before 或 after 只能选一个,取锚点紧邻前/后的一页且不含锚点。页内始终由旧到新。limit 默认20,整数1–100。排序依据是 Authority 接受顺序,不代表故事内日期。历史背景仅说明采集位置的状态,不是当前世界状态;回忆、梦境或段内局部时间结合原文解释,不认证事实或授予角色知识。预览仅含前120个Unicode码点;完整原文用history_read。earlierCursor/laterCursor独立翻页,续页只传{cursor},不可混用首次参数。游标绑定世界、工具、查询和有序历史快照;history_changed须重新查询。开场白可查询。提示词注入中的ref可直接使用,无需再次列举;位置不是身份或回合数。",
313
309
  history_search: "搜索当前时间线已提交原文。queries必填:1–5个非空字面词,每词最多256个Unicode码点,不接受query别名或语义筛选。match默认any,all限于同一消息。始终NFKC规范化,caseSensitive默认false(locale-und大小写折叠),规范化重复词合并。order默认newest_first,可选oldest_first;limit默认10,整数1–50条命中消息。after/before始终按时间线先后排除边界,after必须早于before,与结果排序无关。每项最多3个部分原文片段、逐词命中次数及完整原文长度;可用history_read读取前后文。续页只传{cursor};history_changed须重新查询。",
314
310
  history_read: "完整读取当前时间线已提交原文,无摘要、无截断、无正文分页。{latest:N}单独读取最近1–21条;或{ref,before,after},before/after默认0且合计最多20;两模式互斥。ref可直接使用Runtime提示词注入或工具结果的历史句柄,不必先列举。窗口始终由旧到新且包含锚点。earlier/later是窗口外紧邻句柄,可作为下一次ref并配before/after避免重复读取;边界与空历史正常。数量单位是消息而非回合;21条长消息仍可能产生大量输出。",
315
311
  context_search: "在状态文档原文字面中搜索,source只能为state;已提交历史使用history_search。0 命中只表示给定文本没有字面命中,不证明世界事实不存在;玩家可见叙事不得复述搜索过程。within 只能使用 Runtime 已返回的 @文档或 @dir-* 句柄。",