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
@@ -0,0 +1,1339 @@
1
+ import { isPackageFollowupControlPath } from "../content/PackageFollowups.js";
2
+ import { renderWorldMaintenanceReport } from "../prompt/WorldMaintenanceReport.js";
3
+ import { listWorldDocuments as listQueryableWorldDocuments } from "../prompt/WorldMaintenanceReport.js";
4
+ import { defaultAppLocale } from "../../protocol/appPreferences.js";
5
+ import { isScalar, isSeq, parseDocument, stringify } from "yaml";
6
+ import { inspectContentPackageCurrentTree } from "../content/FileNativeContentTree.js";
7
+ import { worldMaterialCoverage } from "../prompt/WorldMaterialCoverage.js";
8
+ import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
9
+ export const settingImprovementToolNames = [
10
+ "setting_list",
11
+ "setting_search",
12
+ "setting_read",
13
+ "setting_create",
14
+ "setting_write_file",
15
+ "setting_patch",
16
+ "setting_move",
17
+ "setting_delete",
18
+ ];
19
+ const frameExample = `format: narraeon.world-frame/v1
20
+ bindings:
21
+ currentSituation: "@current-situation"
22
+ instructions:
23
+ - markdown: blocks/world.md
24
+ context:
25
+ - slot: { kind: catalog, directory: characters, maxEntries: 24 }
26
+ - slot: { kind: document, document: "@cultivation" }
27
+ - slot: { kind: node, document: "@alex", locator: { yaml: [relationships] } }
28
+ - slot:
29
+ kind: reference_targets
30
+ from: { document: "@current-situation", locator: { yaml: [characters] } }
31
+ maxEntries: 8
32
+ - slot: { kind: current_situation }
33
+ - slot: { kind: history, recent: 2 }
34
+ - slot: { kind: additional_materials }
35
+ `;
36
+ const emptyPlayerViewsExample = `format: narraeon.player-views/v1
37
+ views: []
38
+ `;
39
+ const playerViewsExample = `format: narraeon.player-views/v1
40
+ views:
41
+ - id: player-status
42
+ title: Current status
43
+ items:
44
+ - id: alex-clothes
45
+ label: Alex's clothing
46
+ select:
47
+ document: "@alex"
48
+ locator:
49
+ yaml: [clothing]
50
+ - id: gold-core-rule
51
+ label: Gold Core
52
+ select:
53
+ document: "@cultivation"
54
+ locator:
55
+ markdown: [Gold Core]
56
+ `;
57
+ /** Control-file examples from the author contract, validated by tests. */
58
+ export const settingAuthorContractExamples = {
59
+ frame: frameExample,
60
+ emptyPlayerViews: emptyPlayerViewsExample,
61
+ playerViews: playerViewsExample,
62
+ };
63
+ export function settingImprovementRuntimeContract(locale) {
64
+ return locale === "zh-CN"
65
+ ? `# Runtime 设定完善对话契约
66
+
67
+ - 每条用户消息都追加到所选设定完善对话。默认沿该对话的 Provider 上下文继续;用户显式选择“全新上下文”时,Runtime 才从内容包当前树编译一段新对话。根据用户当前要求决定直接回复、提出问题、读取当前树或修改当前树。
68
+ - 当前请求随附八个读写工具。不调用工具的完整响应会作为普通助手消息显示,并结束本次用户调用。含有工具调用的响应是内部工具步骤;收到全部工具结果后继续处理,最终用一个不调用工具的完整响应答复用户。
69
+ - 工具写入直接修改内容包当前树。每个完整 Provider 工具响应中的调用按顺序执行,成功改动在该响应结算时原子发布,发布后立即成为内容包权威,无需另一步确认。
70
+ - 每个写工具调用独立结算,并按本响应中的调用顺序读取之前已接受的结果。某个调用失败只在对应工具结果中返回精确原因,不回滚同一响应内其他成功调用,也不阻止后续调用。Runtime 在写调用结算后自动运行内容树检查和真实 Prompt Preview,并把检查结果附在最后一个成功写调用的结果中。
71
+ - 修改既有文件前必须完整读取它。setting_list/setting_search 的 cursor,以及 setting_read 的读取授权,只属于产生它们的当前树 revision;任何其他对话或手动编辑改变当前树后,必须重新读取再写。setting_read 总是完整返回精确文件,不截断、不分页。工具只暴露逻辑路径,不暴露宿主路径。
72
+ - world/ 下只写 .yaml 或 .md 世界文档;专用文件只允许 opening.md、control/frame.yaml、control/player-views.yaml 和 control/blocks/*.md,以及 control/followups.yaml 和其中引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。人物、地点、规则与当前情境放在 world/,本世界特有的主持要求放在 control/。
73
+ - opening.md 是玩家看见的第一页,不得替玩家决定行动、台词或内心。会继续约束首次行动的事实也必须写入自然承载它的世界文档。
74
+ - 新建 world 文档使用 setting_create:ref 由你提供,并同时提供路径、标题、摘要、别名与不含技术头的正文。ref 必须是唯一的 2~32 位小写 ASCII 短句柄;Runtime 自行完成技术存储。既有文档选择值会在读取时自动投影为对应的 @ref;跨文档引用和 control 中的文档选择只使用 @ref。
75
+ - YAML 中指向整份文档的机械引用只写成单键 map,例如 { $ref: "@alex" };普通字符串中的 @alex 仍只是文字。frame 与 player-view selector 的 document 值也写成 "@alex",局部位置另用 locator 表达。
76
+ - YAML locator 可混合 map key 与从 0 开始的数组下标。用 remove 删除一个既有节点;用 append 追加数组项;不要为了删一个键或改一条数组项而整份重写文档。
77
+ - 删除不再需要的 world 文档用 setting_delete。Runtime 仅在它不是 currentSituation、没有被 frame 或 player-view 选择、也没有被其他文档以 $ref 指向时才接受,并精确返回所有阻挡位置。
78
+ - 任何准备持久化的字符串中若仍包含字面的 \\uXXXX,说明工具参数被重复转义;该调用会被拒绝。请重新发送真实 Unicode 字符,不要要求 Runtime 猜测解码。
79
+
80
+ ## 内容包在游玩中的生命周期
81
+
82
+ - 编辑对象是用于创建世界的内容包当前树;成功工具写入会直接替换这份当前树。每个已经创建的世界持有创建时复制的状态、控制与历史,并从此独立演化。
83
+ - 创建世界时,world/* 逐份成为可持续修改的 state/*,control/* 成为世界控制,opening.md 逐字成为第一条已提交主持叙事。
84
+ - opening.md 的正文只在创建时作为 genesis 提交。全新游玩上下文由世界控制与当前世界状态编译,并追加本次玩家输入;会继续约束首次行动的事实同时写入自然承载它的 world 文档。
85
+
86
+ ## 游玩怎样读取并使用设定
87
+
88
+ - control/frame.yaml 的 instructions 把世界专属提示块作为作者指令;context 只按声明顺序执行确定性材料选择。
89
+ - 可在 frame.maintenance.documents 中用 @短引用声明正文建议 UTF-8 字节数;slot 可声明唯一稳定 id 和 advisoryBytes。超限只提示,不截断或拒绝写入。frame.maintenance.clock 可明确绑定 {document: "@ref", locator: {yaml: [时间]}};只记录该次写入时的标量或 Markdown 文本,不推断时钟。
90
+ - current_situation、document 和 reference_targets 选择的整份正文会直接注入;node 只注入精确节点。catalog 只注入该目录直接子文档的 title、summary 与 @短引用,不注入正文;history 与 additional_materials 也只按精确声明注入。
91
+ - frame 声明的 catalog 目录即使当前没有文档,也会作为空状态目录由 state_list 返回,并可作为 world_create 目标;目录声明不是另一份持久状态。
92
+ - 其余世界文档由游玩 AI 按需发现:用 state_list 浏览 Runtime 返回的目录句柄,用 context_search 做原文字面搜索,再用 context_read 精确读取。可发现路径由目录、字面搜索和精确读取组成;字面 0 命中不证明事实不存在。
93
+ - control/followups.yaml 可用 narraeon.package-followups/v1 声明 followups 有序数组;每项包含稳定 id、displayName、enabled、mount、prompt(role: author_instruction、markdown 相对 control/)、artifacts 和 maxArtifactBytes。启停保留定义与资源;它们在主叙事提交后独立生成界面产物,不写世界。脚本授权是用户本地设置,文件不能自授信任。
94
+ - control/player-views.yaml 用精确 selector 投影当前原值。它只负责展示;权限、秘密、人物认知与条件显示由世界语义另行表达。
95
+
96
+ ## 游玩怎样更新设定
97
+
98
+ - 游玩 AI 的世界写入范围是 state/*:在正文已经注入或完整读取后用 world_patch 细粒度增改或删除节点,也可用 world_create 创建新文档;对象退出常驻 catalog 时用 world_retire 退役而不销毁,之后仍可读取并恢复。opening.md 的创作发生在内容包阶段;control/* 的调整属于世界外控制编辑。
99
+ - Runtime 负责提交持续状态与叙事。需要跨下一次行动保持的结果写回自然所有者;只约束眼前、没有单一所有者的局面写入当前情境;无需作为当前状态持续保存的细节留在已提交叙事。
100
+ - 机械检查通过不代表内容在游玩中容易发现。新增或重组重要信息时,必须同时决定自然所有者、初始注入或发现路径、未来更新位置和玩家显示方式。`
101
+ : `# Runtime setting-improvement conversation contract
102
+
103
+ - Every user message is appended to the selected setting-improvement conversation. Continue its Provider context by default; only an explicit Fresh context choice compiles a new conversation from the content package's current tree. Decide whether to reply, ask, read the current tree, or change it from the user's current request.
104
+ - The current request includes eight read/write tools. A complete response with no tool calls is shown as an ordinary assistant message and settles this user invocation. A response containing tool calls is an internal tool step; continue after all results and eventually answer with a complete tool-free response.
105
+ - Tool writes directly modify the content package's current tree. Calls in one complete Provider tool response execute in order; its successful changes publish atomically when that response settles, immediately become authoritative, and require no second confirmation.
106
+ - Every write tool call settles independently and sees earlier accepted calls in response order. A failure returns its precise cause only in that call's result, does not roll back successful siblings, and does not stop later calls. Runtime automatically runs content-tree checks and a real Prompt Preview after writes and appends that review to the last successful write result.
107
+ - Read an existing file completely before changing it. Cursors from setting_list and setting_search, and read authorization from setting_read, belong only to the current-tree revision that produced them. setting_read always returns the complete exact file without truncation or pagination. Re-read after another conversation or a manual edit changes the current tree. Tools expose logical paths, never host paths.
108
+ - World documents are .yaml or .md files under world/. Special writes are limited to opening.md, control/frame.yaml, control/player-views.yaml, and control/blocks/*.md, plus control/followups.yaml and its referenced prompts/*.md, renderers/*.html, scripts/*.js, regex/*.yaml and assets/* text resources. Put characters, places, rules, and the current situation under world/, and world-specific hosting guidance under control/.
109
+ - opening.md is the first page shown to the player. Never decide the player's action, dialogue, or inner thoughts. Facts that constrain the first action must also live in the world document that naturally owns them.
110
+ - Create a world document with setting_create. You provide its unique 2-to-32-character lowercase ASCII ref, path, title, summary, aliases, and body without a technical header; Runtime completes the technical storage. Existing document selectors are automatically projected to their @refs when read. Cross-document references and control selectors use only @refs.
111
+ - A mechanical YAML reference to one whole document is a one-key map such as { $ref: "@alex" }; @alex inside an ordinary string is still only text. The document value in frame and player-view selectors is likewise "@alex", with any local position expressed separately by a locator.
112
+ - A YAML locator may mix map keys and zero-based array indexes. Use remove for one existing node and append for a new array item; never rewrite a whole document merely to delete one key or change one array item.
113
+ - Delete an obsolete world document with setting_delete. Runtime accepts it only when it is not currentSituation, no frame or player-view selects it, and no other document points to it with $ref; every blocking location is returned precisely.
114
+ - If any string to be persisted still contains a literal \\uXXXX sequence, its tool arguments were double-escaped and that call is rejected. Resend real Unicode characters; Runtime will not guess-decode them.
115
+
116
+ ## Content-package lifecycle during play
117
+
118
+ - The editing target is the current tree of a content-package template used to create worlds. Successful tool writes replace that current tree directly. Each world already created from it retains the state, control, and history copied at creation and evolves independently from then on.
119
+ - When a world is created, world/* becomes independently mutable state/*, control/* becomes world control, and opening.md is committed verbatim as the first host narrative.
120
+ - The opening.md body is committed only as genesis during world creation. A fresh play context is compiled from world control and current world state, then appends the current player input. Every fact that still constrains the first action also lives in the world document that naturally owns it.
121
+
122
+ ## How play reads and uses the setting
123
+
124
+ - control/frame.yaml instructions insert world-specific prompt blocks as author instructions. Its context entries perform deterministic material selections in their declared order.
125
+ - Optional frame.maintenance.documents maps @short-refs to advisory body UTF-8 byte counts; slots can declare a unique stable id and advisoryBytes. Excess size only produces advice. Optional frame.maintenance.clock binds {document: "@ref", locator: {yaml: [time]}} to record scalar or Markdown world time at actual writes, without inferring a clock.
126
+ - current_situation, document, and reference_targets selections inject whole bodies; node injects only one exact node. A catalog injects only title, summary, and @short-ref for direct child documents, never their bodies. History and additional materials are likewise exact selections.
127
+ - A catalog directory declared by the frame remains available through state_list as an empty state directory and as a world_create destination even when it currently contains no documents. The declaration is not a second piece of persistent state.
128
+ - Play AI discovers the remaining world documents on demand: state_list browses Runtime-provided directory handles, context_search searches literal source text, and context_read reads an exact handle. Discovery consists of directory browsing, literal search, and exact reads; zero literal matches do not prove that a fact is absent.
129
+ - control/player-views.yaml projects current values through exact selectors. Its responsibility is presentation; permissions, secrecy, character knowledge, and conditional visibility are expressed separately through world semantics.
130
+
131
+ ## How play updates the setting
132
+
133
+ - Play AI writes only within state/*: after a document body is injected or completely read, world_patch adds, changes, or removes exact nodes and world_create creates a new document. Use world_retire when an entity leaves the active catalog without destroying it; the document remains readable and restorable. opening.md is authored at the content-package stage; control/* changes belong to world-external control editing.
134
+ - Runtime commits durable state and narrative. Write results that must survive the next action to their natural owner; put short-lived cross-object situations with no single owner in the current situation; leave details that do not need to remain current in committed narrative.
135
+ - Passing mechanical checks does not make content discoverable during play. Whenever important information is created or reorganized, decide its natural owner, initial injection or discovery path, future update location, and player-visible projection together.`;
136
+ }
137
+ export function settingImprovementToolDefinitions(locale = defaultAppLocale) {
138
+ const descriptions = locale === "zh-CN" ? toolDescriptionsZhCN : toolDescriptionsEn;
139
+ const schema = (properties, required) => ({
140
+ type: "object",
141
+ additionalProperties: false,
142
+ properties,
143
+ required,
144
+ });
145
+ const text = { type: "string", minLength: 1 };
146
+ const path = { type: "string", minLength: 1 };
147
+ const cursor = { type: "string", minLength: 1 };
148
+ const limit = { type: "integer", minimum: 1, maximum: 100 };
149
+ return [
150
+ {
151
+ name: "setting_list",
152
+ description: descriptions.setting_list,
153
+ inputSchema: schema({
154
+ directory: { type: "string", pattern: "^world(?:/[^\\\\]+)?$" },
155
+ limit,
156
+ cursor,
157
+ }, []),
158
+ },
159
+ {
160
+ name: "setting_search",
161
+ description: descriptions.setting_search,
162
+ inputSchema: schema({
163
+ query: { type: "string", minLength: 1, maxLength: 256 },
164
+ within: path,
165
+ caseSensitive: { type: "boolean" },
166
+ limit,
167
+ cursor,
168
+ }, ["query"]),
169
+ },
170
+ {
171
+ name: "setting_read",
172
+ description: descriptions.setting_read,
173
+ inputSchema: schema({ path }, ["path"]),
174
+ },
175
+ {
176
+ name: "setting_create",
177
+ description: descriptions.setting_create,
178
+ inputSchema: schema({
179
+ path: {
180
+ type: "string",
181
+ pattern: "^world/.+\\.(?:ya?ml|md)$",
182
+ },
183
+ ref: { type: "string", pattern: "^[a-z][a-z0-9-]{1,31}$" },
184
+ title: { type: "string", minLength: 1, maxLength: 120 },
185
+ summary: { type: "string", minLength: 1, maxLength: 240 },
186
+ aliases: {
187
+ type: "array",
188
+ maxItems: 16,
189
+ items: { type: "string", minLength: 1, maxLength: 64 },
190
+ },
191
+ body: { type: "string", minLength: 1 },
192
+ }, ["path", "ref", "title", "summary", "aliases", "body"]),
193
+ },
194
+ {
195
+ name: "setting_write_file",
196
+ description: descriptions.setting_write_file,
197
+ inputSchema: schema({
198
+ path,
199
+ contents: text,
200
+ ref: { type: "string", pattern: "^[a-z][a-z0-9-]{1,31}$" },
201
+ title: { type: "string", minLength: 1, maxLength: 120 },
202
+ summary: { type: "string", minLength: 1, maxLength: 240 },
203
+ aliases: {
204
+ type: "array",
205
+ maxItems: 16,
206
+ items: { type: "string", minLength: 1, maxLength: 64 },
207
+ },
208
+ }, ["path", "contents"]),
209
+ },
210
+ {
211
+ name: "setting_patch",
212
+ description: descriptions.setting_patch,
213
+ inputSchema: schema({
214
+ document: text,
215
+ op: {
216
+ type: "string",
217
+ enum: ["add", "replace", "append", "remove", "set_metadata"],
218
+ },
219
+ locator: {
220
+ type: "array",
221
+ minItems: 1,
222
+ items: { type: ["string", "integer"] },
223
+ },
224
+ value: {},
225
+ title: { type: "string", minLength: 1, maxLength: 120 },
226
+ summary: { type: "string", minLength: 1, maxLength: 240 },
227
+ aliases: {
228
+ type: "array",
229
+ maxItems: 16,
230
+ items: { type: "string", minLength: 1, maxLength: 64 },
231
+ },
232
+ }, ["document", "op"]),
233
+ },
234
+ {
235
+ name: "setting_move",
236
+ description: descriptions.setting_move,
237
+ inputSchema: schema({ from: text, to: path }, ["from", "to"]),
238
+ },
239
+ {
240
+ name: "setting_delete",
241
+ description: descriptions.setting_delete,
242
+ inputSchema: schema({ document: text }, ["document"]),
243
+ },
244
+ ];
245
+ }
246
+ const toolDescriptionsEn = {
247
+ setting_list: "List the world root or a world/ subdirectory from the content package's current tree. The root also lists opening and control files. A cursor is valid only for the same query and current-tree revision.",
248
+ setting_search: "Search literal source text in world documents in the content package's current tree. within may be world, a world/ directory or path, or @ref.",
249
+ setting_read: "Read a complete world document by world/ path or @ref, or read a complete opening/control file by exact path. Existing document selectors are projected to @refs. The result is never truncated or paginated.",
250
+ setting_create: "Create one world/ .yaml or .md document. Supply a unique lowercase ASCII ref, title, summary, aliases, and body without a technical header; Runtime completes the storage envelope. The created document is already fully read.",
251
+ setting_write_file: "Replace a completely read world/ .yaml or .md document body while preserving its identity and metadata, or create/replace opening.md, control/frame.yaml, control/player-views.yaml, or control/blocks/*.md, plus control/followups.yaml and its referenced prompts/*.md, renderers/*.html, scripts/*.js, regex/*.yaml and assets/* text resources. To repair a damaged world document, also provide ref, title, summary, and aliases; Runtime preserves any recoverable storage identity. Create new world documents with setting_create.",
252
+ setting_patch: 'Update a completely read YAML world document without rewriting unrelated content. locator is a non-empty path whose segments are map keys or zero-based array indexes. Use add for a missing map key or array index, replace for an existing node, append for the end of an existing array, and remove for an existing map key or array item. When title, summary, or aliases are stale, update all three together with op "set_metadata". Rewrite Markdown bodies with setting_write_file.',
253
+ setting_move: "Move a completely read world document to a new world/ .yaml or .md logical path while preserving its identity and contents.",
254
+ setting_delete: "Permanently delete a completely read world document from the content-package current tree. Runtime rejects deletion while currentSituation, a frame slot, a player-view selector, or another document's $ref still points to it, and reports every blocking location. Delete or redirect those references first.",
255
+ };
256
+ const toolDescriptionsZhCN = {
257
+ setting_list: "列出内容包当前树的 world 根目录或 world/ 子目录;根目录同时列出 opening 和 control 专用文件。cursor 只对同一查询和当前树 revision 有效。",
258
+ setting_search: "在内容包当前树的 world 文档中按原文字面搜索;within 可为 world、world/ 目录或路径或 @ref。",
259
+ setting_read: "按 world/ 路径或 @ref 完整读取世界文档,或按精确路径完整读取 opening/control;既有文档选择值自动投影为 @ref。结果不会截断或分页。",
260
+ setting_create: "创建一份 world/ 下的 .yaml 或 .md 文档;提供唯一的小写 ASCII ref、标题、摘要、别名和不含技术头的正文,Runtime 自行完成存储封装。创建成功的文档视为已经完整读取。",
261
+ setting_write_file: "整份替换已完整读取的 world/ .yaml 或 .md 正文并保留身份和元信息,或创建/替换 opening.md、control/frame.yaml、control/player-views.yaml、control/blocks/*.md,以及 control/followups.yaml 和其中引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。修复损坏世界文档时同时提供 ref、title、summary、aliases,Runtime 会保留仍可恢复的存储身份。新建世界文档使用 setting_create。",
262
+ setting_patch: '细粒度更新已完整读取的 YAML 世界文档,不重写无关内容。locator 是非空路径,每段可以是 map key 或从 0 开始的数组下标;add 新建 map key 或数组位置,replace 更新既有节点,append 追加既有数组,remove 删除既有 map key 或数组项。title、summary 或 aliases 过时时用 op "set_metadata" 整组更新三项。Markdown 正文用 setting_write_file 整份重写。',
263
+ setting_move: "把已完整读取的世界文档移动到新的 world/ .yaml 或 .md 逻辑路径,并保留身份与内容。",
264
+ setting_delete: "从内容包当前树永久删除一份已完整读取的 world 文档。若 currentSituation、frame slot、player-view selector 或其他文档的 $ref 仍指向它,Runtime 会拒绝并返回全部精确阻挡位置;请先删除或改向这些引用。",
265
+ };
266
+ export class SettingAuthoringTransaction {
267
+ #baseFiles;
268
+ #locale;
269
+ #preview;
270
+ #validateFiles;
271
+ #snapshot;
272
+ #reads;
273
+ #review;
274
+ constructor(input) {
275
+ this.#baseFiles = cloneFiles(input.baseFiles);
276
+ this.#locale = input.locale ?? defaultAppLocale;
277
+ this.#preview = input.preview;
278
+ this.#validateFiles = input.validateFiles ?? (() => undefined);
279
+ this.#snapshot = WorldDocumentStore.open({
280
+ layout: "content_package",
281
+ files: cloneFiles(input.baseFiles),
282
+ });
283
+ const authorization = input.authorization?.revision === (input.revision ?? this.#snapshot.id)
284
+ ? input.authorization
285
+ : null;
286
+ this.#reads = {
287
+ snapshotId: this.#snapshot.id,
288
+ worldDocumentIds: new Set(authorization?.readWorldDocumentIds ?? []),
289
+ damagedWorldPaths: new Set(authorization?.readableDamagedWorldPaths ?? []),
290
+ opaquePaths: new Set(authorization?.readOpaquePaths ?? []),
291
+ };
292
+ this.#review = this.#inspect();
293
+ }
294
+ files() {
295
+ return cloneFiles(this.#snapshot.files);
296
+ }
297
+ review() {
298
+ return structuredClone(this.#review);
299
+ }
300
+ authorization(revision) {
301
+ return {
302
+ revision,
303
+ readWorldDocumentIds: [...this.#reads.worldDocumentIds].sort(),
304
+ readableDamagedWorldPaths: [...this.#reads.damagedWorldPaths].sort(),
305
+ readOpaquePaths: [...this.#reads.opaquePaths].sort(),
306
+ };
307
+ }
308
+ execute(calls) {
309
+ const normalized = calls.map(normalizeCall);
310
+ const results = [];
311
+ let lastSuccessfulWrite = -1;
312
+ for (const call of normalized) {
313
+ const before = this.files();
314
+ if (isReadTool(call.name)) {
315
+ results.push(toToolResult(call.id, this.#executeRead(call), []));
316
+ continue;
317
+ }
318
+ const result = this.#executeWrite(call);
319
+ results.push(toToolResult(call.id, result, result.ok ? fileDiff(before, this.#snapshot.files) : []));
320
+ if (result.ok)
321
+ lastSuccessfulWrite = results.length - 1;
322
+ }
323
+ if (lastSuccessfulWrite >= 0) {
324
+ this.#review = this.#inspect();
325
+ const settled = results[lastSuccessfulWrite];
326
+ settled.markdown = `${settled.markdown}\n\n${renderAutomaticReview(this.#review, this.#locale)}`;
327
+ }
328
+ return results;
329
+ }
330
+ #executeRead(call) {
331
+ if (call.name === "setting_list")
332
+ return listDocuments(this.#snapshot, call.arguments, this.#locale);
333
+ if (call.name === "setting_search")
334
+ return searchDocuments(this.#snapshot, call.arguments, this.#locale);
335
+ if (call.name === "setting_read")
336
+ return readDocument(this.#snapshot, call.arguments, this.#reads, this.#locale);
337
+ return failure(`Unsupported read tool: ${call.name}`, this.#locale);
338
+ }
339
+ #executeWrite(call) {
340
+ const snapshot = this.#snapshot;
341
+ const reads = cloneReads(this.#reads);
342
+ try {
343
+ if (isWorldWriteCall(call)) {
344
+ if (call.name === "setting_delete") {
345
+ const deleted = deleteWorldDocument(snapshot, call, reads, this.#locale);
346
+ this.#validateFiles(deleted.snapshot.files);
347
+ this.#snapshot = deleted.snapshot;
348
+ this.#reads = reads;
349
+ return success(deleted.markdown);
350
+ }
351
+ const requestedRef = call.name === "setting_create"
352
+ ? requiredSettingRef(call.arguments.ref, this.#locale)
353
+ : null;
354
+ const revised = snapshot.revise({
355
+ commands: [worldRevisionCommand(snapshot, call, this.#locale)],
356
+ });
357
+ if (!revised.ok)
358
+ throw new SettingAuthoringError(renderRevisionFailure(revised.diagnostics, this.#locale));
359
+ if (requestedRef !== null &&
360
+ revised.changes.some(({ shortRef }) => shortRef !== requestedRef))
361
+ throw new SettingAuthoringError(localized(this.#locale, `ref @${requestedRef} already exists; choose a different ref`, `ref @${requestedRef} 已存在;请换一个 ref`));
362
+ assertWorldWritesAuthorized(snapshot.id, reads, revised.changes, this.#locale);
363
+ this.#validateFiles(revised.snapshot.files);
364
+ rebaseReads(reads, revised.snapshot);
365
+ for (const change of revised.changes)
366
+ reads.worldDocumentIds.add(change.documentId);
367
+ this.#snapshot = revised.snapshot;
368
+ this.#reads = reads;
369
+ return success(renderWorldWriteSuccess(revised.changes, this.#locale));
370
+ }
371
+ if (call.name !== "setting_write_file")
372
+ throw new SettingAuthoringError(`Unsupported write tool: ${call.name}`);
373
+ const changed = writeOpaque(snapshot, call, reads, this.#locale);
374
+ this.#validateFiles(changed.snapshot.files);
375
+ this.#snapshot = changed.snapshot;
376
+ this.#reads = reads;
377
+ return success(changed.markdown);
378
+ }
379
+ catch (error) {
380
+ const message = error instanceof Error
381
+ ? error.message
382
+ : "The content-package write failed";
383
+ return failure(message, this.#locale);
384
+ }
385
+ }
386
+ #inspect() {
387
+ const inspection = inspectContentPackageCurrentTree(this.#snapshot.files, {
388
+ worldDocumentSnapshot: this.#snapshot,
389
+ });
390
+ const diff = fileDiff(this.#baseFiles, this.#snapshot.files);
391
+ if (inspection.status !== "usable")
392
+ return {
393
+ status: "needs_repair",
394
+ diff,
395
+ diagnostics: inspection.issues.map(({ code, path, message }) => ({
396
+ code,
397
+ path,
398
+ message: publicSettingDiagnosticMessage(code, message, this.#locale),
399
+ })),
400
+ preview: null,
401
+ playCoverage: null,
402
+ };
403
+ try {
404
+ const preview = structuredClone(this.#preview(this.#snapshot));
405
+ return {
406
+ status: "usable",
407
+ diff,
408
+ diagnostics: [],
409
+ preview,
410
+ playCoverage: buildSettingAuthoringPlayCoverage(this.#snapshot, preview, diff),
411
+ };
412
+ }
413
+ catch (error) {
414
+ return {
415
+ status: "needs_repair",
416
+ diff,
417
+ diagnostics: [
418
+ {
419
+ code: "prompt_preview_failed",
420
+ path: "control/frame.yaml",
421
+ message: error instanceof Error
422
+ ? error.message
423
+ : "Prompt Preview failed for the current content tree",
424
+ },
425
+ ],
426
+ preview: null,
427
+ playCoverage: null,
428
+ };
429
+ }
430
+ }
431
+ }
432
+ function renderAutomaticReview(review, locale) {
433
+ if (review.status === "usable")
434
+ return `${localized(locale, `# Current-tree review passed\n\nContent-tree validation and the real Prompt Preview both passed. This tool response changes ${review.diff.length} file(s) directly in the content package. Use this review and the coverage below to decide whether the current user request is satisfied or further edits are needed.`, `# 当前树自动检查通过\n\n内容树校验和真实 Prompt Preview 均已通过;本次工具响应直接修改内容包中的 ${review.diff.length} 个文件。请根据本次检查与下方覆盖报告判断当前用户要求是否已经满足,或是否还需修改。`)}\n\n${renderPlayCoverage(review.playCoverage, locale)}\n\n${renderWorldMaintenanceReport(review.preview?.compilation.maintenance, locale)}`;
435
+ return [
436
+ localized(locale, "# Current-tree review needs repair", "# 当前树自动检查需要修复"),
437
+ "",
438
+ ...review.diagnostics.map(({ code, path, message }) => `- ${code} · ${path} · ${message}`),
439
+ "",
440
+ localized(locale, "Repair these diagnostics with the ordinary read/write tools. Runtime will run the review again after the next successful write call.", "请用普通读写工具修复这些诊断;下一个写调用成功后 Runtime 会再次自动检查。"),
441
+ ].join("\n");
442
+ }
443
+ function buildSettingAuthoringPlayCoverage(snapshot, preview, diff) {
444
+ const documents = listQueryableWorldDocuments(snapshot);
445
+ const byShortRef = new Map(documents.map((document) => [document.shortRef, document]));
446
+ const { fullInjected, nodeInjected, catalogSummary, injectedSelections } = worldMaterialCoverage(preview.compilation.coverage);
447
+ const referencedFromInjected = new Set();
448
+ for (const selection of injectedSelections) {
449
+ const descriptor = byShortRef.get(selection.shortRef);
450
+ if (descriptor === undefined)
451
+ continue;
452
+ const locator = selection.locator ??
453
+ (descriptor.codec === "yaml" ? { yaml: [] } : null);
454
+ if (locator === null)
455
+ continue;
456
+ const selected = snapshot.query({
457
+ kind: "select_node",
458
+ document: { shortRef: selection.shortRef },
459
+ locator,
460
+ });
461
+ if (selected.kind !== "select_node")
462
+ continue;
463
+ for (const reference of selected.references)
464
+ referencedFromInjected.add(reference.target.shortRef);
465
+ }
466
+ const enabledWorldInstructionPaths = new Set(preview.compilation.logicalMessages
467
+ .flatMap(({ blocks }) => blocks)
468
+ .flatMap(({ source }) => source.startsWith("world:control/blocks/")
469
+ ? [source.slice("world:".length)]
470
+ : []));
471
+ const accessByPath = new Map();
472
+ const totals = {
473
+ fullInjected: 0,
474
+ nodeInjected: 0,
475
+ catalogSummary: 0,
476
+ referencedFromInjected: 0,
477
+ onDemand: 0,
478
+ };
479
+ for (const document of documents) {
480
+ const access = fullInjected.has(document.shortRef)
481
+ ? "full_injected"
482
+ : nodeInjected.has(document.shortRef)
483
+ ? "node_injected"
484
+ : catalogSummary.has(document.shortRef)
485
+ ? "catalog_summary"
486
+ : referencedFromInjected.has(document.shortRef)
487
+ ? "referenced_from_injected"
488
+ : "on_demand";
489
+ accessByPath.set(document.logicalPath, access);
490
+ incrementPlayCoverageTotal(totals, access);
491
+ }
492
+ return {
493
+ totals,
494
+ changed: diff.map(({ path, after }) => {
495
+ const access = changedPathPlayAccess(path, after, accessByPath, enabledWorldInstructionPaths);
496
+ return { path, access, detail: playAccessDetail(access) };
497
+ }),
498
+ };
499
+ }
500
+ function changedPathPlayAccess(path, after, accessByPath, enabledWorldInstructionPaths) {
501
+ if (after === null)
502
+ return "removed";
503
+ if (path === "opening.md")
504
+ return "opening_genesis";
505
+ if (path === "control/player-views.yaml")
506
+ return "player_view";
507
+ if (path.startsWith("control/blocks/"))
508
+ return enabledWorldInstructionPaths.has(path)
509
+ ? "play_control"
510
+ : "unused_control";
511
+ if (path.startsWith("control/"))
512
+ return "play_control";
513
+ return accessByPath.get(path) ?? "on_demand";
514
+ }
515
+ function incrementPlayCoverageTotal(totals, access) {
516
+ if (access === "full_injected")
517
+ totals.fullInjected += 1;
518
+ else if (access === "node_injected")
519
+ totals.nodeInjected += 1;
520
+ else if (access === "catalog_summary")
521
+ totals.catalogSummary += 1;
522
+ else if (access === "referenced_from_injected")
523
+ totals.referencedFromInjected += 1;
524
+ else if (access === "on_demand")
525
+ totals.onDemand += 1;
526
+ }
527
+ function playAccessDetail(access) {
528
+ const details = {
529
+ full_injected: "complete body is injected into every fresh play context",
530
+ node_injected: "only a selected node is injected into fresh play context",
531
+ catalog_summary: "catalog injects title, summary, and short reference only",
532
+ referenced_from_injected: "an injected YAML selection exposes a direct short reference",
533
+ on_demand: "available only through directory, literal search, and exact read",
534
+ opening_genesis: "committed as genesis narrative and excluded from later fresh context",
535
+ play_control: "compiled as immutable play-author control",
536
+ unused_control: "not enabled by control/frame.yaml and unavailable to ordinary play AI",
537
+ player_view: "projected through exact player-view selectors",
538
+ removed: "removed from the content package current tree",
539
+ };
540
+ return details[access];
541
+ }
542
+ function renderPlayCoverage(coverage, locale) {
543
+ if (coverage === null)
544
+ return localized(locale, "# Play-consumption coverage\n\nCoverage is unavailable until the real Prompt Preview succeeds.", "# 游玩读取覆盖\n\n真实 Prompt Preview 通过后才会提供覆盖报告。");
545
+ const labels = locale === "zh-CN"
546
+ ? {
547
+ full_injected: "全文注入",
548
+ node_injected: "仅节点注入",
549
+ catalog_summary: "仅目录摘要",
550
+ referenced_from_injected: "由已注入内容直接引用",
551
+ on_demand: "仅按需发现",
552
+ opening_genesis: "仅作为创世叙事提交",
553
+ play_control: "游玩控制",
554
+ unused_control: "未被 control/frame.yaml 启用",
555
+ player_view: "玩家视图精确投影",
556
+ removed: "已从内容包当前树删除",
557
+ }
558
+ : {
559
+ full_injected: "full text injected",
560
+ node_injected: "selected node only",
561
+ catalog_summary: "catalog summary only",
562
+ referenced_from_injected: "directly referenced from injected content",
563
+ on_demand: "on-demand only",
564
+ opening_genesis: "genesis narrative only",
565
+ play_control: "play-author control",
566
+ unused_control: "not enabled by control/frame.yaml",
567
+ player_view: "exact player-view projection",
568
+ removed: "removed from the content package current tree",
569
+ };
570
+ const maximumReportedChanges = 64;
571
+ const reportedChanges = coverage.changed.slice(0, maximumReportedChanges);
572
+ const changed = reportedChanges.map(({ path, access }) => `- ${path} — ${labels[access]}`);
573
+ const onDemand = coverage.changed.filter(({ access }) => access === "on_demand");
574
+ const unusedControl = coverage.changed.filter(({ access }) => access === "unused_control");
575
+ return [
576
+ localized(locale, "# Play-consumption coverage", "# 游玩读取覆盖"),
577
+ "",
578
+ localized(locale, `World documents: ${coverage.totals.fullInjected} full text injected; ${coverage.totals.nodeInjected} node-only; ${coverage.totals.catalogSummary} catalog-summary-only; ${coverage.totals.referencedFromInjected} directly referenced; ${coverage.totals.onDemand} on-demand-only.`, `世界文档:${coverage.totals.fullInjected} 份全文注入;${coverage.totals.nodeInjected} 份仅节点注入;${coverage.totals.catalogSummary} 份仅目录摘要;${coverage.totals.referencedFromInjected} 份由已注入内容直接引用;${coverage.totals.onDemand} 份仅按需发现。`),
579
+ "",
580
+ ...(changed.length > 0
581
+ ? changed
582
+ : [localized(locale, "- No changed files", "- 没有改动文件")]),
583
+ ...(coverage.changed.length > reportedChanges.length
584
+ ? [
585
+ localized(locale, `- ${coverage.changed.length - reportedChanges.length} additional changed file(s) omitted from this compact report`, `- 此精简报告另省略 ${coverage.changed.length - reportedChanges.length} 个改动文件`),
586
+ ]
587
+ : []),
588
+ ...(onDemand.length > 0
589
+ ? [
590
+ "",
591
+ localized(locale, "Changed world documents marked on-demand only are not present in a fresh play prompt. If play should discover them reliably, consider adding a catalog, injected reference, or world instruction.", "标为仅按需发现的改动世界文档不会出现在全新游玩提示中;如果游玩应可靠发现它们,请考虑增加目录、由已注入内容建立引用,或加入世界指令。"),
592
+ ]
593
+ : []),
594
+ ...(unusedControl.length > 0
595
+ ? [
596
+ "",
597
+ localized(locale, "Changed control blocks not enabled by control/frame.yaml never reach ordinary play AI. Add each intended block to frame.instructions or remove the unused file.", "未被 control/frame.yaml 启用的改动控制块不会进入普通游玩 AI;请把确实需要的块加入 frame.instructions,或删除无用文件。"),
598
+ ]
599
+ : []),
600
+ ].join("\n");
601
+ }
602
+ class SettingAuthoringError extends Error {
603
+ constructor(message) {
604
+ super(message);
605
+ this.name = "SettingAuthoringError";
606
+ }
607
+ }
608
+ function normalizeCall(call) {
609
+ if (!isRecord(call.arguments))
610
+ return { id: call.id, name: call.name, arguments: {} };
611
+ return {
612
+ id: call.id,
613
+ name: call.name,
614
+ arguments: structuredClone(call.arguments),
615
+ };
616
+ }
617
+ function isReadTool(name) {
618
+ return (name === "setting_list" ||
619
+ name === "setting_search" ||
620
+ name === "setting_read");
621
+ }
622
+ function isWorldWriteCall(call) {
623
+ if (call.name === "setting_create" ||
624
+ call.name === "setting_patch" ||
625
+ call.name === "setting_move" ||
626
+ call.name === "setting_delete")
627
+ return true;
628
+ return (call.name === "setting_write_file" &&
629
+ typeof call.arguments.path === "string" &&
630
+ call.arguments.path.startsWith("world/"));
631
+ }
632
+ function worldRevisionCommand(snapshot, call, locale) {
633
+ if (call.name === "setting_create") {
634
+ if (!hasOnly(call.arguments, [
635
+ "path",
636
+ "ref",
637
+ "title",
638
+ "summary",
639
+ "aliases",
640
+ "body",
641
+ ]))
642
+ throw new SettingAuthoringError("setting_create accepts only path, ref, title, summary, aliases, and body");
643
+ const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), locale);
644
+ const ref = requiredSettingRef(call.arguments.ref, locale);
645
+ const title = requiredString(call.arguments.title, "title");
646
+ const summary = requiredString(call.arguments.summary, "summary");
647
+ const aliases = requiredAliases(call.arguments.aliases);
648
+ const body = requiredString(call.arguments.body, "body");
649
+ assertNoLiteralUnicodeEscapes({ ref, title, summary, aliases, body }, locale);
650
+ return {
651
+ kind: "create",
652
+ temporaryName: "setting-document",
653
+ logicalPath,
654
+ codec: logicalPath.endsWith(".md") ? "markdown" : "yaml",
655
+ refHint: ref,
656
+ title,
657
+ summary,
658
+ aliases,
659
+ body,
660
+ };
661
+ }
662
+ if (call.name === "setting_write_file") {
663
+ const logicalPath = safeWorldDocumentPath(requiredString(call.arguments.path, "path"), locale);
664
+ const contents = worldWriteContents(snapshot, logicalPath, call.arguments, locale);
665
+ assertNoLiteralUnicodeEscapes(contents, locale);
666
+ return {
667
+ kind: "write",
668
+ logicalPath,
669
+ contents,
670
+ };
671
+ }
672
+ if (call.name === "setting_move") {
673
+ const toLogicalPath = safePath(requiredString(call.arguments.to, "to"));
674
+ if (!/^world\/.+\.(?:ya?ml|md)$/u.test(toLogicalPath))
675
+ throw new SettingAuthoringError("setting_move.to must be a world/ .yaml or .md path");
676
+ return {
677
+ kind: "move",
678
+ document: revisionTarget(requiredString(call.arguments.from, "from")),
679
+ toLogicalPath,
680
+ };
681
+ }
682
+ if (call.name === "setting_patch") {
683
+ const op = requiredString(call.arguments.op, "op");
684
+ if (op === "set_metadata") {
685
+ const title = requiredString(call.arguments.title, "title");
686
+ const summary = requiredString(call.arguments.summary, "summary");
687
+ const aliases = requiredAliases(call.arguments.aliases);
688
+ assertNoLiteralUnicodeEscapes({ title, summary, aliases }, locale);
689
+ return {
690
+ kind: "patch",
691
+ document: revisionTarget(requiredString(call.arguments.document, "document")),
692
+ edits: [{ op: "set_metadata", title, summary, aliases }],
693
+ };
694
+ }
695
+ if (op !== "add" && op !== "replace" && op !== "append" && op !== "remove")
696
+ throw new SettingAuthoringError("setting_patch.op must be add, replace, append, remove, or set_metadata");
697
+ const locator = requiredYamlLocatorPath(call.arguments.locator, "locator");
698
+ if (op === "remove") {
699
+ if (!hasOnly(call.arguments, ["document", "op", "locator"]))
700
+ throw new SettingAuthoringError("setting_patch remove accepts only document, op, and locator");
701
+ assertNoLiteralUnicodeEscapes(locator, locale);
702
+ return {
703
+ kind: "patch",
704
+ document: revisionTarget(requiredString(call.arguments.document, "document")),
705
+ edits: [{ op: "remove", locator: { yaml: locator } }],
706
+ };
707
+ }
708
+ if (!hasOnly(call.arguments, ["document", "op", "locator", "value"]) ||
709
+ !Object.hasOwn(call.arguments, "value"))
710
+ throw new SettingAuthoringError(`setting_patch ${op} requires document, op, locator, and value`);
711
+ const value = structuredClone(call.arguments.value);
712
+ assertNoLiteralUnicodeEscapes({ locator, value }, locale);
713
+ return {
714
+ kind: "patch",
715
+ document: revisionTarget(requiredString(call.arguments.document, "document")),
716
+ edits: [
717
+ {
718
+ op,
719
+ locator: {
720
+ yaml: locator,
721
+ },
722
+ value,
723
+ },
724
+ ],
725
+ };
726
+ }
727
+ throw new SettingAuthoringError(`Unsupported write tool: ${call.name}`);
728
+ }
729
+ function safeWorldDocumentPath(path, locale) {
730
+ const logicalPath = safePath(path);
731
+ if (!/^world\/.+\.(?:ya?ml|md)$/u.test(logicalPath))
732
+ throw new SettingAuthoringError(localized(locale, "World files must be .yaml or .md documents below world/", "world 文件必须是 world/ 下的 .yaml 或 .md 文档"));
733
+ return logicalPath;
734
+ }
735
+ function worldWriteContents(snapshot, logicalPath, args, locale) {
736
+ if (!hasOnly(args, ["path", "contents", "ref", "title", "summary", "aliases"]))
737
+ throw new SettingAuthoringError("setting_write_file accepts only its declared arguments");
738
+ const body = requiredString(args.contents, "contents");
739
+ const metadataKeys = ["ref", "title", "summary", "aliases"];
740
+ const provided = metadataKeys.filter((key) => args[key] !== undefined);
741
+ const existingFile = snapshot.files.some(({ path }) => path === logicalPath);
742
+ const existingDocument = listQueryableWorldDocuments(snapshot).some(({ logicalPath: existingPath }) => existingPath === logicalPath);
743
+ if (provided.length === 0) {
744
+ if (!existingFile && !containsTechnicalDocumentHeader(logicalPath, body))
745
+ throw new SettingAuthoringError(localized(locale, "Create a new world document with setting_create", "新建世界文档请使用 setting_create"));
746
+ if (existingFile &&
747
+ !existingDocument &&
748
+ !containsTechnicalDocumentHeader(logicalPath, body))
749
+ throw new SettingAuthoringError(localized(locale, "Repair a damaged document by providing ref, title, summary, and aliases with its body", "修复损坏文档时,请在正文之外同时提供 ref、title、summary 和 aliases"));
750
+ return body;
751
+ }
752
+ if (provided.length !== metadataKeys.length)
753
+ throw new SettingAuthoringError(localized(locale, "Repair metadata must provide ref, title, summary, and aliases together", "修复元信息时必须同时提供 ref、title、summary 和 aliases"));
754
+ if (!existingFile)
755
+ throw new SettingAuthoringError(localized(locale, "Create a new world document with setting_create", "新建世界文档请使用 setting_create"));
756
+ if (existingDocument)
757
+ throw new SettingAuthoringError(localized(locale, "Repair metadata is only for a damaged document; update valid metadata with setting_patch set_metadata", "修复元信息只用于损坏文档;有效文档请用 setting_patch set_metadata 更新元信息"));
758
+ const ref = requiredSettingRef(args.ref, locale);
759
+ const title = requiredString(args.title, "title");
760
+ const summary = requiredString(args.summary, "summary");
761
+ const aliases = requiredAliases(args.aliases);
762
+ assertNoLiteralUnicodeEscapes({ body, ref, title, summary, aliases }, locale);
763
+ const storedId = recoverStoredDocumentId(snapshot, logicalPath);
764
+ const header = {
765
+ $document: {
766
+ ...(storedId === null ? {} : { id: storedId }),
767
+ ref,
768
+ title,
769
+ summary,
770
+ aliases,
771
+ },
772
+ };
773
+ if (logicalPath.endsWith(".md"))
774
+ return `---\n${stringify(header, { indent: 2, lineWidth: 0 }).trimEnd()}\n---\n${body.trimEnd()}\n`;
775
+ return `${stringify(header, { indent: 2, lineWidth: 0 }).trimEnd()}\n${body.trimStart()}`;
776
+ }
777
+ function containsTechnicalDocumentHeader(logicalPath, source) {
778
+ if (logicalPath.endsWith(".md"))
779
+ return /^---\r?\n[\s\S]*?^\$document\s*:/mu.test(source);
780
+ return /^\s*\$document\s*:/u.test(source);
781
+ }
782
+ function recoverStoredDocumentId(snapshot, logicalPath) {
783
+ const source = snapshot.files.find(({ path }) => path === logicalPath)?.contents;
784
+ if (source === undefined)
785
+ return null;
786
+ const yamlSource = logicalPath.endsWith(".md")
787
+ ? /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(source)?.[1]
788
+ : source;
789
+ if (yamlSource === undefined)
790
+ return null;
791
+ const document = parseDocument(yamlSource, { uniqueKeys: true });
792
+ if (document.errors.length > 0)
793
+ return null;
794
+ const id = document.getIn(["$document", "id"]);
795
+ return typeof id === "string" &&
796
+ /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u.test(id)
797
+ ? id
798
+ : null;
799
+ }
800
+ function revisionTarget(value) {
801
+ const selector = documentSelector(value);
802
+ if (selector === null)
803
+ throw new SettingAuthoringError(`Invalid world-document selector: ${value}`);
804
+ return selector;
805
+ }
806
+ function deleteWorldDocument(snapshot, call, reads, locale) {
807
+ if (!hasOnly(call.arguments, ["document"]))
808
+ throw new SettingAuthoringError("setting_delete accepts only the document argument");
809
+ const supplied = requiredString(call.arguments.document, "document");
810
+ const selector = documentSelector(supplied);
811
+ if (selector === null)
812
+ throw new SettingAuthoringError(localized(locale, "setting_delete.document must be an @ref or world/ document path", "setting_delete.document 必须是 @ref 或 world/ 文档路径"));
813
+ const resolved = snapshot.query({
814
+ kind: "read_document",
815
+ document: selector,
816
+ });
817
+ if (resolved.kind === "error")
818
+ throw new SettingAuthoringError(renderRevisionFailure(resolved.diagnostics, locale));
819
+ if (resolved.kind !== "read_document")
820
+ throw new Error("Unexpected world-document delete lookup result");
821
+ const target = resolved.document;
822
+ if (reads.snapshotId !== snapshot.id ||
823
+ !reads.worldDocumentIds.has(target.documentId))
824
+ throw new SettingAuthoringError(localized(locale, `Read @${target.shortRef} completely before deleting it`, `删除 @${target.shortRef} 前必须完整读取该文档`));
825
+ const blockers = settingDocumentDeletionBlockers(snapshot, target);
826
+ if (blockers.length > 0) {
827
+ const locations = blockers
828
+ .map(({ path, locator }) => `- ${path} · ${locator}`)
829
+ .join("\n");
830
+ throw new SettingAuthoringError(localized(locale, `Cannot delete @${target.shortRef}; remove or redirect every reference first:\n${locations}`, `无法删除 @${target.shortRef};请先删除或改向全部引用:\n${locations}`));
831
+ }
832
+ const revised = WorldDocumentStore.open({
833
+ layout: "content_package",
834
+ files: sortFiles(snapshot.files.filter(({ path }) => path !== target.logicalPath)),
835
+ });
836
+ rebaseReads(reads, revised);
837
+ reads.worldDocumentIds.delete(target.documentId);
838
+ reads.damagedWorldPaths.delete(target.logicalPath);
839
+ return {
840
+ snapshot: revised,
841
+ markdown: localized(locale, `# Current-tree deletion accepted\n\nDeleted @${target.shortRef} · ${target.logicalPath}. No currentSituation binding, frame slot, player-view selector, or cross-document $ref points to it.`, `# 当前树删除已接受\n\n已删除 @${target.shortRef} · ${target.logicalPath}。没有 currentSituation、frame slot、player-view selector 或跨文档 $ref 指向它。`),
842
+ };
843
+ }
844
+ function settingDocumentDeletionBlockers(snapshot, target) {
845
+ const blockers = [];
846
+ for (const document of listQueryableWorldDocuments(snapshot)) {
847
+ if (document.documentId === target.documentId || document.codec !== "yaml")
848
+ continue;
849
+ const selected = snapshot.query({
850
+ kind: "select_node",
851
+ document: { documentId: document.documentId },
852
+ locator: { yaml: [] },
853
+ });
854
+ if (selected.kind !== "select_node")
855
+ continue;
856
+ for (const reference of selected.references)
857
+ if (reference.target.documentId === target.documentId)
858
+ blockers.push({
859
+ path: document.logicalPath,
860
+ locator: deletionLocator(reference.locator),
861
+ });
862
+ }
863
+ blockers.push(...controlDocumentDeletionBlockers(snapshot, target, "control/frame.yaml"), ...controlDocumentDeletionBlockers(snapshot, target, "control/player-views.yaml"));
864
+ return [
865
+ ...new Map(blockers.map((item) => [`${item.path}\0${item.locator}`, item])).values(),
866
+ ].sort((left, right) => left.path.localeCompare(right.path) ||
867
+ left.locator.localeCompare(right.locator));
868
+ }
869
+ function controlDocumentDeletionBlockers(snapshot, target, path) {
870
+ const stored = snapshot.files.find((file) => file.path === path);
871
+ if (stored === undefined || stored.encoding !== undefined)
872
+ return [];
873
+ const source = projectOpaqueDocumentIds(snapshot, path, stored.contents);
874
+ const document = parseDocument(source, {
875
+ schema: "core",
876
+ uniqueKeys: true,
877
+ strict: true,
878
+ });
879
+ if (document.errors.length > 0 || document.warnings.length > 0)
880
+ return [];
881
+ const expected = `@${target.shortRef}`;
882
+ const blockers = [];
883
+ const recordIfTarget = (selectorPath, label) => {
884
+ const node = document.getIn(selectorPath, true);
885
+ if (isScalar(node) && node.value === expected)
886
+ blockers.push({ path, locator: label });
887
+ };
888
+ if (path === "control/frame.yaml") {
889
+ recordIfTarget(["bindings", "currentSituation"], "bindings.currentSituation");
890
+ recordIfTarget(["maintenance", "clock", "document"], "maintenance.clock.document");
891
+ const context = document.getIn(["context"], true);
892
+ if (isSeq(context))
893
+ for (const index of context.items.keys()) {
894
+ recordIfTarget(["context", index, "slot", "document"], `context[${index}].slot.document`);
895
+ recordIfTarget(["context", index, "slot", "from", "document"], `context[${index}].slot.from.document`);
896
+ }
897
+ }
898
+ else {
899
+ const views = document.getIn(["views"], true);
900
+ if (isSeq(views))
901
+ for (const viewIndex of views.items.keys()) {
902
+ const items = document.getIn(["views", viewIndex, "items"], true);
903
+ if (!isSeq(items))
904
+ continue;
905
+ for (const itemIndex of items.items.keys())
906
+ recordIfTarget(["views", viewIndex, "items", itemIndex, "select", "document"], `views[${viewIndex}].items[${itemIndex}].select.document`);
907
+ }
908
+ }
909
+ return blockers;
910
+ }
911
+ function deletionLocator(locator) {
912
+ return "yaml" in locator
913
+ ? `yaml:${JSON.stringify(locator.yaml)}`
914
+ : `markdown:${JSON.stringify(locator.markdown)}`;
915
+ }
916
+ function writeOpaque(snapshot, call, reads, locale) {
917
+ if (!hasOnly(call.arguments, ["path", "contents"]))
918
+ throw new SettingAuthoringError("Special-file writes accept only path and contents");
919
+ const path = safePath(requiredString(call.arguments.path, "path"));
920
+ if (!writableOpaquePath(path))
921
+ throw new SettingAuthoringError(localized(locale, "Special-file writes accept only opening.md, control/frame.yaml, control/player-views.yaml, or control/blocks/*.md", "专用文件只允许写入 opening.md、control/frame.yaml、control/player-views.yaml 或 control/blocks/*.md"));
922
+ const authoredContents = requiredString(call.arguments.contents, "contents");
923
+ assertNoLiteralUnicodeEscapes(authoredContents, locale);
924
+ const contents = projectOpaqueDocumentIds(snapshot, path, authoredContents);
925
+ const next = cloneFiles(snapshot.files);
926
+ const existing = next.find((file) => file.path === path);
927
+ if (existing !== undefined && !reads.opaquePaths.has(path))
928
+ throw new SettingAuthoringError(localized(locale, `Read ${path} completely before replacing it`, `替换 ${path} 前必须完整读取它`));
929
+ if (existing === undefined)
930
+ next.push({ path, contents });
931
+ else {
932
+ existing.contents = contents;
933
+ delete existing.encoding;
934
+ }
935
+ const revised = WorldDocumentStore.open({
936
+ layout: "content_package",
937
+ files: sortFiles(next),
938
+ });
939
+ rebaseReads(reads, revised);
940
+ reads.opaquePaths.add(path);
941
+ return {
942
+ snapshot: revised,
943
+ markdown: localized(locale, `# Current-tree write accepted\n\n${existing === undefined ? "Created" : "Updated"} ${path} in the content package's current tree. This file is already fully read; do not read it again unless a later decision needs Runtime's exact serialization.`, `# 当前树写入已接受\n\n已在内容包当前树中${existing === undefined ? "创建" : "更新"} ${path}。该文件已视为完整读取;除非后续判断依赖 Runtime 的精确序列化结果,否则不要再次读取。`),
944
+ };
945
+ }
946
+ function writableOpaquePath(path) {
947
+ return (isPackageFollowupControlPath(path) ||
948
+ path === "opening.md" ||
949
+ path === "control/frame.yaml" ||
950
+ path === "control/player-views.yaml" ||
951
+ /^control\/blocks\/[a-z0-9][a-z0-9/_-]*\.md$/u.test(path));
952
+ }
953
+ function assertWorldWritesAuthorized(snapshotId, reads, changes, locale) {
954
+ if (reads.snapshotId !== snapshotId)
955
+ throw new Error("Read authorization belongs to another current-tree revision");
956
+ for (const change of changes) {
957
+ if (change.before === null ||
958
+ reads.worldDocumentIds.has(change.documentId) ||
959
+ reads.damagedWorldPaths.has(change.before.logicalPath))
960
+ continue;
961
+ throw new SettingAuthoringError(localized(locale, `Read @${change.shortRef} completely before changing it`, `修改 @${change.shortRef} 前必须完整读取该文档`));
962
+ }
963
+ }
964
+ function renderWorldWriteSuccess(changes, locale) {
965
+ return [
966
+ localized(locale, "# Current-tree revision accepted", "# 当前树 revision 已接受"),
967
+ "",
968
+ ...(changes.length === 0
969
+ ? [localized(locale, "- No file changed", "- 没有文件发生变化")]
970
+ : changes.map((change) => {
971
+ if (change.before === null)
972
+ return `- ${localized(locale, "Created", "创建")} @${change.shortRef} · ${change.after.logicalPath}`;
973
+ if (change.before.logicalPath === change.after.logicalPath)
974
+ return `- ${localized(locale, "Updated", "更新")} @${change.shortRef} · ${change.after.logicalPath}`;
975
+ return `- ${localized(locale, "Moved", "移动")} @${change.shortRef} · ${change.before.logicalPath} → ${change.after.logicalPath}`;
976
+ })),
977
+ "",
978
+ localized(locale, "This call was accepted independently. Every created or changed document above is already fully read; do not read it again unless a later decision needs Runtime's exact serialization.", "本调用已独立接受。上方新建或修改的文档均视为已经完整读取;除非后续判断依赖 Runtime 的精确序列化结果,否则不要再次读取。"),
979
+ ].join("\n");
980
+ }
981
+ function renderRevisionFailure(diagnostics, locale) {
982
+ return diagnostics
983
+ .map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, locale)}`)
984
+ .join("\n");
985
+ }
986
+ function listDocuments(snapshot, args, locale) {
987
+ if (!hasOnly(args, ["directory", "limit", "cursor"]))
988
+ return failure("setting_list accepts only directory, limit, and cursor", locale);
989
+ const directory = args.directory ?? "world";
990
+ const limit = args.limit ?? 100;
991
+ if (typeof directory !== "string" ||
992
+ (directory !== "world" && !directory.startsWith("world/")) ||
993
+ typeof limit !== "number" ||
994
+ !Number.isInteger(limit) ||
995
+ limit < 1 ||
996
+ limit > 100 ||
997
+ !validCursor(args.cursor))
998
+ return failure("Invalid setting_list directory, limit, or cursor", locale);
999
+ const relative = directory === "world" ? "" : directory.slice(6);
1000
+ const result = snapshot.query({
1001
+ kind: "catalog",
1002
+ directory: relative,
1003
+ limit,
1004
+ ...(args.cursor === undefined ? {} : { cursor: args.cursor }),
1005
+ });
1006
+ if (result.kind === "error")
1007
+ return renderQueryFailure(result, locale);
1008
+ if (result.kind !== "catalog")
1009
+ throw new Error("Unexpected catalog result");
1010
+ const entries = result.entries.map((entry) => {
1011
+ if (entry.kind === "directory")
1012
+ return `- [dir] ${entry.logicalPath}/`;
1013
+ if (entry.document === undefined)
1014
+ return `- [damaged] ${entry.logicalPath} · ${entry.diagnostics.map(({ code }) => code).join(", ")}`;
1015
+ return `- @${entry.document.shortRef} · ${entry.document.title} · ${entry.logicalPath}`;
1016
+ });
1017
+ if (relative === "" && result.page.start === 0)
1018
+ entries.unshift(...snapshot.files
1019
+ .filter(({ path }) => !path.startsWith("world/"))
1020
+ .map(({ path, encoding }) => encoding === undefined ? `- [special] ${path}` : `- [binary] ${path}`));
1021
+ return success(`# ${localized(locale, "Setting directory", "设定目录")}\n\n${entries.join("\n") || localized(locale, "(empty)", "(空)")}\n\n${pageFooter(result.page, locale)}`);
1022
+ }
1023
+ function searchDocuments(snapshot, args, locale) {
1024
+ const query = args.query;
1025
+ const limit = args.limit ?? 20;
1026
+ if (!hasOnly(args, ["query", "within", "caseSensitive", "limit", "cursor"]) ||
1027
+ typeof query !== "string" ||
1028
+ query.length < 1 ||
1029
+ query.length > 256 ||
1030
+ typeof limit !== "number" ||
1031
+ !Number.isInteger(limit) ||
1032
+ limit < 1 ||
1033
+ limit > 100 ||
1034
+ (args.caseSensitive !== undefined &&
1035
+ typeof args.caseSensitive !== "boolean") ||
1036
+ !validCursor(args.cursor))
1037
+ return failure("Invalid setting_search arguments", locale);
1038
+ const within = searchScope(args.within);
1039
+ if (within === invalidScope)
1040
+ return failure("Invalid setting_search within scope", locale);
1041
+ const result = snapshot.query({
1042
+ kind: "literal_search",
1043
+ query,
1044
+ caseSensitive: args.caseSensitive === true,
1045
+ ...(within === undefined ? {} : { within }),
1046
+ limit,
1047
+ ...(args.cursor === undefined ? {} : { cursor: args.cursor }),
1048
+ });
1049
+ if (result.kind === "error")
1050
+ return renderQueryFailure(result, locale);
1051
+ if (result.kind !== "literal_search")
1052
+ throw new Error("Unexpected literal search result");
1053
+ const lines = result.matches.map(({ document, referenceProjection, range }) => `- @${document.shortRef} · ${document.logicalPath} · ${range.start.line}:${range.start.column}\n ${JSON.stringify(referenceProjection.excerpt)}`);
1054
+ return success(`# ${localized(locale, "Literal setting search", "设定字面搜索")}\n\n${lines.join("\n") || localized(locale, "Zero literal matches do not prove the fact is absent.", "0 个字面命中不证明设定中不存在该事实。")}\n\n${pageFooter(result.page, locale)}`);
1055
+ }
1056
+ const invalidScope = Symbol("invalid-scope");
1057
+ function searchScope(value) {
1058
+ if (value === undefined)
1059
+ return undefined;
1060
+ if (typeof value !== "string" || value.length === 0)
1061
+ return invalidScope;
1062
+ if (value === "world")
1063
+ return { directory: "" };
1064
+ if (value.startsWith("@"))
1065
+ return value.length > 1
1066
+ ? { document: { shortRef: value.slice(1) } }
1067
+ : invalidScope;
1068
+ if (value.startsWith("world/")) {
1069
+ const path = safePath(value);
1070
+ return /\.(?:ya?ml|md)$/u.test(path)
1071
+ ? { document: { logicalPath: path } }
1072
+ : { directory: path.slice(6) };
1073
+ }
1074
+ return invalidScope;
1075
+ }
1076
+ function readDocument(snapshot, args, reads, locale) {
1077
+ const path = args.path;
1078
+ if (!hasOnly(args, ["path", "maxBytes", "cursor"]) ||
1079
+ typeof path !== "string" ||
1080
+ path.length === 0)
1081
+ return failure("Invalid setting_read arguments", locale);
1082
+ const opaque = snapshot.files.find((file) => file.path === path && !file.path.startsWith("world/"));
1083
+ if (opaque !== undefined) {
1084
+ if (opaque.encoding !== undefined)
1085
+ return failure("Special files require one complete text read", locale);
1086
+ reads.opaquePaths.add(path);
1087
+ const projected = projectOpaqueDocumentIds(snapshot, path, opaque.contents);
1088
+ return success(`# ${localized(locale, "Special-file source", "专用文件原文")} ${path}\n\n${projected}\n\n---\nComplete: yes`);
1089
+ }
1090
+ const selector = documentSelector(path);
1091
+ if (selector === null)
1092
+ return failure(selector === null && typeof path === "string" && !path.includes("/")
1093
+ ? "Use @ref or a world/ logical path"
1094
+ : "Invalid setting_read path", locale);
1095
+ const result = snapshot.query({
1096
+ kind: "read_document",
1097
+ document: selector,
1098
+ referenceProjection: "short_ref",
1099
+ });
1100
+ if (result.kind === "error") {
1101
+ if (repairableDamagedWorldRead(snapshot, path, result))
1102
+ reads.damagedWorldPaths.add(path);
1103
+ return renderQueryFailure(result, locale);
1104
+ }
1105
+ if (result.kind !== "read_document")
1106
+ throw new Error("Unexpected read result");
1107
+ reads.worldDocumentIds.add(result.document.documentId);
1108
+ return success(`# ${localized(locale, "Exact read", "精确读取")} @${result.document.shortRef}\n\ntitle: ${result.document.title}\nsummary: ${result.document.summary}\ncodec: ${result.document.codec}\nlogicalPath: ${result.document.logicalPath}\n[body]\n${result.body.trimEnd()}\n[/body]\n\n---\nComplete: yes`);
1109
+ }
1110
+ function repairableDamagedWorldRead(snapshot, path, result) {
1111
+ if (!path.startsWith("world/") ||
1112
+ !snapshot.files.some(({ path: candidate }) => candidate === path))
1113
+ return false;
1114
+ // A failed exact read authorizes replacement only when the source itself is
1115
+ // malformed. Bad selectors, missing documents, and ambiguity must never
1116
+ // turn into a read-before-write bypass.
1117
+ return result.diagnostics.some(({ code }) => code !== "cursor_invalid" &&
1118
+ code !== "query_invalid" &&
1119
+ code !== "document_not_found" &&
1120
+ code !== "document_ambiguous");
1121
+ }
1122
+ function documentSelector(value) {
1123
+ if (value.startsWith("@"))
1124
+ return value.length > 1 ? { shortRef: value.slice(1) } : null;
1125
+ if (value.startsWith("world/")) {
1126
+ const path = safePath(value);
1127
+ return /\.(?:ya?ml|md)$/u.test(path) ? { logicalPath: path } : null;
1128
+ }
1129
+ return null;
1130
+ }
1131
+ function projectOpaqueDocumentIds(snapshot, path, source) {
1132
+ if (path !== "control/frame.yaml" && path !== "control/player-views.yaml")
1133
+ return source;
1134
+ const byId = new Map(listQueryableWorldDocuments(snapshot).map((document) => [
1135
+ document.documentId,
1136
+ document.shortRef,
1137
+ ]));
1138
+ if (byId.size === 0)
1139
+ return source;
1140
+ const document = parseDocument(source, { uniqueKeys: true });
1141
+ if (document.errors.length > 0)
1142
+ return source;
1143
+ let changed = false;
1144
+ const projectAt = (selectorPath) => {
1145
+ const node = document.getIn(selectorPath, true);
1146
+ if (!isScalar(node) || typeof node.value !== "string")
1147
+ return;
1148
+ const shortRef = byId.get(node.value);
1149
+ if (shortRef === undefined)
1150
+ return;
1151
+ node.value = `@${shortRef}`;
1152
+ changed = true;
1153
+ };
1154
+ if (path === "control/frame.yaml") {
1155
+ projectAt(["bindings", "currentSituation"]);
1156
+ const context = document.getIn(["context"], true);
1157
+ if (isSeq(context))
1158
+ for (const index of context.items.keys()) {
1159
+ projectAt(["context", index, "slot", "document"]);
1160
+ projectAt(["context", index, "slot", "from", "document"]);
1161
+ }
1162
+ }
1163
+ else {
1164
+ const views = document.getIn(["views"], true);
1165
+ if (isSeq(views))
1166
+ for (const viewIndex of views.items.keys()) {
1167
+ const items = document.getIn(["views", viewIndex, "items"], true);
1168
+ if (!isSeq(items))
1169
+ continue;
1170
+ for (const itemIndex of items.items.keys())
1171
+ projectAt([
1172
+ "views",
1173
+ viewIndex,
1174
+ "items",
1175
+ itemIndex,
1176
+ "select",
1177
+ "document",
1178
+ ]);
1179
+ }
1180
+ }
1181
+ return changed ? document.toString({ indent: 2, lineWidth: 0 }) : source;
1182
+ }
1183
+ function renderQueryFailure(result, locale) {
1184
+ return failure(result.diagnostics
1185
+ .map(({ code, logicalPath, message }) => `${code}${logicalPath === undefined ? "" : ` · ${logicalPath}`} · ${publicSettingDiagnosticMessage(code, message, locale)}`)
1186
+ .join("\n"), locale);
1187
+ }
1188
+ function publicSettingDiagnosticMessage(code, message, locale) {
1189
+ if (code === "document_header_invalid" ||
1190
+ code === "document_identity_invalid" ||
1191
+ code === "invalid_document_header")
1192
+ return localized(locale, "Document storage metadata is missing or damaged. Repair this document with setting_write_file using its ref, title, summary, aliases, and body", "文档存储元信息缺失或损坏;请用 setting_write_file 提供 ref、title、summary、aliases 和正文进行修复");
1193
+ if (code === "document_identity_duplicate" || code === "duplicate_id")
1194
+ return localized(locale, "Runtime-owned storage identity is duplicated; repair the affected document without supplying an identity", "Runtime 维护的存储身份发生重复;请修复受影响文档,不要提供内部身份");
1195
+ if (code === "document_short_ref_invalid")
1196
+ return localized(locale, "Document ref must be a 2-to-32-character lowercase ASCII short handle; repair it with setting_write_file and public metadata", "文档 ref 必须是 2~32 位小写 ASCII 短句柄;请用 setting_write_file 和公开元信息修复");
1197
+ if (code === "document_short_ref_duplicate" || code === "duplicate_ref")
1198
+ return localized(locale, "Document refs must be unique; choose a different ref for the affected document", "文档 ref 必须唯一;请为受影响文档选择另一个 ref");
1199
+ return message
1200
+ .replaceAll("$document.id", "Runtime-owned storage identity")
1201
+ .replaceAll("$document.ref", "ref")
1202
+ .replaceAll("$document technical header", "document storage metadata")
1203
+ .replaceAll("$document", "document storage metadata")
1204
+ .replace(/document identity/giu, "Runtime-owned storage identity");
1205
+ }
1206
+ function pageFooter(page, locale) {
1207
+ return `${localized(locale, "Page", "本页")} ${page.start}..${page.end} / ${page.total}\nComplete: ${page.complete ? "yes" : "no"}${page.nextCursor === null ? "" : `\nNext cursor: ${page.nextCursor}`}`;
1208
+ }
1209
+ function success(markdown) {
1210
+ return { ok: true, markdown };
1211
+ }
1212
+ function failure(message, locale) {
1213
+ return {
1214
+ ok: false,
1215
+ markdown: `${localized(locale, "# Runtime tool rejected", "# Runtime 工具拒绝")}\n\n${message}`,
1216
+ };
1217
+ }
1218
+ function toToolResult(id, result, changes) {
1219
+ return {
1220
+ toolCallId: id,
1221
+ markdown: result.markdown,
1222
+ isError: !result.ok,
1223
+ changes,
1224
+ };
1225
+ }
1226
+ function cloneReads(source) {
1227
+ return {
1228
+ snapshotId: source.snapshotId,
1229
+ worldDocumentIds: new Set(source.worldDocumentIds),
1230
+ damagedWorldPaths: new Set(source.damagedWorldPaths),
1231
+ opaquePaths: new Set(source.opaquePaths),
1232
+ };
1233
+ }
1234
+ function rebaseReads(reads, snapshot) {
1235
+ reads.snapshotId = snapshot.id;
1236
+ }
1237
+ function fileDiff(baseFiles, draftFiles) {
1238
+ const before = new Map(baseFiles.map((file) => [file.path, file]));
1239
+ const after = new Map(draftFiles.map((file) => [file.path, file]));
1240
+ return [...new Set([...before.keys(), ...after.keys()])]
1241
+ .sort()
1242
+ .flatMap((path) => {
1243
+ const left = before.get(path) ?? null;
1244
+ const right = after.get(path) ?? null;
1245
+ if (left?.contents === right?.contents &&
1246
+ left?.encoding === right?.encoding)
1247
+ return [];
1248
+ return [
1249
+ {
1250
+ path,
1251
+ kind: left === null ? "create" : right === null ? "delete" : "modify",
1252
+ before: left?.contents ?? null,
1253
+ after: right?.contents ?? null,
1254
+ },
1255
+ ];
1256
+ });
1257
+ }
1258
+ function safePath(path) {
1259
+ if (path.startsWith("/") ||
1260
+ path.includes("\\") ||
1261
+ path
1262
+ .split("/")
1263
+ .some((part) => part === "." || part === ".." || part.length === 0))
1264
+ throw new SettingAuthoringError(`Unsafe content-package path: ${path}`);
1265
+ return path;
1266
+ }
1267
+ function requiredString(value, name) {
1268
+ if (typeof value !== "string" || value.length === 0)
1269
+ throw new SettingAuthoringError(`${name} must be a non-empty string`);
1270
+ return value;
1271
+ }
1272
+ function requiredSettingRef(value, locale) {
1273
+ if (typeof value !== "string" ||
1274
+ value.length < 2 ||
1275
+ value.length > 32 ||
1276
+ !/^[a-z][a-z0-9-]*$/u.test(value))
1277
+ throw new SettingAuthoringError(localized(locale, "ref must be 2 to 32 lowercase ASCII letters, digits, or hyphens, beginning with a letter", "ref 必须是 2~32 位小写 ASCII 字母、数字或连字符,并以字母开头"));
1278
+ return value;
1279
+ }
1280
+ function requiredAliases(value) {
1281
+ if (!Array.isArray(value) ||
1282
+ value.length > 16 ||
1283
+ !value.every((item) => typeof item === "string" &&
1284
+ [...item].length >= 1 &&
1285
+ [...item].length <= 64))
1286
+ throw new SettingAuthoringError("aliases must be an array of at most 16 strings, each 1 to 64 characters");
1287
+ return value.filter((item) => typeof item === "string");
1288
+ }
1289
+ function assertNoLiteralUnicodeEscapes(value, locale) {
1290
+ const seen = new Set();
1291
+ const walk = (candidate) => {
1292
+ if (typeof candidate === "string") {
1293
+ if (/\\u[0-9a-f]{4}/iu.test(candidate))
1294
+ throw new SettingAuthoringError(localized(locale, "Persisted text contains a literal Unicode escape such as \\u4f60. Resend real Unicode characters instead of a double-escaped string", "待写入文本包含 \\u4f60 这类字面 Unicode 转义;请重新发送真实 Unicode 字符,不要发送重复转义字符串"));
1295
+ return;
1296
+ }
1297
+ if (typeof candidate !== "object" || candidate === null)
1298
+ return;
1299
+ if (seen.has(candidate))
1300
+ return;
1301
+ seen.add(candidate);
1302
+ if (Array.isArray(candidate)) {
1303
+ for (const item of candidate)
1304
+ walk(item);
1305
+ return;
1306
+ }
1307
+ for (const [key, item] of Object.entries(candidate)) {
1308
+ walk(key);
1309
+ walk(item);
1310
+ }
1311
+ };
1312
+ walk(value);
1313
+ }
1314
+ function requiredYamlLocatorPath(value, name) {
1315
+ if (!Array.isArray(value) ||
1316
+ value.length === 0 ||
1317
+ !value.every((item) => (typeof item === "string" && item.length > 0) ||
1318
+ (typeof item === "number" && Number.isInteger(item) && item >= 0)))
1319
+ throw new SettingAuthoringError(`${name} must be a non-empty array of map keys or zero-based array indexes`);
1320
+ return value.filter((item) => typeof item === "string" || typeof item === "number");
1321
+ }
1322
+ function hasOnly(value, allowed) {
1323
+ return Object.keys(value).every((key) => allowed.includes(key));
1324
+ }
1325
+ function validCursor(value) {
1326
+ return value === undefined || value === null || typeof value === "string";
1327
+ }
1328
+ function isRecord(value) {
1329
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1330
+ }
1331
+ function cloneFiles(files) {
1332
+ return files.map((file) => structuredClone(file));
1333
+ }
1334
+ function sortFiles(files) {
1335
+ return files.sort((left, right) => left.path.localeCompare(right.path));
1336
+ }
1337
+ function localized(locale, english, chinese) {
1338
+ return locale === "zh-CN" ? chinese : english;
1339
+ }