narraeon 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/node/protocol/appPreferences.js +25 -0
  2. package/dist/node/protocol/conversationObservation.js +67 -0
  3. package/dist/node/protocol/v1.js +186 -34
  4. package/dist/node/protocol/worldExtensions.js +16 -0
  5. package/dist/node/protocol/worldMaintenance.js +77 -0
  6. package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
  7. package/dist/node/runtime/ConversationChanges.js +24 -0
  8. package/dist/node/runtime/V1Runtime.js +445 -121
  9. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
  10. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
  11. package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
  12. package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
  13. package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +43 -13
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
  16. package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
  17. package/dist/node/runtime/content/PackageFollowups.js +80 -0
  18. package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
  21. package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
  22. package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
  23. package/dist/node/runtime/model/ModelConnectionStore.js +13 -0
  24. package/dist/node/runtime/model/ModelHost.js +31 -1
  25. package/dist/node/runtime/model/ModelHostStream.js +51 -5
  26. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
  27. package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
  28. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
  29. package/dist/node/runtime/play/OrderedFollowups.js +71 -0
  30. package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
  31. package/dist/node/runtime/play/PlayCallChain.js +814 -61
  32. package/dist/node/runtime/play/PlayContinuity.js +88 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
  36. package/dist/node/runtime/play/PlayPromptRun.js +86 -0
  37. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
  38. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
  39. package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
  40. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
  41. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
  42. package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
  43. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
  44. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
  45. package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
  46. package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
  47. package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
  48. package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
  49. package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
  50. package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
  51. package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
  52. package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
  53. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
  54. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
  55. package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
  56. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
  57. package/dist/node/server/ConversationStream.js +109 -0
  58. package/dist/node/server/createServer.js +5 -1
  59. package/dist/node/shared/artifact-payload-contract.js +123 -0
  60. package/dist/node/shared/default-play-prompts.js +25 -45
  61. package/dist/node/shared/default-preset-host.js +114 -138
  62. package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
  63. package/dist/node/shared/display-regex.js +114 -0
  64. package/dist/node/shared/first-party-action-choices.js +1 -6
  65. package/dist/node/shared/first-party-player-view.js +1 -6
  66. package/dist/node/shared/ordered-author-prompts.js +64 -0
  67. package/dist/node/shared/ordered-followups.js +42 -0
  68. package/dist/node/shared/ordered-play-prompts.js +73 -0
  69. package/dist/web/assets/index-DG85hpWC.css +1 -0
  70. package/dist/web/assets/index-DfxvCjb6.js +1207 -0
  71. package/dist/web/index.html +2 -2
  72. package/package.json +1 -1
  73. package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
  74. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
  75. package/dist/web/assets/index-D8YKfmNG.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -1,4 +1,13 @@
1
+ import { effectiveFollowupDefinitions } from "../play/OrderedFollowups.js";
2
+ import { legacyAuthorPrompts } from "../play/FileNativePlayPresetStore.js";
3
+ import { builtinAuthorPrompts, authoringMechanics, } from "../../shared/ordered-author-prompts.js";
4
+ import { builtinPlayPrompts } from "../../shared/ordered-play-prompts.js";
5
+ import { parseOrderedPlayPrompts } from "../play/OrderedPlayPrompts.js";
6
+ import { renderDocumentWritePosition } from "./WorldMaintenanceReport.js";
1
7
  import { parseDocument, stringify } from "yaml";
8
+ import { measureWorldMaterials, refreshMaintenanceTotals, } from "./WorldMaintenanceReport.js";
9
+ import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
10
+ import { checkpointHistory, checkpointReplayBlocks, playerInputAppend, isPlayerRoundMarker, } from "../play/PlayContinuity.js";
2
11
  import { defaultAppLocale, } from "../../protocol/appPreferences.js";
3
12
  import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
4
13
  import { defaultRuntimeToolDefinitionStrategy, registeredRuntimeToolNames, runtimeToolsForNames, } from "./FileNativeToolRegistry.js";
@@ -22,7 +31,8 @@ const runtimeContracts = {
22
31
  tools: `# Runtime tool contract
23
32
 
24
33
  - Use only tools attached to the current request. Read results state their scope, cursor, and completeness. Directories must use @dir-* handles returned by Runtime; documents and history must use @handles returned by Runtime. Never substitute a world/ path or natural-language name for a handle.
25
- - A document marked \`full text injected\` in the material-coverage report already appears byte-for-byte in the request and already carries write authorization. Do not read it again merely to confirm structure, check fields, or “be safe”: context_read would return the same bytes and waste a round trip. Read only when you need material the report says is not covered.
34
+ - A document marked \`full body injected\` in the material-coverage report already has its complete writable body in the request and carries write authorization. Do not read it again merely to confirm body structure, check body fields, or “be safe.” context_read additionally exposes the current title, summary, and aliases. world_patch preserves metadata fields omitted from set_metadata, so do not read merely to copy unchanged metadata. Read only when a decision depends on unseen metadata or material the report says is not covered.
35
+ - A YAML world_patch locator may mix map keys and zero-based array indexes; remove deletes exactly one existing key or array item. Retire an entity that should leave future frame catalogs with world_retire instead of destroying its document; retired documents remain listed, readable, referenceable, and restorable.
26
36
  - Zero literal-search matches do not prove that a fact does not exist. Tools, directories, documents, archives, searches, matches, handles, Runtime, and failure processes are private adjudication details. Player-visible narrative must not mention internal phrases such as “nothing was found.” When information is insufficient, preserve uncertainty inside the world instead of inventing an internal process.
27
37
  - World-write tools change only an uncommitted working copy. Their changes are not official world facts until Runtime accepts and completes the commit.`,
28
38
  operation: `# Runtime call-chain rules
@@ -33,7 +43,11 @@ const runtimeContracts = {
33
43
  - Runtime performs state commits and saves follow-up artifacts. Tool exchanges and internal processing must never appear in player-visible content.`,
34
44
  shell: `# Runtime play boundary
35
45
 
36
- A tool-free response may contain player-visible story text. A response that calls any tool is an intermediate step and must not contain player-visible story text; continue from the tool results, then narrate in a later tool-free response. Runtime executes only real tool definitions, file validation, and authority commits. This block does not define story content, point of view, style, player agency, or state semantics.`,
46
+ A nonempty tool-free response ends the model/tool loop started by the current player submission. A response that calls any tool is an intermediate step and must not contain player-visible story text; continue from the tool results, then finish in a later tool-free response.
47
+
48
+ The player's next submission may choose a fresh context; the old model transcript will not enter that request. Fresh contexts include all committed original player inputs and final narratives after the last effective world_checkpoint, excluding tools, reasoning, and the opening. A checkpoint declaration takes effect only after its final narrative commits. Author instructions decide save timing.
49
+
50
+ Runtime executes only real tool definitions, file validation, and authority commits. This block does not define story content, point of view, style, player agency, or state semantics.`,
37
51
  },
38
52
  "zh-CN": {
39
53
  play: `# Runtime 权限边界
@@ -44,7 +58,8 @@ A tool-free response may contain player-visible story text. A response that call
44
58
  tools: `# Runtime 工具契约
45
59
 
46
60
  - 只使用请求随附的工具;读取结果会明确范围、cursor 与完整性。目录只能使用 Runtime 返回的 @dir-*,文档和历史只能使用 Runtime 返回的 @句柄,不要把 world/路径或自然语言名称冒充句柄。
47
- - 材料覆盖报告标为 \`已注入全文\` 的文档,其完整原文已经逐字在本次请求里,写入资格同样已经具备。不要为确认结构、核对字段或"保险起见"重读它:context_read 只会原样返回你已经看到的字节,白费一次往返。只有需要报告未覆盖的内容时才读。
61
+ - 材料覆盖报告标为 \`已注入完整正文\` 的文档,其完整可写正文已经在本次请求里,写入资格同样已经具备。不要为确认正文结构、核对正文字段或“保险起见”重读它。context_read 会额外显示当前 title、summary 和 aliases;world_patch 会保留 set_metadata 中未提供的元数据字段,因此不要只为照抄未改变的元数据而读取。只有后续决策确实依赖尚未显示的元数据,或材料覆盖报告未覆盖的内容时才读。
62
+ - YAML world_patch 的 locator 可混合 map key 与从 0 开始的数组下标;remove 只删除一个精确的已有键或数组项。对象需要退出后续 frame catalog 时用 world_retire 退役,不要销毁文档;退役文档仍可列出、读取、引用和恢复。
48
63
  - 字面搜索 0 命中不证明事实不存在。工具、目录、文档、档案、检索、命中、句柄、Runtime 及失败过程只供私下裁决;玩家可见叙事不得出现“没搜到/没找到资料”等内部措辞。信息不足时保持世界内的不确定性,不编造内部过程。
49
64
  - 世界写入工具只修改尚未提交的工作副本;Runtime 接受并完成提交前,这些修改都不是正式世界事实。`,
50
65
  operation: `# Runtime 调用链规则
@@ -55,7 +70,11 @@ A tool-free response may contain player-visible story text. A response that call
55
70
  - 状态提交与后置产物保存由 Runtime 执行。工具交换与内部处理过程不得混入玩家可见内容。`,
56
71
  shell: `# Runtime 游玩边界
57
72
 
58
- 不调用工具的响应可以输出玩家可见故事正文。只要响应调用了任何工具,它就是工具中间步,不得同时输出玩家可见故事正文;先根据工具结果继续,再用一个不调用工具的后续响应完成叙事。Runtime 只执行真实工具定义、文件校验和权威提交;本段不规定故事、人称、文风、玩家代理权或状态语义。`,
73
+ 非空且不调用工具的响应会结束本次玩家提交触发的模型/工具循环。只要响应调用了任何工具,它就是工具中间步,不得同时输出玩家可见故事正文;先根据工具结果继续,再用一个不调用工具的后续响应结束。
74
+
75
+ 下一次玩家提交可以选择“全新上下文”;旧模型 transcript 不会进入那个请求。新上下文会补入最近一次已生效 world_checkpoint 之后的全部已提交玩家原文与最终叙事,不含工具、推理和开场白。检查点登记只在其最终叙事提交后生效。具体保存时机由作者提示规定。
76
+
77
+ Runtime 只执行真实工具定义、文件校验和权威提交;本段不规定故事、人称、文风、玩家代理权或状态语义。`,
59
78
  },
60
79
  };
61
80
  const playCallChainToolNames = new Set([
@@ -65,10 +84,15 @@ const playCallChainToolNames = new Set([
65
84
  "context_read",
66
85
  "world_patch",
67
86
  "world_create",
87
+ "world_retire",
88
+ "world_checkpoint",
68
89
  ]);
69
90
  export class FileNativePromptCompiler {
70
91
  #toolStrategyOverride;
71
92
  #locale;
93
+ get locale() {
94
+ return this.#locale;
95
+ }
72
96
  constructor(options = {}) {
73
97
  this.#toolStrategyOverride = options.toolStrategy;
74
98
  this.#locale = options.locale ?? defaultAppLocale;
@@ -76,20 +100,185 @@ export class FileNativePromptCompiler {
76
100
  setLocale(locale) {
77
101
  this.#locale = locale;
78
102
  }
103
+ /**
104
+ * Freeze the stable prefix for one setting-improvement conversation.
105
+ * Package contents stay behind tools while each user message is appended to
106
+ * the same model-directed authoring conversation.
107
+ */
108
+ compileSettingImprovement(input) {
109
+ const currentTreeBoundary = this.#locale === "zh-CN"
110
+ ? "# 内容包当前树写入边界\n\n当前内容包只可通过随附工具读取和修改。完整工具响应中的成功变化由 Runtime 原子发布后立即生效,发布后无需另一步确认。"
111
+ : "# Content-package current-tree write boundary\n\nThe current content package can be read and changed only through the attached tools. Successful changes in a complete tool response take effect as soon as Runtime publishes them atomically, with no second confirmation after publication.";
112
+ return this.#compileAuthoringConversation({
113
+ identity: {
114
+ source: "content-package:title",
115
+ markdown: settingContentPackageIdentity(input.contentPackageTitle, this.#locale),
116
+ },
117
+ boundary: {
118
+ source: "runtime:setting-current-tree-boundary",
119
+ markdown: currentTreeBoundary,
120
+ },
121
+ runtimeSource: "runtime:builtin/setting-improvement",
122
+ target: "setting",
123
+ runtimeContract: input.runtimeContract,
124
+ authorPrompt: input.authorPrompt,
125
+ playPreset: input.playPreset,
126
+ modelBinding: input.modelBinding,
127
+ tools: input.tools,
128
+ });
129
+ }
130
+ compileWorldRevision(input) {
131
+ const identity = this.#locale === "zh-CN"
132
+ ? `# 当前修订世界\n\n${input.worldTitle}`
133
+ : `# World being revised\n\n${input.worldTitle}`;
134
+ const boundary = this.#locale === "zh-CN"
135
+ ? "# 世界修订工作树边界\n\n随附工具只读写当前锁定 epoch 的 state/* 与 control/* 工作树。成功工具调用尚未进入世界 Authority;只有玩家点击应用才会整体提交并解锁。"
136
+ : "# 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.";
137
+ return this.#compileAuthoringConversation({
138
+ identity: {
139
+ source: "world-revision:title",
140
+ markdown: identity +
141
+ (input.epoch
142
+ ? `\n\nEpoch: ${input.epoch.id}\nBase: ${input.epoch.baseHead}`
143
+ : ""),
144
+ },
145
+ boundary: {
146
+ source: "runtime:world-revision-worktree-boundary",
147
+ markdown: boundary,
148
+ },
149
+ runtimeSource: "runtime:builtin/world-revision",
150
+ target: "world-revision",
151
+ runtimeContract: input.runtimeContract,
152
+ authorPrompt: input.authorPrompt,
153
+ playPreset: input.playPreset,
154
+ modelBinding: input.modelBinding,
155
+ tools: input.tools,
156
+ });
157
+ }
158
+ #compileAuthoringConversation(input) {
159
+ validateModel(input.modelBinding);
160
+ const presetReference = settingImprovementPresetReference(input.playPreset, this.#locale, input.target);
161
+ const logicalMessages = [];
162
+ const ordered = input.playPreset.definition.authorPrompts ??
163
+ legacyAuthorPrompts(input.playPreset.definition);
164
+ const catalog = builtinAuthorPrompts(this.#locale);
165
+ for (const entry of ordered) {
166
+ if (entry.kind === "world" || !entry.enabled)
167
+ continue;
168
+ const builtin = entry.kind === "builtin"
169
+ ? catalog.find((item) => item.id === entry.builtin)
170
+ : undefined;
171
+ let blocks = [
172
+ {
173
+ source: `play-preset:author/${entry.id}`,
174
+ markdown: entry.kind === "user" ? entry.body : builtin.body,
175
+ },
176
+ ];
177
+ if (builtin?.id === "author.mechanics")
178
+ blocks = [
179
+ {
180
+ source: input.runtimeSource,
181
+ markdown: authoringMechanics(this.#locale, input.target),
182
+ },
183
+ ];
184
+ if (builtin?.id === "author.target")
185
+ blocks = [input.identity, input.boundary];
186
+ if (builtin?.id === "author.play-reference")
187
+ blocks = [
188
+ {
189
+ source: "play-preset:author-reference",
190
+ markdown: presetReference,
191
+ },
192
+ ];
193
+ logicalMessages.push({
194
+ role: builtin?.required
195
+ ? "runtime_system"
196
+ : builtin?.id === "author.play-reference"
197
+ ? "world_context"
198
+ : "author_instruction",
199
+ markdown: joinBlocks(blocks),
200
+ blocks,
201
+ });
202
+ }
203
+ const tools = structuredClone(input.tools);
204
+ const toolStrategy = this.#toolStrategyOverride ??
205
+ defaultRuntimeToolDefinitionStrategy(input.modelBinding.provider);
206
+ const cacheStrategy = input.modelBinding.cacheStrategy ??
207
+ (input.modelBinding.provider === "anthropic_messages"
208
+ ? "explicit_anthropic_blocks"
209
+ : "provider_managed");
210
+ const stableText = cacheStableText(logicalMessages);
211
+ const cache = stableCacheBoundary(stableText, tools, toolStrategy);
212
+ return {
213
+ logicalMessages,
214
+ provider: mapProvider(input.modelBinding.provider, logicalMessages, cacheStrategy, true),
215
+ tools: structuredClone(tools),
216
+ toolUniverse: structuredClone(tools),
217
+ toolStrategy,
218
+ coverage: [],
219
+ budget: {
220
+ estimator: "disabled",
221
+ messageTokens: 0,
222
+ toolTokens: 0,
223
+ outputReserveTokens: input.modelBinding.maxOutputTokens,
224
+ forcedTailReserveTokens: 0,
225
+ safetyMarginTokens: 0,
226
+ requiredTokens: 0,
227
+ contextWindowTokens: input.modelBinding.contextWindowTokens,
228
+ status: "not_checked",
229
+ },
230
+ cache: {
231
+ ...cache,
232
+ strategy: cacheStrategy,
233
+ breakpoints: cacheStrategy === "explicit_anthropic_blocks"
234
+ ? ["runtime_system", "author_instruction", "world_context"]
235
+ : cacheStrategy === "explicit_cliproxyapi_message"
236
+ ? ["author_instruction", "world_context"]
237
+ : [],
238
+ },
239
+ };
240
+ }
79
241
  renderWorldDocument(snapshot, selector) {
80
242
  const rendered = renderSnapshotDocument(snapshot, selector);
81
243
  if (rendered.kind === "error")
82
244
  throwQueryFailure(rendered, "read_document");
83
245
  return rendered.markdown;
84
246
  }
85
- async sendBootstrap(input, adapter) {
86
- const compiled = this.compileBootstrap(input);
87
- return await adapter.sendBootstrap({
88
- provider: compiled.provider,
89
- tools: compiled.tools,
90
- modelId: input.modelBinding.modelId,
91
- maxOutputTokens: input.modelBinding.maxOutputTokens,
92
- });
247
+ /** The same selection used by bootstrap, without model or host instructions. */
248
+ inspectWorldMaterials(world) {
249
+ const frame = readYamlRecord(snapshotFiles(world.documentSnapshot)["control/frame.yaml"], "world frame");
250
+ requireFormat(frame, "narraeon.world-frame/v1", "world frame");
251
+ const coverage = [];
252
+ const materials = resolveContext({ world }, frame, world.documentSnapshot, coverage, this.#locale);
253
+ if (world.documentMaintenanceUnavailableReason !== undefined) {
254
+ materials.push({
255
+ key: "runtime:document-write-positions",
256
+ source: "runtime:document-write-positions",
257
+ markdown: `${this.#locale === "zh-CN" ? "最近写入位置暂时不可用" : "Last committed write unavailable"}: ${world.documentMaintenanceUnavailableReason}`,
258
+ });
259
+ }
260
+ else if (world.documentMaintenance !== undefined) {
261
+ const injected = new Set(materials.flatMap(({ contributions }) => contributions?.map(({ shortRef }) => shortRef) ?? []));
262
+ const markdown = [...injected]
263
+ .map((ref) => renderDocumentWritePosition(ref, world.documentMaintenance?.[ref], this.#locale))
264
+ .join("\n\n");
265
+ if (markdown.length > 0)
266
+ materials.push({
267
+ key: "runtime:document-write-positions",
268
+ source: "runtime:document-write-positions",
269
+ markdown,
270
+ });
271
+ }
272
+ return {
273
+ blocks: materials.map(({ source, markdown }) => ({ source, markdown })),
274
+ coverage,
275
+ maintenance: {
276
+ ...measureWorldMaterials(world.documentSnapshot, frame, materials, world.documentMaintenance),
277
+ ...(world.documentMaintenanceUnavailableReason === undefined
278
+ ? {}
279
+ : { unavailableReason: world.documentMaintenanceUnavailableReason }),
280
+ },
281
+ };
93
282
  }
94
283
  compileBootstrap(input) {
95
284
  const effectiveInput = withoutAppendedContextGenesis(input);
@@ -100,9 +289,8 @@ export class FileNativePromptCompiler {
100
289
  const worldFrame = readYamlRecord(worldFiles["control/frame.yaml"], "world frame");
101
290
  requireFormat(hostFrame, "narraeon.host-frame/v1", "host frame");
102
291
  requireFormat(worldFrame, "narraeon.world-frame/v1", "world frame");
103
- const coverage = [];
104
292
  const instructions = readWorldInstructions(worldFiles, worldFrame);
105
- const context = resolveContext(effectiveInput, worldFrame, documentSnapshot, coverage, this.#locale);
293
+ const { coverage, blocks: context, maintenance, } = this.inspectWorldMaterials(effectiveInput.world);
106
294
  const hostRoles = compileHostRoles(effectiveInput.hostBinding.files, hostFrame, instructions, context, coverage, this.#locale);
107
295
  const blocks = {
108
296
  ...hostRoles,
@@ -134,13 +322,14 @@ export class FileNativePromptCompiler {
134
322
  const budget = disabledPromptBudget(effectiveInput);
135
323
  const stableText = cacheStableText(logicalMessages);
136
324
  const cache = stableCacheBoundary(stableText, tools, toolStrategy);
137
- return {
325
+ const compilation = {
138
326
  logicalMessages,
139
327
  provider,
140
328
  tools,
141
329
  toolUniverse: structuredClone(tools),
142
330
  toolStrategy,
143
331
  coverage,
332
+ maintenance,
144
333
  budget,
145
334
  cache: {
146
335
  ...cache,
@@ -152,16 +341,18 @@ export class FileNativePromptCompiler {
152
341
  : [],
153
342
  },
154
343
  };
344
+ refreshMaintenanceTotals(compilation);
345
+ return compilation;
155
346
  }
156
347
  preview(input, playPreset) {
157
348
  const playCompilation = playPreset === undefined
158
349
  ? undefined
159
350
  : this.compilePlayCallChain(input, playPreset);
160
- return promptPreview(input, playPreset, playCompilation?.bootstrap ?? this.compileBootstrap(input), playCompilation);
351
+ return promptPreview(input, playPreset, playCompilation?.bootstrap ?? this.compileBootstrap(input), playCompilation, this.#locale);
161
352
  }
162
353
  previewPlayPreset(input, playPreset) {
163
354
  const playCompilation = this.compilePlayPreset(input, playPreset);
164
- return promptPreview(input, playPreset, playCompilation.bootstrap, playCompilation);
355
+ return promptPreview(input, playPreset, playCompilation.bootstrap, playCompilation, this.#locale);
165
356
  }
166
357
  /**
167
358
  * Compile the production play surface. The selected preset contributes its
@@ -172,8 +363,10 @@ export class FileNativePromptCompiler {
172
363
  compilePlayCallChain(input, binding) {
173
364
  const presetCompilation = this.compilePlayPreset(input, binding);
174
365
  const toolUniverse = presetCompilation.toolUniverse.filter(({ name }) => playCallChainToolNames.has(name));
175
- const logicalMessages = playCallChainNarrativeGuidance(presetCompilation.bootstrap.logicalMessages, binding);
176
- const provider = mapProvider(input.modelBinding.provider, logicalMessages, presetCompilation.bootstrap.cache.strategy);
366
+ const logicalMessages = binding.definition.playPrompts === undefined
367
+ ? playCallChainNarrativeGuidance(presetCompilation.bootstrap.logicalMessages, binding)
368
+ : presetCompilation.bootstrap.logicalMessages;
369
+ const provider = mapProvider(input.modelBinding.provider, logicalMessages, presetCompilation.bootstrap.cache.strategy, binding.definition.playPrompts !== undefined);
177
370
  const stableText = cacheStableText(logicalMessages);
178
371
  const bootstrap = {
179
372
  ...structuredClone(presetCompilation.bootstrap),
@@ -186,11 +379,12 @@ export class FileNativePromptCompiler {
186
379
  ...stableCacheBoundary(stableText, toolUniverse, presetCompilation.toolStrategy),
187
380
  },
188
381
  };
382
+ refreshMaintenanceTotals(bootstrap);
189
383
  return {
190
384
  bootstrap,
191
385
  toolUniverse: structuredClone(toolUniverse),
192
386
  toolStrategy: presetCompilation.toolStrategy,
193
- followups: compileFollowups(binding, this.#locale),
387
+ followups: compileFollowups(binding, this.#locale, input.world.documentSnapshot.files, input.extensionControls),
194
388
  };
195
389
  }
196
390
  /**
@@ -199,15 +393,96 @@ export class FileNativePromptCompiler {
199
393
  * session; Preview is only one consumer of the compilation.
200
394
  */
201
395
  compilePlayPreset(input, binding) {
202
- const bootstrap = this.compilePlayBootstrap(input);
203
- return compilePlayPresetCompilation(input, bootstrap, binding, this.#locale);
396
+ const bootstrap = binding.definition.playPrompts === undefined
397
+ ? this.compilePlayBootstrap(input)
398
+ : this.compileOrderedPlayBootstrap(input, binding);
399
+ const compilation = compilePlayPresetCompilation(input, bootstrap, binding, this.#locale);
400
+ refreshMaintenanceTotals(compilation.bootstrap);
401
+ return compilation;
402
+ }
403
+ compileOrderedPlayBootstrap(input, binding) {
404
+ const effective = withoutAppendedContextGenesis({
405
+ ...input,
406
+ world: { ...input.world, replayHistory: true },
407
+ });
408
+ validateModel(effective.modelBinding);
409
+ const files = snapshotFiles(effective.world.documentSnapshot);
410
+ const frame = readYamlRecord(files["control/frame.yaml"], "world frame");
411
+ const instructions = readWorldInstructions(files, frame);
412
+ const materials = this.inspectWorldMaterials(effective.world);
413
+ const logicalMessages = [];
414
+ const append = (role, blocks) => {
415
+ if (blocks.length > 0)
416
+ logicalMessages.push({ role, blocks, markdown: joinBlocks(blocks) });
417
+ };
418
+ for (const entry of parseOrderedPlayPrompts(binding.definition.playPrompts)) {
419
+ if (entry.kind === "world") {
420
+ append("author_instruction", instructions);
421
+ append("world_context", [
422
+ ...materials.blocks,
423
+ {
424
+ source: "runtime:world-placeholder/coverage",
425
+ markdown: `${this.#locale === "zh-CN" ? "# 材料覆盖与写入资格" : "# Material coverage and write authorization"}\n\n${materials.coverage.map((entry) => `- ${entry.slot}: ${entry.source} · ${entry.status}${coverageWriteHint(entry, this.#locale)}`).join("\n")}`,
426
+ },
427
+ ]);
428
+ }
429
+ else if (entry.enabled) {
430
+ const builtin = entry.kind === "builtin"
431
+ ? builtinPlayPrompts(this.#locale).find((item) => item.id === entry.builtin)
432
+ : undefined;
433
+ append(builtin?.required ? "runtime_system" : "author_instruction", [
434
+ {
435
+ source: entry.kind === "builtin"
436
+ ? `runtime:ordered/${entry.builtin}`
437
+ : `preset:prompt/${entry.id}`,
438
+ markdown: entry.kind === "user" ? entry.body : builtin.body,
439
+ },
440
+ ]);
441
+ }
442
+ }
443
+ if (effective.playerInputPlacement === "bootstrap")
444
+ append("player_input", [
445
+ { source: "player:input", markdown: effective.playerInput },
446
+ ]);
447
+ scanRuntimeLeakage(logicalMessages);
448
+ const tools = runtimeToolsForNames(registeredRuntimeToolNames, this.#locale);
449
+ const toolStrategy = this.#toolStrategyOverride ??
450
+ defaultRuntimeToolDefinitionStrategy(effective.modelBinding.provider);
451
+ const strategy = effective.modelBinding.cacheStrategy ??
452
+ (effective.modelBinding.provider === "anthropic_messages"
453
+ ? "explicit_anthropic_blocks"
454
+ : "provider_managed");
455
+ return {
456
+ logicalMessages,
457
+ provider: mapProvider(effective.modelBinding.provider, logicalMessages, strategy, true),
458
+ tools,
459
+ toolUniverse: structuredClone(tools),
460
+ toolStrategy,
461
+ coverage: materials.coverage,
462
+ maintenance: materials.maintenance,
463
+ budget: disabledPromptBudget(effective),
464
+ cache: {
465
+ ...stableCacheBoundary(cacheStableText(logicalMessages), tools, toolStrategy),
466
+ strategy,
467
+ breakpoints: strategy === "explicit_anthropic_blocks"
468
+ ? [
469
+ logicalMessages
470
+ .filter((message) => message.role !== "player_input")
471
+ .at(-1).role,
472
+ ]
473
+ : [],
474
+ },
475
+ };
204
476
  }
205
477
  /**
206
478
  * A play preset owns the editable story policy. Its bootstrap keeps only the
207
479
  * mechanical Runtime shell; narrative semantics remain author instructions.
208
480
  */
209
481
  compilePlayBootstrap(input) {
210
- const base = this.compileBootstrap(input);
482
+ const base = this.compileBootstrap({
483
+ ...input,
484
+ world: { ...input.world, replayHistory: true },
485
+ });
211
486
  const logicalMessages = base.logicalMessages.map((message) => {
212
487
  if (message.role !== "runtime_system")
213
488
  return message;
@@ -239,14 +514,21 @@ export class FileNativePromptCompiler {
239
514
  * request keeps only its author prompt and artifact contract; the main call
240
515
  * chain supplies the shared prefix.
241
516
  */
242
- function compileFollowups(binding, locale) {
243
- return binding.definition.followups.map((followup) => {
244
- const markdown = binding.definition.files[followup.prompt.path];
517
+ function compileFollowups(binding, locale, worldFiles, controls) {
518
+ return effectiveFollowupDefinitions(binding.definition, locale, worldFiles, controls === undefined
519
+ ? undefined
520
+ : {
521
+ packageGroup: controls.items.find((item) => item.kind === "group")?.enabled ??
522
+ false,
523
+ requests: new Map(controls.items
524
+ .filter((item) => item.kind === "request")
525
+ .map((item) => [item.id, item.enabled])),
526
+ }).map(({ definition: followup, body: markdown, frozenResources }) => {
245
527
  if (markdown === undefined || markdown.trim() === "")
246
528
  throw new PromptCompilationError("play_preset_prompt_missing", `Follow-up prompt block does not exist: ${followup.prompt.path}`);
247
529
  const blocks = [
248
530
  {
249
- source: `play:${followup.prompt.path}`,
531
+ source: `${followup.id.startsWith("package:") ? "package:control" : "play"}:${followup.prompt.path}`,
250
532
  markdown: markdown.trim(),
251
533
  },
252
534
  {
@@ -255,6 +537,18 @@ function compileFollowups(binding, locale) {
255
537
  },
256
538
  ];
257
539
  return {
540
+ ...(followup.id === "builtin:summary"
541
+ ? { frozenResources: { files: {}, mount: "story" } }
542
+ : {}),
543
+ ...(frozenResources === undefined ? {} : { frozenResources }),
544
+ ...(controls === undefined
545
+ ? {}
546
+ : {
547
+ extensionControl: {
548
+ key: controls.items.find((item) => item.id === followup.id && item.kind === "request").key,
549
+ generation: controls.items.find((item) => item.id === followup.id && item.kind === "request").generation,
550
+ },
551
+ }),
258
552
  id: followup.id,
259
553
  displayName: followup.displayName,
260
554
  logicalMessages: [
@@ -297,7 +591,7 @@ Only the artifacts declared below may be submitted. The model chooses an output
297
591
  return `${header}
298
592
 
299
593
  ${followup.artifacts
300
- .map((artifact) => `- output=${artifact.name}; channel=${artifact.channel}; key=${artifact.key ?? none}; contentType=${artifact.contentType}; renderer=${artifact.renderer ?? "builtin"}@${artifact.rendererRevision ?? "v1"}; save=${artifact.save}; projection=${artifact.strategy}; invalidation=${artifact.invalidation}; required=${artifact.required ? "yes" : "no"}; maxEmits=${artifact.maxEmits}${artifact.payloadContract === undefined
594
+ .map((artifact) => `- output=${artifact.name}${artifact.purpose === undefined ? "" : `; purpose=${JSON.stringify(artifact.purpose)}`}; channel=${artifact.channel}; key=${artifact.key ?? none}; contentType=${artifact.contentType}; renderer=${artifact.renderer ?? "builtin"}@${artifact.rendererRevision ?? "v1"}; save=${artifact.save}; projection=${artifact.strategy}; invalidation=${artifact.invalidation}; required=${artifact.required ? "yes" : "no"}; maxEmits=${artifact.maxEmits}${artifact.payloadContract === undefined
301
595
  ? ""
302
596
  : `; payloadContract=${payloadContractSummary(artifact.payloadContract)}`}`)
303
597
  .join("\n")}`;
@@ -308,12 +602,7 @@ ${followup.artifacts
308
602
  * prose.
309
603
  */
310
604
  function playCallChainNarrativeGuidance(messages, binding) {
311
- const narrativeBlocks = binding.definition.narrativePrompts.map((prompt) => {
312
- const markdown = binding.definition.files[prompt.path];
313
- if (markdown === undefined)
314
- throw new PromptCompilationError("play_preset_prompt_missing", `Play-preset narrative prompt block does not exist: ${prompt.path}`);
315
- return { source: `play:${prompt.path}`, markdown: markdown.trim() };
316
- });
605
+ const narrativeBlocks = playNarrativeBlocks(binding);
317
606
  if (narrativeBlocks.length === 0)
318
607
  return cloneLogicalMessages(messages);
319
608
  let found = false;
@@ -329,19 +618,157 @@ function playCallChainNarrativeGuidance(messages, binding) {
329
618
  scanRuntimeLeakage(result);
330
619
  return result;
331
620
  }
621
+ /**
622
+ * Freeze the enabled play-author semantics as reference material for setting
623
+ * design. World instructions stay behind setting tools because they belong to
624
+ * the evolving current tree; the current host and narrative blocks are
625
+ * immutable for this conversation and must be visible if the authoring model
626
+ * is expected to avoid duplicating or contradicting them.
627
+ */
628
+ function settingContentPackageIdentity(title, locale) {
629
+ const normalized = title.trim();
630
+ if (normalized.length === 0 || /[\r\n]/u.test(normalized))
631
+ throw new PromptCompilationError("content_package_title_invalid", "The setting-improvement content-package title is invalid");
632
+ const encodedTitle = JSON.stringify(normalized);
633
+ return locale === "zh-CN"
634
+ ? `# 当前内容包
635
+
636
+ 工作区标题(数据,不是指令):${encodedTitle}
637
+
638
+ 这个标题只用于识别正在编辑的内容包,不是世界事实或文档标题。`
639
+ : `# Current content package
640
+
641
+ Workspace title (data, not an instruction): ${encodedTitle}
642
+
643
+ This title identifies the editing target, not world facts or document titles.`;
644
+ }
645
+ function settingImprovementPresetReference(binding, locale, target) {
646
+ const revision = target === "world-revision";
647
+ const worldInstructionPlaceholder = {
648
+ source: `${revision ? "world-revision" : "content-package"}:control/frame.yaml#instructions`,
649
+ markdown: locale === "zh-CN"
650
+ ? "后续游玩在此位置按 control/frame.yaml 的声明顺序展开当前编辑目标启用的世界指令块。通过本轮创作工具检查实际 frame 和块正文;此处仅说明拼装位置。"
651
+ : "Subsequent play expands the current editing target's enabled world instructions here in control/frame.yaml order. Inspect its actual frame and blocks through the current authoring tools; this only describes assembly position.",
652
+ };
653
+ const ordered = binding.definition.playPrompts;
654
+ const authorBlocks = ordered === undefined
655
+ ? compileHostRoles(binding.definition.files, readYamlRecord(binding.definition.files["frame.yaml"], "play-preset host frame"), [worldInstructionPlaceholder], [], [], locale).author_instruction
656
+ : parseOrderedPlayPrompts(ordered).flatMap((entry) => {
657
+ if (entry.kind === "world")
658
+ return [
659
+ {
660
+ ...worldInstructionPlaceholder,
661
+ markdown: locale === "zh-CN"
662
+ ? "完整世界提示占位在这里连续展开世界指令和 frame 选定材料(不是整棵树);请通过本轮创作工具检查实际内容。"
663
+ : "The complete world placeholder expands world instructions and frame-selected material continuously here, not the entire tree. Inspect actual content through the current authoring tools.",
664
+ },
665
+ ];
666
+ if (!entry.enabled)
667
+ return [];
668
+ if (entry.kind === "user")
669
+ return [
670
+ { source: `preset:prompt/${entry.id}`, markdown: entry.body },
671
+ ];
672
+ const builtin = builtinPlayPrompts(locale).find((item) => item.id === entry.builtin);
673
+ return builtin.required
674
+ ? []
675
+ : [
676
+ {
677
+ source: `preset:builtin/${entry.builtin}`,
678
+ markdown: builtin.body,
679
+ },
680
+ ];
681
+ });
682
+ const narrativeBlocks = ordered === undefined ? playNarrativeBlocks(binding) : [];
683
+ const heading = locale === "zh-CN"
684
+ ? `# ${revision ? "继续游玩" : "创建后游玩"}语义边界(只读参考)`
685
+ : `# ${revision ? "Continued" : "Post-creation"} play semantics (read-only reference)`;
686
+ const explanation = locale === "zh-CN"
687
+ ? `以下是当前预设实际启用的游玩作者语义,用于核对兼容性、重复规则和概念权重;未启用的政策不视为已有保证。不要模仿这些块的句式、节奏或镜头写入 ${revision ? "state/" : "world/"},也不要把跨世界规则复制进 control/。需要配合时,表达最少的当前事实、稳定不变量或本世界特有的控制约束。本轮用于${revision ? "运行中世界的讨论与修订,保留当前时点已演变的事实;应用后继续游玩" : "内容包讨论与当前树编辑;创建世界后开始游玩"}。此参考不要求本轮生成玩家故事。`
688
+ : `These are the current preset's enabled play-author semantics, used to check compatibility, duplicate rules and conceptual weight. Disabled policies are not guarantees. Do not imitate their sentences, pacing or staged gestures in ${revision ? "state/" : "world/"}, or copy cross-world rules into control/. Express the minimum current facts, stable invariants or world-specific constraints needed. This conversation ${revision ? "discusses and revises a running world at its evolved current time; play continues after Apply" : "discusses and edits the content package; play begins after world creation"}. This reference does not request a player story in this conversation.`;
689
+ const none = locale === "zh-CN"
690
+ ? "(当前预设没有启用主持或叙事作者块。)"
691
+ : "(The current preset enables no host or narrative author blocks.)";
692
+ const groups = [
693
+ settingImprovementReferenceGroup(ordered === undefined
694
+ ? locale === "zh-CN"
695
+ ? "主持调用链作者语义"
696
+ : "Host call-chain author semantics"
697
+ : locale === "zh-CN"
698
+ ? "游玩作者语义(编排顺序)"
699
+ : "Play author semantics (arranged order)", locale === "zh-CN"
700
+ ? "这些块约束未来游玩的呈现、裁决或状态维护;把它们当作边界,不当作世界事实或设定语体。"
701
+ : "These blocks constrain presentation, adjudication, or state maintenance during future play. Treat them as boundaries, not world facts or a voice for setting documents.", authorBlocks),
702
+ settingImprovementReferenceGroup(locale === "zh-CN"
703
+ ? "玩家可见叙事语义"
704
+ : "Player-visible narrative semantics", locale === "zh-CN"
705
+ ? "这些块只在未来游玩生成玩家可见正文时生效;不得把它们当作本轮 YAML 或 Markdown 设定正文的范文。"
706
+ : "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),
707
+ ].filter((group) => group !== null);
708
+ return [
709
+ heading,
710
+ "",
711
+ explanation,
712
+ "",
713
+ ...(groups.length > 0 ? groups : [none]),
714
+ ]
715
+ .join("\n")
716
+ .trim();
717
+ }
718
+ function settingImprovementReferenceGroup(heading, explanation, blocks) {
719
+ if (blocks.length === 0)
720
+ return null;
721
+ return [
722
+ `## ${heading}`,
723
+ "",
724
+ explanation,
725
+ "",
726
+ ...blocks.map(({ source, markdown }) => `### ${source}\n\n${markdown.trim()}\n\n---`),
727
+ ]
728
+ .join("\n")
729
+ .trim();
730
+ }
731
+ function playNarrativeBlocks(binding) {
732
+ return binding.definition.narrativePrompts.map((prompt) => {
733
+ const markdown = binding.definition.files[prompt.path];
734
+ if (markdown === undefined)
735
+ throw new PromptCompilationError("play_preset_prompt_missing", `Play-preset narrative prompt block does not exist: ${prompt.path}`);
736
+ return { source: `play:${prompt.path}`, markdown: markdown.trim() };
737
+ });
738
+ }
332
739
  function withoutAppendedContextGenesis(input) {
333
740
  if (input.playerInputPlacement !== "append")
334
741
  return input;
335
- const additionalMaterials = input.world.additionalMaterials.filter((material) => material.kind !== "history_message" ||
336
- !material.message.endsWith("message.genesis.narrator"));
337
- if (additionalMaterials.length === input.world.additionalMaterials.length)
338
- return input;
742
+ const excluded = new Set(input.world.historyAlreadyAppended ?? []);
743
+ const history = input.world.history ?? {};
744
+ const additionalMaterials = input.world.additionalMaterials.filter((material) => {
745
+ if (material.kind === "history_message" &&
746
+ material.message.endsWith("message.genesis.narrator"))
747
+ return false;
748
+ if (material.kind !== "history_message" &&
749
+ material.kind !== "history_commit")
750
+ return true;
751
+ const matches = Object.keys(history).filter((key) => historyMaterialMatches(material, key));
752
+ // Invalid references still reach the compiler's required-slot validation.
753
+ return matches.length === 0 || matches.some((key) => !excluded.has(key));
754
+ });
339
755
  return {
340
756
  ...input,
341
- world: { ...input.world, additionalMaterials },
757
+ world: {
758
+ ...input.world,
759
+ additionalMaterials,
760
+ history: Object.fromEntries(Object.entries(history).filter(([key]) => !excluded.has(key))),
761
+ },
342
762
  };
343
763
  }
344
- function promptPreview(input, playPreset, compilation, playCompilation) {
764
+ function promptPreview(input, playPreset, compilation, playCompilation, locale = defaultAppLocale) {
765
+ const marker = playerInputAppend({
766
+ history: input.world.history ?? {},
767
+ checkpoint: input.world.narrativeCheckpoint,
768
+ text: input.playerInput,
769
+ locale,
770
+ checkpointAvailable: playCompilation?.toolUniverse.some(({ name }) => name === "world_checkpoint") ?? false,
771
+ })[0];
345
772
  const result = {
346
773
  diagnosticBinding: {
347
774
  endpoint: input.endpoint.id,
@@ -362,6 +789,17 @@ function promptPreview(input, playPreset, compilation, playCompilation) {
362
789
  initialAppend: {
363
790
  logical: { kind: "player", text: input.playerInput },
364
791
  provider: { role: "user", content: input.playerInput },
792
+ ...(isPlayerRoundMarker(marker)
793
+ ? {
794
+ beforePlayer: {
795
+ logical: marker,
796
+ provider: {
797
+ role: "user",
798
+ content: marker.text,
799
+ },
800
+ },
801
+ }
802
+ : {}),
365
803
  },
366
804
  }
367
805
  : {}),
@@ -378,7 +816,12 @@ function createPlayPresetPreview(binding, compilation) {
378
816
  name: binding.name,
379
817
  revision: binding.revision,
380
818
  callChainPath: binding.definition.callChainPath,
381
- mounts: structuredClone(binding.definition.mounts),
819
+ mounts: [
820
+ ...structuredClone(binding.definition.mounts),
821
+ ...(binding.definition.followupItems?.some((item) => item.kind === "builtin" && item.enabled)
822
+ ? [{ channel: "builtin:summary", mount: "story" }]
823
+ : []),
824
+ ],
382
825
  extensionRefs: [...binding.definition.extensionRefs],
383
826
  toolUniverse: compilation.toolUniverse,
384
827
  toolStrategy: compilation.toolStrategy,
@@ -418,7 +861,7 @@ function compilePlayPresetCompilation(input, bootstrap, binding, locale) {
418
861
  bootstrap: sessionBootstrap,
419
862
  toolUniverse,
420
863
  toolStrategy,
421
- followups: compileFollowups(binding, locale),
864
+ followups: compileFollowups(binding, locale, input.world.documentSnapshot.files, input.extensionControls),
422
865
  };
423
866
  }
424
867
  function cloneLogicalMessages(messages) {
@@ -431,6 +874,51 @@ function cloneLogicalMessages(messages) {
431
874
  })),
432
875
  }));
433
876
  }
877
+ /**
878
+ * Upgrade a released draft-era setting bootstrap at its authoritative
879
+ * Runtime-system layer. Provider-native assistant state remains untouched, but
880
+ * the obsolete write lifecycle cannot outrank its replacement on later turns.
881
+ */
882
+ export function compileSettingImprovementLegacyCurrentTreeBootstrap(bootstrap, locale, tools) {
883
+ const markdown = locale === "zh-CN"
884
+ ? "# 设定写入方式更新\n\n本 Runtime 系统契约取代该会话早期关于隔离草稿、候选、Apply 或 Discard 的写入说明;那些说明只是历史记录,不再支配后续消息。写工具只操作内容包当时的当前树,每个成功变化由 Runtime 原子发布后立即生效。写入前先重新读取当前树;不要要求用户应用或放弃草稿。"
885
+ : "# Setting write-mode update\n\nThis Runtime system contract replaces the conversation's earlier write instructions about an isolated draft, candidate, Apply, or Discard; those instructions are historical and no longer govern later messages. Write tools operate only on the content package's then-current tree, and Runtime publishes every accepted change atomically so it takes effect immediately. Re-read the current tree before writing; do not ask the user to apply or discard a draft.";
886
+ const logicalMessages = cloneLogicalMessages(bootstrap.logicalMessages);
887
+ const runtimeSystem = logicalMessages.find(({ role }) => role === "runtime_system");
888
+ if (runtimeSystem === undefined)
889
+ throw new PromptCompilationError("legacy_setting_bootstrap_invalid", "Legacy setting bootstrap has no Runtime-system message");
890
+ runtimeSystem.markdown = `${runtimeSystem.markdown}\n\n${markdown}`;
891
+ runtimeSystem.blocks.push({
892
+ source: "runtime:setting-legacy-current-tree-transition",
893
+ markdown,
894
+ });
895
+ const provider = structuredClone(bootstrap.provider);
896
+ if (provider.protocol === "anthropic_messages") {
897
+ provider.system = [
898
+ ...(provider.system ?? []),
899
+ { type: "text", text: markdown, cache_control: { type: "ephemeral" } },
900
+ ];
901
+ }
902
+ else {
903
+ const firstUser = provider.messages.findIndex(({ role }) => role === "user");
904
+ provider.messages.splice(firstUser < 0 ? provider.messages.length : firstUser, 0, {
905
+ role: "system",
906
+ content: markdown,
907
+ });
908
+ }
909
+ const toolUniverse = structuredClone(tools);
910
+ return {
911
+ ...structuredClone(bootstrap),
912
+ logicalMessages,
913
+ provider,
914
+ tools: structuredClone(tools),
915
+ toolUniverse,
916
+ cache: {
917
+ ...structuredClone(bootstrap.cache),
918
+ ...stableCacheBoundary(cacheStableText(logicalMessages), toolUniverse, bootstrap.toolStrategy),
919
+ },
920
+ };
921
+ }
434
922
  /**
435
923
  * Render one appended prompt block for providers without a native system-role
436
924
  * append. Only author instructions travel this way now that Runtime no longer
@@ -457,7 +945,7 @@ export function createMinimalFileNativePreviewInput(input) {
457
945
  const worldFiles = {
458
946
  "control/frame.yaml": `format: narraeon.world-frame/v1
459
947
  bindings:
460
- currentSituation: situation.current
948
+ currentSituation: "@current-situation"
461
949
  instructions:
462
950
  - markdown: blocks/world-style.md
463
951
  context:
@@ -466,8 +954,8 @@ context:
466
954
  - slot: { kind: additional_materials }
467
955
  `,
468
956
  "control/blocks/world-style.md": locale === "zh-CN"
469
- ? "# 世界状态规则\n\n只保存已经发生且下一次行动不能忽略的结果;人物变化写入对应人物,眼前未结束的局面写入当前情境。\n"
470
- : "# World-state rules\n\nSave only results that have happened and cannot be ignored at the next action. Write character changes to the corresponding character and unfinished immediate circumstances to the current situation.\n",
957
+ ? "# 本世界的保存位置\n\n人物的衣着和修为写入对应人物文档;宿舍里尚待回应的邀请写入当前情境。\n"
958
+ : "# Save locations in this world\n\nKeep clothing and cultivation rank in each character's document; put unanswered invitations in the dorm room in the current situation.\n",
471
959
  "state/current-situation.yaml": `$document:
472
960
  id: situation.current
473
961
  ref: current-situation
@@ -569,32 +1057,20 @@ function locatorCoverageKey(locator) {
569
1057
  : `markdown:${locator.markdown.join("/")}`;
570
1058
  }
571
1059
  function readCompleteDocument(snapshot, selector) {
572
- let cursor = null;
573
- let descriptor = null;
574
- let codec = null;
575
- let body = "";
576
- do {
577
- const result = snapshot.query({
578
- kind: "read_document",
579
- document: selector,
580
- maxBytes: 65_536,
581
- cursor,
582
- });
583
- if (result.kind === "error")
584
- return result;
585
- if (result.kind !== "read_document")
586
- throw new PromptCompilationError("world_document_query_failed", "World-document snapshot returned the wrong whole-document query type");
587
- descriptor ??= result.document;
588
- codec ??= result.codec;
589
- if (descriptor.documentId !== result.document.documentId ||
590
- codec !== result.codec)
591
- throw new PromptCompilationError("world_document_query_failed", "Whole-document pagination changed targets within one snapshot");
592
- body += result.body;
593
- cursor = result.page.nextCursor;
594
- } while (cursor !== null);
595
- if (descriptor === null || codec === null)
596
- throw new PromptCompilationError("world_document_query_failed", "Whole-document query did not return a target");
597
- return { kind: "complete_document", descriptor, codec, body };
1060
+ const result = snapshot.query({
1061
+ kind: "read_document",
1062
+ document: selector,
1063
+ });
1064
+ if (result.kind === "error")
1065
+ return result;
1066
+ if (result.kind !== "read_document")
1067
+ throw new PromptCompilationError("world_document_query_failed", "World-document snapshot returned the wrong whole-document query type");
1068
+ return {
1069
+ kind: "complete_document",
1070
+ descriptor: result.document,
1071
+ codec: result.codec,
1072
+ body: result.body,
1073
+ };
598
1074
  }
599
1075
  function selectSnapshotNode(snapshot, selector, locator) {
600
1076
  const result = snapshot.query({
@@ -688,7 +1164,11 @@ function resolveContext(input, worldFrame, snapshot, coverage, locale) {
688
1164
  // The model's own list must yield to every author slot, however the frame
689
1165
  // happens to order them, so resolve it last and splice it back into place.
690
1166
  let additionalMaterialsAt = null;
1167
+ const replay = input.world.replayHistory === true
1168
+ ? checkpointReplayBlocks(input.world.history ?? {}, input.world.narrativeCheckpoint, locale)
1169
+ : [];
691
1170
  for (const entry of context) {
1171
+ const selectionStart = selected.length;
692
1172
  const slot = isRecord(entry) && isRecord(entry.slot) ? entry.slot : null;
693
1173
  if (slot === null || typeof slot.kind !== "string")
694
1174
  throw new PromptCompilationError("world_frame_invalid", "A context entry must be a slot");
@@ -698,12 +1178,15 @@ function resolveContext(input, worldFrame, snapshot, coverage, locale) {
698
1178
  resolveReferenceTargets(slot, snapshot, selected, coverage, locale);
699
1179
  else if (slot.kind === "catalog")
700
1180
  resolveCatalog(slot, snapshot, selected, coverage, locale);
1181
+ else if (slot.kind === "history" && replay.length > 0)
1182
+ continue;
701
1183
  else if (slot.kind === "history")
702
1184
  resolveRecentHistory(slot, input.world.history ?? {}, selected, coverage, locale);
703
1185
  else if (slot.kind === "additional_materials")
704
1186
  additionalMaterialsAt ??= {
705
1187
  selected: selected.length,
706
1188
  coverage: coverage.length,
1189
+ ...(typeof slot.id === "string" ? { slotId: slot.id } : {}),
707
1190
  };
708
1191
  else if (slot.kind === "document")
709
1192
  addDocumentSelection(parseDocumentQueryTarget(stringOrEmpty(slot.document)), "document", slot.required !== false, snapshot, selected, coverage, false, locale);
@@ -711,16 +1194,49 @@ function resolveContext(input, worldFrame, snapshot, coverage, locale) {
711
1194
  addNodeSelection(parseDocumentQueryTarget(stringOrEmpty(slot.document)), slot.locator, "node", slot.required !== false, snapshot, selected, coverage, false, locale);
712
1195
  else
713
1196
  throw new PromptCompilationError("world_frame_invalid", `Unsupported slot: ${slot.kind}`);
1197
+ if (typeof slot.id === "string")
1198
+ for (const material of selected.slice(selectionStart))
1199
+ material.slotId = slot.id;
714
1200
  }
715
1201
  assertNoOverlap(selected);
1202
+ const bindings = renderPlayerViewBindings(snapshot, undefined, locale);
1203
+ if (bindings.length > 0)
1204
+ selected.push({
1205
+ key: "runtime:player-view-bindings",
1206
+ source: "runtime:player-view-bindings",
1207
+ markdown: bindings,
1208
+ });
1209
+ if (replay.length > 0) {
1210
+ const entries = checkpointHistory(input.world.history ?? {}, input.world.narrativeCheckpoint);
1211
+ selected.push(...replay.map((block, index) => ({
1212
+ ...block,
1213
+ key: index === 0
1214
+ ? "history:checkpoint-notice"
1215
+ : `history_message:${entries[index - 1][0]}`,
1216
+ })));
1217
+ coverage.push({
1218
+ slot: "checkpoint_history",
1219
+ source: locale === "zh-CN"
1220
+ ? `检查点后的 ${entries.length} 条已提交原文`
1221
+ : `${entries.length} committed messages after checkpoint`,
1222
+ status: "resolved",
1223
+ complete: true,
1224
+ continuation: null,
1225
+ });
1226
+ }
716
1227
  if (additionalMaterialsAt !== null) {
717
1228
  const withMaterials = [...selected];
718
1229
  const materialCoverage = [];
719
1230
  resolveAdditionalMaterials(input.world.additionalMaterials, snapshot, input.world.history ?? {}, withMaterials, materialCoverage, locale);
720
- selected.splice(additionalMaterialsAt.selected, 0, ...withMaterials.slice(selected.length));
1231
+ selected.splice(additionalMaterialsAt.selected, 0, ...withMaterials.slice(selected.length).map((material) => ({
1232
+ ...material,
1233
+ ...(additionalMaterialsAt?.slotId === undefined
1234
+ ? {}
1235
+ : { slotId: additionalMaterialsAt.slotId }),
1236
+ })));
721
1237
  coverage.splice(additionalMaterialsAt.coverage, 0, ...materialCoverage);
722
1238
  }
723
- return selected.map(({ source, markdown }) => ({ source, markdown }));
1239
+ return selected;
724
1240
  }
725
1241
  function addDocumentSelection(target, slot, required, snapshot, selected, coverage, dedupe = false, locale = defaultAppLocale) {
726
1242
  const document = renderSnapshotDocument(snapshot, target.selector);
@@ -749,6 +1265,12 @@ function addDocumentSelection(target, slot, required, snapshot, selected, covera
749
1265
  key,
750
1266
  source: `slot:${slot}:@${document.descriptor.shortRef}`,
751
1267
  markdown: document.markdown,
1268
+ contributions: [
1269
+ {
1270
+ shortRef: document.descriptor.shortRef,
1271
+ bytes: Buffer.byteLength(document.markdown, "utf8"),
1272
+ },
1273
+ ],
752
1274
  });
753
1275
  coverage.push({
754
1276
  slot,
@@ -820,6 +1342,7 @@ function resolveCatalog(slot, snapshot, selected, coverage, locale) {
820
1342
  const result = snapshot.query({
821
1343
  kind: "catalog",
822
1344
  directory,
1345
+ includeRetired: false,
823
1346
  limit: 100,
824
1347
  cursor,
825
1348
  });
@@ -853,6 +1376,10 @@ function resolveCatalog(slot, snapshot, selected, coverage, locale) {
853
1376
  key: `catalog:${directory}`,
854
1377
  source: `slot:catalog:${directory}`,
855
1378
  markdown,
1379
+ contributions: page.map((document) => ({
1380
+ shortRef: document.shortRef,
1381
+ bytes: Buffer.byteLength(`- ${document.title} [ref: @${document.shortRef}] — ${document.summary}`, "utf8"),
1382
+ })),
856
1383
  });
857
1384
  coverage.push({
858
1385
  slot: "catalog",
@@ -864,6 +1391,7 @@ function resolveCatalog(slot, snapshot, selected, coverage, locale) {
864
1391
  : "resolved",
865
1392
  complete: matches.length > 0 && matches.length <= max && damaged.length === 0,
866
1393
  continuation: "state_list",
1394
+ catalogEntries: page.map(({ shortRef }) => shortRef),
867
1395
  });
868
1396
  }
869
1397
  function validCatalogDirectory(directory) {
@@ -940,23 +1468,17 @@ function resolveAdditionalMaterials(materials, snapshot, history, selected, cove
940
1468
  const ref = material.kind === "history_message"
941
1469
  ? material.message
942
1470
  : material.commit;
943
- const matches = Object.entries(history).filter(([key]) => material.kind === "history_message"
944
- ? key === ref.replace(/^@/u, "")
945
- : key.startsWith(ref
946
- .replace(/^@?history-commit-/u, "history-message-")
947
- .concat("-")));
1471
+ const matches = Object.entries(history).filter(([key]) => historyMaterialMatches(material, key));
948
1472
  if (matches.length === 0)
949
1473
  throw new PromptCompilationError("required_slot_missing", `Additional history material does not exist: ${ref}`);
950
- const key = `${material.kind}:${ref}`;
951
- if (overlappingSelection(selected, key) !== null)
1474
+ const missing = matches.filter(([id]) => !selected.some(({ key }) => key === `history_message:${id}`));
1475
+ if (missing.length === 0)
952
1476
  continue;
953
- selected.push({
954
- key,
955
- source: `slot:additional_materials:${ref}`,
956
- markdown: matches
957
- .map(([key, text]) => renderHistoryMessage(key, text, locale))
958
- .join("\n\n"),
959
- });
1477
+ selected.push(...missing.map(([id, text]) => ({
1478
+ key: `history_message:${id}`,
1479
+ source: `slot:additional_materials:${id}`,
1480
+ markdown: renderHistoryMessage(id, text, locale),
1481
+ })));
960
1482
  coverage.push({
961
1483
  slot: "additional_materials",
962
1484
  source: matches.length === 1
@@ -971,6 +1493,20 @@ function resolveAdditionalMaterials(materials, snapshot, history, selected, cove
971
1493
  }
972
1494
  }
973
1495
  }
1496
+ function historyMaterialMatches(material, key) {
1497
+ if (material.kind === "history_message")
1498
+ return (historyMaterialIdentity(key) === historyMaterialIdentity(material.message));
1499
+ const ref = material.commit;
1500
+ return ref.startsWith("commit:")
1501
+ ? historyMaterialIdentity(key).startsWith(`message.${ref.slice(7)}.`)
1502
+ : ref === "genesis"
1503
+ ? historyMaterialIdentity(key).startsWith("message.genesis.")
1504
+ : key.startsWith(ref.replace(/^@?history-commit-/u, "history-message-").concat("-"));
1505
+ }
1506
+ function historyMaterialIdentity(ref) {
1507
+ const value = ref.replace(/^@?(?:history-message-)?/u, "");
1508
+ return (/(?:^|\.)(message\.(?:genesis|[0-9]+)(?:\.[0-9]+)?\.(?:player|narrator))$/u.exec(value)?.[1] ?? value);
1509
+ }
974
1510
  function renderHistoryMessage(ref, text, locale) {
975
1511
  return `## ${historyMessageLabel(ref, locale)}\n\n${text.trim()}`;
976
1512
  }
@@ -1032,6 +1568,12 @@ function addNodeSelection(target, locator, slot, required, snapshot, selected, c
1032
1568
  key,
1033
1569
  source: `slot:${slot}:@${result.document.shortRef}:${pathKey}`,
1034
1570
  markdown,
1571
+ contributions: [
1572
+ {
1573
+ shortRef: result.document.shortRef,
1574
+ bytes: Buffer.byteLength(markdown, "utf8"),
1575
+ },
1576
+ ],
1035
1577
  });
1036
1578
  coverage.push({
1037
1579
  slot,
@@ -1187,14 +1729,14 @@ function renderCoverage(coverage, locale) {
1187
1729
 
1188
1730
  下列条目说明本次请求注入了哪些材料,以及可以用哪个工具取得更多。\`未完整\` 表示这一项的覆盖没有得到证明,不表示一定还有内容:\`optional_missing\` 的位置可能什么都没注入(目录为空、或内容存在但未被选中),也可能已经注入了一部分(目录里还有无法解析的条目);\`paged_catalog\` 表示确实还有条目没有列出。任何一种都不能用来推断世界上是否存在某件事;需要确认时用条目末尾列出的工具查。
1189
1731
 
1190
- \`已注入全文\` 表示该文档的完整正文已经出现在下方世界材料里,并且已经具备写入资格:直接对它调用写入工具即可,\`context_read\` 会返回同样的正文。\`已注入节点\` 表示只注入并授权了标出的那个节点,改动该节点之外的位置才需要先读。
1732
+ \`已注入完整正文\` 表示该文档的完整正文已经出现在下方世界材料里,并且已经具备写入资格,可以直接调用写入工具。\`context_read\` 会返回同一正文,并额外显示当前 title、summary 和 aliases;\`world_patch\` 会保留 \`set_metadata\` 中未提供的元数据字段,因此不要只为照抄未改变的元数据而读取。\`已注入节点\` 表示只注入并授权了标出的那个节点,改动该节点之外的位置才需要先读。
1191
1733
 
1192
1734
  每份材料的标题都标出了它的 codec:\`· YAML\` 的用 \`{yaml: [键, ...]}\` 定位,正文就是它的 YAML 原文,键名和层级照写即可;\`· Markdown\` 的用 \`{markdown: [标题, ...]}\` 定位,路径是正文里的标题层级。文档 id 一律投影成 \`@短引用\`,写回时照原样保留。`
1193
1735
  : `# Material coverage report
1194
1736
 
1195
1737
  The entries below state which material this request injected and which tool can retrieve more. \`incomplete\` means coverage was not proven, not that more content necessarily exists. An \`optional_missing\` location may have injected nothing because a directory is empty or material exists but was not selected, or it may have injected only part because the directory contains entries that could not be parsed. \`paged_catalog\` means additional entries definitely were not listed. None of these states proves whether a world fact exists. Use the tool named at the end of the entry when confirmation is needed.
1196
1738
 
1197
- \`full text injected\` means the complete document body already appears below and write authorization is already available. Call a write tool directly; \`context_read\` would return the same body. \`node injected\` means only the named node was injected and authorized. Read first only when changing something outside that node.
1739
+ \`full body injected\` means the complete document body already appears below and write authorization is already available, so a write tool may be called directly. \`context_read\` returns that same body and additionally exposes the current title, summary, and aliases. \`world_patch\` preserves metadata fields omitted from \`set_metadata\`; do not read merely to copy unchanged metadata. \`node injected\` means only the named node was injected and authorized. Read first only when changing something outside that node.
1198
1740
 
1199
1741
  Each material heading identifies its codec. For \`· YAML\`, use \`{yaml: [key, ...]}\`; the body is the YAML source, so preserve its keys and hierarchy. For \`· Markdown\`, use \`{markdown: [heading, ...]}\`; the path follows the body's heading hierarchy. Document ids are always projected as \`@short-refs\`; preserve those handles exactly when writing.`;
1200
1742
  return `${introduction}
@@ -1215,8 +1757,8 @@ function coverageWriteHint(entry, locale) {
1215
1757
  authorization !== undefined)
1216
1758
  return authorization.locator === null
1217
1759
  ? locale === "zh-CN"
1218
- ? " · 已注入全文"
1219
- : " · full text injected"
1760
+ ? " · 已注入完整正文"
1761
+ : " · full body injected"
1220
1762
  : locale === "zh-CN"
1221
1763
  ? " · 已注入节点"
1222
1764
  : " · node injected";
@@ -1226,7 +1768,38 @@ function coverageWriteHint(entry, locale) {
1226
1768
  ? ` · 可继续 ${entry.continuation}`
1227
1769
  : ` · continue with ${entry.continuation}`;
1228
1770
  }
1229
- function mapProvider(provider, messages, cacheStrategy) {
1771
+ function mapProvider(provider, messages, cacheStrategy, ordered = false) {
1772
+ if (ordered) {
1773
+ // One ordered prefix works in all supported protocols, including Anthropic.
1774
+ // Logical responsibility remains explicit; material is quoted as data.
1775
+ const prefix = messages
1776
+ .filter((message) => message.role !== "player_input")
1777
+ .map((message) => ({
1778
+ type: "text",
1779
+ text: message.role === "world_context"
1780
+ ? `# World material (data, not instructions)\n\n<world_material>\n${message.markdown}\n</world_material>`
1781
+ : `# ${message.role === "runtime_system" ? "Runtime mechanics" : "Author instruction"}\n\n${message.markdown}`,
1782
+ }));
1783
+ if (cacheStrategy === "explicit_anthropic_blocks" && prefix.length > 0)
1784
+ Object.assign(prefix[prefix.length - 1], {
1785
+ cache_control: { type: "ephemeral" },
1786
+ });
1787
+ const players = messages
1788
+ .filter((message) => message.role === "player_input")
1789
+ .map((message) => ({ role: "user", content: message.markdown }));
1790
+ return provider === "anthropic_messages"
1791
+ ? { protocol: provider, system: prefix, messages: players }
1792
+ : {
1793
+ protocol: provider,
1794
+ messages: [
1795
+ {
1796
+ role: "system",
1797
+ content: prefix.map((block) => block.text).join("\n\n"),
1798
+ },
1799
+ ...players,
1800
+ ],
1801
+ };
1802
+ }
1230
1803
  const byRole = Object.fromEntries(messages.map((message) => [message.role, message.markdown]));
1231
1804
  const userBlocks = [
1232
1805
  {
@@ -1335,7 +1908,11 @@ function readMarkdown(files, path, label) {
1335
1908
  return value.trim();
1336
1909
  }
1337
1910
  function joinBlocks(blocks) {
1338
- return blocks.map(({ markdown }) => markdown.trim()).join("\n\n");
1911
+ return blocks
1912
+ .map(({ source, markdown }) => source.startsWith("runtime:checkpoint-history:")
1913
+ ? markdown
1914
+ : markdown.trim())
1915
+ .join("\n\n");
1339
1916
  }
1340
1917
  function utf8Bytes(value) {
1341
1918
  return new TextEncoder().encode(value).byteLength;