narraeon 0.7.10 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/node/protocol/modelConnections.js +15 -0
  2. package/dist/node/protocol/modelUsage.js +4 -0
  3. package/dist/node/protocol/sillyTavernContent.js +1 -0
  4. package/dist/node/protocol/v1.js +75 -0
  5. package/dist/node/runtime/V1Runtime.js +71 -1
  6. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
  7. package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
  8. package/dist/node/runtime/authoring/ModelToolConversation.js +47 -22
  9. package/dist/node/runtime/content/ContentWorkspace.js +3 -0
  10. package/dist/node/runtime/content/SillyTavernContent.js +796 -0
  11. package/dist/node/runtime/model/AiFailureLog.js +8 -1
  12. package/dist/node/runtime/model/FileNativeModelAdapters.js +610 -18
  13. package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
  14. package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
  15. package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
  16. package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
  17. package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
  18. package/dist/node/runtime/model/ProviderStream.js +4 -2
  19. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
  20. package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
  21. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +150 -10
  22. package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
  23. package/dist/node/runtime/play/PlayAdvanceLifecycle.js +33 -9
  24. package/dist/node/runtime/play/PlayCallChain.js +76 -4
  25. package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
  26. package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
  27. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +112 -8
  28. package/dist/node/runtime/setting/SettingImprovementSession.js +48 -7
  29. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +84 -8
  30. package/dist/node/runtime/world-revision/WorldRevisionSession.js +125 -17
  31. package/dist/node/shared/application-preset-files.js +29 -0
  32. package/dist/node/shared/default-preset-host.js +50 -38
  33. package/dist/node/shared/first-party-action-choices.js +7 -11
  34. package/dist/node/shared/first-party-generic-panels.js +7 -11
  35. package/dist/node/shared/first-party-player-view.js +7 -11
  36. package/dist/node/shared/ordered-author-prompts.js +6 -4
  37. package/dist/node/shared/ordered-play-prompts.js +4 -2
  38. package/dist/node/shared/prompt-arrangement.js +205 -0
  39. package/dist/node/shared/sillytavern-preset.js +454 -0
  40. package/dist/web/assets/{index-DH2BVsYd.css → index-BKA1D0Rz.css} +1 -1
  41. package/dist/web/assets/index-C_Qta8UL.js +1432 -0
  42. package/dist/web/index.html +2 -2
  43. package/package.json +1 -1
  44. package/dist/web/assets/index-B2kbaOGe.js +0 -1408
@@ -1,7 +1,8 @@
1
1
  import { authoringRequestPreviews } from "../authoring/AuthoringRequestSnapshot.js";
2
2
  import { createHash } from "node:crypto";
3
+ import { isDeepStrictEqual } from "node:util";
3
4
  import { emptyAggregatedModelUsage } from "../../protocol/modelUsage.js";
4
- import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
5
+ import { appendAuthoringUserMessage, authoringExchange, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
5
6
  import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
6
7
  import { equalModelHostBinding } from "../model/ModelHost.js";
7
8
  import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
@@ -60,7 +61,7 @@ export class WorldRevisionSession {
60
61
  this.#recoverWorldSessions(worldId),
61
62
  this.#workspace.sealed(worldId),
62
63
  ]);
63
- const latest = sessions[0] ?? null;
64
+ const latest = sessions.find((session) => !session.damagedContinuation) ?? null;
64
65
  const shownEpoch = activeEpoch ?? (await this.#workspace.latest(worldId));
65
66
  return {
66
67
  epoch: shownEpoch === null
@@ -188,15 +189,25 @@ export class WorldRevisionSession {
188
189
  throw new Error("Another message in this world-revision conversation is still running");
189
190
  return { session, run: running.promise };
190
191
  }
192
+ if (session.googleOutcomeUnknown)
193
+ throw new Error("The Google request outcome is unknown; start a fresh model context.");
194
+ if (session.runStatus === "paused")
195
+ throw new Error("Resume the paused authoring request before sending another message");
191
196
  }
192
197
  await this.#assertNoRunningConversation(input.worldId, session?.sessionId);
198
+ let host;
199
+ if (session !== null) {
200
+ host = await this.#bindExistingModelHost(session.modelBinding);
201
+ if (!equalModelHostBinding(session.modelBinding, host.binding()))
202
+ throw new Error("No saved model connection matches this world-revision conversation");
203
+ }
193
204
  const epoch = await this.#workspace.open(input.worldId);
194
205
  session ??= await this.#create(input.worldId, epoch, requestId);
195
206
  if (session.epochId !== epoch.epochId) {
196
207
  retargetSession(session, epoch, this.#locale());
197
208
  await this.#store.saveSession(session);
198
209
  }
199
- const host = await this.#bindExistingModelHost(session.modelBinding);
210
+ host ??= await this.#bindExistingModelHost(session.modelBinding);
200
211
  if (!equalModelHostBinding(session.modelBinding, host.binding()))
201
212
  throw new Error("No saved model connection matches this world-revision conversation");
202
213
  const { playPreset, bootstrap, preload } = await this.#compileCurrent(input.worldId, session.modelBinding);
@@ -214,10 +225,18 @@ export class WorldRevisionSession {
214
225
  if (preload !== null)
215
226
  session.authorization = authoringPreloadAuthorization(session.authorization, preload.record);
216
227
  appendAuthoringUserMessage(session, message, requestId);
228
+ if (session.modelBinding.provider === "google_interactions")
229
+ session.googleOutcomeUnknown = false;
230
+ const boundaries = host.binding().provider === "google_interactions"
231
+ ? await this.#validatedEpochBoundaries(session)
232
+ : undefined;
233
+ if (boundaries !== undefined &&
234
+ host.previewAuthoringRequest !== undefined)
235
+ session.requests.at(-1).wireRequest = host.previewAuthoringRequest(authoringExchange(session, requestId, boundaries));
217
236
  await this.#store.saveSession(session);
218
237
  return {
219
238
  session,
220
- run: this.#startRun(session, host, requestId),
239
+ run: this.#startRun(session, host, requestId, boundaries),
221
240
  };
222
241
  });
223
242
  return prepared.run ?? this.#view(prepared.session);
@@ -245,8 +264,14 @@ export class WorldRevisionSession {
245
264
  if (!equalModelHostBinding(session.modelBinding, host.binding()))
246
265
  throw new Error("No saved model connection matches this world-revision conversation");
247
266
  resumePausedAuthoringConversation(session);
267
+ const boundaries = host.binding().provider === "google_interactions"
268
+ ? await this.#validatedEpochBoundaries(session)
269
+ : undefined;
248
270
  await this.#store.saveSession(session);
249
- return { session, run: this.#startRun(session, host, requestId) };
271
+ return {
272
+ session,
273
+ run: this.#startRun(session, host, requestId, boundaries),
274
+ };
250
275
  });
251
276
  return prepared.run ?? this.#view(prepared.session);
252
277
  }
@@ -388,14 +413,18 @@ export class WorldRevisionSession {
388
413
  await this.#store.saveSession(session);
389
414
  return session;
390
415
  }
391
- #startRun(session, host, requestId) {
416
+ #startRun(session, host, requestId, worldRevisionBoundaries) {
392
417
  let activeTransaction = null;
393
418
  return this.#conversation.run({
394
419
  changed: (durable) => this.#changed(session.worldId, durable),
395
420
  session,
396
421
  host,
397
422
  requestId,
423
+ ...(worldRevisionBoundaries === undefined
424
+ ? {}
425
+ : { worldRevisionBoundaries }),
398
426
  save: (next) => this.#store.saveSession(next),
427
+ recordGoogleHop: (id, hop) => this.#store.recordGoogleHop(session.sessionId, id, hop),
399
428
  settleToolResponse: async (next, assistantItemIndex, calls) => {
400
429
  activeTransaction = await this.#settleToolResponse(next, assistantItemIndex, calls, activeTransaction);
401
430
  },
@@ -406,6 +435,32 @@ export class WorldRevisionSession {
406
435
  },
407
436
  });
408
437
  }
438
+ async #validatedEpochBoundaries(session) {
439
+ const entries = [];
440
+ for (const [itemIndex, item] of session.modelItems.entries()) {
441
+ if (item.kind !== "prompt_delta")
442
+ continue;
443
+ const source = item.logicalMessages[0]?.blocks[0]?.source;
444
+ if (source?.startsWith("runtime:world-revision-epoch:") !== true)
445
+ continue;
446
+ const epochId = source.slice("runtime:world-revision-epoch:".length);
447
+ const epoch = await this.#store.readEpoch(epochId);
448
+ if (epoch.worldId !== session.worldId)
449
+ throw new Error("World-revision epoch boundary belongs to another world");
450
+ const valid = ["en", "zh-CN"].some((locale) => isDeepStrictEqual(item, epochBoundaryItem(epoch, locale)));
451
+ if (!valid)
452
+ throw new Error("World-revision epoch boundary is damaged");
453
+ entries.push({
454
+ itemIndex,
455
+ epochId,
456
+ baseHead: epoch.baseHead,
457
+ markdown: item.logicalMessages[0].markdown,
458
+ });
459
+ }
460
+ if (entries.length > 0 && entries.at(-1)?.epochId !== session.epochId)
461
+ throw new Error("World-revision epoch boundary does not match the current epoch");
462
+ return { currentEpochId: session.epochId, entries };
463
+ }
409
464
  async #settleToolResponse(session, assistantItemIndex, calls, active) {
410
465
  const epoch = await this.#requiredActiveEpoch(session);
411
466
  const beforeRevision = epoch.revision;
@@ -512,9 +567,49 @@ export class WorldRevisionSession {
512
567
  }
513
568
  }
514
569
  async #recoverIfNeeded(session) {
570
+ if (session.damagedContinuation)
571
+ return session;
515
572
  if (this.#conversation.isRunning(session.sessionId))
516
573
  return session;
574
+ const lastItem = session.modelItems.at(-1);
575
+ if (session.pendingSettlement === null &&
576
+ session.modelBinding.provider === "google_interactions" &&
577
+ session.runStatus === "interrupted" &&
578
+ session.googleOutcomeUnknown === undefined &&
579
+ (lastItem?.kind === "user" || lastItem?.kind === "tool")) {
580
+ session.googleOutcomeUnknown = true;
581
+ session.lastFailure =
582
+ "The Google request outcome is unknown; start a fresh model context.";
583
+ session.updatedAt = Date.now();
584
+ await this.#store.saveSession(session);
585
+ return session;
586
+ }
587
+ if (session.pendingSettlement === null &&
588
+ lastItem?.kind === "assistant" &&
589
+ lastItem.toolCalls.length > 0) {
590
+ finalizeUnconfirmed(session, session.modelItems.length - 1, new Error("Runtime found a complete tool response without a persisted worktree settlement intent"));
591
+ if (session.modelBinding.provider === "google_interactions")
592
+ session.googleOutcomeUnknown = false;
593
+ const requestId = session.activeRequestId;
594
+ session.runStatus = "interrupted";
595
+ session.activeRequestId = null;
596
+ if (requestId !== null)
597
+ session.completedRequestIds = rememberAuthoringRequest(session.completedRequestIds, requestId);
598
+ session.lastFailure =
599
+ "The complete tool response could not be settled; its calls received failure receipts. Re-read the worktree before editing.";
600
+ session.updatedAt = Date.now();
601
+ await this.#store.saveSession(session);
602
+ return session;
603
+ }
604
+ if (session.runStatus === "running" &&
605
+ session.pendingSettlement === null &&
606
+ session.modelBinding.provider === "google_interactions" &&
607
+ session.googleOutcomeUnknown !== false)
608
+ session.googleOutcomeUnknown = true;
609
+ const recoveringSettlement = session.pendingSettlement !== null;
517
610
  if (session.pendingSettlement !== null) {
611
+ if (session.modelBinding.provider === "google_interactions")
612
+ session.googleOutcomeUnknown = false;
518
613
  const pending = session.pendingSettlement;
519
614
  if (pending.phase === "publication_prepared") {
520
615
  const active = await this.#workspace.active(session.worldId);
@@ -540,15 +635,20 @@ export class WorldRevisionSession {
540
635
  else
541
636
  finalizeUnconfirmed(session, pending.assistantItemIndex, new Error("Runtime restarted before the complete tool response was prepared"));
542
637
  }
543
- if (session.runStatus !== "running" && session.pendingSettlement === null)
638
+ if (session.runStatus !== "running" && !recoveringSettlement)
544
639
  return session;
545
640
  const requestId = session.activeRequestId;
546
- session.runStatus = "interrupted";
641
+ session.runStatus =
642
+ session.modelBinding.provider === "google_interactions" &&
643
+ session.googleOutcomeUnknown === false
644
+ ? "paused"
645
+ : "interrupted";
547
646
  session.activeRequestId = null;
548
647
  if (requestId !== null)
549
648
  session.completedRequestIds = rememberAuthoringRequest(session.completedRequestIds, requestId);
550
- session.lastFailure =
551
- "The previous model request stopped after its last settled worktree change. Continue this conversation when ready.";
649
+ session.lastFailure = session.googleOutcomeUnknown
650
+ ? "The Google request outcome is unknown; start a fresh model context."
651
+ : "The previous model request stopped after its last settled worktree change. Continue this conversation when ready.";
552
652
  session.updatedAt = Date.now();
553
653
  await this.#store.saveSession(session);
554
654
  return session;
@@ -624,10 +724,18 @@ export class WorldRevisionSession {
624
724
  }
625
725
  }
626
726
  function retargetSession(session, epoch, locale) {
727
+ session.modelItems.push(epochBoundaryItem(epoch, locale));
728
+ session.epochId = epoch.epochId;
729
+ session.authorization = null;
730
+ session.pendingSettlement = null;
731
+ session.lastFailure = null;
732
+ session.updatedAt = Date.now();
733
+ }
734
+ function epochBoundaryItem(epoch, locale) {
627
735
  const markdown = locale === "zh-CN"
628
736
  ? `# 新的世界修订 epoch\n\n上一轮修订已经应用或放弃。当前工作树是 ${epoch.epochId},从世界端点 ${epoch.baseHead} 重新建立。此前全部 search cursor 和 read 写授权已失效;修改前必须重新读取。`
629
737
  : `# New world-revision epoch\n\nThe previous revision was applied or discarded. Worktree ${epoch.epochId} was rebuilt from world endpoint ${epoch.baseHead}. Every prior search cursor and read authorization is invalid; re-read before writing.`;
630
- session.modelItems.push({
738
+ return {
631
739
  kind: "prompt_delta",
632
740
  logicalMessages: [
633
741
  {
@@ -641,12 +749,7 @@ function retargetSession(session, epoch, locale) {
641
749
  ],
642
750
  },
643
751
  ],
644
- });
645
- session.epochId = epoch.epochId;
646
- session.authorization = null;
647
- session.pendingSettlement = null;
648
- session.lastFailure = null;
649
- session.updatedAt = Date.now();
752
+ };
650
753
  }
651
754
  function finalizeSettlement(session, pending) {
652
755
  for (const result of pending.toolResults)
@@ -745,6 +848,11 @@ function historyItem(session) {
745
848
  const changed = new Set(session.modelItems.flatMap((item) => item.kind === "tool" ? item.changes.map(({ path }) => path) : []));
746
849
  return {
747
850
  sessionId: session.sessionId,
851
+ ...(session.damagedContinuation
852
+ ? {
853
+ unavailableReason: "Google continuation is damaged; start a fresh model context.",
854
+ }
855
+ : {}),
748
856
  epochId: session.epochId,
749
857
  runStatus: session.runStatus,
750
858
  createdAt: session.createdAt,
@@ -0,0 +1,29 @@
1
+ import { stringify } from "yaml";
2
+ /** App-owned defaults contain declarations, never copies of the prompt catalog. */
3
+ export function applicationPresetFilesForLocale(locale) {
4
+ return {
5
+ "preset.yaml": stringify({
6
+ format: "narraeon.play-preset/v3",
7
+ name: locale === "zh-CN" ? "系统推荐" : "System recommended",
8
+ callChain: "call-chain.yaml",
9
+ playPrompts: {},
10
+ authorPrompts: {},
11
+ mergeConsecutiveMessages: false,
12
+ mounts: [],
13
+ extensions: [],
14
+ }),
15
+ "call-chain.yaml": "format: narraeon.play-call-chain/v1\nnarrative: []\nfollowups: []\n",
16
+ "frame.yaml": `format: narraeon.host-frame/v1
17
+ roles:
18
+ runtime_system:
19
+ - builtin: runtime.play-contract
20
+ - builtin: runtime.tool-contract
21
+ - builtin: runtime.operation-contract
22
+ author_instruction:
23
+ - include: world.instructions
24
+ world_context:
25
+ - include: world.context
26
+ - builtin: runtime.coverage
27
+ `,
28
+ };
29
+ }
@@ -51,23 +51,23 @@ roles:
51
51
  `,
52
52
  "blocks/person-second.md": `# 人称:第二人称
53
53
 
54
- 玩家可见叙事以第二人称“你”称呼玩家角色。玩家的知觉、动作和话语都以“你”写出;其他人物按名字或第三人称称呼。观察视角跟随玩家角色,不写他无法感知的事。
54
+ 玩家可见叙事以第二人称"你"称呼玩家角色。玩家的知觉、动作和话语都以"你"写出;其他人物按名字或第三人称称呼。观察视角跟随玩家角色,不写他无法感知的事。
55
55
  `,
56
56
  "blocks/person-first.md": `# 人称:第一人称
57
57
 
58
- 玩家可见叙事以第一人称“我”写玩家角色,像玩家角色自己在讲述。知觉、判断和情绪只能是“我”能感知到的;其他人物按名字或第三人称称呼。叙述口吻贴近玩家角色已经建立的性格,不替他下结论或表态。本块启用时取代第二人称的要求。
58
+ 玩家可见叙事以第一人称"我"写玩家角色,像玩家角色自己在讲述。知觉、判断和情绪只能是"我"能感知到的;其他人物按名字或第三人称称呼。叙述口吻贴近玩家角色已经建立的性格,不替他下结论或表态。本块启用时取代第二人称的要求。
59
59
  `,
60
60
  "blocks/person-third.md": `# 人称:第三人称
61
61
 
62
- 玩家可见叙事以第三人称写玩家角色:已知名字时用名字,否则用与其性别一致的“他/她”,并与其他人物明确区分。观察视角仍跟随玩家角色,不越入其他人物未表露的内心。玩家原文里的“我”“你”在正文中转为相应的第三人称。本块启用时取代第二人称的要求。
62
+ 玩家可见叙事以第三人称写玩家角色:已知名字时用名字,否则用与其性别一致的"他/她",并与其他人物明确区分。观察视角仍跟随玩家角色,不越入其他人物未表露的内心。玩家原文里的"我""你"在正文中转为相应的第三人称。本块启用时取代第二人称的要求。
63
63
  `,
64
64
  "blocks/continue-stage.md": `# 承接:先演玩家行动
65
65
 
66
- 玩家原文是玩家对本轮行动和台词的简写,不是已经成立的小说正文。玩家页面上它以“你”的身份单独显示,正文里还没有它。
66
+ 玩家原文是玩家对本轮行动和台词的简写,不是已经成立的小说正文。玩家页面上它以"你"的身份单独显示,正文里还没有它。
67
67
 
68
68
  ## 先写玩家做了什么
69
69
 
70
- 每段正文先把玩家这次的行动和台词以叙事方式写出来:动作怎样完成、话怎样说出口、周围的人和环境此刻的样子,然后才写世界的反应。台词可以保留原话,也可以按人物口吻微调措辞,不改变意思、立场和承诺。不用“你说要……”式的旁白转述代替演出。
70
+ 每段正文先把玩家这次的行动和台词以叙事方式写出来:动作怎样完成、话怎样说出口、周围的人和环境此刻的样子,然后才写世界的反应。台词可以保留原话,也可以按人物口吻微调措辞,不改变意思、立场和承诺。不用"你说要……"式的旁白转述代替演出。
71
71
 
72
72
  ## 分量
73
73
 
@@ -75,7 +75,7 @@ roles:
75
75
  `,
76
76
  "blocks/continue-advance.md": `# 承接:直接推进
77
77
 
78
- 玩家原文已经是正文的一部分:玩家页面上它以“你”的身份显示,玩家刚读过。不要把它再演一遍。本块启用时取代“先演玩家行动”的要求。
78
+ 玩家原文已经是正文的一部分:玩家页面上它以"你"的身份显示,玩家刚读过。不要把它再演一遍。本块启用时取代"先演玩家行动"的要求。
79
79
 
80
80
  ## 从下一件事写起
81
81
 
@@ -83,7 +83,7 @@ roles:
83
83
 
84
84
  ## 必要的衔接
85
85
 
86
- 输入只是简写(“我去找他”)、不交代过程就无法呈现结果时,用最短的衔接把它落到场景里,不扩写玩家的动作和话语本身。
86
+ 输入只是简写("我去找他")、不交代过程就无法呈现结果时,用最短的衔接把它落到场景里,不扩写玩家的动作和话语本身。
87
87
  `,
88
88
  "blocks/agency-default.md": `# 代理权:默认
89
89
 
@@ -91,11 +91,11 @@ roles:
91
91
 
92
92
  ## 可以替玩家补什么
93
93
 
94
- 在已表达的选择内补足动作衔接、不改变实质的措辞和自然感官。“我去找他”这类日常简写允许开始并完成通常的连带动作:穿过走廊、敲门、把东西递过去不必逐项确认。寒暄可以替玩家接,但不能暗中承诺新事项。
94
+ 在已表达的选择内补足动作衔接、不改变实质的措辞和自然感官。"我去找他"这类日常简写允许开始并完成通常的连带动作:穿过走廊、敲门、把东西递过去不必逐项确认。寒暄可以替玩家接,但不能暗中承诺新事项。
95
95
 
96
96
  ## 在哪里交回玩家
97
97
 
98
- 途中出现玩家没有表达过的新风险、交易或关键选择时,停在需要玩家决定的位置。不能把“去找他”扩展成提出新请求、答应他的条件或交出物品;NPC 的提议或追问也不构成玩家尚未表达的目标与话语。拿不准时保留决定空间;不能以“下次还能改口”为理由替玩家确立当前选择。
98
+ 途中出现玩家没有表达过的新风险、交易或关键选择时,停在需要玩家决定的位置。不能把"去找他"扩展成提出新请求、答应他的条件或交出物品;NPC 的提议或追问也不构成玩家尚未表达的目标与话语。拿不准时保留决定空间;不能以"下次还能改口"为理由替玩家确立当前选择。
99
99
 
100
100
  ## 感官不是态度
101
101
 
@@ -105,7 +105,7 @@ roles:
105
105
 
106
106
  正文只演玩家原文明确写出的动作和台词。本块启用时取代其他代理权块。
107
107
 
108
- 连带动作只做到原文说到的地方:说“去找他”就写到走到门口,敲门和开口都交回玩家;说“把信递过去”就写到递出,对方接不接和玩家接下来说什么留给下一轮。不替玩家补台词、寒暄或回应,不替玩家做任何选择。
108
+ 连带动作只做到原文说到的地方:说"去找他"就写到走到门口,敲门和开口都交回玩家;说"把信递过去"就写到递出,对方接不接和玩家接下来说什么留给下一轮。不替玩家补台词、寒暄或回应,不替玩家做任何选择。
109
109
 
110
110
  玩家角色的自然感官和已有事实造成的身体经验可以直接写;心跳、脸红、僵住或发不出声不能替他确立爱慕、恐惧、认同、同意、拒绝或新的行动选择。不替玩家生成未表达的感情。世界外查询或文档维护不构成角色的新决定。
111
111
  `,
@@ -132,6 +132,8 @@ roles:
132
132
  "blocks/ending.md": `# 结尾
133
133
 
134
134
  结尾停在实际发生的最后一句话、一个动作或一个环境变化上。不写“轮到你了”“你打算怎么做”及“众人等你开口”一类的交接句,不在结尾列选项,不重复总结局势,不为了收尾让 NPC 硬添动作、邀请或承诺。真正发生的注视或等待可以描写。
135
+
136
+ 一件事自然结束时,也可以就停在那里,不必再添一句话或一个动作来等玩家接续。
135
137
  `,
136
138
  "blocks/output.md": `# 输出边界
137
139
 
@@ -139,7 +141,7 @@ roles:
139
141
 
140
142
  本轮新发生且玩家能感知的结果要成为具体可读的事:物品怎样递接、人物如何离开、态度变化怎样在话语或行动中显露。界面数值不能代替事件表现,叙事也不能与当前值冲突。文档中补录旧结果、去重和整理不要求再次演出;未向玩家表露的判断和场外事实无需用旁白揭露。
141
143
 
142
- 工具、目录、文档、句柄、检索、命中、Runtime 及失败过程只供私下裁决;正文不得出现“没搜到/没找到资料”等内部措辞,信息不足时保持世界内的不确定性。
144
+ 工具、目录、文档、句柄、检索、命中、Runtime 及失败过程只供私下裁决;正文不得出现"没搜到/没找到资料"等内部措辞,信息不足时保持世界内的不确定性。
143
145
 
144
146
  本次只有世界外查询或维护时,按请求简洁回答或说明整理结果,不为满足小说形式添加动作、对话或时间推进。混合请求中的维护要求不成为角色台词或行动。
145
147
  `,
@@ -206,9 +208,13 @@ summary 是目录简介,用于识别文档和判断是否需要读取。只写
206
208
  `,
207
209
  "blocks/checkpoint-policy.md": `# 检查点政策
208
210
 
209
- world_checkpoint 只用于事件之间的间隙:上一事件已在本轮停笔位置自然告一段落,或场景已实际转换,接下来从全新上下文开始也不会丢失仍然重要的细节。登记前核对上次检查点后的交互,把刚结束事件中后续仍需要的结果写入文档,包括当前状态、重要认知、承诺与待办和界面值;全新上下文只补入检查点之后的原文,上一事件的经过不会再出现。
211
+ world_checkpoint 只用于事件之间的间隙。一次对话、交易、冲突或活动自然结束,或场景已经转换、前一段事情告一段落时,可以登记检查点。地点改变、时间推进、在场者更换都是典型线索,但追逐穿过街道仍是同一场活动。事件结束不意味着无话可接,告别后还可以道谢,一件事办完后也可以接着谈下一件事。
212
+
213
+ 登记前核对上次检查点后的交互,把后续仍需要的结果保存下来,包括当前状态、重要认知、承诺与待办和界面值,让接下来的互动不必依赖前一阶段的全部过程。新上下文按现有历史配置承接近期叙事,更早仍需要的信息应已保存。
214
+
215
+ 玩家通过本轮要求表达「在此收尾」时,结合玩家输入按这个意图完成收束和保存,再登记检查点,不代玩家作出无关决定。
210
216
 
211
- 持续活动中途、对方刚给出需要玩家回应的反馈,或仍有待玩家决定的事项时,不登记。整理不等于检查点,整理随时可做且不需要登记。检查点只标记已经出现的间隙,不是本轮目标:不为了到达间隙而加快节奏、压缩过程、替玩家完成玩家没有表达的步骤或提前结束事件。登记后若叙事又增加重要结果,继续工具调用补齐。
217
+ 持续活动尚在进行,或当前事情还等着玩家作出决定、给出关键回应时,不因普通停笔或整理完成而登记。整理随时可做,不等于检查点。检查点不是本轮目标,不要为了登记而加快节奏、压缩过程、替玩家完成未表达的步骤或提前结束事件。登记后若又有需要保存的结果,继续工具调用补齐,再提交最终正文。
212
218
  `,
213
219
  "blocks/style-intimate.md": `# 文风:情感与亲密
214
220
 
@@ -216,15 +222,15 @@ world_checkpoint 只用于事件之间的间隙:上一事件已在本轮停笔
216
222
 
217
223
  ## 把那一刻拆开
218
224
 
219
- 不要跳过过程直接给结论。“你们牵起了手”把整段最值钱的东西省掉了。详写本轮停笔位置以内的那一刻:手伸过去的半路、指尖碰到哪里、对方是迎上来还是没有动、已经握住时的力度。
225
+ 不要跳过过程直接给结论。"你们牵起了手"把整段最值钱的东西省掉了。详写本轮停笔位置以内的那一刻:手伸过去的半路、指尖碰到哪里、对方是迎上来还是没有动、已经握住时的力度。
220
226
 
221
- 一个动作可以占三四行。这里的“慢”不是拖,是把玩家真正在意的那几秒补足。
227
+ 一个动作可以占三四行。这里的"慢"不是拖,是把玩家真正在意的那几秒补足。
222
228
 
223
229
  ## 触觉优先
224
230
 
225
231
  这类场面里触觉排在视觉前面:温度(他的手比你想的凉)、干湿、粗糙或细腻、力度、脉搏、布料隔着的厚度。然后才是气味、呼吸的声音、贴近时听见的动静。
226
232
 
227
- 写具体的身体感觉,别只丢一个现成比喻。“像电流窜过”所有人都读过,它替代了描写而不是完成描写;如果要用,也让它落回具体位置——从指尖窜到手肘,然后停在那儿发麻。
233
+ 写具体的身体感觉,别只丢一个现成比喻。"像电流窜过"所有人都读过,它替代了描写而不是完成描写;如果要用,也让它落回具体位置——从指尖窜到手肘,然后停在那儿发麻。
228
234
 
229
235
  ## 身体不听话
230
236
 
@@ -240,7 +246,7 @@ world_checkpoint 只用于事件之间的间隙:上一事件已在本轮停笔
240
246
 
241
247
  ## 时间会变形
242
248
 
243
- 紧张或动情的时候,一瞬间在感觉里会被拉长:你会注意到平时根本不会注意的东西——他睫毛的影子、空调的声音、自己手心出汗。用这种被放大的细节表示时间慢下来了,比直接写“时间仿佛静止”有效得多。
249
+ 紧张或动情的时候,一瞬间在感觉里会被拉长:你会注意到平时根本不会注意的东西——他睫毛的影子、空调的声音、自己手心出汗。用这种被放大的细节表示时间慢下来了,比直接写"时间仿佛静止"有效得多。
244
250
  `,
245
251
  "blocks/style-noir.md": `# 文风:冷硬与推理
246
252
 
@@ -250,7 +256,7 @@ world_checkpoint 只用于事件之间的间隙:上一事件已在本轮停笔
250
256
 
251
257
  摄影机式的叙述:写人物做了什么、说了什么、现场有什么,不写他心里怎么想。情绪从动作的偏差里渗出来——手抖了一下、烟点了两次才着、答话比正常慢了半秒。
252
258
 
253
- 不替角色下判断。不写“他显然在撒谎”,写他回答之前先看了一眼门。
259
+ 不替角色下判断。不写"他显然在撒谎",写他回答之前先看了一眼门。
254
260
 
255
261
  ## 细节是证据
256
262
 
@@ -342,7 +348,7 @@ NPC 的回答可以短、冷、有停顿;对话的紧张不要求玩家连续
342
348
 
343
349
  ## 具体的小事
344
350
 
345
- 抽象的感受落到具体的东西上:不写“他很孤独”,写他多摆了一副碗筷。不写“时间过得很久”,写窗台上那盆花换了一茬。
351
+ 抽象的感受落到具体的东西上:不写"他很孤独",写他多摆了一副碗筷。不写"时间过得很久",写窗台上那盆花换了一茬。
346
352
 
347
353
  ## 对话有下文
348
354
 
@@ -395,23 +401,23 @@ Show a scene through ongoing activity. Do not inventory everyone by name, and no
395
401
  `,
396
402
  "blocks/person-second.md": `# Person: second person
397
403
 
398
- Address the player character as “you” in all player-visible narrative. Their perceptions, actions and words are written as “you”; other characters are named or referred to in the third person. The viewpoint follows the player character and does not describe what they cannot perceive.
404
+ Address the player character as "you" in all player-visible narrative. Their perceptions, actions and words are written as "you"; other characters are named or referred to in the third person. The viewpoint follows the player character and does not describe what they cannot perceive.
399
405
  `,
400
406
  "blocks/person-first.md": `# Person: first person
401
407
 
402
- Write the player character in the first person, “I”, as if the player character were telling the story. Perceptions, judgments and feelings are limited to what “I” can perceive; other characters are named or referred to in the third person. Keep the voice close to the player character's established personality without drawing conclusions or taking stances on their behalf. When this block is enabled it replaces the second-person requirement.
408
+ Write the player character in the first person, "I", as if the player character were telling the story. Perceptions, judgments and feelings are limited to what "I" can perceive; other characters are named or referred to in the third person. Keep the voice close to the player character's established personality without drawing conclusions or taking stances on their behalf. When this block is enabled it replaces the second-person requirement.
403
409
  `,
404
410
  "blocks/person-third.md": `# Person: third person
405
411
 
406
- Write the player character in the third person: by name when known, otherwise with a pronoun that matches their gender, clearly distinguished from other characters. The viewpoint still follows the player character and does not enter other characters' unexpressed thoughts. “I” and “you” in the player's text become the corresponding third person in the prose. When this block is enabled it replaces the second-person requirement.
412
+ Write the player character in the third person: by name when known, otherwise with a pronoun that matches their gender, clearly distinguished from other characters. The viewpoint still follows the player character and does not enter other characters' unexpressed thoughts. "I" and "you" in the player's text become the corresponding third person in the prose. When this block is enabled it replaces the second-person requirement.
407
413
  `,
408
414
  "blocks/continue-stage.md": `# Continuation: stage the player's action first
409
415
 
410
- The player's text is shorthand for this turn's action and dialogue, not finished prose of the novel. The player page shows it separately under “you”; the narrative does not contain it yet.
416
+ The player's text is shorthand for this turn's action and dialogue, not finished prose of the novel. The player page shows it separately under "you"; the narrative does not contain it yet.
411
417
 
412
418
  ## Write what the player does first
413
419
 
414
- Each passage first stages the player's action and dialogue as narrative: how the movement is carried out, how the words come out, what the people and surroundings look like at that moment. Only then write the world's reaction. Dialogue may keep the original words or be lightly adjusted to the character's voice without changing its meaning, stance or commitments. Do not replace the staging with a narrated summary such as “You said you would…”.
420
+ Each passage first stages the player's action and dialogue as narrative: how the movement is carried out, how the words come out, what the people and surroundings look like at that moment. Only then write the world's reaction. Dialogue may keep the original words or be lightly adjusted to the character's voice without changing its meaning, stance or commitments. Do not replace the staging with a narrated summary such as "You said you would…".
415
421
 
416
422
  ## Weight
417
423
 
@@ -419,7 +425,7 @@ The length of the player's own action follows the pacing rules: an ordinary acti
419
425
  `,
420
426
  "blocks/continue-advance.md": `# Continuation: advance directly
421
427
 
422
- The player's text is already part of the narrative: the player page shows it under “you” and the player has just read it. Do not perform it again. When this block is enabled it replaces the requirement to stage the player's action first.
428
+ The player's text is already part of the narrative: the player page shows it under "you" and the player has just read it. Do not perform it again. When this block is enabled it replaces the requirement to stage the player's action first.
423
429
 
424
430
  ## Start with the next thing
425
431
 
@@ -427,7 +433,7 @@ Begin the passage with the first thing that happens after the player's input: th
427
433
 
428
434
  ## Minimal bridging
429
435
 
430
- When the input is shorthand (“I go find him”) and the result cannot be shown without some process, use the shortest bridge that places it in the scene, without expanding the player's own action or words.
436
+ When the input is shorthand ("I go find him") and the result cannot be shown without some process, use the shortest bridge that places it in the scene, without expanding the player's own action or words.
431
437
  `,
432
438
  "blocks/agency-default.md": `# Agency: default
433
439
 
@@ -435,7 +441,7 @@ The player's goals, destination, stance, attitude, promises, giving or accepting
435
441
 
436
442
  ## What may be filled in for the player
437
443
 
438
- Within an expressed choice, fill in supporting movements, wording that preserves the meaning, and natural sensory experience. Everyday shorthand such as “I go find him” allows starting and completing ordinary supporting actions: walking the corridor, knocking, handing something over needs no separate confirmation. Small talk may be answered for the player, but it must not silently create a commitment.
444
+ Within an expressed choice, fill in supporting movements, wording that preserves the meaning, and natural sensory experience. Everyday shorthand such as "I go find him" allows starting and completing ordinary supporting actions: walking the corridor, knocking, handing something over needs no separate confirmation. Small talk may be answered for the player, but it must not silently create a commitment.
439
445
 
440
446
  ## Where to hand back
441
447
 
@@ -449,7 +455,7 @@ The player character's natural sensory and bodily experience may be stated direc
449
455
 
450
456
  The prose performs only the actions and dialogue the player's text states explicitly. When this block is enabled it replaces the other agency blocks.
451
457
 
452
- Supporting actions go only as far as the text says: “go find him” is written up to reaching the door, and knocking or speaking returns to the player; “hand over the letter” is written up to the handover, and whether it is taken and what the player says next belong to the next turn. Do not fill in lines, small talk or replies for the player, and do not make any choice for them.
458
+ Supporting actions go only as far as the text says: "go find him" is written up to reaching the door, and knocking or speaking returns to the player; "hand over the letter" is written up to the handover, and whether it is taken and what the player says next belong to the next turn. Do not fill in lines, small talk or replies for the player, and do not make any choice for them.
453
459
 
454
460
  The player character's natural sensory and bodily experience may be stated directly when supported by established facts; heartbeat, blushing, freezing or loss of speech must not establish attraction, fear, agreement, consent, refusal or a new choice for the player. Do not invent unexpressed player feelings. Out-of-world inquiry or document maintenance constitutes no new character decision.
455
461
  `,
@@ -475,7 +481,9 @@ Ending a reply does not require ending the in-world event.
475
481
  `,
476
482
  "blocks/ending.md": `# Ending
477
483
 
478
- End on the last line actually spoken, an action or a change in the surroundings. Do not write handoffs such as “Your turn”, “What do you do?” or “Everyone waits for you to speak”; do not end by listing options or repeating a situation summary; do not invent an NPC action, invitation or promise to close the passage. Actual attention or waiting may be described.
484
+ End on the last line, action or change in the surroundings that actually occurred. Do not write handoffs such as “Your turn”, “What do you do?” or “Everyone waits for you to speak”; do not end by listing options or repeating a situation summary; do not invent an NPC action, invitation or promise to close the passage. Actual attention or waiting may be described.
485
+
486
+ When an event naturally ends, you can stop there without adding another line or action to invite continuation.
479
487
  `,
480
488
  "blocks/output.md": `# Output boundaries
481
489
 
@@ -483,7 +491,7 @@ In-world narrative uses only the novel's prose, in several natural paragraphs if
483
491
 
484
492
  Make new perceivable outcomes this turn concrete: how an object is handed over, how a person leaves, how an attitude shows in words or action. Interface values do not replace dramatization, and prose must agree with current values. Delayed recording, deduplication and maintenance do not require performing old events again; unexpressed judgments and offstage facts need not be revealed in narration.
485
493
 
486
- Tools, directories, documents, handles, searches, matches, Runtime and failed procedures serve private adjudication only. The prose must not contain internal wording such as “nothing was found in the records”; keep uncertainty in-world when information is insufficient.
494
+ Tools, directories, documents, handles, searches, matches, Runtime and failed procedures serve private adjudication only. The prose must not contain internal wording such as "nothing was found in the records"; keep uncertainty in-world when information is insufficient.
487
495
 
488
496
  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.
489
497
  `,
@@ -550,9 +558,13 @@ Preserve the visible meaning of player-view targets. Containers display subtrees
550
558
  `,
551
559
  "blocks/checkpoint-policy.md": `# Checkpoint policy
552
560
 
553
- Use world_checkpoint only for gaps between events: the previous event has naturally settled at this turn's stopping point, or the scene has actually changed, and starting a fresh context next would not lose still-important details. Before registration, reconcile interactions since the previous checkpoint and write the finished event's still-needed outcomes into documents, including current state, important knowledge, commitments, pending work and interface values; a fresh context replays only originals after the checkpoint, and the previous event's account will no longer appear.
561
+ Use world_checkpoint only for gaps between events. You may register when a conversation, transaction, conflict or activity naturally ends, or when the scene has changed and the preceding event has concluded. Typical clues include a change of location, passage of time or change of participants, but a chase through another street is still the same activity. An event can end while leaving room to reply: the player can still thank someone after a farewell, or start another topic after completing a task.
562
+
563
+ Before registration, reconcile interactions since the previous checkpoint and save outcomes that will still be needed, including current state, important knowledge, commitments, pending work and interface values, so the next interaction can continue without depending on the previous phase's entire process. A fresh context carries recent narrative according to the existing history configuration; earlier information that still matters should already be saved.
564
+
565
+ When the player's request for this turn says “Wrap up here”, follow that intent together with their input to conclude and save, then register a checkpoint, without making unrelated decisions for them.
554
566
 
555
- Do not register during an ongoing activity, immediately after feedback needing the player's response, or while matters await the player's decision. Consolidation is not a checkpoint; it can happen at any time and needs no registration. A checkpoint marks a gap that has already appeared, not a goal for this turn: do not accelerate the pace, compress the process, complete steps the player has not expressed or end an event early to reach one. If the narrative after registration adds another important outcome, continue tool calls to save it.
567
+ While an activity is still ongoing, or the current event still needs the player to decide or give a key response, do not register just because you stop writing or finish consolidation. Consolidation can happen at any time; it is not a checkpoint. A checkpoint is not a goal for this turn. Do not accelerate pace, compress process, complete steps the player has not expressed, or end an event early to register one. If further outcomes need saving after registration, continue tool calls to save them, then submit the final prose.
556
568
  `,
557
569
  "blocks/style-intimate.md": `# Style: emotion and intimacy
558
570
 
@@ -560,15 +572,15 @@ In these scenes, the center is not what happened but what the body and mind expe
560
572
 
561
573
  ## Separate the moment
562
574
 
563
- Do not skip the process and state only the conclusion. “You held hands” discards the most valuable part. Detail the moment within this reply's stopping point: the hand crossing the distance, what the fingertips touch, whether the other person meets it or stays still, and the pressure if the grip has closed.
575
+ Do not skip the process and state only the conclusion. "You held hands" discards the most valuable part. Detail the moment within this reply's stopping point: the hand crossing the distance, what the fingertips touch, whether the other person meets it or stays still, and the pressure if the grip has closed.
564
576
 
565
- One action may take three or four lines. “Slow” here does not mean dragging; it means fully rendering the few seconds the player cares about.
577
+ One action may take three or four lines. "Slow" here does not mean dragging; it means fully rendering the few seconds the player cares about.
566
578
 
567
579
  ## Put touch first
568
580
 
569
581
  In an intimate scene, touch comes before sight: temperature (their hand is cooler than expected), dryness or dampness, roughness or softness, pressure, pulse, and the thickness of fabric between bodies. Then bring in scent, the sound of breathing, and noises audible only at close range.
570
582
 
571
- Describe the actual bodily sensation instead of reaching immediately for a familiar comparison. Everyone has read “a jolt of electricity”; it replaces description instead of completing it. If you use it, return it to a specific place—from fingertips to elbow, leaving numbness there.
583
+ Describe the actual bodily sensation instead of reaching immediately for a familiar comparison. Everyone has read "a jolt of electricity"; it replaces description instead of completing it. If you use it, return it to a specific place—from fingertips to elbow, leaving numbness there.
572
584
 
573
585
  ## The body does not obey
574
586
 
@@ -584,7 +596,7 @@ Whether the other person accepts is always determined by their present character
584
596
 
585
597
  ## Time changes shape
586
598
 
587
- Under tension or emotion, an instant stretches in perception. Details normally ignored become vivid—the shadow of eyelashes, the air conditioner's hum, sweat in one's palm. Use such magnified detail to show time slowing instead of saying “time seemed to stop.”
599
+ Under tension or emotion, an instant stretches in perception. Details normally ignored become vivid—the shadow of eyelashes, the air conditioner's hum, sweat in one's palm. Use such magnified detail to show time slowing instead of saying "time seemed to stop."
588
600
  `,
589
601
  "blocks/style-noir.md": `# Style: hard-boiled investigation
590
602
 
@@ -594,7 +606,7 @@ Short sentences. End on verbs. Remove every adjective you can.
594
606
 
595
607
  Use a camera-like narration: write what people do, what they say, and what is present. Do not state what they think. Let emotion leak through deviations in action—a shaking hand, a cigarette that takes two attempts to light, an answer half a second too slow.
596
608
 
597
- Do not judge for a character. Do not write “they are obviously lying”; write that they look at the door before answering.
609
+ Do not judge for a character. Do not write "they are obviously lying"; write that they look at the door before answering.
598
610
 
599
611
  ## Details are evidence
600
612
 
@@ -686,7 +698,7 @@ Leave emotional interpretation to the reader, but make established outcomes clea
686
698
 
687
699
  ## Small, concrete things
688
700
 
689
- Anchor abstract feeling in an object or action. Do not write “he is lonely”; write that he sets out an extra bowl and pair of chopsticks. Do not write “a long time passed”; write that the plant on the sill has bloomed and been replaced.
701
+ Anchor abstract feeling in an object or action. Do not write "he is lonely"; write that he sets out an extra bowl and pair of chopsticks. Do not write "a long time passed"; write that the plant on the sill has bloomed and been replaced.
690
702
 
691
703
  ## Dialogue has another layer
692
704
 
@@ -1,6 +1,4 @@
1
- import { defaultNarrationPromptForLocale } from "./default-play-prompts.js";
2
- import { defaultPresetHostFilesForLocale } from "./default-preset-host.js";
3
- import { defaultSettingImprovementPromptForLocale, defaultSettingImprovementPromptPath, } from "./default-setting-improvement-prompt.js";
1
+ import { applicationPresetFilesForLocale } from "./application-preset-files.js";
4
2
  /**
5
3
  * An ordinary, portable play-preset asset. Runtime treats this exactly like
6
4
  * any author-supplied file map. The export is only a convenient workbench
@@ -9,11 +7,13 @@ import { defaultSettingImprovementPromptForLocale, defaultSettingImprovementProm
9
7
  export function firstPartyActionChoicesPresetFilesForLocale(locale) {
10
8
  const displayName = locale === "zh-CN" ? "下一步建议" : "Next-step ideas";
11
9
  return {
12
- "preset.yaml": `format: narraeon.play-preset/v1
10
+ ...applicationPresetFilesForLocale(locale),
11
+ "preset.yaml": `format: narraeon.play-preset/v3
13
12
  name: action-choices-recommended
14
13
  callChain: call-chain.yaml
15
- settingImprovement:
16
- markdown: prompts/setting-improvement.md
14
+ playPrompts: {}
15
+ authorPrompts: {}
16
+ mergeConsecutiveMessages: false
17
17
  mounts:
18
18
  player.options: composer_below
19
19
  extensions:
@@ -22,8 +22,7 @@ extensions:
22
22
  - assets/player-options.css
23
23
  `,
24
24
  "call-chain.yaml": `format: narraeon.play-call-chain/v1
25
- narrative:
26
- - markdown: prompts/narrate.md
25
+ narrative: []
27
26
  followups:
28
27
  - id: player_options
29
28
  displayName: ${displayName}
@@ -76,9 +75,6 @@ followups:
76
75
  minLength: 1
77
76
  maxLength: 1000
78
77
  `,
79
- ...defaultPresetHostFilesForLocale(locale),
80
- "prompts/narrate.md": defaultNarrationPromptForLocale(locale),
81
- [defaultSettingImprovementPromptPath]: defaultSettingImprovementPromptForLocale(locale),
82
78
  "prompts/options.md": locale === "zh-CN"
83
79
  ? `# 提交后下一步建议
84
80