narraeon 0.5.7 → 0.6.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.
- package/README.md +13 -0
- package/README.zh-CN.md +10 -0
- package/dist/node/protocol/gameplay.js +69 -0
- package/dist/node/protocol/resolution.js +274 -0
- package/dist/node/protocol/v1.js +15 -2
- package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
- package/dist/node/runtime/RuntimeErrorLog.js +95 -0
- package/dist/node/runtime/V1Runtime.js +58 -7
- package/dist/node/runtime/archive/WorldArchives.js +14 -46
- package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
- package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
- package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
- package/dist/node/runtime/config/dataRootLock.js +108 -0
- package/dist/node/runtime/content/ContentWorkspace.js +10 -12
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +50 -46
- package/dist/node/runtime/content/FileNativeContentTree.js +8 -1
- package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
- package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
- package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +31 -170
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +48 -3
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +36 -239
- package/dist/node/runtime/play/OrderedPlayPrompts.js +68 -6
- package/dist/node/runtime/play/PlayCallChain.js +359 -359
- package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
- package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
- package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
- package/dist/node/runtime/play/TabletopResolution.js +372 -0
- package/dist/node/runtime/play/TabletopRules.js +141 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +45 -10
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +35 -2
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +7 -16
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +56 -13
- package/dist/node/runtime/setting/SettingImprovementSession.js +6 -4
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
- package/dist/node/runtime/world/FileNativeWorldStore.js +266 -783
- package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
- package/dist/node/runtime/world/WorldActivity.js +14 -22
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
- package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
- package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
- package/dist/node/runtime/world/WorldQueue.js +46 -0
- package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -12
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +35 -6
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +9 -5
- package/dist/node/server/ConversationStream.js +2 -0
- package/dist/node/server/mobile.js +20 -0
- package/dist/node/server/startWebServer.js +15 -8
- package/dist/node/shared/ordered-author-prompts.js +8 -0
- package/dist/node/shared/ordered-play-prompts.js +23 -0
- package/dist/node/shared/tabletop-guidance.js +100 -0
- package/dist/web/assets/index-7GzLSUpU.js +1323 -0
- package/dist/web/assets/index-BJ-1Mb0e.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
- package/dist/web/assets/index-DxNtxzLR.js +0 -1255
- package/dist/web/assets/index-DzOnu9uv.css +0 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { gameplayLabel } from "../../protocol/gameplay.js";
|
|
2
|
+
import { resolutionCheckToolForScheme, resolutionToolDefinition, } from "./TabletopResolution.js";
|
|
3
|
+
/**
|
|
4
|
+
* The scheme's fixed rules, taken from the same description the check tool
|
|
5
|
+
* sends to the model, so the prompt and the tool cannot drift apart.
|
|
6
|
+
*/
|
|
7
|
+
export function tabletopSchemeRules(gameplay, locale) {
|
|
8
|
+
return resolutionToolDefinition(resolutionCheckToolForScheme(gameplay.scheme), locale).description;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Runtime-owned rules at the world placeholder of a tabletop world. The block
|
|
12
|
+
* is world-derived and carries no preset state, so a scheme never requires a
|
|
13
|
+
* preset change.
|
|
14
|
+
*/
|
|
15
|
+
export function tabletopWorldRulesBlock(gameplay, locale) {
|
|
16
|
+
const label = gameplayLabel(gameplay, locale);
|
|
17
|
+
const rules = tabletopSchemeRules(gameplay, locale);
|
|
18
|
+
// Only the bag scheme has a world-defined token list; other schemes never mention one.
|
|
19
|
+
const bagLine = gameplay.scheme !== "bag"
|
|
20
|
+
? ""
|
|
21
|
+
: locale === "zh-CN"
|
|
22
|
+
? "- 世界文档中定义的混乱袋必须读取,并原样提供其当前标记清单;袋的变化由你在世界文档中维护。\n"
|
|
23
|
+
: "- A chaos bag defined in the world documents must be read and supplied exactly as its current token list. You maintain the bag's changes in the world documents.\n";
|
|
24
|
+
const markdown = locale === "zh-CN"
|
|
25
|
+
? `# 跑团判定规则
|
|
26
|
+
|
|
27
|
+
本世界是跑团世界,判定方案为 ${label}。以下规则由 Runtime 固定,随本世界的玩法身份生效。
|
|
28
|
+
|
|
29
|
+
## 判定工具
|
|
30
|
+
|
|
31
|
+
${rules}
|
|
32
|
+
|
|
33
|
+
## 辅助工具
|
|
34
|
+
|
|
35
|
+
- roll_dice:辅助数值掷骰,用于伤害、数量等,不重新裁决已确定的行动成败。
|
|
36
|
+
- calculate:对两个数执行加减乘除,不产生随机数,不修改世界。
|
|
37
|
+
|
|
38
|
+
## 绑定契约
|
|
39
|
+
|
|
40
|
+
- 调用判定工具前,先声明行动目标、所用数值与难度;只有有成败不确定性或对抗的行动才需要判定。
|
|
41
|
+
- 必须先调用判定工具并取得回执,再叙述结果。
|
|
42
|
+
- 判定成功必须实现已声明的目标,失败则不实现,不论情节是否看似合理。
|
|
43
|
+
- 大成功与大失败按工具的固定规则兑现,不能由叙述改写。
|
|
44
|
+
- 掷骰之后不得更改该行动的目标或难度,也不得为同一行动重掷。
|
|
45
|
+
- 辅助掷骰与算术只决定伤害、数量等数值,不能推翻已经结算的判定。
|
|
46
|
+
${bagLine}- 参数不合法属于工具错误,不等于行动失败;修正参数后重新调用即可。`
|
|
47
|
+
: `# Tabletop resolution rules
|
|
48
|
+
|
|
49
|
+
This world is tabletop, with resolution scheme ${label}. The Runtime-fixed rules below apply because of this world's gameplay identity.
|
|
50
|
+
|
|
51
|
+
## Check tool
|
|
52
|
+
|
|
53
|
+
${rules}
|
|
54
|
+
|
|
55
|
+
## Auxiliary tools
|
|
56
|
+
|
|
57
|
+
- roll_dice: auxiliary numeric dice for damage, quantities and similar values. It never re-decides a resolved action.
|
|
58
|
+
- calculate: add, subtract, multiply or divide two numbers. It draws no randomness and changes no world state.
|
|
59
|
+
|
|
60
|
+
## Binding contract
|
|
61
|
+
|
|
62
|
+
- Before calling the check tool, declare the action's goal, the inputs and the difficulty. Only actions with uncertain success or opposition need a check.
|
|
63
|
+
- Call the check tool and receive its receipt before narrating the outcome.
|
|
64
|
+
- A success must fulfil the declared goal and a failure must not, regardless of whether the story seems plausible.
|
|
65
|
+
- Critical outcomes follow the tool's fixed rules and cannot be rewritten by narration.
|
|
66
|
+
- After a draw, never change that action's goal or difficulty, and never reroll the same action.
|
|
67
|
+
- Auxiliary dice and arithmetic decide numeric values such as damage or quantities; they never reverse a resolved check.
|
|
68
|
+
${bagLine}- Invalid arguments are tool errors, not failed actions; correct the arguments and call the tool again.`;
|
|
69
|
+
return { source: "runtime:world-placeholder/gameplay", markdown };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Author-side statement of a tabletop package or world revision: the mode and
|
|
73
|
+
* scheme, what a write may change, and the scheme's rules.
|
|
74
|
+
*/
|
|
75
|
+
export function tabletopAuthoringRulesBlock(gameplay, target, locale) {
|
|
76
|
+
const label = gameplayLabel(gameplay, locale);
|
|
77
|
+
const rules = tabletopSchemeRules(gameplay, locale);
|
|
78
|
+
const source = target === "setting"
|
|
79
|
+
? "runtime:content-package/gameplay"
|
|
80
|
+
: "runtime:world-revision/gameplay";
|
|
81
|
+
const markdown = target === "setting"
|
|
82
|
+
? locale === "zh-CN"
|
|
83
|
+
? `# 玩法模式与判定方案
|
|
84
|
+
|
|
85
|
+
当前内容包是跑团内容包,当前判定方案为 ${label}。玩法模式固定为跑团,不能改为互动式小说。判定方案可通过写入 control/gameplay.json 改为 d20、d100 或 bag;这只影响之后创建的世界,已有世界不随源内容包变化。改变方案时,应同步调整人物数值、难度与规则,使其符合新方案的数值尺度。
|
|
86
|
+
|
|
87
|
+
当前方案的判定工具规则:
|
|
88
|
+
|
|
89
|
+
${rules}`
|
|
90
|
+
: `# Gameplay mode and resolution scheme
|
|
91
|
+
|
|
92
|
+
This content package is tabletop, with current resolution scheme ${label}. The gameplay mode is fixed as tabletop and cannot become interactive fiction. The scheme may be changed by writing control/gameplay.json to d20, d100 or bag. The change affects only worlds created later; existing worlds never follow the source package. When changing the scheme, adjust character values, difficulty and rules so they match the new scheme's numeric scale.
|
|
93
|
+
|
|
94
|
+
Check-tool rules for the current scheme:
|
|
95
|
+
|
|
96
|
+
${rules}`
|
|
97
|
+
: locale === "zh-CN"
|
|
98
|
+
? `# 玩法模式与判定方案(固定)
|
|
99
|
+
|
|
100
|
+
当前修订的世界是跑团世界,判定方案为 ${label}。玩法模式与判定方案在世界创建后固定,修订不能修改 control/gameplay.json。数值与难度须遵循该方案。
|
|
101
|
+
|
|
102
|
+
当前方案的判定工具规则:
|
|
103
|
+
|
|
104
|
+
${rules}`
|
|
105
|
+
: `# Gameplay mode and resolution scheme (fixed)
|
|
106
|
+
|
|
107
|
+
The world under revision is tabletop, with resolution scheme ${label}. Its gameplay mode and scheme were fixed at creation; a revision cannot change control/gameplay.json. Numeric values and difficulty must follow this scheme.
|
|
108
|
+
|
|
109
|
+
Check-tool rules for the current scheme:
|
|
110
|
+
|
|
111
|
+
${rules}`;
|
|
112
|
+
const bag = gameplay.scheme === "bag" ? `\n\n${bagAuthoringGuidance(locale)}` : "";
|
|
113
|
+
return { source, markdown: `${markdown}${bag}` };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Authoring guidance for a chaos-bag scheme: the default bag, or a bag the world
|
|
117
|
+
* documents, and who maintains its contents. Only the bag scheme carries it.
|
|
118
|
+
*/
|
|
119
|
+
export function bagAuthoringGuidance(locale) {
|
|
120
|
+
return locale === "zh-CN"
|
|
121
|
+
? `## 混乱袋内容
|
|
122
|
+
|
|
123
|
+
- 默认袋:世界文档未定义标记时,判定使用默认袋:+1、0×2、-1×2、-2、大成功、大失败。
|
|
124
|
+
- 世界自定义袋:在世界文档中写明每种标记的名称与数量,并且每种标记恰好写明修正值或大成功/大失败之一。判定时必须读取该清单,并原样提供。
|
|
125
|
+
- 袋内容的变化由你在世界文档中维护。每次抽取后标记放回,抽取本身不改变袋。`
|
|
126
|
+
: `## Chaos-bag contents
|
|
127
|
+
|
|
128
|
+
- Default bag: when the world documents define no tokens, the check uses the default bag: +1, 0 ×2, -1 ×2, -2, critical success, critical failure.
|
|
129
|
+
- World-documented bag: write each token's label and count in the world documents, and give each token exactly one of a modifier or a critical outcome. Read that list and supply it exactly when checking.
|
|
130
|
+
- You maintain any change to the bag's contents in the world documents. Each drawn token returns to the bag, so drawing never changes the bag itself.`;
|
|
131
|
+
}
|
|
132
|
+
/** The receipt sent after a successful gameplay write; it teaches the new scheme's rules mid-conversation. */
|
|
133
|
+
export function tabletopSchemeReceipt(gameplay, locale) {
|
|
134
|
+
const rules = tabletopSchemeRules(gameplay, locale);
|
|
135
|
+
const receipt = locale === "zh-CN"
|
|
136
|
+
? `新判定方案规则(之后请按此调整数值与难度):\n\n${rules}`
|
|
137
|
+
: `New resolution-scheme rules (adjust numbers and difficulty to match from now on):\n\n${rules}`;
|
|
138
|
+
return gameplay.scheme === "bag"
|
|
139
|
+
? `${receipt}\n\n${bagAuthoringGuidance(locale)}`
|
|
140
|
+
: receipt;
|
|
141
|
+
}
|
|
@@ -5,7 +5,7 @@ import { hasLegacyHistoryTools } from "./FileNativeToolRegistry.js";
|
|
|
5
5
|
import { effectiveFollowupDefinitions } from "../play/OrderedFollowups.js";
|
|
6
6
|
import { legacyAuthorPrompts } from "../play/FileNativePlayPresetStore.js";
|
|
7
7
|
import { builtinAuthorPrompts, authoringMechanics, } from "../../shared/ordered-author-prompts.js";
|
|
8
|
-
import { builtinPlayPrompts, } from "../../shared/ordered-play-prompts.js";
|
|
8
|
+
import { builtinPlayPrompts, orderedPromptScope, promptScopeApplies, } from "../../shared/ordered-play-prompts.js";
|
|
9
9
|
import { parseOrderedPlayPrompts } from "../play/OrderedPlayPrompts.js";
|
|
10
10
|
import { renderDocumentWritePosition } from "./WorldMaintenanceReport.js";
|
|
11
11
|
import { parseDocument } from "yaml";
|
|
@@ -16,7 +16,10 @@ import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
|
|
|
16
16
|
import { checkpointHistory, checkpointReplayBlocks, } from "../play/PlayContinuity.js";
|
|
17
17
|
import { defaultAppLocale, } from "../../protocol/appPreferences.js";
|
|
18
18
|
import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
|
|
19
|
-
import { defaultRuntimeToolDefinitionStrategy,
|
|
19
|
+
import { defaultRuntimeToolDefinitionStrategy, runtimeToolNamesForGameplay, runtimeToolsForNames, } from "./FileNativeToolRegistry.js";
|
|
20
|
+
import { resolutionToolNames } from "../play/TabletopResolution.js";
|
|
21
|
+
import { readGameplay } from "../../protocol/gameplay.js";
|
|
22
|
+
import { tabletopAuthoringRulesBlock, tabletopWorldRulesBlock, } from "../play/TabletopRules.js";
|
|
20
23
|
import { stateDirectoryHandle } from "../world/StateDirectoryHandles.js";
|
|
21
24
|
export class PromptCompilationError extends Error {
|
|
22
25
|
code;
|
|
@@ -103,6 +106,7 @@ const playCallChainToolNames = new Set([
|
|
|
103
106
|
"world_create",
|
|
104
107
|
"world_retire",
|
|
105
108
|
"world_checkpoint",
|
|
109
|
+
...resolutionToolNames,
|
|
106
110
|
]);
|
|
107
111
|
export class FileNativePromptCompiler {
|
|
108
112
|
#toolStrategyOverride;
|
|
@@ -137,6 +141,7 @@ export class FileNativePromptCompiler {
|
|
|
137
141
|
},
|
|
138
142
|
runtimeSource: "runtime:builtin/setting-improvement",
|
|
139
143
|
target: "setting",
|
|
144
|
+
gameplay: input.gameplay,
|
|
140
145
|
runtimeContract: input.runtimeContract,
|
|
141
146
|
authorPrompt: input.authorPrompt,
|
|
142
147
|
playPreset: input.playPreset,
|
|
@@ -165,6 +170,7 @@ export class FileNativePromptCompiler {
|
|
|
165
170
|
},
|
|
166
171
|
runtimeSource: "runtime:builtin/world-revision",
|
|
167
172
|
target: "world-revision",
|
|
173
|
+
gameplay: input.gameplay,
|
|
168
174
|
runtimeContract: input.runtimeContract,
|
|
169
175
|
authorPrompt: input.authorPrompt,
|
|
170
176
|
playPreset: input.playPreset,
|
|
@@ -174,7 +180,7 @@ export class FileNativePromptCompiler {
|
|
|
174
180
|
}
|
|
175
181
|
#compileAuthoringConversation(input) {
|
|
176
182
|
validateModel(input.modelBinding);
|
|
177
|
-
const presetReference = settingImprovementPresetReference(input.playPreset, this.#locale, input.target);
|
|
183
|
+
const presetReference = settingImprovementPresetReference(input.playPreset, this.#locale, input.target, input.gameplay);
|
|
178
184
|
const logicalMessages = [];
|
|
179
185
|
const ordered = input.playPreset.definition.authorPrompts ??
|
|
180
186
|
legacyAuthorPrompts(input.playPreset.definition);
|
|
@@ -182,6 +188,8 @@ export class FileNativePromptCompiler {
|
|
|
182
188
|
for (const entry of ordered) {
|
|
183
189
|
if (entry.kind === "world" || !entry.enabled)
|
|
184
190
|
continue;
|
|
191
|
+
if (!promptScopeApplies(orderedPromptScope(entry, catalog), input.gameplay))
|
|
192
|
+
continue;
|
|
185
193
|
const builtin = entry.kind === "builtin"
|
|
186
194
|
? catalog.find((item) => item.id === entry.builtin)
|
|
187
195
|
: undefined;
|
|
@@ -199,7 +207,15 @@ export class FileNativePromptCompiler {
|
|
|
199
207
|
},
|
|
200
208
|
];
|
|
201
209
|
if (builtin?.id === "author.target")
|
|
202
|
-
blocks = [
|
|
210
|
+
blocks = [
|
|
211
|
+
input.identity,
|
|
212
|
+
input.boundary,
|
|
213
|
+
...(input.gameplay.mode === "tabletop"
|
|
214
|
+
? [
|
|
215
|
+
tabletopAuthoringRulesBlock(input.gameplay, input.target, this.#locale),
|
|
216
|
+
]
|
|
217
|
+
: []),
|
|
218
|
+
];
|
|
203
219
|
if (builtin?.id === "author.play-reference")
|
|
204
220
|
blocks = [
|
|
205
221
|
{
|
|
@@ -336,7 +352,7 @@ export class FileNativePromptCompiler {
|
|
|
336
352
|
markdown: joinBlocks(blocks[role]),
|
|
337
353
|
}));
|
|
338
354
|
scanRuntimeLeakage(logicalMessages);
|
|
339
|
-
const tools = runtimeToolsForNames(
|
|
355
|
+
const tools = runtimeToolsForNames(runtimeToolNamesForGameplay(playGameplay(documentSnapshot)), this.#locale);
|
|
340
356
|
const toolStrategy = this.#toolStrategyOverride ??
|
|
341
357
|
defaultRuntimeToolDefinitionStrategy(effectiveInput.modelBinding.provider);
|
|
342
358
|
const cacheStrategy = effectiveInput.modelBinding.cacheStrategy ??
|
|
@@ -468,7 +484,7 @@ export class FileNativePromptCompiler {
|
|
|
468
484
|
const bootstrap = binding.definition.playPrompts === undefined
|
|
469
485
|
? this.compilePlayBootstrap(input)
|
|
470
486
|
: this.compileOrderedPlayBootstrap(input, binding);
|
|
471
|
-
const compilation = compilePlayPresetCompilation(input, bootstrap, binding, this.#locale);
|
|
487
|
+
const compilation = compilePlayPresetCompilation(input, bootstrap, binding, this.#locale, runtimeToolNamesForGameplay(playGameplay(input.world.documentSnapshot)));
|
|
472
488
|
refreshMaintenanceTotals(compilation.bootstrap);
|
|
473
489
|
return compilation;
|
|
474
490
|
}
|
|
@@ -493,7 +509,12 @@ export class FileNativePromptCompiler {
|
|
|
493
509
|
...prompt,
|
|
494
510
|
});
|
|
495
511
|
};
|
|
512
|
+
const playCatalog = builtinPlayPrompts(this.#locale);
|
|
513
|
+
const gameplay = playGameplay(effective.world.documentSnapshot);
|
|
496
514
|
for (const entry of parseOrderedPlayPrompts(binding.definition.playPrompts)) {
|
|
515
|
+
// A scoped entry is absent from the request, not an empty message.
|
|
516
|
+
if (!promptScopeApplies(orderedPromptScope(entry, playCatalog), gameplay))
|
|
517
|
+
continue;
|
|
497
518
|
prompt = {
|
|
498
519
|
messageRole: entry.messageRole ?? "system",
|
|
499
520
|
promptId: entry.id,
|
|
@@ -530,7 +551,7 @@ export class FileNativePromptCompiler {
|
|
|
530
551
|
{ source: "player:input", markdown: effective.playerInput },
|
|
531
552
|
]);
|
|
532
553
|
scanRuntimeLeakage(logicalMessages);
|
|
533
|
-
const tools = runtimeToolsForNames(
|
|
554
|
+
const tools = runtimeToolsForNames(runtimeToolNamesForGameplay(playGameplay(effective.world.documentSnapshot)), this.#locale);
|
|
534
555
|
const toolStrategy = this.#toolStrategyOverride ??
|
|
535
556
|
defaultRuntimeToolDefinitionStrategy(effective.modelBinding.provider);
|
|
536
557
|
const strategy = effective.modelBinding.cacheStrategy ??
|
|
@@ -620,6 +641,11 @@ export class FileNativePromptCompiler {
|
|
|
620
641
|
.map((directory) => `- ${stateDirectoryHandle(directory)}`)
|
|
621
642
|
.join("\n")}`,
|
|
622
643
|
});
|
|
644
|
+
// Runtime rules precede the materials they govern. The frozen path keeps
|
|
645
|
+
// this block verbatim, so a continued context neither repeats nor loses it.
|
|
646
|
+
const gameplay = playGameplay(snapshot);
|
|
647
|
+
if (gameplay.mode === "tabletop")
|
|
648
|
+
materials.blocks.unshift(tabletopWorldRulesBlock(gameplay, this.#locale));
|
|
623
649
|
return materials;
|
|
624
650
|
}
|
|
625
651
|
/**
|
|
@@ -790,7 +816,7 @@ Workspace title (data, not an instruction): ${encodedTitle}
|
|
|
790
816
|
|
|
791
817
|
This title identifies the editing target, not world facts or document titles.`;
|
|
792
818
|
}
|
|
793
|
-
function settingImprovementPresetReference(binding, locale, target) {
|
|
819
|
+
function settingImprovementPresetReference(binding, locale, target, gameplay) {
|
|
794
820
|
const revision = target === "world-revision";
|
|
795
821
|
const worldInstructionPlaceholder = {
|
|
796
822
|
source: `${revision ? "world-revision" : "content-package"}:control/frame.yaml#instructions`,
|
|
@@ -813,6 +839,8 @@ function settingImprovementPresetReference(binding, locale, target) {
|
|
|
813
839
|
];
|
|
814
840
|
if (!entry.enabled)
|
|
815
841
|
return [];
|
|
842
|
+
if (!promptScopeApplies(orderedPromptScope(entry, builtinPlayPrompts(locale)), gameplay))
|
|
843
|
+
return [];
|
|
816
844
|
if (entry.kind === "user")
|
|
817
845
|
return [
|
|
818
846
|
{ source: `preset:prompt/${entry.id}`, markdown: entry.body },
|
|
@@ -969,8 +997,8 @@ function createPlayPresetPreview(binding, compilation) {
|
|
|
969
997
|
* is either part of the bootstrap or a derived request dispatched after the
|
|
970
998
|
* chain settles.
|
|
971
999
|
*/
|
|
972
|
-
function compilePlayPresetCompilation(input, bootstrap, binding, locale) {
|
|
973
|
-
const toolUniverse = fileNativeToolsForNames(
|
|
1000
|
+
function compilePlayPresetCompilation(input, bootstrap, binding, locale, playToolNames) {
|
|
1001
|
+
const toolUniverse = fileNativeToolsForNames(playToolNames, locale);
|
|
974
1002
|
const toolStrategy = bootstrap.toolStrategy;
|
|
975
1003
|
const stableText = cacheStableText(bootstrap.logicalMessages, binding.definition.playPrompts ? bootstrap.provider : undefined);
|
|
976
1004
|
const sessionBootstrap = {
|
|
@@ -1152,6 +1180,13 @@ function validateModel(model) {
|
|
|
1152
1180
|
throw new PromptCompilationError("model_max_output_invalid", "The model must provide a valid maxOutputTokens value");
|
|
1153
1181
|
}
|
|
1154
1182
|
}
|
|
1183
|
+
/**
|
|
1184
|
+
* A world or package keeps its gameplay in control/gameplay.json; a missing
|
|
1185
|
+
* file is interactive fiction. Every play universe is derived from this value.
|
|
1186
|
+
*/
|
|
1187
|
+
function playGameplay(snapshot) {
|
|
1188
|
+
return readGameplay(snapshot.files);
|
|
1189
|
+
}
|
|
1155
1190
|
function snapshotFiles(snapshot) {
|
|
1156
1191
|
return Object.fromEntries(snapshot.files.map(({ path, contents }) => [path, contents]));
|
|
1157
1192
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { historyUseGuidance } from "../../shared/history-use-guidance.js";
|
|
2
2
|
import { defaultAppLocale, } from "../../protocol/appPreferences.js";
|
|
3
|
+
import { resolutionCheckToolForScheme, resolutionToolDefinition, resolutionToolNames, } from "../play/TabletopResolution.js";
|
|
3
4
|
/**
|
|
4
5
|
* Runtime-owned prompt tool registry.
|
|
5
6
|
*
|
|
@@ -7,7 +8,12 @@ import { defaultAppLocale, } from "../../protocol/appPreferences.js";
|
|
|
7
8
|
* schemas and descriptions stay here so authors cannot fork the executable
|
|
8
9
|
* tool contract.
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Tools that every current play and authoring surface may expose. Resolution
|
|
13
|
+
* tools are registered below but never enter a surface until a per-world
|
|
14
|
+
* selection (Phase 2) includes them.
|
|
15
|
+
*/
|
|
16
|
+
export const baseRuntimeToolNames = [
|
|
11
17
|
"state_list",
|
|
12
18
|
"history_list",
|
|
13
19
|
"history_search",
|
|
@@ -21,8 +27,28 @@ export const registeredRuntimeToolNames = [
|
|
|
21
27
|
"artifact_emit",
|
|
22
28
|
"artifact_clear",
|
|
23
29
|
];
|
|
30
|
+
/** The full executable Runtime vocabulary. Exposure is chosen per surface. */
|
|
31
|
+
export const registeredRuntimeToolNames = [
|
|
32
|
+
...baseRuntimeToolNames,
|
|
33
|
+
...resolutionToolNames,
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* The tool surface of one world or package, derived only from its gameplay.
|
|
37
|
+
* Interactive fiction keeps the base surface exactly; tabletop adds the
|
|
38
|
+
* scheme's check plus auxiliary dice and arithmetic.
|
|
39
|
+
*/
|
|
40
|
+
export function runtimeToolNamesForGameplay(gameplay) {
|
|
41
|
+
if (gameplay.mode !== "tabletop")
|
|
42
|
+
return [...baseRuntimeToolNames];
|
|
43
|
+
return [
|
|
44
|
+
...baseRuntimeToolNames,
|
|
45
|
+
resolutionCheckToolForScheme(gameplay.scheme),
|
|
46
|
+
"roll_dice",
|
|
47
|
+
"calculate",
|
|
48
|
+
];
|
|
49
|
+
}
|
|
24
50
|
/** Changes only when the executable Runtime tool universe or schemas change. */
|
|
25
|
-
export const runtimeToolRegistryRevision = "runtime-tools-
|
|
51
|
+
export const runtimeToolRegistryRevision = "runtime-tools-v10";
|
|
26
52
|
export function defaultRuntimeToolDefinitionStrategy(provider) {
|
|
27
53
|
return provider === "openai_responses"
|
|
28
54
|
? "native_allowed_subset"
|
|
@@ -89,6 +115,7 @@ function createToolDefinitions(locale) {
|
|
|
89
115
|
}, ["yaml"]);
|
|
90
116
|
const markdownLocator = object({ markdown: { type: "array", items: { type: "string" }, minItems: 1 } }, ["markdown"]);
|
|
91
117
|
const definitions = {
|
|
118
|
+
...resolutionDefinitions(locale),
|
|
92
119
|
state_list: {
|
|
93
120
|
description: descriptions.state_list,
|
|
94
121
|
inputSchema: object({
|
|
@@ -237,6 +264,12 @@ function createToolDefinitions(locale) {
|
|
|
237
264
|
};
|
|
238
265
|
return definitions;
|
|
239
266
|
}
|
|
267
|
+
function resolutionDefinitions(locale) {
|
|
268
|
+
return Object.fromEntries(resolutionToolNames.map((name) => {
|
|
269
|
+
const { description, inputSchema } = resolutionToolDefinition(name, locale);
|
|
270
|
+
return [name, { description, inputSchema }];
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
240
273
|
const toolDescriptions = {
|
|
241
274
|
en: {
|
|
242
275
|
world_checkpoint: "Register a gap between events: the previous event has naturally settled at this turn's stopping point, or the scene has actually changed, suggesting that the player start a fresh context here. Arguments must be an empty object. Registration takes effect only after the subsequent nonempty tool-free final prose successfully commits, and includes that prose. A fresh context replays only committed player inputs and final narratives after this boundary, so save still-needed outcomes before registration. Consolidation and maintenance do not require this tool. Do not advance the story to register a checkpoint; registration neither switches context nor ends an in-world event. Further tool calls may follow registration; Runtime does not verify semantic completeness.",
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { validPromptCompilation, validLogicalMessages, validPromptTools, validPlayFollowup, validProviderKind, validCacheStrategy, validToolStrategy, } from "../prompt/PromptCompilationCodec.js";
|
|
2
2
|
import { validAuthoringRequests, } from "../authoring/AuthoringRequestSnapshot.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
|
|
5
|
+
import { linkExclusive } from "../FileNativeExclusiveLink.js";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
import { isDeepStrictEqual } from "node:util";
|
|
8
|
+
import { fictionGameplay } from "../../protocol/gameplay.js";
|
|
7
9
|
import { ContentWorkspace } from "../content/ContentWorkspace.js";
|
|
8
10
|
import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
|
|
9
11
|
import { isPlayPresetBinding, } from "../play/FileNativePlayPresetStore.js";
|
|
@@ -447,7 +449,9 @@ function migrateLegacySession(value, applyOutcome) {
|
|
|
447
449
|
createdAt: value.createdAt,
|
|
448
450
|
updatedAt: value.updatedAt,
|
|
449
451
|
creationRequestId: `legacy:${value.sessionId}`,
|
|
450
|
-
bootstrap: compileSettingImprovementLegacyCurrentTreeBootstrap(structuredClone(value.bootstrap), value.locale,
|
|
452
|
+
bootstrap: compileSettingImprovementLegacyCurrentTreeBootstrap(structuredClone(value.bootstrap), value.locale,
|
|
453
|
+
// Released sessions predate gameplay, and a package's mode is fixed, so they are fiction.
|
|
454
|
+
settingImprovementToolDefinitions(value.locale, fictionGameplay)),
|
|
451
455
|
modelBinding: structuredClone(value.modelBinding),
|
|
452
456
|
...(value.playPreset === undefined
|
|
453
457
|
? {}
|
|
@@ -966,13 +970,11 @@ async function publishJson(path, value) {
|
|
|
966
970
|
const handle = await open(temporary, "wx", 0o600);
|
|
967
971
|
try {
|
|
968
972
|
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
969
|
-
await handle.sync();
|
|
970
973
|
}
|
|
971
974
|
finally {
|
|
972
975
|
await handle.close();
|
|
973
976
|
}
|
|
974
977
|
await rename(temporary, path);
|
|
975
|
-
await syncDirectory(dirname(path));
|
|
976
978
|
}
|
|
977
979
|
finally {
|
|
978
980
|
await rm(temporary, { force: true });
|
|
@@ -985,14 +987,12 @@ async function publishImmutableSource(path, contents) {
|
|
|
985
987
|
const handle = await open(temporary, "wx", 0o600);
|
|
986
988
|
try {
|
|
987
989
|
await handle.writeFile(contents, "utf8");
|
|
988
|
-
await handle.sync();
|
|
989
990
|
}
|
|
990
991
|
finally {
|
|
991
992
|
await handle.close();
|
|
992
993
|
}
|
|
993
994
|
try {
|
|
994
|
-
await
|
|
995
|
-
await syncDirectory(dirname(path));
|
|
995
|
+
await linkExclusive(temporary, path);
|
|
996
996
|
}
|
|
997
997
|
catch (error) {
|
|
998
998
|
if (!isNodeError(error) || error.code !== "EEXIST")
|
|
@@ -1005,15 +1005,6 @@ async function publishImmutableSource(path, contents) {
|
|
|
1005
1005
|
await rm(temporary, { force: true });
|
|
1006
1006
|
}
|
|
1007
1007
|
}
|
|
1008
|
-
async function syncDirectory(path) {
|
|
1009
|
-
const handle = await open(path, "r");
|
|
1010
|
-
try {
|
|
1011
|
-
await handle.sync();
|
|
1012
|
-
}
|
|
1013
|
-
finally {
|
|
1014
|
-
await handle.close();
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
1008
|
function isRecord(value) {
|
|
1018
1009
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1019
1010
|
}
|
|
@@ -2,7 +2,9 @@ import { isPackageFollowupControlPath } from "../content/PackageFollowups.js";
|
|
|
2
2
|
import { renderWorldMaintenanceReport } from "../prompt/WorldMaintenanceReport.js";
|
|
3
3
|
import { listWorldDocuments as listQueryableWorldDocuments } from "../prompt/WorldMaintenanceReport.js";
|
|
4
4
|
import { defaultAppLocale } from "../../protocol/appPreferences.js";
|
|
5
|
+
import { gameplayLabel, gameplayPath, readGameplay, } from "../../protocol/gameplay.js";
|
|
5
6
|
import { isScalar, isSeq, parseDocument, stringify } from "yaml";
|
|
7
|
+
import { tabletopSchemeReceipt } from "../play/TabletopRules.js";
|
|
6
8
|
import { inspectContentPackageCurrentTree } from "../content/FileNativeContentTree.js";
|
|
7
9
|
import { worldMaterialCoverage } from "../prompt/WorldMaterialCoverage.js";
|
|
8
10
|
import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
|
|
@@ -60,7 +62,23 @@ export const settingAuthorContractExamples = {
|
|
|
60
62
|
emptyPlayerViews: emptyPlayerViewsExample,
|
|
61
63
|
playerViews: playerViewsExample,
|
|
62
64
|
};
|
|
63
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Model-facing mention of control/gameplay.json. Only a tabletop package may
|
|
67
|
+
* change the scheme, so fiction packages never see the file in their contract
|
|
68
|
+
* or tool text. Runtime validation of the path is unaffected.
|
|
69
|
+
*/
|
|
70
|
+
function gameplayFileZh(tabletop) {
|
|
71
|
+
return tabletop
|
|
72
|
+
? "、control/blocks/*.md 和 control/gameplay.json(只能修改跑团判定方案,玩法模式不可改)"
|
|
73
|
+
: " 和 control/blocks/*.md";
|
|
74
|
+
}
|
|
75
|
+
function gameplayFileEn(tabletop) {
|
|
76
|
+
return tabletop
|
|
77
|
+
? ", control/blocks/*.md, and control/gameplay.json (only a tabletop resolution scheme may change)"
|
|
78
|
+
: ", and control/blocks/*.md";
|
|
79
|
+
}
|
|
80
|
+
export function settingImprovementRuntimeContract(locale, gameplay) {
|
|
81
|
+
const tabletop = gameplay.mode === "tabletop";
|
|
64
82
|
return locale === "zh-CN"
|
|
65
83
|
? `# Runtime 设定完善对话契约
|
|
66
84
|
|
|
@@ -69,7 +87,7 @@ export function settingImprovementRuntimeContract(locale) {
|
|
|
69
87
|
- 工具写入直接修改内容包当前树。每个完整 Provider 工具响应中的调用按顺序执行,成功改动在该响应结算时原子发布,发布后立即成为内容包权威,无需另一步确认。
|
|
70
88
|
- 每个写工具调用独立结算,并按本响应中的调用顺序读取之前已接受的结果。某个调用失败只在对应工具结果中返回精确原因,不回滚同一响应内其他成功调用,也不阻止后续调用。Runtime 在写调用结算后自动运行内容树检查和真实 Prompt Preview,并把检查结果附在最后一个成功写调用的结果中。
|
|
71
89
|
- 修改既有文件前必须完整读取它。setting_list/setting_search 的 cursor,以及 setting_read 的读取授权,只属于产生它们的当前树 revision;任何其他对话或手动编辑改变当前树后,必须重新读取再写。setting_read 总是完整返回精确文件,不截断、不分页。工具只暴露逻辑路径,不暴露宿主路径。
|
|
72
|
-
- world/ 下只写 .yaml 或 .md 世界文档;专用文件只允许 opening.md、control/frame.yaml、control/player-views.yaml
|
|
90
|
+
- world/ 下只写 .yaml 或 .md 世界文档;专用文件只允许 opening.md、control/frame.yaml、control/player-views.yaml${gameplayFileZh(tabletop)},以及 control/followups.yaml 和其中引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。人物、地点、规则与当前情境放在 world/,本世界特有的主持要求放在 control/。
|
|
73
91
|
- opening.md 是玩家看见的第一页,不得替玩家决定行动、台词或内心。会继续约束首次行动的事实也必须写入自然承载它的世界文档。
|
|
74
92
|
- 新建 world 文档使用 setting_create:ref 由你提供,并同时提供路径、标题、摘要、别名与不含技术头的正文。ref 必须是唯一的 2~32 位小写 ASCII 短句柄;Runtime 自行完成技术存储。既有文档选择值会在读取时自动投影为对应的 @ref;跨文档引用和 control 中的文档选择只使用 @ref。
|
|
75
93
|
- YAML 中指向整份文档的机械引用只写成单键 map,例如 { $ref: "@alex" };普通字符串中的 @alex 仍只是文字。frame 与 player-view selector 的 document 值也写成 "@alex",局部位置另用 locator 表达。
|
|
@@ -105,7 +123,7 @@ export function settingImprovementRuntimeContract(locale) {
|
|
|
105
123
|
- 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
124
|
- 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
125
|
- 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,
|
|
126
|
+
- World documents are .yaml or .md files under world/. Special writes are limited to opening.md, control/frame.yaml, control/player-views.yaml${gameplayFileEn(tabletop)}, 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
127
|
- 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
128
|
- 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
129
|
- 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.
|
|
@@ -134,8 +152,11 @@ export function settingImprovementRuntimeContract(locale) {
|
|
|
134
152
|
- 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
153
|
- 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
154
|
}
|
|
137
|
-
export function settingImprovementToolDefinitions(locale
|
|
138
|
-
const
|
|
155
|
+
export function settingImprovementToolDefinitions(locale, gameplay) {
|
|
156
|
+
const tabletop = gameplay.mode === "tabletop";
|
|
157
|
+
const descriptions = locale === "zh-CN"
|
|
158
|
+
? toolDescriptionsZhCN(tabletop)
|
|
159
|
+
: toolDescriptionsEn(tabletop);
|
|
139
160
|
const schema = (properties, required) => ({
|
|
140
161
|
type: "object",
|
|
141
162
|
additionalProperties: false,
|
|
@@ -243,26 +264,26 @@ export function settingImprovementToolDefinitions(locale = defaultAppLocale) {
|
|
|
243
264
|
},
|
|
244
265
|
];
|
|
245
266
|
}
|
|
246
|
-
const toolDescriptionsEn = {
|
|
267
|
+
const toolDescriptionsEn = (tabletop) => ({
|
|
247
268
|
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
269
|
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
270
|
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
271
|
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:
|
|
272
|
+
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, ${tabletop ? "control/blocks/*.md, or control/gameplay.json (only a tabletop resolution scheme may change)" : "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
273
|
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
274
|
setting_move: "Move a completely read world document to a new world/ .yaml or .md logical path while preserving its identity and contents.",
|
|
254
275
|
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 = {
|
|
276
|
+
});
|
|
277
|
+
const toolDescriptionsZhCN = (tabletop) => ({
|
|
257
278
|
setting_list: "列出内容包当前树的 world 根目录或 world/ 子目录;根目录同时列出 opening 和 control 专用文件。cursor 只对同一查询和当前树 revision 有效。",
|
|
258
279
|
setting_search: "在内容包当前树的 world 文档中按原文字面搜索;within 可为 world、world/ 目录或路径或 @ref。",
|
|
259
280
|
setting_read: "按 world/ 路径或 @ref 完整读取世界文档,或按精确路径完整读取 opening/control;既有文档选择值自动投影为 @ref。结果不会截断或分页。",
|
|
260
281
|
setting_create: "创建一份 world/ 下的 .yaml 或 .md 文档;提供唯一的小写 ASCII ref、标题、摘要、别名和不含技术头的正文,Runtime 自行完成存储封装。创建成功的文档视为已经完整读取。",
|
|
261
|
-
setting_write_file:
|
|
282
|
+
setting_write_file: `整份替换已完整读取的 world/ .yaml 或 .md 正文并保留身份和元信息,或创建/替换 opening.md、control/frame.yaml、control/player-views.yaml、control/blocks/*.md${tabletop ? "、control/gameplay.json(只能修改跑团判定方案,玩法模式不可改)" : ""},以及 control/followups.yaml 和其中引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。修复损坏世界文档时同时提供 ref、title、summary、aliases,Runtime 会保留仍可恢复的存储身份。新建世界文档使用 setting_create。`,
|
|
262
283
|
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
284
|
setting_move: "把已完整读取的世界文档移动到新的 world/ .yaml 或 .md 逻辑路径,并保留身份与内容。",
|
|
264
285
|
setting_delete: "从内容包当前树永久删除一份已完整读取的 world 文档。若 currentSituation、frame slot、player-view selector 或其他文档的 $ref 仍指向它,Runtime 会拒绝并返回全部精确阻挡位置;请先删除或改向这些引用。",
|
|
265
|
-
};
|
|
286
|
+
});
|
|
266
287
|
export class SettingAuthoringTransaction {
|
|
267
288
|
#baseFiles;
|
|
268
289
|
#locale;
|
|
@@ -918,10 +939,12 @@ function writeOpaque(snapshot, call, reads, locale) {
|
|
|
918
939
|
throw new SettingAuthoringError("Special-file writes accept only path and contents");
|
|
919
940
|
const path = safePath(requiredString(call.arguments.path, "path"));
|
|
920
941
|
if (!writableOpaquePath(path))
|
|
921
|
-
throw new SettingAuthoringError(localized(locale, "Special-file writes accept only opening.md, control/frame.yaml, control/player-views.yaml,
|
|
942
|
+
throw new SettingAuthoringError(localized(locale, "Special-file writes accept only opening.md, control/frame.yaml, control/player-views.yaml, control/blocks/*.md, or control/gameplay.json", "专用文件只允许写入 opening.md、control/frame.yaml、control/player-views.yaml、control/blocks/*.md 或 control/gameplay.json"));
|
|
922
943
|
const authoredContents = requiredString(call.arguments.contents, "contents");
|
|
923
944
|
assertNoLiteralUnicodeEscapes(authoredContents, locale);
|
|
924
945
|
const contents = projectOpaqueDocumentIds(snapshot, path, authoredContents);
|
|
946
|
+
if (path === gameplayPath)
|
|
947
|
+
assertGameplayWriteKeepsMode(snapshot.files, contents, locale);
|
|
925
948
|
const next = cloneFiles(snapshot.files);
|
|
926
949
|
const existing = next.find((file) => file.path === path);
|
|
927
950
|
if (existing !== undefined && !reads.opaquePaths.has(path))
|
|
@@ -938,16 +961,36 @@ function writeOpaque(snapshot, call, reads, locale) {
|
|
|
938
961
|
});
|
|
939
962
|
rebaseReads(reads, revised);
|
|
940
963
|
reads.opaquePaths.add(path);
|
|
964
|
+
const gameplay = path === gameplayPath ? readGameplay([{ path, contents }]) : null;
|
|
941
965
|
return {
|
|
942
966
|
snapshot: revised,
|
|
943
|
-
markdown:
|
|
967
|
+
markdown: [
|
|
968
|
+
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 的精确序列化结果,否则不要再次读取。`),
|
|
969
|
+
...(gameplay === null
|
|
970
|
+
? []
|
|
971
|
+
: [
|
|
972
|
+
localized(locale, `Current gameplay: ${gameplayLabel(gameplay, locale)}.`, `当前玩法:${gameplayLabel(gameplay, locale)}。`),
|
|
973
|
+
// Mid-conversation the model learns the new scheme's rules here (ADR-0037 append-only).
|
|
974
|
+
...(gameplay.mode === "tabletop"
|
|
975
|
+
? [tabletopSchemeReceipt(gameplay, locale)]
|
|
976
|
+
: []),
|
|
977
|
+
]),
|
|
978
|
+
].join("\n\n"),
|
|
944
979
|
};
|
|
945
980
|
}
|
|
981
|
+
/** The gameplay mode is fixed; a write may only keep it or change a tabletop scheme. */
|
|
982
|
+
function assertGameplayWriteKeepsMode(files, contents, locale) {
|
|
983
|
+
const before = readGameplay(files);
|
|
984
|
+
const after = readGameplay([{ path: gameplayPath, contents }]);
|
|
985
|
+
if (before.mode !== after.mode)
|
|
986
|
+
throw new SettingAuthoringError(localized(locale, "Gameplay mode is fixed; control/gameplay.json may change only the resolution scheme of a tabletop package", "玩法模式固定;control/gameplay.json 只能修改跑团的判定方案,不能在互动式小说与跑团之间切换"));
|
|
987
|
+
}
|
|
946
988
|
function writableOpaquePath(path) {
|
|
947
989
|
return (isPackageFollowupControlPath(path) ||
|
|
948
990
|
path === "opening.md" ||
|
|
949
991
|
path === "control/frame.yaml" ||
|
|
950
992
|
path === "control/player-views.yaml" ||
|
|
993
|
+
path === gameplayPath ||
|
|
951
994
|
/^control\/blocks\/[a-z0-9][a-z0-9/_-]*\.md$/u.test(path));
|
|
952
995
|
}
|
|
953
996
|
function assertWorldWritesAuthorized(snapshotId, reads, changes, locale) {
|
|
@@ -231,11 +231,12 @@ export class SettingImprovementSession {
|
|
|
231
231
|
const package_ = await this.#content.readCurrentTreeContentPackage(packageId);
|
|
232
232
|
const bootstrap = this.#compiler.compileSettingImprovement({
|
|
233
233
|
contentPackageTitle: package_.title,
|
|
234
|
-
|
|
234
|
+
gameplay: package_.gameplay,
|
|
235
|
+
runtimeContract: settingImprovementRuntimeContract(locale, package_.gameplay),
|
|
235
236
|
authorPrompt: settingImprovementPromptForBinding(playPreset, locale),
|
|
236
237
|
playPreset,
|
|
237
238
|
modelBinding: modelBinding,
|
|
238
|
-
tools: settingImprovementToolDefinitions(locale),
|
|
239
|
+
tools: settingImprovementToolDefinitions(locale, package_.gameplay),
|
|
239
240
|
});
|
|
240
241
|
return { bootstrap, playPreset };
|
|
241
242
|
}
|
|
@@ -253,10 +254,11 @@ export class SettingImprovementSession {
|
|
|
253
254
|
]);
|
|
254
255
|
const locale = this.#locale();
|
|
255
256
|
const modelBinding = host.binding();
|
|
256
|
-
const tools = settingImprovementToolDefinitions(locale);
|
|
257
|
+
const tools = settingImprovementToolDefinitions(locale, package_.gameplay);
|
|
257
258
|
const bootstrap = this.#compiler.compileSettingImprovement({
|
|
258
259
|
contentPackageTitle: package_.title,
|
|
259
|
-
|
|
260
|
+
gameplay: package_.gameplay,
|
|
261
|
+
runtimeContract: settingImprovementRuntimeContract(locale, package_.gameplay),
|
|
260
262
|
authorPrompt: settingImprovementPromptForBinding(playPreset, locale),
|
|
261
263
|
playPreset,
|
|
262
264
|
modelBinding,
|