narraeon 0.5.2 → 0.5.4
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/dist/node/runtime/history/HistoryQuery.js +550 -0
- package/dist/node/runtime/history/HistoryRendering.js +86 -0
- package/dist/node/runtime/play/PlayCallChain.js +35 -294
- package/dist/node/runtime/play/PlayCallChainError.js +6 -0
- package/dist/node/runtime/play/PlayContinuity.js +19 -14
- package/dist/node/runtime/play/PlayDocumentTools.js +58 -125
- package/dist/node/runtime/play/PlayTraceRetention.js +264 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +97 -68
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +58 -14
- package/dist/node/shared/default-play-prompts.js +10 -6
- package/dist/node/shared/default-preset-host.js +24 -16
- package/dist/node/shared/history-use-guidance.js +6 -0
- package/dist/node/shared/ordered-play-prompts.js +7 -2
- package/dist/web/assets/index-B1Kd7NCp.js +1223 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-CVllMgyP.js +0 -1207
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { HistoryQuery, historyInputs, historySelectionMatches, } from "../history/HistoryQuery.js";
|
|
2
|
+
import { renderHistoryMessage, renderHistoryInjectionNotice, historyRoleLabel, } from "../history/HistoryRendering.js";
|
|
3
|
+
import { historyUseGuidance } from "../../shared/history-use-guidance.js";
|
|
4
|
+
import { hasLegacyHistoryTools } from "./FileNativeToolRegistry.js";
|
|
1
5
|
import { effectiveFollowupDefinitions } from "../play/OrderedFollowups.js";
|
|
2
6
|
import { legacyAuthorPrompts } from "../play/FileNativePlayPresetStore.js";
|
|
3
7
|
import { builtinAuthorPrompts, authoringMechanics, } from "../../shared/ordered-author-prompts.js";
|
|
@@ -35,20 +39,24 @@ const runtimeContracts = {
|
|
|
35
39
|
- A document marked \`full body injected\` in the material-coverage report already has its complete writable body in the request and carries write authorization. Do not read it again merely to confirm body structure, check body fields, or “be safe.” context_read additionally exposes the current title, summary, and aliases. world_patch preserves metadata fields omitted from set_metadata, so do not read merely to copy unchanged metadata. Read only when a decision depends on unseen metadata or material the report says is not covered.
|
|
36
40
|
- A YAML world_patch locator may mix map keys and zero-based array indexes; remove deletes exactly one existing key or array item. Retire an entity that should leave future frame catalogs with world_retire instead of destroying its document; retired documents remain listed, readable, referenceable, and restorable.
|
|
37
41
|
- Zero literal-search matches do not prove that a fact does not exist. Tools, directories, documents, archives, searches, matches, handles, Runtime, and failure processes are private adjudication details. Player-visible narrative must not mention internal phrases such as “nothing was found.” When information is insufficient, preserve uncertainty inside the world instead of inventing an internal process.
|
|
38
|
-
- World-write tools change only an uncommitted working copy. Their changes are not official world facts until Runtime accepts and completes the commit
|
|
42
|
+
- World-write tools change only an uncommitted working copy. Their changes are not official world facts until Runtime accepts and completes the commit.
|
|
43
|
+
|
|
44
|
+
${historyUseGuidance("en")}`,
|
|
39
45
|
operation: `# Runtime call-chain rules
|
|
40
46
|
|
|
41
47
|
- A response with no tool calls may contain player-visible story text and finish the current call-chain step.
|
|
42
|
-
- A response that calls any tool is an intermediate
|
|
48
|
+
- A response that calls any tool is an intermediate step; its text is retained in native continuation and diagnostics without being displayed or committed as final story. After all tool results, use the receipts to present this turn's still-unshown content in a later tool-free response consistent with committed state.
|
|
43
49
|
- Obey only the real tool definitions attached to this request. A tool that is not defined cannot be called.
|
|
44
50
|
- Runtime performs state commits and saves follow-up artifacts. Tool exchanges and internal processing must never appear in player-visible content.`,
|
|
45
51
|
shell: `# Runtime play boundary
|
|
46
52
|
|
|
47
|
-
A nonempty tool-free response ends the model/tool loop started by the current player submission. A response that calls any tool is an intermediate step
|
|
53
|
+
A nonempty tool-free response ends the model/tool loop started by the current player submission. A response that calls any tool is an intermediate step; its text has not yet been displayed or committed as final story. After all tool results, use the receipts to present this turn's still-unshown content in a later tool-free response consistent with committed state.
|
|
54
|
+
|
|
55
|
+
The player's next submission may choose a fresh context; the old model transcript will not enter that request. Fresh contexts include all committed original player inputs and final narratives after the last effective world_checkpoint, excluding tools, reasoning, and the opening. A checkpoint declaration takes effect only after the subsequent nonempty tool-free final prose successfully commits, including that prose in its boundary; further tool calls may follow registration. Normal replies need no checkpoint; registration neither ends an in-world event nor switches context. Author instructions decide save timing.
|
|
48
56
|
|
|
49
|
-
|
|
57
|
+
Runtime executes only real tool definitions, file validation, and authority commits. This block does not define story content, point of view, style, player agency, or state semantics.
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
${historyUseGuidance("en")}`,
|
|
52
60
|
},
|
|
53
61
|
"zh-CN": {
|
|
54
62
|
play: `# Runtime 权限边界
|
|
@@ -62,25 +70,31 @@ Runtime executes only real tool definitions, file validation, and authority comm
|
|
|
62
70
|
- 材料覆盖报告标为 \`已注入完整正文\` 的文档,其完整可写正文已经在本次请求里,写入资格同样已经具备。不要为确认正文结构、核对正文字段或“保险起见”重读它。context_read 会额外显示当前 title、summary 和 aliases;world_patch 会保留 set_metadata 中未提供的元数据字段,因此不要只为照抄未改变的元数据而读取。只有后续决策确实依赖尚未显示的元数据,或材料覆盖报告未覆盖的内容时才读。
|
|
63
71
|
- YAML world_patch 的 locator 可混合 map key 与从 0 开始的数组下标;remove 只删除一个精确的已有键或数组项。对象需要退出后续 frame catalog 时用 world_retire 退役,不要销毁文档;退役文档仍可列出、读取、引用和恢复。
|
|
64
72
|
- 字面搜索 0 命中不证明事实不存在。工具、目录、文档、档案、检索、命中、句柄、Runtime 及失败过程只供私下裁决;玩家可见叙事不得出现“没搜到/没找到资料”等内部措辞。信息不足时保持世界内的不确定性,不编造内部过程。
|
|
65
|
-
- 世界写入工具只修改尚未提交的工作副本;Runtime
|
|
73
|
+
- 世界写入工具只修改尚未提交的工作副本;Runtime 接受并完成提交前,这些修改都不是正式世界事实。
|
|
74
|
+
|
|
75
|
+
${historyUseGuidance("zh-CN")}`,
|
|
66
76
|
operation: `# Runtime 调用链规则
|
|
67
77
|
|
|
68
78
|
- 不调用工具的响应可以输出玩家可见故事正文,并结束当前调用链步骤。
|
|
69
|
-
-
|
|
79
|
+
- 只要响应调用了任何工具,它就是中间步;其中的文字保留在原生续传与诊断中,不展示或提交为最终故事。收到全部工具结果后,根据回执在后续无工具响应中呈现本轮尚未展示的内容,并与已提交状态一致。
|
|
70
80
|
- 只服从当前请求的真实工具定义;没有定义的工具不可调用。
|
|
71
81
|
- 状态提交与后置产物保存由 Runtime 执行。工具交换与内部处理过程不得混入玩家可见内容。`,
|
|
72
82
|
shell: `# Runtime 游玩边界
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
非空且不调用工具的响应会结束本次玩家提交触发的模型/工具循环。只要响应调用了任何工具,它就是工具中间步,其中的文字尚未展示或提交为最终故事。收到全部工具结果后,根据回执在后续无工具响应中呈现本轮尚未展示的内容,并与已提交状态一致。
|
|
75
85
|
|
|
76
|
-
下一次玩家提交可以选择“全新上下文”;旧模型 transcript 不会进入那个请求。新上下文会补入最近一次已生效 world_checkpoint
|
|
86
|
+
下一次玩家提交可以选择“全新上下文”;旧模型 transcript 不会进入那个请求。新上下文会补入最近一次已生效 world_checkpoint 之后的全部已提交玩家原文与最终叙事,不含工具、推理和开场白。检查点登记仅在随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文;登记后仍可继续工具调用。正常回复无需检查点,登记不结束世界内事件,也不自动切换上下文。具体保存时机由作者提示规定。
|
|
77
87
|
|
|
78
|
-
Runtime
|
|
88
|
+
Runtime 只执行真实工具定义、文件校验和权威提交;本段不规定故事、人称、文风、玩家代理权或状态语义。
|
|
89
|
+
|
|
90
|
+
${historyUseGuidance("zh-CN")}`,
|
|
79
91
|
},
|
|
80
92
|
};
|
|
81
93
|
const playCallChainToolNames = new Set([
|
|
82
94
|
"state_list",
|
|
83
95
|
"history_list",
|
|
96
|
+
"history_search",
|
|
97
|
+
"history_read",
|
|
84
98
|
"context_search",
|
|
85
99
|
"context_read",
|
|
86
100
|
"world_patch",
|
|
@@ -251,11 +265,14 @@ export class FileNativePromptCompiler {
|
|
|
251
265
|
return rendered.markdown;
|
|
252
266
|
}
|
|
253
267
|
/** The same selection used by bootstrap, without model or host instructions. */
|
|
254
|
-
inspectWorldMaterials(world) {
|
|
268
|
+
inspectWorldMaterials(world, playerInputPlacement) {
|
|
255
269
|
const frame = readYamlRecord(snapshotFiles(world.documentSnapshot)["control/frame.yaml"], "world frame");
|
|
256
270
|
requireFormat(frame, "narraeon.world-frame/v1", "world frame");
|
|
257
271
|
const coverage = [];
|
|
258
|
-
const materials = resolveContext({
|
|
272
|
+
const materials = resolveContext({
|
|
273
|
+
world,
|
|
274
|
+
...(playerInputPlacement === undefined ? {} : { playerInputPlacement }),
|
|
275
|
+
}, frame, world.documentSnapshot, coverage, this.#locale);
|
|
259
276
|
if (world.documentMaintenanceUnavailableReason !== undefined) {
|
|
260
277
|
materials.push({
|
|
261
278
|
key: "runtime:document-write-positions",
|
|
@@ -368,10 +385,24 @@ export class FileNativePromptCompiler {
|
|
|
368
385
|
*/
|
|
369
386
|
compilePlayCallChain(input, binding) {
|
|
370
387
|
const presetCompilation = this.compilePlayPreset(input, binding);
|
|
371
|
-
const
|
|
372
|
-
const
|
|
388
|
+
const frozenTools = input.frozenWorldContext?.toolUniverse ?? input.frozenWorldContext?.tools;
|
|
389
|
+
const toolUniverse = frozenTools !== undefined && hasLegacyHistoryTools(frozenTools)
|
|
390
|
+
? structuredClone(frozenTools)
|
|
391
|
+
: presetCompilation.toolUniverse.filter(({ name }) => playCallChainToolNames.has(name));
|
|
392
|
+
const initialMessages = binding.definition.playPrompts === undefined
|
|
373
393
|
? playCallChainNarrativeGuidance(presetCompilation.bootstrap.logicalMessages, binding)
|
|
374
394
|
: presetCompilation.bootstrap.logicalMessages;
|
|
395
|
+
const selectedMessages = frozenTools !== undefined && hasLegacyHistoryTools(frozenTools)
|
|
396
|
+
? initialMessages.map((message) => {
|
|
397
|
+
if (message.role !== "runtime_system")
|
|
398
|
+
return message;
|
|
399
|
+
const blocks = message.blocks.map((block) => ({
|
|
400
|
+
...block,
|
|
401
|
+
markdown: block.markdown.replace(historyUseGuidance(this.#locale), historyUseGuidance(this.#locale, true)),
|
|
402
|
+
}));
|
|
403
|
+
return { ...message, blocks, markdown: joinBlocks(blocks) };
|
|
404
|
+
})
|
|
405
|
+
: initialMessages;
|
|
375
406
|
// Instructions keep author order; initial reads follow as protocol tool results.
|
|
376
407
|
const logicalMessages = [
|
|
377
408
|
...selectedMessages.filter(({ role }) => role !== "world_context" && role !== "player_input"),
|
|
@@ -544,7 +575,7 @@ export class FileNativePromptCompiler {
|
|
|
544
575
|
: { maintenance: frozen.maintenance }),
|
|
545
576
|
});
|
|
546
577
|
}
|
|
547
|
-
const materials = this.inspectWorldMaterials(input.world);
|
|
578
|
+
const materials = this.inspectWorldMaterials(input.world, input.playerInputPlacement);
|
|
548
579
|
const snapshot = input.world.documentSnapshot;
|
|
549
580
|
const declaredDirectories = materials.coverage
|
|
550
581
|
.filter(({ slot }) => slot === "catalog")
|
|
@@ -863,7 +894,7 @@ function withoutAppendedContextGenesis(input) {
|
|
|
863
894
|
if (material.kind !== "history_message" &&
|
|
864
895
|
material.kind !== "history_commit")
|
|
865
896
|
return true;
|
|
866
|
-
const matches = Object.keys(history).filter((key) =>
|
|
897
|
+
const matches = Object.keys(history).filter((key) => historySelectionMatches(material, key));
|
|
867
898
|
// Invalid references still reach the compiler's required-slot validation.
|
|
868
899
|
return matches.length === 0 || matches.some((key) => !excluded.has(key));
|
|
869
900
|
});
|
|
@@ -872,7 +903,7 @@ function withoutAppendedContextGenesis(input) {
|
|
|
872
903
|
world: {
|
|
873
904
|
...input.world,
|
|
874
905
|
additionalMaterials,
|
|
875
|
-
history
|
|
906
|
+
history,
|
|
876
907
|
},
|
|
877
908
|
};
|
|
878
909
|
}
|
|
@@ -1276,9 +1307,15 @@ function resolveContext(input, worldFrame, snapshot, coverage, locale) {
|
|
|
1276
1307
|
// The model's own list must yield to every author slot, however the frame
|
|
1277
1308
|
// happens to order them, so resolve it last and splice it back into place.
|
|
1278
1309
|
let additionalMaterialsAt = null;
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1310
|
+
const history = new HistoryQuery("prompt-compilation", historyInputs(Object.entries(input.world.history ?? {}).map(([path, contents]) => ({
|
|
1311
|
+
path,
|
|
1312
|
+
contents,
|
|
1313
|
+
}))));
|
|
1314
|
+
const excluded = new Set(input.world.historyAlreadyAppended ?? []);
|
|
1315
|
+
const checkpointEntries = input.world.replayHistory === true
|
|
1316
|
+
? checkpointHistory(input.world.history ?? {}, input.world.narrativeCheckpoint)
|
|
1281
1317
|
: [];
|
|
1318
|
+
const replayEntries = checkpointEntries.filter(([id]) => !excluded.has(id));
|
|
1282
1319
|
for (const entry of context) {
|
|
1283
1320
|
const selectionStart = selected.length;
|
|
1284
1321
|
const slot = isRecord(entry) && isRecord(entry.slot) ? entry.slot : null;
|
|
@@ -1290,10 +1327,10 @@ function resolveContext(input, worldFrame, snapshot, coverage, locale) {
|
|
|
1290
1327
|
resolveReferenceTargets(slot, snapshot, selected, coverage, locale);
|
|
1291
1328
|
else if (slot.kind === "catalog")
|
|
1292
1329
|
resolveCatalog(slot, snapshot, selected, coverage, locale);
|
|
1293
|
-
else if (slot.kind === "history" &&
|
|
1330
|
+
else if (slot.kind === "history" && checkpointEntries.length > 0)
|
|
1294
1331
|
continue;
|
|
1295
1332
|
else if (slot.kind === "history")
|
|
1296
|
-
resolveRecentHistory(slot,
|
|
1333
|
+
resolveRecentHistory(slot, history, selected, coverage, locale, excluded);
|
|
1297
1334
|
else if (slot.kind === "additional_materials")
|
|
1298
1335
|
additionalMaterialsAt ??= {
|
|
1299
1336
|
selected: selected.length,
|
|
@@ -1318,9 +1355,13 @@ function resolveContext(input, worldFrame, snapshot, coverage, locale) {
|
|
|
1318
1355
|
source: "runtime:player-view-bindings",
|
|
1319
1356
|
markdown: bindings,
|
|
1320
1357
|
});
|
|
1321
|
-
if (
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1358
|
+
if (checkpointEntries.length > 0) {
|
|
1359
|
+
const selectedIds = new Set(selected.flatMap(({ key }) => key.startsWith("history_message:")
|
|
1360
|
+
? [key.slice("history_message:".length)]
|
|
1361
|
+
: []));
|
|
1362
|
+
const entries = replayEntries.filter(([id]) => !selectedIds.has(id));
|
|
1363
|
+
const deduplicatedReplay = checkpointReplayBlocks(input.world.history ?? {}, input.world.narrativeCheckpoint, locale, new Set([...excluded, ...selectedIds]), history);
|
|
1364
|
+
selected.push(...deduplicatedReplay.map((block, index) => ({
|
|
1324
1365
|
...block,
|
|
1325
1366
|
key: index === 0
|
|
1326
1367
|
? "history:checkpoint-notice"
|
|
@@ -1339,7 +1380,7 @@ function resolveContext(input, worldFrame, snapshot, coverage, locale) {
|
|
|
1339
1380
|
if (additionalMaterialsAt !== null) {
|
|
1340
1381
|
const withMaterials = [...selected];
|
|
1341
1382
|
const materialCoverage = [];
|
|
1342
|
-
resolveAdditionalMaterials(input.world.additionalMaterials, snapshot,
|
|
1383
|
+
resolveAdditionalMaterials(input.world.additionalMaterials, snapshot, history, withMaterials, materialCoverage, locale, excluded, input.playerInputPlacement === "append");
|
|
1343
1384
|
selected.splice(additionalMaterialsAt.selected, 0, ...withMaterials.slice(selected.length).map((material) => ({
|
|
1344
1385
|
...material,
|
|
1345
1386
|
...(additionalMaterialsAt?.slotId === undefined
|
|
@@ -1518,7 +1559,7 @@ function validCatalogDirectory(directory) {
|
|
|
1518
1559
|
* every detail into a world document, so recent history remains an explicit
|
|
1519
1560
|
* source alongside the current state.
|
|
1520
1561
|
*/
|
|
1521
|
-
function resolveRecentHistory(slot, history, selected, coverage, locale) {
|
|
1562
|
+
function resolveRecentHistory(slot, history, selected, coverage, locale, excluded) {
|
|
1522
1563
|
const recent = Number(slot.recent ?? 2);
|
|
1523
1564
|
if (!Number.isInteger(recent) || recent < 1 || recent > 32)
|
|
1524
1565
|
throw new PromptCompilationError("world_frame_invalid", "history slot recent must be an integer from 1 to 32");
|
|
@@ -1526,15 +1567,15 @@ function resolveRecentHistory(slot, history, selected, coverage, locale) {
|
|
|
1526
1567
|
// in commit order. Its keys are semantic message IDs, not sortable file
|
|
1527
1568
|
// names: lexical sorting would put `message.genesis` after every numbered
|
|
1528
1569
|
// message and `message.9` after `message.15`.
|
|
1529
|
-
const ordered =
|
|
1530
|
-
const chosen = ordered.slice(-recent);
|
|
1570
|
+
const ordered = history.messages.filter(({ isOpening }) => !isOpening);
|
|
1571
|
+
const chosen = ordered.slice(-recent).filter(({ id }) => !excluded.has(id));
|
|
1531
1572
|
if (chosen.length === 0) {
|
|
1532
1573
|
selected.push({
|
|
1533
1574
|
key: "history:empty",
|
|
1534
1575
|
source: "slot:history:empty",
|
|
1535
1576
|
markdown: locale === "zh-CN"
|
|
1536
|
-
?
|
|
1537
|
-
:
|
|
1577
|
+
? `# 最近已提交对话\n\n${renderHistoryInjectionNotice(history.messages.length, [], "recent", locale)}\n(本次没有可注入的非开场白原文:可能只有开场白,或近期材料已在原生对话中。无需为寻找上一条记录例行调用历史检索工具。)`
|
|
1578
|
+
: `# Recent committed conversation\n\n${renderHistoryInjectionNotice(history.messages.length, [], "recent", locale)}\n(No non-opening originals selected for injection: only the opening exists, or recent entries are already in native conversation. Do not call history tools merely to look for a previous message.)`,
|
|
1538
1579
|
});
|
|
1539
1580
|
coverage.push({
|
|
1540
1581
|
slot: "history",
|
|
@@ -1545,27 +1586,35 @@ function resolveRecentHistory(slot, history, selected, coverage, locale) {
|
|
|
1545
1586
|
});
|
|
1546
1587
|
return;
|
|
1547
1588
|
}
|
|
1548
|
-
|
|
1589
|
+
const missing = chosen.filter(({ id }) => overlappingSelection(selected, `history_message:${id}`) === null);
|
|
1590
|
+
if (missing.length > 0)
|
|
1591
|
+
selected.push({
|
|
1592
|
+
key: "history:recent-notice",
|
|
1593
|
+
source: "slot:history:notice",
|
|
1594
|
+
markdown: renderHistoryInjectionNotice(history.messages.length, missing, "recent", locale),
|
|
1595
|
+
});
|
|
1596
|
+
for (const [index, message] of missing.entries()) {
|
|
1597
|
+
const ref = message.id;
|
|
1549
1598
|
const key = `history_message:${ref}`;
|
|
1550
1599
|
if (overlappingSelection(selected, key) !== null)
|
|
1551
1600
|
continue;
|
|
1552
1601
|
selected.push({
|
|
1553
1602
|
key,
|
|
1554
1603
|
source: `slot:history:${ref}`,
|
|
1555
|
-
markdown: renderHistoryMessage(
|
|
1604
|
+
markdown: renderHistoryMessage(message, locale),
|
|
1556
1605
|
});
|
|
1557
1606
|
coverage.push({
|
|
1558
1607
|
slot: "history",
|
|
1559
1608
|
source: locale === "zh-CN"
|
|
1560
|
-
? `${
|
|
1561
|
-
: `${
|
|
1609
|
+
? `${historyRoleLabel(message, locale)}(最近记录 ${index + 1}/${chosen.length})`
|
|
1610
|
+
: `${historyRoleLabel(message, locale)} (recent ${index + 1}/${chosen.length})`,
|
|
1562
1611
|
status: "resolved",
|
|
1563
1612
|
complete: true,
|
|
1564
1613
|
continuation: "history_list",
|
|
1565
1614
|
});
|
|
1566
1615
|
}
|
|
1567
1616
|
}
|
|
1568
|
-
function resolveAdditionalMaterials(materials, snapshot, history, selected, coverage, locale) {
|
|
1617
|
+
function resolveAdditionalMaterials(materials, snapshot, history, selected, coverage, locale, excluded, excludeOpening) {
|
|
1569
1618
|
if (materials.length > 32)
|
|
1570
1619
|
throw new PromptCompilationError("material_limit_exceeded", "Additional material exceeds 32 items");
|
|
1571
1620
|
for (const material of materials) {
|
|
@@ -1580,21 +1629,27 @@ function resolveAdditionalMaterials(materials, snapshot, history, selected, cove
|
|
|
1580
1629
|
const ref = material.kind === "history_message"
|
|
1581
1630
|
? material.message
|
|
1582
1631
|
: material.commit;
|
|
1583
|
-
const matches =
|
|
1632
|
+
const matches = history.messages.filter(({ id }) => historySelectionMatches(material, id));
|
|
1584
1633
|
if (matches.length === 0)
|
|
1585
1634
|
throw new PromptCompilationError("required_slot_missing", `Additional history material does not exist: ${ref}`);
|
|
1586
|
-
const missing = matches.filter((
|
|
1635
|
+
const missing = matches.filter(({ id, isOpening }) => !excluded.has(id) &&
|
|
1636
|
+
!(excludeOpening && isOpening) &&
|
|
1637
|
+
!selected.some(({ key }) => key === `history_message:${id}`));
|
|
1587
1638
|
if (missing.length === 0)
|
|
1588
1639
|
continue;
|
|
1589
|
-
selected.push(
|
|
1590
|
-
key: `
|
|
1591
|
-
source:
|
|
1592
|
-
markdown:
|
|
1640
|
+
selected.push({
|
|
1641
|
+
key: `history:additional-notice:${selected.length}`,
|
|
1642
|
+
source: "slot:additional_materials:history-notice",
|
|
1643
|
+
markdown: renderHistoryInjectionNotice(history.messages.length, missing, "additional", locale),
|
|
1644
|
+
}, ...missing.map((message) => ({
|
|
1645
|
+
key: `history_message:${message.id}`,
|
|
1646
|
+
source: `slot:additional_materials:${message.id}`,
|
|
1647
|
+
markdown: renderHistoryMessage(message, locale),
|
|
1593
1648
|
})));
|
|
1594
1649
|
coverage.push({
|
|
1595
1650
|
slot: "additional_materials",
|
|
1596
1651
|
source: matches.length === 1
|
|
1597
|
-
?
|
|
1652
|
+
? historyRoleLabel(matches[0], locale)
|
|
1598
1653
|
: locale === "zh-CN"
|
|
1599
1654
|
? `已选历史提交(${matches.length} 条)`
|
|
1600
1655
|
: `selected history commit (${matches.length} messages)`,
|
|
@@ -1605,32 +1660,6 @@ function resolveAdditionalMaterials(materials, snapshot, history, selected, cove
|
|
|
1605
1660
|
}
|
|
1606
1661
|
}
|
|
1607
1662
|
}
|
|
1608
|
-
function historyMaterialMatches(material, key) {
|
|
1609
|
-
if (material.kind === "history_message")
|
|
1610
|
-
return (historyMaterialIdentity(key) === historyMaterialIdentity(material.message));
|
|
1611
|
-
const ref = material.commit;
|
|
1612
|
-
return ref.startsWith("commit:")
|
|
1613
|
-
? historyMaterialIdentity(key).startsWith(`message.${ref.slice(7)}.`)
|
|
1614
|
-
: ref === "genesis"
|
|
1615
|
-
? historyMaterialIdentity(key).startsWith("message.genesis.")
|
|
1616
|
-
: key.startsWith(ref.replace(/^@?history-commit-/u, "history-message-").concat("-"));
|
|
1617
|
-
}
|
|
1618
|
-
function historyMaterialIdentity(ref) {
|
|
1619
|
-
const value = ref.replace(/^@?(?:history-message-)?/u, "");
|
|
1620
|
-
return (/(?:^|\.)(message\.(?:genesis|[0-9]+)(?:\.[0-9]+)?\.(?:player|narrator))$/u.exec(value)?.[1] ?? value);
|
|
1621
|
-
}
|
|
1622
|
-
function renderHistoryMessage(ref, text, locale) {
|
|
1623
|
-
return `## ${historyMessageLabel(ref, locale)}\n\n${text.trim()}`;
|
|
1624
|
-
}
|
|
1625
|
-
function historyMessageLabel(ref, locale) {
|
|
1626
|
-
if (ref.endsWith("message.genesis.narrator"))
|
|
1627
|
-
return locale === "zh-CN" ? "开场白" : "Opening";
|
|
1628
|
-
if (/(?:^|\.)message\.[^.]+(?:\.[0-9]+)?\.player$|(?:^|-)player(?:-|$)/u.test(ref))
|
|
1629
|
-
return locale === "zh-CN" ? "玩家原文" : "Player input";
|
|
1630
|
-
if (/(?:^|\.)message\.[^.]+(?:\.[0-9]+)?\.narrator$|(?:^|-)narrator(?:-|$)/u.test(ref))
|
|
1631
|
-
return locale === "zh-CN" ? "主持叙事" : "Host narrative";
|
|
1632
|
-
return locale === "zh-CN" ? "已提交消息" : "Committed message";
|
|
1633
|
-
}
|
|
1634
1663
|
function addNodeSelection(target, locator, slot, required, snapshot, selected, coverage, dedupe = false, locale = defaultAppLocale) {
|
|
1635
1664
|
let exactLocator = null;
|
|
1636
1665
|
if (isRecord(locator) && Array.isArray(locator.yaml)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { historyUseGuidance } from "../../shared/history-use-guidance.js";
|
|
1
2
|
import { defaultAppLocale, } from "../../protocol/appPreferences.js";
|
|
2
3
|
/**
|
|
3
4
|
* Runtime-owned prompt tool registry.
|
|
@@ -9,6 +10,8 @@ import { defaultAppLocale, } from "../../protocol/appPreferences.js";
|
|
|
9
10
|
export const registeredRuntimeToolNames = [
|
|
10
11
|
"state_list",
|
|
11
12
|
"history_list",
|
|
13
|
+
"history_search",
|
|
14
|
+
"history_read",
|
|
12
15
|
"context_search",
|
|
13
16
|
"context_read",
|
|
14
17
|
"world_patch",
|
|
@@ -19,7 +22,7 @@ export const registeredRuntimeToolNames = [
|
|
|
19
22
|
"artifact_clear",
|
|
20
23
|
];
|
|
21
24
|
/** Changes only when the executable Runtime tool universe or schemas change. */
|
|
22
|
-
export const runtimeToolRegistryRevision = "runtime-tools-
|
|
25
|
+
export const runtimeToolRegistryRevision = "runtime-tools-v9";
|
|
23
26
|
export function defaultRuntimeToolDefinitionStrategy(provider) {
|
|
24
27
|
return provider === "openai_responses"
|
|
25
28
|
? "native_allowed_subset"
|
|
@@ -95,17 +98,45 @@ function createToolDefinitions(locale) {
|
|
|
95
98
|
}, ["parent"]),
|
|
96
99
|
},
|
|
97
100
|
history_list: {
|
|
98
|
-
description: descriptions.history_list,
|
|
101
|
+
description: descriptions.history_list + " " + historyUseGuidance(locale),
|
|
99
102
|
inputSchema: object({
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
before: string,
|
|
104
|
+
after: string,
|
|
102
105
|
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
103
|
-
|
|
106
|
+
cursor: string,
|
|
107
|
+
}, []),
|
|
108
|
+
},
|
|
109
|
+
history_search: {
|
|
110
|
+
description: descriptions.history_search + " " + historyUseGuidance(locale),
|
|
111
|
+
inputSchema: object({
|
|
112
|
+
queries: {
|
|
113
|
+
type: "array",
|
|
114
|
+
minItems: 1,
|
|
115
|
+
maxItems: 5,
|
|
116
|
+
items: { type: "string", minLength: 1, maxLength: 256 },
|
|
117
|
+
},
|
|
118
|
+
match: { enum: ["any", "all"] },
|
|
119
|
+
order: { enum: ["newest_first", "oldest_first"] },
|
|
120
|
+
caseSensitive: { type: "boolean" },
|
|
121
|
+
after: string,
|
|
122
|
+
before: string,
|
|
123
|
+
limit: { type: "integer", minimum: 1, maximum: 50 },
|
|
124
|
+
cursor: string,
|
|
125
|
+
}, []),
|
|
126
|
+
},
|
|
127
|
+
history_read: {
|
|
128
|
+
description: descriptions.history_read + " " + historyUseGuidance(locale),
|
|
129
|
+
inputSchema: object({
|
|
130
|
+
latest: { type: "integer", minimum: 1, maximum: 21 },
|
|
131
|
+
ref: string,
|
|
132
|
+
before: { type: "integer", minimum: 0, maximum: 20 },
|
|
133
|
+
after: { type: "integer", minimum: 0, maximum: 20 },
|
|
134
|
+
}, []),
|
|
104
135
|
},
|
|
105
136
|
context_search: {
|
|
106
137
|
description: descriptions.context_search,
|
|
107
138
|
inputSchema: object({
|
|
108
|
-
source: {
|
|
139
|
+
source: { const: "state" },
|
|
109
140
|
query: { type: "string", minLength: 1, maxLength: 256 },
|
|
110
141
|
caseSensitive: { type: "boolean" },
|
|
111
142
|
within: string,
|
|
@@ -208,11 +239,13 @@ function createToolDefinitions(locale) {
|
|
|
208
239
|
}
|
|
209
240
|
const toolDescriptions = {
|
|
210
241
|
en: {
|
|
211
|
-
world_checkpoint: "
|
|
242
|
+
world_checkpoint: "Register a consolidation declaration and a history-replay boundary. No arguments. Registration takes effect only after the subsequent nonempty tool-free final prose successfully commits, and includes that prose. A fresh context replays committed player inputs and final narratives after this boundary. The player may choose a fresh context; registration neither switches context nor ends an in-world event. Normal replies need no checkpoint. Further tool calls may follow registration; Runtime does not verify semantic completeness.",
|
|
212
243
|
state_list: "List document and state directory handles known to Runtime. parent must be @dir-/ for the state root or an @dir-* handle from the initial directory index or an earlier state_list result; no preliminary listing is needed for an already supplied handle. Catalog directories declared by the frozen world frame remain listed and usable when empty. A cursor is valid only for the same state snapshot, parent, and limit. Pass returned document handles to context_read. This tool never lists committed history.",
|
|
213
|
-
history_list: "
|
|
214
|
-
|
|
215
|
-
|
|
244
|
+
history_list: "Browse committed history on the current timeline. Omit anchors for the latest page; use only before or after with a Runtime ref (including refs in injected history) for the immediately adjacent page, excluding the anchor. Every page is oldest to newest. limit defaults to 20 (1–100 messages). Previews are only the first 120 Unicode code points. Use independent earlierCursor/laterCursor with {cursor} alone; no first-query parameters on continuation. Cursors bind world, tool, query and immutable ordered history. After history_changed rerun the query. Opening messages are queryable. Read full originals with history_read; positions are snapshot locations, not identities or turns.",
|
|
245
|
+
history_search: "Search committed originals on the current timeline with queries: 1–5 nonempty literal strings (max 256 code points each); no query alias or semantic filters. match defaults to any, or all within one message. NFKC normalization always applies; caseSensitive defaults to false (locale-und case folding), normalized duplicate queries collapse. order defaults to newest_first, or oldest_first. limit defaults to 10 (1–50 matching messages). Optional after/before refs exclude timeline boundaries independently of order; after must precede before. Each result has at most 3 partial original fragments, query occurrence counts and complete-original length. Read a hit and nearby context with history_read. Continue with {cursor} alone; history_changed requires a new query.",
|
|
246
|
+
history_read: "Read complete committed originals on the current timeline, never summarized, truncated or body-paginated. Use {latest:N} alone (1–21 messages), or {ref,before,after} with defaults before=after=0 and sum at most 20; do not mix modes. Ref may come directly from Runtime prompt injections or tools without listing first. Windows are always oldest to newest, including the anchor. earlier/later are immediately outside this window and can be the next ref with before/after to avoid rereading. Boundaries and empty history are normal. Counts are messages, not turns; 21 long messages can produce large output.",
|
|
247
|
+
context_search: "Search literal source text in state documents. Use source=state; committed history uses history_search. Zero matches means only that the supplied text had no literal match; it does not prove that a world fact does not exist. Player-visible narrative must not recount the search process. within accepts only an @document or @dir-* handle returned by Runtime.",
|
|
248
|
+
context_read: "Read the complete content of an @document or @node handle previously returned by Runtime. The read heading, metadata boundaries, body boundaries, scope, and completion marker in the result are not source text. set_metadata may include only the fields that must change; Runtime preserves omitted metadata fields. Update the writable body between the boundaries with locators. YAML bodies omit the $document technical header; Markdown bodies begin at the document's level-one heading. Results are never truncated or paginated. Never pass file paths, natural-language names, or an invented world/... value.",
|
|
216
249
|
world_patch: 'Update a document that has been read precisely or whose complete body was injected with write authorization. target must be an @short-ref. When title, summary, or aliases are stale, use set_metadata and include only the title, summary, or aliases that must change. At least one is required; Runtime preserves omitted fields from the current candidate document. A YAML edit uses locator:{yaml:["field","child"]}, for example {op:"replace",locator:{yaml:["status"]},value:"new value"}. Locator segments address map keys or a zero-based array index; remove deletes exactly the existing key or array item at its locator. A YAML value may reference a whole document only as {$ref:"@short-ref"}; the short reference must come from a Runtime list, read, or create result. Never invent a document id. If no handle is available, use ordinary text when semantically correct or call world_create first and use its returned @short-ref. To add an item to the end of an existing sequence, use append with the locator pointing to that sequence; add creates only a map key or list index that does not exist. A Markdown locator excludes the document level-one heading: {markdown:["Responsibilities"]} points exactly to ## Responsibilities, and replace_section.markdown must begin with the same heading at the same level. Use replace_preamble for text after the level-one heading and before the first level-two heading; send only that text. Use replace_body to replace the whole Markdown body and retain the original level-one heading. Success reports only whether the document changed and does not echo the body. Call context_read again only when a later decision depends on Runtime\'s exact serialized body or current metadata. Do not use path, JSON Pointer, set, or a file name.',
|
|
217
250
|
world_create: 'Create a document inside a Runtime-known state directory. parent must be an @dir-* handle supplied by the initial directory index or state_list, such as @dir-/characters; catalog directories declared by the frozen world frame remain available when empty, and @dir-/ is the root. A syntactically valid but unknown directory is rejected. refHint is a lowercase ASCII short-reference hint, not a world/ path. A YAML body may reference a whole document only as {$ref:"@short-ref"}, and the short reference must come from a Runtime list, read, or create result. Never invent a document id.',
|
|
218
251
|
world_retire: "Retire or restore an exact state document without deleting its narrative record. target must be an @short-ref that was read precisely or injected completely. retired:true removes the document from future frame catalog injection but keeps it listed, searchable, readable, referenceable, and restorable; retired:false restores catalog participation. This changes catalog participation, not explicit full-document injection or character activity; use world_patch for ordinary location or field changes.",
|
|
@@ -220,11 +253,13 @@ const toolDescriptions = {
|
|
|
220
253
|
artifact_clear: "Clear the projection of an artifact that this follow-up declared in advance and explicitly allows to be cleared. The call cannot choose a channel, key, or any other output contract.",
|
|
221
254
|
},
|
|
222
255
|
"zh-CN": {
|
|
223
|
-
world_checkpoint: "
|
|
256
|
+
world_checkpoint: "登记整理声明与历史重放边界,参数为空对象。仅在随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文;全新上下文补入边界之后的已提交玩家原文与最终叙事。玩家可自行选择全新上下文,登记不切换上下文,也不结束世界内事件。正常回复无需检查点,登记后仍可继续工具调用;Runtime 不认证语义整理完整。",
|
|
224
257
|
state_list: "列出 Runtime 已知的文档与状态目录句柄。状态根目录的 parent 使用 @dir-/;继续下级时使用初始目录索引或 state_list 返回的 @dir-*;已有句柄无需再次列目录确认。冻结世界 frame 声明的 catalog 目录即使为空,也会继续列出并可用。cursor 只对同一状态快照、parent 和 limit 有效。返回的文档句柄交给 context_read;本工具不列出已提交历史。",
|
|
225
|
-
history_list: "
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
history_list: "浏览当前时间线已提交历史。无锚点时取最新一页;before 或 after 只能选一个,取锚点紧邻前/后的一页且不含锚点。页内始终由旧到新。limit 默认20,整数1–100。预览仅含前120个Unicode码点;完整原文用history_read。earlierCursor/laterCursor独立翻页,续页只传{cursor},不可混用首次参数。游标绑定世界、工具、查询和有序历史快照;history_changed须重新查询。开场白可查询。提示词注入中的ref可直接使用,无需再次列举;位置不是身份或回合数。",
|
|
259
|
+
history_search: "搜索当前时间线已提交原文。queries必填:1–5个非空字面词,每词最多256个Unicode码点,不接受query别名或语义筛选。match默认any,all限于同一消息。始终NFKC规范化,caseSensitive默认false(locale-und大小写折叠),规范化重复词合并。order默认newest_first,可选oldest_first;limit默认10,整数1–50条命中消息。after/before始终按时间线先后排除边界,after必须早于before,与结果排序无关。每项最多3个部分原文片段、逐词命中次数及完整原文长度;可用history_read读取前后文。续页只传{cursor};history_changed须重新查询。",
|
|
260
|
+
history_read: "完整读取当前时间线已提交原文,无摘要、无截断、无正文分页。{latest:N}单独读取最近1–21条;或{ref,before,after},before/after默认0且合计最多20;两模式互斥。ref可直接使用Runtime提示词注入或工具结果的历史句柄,不必先列举。窗口始终由旧到新且包含锚点。earlier/later是窗口外紧邻句柄,可作为下一次ref并配before/after避免重复读取;边界与空历史正常。数量单位是消息而非回合;21条长消息仍可能产生大量输出。",
|
|
261
|
+
context_search: "在状态文档原文字面中搜索,source只能为state;已提交历史使用history_search。0 命中只表示给定文本没有字面命中,不证明世界事实不存在;玩家可见叙事不得复述搜索过程。within 只能使用 Runtime 已返回的 @文档或 @dir-* 句柄。",
|
|
262
|
+
context_read: "完整读取 Runtime 先前返回的 @文档或@节点句柄。结果中的读取标题、元信息边界、正文边界、范围和完整标记不属于源文;set_metadata 可以只提交需要改变的字段,Runtime 会保留未提供的元数据字段。中间的可写正文用 locator 更新。YAML 正文不含 $document 技术头,Markdown 正文从文档 # 一级标题开始。结果不会截断或分页。不要传文件路径、自然语言名称或自行拼造的 world/...。",
|
|
228
263
|
world_patch: '更新已精确读取,或已随写入资格注入完整正文的文档。target 必须使用 @短引用;文档 title、summary 或 aliases 过时时使用 set_metadata,只提供需要改变的 title、summary 或 aliases。至少提供一项;未提供的字段由 Runtime 从当前候选文档保留。YAML edit 使用 locator:{yaml:["字段","子字段"]},例如 {op:"replace",locator:{yaml:["情况"]},value:"新值"}。locator 的每一段可以定位 map key 或从 0 开始的数组下标;remove 只删除 locator 精确指向的已有键或数组项。YAML value 需要引用整份文档时只能写 {$ref:"@短引用"},短引用必须来自 Runtime 的 list、read 或 create 结果;不得自行编造文档 id。没有可用句柄时,根据语义使用普通文本,或先 world_create 后使用其返回的 @短引用。向已存在的 sequence 末尾加一项必须使用 append 并把 locator 指向该 sequence;add 只创建尚不存在的 map key 或 list index。Markdown locator 不包含文档 # 一级标题:{markdown:["职责"]} 精确指向 ## 职责,replace_section.markdown 必须从同级同名标题开始。修改 # 标题下、第一个 ## 前的文字用 replace_preamble(只传该段文字);替换整个 Markdown 正文用 replace_body,且 markdown 必须保留原 # 一级标题。成功结果只报告文档是否发生变化,不回显正文;只有后续决策依赖 Runtime 序列化后的精确正文或当前元数据时才重新 context_read。不要使用 path、JSON Pointer、set 或文件名。',
|
|
229
264
|
world_create: '在 Runtime 已知的状态目录中创建文档。parent 必须是初始目录索引或 state_list 列出的 @dir-*,例如 @dir-/characters;冻结世界 frame 声明的 catalog 目录为空时仍然可用,根目录使用 @dir-/。只有句柄形式正确但 Runtime 并不知道的目录会被拒绝。refHint 是小写 ASCII 短引用,不要传 world/ 路径。YAML body 需要引用整份文档时只能写 {$ref:"@短引用"},且短引用必须来自 Runtime 的 list、read 或 create 结果;不得自行编造文档 id。',
|
|
230
265
|
world_retire: "让一份精确状态文档退役或恢复,而不销毁叙事记录。target 必须是已精确读取或已完整注入的 @短引用。retired:true 会让文档不再进入后续 frame catalog 注入,但仍可列出、搜索、读取、引用和恢复;retired:false 恢复 catalog 参与。它只改变 catalog 参与,不取消显式全文注入、不冻结人物活动;普通位置或字段变化仍用 world_patch。",
|
|
@@ -232,3 +267,12 @@ const toolDescriptions = {
|
|
|
232
267
|
artifact_clear: "清除本次后置请求预先声明且允许 clear 的产物投影;不能选择频道、key 或其他输出契约。",
|
|
233
268
|
},
|
|
234
269
|
};
|
|
270
|
+
export function hasLegacyHistoryTools(tools) {
|
|
271
|
+
return tools.some(({ name, inputSchema }) => {
|
|
272
|
+
const schema = inputSchema;
|
|
273
|
+
return (name === "context_list" ||
|
|
274
|
+
(name === "history_list" && schema.properties?.order !== undefined) ||
|
|
275
|
+
(name === "context_search" &&
|
|
276
|
+
schema.properties?.source?.enum?.includes("history") === true));
|
|
277
|
+
});
|
|
278
|
+
}
|
|
@@ -15,11 +15,13 @@ export const defaultNarrationPromptZhCN = `# 玩家可见叙事规则
|
|
|
15
15
|
|
|
16
16
|
本轮新发生且玩家能感知的结果,要成为具体可读的事:物品怎样递接、人物如何离开、态度变化怎样在话语或行动中显露。文档中补录旧结果、去重和整理不要求再次演出。界面数值不能代替事件表现,叙事也不能与当前值冲突。未向玩家表露的判断和场外事实无需用旁白揭露。
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
具体表演细节随最终叙事保存,可按需追溯;不要假定每次请求自动带着全部旧段落。必须持续保存的结果按状态维护政策处理,不能只留在检查点覆盖的最终原文中。
|
|
19
19
|
|
|
20
20
|
## 停在哪里
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
先按本次玩家授权和互动需要确定停笔位置。持续活动中,新反馈会影响玩家如何接话、调整或继续时,呈现反馈并留下回应机会,即使不涉及重大决定。开始按腿、教学或交谈不默认授权包办全过程;普通走路、敲门、递送的连带动作可以自然完成,明确要求略过过程或做到某个终点时则在授权内推进。
|
|
23
|
+
|
|
24
|
+
当前行动与反应形成有内容的局面便可以停笔;具体动作、原话或环境变化都可以承接下一段,有意义的沉默、独处、休息和等待也可以自然结束。回复结束与截至此刻整理完成都不要求世界内事件结束,检查点适应停笔位置。不要为了结尾形式让 NPC 硬添动作、邀请或承诺。
|
|
23
25
|
|
|
24
26
|
避免“轮到你了”“你打算怎么做”及把同一句套话改成“众人等你开口”的机械交接,也不要在结尾列选项或重复总结局势。真正发生的注视或等待可以描写,不必强行消除。
|
|
25
27
|
|
|
@@ -29,7 +31,7 @@ export const defaultNarrationPromptZhCN = `# 玩家可见叙事规则
|
|
|
29
31
|
|
|
30
32
|
本次只有世界外查询或维护时,按请求简洁回答或说明整理结果,不为满足小说形式添加动作、对话或时间推进。混合请求中的维护要求不成为角色台词或行动。
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
玩家的决定与回应仍归玩家;表现自然感官与动作细节不能替他表达立场、感情、同意或承诺。已经提交的行动不能用一句普通改口当作从未发生。
|
|
33
35
|
`;
|
|
34
36
|
export const defaultNarrationPromptEn = `# Player-visible narrative rules
|
|
35
37
|
|
|
@@ -41,11 +43,13 @@ When there is new player input, naturally stage its explicit action or dialogue
|
|
|
41
43
|
|
|
42
44
|
Make new perceivable outcomes this turn concrete: an object handed over, a person leaving, an attitude expressed through words or action. Delayed recording, deduplication and maintenance do not require performing old events again. Interface values do not replace dramatization, and prose must agree with current values. Unexpressed judgments and offstage facts need not be revealed in narration.
|
|
43
45
|
|
|
44
|
-
Specific performance details are saved with final narrative and can be retrieved when needed; do not assume every request automatically includes every earlier passage. Handle required durable results under the state-maintenance policy instead of leaving them only in a checkpoint
|
|
46
|
+
Specific performance details are saved with final narrative and can be retrieved when needed; do not assume every request automatically includes every earlier passage. Handle required durable results under the state-maintenance policy instead of leaving them only in the final prose covered by a checkpoint.
|
|
45
47
|
|
|
46
48
|
## Where to stop
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
First choose where to stop from the player's authorization and the interaction's needs. During an ongoing activity, when new feedback could affect how the player replies, adjusts or continues, present it and leave room to respond even without a consequential decision. Starting a leg massage, lesson or conversation does not by itself authorize the entire activity; ordinary supporting actions in walking, knocking or delivering can complete naturally. Explicit requests to skip the process or reach an endpoint allow advancement within that authorization.
|
|
51
|
+
|
|
52
|
+
You may stop once action and reactions form a meaningful situation. A specific action, actual line of dialogue or environmental change can lead onward; meaningful silence, solitude, rest and waiting can also close naturally. Ending a reply or consolidating records through this moment does not require ending the in-world event; fit checkpoint maintenance to the stopping point. Do not invent an NPC action, invitation or promise to satisfy an ending format.
|
|
49
53
|
|
|
50
54
|
Avoid mechanical handoffs such as “Your turn”, “What do you do?” or their stock substitute “Everyone waits for you to speak”. Do not end by listing options or repeating a situation summary. Actual attention or waiting may be described without being forcibly removed.
|
|
51
55
|
|
|
@@ -55,7 +59,7 @@ In-world narrative uses only the novel's prose, in several natural paragraphs if
|
|
|
55
59
|
|
|
56
60
|
If this request is solely out-of-world inquiry or maintenance, answer or explain the consolidation briefly as requested without adding actions, dialogue or time passage to satisfy novel form. Maintenance instructions within a mixed request do not become character speech or actions.
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
Decisions and responses belong to the player. Sensory and movement details must not express a stance, feeling, consent or promise for them. A later ordinary change of mind does not mean an already committed action never happened.
|
|
59
63
|
`;
|
|
60
64
|
export const defaultNarrationPrompt = defaultNarrationPromptEn;
|
|
61
65
|
export function defaultNarrationPromptForLocale(locale) {
|