narraeon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +164 -0
  3. package/README.zh-CN.md +164 -0
  4. package/dist/node/cli/NarraeonCli.js +163 -0
  5. package/dist/node/cli/main.js +38 -0
  6. package/dist/node/protocol/appPreferences.js +5 -0
  7. package/dist/node/protocol/contentTree.js +14 -0
  8. package/dist/node/protocol/modelConnections.js +110 -0
  9. package/dist/node/protocol/playerViews.js +1 -0
  10. package/dist/node/protocol/v1.js +278 -0
  11. package/dist/node/runtime/V1Runtime.js +625 -0
  12. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +1914 -0
  13. package/dist/node/runtime/config/AppPreferencesStore.js +92 -0
  14. package/dist/node/runtime/config/appPaths.js +19 -0
  15. package/dist/node/runtime/content/ContentLimits.js +1 -0
  16. package/dist/node/runtime/content/ContentTreeFile.js +1 -0
  17. package/dist/node/runtime/content/ContentWorkspace.js +601 -0
  18. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +721 -0
  19. package/dist/node/runtime/content/FileNativeContentTree.js +512 -0
  20. package/dist/node/runtime/extension/FrontendExtensionBundle.js +229 -0
  21. package/dist/node/runtime/extension/PlayerViewPanelProjector.js +121 -0
  22. package/dist/node/runtime/model/AiFailureLog.js +332 -0
  23. package/dist/node/runtime/model/FileNativeModelAdapters.js +1246 -0
  24. package/dist/node/runtime/model/ModelConnectionStore.js +378 -0
  25. package/dist/node/runtime/model/ModelHost.js +89 -0
  26. package/dist/node/runtime/model/ModelHostStream.js +367 -0
  27. package/dist/node/runtime/model/ProviderExchangeState.js +1 -0
  28. package/dist/node/runtime/model/ProviderStream.js +90 -0
  29. package/dist/node/runtime/model/SettingAuthorStream.js +118 -0
  30. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +304 -0
  31. package/dist/node/runtime/play/FileNativePlayPresetStore.js +1856 -0
  32. package/dist/node/runtime/play/PlayCallChain.js +1280 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +1121 -0
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +183 -0
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +185 -0
  36. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +1425 -0
  37. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +192 -0
  38. package/dist/node/runtime/prompt/MaterialSelection.js +95 -0
  39. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +1726 -0
  40. package/dist/node/runtime/world/FileNativeWorldStore.js +2028 -0
  41. package/dist/node/runtime/world/PlayerViewRenderer.js +322 -0
  42. package/dist/node/runtime/world/WorldDocumentStore.js +2808 -0
  43. package/dist/node/runtime/world/WorldOperationCoordinator.js +490 -0
  44. package/dist/node/runtime/world/WorldStateRevision.js +75 -0
  45. package/dist/node/server/createRuntime.js +6 -0
  46. package/dist/node/server/createServer.js +218 -0
  47. package/dist/node/server/main.js +25 -0
  48. package/dist/node/server/startWebServer.js +28 -0
  49. package/dist/node/shared/default-play-prompts.js +85 -0
  50. package/dist/node/shared/default-preset-host.js +538 -0
  51. package/dist/node/shared/default-setting-improvement-prompt.js +77 -0
  52. package/dist/node/shared/first-party-action-choices.js +164 -0
  53. package/dist/node/shared/first-party-generic-panels.js +105 -0
  54. package/dist/node/shared/first-party-play-preset-templates.js +41 -0
  55. package/dist/node/shared/first-party-player-view.js +179 -0
  56. package/dist/node/shared/play-preset-prompt-roles.js +4 -0
  57. package/dist/web/assets/index-CRGTZ9Ak.css +1 -0
  58. package/dist/web/assets/index-DDx1itzT.js +1192 -0
  59. package/dist/web/index.html +14 -0
  60. package/package.json +86 -0
@@ -0,0 +1,1425 @@
1
+ import { parseDocument, stringify } from "yaml";
2
+ import { defaultAppLocale, } from "../../protocol/appPreferences.js";
3
+ import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
4
+ import { defaultRuntimeToolDefinitionStrategy, registeredRuntimeToolNames, runtimeToolsForNames, } from "./FileNativeToolRegistry.js";
5
+ export class PromptCompilationError extends Error {
6
+ code;
7
+ details;
8
+ constructor(code, message, details = null) {
9
+ super(message);
10
+ this.name = "PromptCompilationError";
11
+ this.code = code;
12
+ this.details = details;
13
+ }
14
+ }
15
+ const runtimeContracts = {
16
+ en: {
17
+ play: `# Runtime authority boundary
18
+
19
+ - Only Runtime can formally write an operation into the world. Model text and tool calls cannot bypass Runtime to commit results directly.
20
+ - The current play preset declares narrative rules, call-chain tools, and follow-up artifacts. Any capability it does not declare is unavailable.
21
+ - Editable host, world, and play prompts determine story semantics, player agency, point of view, and style. Runtime checks only files, references, authority, and atomic commits; it does not judge the story for the author.`,
22
+ tools: `# Runtime tool contract
23
+
24
+ - 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.
26
+ - 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
+ - World-write tools change only an uncommitted working copy. Their changes are not official world facts until Runtime accepts and completes the commit.`,
28
+ operation: `# Runtime call-chain rules
29
+
30
+ - The model may output player-visible text directly, call attached read or world-write tools, and then continue from tool results.
31
+ - Obey only the real tool definitions attached to this request. A tool that is not defined cannot be called.
32
+ - Runtime performs state commits and saves follow-up artifacts. Tool exchanges and internal processing must never appear in player-visible content.`,
33
+ shell: `# Runtime play boundary
34
+
35
+ The model may output player-visible text directly, call attached read or world-write tools as needed, or continue after receiving tool results. 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.`,
36
+ },
37
+ "zh-CN": {
38
+ play: `# Runtime 权限边界
39
+
40
+ - 只有 Runtime 能把一次操作正式写入世界。模型的文字或工具调用都不能绕过 Runtime 直接提交结果。
41
+ - 当前玩法文件声明叙事规则、调用链工具和后置产物;未声明的能力一律不可用。
42
+ - 可编辑的主持、世界和玩法提示决定故事语义、玩家代理权、人称与文风;Runtime 只检查文件、引用、权限和原子提交,不替作者判断剧情。`,
43
+ tools: `# Runtime 工具契约
44
+
45
+ - 只使用请求随附的工具;读取结果会明确范围、cursor 与完整性。目录只能使用 Runtime 返回的 @dir-*,文档和历史只能使用 Runtime 返回的 @句柄,不要把 world/路径或自然语言名称冒充句柄。
46
+ - 材料覆盖报告标为 \`已注入全文\` 的文档,其完整原文已经逐字在本次请求里,写入资格同样已经具备。不要为确认结构、核对字段或"保险起见"重读它:context_read 只会原样返回你已经看到的字节,白费一次往返。只有需要报告未覆盖的内容时才读。
47
+ - 字面搜索 0 命中不证明事实不存在。工具、目录、文档、档案、检索、命中、句柄、Runtime 及失败过程只供私下裁决;玩家可见叙事不得出现“没搜到/没找到资料”等内部措辞。信息不足时保持世界内的不确定性,不编造内部过程。
48
+ - 世界写入工具只修改尚未提交的工作副本;Runtime 接受并完成提交前,这些修改都不是正式世界事实。`,
49
+ operation: `# Runtime 调用链规则
50
+
51
+ - 模型可以直接输出玩家可见文本,或调用本次请求随附的读取和世界写入工具,再根据工具结果继续。
52
+ - 只服从当前请求的真实工具定义;没有定义的工具不可调用。
53
+ - 状态提交与后置产物保存由 Runtime 执行。工具交换与内部处理过程不得混入玩家可见内容。`,
54
+ shell: `# Runtime 游玩边界
55
+
56
+ 模型可以根据当前需要直接输出玩家可见文本、调用请求随附的读取或世界写入工具,或在取得工具结果后继续。Runtime 只执行真实工具定义、文件校验和权威提交;本段不规定故事、人称、文风、玩家代理权或状态语义。`,
57
+ },
58
+ };
59
+ const playCallChainToolNames = new Set([
60
+ "context_list",
61
+ "context_search",
62
+ "context_read",
63
+ "world_patch",
64
+ "world_create",
65
+ ]);
66
+ export class FileNativePromptCompiler {
67
+ #toolStrategyOverride;
68
+ #locale;
69
+ constructor(options = {}) {
70
+ this.#toolStrategyOverride = options.toolStrategy;
71
+ this.#locale = options.locale ?? defaultAppLocale;
72
+ }
73
+ setLocale(locale) {
74
+ this.#locale = locale;
75
+ }
76
+ renderWorldDocument(snapshot, selector) {
77
+ const rendered = renderSnapshotDocument(snapshot, selector);
78
+ if (rendered.kind === "error")
79
+ throwQueryFailure(rendered, "read_document");
80
+ return rendered.markdown;
81
+ }
82
+ async sendBootstrap(input, adapter) {
83
+ const compiled = this.compileBootstrap(input);
84
+ return await adapter.sendBootstrap({
85
+ provider: compiled.provider,
86
+ tools: compiled.tools,
87
+ modelId: input.modelBinding.modelId,
88
+ maxOutputTokens: input.modelBinding.maxOutputTokens,
89
+ });
90
+ }
91
+ compileBootstrap(input) {
92
+ const effectiveInput = withoutAppendedContextGenesis(input);
93
+ validateModel(effectiveInput.modelBinding);
94
+ const documentSnapshot = effectiveInput.world.documentSnapshot;
95
+ const worldFiles = snapshotFiles(documentSnapshot);
96
+ const hostFrame = readYamlRecord(effectiveInput.hostBinding.files["frame.yaml"], "host frame");
97
+ const worldFrame = readYamlRecord(worldFiles["control/frame.yaml"], "world frame");
98
+ requireFormat(hostFrame, "narraeon.host-frame/v1", "host frame");
99
+ requireFormat(worldFrame, "narraeon.world-frame/v1", "world frame");
100
+ const coverage = [];
101
+ const instructions = readWorldInstructions(worldFiles, worldFrame);
102
+ const context = resolveContext(effectiveInput, worldFrame, documentSnapshot, coverage, this.#locale);
103
+ const hostRoles = compileHostRoles(effectiveInput.hostBinding.files, hostFrame, instructions, context, coverage, this.#locale);
104
+ const blocks = {
105
+ ...hostRoles,
106
+ player_input: effectiveInput.playerInputPlacement === "append"
107
+ ? []
108
+ : [
109
+ {
110
+ source: "player:input",
111
+ markdown: `${this.#locale === "zh-CN" ? "# 玩家原文" : "# Player input"}\n\n${effectiveInput.playerInput.trim()}`,
112
+ },
113
+ ],
114
+ };
115
+ const logicalMessages = Object.keys(blocks)
116
+ .filter((role) => blocks[role].length > 0)
117
+ .map((role) => ({
118
+ role,
119
+ blocks: blocks[role],
120
+ markdown: joinBlocks(blocks[role]),
121
+ }));
122
+ scanRuntimeLeakage(logicalMessages);
123
+ const tools = runtimeToolsForNames(registeredRuntimeToolNames, this.#locale);
124
+ const toolStrategy = this.#toolStrategyOverride ??
125
+ defaultRuntimeToolDefinitionStrategy(effectiveInput.modelBinding.provider);
126
+ const provider = mapProvider(effectiveInput.modelBinding.provider, logicalMessages);
127
+ const budget = disabledPromptBudget(effectiveInput);
128
+ const stableMessages = logicalMessages.filter(({ role }) => role !== "player_input");
129
+ const stableText = stableMessages
130
+ .map(({ role, markdown }) => `${role}\n${markdown}`)
131
+ .join("\n");
132
+ const cache = stableCacheBoundary(stableText, tools, toolStrategy);
133
+ return {
134
+ logicalMessages,
135
+ provider,
136
+ tools,
137
+ toolUniverse: structuredClone(tools),
138
+ toolStrategy,
139
+ coverage,
140
+ budget,
141
+ cache: {
142
+ ...cache,
143
+ breakpoints: ["runtime_system", "author_instruction", "world_context"],
144
+ },
145
+ };
146
+ }
147
+ preview(input, playPreset) {
148
+ const playCompilation = playPreset === undefined
149
+ ? undefined
150
+ : this.compilePlayCallChain(input, playPreset);
151
+ return promptPreview(input, playPreset, playCompilation?.bootstrap ?? this.compileBootstrap(input), playCompilation);
152
+ }
153
+ previewPlayPreset(input, playPreset) {
154
+ const playCompilation = this.compilePlayPreset(input, playPreset);
155
+ return promptPreview(input, playPreset, playCompilation.bootstrap, playCompilation);
156
+ }
157
+ /**
158
+ * Compile the production play surface. The selected preset contributes its
159
+ * frozen tool universe and narrative guidance. The genesis opening remains
160
+ * Authority history for the player-facing record; it is not repeated as
161
+ * model history in a fresh play context.
162
+ */
163
+ compilePlayCallChain(input, binding) {
164
+ const presetCompilation = this.compilePlayPreset(input, binding);
165
+ const toolUniverse = presetCompilation.toolUniverse.filter(({ name }) => playCallChainToolNames.has(name));
166
+ const logicalMessages = playCallChainNarrativeGuidance(presetCompilation.bootstrap.logicalMessages, binding);
167
+ const provider = mapProvider(input.modelBinding.provider, logicalMessages);
168
+ const stableText = logicalMessages
169
+ .filter(({ role }) => role !== "player_input")
170
+ .map(({ role, markdown }) => `${role}\n${markdown}`)
171
+ .join("\n");
172
+ const bootstrap = {
173
+ ...structuredClone(presetCompilation.bootstrap),
174
+ logicalMessages,
175
+ provider,
176
+ tools: structuredClone(toolUniverse),
177
+ toolUniverse: structuredClone(toolUniverse),
178
+ cache: {
179
+ ...presetCompilation.bootstrap.cache,
180
+ ...stableCacheBoundary(stableText, toolUniverse, presetCompilation.toolStrategy),
181
+ },
182
+ };
183
+ return {
184
+ bootstrap,
185
+ toolUniverse: structuredClone(toolUniverse),
186
+ toolStrategy: presetCompilation.toolStrategy,
187
+ followups: compileFollowups(binding, this.#locale),
188
+ };
189
+ }
190
+ /**
191
+ * Compile a frozen play preset into one stable bootstrap plus its followups.
192
+ * Runtime orchestration can reuse this seam when it starts a real provider
193
+ * session; Preview is only one consumer of the compilation.
194
+ */
195
+ compilePlayPreset(input, binding) {
196
+ const bootstrap = this.compilePlayBootstrap(input);
197
+ return compilePlayPresetCompilation(input, bootstrap, binding, this.#locale);
198
+ }
199
+ /**
200
+ * A play preset owns the editable story policy. Its bootstrap keeps only the
201
+ * mechanical Runtime shell; narrative semantics remain author instructions.
202
+ */
203
+ compilePlayBootstrap(input) {
204
+ const base = this.compileBootstrap(input);
205
+ const logicalMessages = base.logicalMessages.map((message) => {
206
+ if (message.role !== "runtime_system")
207
+ return message;
208
+ const blocks = message.blocks.filter(({ source }) => !source.startsWith("runtime:builtin/"));
209
+ blocks.push({
210
+ source: "runtime:play-shell",
211
+ markdown: runtimeContracts[this.#locale].shell,
212
+ });
213
+ return { role: message.role, blocks, markdown: joinBlocks(blocks) };
214
+ });
215
+ const provider = mapProvider(input.modelBinding.provider, logicalMessages);
216
+ const budget = disabledPromptBudget(input);
217
+ const stableMessages = logicalMessages.filter(({ role }) => role !== "player_input");
218
+ const stableText = stableMessages
219
+ .map(({ role, markdown }) => `${role}\n${markdown}`)
220
+ .join("\n");
221
+ const cache = stableCacheBoundary(stableText, base.tools, base.toolStrategy);
222
+ return {
223
+ ...base,
224
+ logicalMessages,
225
+ provider,
226
+ budget,
227
+ cache: {
228
+ ...base.cache,
229
+ ...cache,
230
+ },
231
+ };
232
+ }
233
+ }
234
+ /**
235
+ * Compile each post-commit followup as an independent derived request. Each
236
+ * request keeps only its author prompt and artifact contract; the main call
237
+ * chain supplies the shared prefix.
238
+ */
239
+ function compileFollowups(binding, locale) {
240
+ return binding.definition.followups.map((followup) => {
241
+ const markdown = binding.definition.files[followup.prompt.path];
242
+ if (markdown === undefined || markdown.trim() === "")
243
+ throw new PromptCompilationError("play_preset_prompt_missing", `Follow-up prompt block does not exist: ${followup.prompt.path}`);
244
+ const blocks = [
245
+ {
246
+ source: `play:${followup.prompt.path}`,
247
+ markdown: markdown.trim(),
248
+ },
249
+ {
250
+ source: `runtime:followup/${followup.id}`,
251
+ markdown: followupRuntimeContract(followup, locale),
252
+ },
253
+ ];
254
+ return {
255
+ id: followup.id,
256
+ displayName: followup.displayName,
257
+ logicalMessages: [
258
+ {
259
+ role: "author_instruction",
260
+ blocks,
261
+ markdown: joinBlocks(blocks),
262
+ },
263
+ ],
264
+ tools: runtimeToolsForNames(followupToolNames, locale),
265
+ allowedTools: [...followupToolNames],
266
+ artifacts: structuredClone(followup.artifacts),
267
+ maxArtifactBytes: followup.maxArtifactBytes,
268
+ };
269
+ });
270
+ }
271
+ const followupToolNames = [
272
+ "artifact_emit",
273
+ "artifact_clear",
274
+ ];
275
+ /**
276
+ * The mechanical half of a followup prompt. It states the one thing the model
277
+ * cannot infer from the author text: this request is dispatched once after the
278
+ * main call chain has committed its core results, and it may only emit the
279
+ * declared artifacts.
280
+ */
281
+ function followupRuntimeContract(followup, locale) {
282
+ const none = locale === "zh-CN" ? "(无)" : "(none)";
283
+ const header = locale === "zh-CN"
284
+ ? `# Runtime 后置请求规则
285
+
286
+ 核心叙事与世界状态已经提交,本次请求不写世界、不产生叙事、不影响已提交的结果。它只发出一次,没有后续往返。
287
+
288
+ 只能提交下面声明的产物;模型选择 output name 并提供 payload,其余字段由 Runtime 固定。`
289
+ : `# Runtime follow-up request rules
290
+
291
+ The core narrative and world state have already been committed. This request does not write the world, produce narrative, or alter committed results. It is dispatched once and has no later round trip.
292
+
293
+ Only the artifacts declared below may be submitted. The model chooses an output name and supplies its payload; Runtime fixes every other field.`;
294
+ return `${header}
295
+
296
+ ${followup.artifacts
297
+ .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
298
+ ? ""
299
+ : `; payloadContract=${payloadContractSummary(artifact.payloadContract)}`}`)
300
+ .join("\n")}`;
301
+ }
302
+ /**
303
+ * Narrative prompt blocks are author instructions appended to
304
+ * `author_instruction`; they apply whenever the model writes player-visible
305
+ * prose.
306
+ */
307
+ function playCallChainNarrativeGuidance(messages, binding) {
308
+ const narrativeBlocks = binding.definition.narrativePrompts.map((prompt) => {
309
+ const markdown = binding.definition.files[prompt.path];
310
+ if (markdown === undefined)
311
+ throw new PromptCompilationError("play_preset_prompt_missing", `Play-preset narrative prompt block does not exist: ${prompt.path}`);
312
+ return { source: `play:${prompt.path}`, markdown: markdown.trim() };
313
+ });
314
+ if (narrativeBlocks.length === 0)
315
+ return cloneLogicalMessages(messages);
316
+ let found = false;
317
+ const result = cloneLogicalMessages(messages).map((message) => {
318
+ if (message.role !== "author_instruction")
319
+ return message;
320
+ found = true;
321
+ const blocks = [...message.blocks, ...narrativeBlocks];
322
+ return { ...message, blocks, markdown: joinBlocks(blocks) };
323
+ });
324
+ if (!found)
325
+ throw new PromptCompilationError("host_frame_invalid", "The play call chain has no author_instruction role for narrative guidance");
326
+ scanRuntimeLeakage(result);
327
+ return result;
328
+ }
329
+ function withoutAppendedContextGenesis(input) {
330
+ if (input.playerInputPlacement !== "append")
331
+ return input;
332
+ const additionalMaterials = input.world.additionalMaterials.filter((material) => material.kind !== "history_message" ||
333
+ !material.message.endsWith(".message.genesis.narrator"));
334
+ if (additionalMaterials.length === input.world.additionalMaterials.length)
335
+ return input;
336
+ return {
337
+ ...input,
338
+ world: { ...input.world, additionalMaterials },
339
+ };
340
+ }
341
+ function promptPreview(input, playPreset, compilation, playCompilation) {
342
+ const result = {
343
+ diagnosticBinding: {
344
+ endpoint: input.endpoint.id,
345
+ commit: input.endpoint.commit,
346
+ hostPresetId: input.hostBinding.hostPresetId,
347
+ controlFingerprint: input.world.controlFingerprint,
348
+ modelId: input.modelBinding.modelId,
349
+ ...(playPreset === undefined
350
+ ? {}
351
+ : {
352
+ playPresetId: playPreset.id,
353
+ playPresetRevision: playPreset.revision,
354
+ }),
355
+ },
356
+ compilation,
357
+ ...(input.playerInputPlacement === "append"
358
+ ? {
359
+ initialAppend: {
360
+ logical: { kind: "player", text: input.playerInput },
361
+ provider: { role: "user", content: input.playerInput },
362
+ },
363
+ }
364
+ : {}),
365
+ leakage: { status: "clean", checkedFields: leakageFields },
366
+ };
367
+ if (playPreset !== undefined && playCompilation !== undefined)
368
+ result.playPreset = createPlayPresetPreview(playPreset, playCompilation);
369
+ return result;
370
+ }
371
+ function createPlayPresetPreview(binding, compilation) {
372
+ const { bootstrap } = compilation;
373
+ return {
374
+ id: binding.id,
375
+ name: binding.name,
376
+ revision: binding.revision,
377
+ callChainPath: binding.definition.callChainPath,
378
+ mounts: structuredClone(binding.definition.mounts),
379
+ extensionRefs: [...binding.definition.extensionRefs],
380
+ toolUniverse: compilation.toolUniverse,
381
+ toolStrategy: compilation.toolStrategy,
382
+ bootstrap,
383
+ followups: structuredClone(compilation.followups),
384
+ cache: {
385
+ stablePrefixFingerprint: bootstrap.cache.stablePrefixFingerprint,
386
+ toolDefinitionBoundary: "stable",
387
+ },
388
+ };
389
+ }
390
+ /**
391
+ * A frozen preset compiles to one stable bootstrap plus its follow-ups. The
392
+ * main chain is a single logical context, so everything the preset contributes
393
+ * is either part of the bootstrap or a derived request dispatched after the
394
+ * chain settles.
395
+ */
396
+ function compilePlayPresetCompilation(input, bootstrap, binding, locale) {
397
+ const toolUniverse = fileNativeToolsForNames(registeredRuntimeToolNames, locale);
398
+ const toolStrategy = bootstrap.toolStrategy;
399
+ const stableMessages = bootstrap.logicalMessages.filter(({ role }) => role !== "player_input");
400
+ const stableText = stableMessages
401
+ .map(({ role, markdown }) => `${role}\n${markdown}`)
402
+ .join("\n");
403
+ const sessionBootstrap = {
404
+ ...bootstrap,
405
+ tools: structuredClone(toolUniverse),
406
+ toolUniverse: structuredClone(toolUniverse),
407
+ toolStrategy,
408
+ budget: disabledPromptBudget(input),
409
+ cache: {
410
+ ...bootstrap.cache,
411
+ ...stableCacheBoundary(stableText, toolUniverse, toolStrategy),
412
+ },
413
+ };
414
+ return {
415
+ bootstrap: sessionBootstrap,
416
+ toolUniverse,
417
+ toolStrategy,
418
+ followups: compileFollowups(binding, locale),
419
+ };
420
+ }
421
+ function cloneLogicalMessages(messages) {
422
+ return messages.map((message) => ({
423
+ role: message.role,
424
+ markdown: message.markdown,
425
+ blocks: message.blocks.map(({ source, markdown }) => ({
426
+ source,
427
+ markdown,
428
+ })),
429
+ }));
430
+ }
431
+ /**
432
+ * Render one appended prompt block for providers without a native system-role
433
+ * append. Only author instructions travel this way now that Runtime no longer
434
+ * authors phase deltas.
435
+ */
436
+ export function renderPromptDeltaMessage(role, markdown, locale = defaultAppLocale) {
437
+ if (role !== "author_instruction")
438
+ throw new PromptCompilationError("prompt_delta_role_invalid", `Runtime constructed an invalid appended role: ${role}`);
439
+ return `${locale === "zh-CN" ? "# 作者提示" : "# Author instruction"}\n\n${markdown}`;
440
+ }
441
+ function payloadContractSummary(contract) {
442
+ const shape = contract.type === "array"
443
+ ? `array${contract.minItems === undefined ? "" : `[${contract.minItems},`}${contract.maxItems === undefined ? "" : `${contract.maxItems}]`}${contract.uniqueBy === undefined ? "" : ` uniqueBy=${contract.uniqueBy}`}`
444
+ : contract.type === "object"
445
+ ? `object{${Object.keys(contract.properties ?? {}).join(",")}}`
446
+ : contract.type;
447
+ const required = contract.required === undefined
448
+ ? ""
449
+ : ` required=${contract.required.join(",")}`;
450
+ return `${shape}${required}${contract.maxBytes === undefined ? "" : ` maxBytes=${contract.maxBytes}`}`;
451
+ }
452
+ export function createMinimalFileNativePreviewInput(input) {
453
+ const locale = input.locale ?? defaultAppLocale;
454
+ const worldFiles = {
455
+ "control/frame.yaml": `format: narraeon.world-frame/v1
456
+ bindings:
457
+ currentSituation: situation.current
458
+ instructions:
459
+ - markdown: blocks/world-style.md
460
+ context:
461
+ - slot: { kind: current_situation }
462
+ - slot: { kind: catalog, directory: characters, maxEntries: 24 }
463
+ - slot: { kind: additional_materials }
464
+ `,
465
+ "control/blocks/world-style.md": locale === "zh-CN"
466
+ ? "# 世界状态规则\n\n只保存已经发生且下一次行动不能忽略的结果;人物变化写入对应人物,眼前未结束的局面写入当前情境。\n"
467
+ : "# 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",
468
+ "state/current-situation.yaml": `$document:
469
+ id: situation.current
470
+ ref: current-situation
471
+ title: ${locale === "zh-CN" ? "当前情境" : "Current situation"}
472
+ summary: ${locale === "zh-CN" ? "宿舍里的当前局面。" : "The current situation in the dorm room."}
473
+ aliases: []
474
+ ${locale === "zh-CN" ? "地点" : "location"}: ${locale === "zh-CN" ? "男生宿舍 302" : "Dorm room 302"}
475
+ ${locale === "zh-CN" ? "人物" : "characters"}:
476
+ - $ref: character.alex
477
+ ${locale === "zh-CN" ? "情况" : "situation"}: ${locale === "zh-CN" ? "Alex 正在整理球衣。" : "Alex is folding a jersey."}
478
+ `,
479
+ "state/characters/alex.yaml": `$document:
480
+ id: character.alex
481
+ ref: alex
482
+ title: Alex
483
+ summary: ${locale === "zh-CN" ? "篮球队前锋,直率护短。" : "A direct, loyal basketball forward."}
484
+ aliases: []
485
+ ${locale === "zh-CN" ? "衣着" : "clothing"}: ${locale === "zh-CN" ? "白色运动背心,运动短裤,拖鞋" : "White athletic top, shorts, and sandals"}
486
+ `,
487
+ };
488
+ return {
489
+ endpoint: { id: "preview-endpoint", commit: "preview-commit" },
490
+ hostBinding: {
491
+ hostPresetId: "preview-host",
492
+ files: {
493
+ "frame.yaml": `format: narraeon.host-frame/v1
494
+ roles:
495
+ runtime_system:
496
+ - builtin: runtime.play-contract
497
+ - builtin: runtime.tool-contract
498
+ - builtin: runtime.operation-contract
499
+ author_instruction:
500
+ - markdown: blocks/style.md
501
+ - include: world.instructions
502
+ world_context:
503
+ - builtin: runtime.coverage
504
+ - include: world.context
505
+ `,
506
+ "blocks/style.md": locale === "zh-CN"
507
+ ? "# 主持风格\n\n克制、具体,不替玩家行动。\n"
508
+ : "# Hosting style\n\nBe restrained and specific, and never act on the player's behalf.\n",
509
+ },
510
+ },
511
+ world: {
512
+ controlFingerprint: "preview-control",
513
+ documentSnapshot: WorldDocumentStore.open({
514
+ layout: "world_state",
515
+ files: Object.entries(worldFiles).map(([path, contents]) => ({
516
+ path,
517
+ contents,
518
+ })),
519
+ }),
520
+ additionalMaterials: [],
521
+ },
522
+ playerInputPlacement: input.playerInputPlacement,
523
+ playerInput: input.playerInput,
524
+ modelBinding: input,
525
+ };
526
+ }
527
+ function validateModel(model) {
528
+ if (!Number.isSafeInteger(model.contextWindowTokens) ||
529
+ model.contextWindowTokens <= 0) {
530
+ throw new PromptCompilationError("model_context_window_invalid", "The model must provide a valid contextWindowTokens value");
531
+ }
532
+ if (!Number.isSafeInteger(model.maxOutputTokens) ||
533
+ model.maxOutputTokens <= 0) {
534
+ throw new PromptCompilationError("model_max_output_invalid", "The model must provide a valid maxOutputTokens value");
535
+ }
536
+ }
537
+ function snapshotFiles(snapshot) {
538
+ return Object.fromEntries(snapshot.files.map(({ path, contents }) => [path, contents]));
539
+ }
540
+ function parseDocumentQueryTarget(handle) {
541
+ return {
542
+ selector: handle.startsWith("@")
543
+ ? { shortRef: handle.slice(1) }
544
+ : { documentId: handle },
545
+ source: handle,
546
+ };
547
+ }
548
+ function descriptorQueryTarget(descriptor) {
549
+ return {
550
+ selector: { documentId: descriptor.documentId },
551
+ source: `@${descriptor.shortRef}`,
552
+ };
553
+ }
554
+ function modelVisibleDocumentSource(target, failure, locale = defaultAppLocale) {
555
+ if (failure?.document !== undefined)
556
+ return `@${failure.document.shortRef}`;
557
+ return target.source.startsWith("@")
558
+ ? target.source
559
+ : locale === "zh-CN"
560
+ ? "(文档不可用)"
561
+ : "(document unavailable)";
562
+ }
563
+ function locatorCoverageKey(locator) {
564
+ return "yaml" in locator
565
+ ? `yaml:${locator.yaml.join("/")}`
566
+ : `markdown:${locator.markdown.join("/")}`;
567
+ }
568
+ function readCompleteDocument(snapshot, selector) {
569
+ let cursor = null;
570
+ let descriptor = null;
571
+ let codec = null;
572
+ let body = "";
573
+ do {
574
+ const result = snapshot.query({
575
+ kind: "read_document",
576
+ document: selector,
577
+ maxBytes: 65_536,
578
+ cursor,
579
+ });
580
+ if (result.kind === "error")
581
+ return result;
582
+ if (result.kind !== "read_document")
583
+ throw new PromptCompilationError("world_document_query_failed", "World-document snapshot returned the wrong whole-document query type");
584
+ descriptor ??= result.document;
585
+ codec ??= result.codec;
586
+ if (descriptor.documentId !== result.document.documentId ||
587
+ codec !== result.codec)
588
+ throw new PromptCompilationError("world_document_query_failed", "Whole-document pagination changed targets within one snapshot");
589
+ body += result.body;
590
+ cursor = result.page.nextCursor;
591
+ } while (cursor !== null);
592
+ if (descriptor === null || codec === null)
593
+ throw new PromptCompilationError("world_document_query_failed", "Whole-document query did not return a target");
594
+ return { kind: "complete_document", descriptor, codec, body };
595
+ }
596
+ function selectSnapshotNode(snapshot, selector, locator) {
597
+ const result = snapshot.query({
598
+ kind: "select_node",
599
+ document: selector,
600
+ locator,
601
+ });
602
+ if (result.kind === "error" || result.kind === "select_node")
603
+ return result;
604
+ throw new PromptCompilationError("world_document_query_failed", "World-document snapshot returned the wrong node-query type");
605
+ }
606
+ function renderSnapshotDocument(snapshot, selector) {
607
+ const read = readCompleteDocument(snapshot, selector);
608
+ if (read.kind === "error")
609
+ return read;
610
+ if (read.codec === "markdown")
611
+ return {
612
+ kind: "rendered_document",
613
+ descriptor: read.descriptor,
614
+ markdown: `## ${read.descriptor.title} [ref: @${read.descriptor.shortRef} · Markdown]\n\n> ${read.descriptor.summary}\n\n${read.body.trim()}`,
615
+ };
616
+ const selected = snapshot.query({
617
+ kind: "select_node",
618
+ document: selector,
619
+ locator: { yaml: [] },
620
+ });
621
+ if (selected.kind === "error")
622
+ return selected;
623
+ if (selected.kind !== "select_node" || selected.node.codec !== "yaml")
624
+ throw new PromptCompilationError("world_document_query_failed", "Whole YAML document did not return a YAML root-node projection");
625
+ return {
626
+ kind: "rendered_document",
627
+ descriptor: read.descriptor,
628
+ markdown: renderYamlDocument(read.descriptor, selected.node.value),
629
+ };
630
+ }
631
+ /**
632
+ * Serialize a YAML node exactly the way context_read does, so what bootstrap
633
+ * injects is what a later read would hand back. Internal document ids become
634
+ * the @shortRef handles the model can actually pass to a tool.
635
+ */
636
+ export function renderWorldYamlSource(value) {
637
+ return stringify(promptYamlValue(value), {
638
+ indent: 2,
639
+ lineWidth: 0,
640
+ }).trimEnd();
641
+ }
642
+ function renderYamlDocument(descriptor, value) {
643
+ if (!isRecord(value))
644
+ throw new PromptCompilationError("world_document_query_failed", "YAML world-document root node must be a map");
645
+ return `## ${descriptor.title} [ref: @${descriptor.shortRef} · YAML]\n\n> ${descriptor.summary}\n\n${renderWorldYamlSource(value)}`;
646
+ }
647
+ function projectedReference(value) {
648
+ if (!isRecord(value) ||
649
+ Object.keys(value).length !== 2 ||
650
+ typeof value.$ref !== "string" ||
651
+ !isRecord(value.target))
652
+ return null;
653
+ return typeof value.target.shortRef === "string"
654
+ ? { shortRef: value.target.shortRef }
655
+ : null;
656
+ }
657
+ function promptYamlValue(value) {
658
+ const reference = projectedReference(value);
659
+ if (reference !== null)
660
+ return { $ref: `@${reference.shortRef}` };
661
+ if (Array.isArray(value))
662
+ return value.map(promptYamlValue);
663
+ if (isRecord(value))
664
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
665
+ key,
666
+ promptYamlValue(child),
667
+ ]));
668
+ return value;
669
+ }
670
+ function throwQueryFailure(failure, operation) {
671
+ throw new PromptCompilationError("world_document_query_failed", `World-document snapshot could not complete the ${operation} query`, {
672
+ requestKind: failure.requestKind,
673
+ snapshotStatus: failure.snapshotStatus,
674
+ diagnostics: failure.diagnostics,
675
+ });
676
+ }
677
+ function resolveContext(input, worldFrame, snapshot, coverage, locale) {
678
+ const context = worldFrame.context;
679
+ if (!Array.isArray(context))
680
+ throw new PromptCompilationError("world_frame_invalid", "world frame.context must be an array");
681
+ const selected = [];
682
+ const currentSituation = isRecord(worldFrame.bindings)
683
+ ? worldFrame.bindings.currentSituation
684
+ : undefined;
685
+ // The model's own list must yield to every author slot, however the frame
686
+ // happens to order them, so resolve it last and splice it back into place.
687
+ let additionalMaterialsAt = null;
688
+ for (const entry of context) {
689
+ const slot = isRecord(entry) && isRecord(entry.slot) ? entry.slot : null;
690
+ if (slot === null || typeof slot.kind !== "string")
691
+ throw new PromptCompilationError("world_frame_invalid", "A context entry must be a slot");
692
+ if (slot.kind === "current_situation")
693
+ addDocumentSelection(parseDocumentQueryTarget(stringOrEmpty(currentSituation)), "current_situation", true, snapshot, selected, coverage, false, locale);
694
+ else if (slot.kind === "reference_targets")
695
+ resolveReferenceTargets(slot, snapshot, selected, coverage, locale);
696
+ else if (slot.kind === "catalog")
697
+ resolveCatalog(slot, snapshot, selected, coverage, locale);
698
+ else if (slot.kind === "history")
699
+ resolveRecentHistory(slot, input.world.history ?? {}, selected, coverage, locale);
700
+ else if (slot.kind === "additional_materials")
701
+ additionalMaterialsAt ??= {
702
+ selected: selected.length,
703
+ coverage: coverage.length,
704
+ };
705
+ else if (slot.kind === "document")
706
+ addDocumentSelection(parseDocumentQueryTarget(stringOrEmpty(slot.document)), "document", slot.required !== false, snapshot, selected, coverage, false, locale);
707
+ else if (slot.kind === "node")
708
+ addNodeSelection(parseDocumentQueryTarget(stringOrEmpty(slot.document)), slot.locator, "node", slot.required !== false, snapshot, selected, coverage, false, locale);
709
+ else
710
+ throw new PromptCompilationError("world_frame_invalid", `Unsupported slot: ${slot.kind}`);
711
+ }
712
+ assertNoOverlap(selected);
713
+ if (additionalMaterialsAt !== null) {
714
+ const withMaterials = [...selected];
715
+ const materialCoverage = [];
716
+ resolveAdditionalMaterials(input.world.additionalMaterials, snapshot, input.world.history ?? {}, withMaterials, materialCoverage, locale);
717
+ selected.splice(additionalMaterialsAt.selected, 0, ...withMaterials.slice(selected.length));
718
+ coverage.splice(additionalMaterialsAt.coverage, 0, ...materialCoverage);
719
+ }
720
+ return selected.map(({ source, markdown }) => ({ source, markdown }));
721
+ }
722
+ function addDocumentSelection(target, slot, required, snapshot, selected, coverage, dedupe = false, locale = defaultAppLocale) {
723
+ const document = renderSnapshotDocument(snapshot, target.selector);
724
+ if (document.kind === "error") {
725
+ if (required)
726
+ throw new PromptCompilationError("required_slot_missing", `Document for required slot ${slot} could not be read from the fixed snapshot: ${target.source}`, document.diagnostics);
727
+ coverage.push({
728
+ slot,
729
+ source: modelVisibleDocumentSource(target, document, locale),
730
+ status: "optional_missing",
731
+ complete: false,
732
+ continuation: "context_list",
733
+ });
734
+ return;
735
+ }
736
+ const key = `document:${document.descriptor.documentId}`;
737
+ const overlapping = overlappingSelection(selected, key);
738
+ // Selecting the same complete document is idempotent. In particular, an
739
+ // author may keep a character document fixed in the frame while
740
+ // reference_targets independently selects that character whenever the
741
+ // current situation says they are present. Presence remains world state;
742
+ // it must not make a valid fixed material fail prompt compilation.
743
+ if (overlapping?.key === key || (dedupe && overlapping !== null))
744
+ return;
745
+ selected.push({
746
+ key,
747
+ source: `slot:${slot}:@${document.descriptor.shortRef}`,
748
+ markdown: document.markdown,
749
+ });
750
+ coverage.push({
751
+ slot,
752
+ source: `@${document.descriptor.shortRef}`,
753
+ status: "resolved",
754
+ complete: true,
755
+ continuation: "context_read",
756
+ readAuthorization: {
757
+ shortRef: document.descriptor.shortRef,
758
+ locator: null,
759
+ },
760
+ });
761
+ }
762
+ function resolveReferenceTargets(slot, snapshot, selected, coverage, locale) {
763
+ const from = isRecord(slot.from) ? slot.from : {};
764
+ const sourceTarget = parseDocumentQueryTarget(stringOrEmpty(from.document));
765
+ const required = slot.required !== false;
766
+ const locator = isRecord(from.locator) &&
767
+ Object.keys(from.locator).length === 1 &&
768
+ Array.isArray(from.locator.yaml) &&
769
+ from.locator.yaml.every((segment) => typeof segment === "string")
770
+ ? { yaml: from.locator.yaml }
771
+ : null;
772
+ const locatorCoverage = locator === null ? "[]" : JSON.stringify(locator.yaml);
773
+ const result = locator === null
774
+ ? null
775
+ : selectSnapshotNode(snapshot, sourceTarget.selector, locator);
776
+ if (result === null ||
777
+ result.kind === "error" ||
778
+ result.node.codec !== "yaml") {
779
+ if (required)
780
+ throw new PromptCompilationError("required_slot_missing", "reference_targets source could not be selected exactly from the fixed snapshot", result?.kind === "error" ? result.diagnostics : null);
781
+ coverage.push({
782
+ slot: "reference_targets",
783
+ source: `${modelVisibleDocumentSource(sourceTarget, result?.kind === "error" ? result : undefined, locale)}#yaml:${locatorCoverage}`,
784
+ status: "optional_missing",
785
+ complete: false,
786
+ continuation: "context_read",
787
+ });
788
+ return;
789
+ }
790
+ const max = Number(slot.maxEntries ?? 0);
791
+ if (!Number.isInteger(max) ||
792
+ max < 1 ||
793
+ max > 64 ||
794
+ result.references.length > max)
795
+ throw new PromptCompilationError("slot_limit_exceeded", "reference_targets exceeds its explicit limit");
796
+ coverage.push({
797
+ slot: "reference_targets",
798
+ source: `@${result.document.shortRef}#yaml:${locatorCoverage}`,
799
+ status: "resolved",
800
+ complete: true,
801
+ continuation: "context_read",
802
+ });
803
+ for (const { target } of result.references)
804
+ addDocumentSelection(descriptorQueryTarget(target), "reference_targets", required, snapshot, selected, coverage, false, locale);
805
+ }
806
+ function resolveCatalog(slot, snapshot, selected, coverage, locale) {
807
+ const directory = stringOrEmpty(slot.directory);
808
+ const max = Number(slot.maxEntries ?? 0);
809
+ if (!validCatalogDirectory(directory))
810
+ throw new PromptCompilationError("world_frame_invalid", "catalog.directory must be a safe directory path relative to the world-document root");
811
+ if (!Number.isInteger(max) || max < 1 || max > 100)
812
+ throw new PromptCompilationError("world_frame_invalid", "catalog.maxEntries is invalid");
813
+ const matches = [];
814
+ const damaged = [];
815
+ let cursor = null;
816
+ do {
817
+ const result = snapshot.query({
818
+ kind: "catalog",
819
+ directory,
820
+ limit: 100,
821
+ cursor,
822
+ });
823
+ if (result.kind === "error")
824
+ throwQueryFailure(result, "catalog");
825
+ if (result.kind !== "catalog")
826
+ throw new PromptCompilationError("world_document_query_failed", "World-document snapshot returned the wrong directory-query type");
827
+ for (const entry of result.entries) {
828
+ if (entry.kind !== "document")
829
+ continue;
830
+ if (entry.status === "queryable" && entry.document !== undefined)
831
+ matches.push(entry.document);
832
+ else
833
+ damaged.push(entry.logicalPath);
834
+ }
835
+ cursor = result.page.nextCursor;
836
+ } while (cursor !== null);
837
+ if ((matches.length === 0 || damaged.length > 0) && slot.required !== false)
838
+ throw new PromptCompilationError("required_slot_missing", `Required catalog ${directory} does not completely associate its queryable direct child documents`, {
839
+ slot: "catalog",
840
+ directory,
841
+ logicalRoot: snapshot.logicalRoot,
842
+ matchedEntries: matches.length,
843
+ damaged,
844
+ });
845
+ const page = matches.slice(0, max);
846
+ const markdown = locale === "zh-CN"
847
+ ? `# ${directory} 目录\n\n${page.map((document) => `- ${document.title} [ref: @${document.shortRef}] — ${document.summary}`).join("\n") || "(空)"}\n\n显示 ${page.length}/${matches.length}。`
848
+ : `# ${directory} catalog\n\n${page.map((document) => `- ${document.title} [ref: @${document.shortRef}] — ${document.summary}`).join("\n") || "(empty)"}\n\nShowing ${page.length}/${matches.length}.`;
849
+ selected.push({
850
+ key: `catalog:${directory}`,
851
+ source: `slot:catalog:${directory}`,
852
+ markdown,
853
+ });
854
+ coverage.push({
855
+ slot: "catalog",
856
+ source: directory,
857
+ status: matches.length === 0 || damaged.length > 0
858
+ ? "optional_missing"
859
+ : matches.length > max
860
+ ? "paged_catalog"
861
+ : "resolved",
862
+ complete: matches.length > 0 && matches.length <= max && damaged.length === 0,
863
+ continuation: "context_list",
864
+ });
865
+ }
866
+ function validCatalogDirectory(directory) {
867
+ return (directory.length > 0 &&
868
+ directory
869
+ .split("/")
870
+ .every((part) => part !== "" && part !== "." && part !== ".."));
871
+ }
872
+ /**
873
+ * Inject the last few committed messages verbatim when a fresh context is
874
+ * compiled. Model-directed play may commit visible detail without duplicating
875
+ * every detail into a world document, so recent history remains an explicit
876
+ * source alongside the current state.
877
+ */
878
+ function resolveRecentHistory(slot, history, selected, coverage, locale) {
879
+ const recent = Number(slot.recent ?? 2);
880
+ if (!Number.isInteger(recent) || recent < 1 || recent > 32)
881
+ throw new PromptCompilationError("world_frame_invalid", "history slot recent must be an integer from 1 to 32");
882
+ // FileNativeWorldStore builds this record from the recovered Authority array
883
+ // in commit order. Its keys are semantic message IDs, not sortable file
884
+ // names: lexical sorting would put `message.genesis` after every numbered
885
+ // message and `message.9` after `message.15`.
886
+ const ordered = Object.entries(history).filter(([ref]) => !ref.endsWith(".message.genesis.narrator"));
887
+ const chosen = ordered.slice(-recent);
888
+ if (chosen.length === 0) {
889
+ selected.push({
890
+ key: "history:empty",
891
+ source: "slot:history:empty",
892
+ markdown: locale === "zh-CN"
893
+ ? "# 最近已提交对话\n\n(空:当前世界没有更早的玩家原文或主持叙事;无需为寻找上一条记录调用历史检索工具。)"
894
+ : "# Recent committed conversation\n\n(Empty: this world has no earlier player input or host narrative. Do not call history tools merely to look for a previous message.)",
895
+ });
896
+ coverage.push({
897
+ slot: "history",
898
+ source: locale === "zh-CN" ? `最近 ${recent} 条` : `most recent ${recent}`,
899
+ status: "resolved",
900
+ complete: true,
901
+ continuation: null,
902
+ });
903
+ return;
904
+ }
905
+ for (const [index, [ref, text]] of chosen.entries()) {
906
+ const key = `history_message:${ref}`;
907
+ if (overlappingSelection(selected, key) !== null)
908
+ continue;
909
+ selected.push({
910
+ key,
911
+ source: `slot:history:${ref}`,
912
+ markdown: renderHistoryMessage(ref, text, locale),
913
+ });
914
+ coverage.push({
915
+ slot: "history",
916
+ source: locale === "zh-CN"
917
+ ? `${historyMessageLabel(ref, locale)}(最近记录 ${index + 1}/${chosen.length})`
918
+ : `${historyMessageLabel(ref, locale)} (recent ${index + 1}/${chosen.length})`,
919
+ status: "resolved",
920
+ complete: true,
921
+ continuation: "context_list",
922
+ });
923
+ }
924
+ }
925
+ function resolveAdditionalMaterials(materials, snapshot, history, selected, coverage, locale) {
926
+ if (materials.length > 32)
927
+ throw new PromptCompilationError("material_limit_exceeded", "Additional material exceeds 32 items");
928
+ for (const material of materials) {
929
+ // The model picks this list, and it cannot see which documents a context
930
+ // slot already injected verbatim. A duplicate pick adds nothing the prompt
931
+ // is missing, so drop it instead of failing the whole compilation.
932
+ if (material.kind === "document")
933
+ addDocumentSelection(parseDocumentQueryTarget(material.document), "additional_materials", true, snapshot, selected, coverage, true, locale);
934
+ else if (material.kind === "node")
935
+ addNodeSelection(parseDocumentQueryTarget(material.document), material.locator, "additional_materials", true, snapshot, selected, coverage, true, locale);
936
+ else {
937
+ const ref = material.kind === "history_message"
938
+ ? material.message
939
+ : material.commit;
940
+ const matches = Object.entries(history).filter(([key]) => material.kind === "history_message"
941
+ ? key === ref.replace(/^@/u, "")
942
+ : key.startsWith(ref
943
+ .replace(/^@?history-commit-/u, "history-message-")
944
+ .concat("-")));
945
+ if (matches.length === 0)
946
+ throw new PromptCompilationError("required_slot_missing", `Additional history material does not exist: ${ref}`);
947
+ const key = `${material.kind}:${ref}`;
948
+ if (overlappingSelection(selected, key) !== null)
949
+ continue;
950
+ selected.push({
951
+ key,
952
+ source: `slot:additional_materials:${ref}`,
953
+ markdown: matches
954
+ .map(([key, text]) => renderHistoryMessage(key, text, locale))
955
+ .join("\n\n"),
956
+ });
957
+ coverage.push({
958
+ slot: "additional_materials",
959
+ source: matches.length === 1
960
+ ? historyMessageLabel(matches[0][0], locale)
961
+ : locale === "zh-CN"
962
+ ? `已选历史提交(${matches.length} 条)`
963
+ : `selected history commit (${matches.length} messages)`,
964
+ status: "resolved",
965
+ complete: true,
966
+ continuation: "context_list",
967
+ });
968
+ }
969
+ }
970
+ }
971
+ function renderHistoryMessage(ref, text, locale) {
972
+ return `## ${historyMessageLabel(ref, locale)}\n\n${text.trim()}`;
973
+ }
974
+ function historyMessageLabel(ref, locale) {
975
+ if (ref.endsWith(".message.genesis.narrator"))
976
+ return locale === "zh-CN" ? "开场白" : "Opening";
977
+ if (/(?:\.message\.[^.]+(?:\.[0-9]+)?\.|-)(player)(?:-|$)/u.test(ref))
978
+ return locale === "zh-CN" ? "玩家原文" : "Player input";
979
+ if (/(?:\.message\.[^.]+(?:\.[0-9]+)?\.|-)(narrator)(?:-|$)/u.test(ref))
980
+ return locale === "zh-CN" ? "主持叙事" : "Host narrative";
981
+ return locale === "zh-CN" ? "已提交消息" : "Committed message";
982
+ }
983
+ function addNodeSelection(target, locator, slot, required, snapshot, selected, coverage, dedupe = false, locale = defaultAppLocale) {
984
+ let exactLocator = null;
985
+ if (isRecord(locator) && Array.isArray(locator.yaml)) {
986
+ if (locator.yaml.some((segment) => typeof segment !== "string"))
987
+ throw new PromptCompilationError("persistent_locator_invalid", "Persistent YAML locators in world frames and additional material cannot use list indexes");
988
+ exactLocator = { yaml: locator.yaml };
989
+ }
990
+ else if (isRecord(locator) &&
991
+ Array.isArray(locator.markdown) &&
992
+ locator.markdown.every((segment) => typeof segment === "string"))
993
+ exactLocator = { markdown: locator.markdown };
994
+ const result = exactLocator === null
995
+ ? null
996
+ : selectSnapshotNode(snapshot, target.selector, exactLocator);
997
+ if (result === null || result.kind === "error") {
998
+ if (required)
999
+ throw new PromptCompilationError("required_slot_missing", `Logical locator could not be selected from the fixed snapshot: ${target.source}`, result?.kind === "error" ? result.diagnostics : null);
1000
+ coverage.push({
1001
+ slot,
1002
+ source: exactLocator === null
1003
+ ? modelVisibleDocumentSource(target, result?.kind === "error" ? result : undefined, locale)
1004
+ : `${modelVisibleDocumentSource(target, result?.kind === "error" ? result : undefined, locale)} · ${locatorCoverageKey(exactLocator)}`,
1005
+ status: "optional_missing",
1006
+ complete: false,
1007
+ continuation: "context_read",
1008
+ });
1009
+ return;
1010
+ }
1011
+ let pathKey;
1012
+ let markdown;
1013
+ if (result.node.codec === "yaml" && "yaml" in result.node.locator) {
1014
+ pathKey = locatorCoverageKey(result.node.locator);
1015
+ markdown = `## ${result.document.title} [ref: @${result.document.shortRef} · YAML] · ${locale === "zh-CN" ? "节点" : "node"} ${result.node.locator.yaml.join(" / ")}\n\n${renderWorldYamlSource(result.node.value)}`;
1016
+ }
1017
+ else if (result.node.codec === "markdown" &&
1018
+ "markdown" in result.node.locator) {
1019
+ pathKey = locatorCoverageKey(result.node.locator);
1020
+ markdown = `## ${result.document.title} [ref: @${result.document.shortRef} · Markdown] · ${locale === "zh-CN" ? "节点" : "node"} ${result.node.locator.markdown.join(" / ")}\n\n${result.node.markdown.trim()}`;
1021
+ }
1022
+ else {
1023
+ throw new PromptCompilationError("world_document_query_failed", "Exact-node result codec does not match its locator");
1024
+ }
1025
+ const key = `node:${result.document.documentId}:${pathKey}`;
1026
+ if (dedupe && overlappingSelection(selected, key) !== null)
1027
+ return;
1028
+ selected.push({
1029
+ key,
1030
+ source: `slot:${slot}:@${result.document.shortRef}:${pathKey}`,
1031
+ markdown,
1032
+ });
1033
+ coverage.push({
1034
+ slot,
1035
+ source: `@${result.document.shortRef} · ${pathKey}`,
1036
+ status: "resolved",
1037
+ complete: true,
1038
+ continuation: "context_read",
1039
+ readAuthorization: {
1040
+ shortRef: result.document.shortRef,
1041
+ locator: result.node.locator,
1042
+ },
1043
+ });
1044
+ }
1045
+ /** Expand an author allowlist through the Runtime-owned registry. */
1046
+ export function fileNativeToolsForNames(names, locale = defaultAppLocale) {
1047
+ return runtimeToolsForNames(names, locale);
1048
+ }
1049
+ function keysOverlap(a, b) {
1050
+ const aNode = parseNodeKey(a);
1051
+ const bNode = parseNodeKey(b);
1052
+ return (a === b ||
1053
+ (a.startsWith("document:") && b.startsWith(`node:${a.slice(9)}:`)) ||
1054
+ (b.startsWith("document:") && a.startsWith(`node:${b.slice(9)}:`)) ||
1055
+ (aNode !== null &&
1056
+ bNode !== null &&
1057
+ aNode.document === bNode.document &&
1058
+ aNode.codec === bNode.codec &&
1059
+ (isPathPrefix(aNode.path, bNode.path) ||
1060
+ isPathPrefix(bNode.path, aNode.path))));
1061
+ }
1062
+ /** Which already-selected material covers `key`, or null when nothing does. */
1063
+ function overlappingSelection(selected, key) {
1064
+ return selected.find((material) => keysOverlap(material.key, key)) ?? null;
1065
+ }
1066
+ function assertNoOverlap(selected) {
1067
+ for (let index = 0; index < selected.length; index += 1) {
1068
+ for (let other = index + 1; other < selected.length; other += 1) {
1069
+ const a = selected[index];
1070
+ const b = selected[other];
1071
+ if (keysOverlap(a.key, b.key))
1072
+ throw new PromptCompilationError("material_overlap", `Material overlap: ${a.source} and ${b.source}`);
1073
+ }
1074
+ }
1075
+ }
1076
+ function parseNodeKey(key) {
1077
+ const match = /^node:(.+?):(yaml|markdown):(.*)$/u.exec(key);
1078
+ return match === null
1079
+ ? null
1080
+ : {
1081
+ document: match[1],
1082
+ codec: match[2],
1083
+ path: match[3].split("/"),
1084
+ };
1085
+ }
1086
+ function isPathPrefix(prefix, path) {
1087
+ return (prefix.length <= path.length &&
1088
+ prefix.every((segment, index) => segment === path[index]));
1089
+ }
1090
+ function readWorldInstructions(files, frame) {
1091
+ if (!Array.isArray(frame.instructions))
1092
+ throw new PromptCompilationError("world_frame_invalid", "world frame.instructions must be an array");
1093
+ return frame.instructions.map((entry) => {
1094
+ if (!isRecord(entry) || typeof entry.markdown !== "string")
1095
+ throw new PromptCompilationError("world_frame_invalid", "A world instruction must reference a Markdown block");
1096
+ const path = `control/${entry.markdown}`;
1097
+ return {
1098
+ source: `world:${path}`,
1099
+ markdown: readMarkdown(files, path, "world prompt block"),
1100
+ };
1101
+ });
1102
+ }
1103
+ function compileHostRoles(files, frame, worldInstructions, worldContext, coverage, locale) {
1104
+ if (!isRecord(frame.roles))
1105
+ throw new PromptCompilationError("host_frame_invalid", "host frame.roles must explicitly arrange all three logical roles");
1106
+ const roles = frame.roles;
1107
+ const expectedRoles = [
1108
+ "runtime_system",
1109
+ "author_instruction",
1110
+ "world_context",
1111
+ ];
1112
+ if (Object.keys(frame.roles).length !== expectedRoles.length ||
1113
+ expectedRoles.some((role) => !Array.isArray(roles[role]))) {
1114
+ throw new PromptCompilationError("host_frame_invalid", "host frame.roles must contain only runtime_system, author_instruction, and world_context");
1115
+ }
1116
+ const result = Object.fromEntries(expectedRoles.map((role) => [
1117
+ role,
1118
+ roles[role].flatMap((entry) => {
1119
+ if (!isRecord(entry))
1120
+ throw new PromptCompilationError("host_frame_invalid", `${role} contains an invalid entry`);
1121
+ if (role === "runtime_system" && Object.keys(entry).length === 1) {
1122
+ const builtin = {
1123
+ "runtime.play-contract": runtimeContracts[locale].play,
1124
+ "runtime.tool-contract": runtimeContracts[locale].tools,
1125
+ "runtime.operation-contract": runtimeContracts[locale].operation,
1126
+ }[String(entry.builtin)];
1127
+ if (builtin !== undefined)
1128
+ return [
1129
+ {
1130
+ source: `runtime:builtin/${String(entry.builtin)}`,
1131
+ markdown: builtin,
1132
+ },
1133
+ ];
1134
+ }
1135
+ if (role === "world_context" &&
1136
+ entry.builtin === "runtime.coverage" &&
1137
+ Object.keys(entry).length === 1)
1138
+ return [
1139
+ {
1140
+ source: "runtime:builtin/runtime.coverage",
1141
+ markdown: renderCoverage(coverage, locale),
1142
+ },
1143
+ ];
1144
+ if (role === "author_instruction" &&
1145
+ typeof entry.markdown === "string" &&
1146
+ Object.keys(entry).length === 1) {
1147
+ return [
1148
+ {
1149
+ source: `host:${entry.markdown}`,
1150
+ markdown: readMarkdown(files, entry.markdown, "host preset block"),
1151
+ },
1152
+ ];
1153
+ }
1154
+ if (role === "author_instruction" &&
1155
+ entry.include === "world.instructions" &&
1156
+ Object.keys(entry).length === 1)
1157
+ return worldInstructions;
1158
+ if (role === "world_context" &&
1159
+ entry.include === "world.context" &&
1160
+ Object.keys(entry).length === 1)
1161
+ return worldContext;
1162
+ throw new PromptCompilationError("host_frame_invalid", `${role} contains an unauthorized, unknown, or misplaced arrangement entry`);
1163
+ }),
1164
+ ]));
1165
+ const roleEntries = expectedRoles.flatMap((role) => roles[role]);
1166
+ const sources = Object.values(result).flatMap((blocks) => blocks.map(({ source }) => source));
1167
+ const requiredSources = [
1168
+ "runtime:builtin/runtime.play-contract",
1169
+ "runtime:builtin/runtime.tool-contract",
1170
+ "runtime:builtin/runtime.operation-contract",
1171
+ "runtime:builtin/runtime.coverage",
1172
+ ];
1173
+ if (requiredSources.some((source) => sources.filter((candidate) => candidate === source).length !== 1) ||
1174
+ roleEntries.filter((entry) => entry.include === "world.instructions")
1175
+ .length !== 1 ||
1176
+ roleEntries.filter((entry) => entry.include === "world.context").length !==
1177
+ 1)
1178
+ throw new PromptCompilationError("host_frame_invalid", "The three Runtime contracts, runtime.coverage, and both world includes must each appear exactly once");
1179
+ return result;
1180
+ }
1181
+ function renderCoverage(coverage, locale) {
1182
+ const introduction = locale === "zh-CN"
1183
+ ? `# 材料覆盖报告
1184
+
1185
+ 下列条目说明本次请求注入了哪些材料,以及可以用哪个工具取得更多。\`未完整\` 表示这一项的覆盖没有得到证明,不表示一定还有内容:\`optional_missing\` 的位置可能什么都没注入(目录为空、或内容存在但未被选中),也可能已经注入了一部分(目录里还有无法解析的条目);\`paged_catalog\` 表示确实还有条目没有列出。任何一种都不能用来推断世界上是否存在某件事;需要确认时用条目末尾列出的工具查。
1186
+
1187
+ \`已注入全文\` 表示该文档的完整正文已经出现在下方世界材料里,并且已经具备写入资格:直接对它调用写入工具即可,\`context_read\` 会返回同样的正文。\`已注入节点\` 表示只注入并授权了标出的那个节点,改动该节点之外的位置才需要先读。
1188
+
1189
+ 每份材料的标题都标出了它的 codec:\`· YAML\` 的用 \`{yaml: [键, ...]}\` 定位,正文就是它的 YAML 原文,键名和层级照写即可;\`· Markdown\` 的用 \`{markdown: [标题, ...]}\` 定位,路径是正文里的标题层级。文档 id 一律投影成 \`@短引用\`,写回时照原样保留。`
1190
+ : `# Material coverage report
1191
+
1192
+ 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.
1193
+
1194
+ \`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.
1195
+
1196
+ 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.`;
1197
+ return `${introduction}
1198
+
1199
+ ${coverage
1200
+ .map((entry) => `- ${entry.slot}: ${entry.source} · ${entry.status} · ${entry.complete ? (locale === "zh-CN" ? "完整" : "complete") : locale === "zh-CN" ? "未完整" : "incomplete"}${coverageWriteHint(entry, locale)}`)
1201
+ .join("\n")}`;
1202
+ }
1203
+ /**
1204
+ * A resolved slot already carries the bytes and the write authorization, so
1205
+ * pointing at context_read there reads as an invitation to re-read what the
1206
+ * prompt just supplied. Name the write right instead.
1207
+ */
1208
+ function coverageWriteHint(entry, locale) {
1209
+ const authorization = entry.readAuthorization;
1210
+ if (entry.status === "resolved" &&
1211
+ entry.complete &&
1212
+ authorization !== undefined)
1213
+ return authorization.locator === null
1214
+ ? locale === "zh-CN"
1215
+ ? " · 已注入全文"
1216
+ : " · full text injected"
1217
+ : locale === "zh-CN"
1218
+ ? " · 已注入节点"
1219
+ : " · node injected";
1220
+ return entry.continuation === null
1221
+ ? ""
1222
+ : locale === "zh-CN"
1223
+ ? ` · 可继续 ${entry.continuation}`
1224
+ : ` · continue with ${entry.continuation}`;
1225
+ }
1226
+ function mapProvider(provider, messages) {
1227
+ const byRole = Object.fromEntries(messages.map((message) => [message.role, message.markdown]));
1228
+ const userBlocks = [
1229
+ { type: "text", text: byRole.world_context },
1230
+ ...(typeof byRole.player_input === "string" &&
1231
+ byRole.player_input.length > 0
1232
+ ? [{ type: "text", text: byRole.player_input }]
1233
+ : []),
1234
+ ];
1235
+ if (provider === "anthropic_messages") {
1236
+ return {
1237
+ protocol: provider,
1238
+ system: [
1239
+ {
1240
+ type: "text",
1241
+ text: byRole.runtime_system,
1242
+ cache_control: { type: "ephemeral" },
1243
+ },
1244
+ {
1245
+ type: "text",
1246
+ text: byRole.author_instruction,
1247
+ cache_control: { type: "ephemeral" },
1248
+ },
1249
+ ],
1250
+ messages: [{ role: "user", content: userBlocks }],
1251
+ };
1252
+ }
1253
+ return {
1254
+ protocol: provider,
1255
+ messages: [
1256
+ {
1257
+ role: "system",
1258
+ content: `# Runtime System\n\n${byRole.runtime_system}\n\n# Author Instruction\n\n${byRole.author_instruction}`,
1259
+ },
1260
+ { role: "user", content: userBlocks },
1261
+ ],
1262
+ };
1263
+ }
1264
+ function disabledPromptBudget(input) {
1265
+ return {
1266
+ estimator: "disabled",
1267
+ messageTokens: 0,
1268
+ toolTokens: 0,
1269
+ outputReserveTokens: 0,
1270
+ forcedTailReserveTokens: 0,
1271
+ safetyMarginTokens: 0,
1272
+ requiredTokens: 0,
1273
+ contextWindowTokens: input.modelBinding.contextWindowTokens,
1274
+ status: "not_checked",
1275
+ };
1276
+ }
1277
+ const leakageFields = [
1278
+ "hostPresetId",
1279
+ "materialId",
1280
+ "sourceType",
1281
+ "recordId",
1282
+ "operationId",
1283
+ "version",
1284
+ ];
1285
+ export function scanRuntimeLeakage(messages) {
1286
+ for (const message of messages) {
1287
+ for (const block of message.blocks) {
1288
+ if (!block.source.startsWith("runtime:"))
1289
+ continue;
1290
+ const field = leakageFields.find((candidate) => new RegExp(`\\b${candidate}\\b`, "u").test(block.markdown));
1291
+ if (field !== undefined)
1292
+ throw new PromptCompilationError("internal_field_leakage", `Runtime-generated block leaks internal field: ${field}`);
1293
+ if (/(?:[A-Za-z]:[\\/]|\/(?:home|Users|mnt|tmp|var|workspace|private|opt|root|etc)\/)/u.test(block.markdown))
1294
+ throw new PromptCompilationError("internal_path_leakage", "Runtime-generated block leaks an absolute path");
1295
+ }
1296
+ }
1297
+ }
1298
+ function readYamlRecord(source, label) {
1299
+ if (source === undefined || source.trim() === "")
1300
+ throw new PromptCompilationError("required_slot_missing", `${label} is missing`);
1301
+ if (/(^|\s)[&*!][^\s,\]}]+/mu.test(source) ||
1302
+ /^\s*<<\s*:/mu.test(source) ||
1303
+ /^---\s*$/mu.test(source) ||
1304
+ /^\.\.\.\s*$/mu.test(source))
1305
+ throw new PromptCompilationError("unsafe_yaml", `${label} uses anchors, aliases, tags, merges, or multi-document syntax forbidden by restricted YAML`);
1306
+ const document = parseDocument(source, {
1307
+ schema: "core",
1308
+ uniqueKeys: true,
1309
+ strict: true,
1310
+ });
1311
+ if (document.errors.length > 0 || document.warnings.length > 0)
1312
+ throw new PromptCompilationError("unsafe_yaml", `${label} is not safe restricted YAML`, [...document.errors, ...document.warnings].map(({ message }) => message));
1313
+ const value = document.toJS({ maxAliasCount: 0 });
1314
+ if (!isRecord(value))
1315
+ throw new PromptCompilationError("unsafe_yaml", `${label} top level must be a map`);
1316
+ return value;
1317
+ }
1318
+ function requireFormat(record, format, label) {
1319
+ if (record.format !== format)
1320
+ throw new PromptCompilationError("frame_format_invalid", `${label} format must be ${format}`);
1321
+ }
1322
+ function readMarkdown(files, path, label) {
1323
+ const value = files[path];
1324
+ if (value === undefined || value.trim() === "")
1325
+ throw new PromptCompilationError("required_slot_missing", `${label} does not exist: ${path}`);
1326
+ return value.trim();
1327
+ }
1328
+ function joinBlocks(blocks) {
1329
+ return blocks.map(({ markdown }) => markdown.trim()).join("\n\n");
1330
+ }
1331
+ function utf8Tokens(value) {
1332
+ return new TextEncoder().encode(value).byteLength;
1333
+ }
1334
+ function stableCacheBoundary(stableText, toolUniverse, toolStrategy) {
1335
+ const stableTools = JSON.stringify({ toolUniverse, toolStrategy });
1336
+ const stablePrefix = JSON.stringify({
1337
+ stableText,
1338
+ toolUniverse,
1339
+ toolStrategy,
1340
+ });
1341
+ const stableBytes = utf8Tokens(stableText) + utf8Tokens(stableTools);
1342
+ return {
1343
+ stablePrefixFingerprint: `sha256:${sha256(stablePrefix)}`,
1344
+ estimatedCacheableTokens: stableBytes,
1345
+ firstDynamicByte: stableBytes,
1346
+ };
1347
+ }
1348
+ // Small synchronous SHA-256 keeps the compiler usable by the browser preview and
1349
+ // the Node sender without introducing two cache-key implementations.
1350
+ function sha256(value) {
1351
+ const bytes = new TextEncoder().encode(value);
1352
+ const words = [];
1353
+ for (let index = 0; index < bytes.length; index += 1) {
1354
+ words[index >> 2] =
1355
+ (words[index >> 2] ?? 0) | (bytes[index] << (24 - (index % 4) * 8));
1356
+ }
1357
+ const bitLength = bytes.length * 8;
1358
+ words[bitLength >> 5] =
1359
+ (words[bitLength >> 5] ?? 0) | (0x80 << (24 - (bitLength % 32)));
1360
+ words[(((bitLength + 64) >> 9) << 4) + 15] = bitLength;
1361
+ const constants = [
1362
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
1363
+ 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
1364
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
1365
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
1366
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
1367
+ 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
1368
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
1369
+ 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
1370
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
1371
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
1372
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
1373
+ ];
1374
+ const hash = [
1375
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
1376
+ 0x1f83d9ab, 0x5be0cd19,
1377
+ ];
1378
+ const rotate = (word, amount) => (word >>> amount) | (word << (32 - amount));
1379
+ for (let offset = 0; offset < words.length; offset += 16) {
1380
+ const schedule = new Array(64);
1381
+ for (let index = 0; index < 64; index += 1) {
1382
+ if (index < 16)
1383
+ schedule[index] = words[offset + index] ?? 0;
1384
+ else {
1385
+ const first = schedule[index - 15];
1386
+ const second = schedule[index - 2];
1387
+ const sigma0 = rotate(first, 7) ^ rotate(first, 18) ^ (first >>> 3);
1388
+ const sigma1 = rotate(second, 17) ^ rotate(second, 19) ^ (second >>> 10);
1389
+ schedule[index] =
1390
+ (schedule[index - 16] + sigma0 + schedule[index - 7] + sigma1) | 0;
1391
+ }
1392
+ }
1393
+ let [a, b, c, d, e, f, g, h] = hash;
1394
+ for (let index = 0; index < 64; index += 1) {
1395
+ const sum1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25);
1396
+ const choice = (e & f) ^ (~e & g);
1397
+ const temp1 = (h + sum1 + choice + constants[index] + schedule[index]) | 0;
1398
+ const sum0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22);
1399
+ const majority = (a & b) ^ (a & c) ^ (b & c);
1400
+ const temp2 = (sum0 + majority) | 0;
1401
+ [a, b, c, d, e, f, g, h] = [
1402
+ (temp1 + temp2) | 0,
1403
+ a,
1404
+ b,
1405
+ c,
1406
+ (d + temp1) | 0,
1407
+ e,
1408
+ f,
1409
+ g,
1410
+ ];
1411
+ }
1412
+ [a, b, c, d, e, f, g, h].forEach((value, index) => {
1413
+ hash[index] = (hash[index] + value) | 0;
1414
+ });
1415
+ }
1416
+ return hash
1417
+ .map((word) => (word >>> 0).toString(16).padStart(8, "0"))
1418
+ .join("");
1419
+ }
1420
+ function isRecord(value) {
1421
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1422
+ }
1423
+ function stringOrEmpty(value) {
1424
+ return typeof value === "string" ? value : "";
1425
+ }