narraeon 0.5.6 → 0.6.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 (59) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/dist/node/protocol/appPreferences.js +4 -2
  4. package/dist/node/protocol/gameplay.js +69 -0
  5. package/dist/node/protocol/playFailure.js +13 -0
  6. package/dist/node/protocol/protagonistTemplates.js +13 -0
  7. package/dist/node/protocol/resolution.js +274 -0
  8. package/dist/node/protocol/v1.js +40 -19
  9. package/dist/node/protocol/worldArchives.js +7 -0
  10. package/dist/node/runtime/RuntimeErrorLog.js +95 -0
  11. package/dist/node/runtime/V1Runtime.js +103 -8
  12. package/dist/node/runtime/archive/WorldArchiveZip.js +202 -0
  13. package/dist/node/runtime/archive/WorldArchives.js +389 -0
  14. package/dist/node/runtime/config/AppPreferencesStore.js +23 -10
  15. package/dist/node/runtime/config/ProtagonistTemplateStore.js +134 -0
  16. package/dist/node/runtime/content/ContentWorkspace.js +15 -2
  17. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +43 -2
  18. package/dist/node/runtime/content/FileNativeContentTree.js +8 -1
  19. package/dist/node/runtime/history/StoryExport.js +33 -0
  20. package/dist/node/runtime/model/FileNativeModelAdapters.js +34 -2
  21. package/dist/node/runtime/model/ModelHost.js +12 -1
  22. package/dist/node/runtime/model/ModelHostStream.js +11 -3
  23. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +37 -1
  24. package/dist/node/runtime/play/FileNativePlayPresetStore.js +48 -3
  25. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +87 -4
  26. package/dist/node/runtime/play/LegacyModelBindingFailure.js +10 -0
  27. package/dist/node/runtime/play/OrderedPlayPrompts.js +68 -6
  28. package/dist/node/runtime/play/PlayCallChain.js +177 -57
  29. package/dist/node/runtime/play/PlayCallChainError.js +3 -1
  30. package/dist/node/runtime/play/PlayContinuity.js +3 -20
  31. package/dist/node/runtime/play/PlayFailure.js +20 -0
  32. package/dist/node/runtime/play/PlayTraceRetention.js +3 -11
  33. package/dist/node/runtime/play/TabletopResolution.js +372 -0
  34. package/dist/node/runtime/play/TabletopRules.js +141 -0
  35. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +49 -32
  36. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -4
  37. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +4 -1
  38. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +56 -13
  39. package/dist/node/runtime/setting/SettingImprovementSession.js +6 -4
  40. package/dist/node/runtime/world/FileNativeWorldStore.js +207 -40
  41. package/dist/node/runtime/world/WorldActivity.js +143 -0
  42. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -0
  43. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +35 -6
  44. package/dist/node/runtime/world-revision/WorldRevisionSession.js +9 -5
  45. package/dist/node/server/ConversationStream.js +2 -0
  46. package/dist/node/server/createServer.js +48 -0
  47. package/dist/node/shared/default-preset-host.js +10 -6
  48. package/dist/node/shared/ordered-author-prompts.js +8 -0
  49. package/dist/node/shared/ordered-play-prompts.js +25 -2
  50. package/dist/node/shared/sample-packages/index.js +10 -0
  51. package/dist/node/shared/sample-packages/rainy-inn-en.js +194 -0
  52. package/dist/node/shared/sample-packages/rainy-inn-zh.js +194 -0
  53. package/dist/node/shared/tabletop-guidance.js +100 -0
  54. package/dist/web/assets/index-BJ-1Mb0e.css +1 -0
  55. package/dist/web/assets/index-BgBL8G-A.js +1323 -0
  56. package/dist/web/index.html +2 -2
  57. package/package.json +6 -2
  58. package/dist/web/assets/index-BH8NyuE4.js +0 -1233
  59. package/dist/web/assets/index-CVN48xrw.css +0 -1
@@ -1,5 +1,6 @@
1
1
  import { isPackageFollowupControlPath } from "../content/PackageFollowups.js";
2
2
  import { defaultAppLocale } from "../../protocol/appPreferences.js";
3
+ import { assertSameGameplay, gameplayPath, readGameplay, } from "../../protocol/gameplay.js";
3
4
  import { inspectContentPackageCurrentTree } from "../content/FileNativeContentTree.js";
4
5
  import { SettingAuthoringTransaction, settingImprovementToolDefinitions, } from "../setting/SettingAuthoringTransaction.js";
5
6
  import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
@@ -71,8 +72,8 @@ export class WorldRevisionAuthoringTransaction {
71
72
  });
72
73
  }
73
74
  }
74
- export function worldRevisionToolDefinitions(locale = defaultAppLocale) {
75
- return settingImprovementToolDefinitions(locale).map((definition) => {
75
+ export function worldRevisionToolDefinitions(locale, gameplay) {
76
+ return settingImprovementToolDefinitions(locale, gameplay).map((definition) => {
76
77
  const name = settingToRevisionTool.get(definition.name);
77
78
  if (name === undefined)
78
79
  throw new Error(`Unsupported setting authoring tool: ${definition.name}`);
@@ -84,7 +85,9 @@ export function worldRevisionToolDefinitions(locale = defaultAppLocale) {
84
85
  };
85
86
  });
86
87
  }
87
- export function worldRevisionRuntimeContract(locale) {
88
+ export function worldRevisionRuntimeContract(locale, gameplay) {
89
+ // A world's mode and scheme are fixed, so only tabletop worlds mention the file.
90
+ const tabletop = gameplay.mode === "tabletop";
88
91
  return locale === "zh-CN"
89
92
  ? `# Runtime 世界修订对话契约
90
93
 
@@ -93,7 +96,7 @@ export function worldRevisionRuntimeContract(locale) {
93
96
  - 工具调用修改的是尚未应用的工作树。只有玩家点击“应用修订”才会把当前整棵树提交为新的世界状态和控制并解除锁;“放弃修订”会丢弃工作树并解除锁。你不得宣称自己已经应用或放弃修订。
94
97
  - 每个完整工具响应按调用顺序结算。成功调用留下逐文件 before-image,可以由玩家单独回滚;失败调用不会撤销同一响应内其他成功调用。
95
98
  - 修改既有文件前必须完整读取。list/search cursor 和 read 授权只属于产生它们的工作树 revision;工作树发生任何手动、AI 或回滚写入后,继续修改前必须重新读取。
96
- - state/* 只接受 .yaml 或 .md 世界文档;control/* 接受 frame.yaml、player-views.yaml、blocks/*.md,以及 followups.yaml 与后置请求引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。可以创建新状态文档;既有状态文档不能删除或移动,因为 Authority 保留其稳定身份。
99
+ - state/* 只接受 .yaml 或 .md 世界文档;control/* 接受 frame.yaml、player-views.yaml、blocks/*.md,以及 followups.yaml 与后置请求引用的 prompts/*.md、renderers/*.html、scripts/*.js、regex/*.yaml、assets/* 文本资源。${tabletop ? "control/gameplay.json 记录世界的玩法模式与判定方案,修订中只能保持不变。" : ""}可以创建新状态文档;既有状态文档不能删除或移动,因为 Authority 保留其稳定身份。
97
100
  - 对话跨越“应用”或“放弃”继续时,Runtime 会明确宣布一个新修订 epoch。此前读取授权全部失效;先重新读取新世界,再继续修改。`
98
101
  : `# Runtime world-revision conversation contract
99
102
 
@@ -102,11 +105,12 @@ export function worldRevisionRuntimeContract(locale) {
102
105
  - Tools change the unapplied worktree. Only the player can Apply the complete tree to world state/control and unlock it, or Discard the tree and unlock it. Never claim that you applied or discarded the revision yourself.
103
106
  - Calls in each complete tool response settle in order. Every successful call retains per-file before-images for selective rollback; a failed sibling does not undo successful calls.
104
107
  - Completely read an existing file before changing it. List/search cursors and read authorization belong only to the worktree revision that produced them. Re-read after any manual, AI, or rollback write.
105
- - state/* accepts only YAML or Markdown world documents. control/* accepts frame.yaml, player-views.yaml, blocks/*.md, plus followups.yaml and its referenced prompts/*.md, renderers/*.html, scripts/*.js, regex/*.yaml and assets/* text resources. New state documents are allowed; existing state documents cannot be deleted or moved because Authority retains their stable identity.
108
+ - state/* accepts only YAML or Markdown world documents. control/* accepts frame.yaml, player-views.yaml, blocks/*.md, plus followups.yaml and its referenced prompts/*.md, renderers/*.html, scripts/*.js, regex/*.yaml and assets/* text resources. ${tabletop ? "control/gameplay.json records the world's gameplay mode and resolution scheme; a revision must leave both unchanged. " : ""}New state documents are allowed; existing state documents cannot be deleted or moved because Authority retains their stable identity.
106
109
  - When a conversation continues after Apply or Discard, Runtime announces a new revision epoch and invalidates every prior read authorization. Re-read the new world before editing.`;
107
110
  }
108
111
  export function inspectWorldRevisionFiles(files, immutableBaseFiles) {
109
112
  const diagnostics = [];
113
+ diagnostics.push(...gameplayDiagnostics(immutableBaseFiles, files));
110
114
  const paths = new Set();
111
115
  for (const file of files) {
112
116
  if (paths.has(file.path))
@@ -188,6 +192,30 @@ export function inspectWorldRevisionFiles(files, immutableBaseFiles) {
188
192
  diagnostics,
189
193
  };
190
194
  }
195
+ /** A malformed gameplay file is reported by content inspection, not as a scheme change. */
196
+ function gameplayDiagnostics(immutableBaseFiles, files) {
197
+ try {
198
+ readGameplay(files);
199
+ }
200
+ catch {
201
+ return [];
202
+ }
203
+ try {
204
+ assertSameGameplay(immutableBaseFiles, files);
205
+ return [];
206
+ }
207
+ catch (error) {
208
+ return [
209
+ {
210
+ code: "gameplay_change_forbidden",
211
+ path: gameplayPath,
212
+ message: error instanceof Error
213
+ ? error.message
214
+ : "World gameplay mode and resolution scheme are fixed",
215
+ },
216
+ ];
217
+ }
218
+ }
191
219
  export function assertUsableWorldRevisionFiles(files, immutableBaseFiles) {
192
220
  const inspection = inspectWorldRevisionFiles(files, immutableBaseFiles);
193
221
  if (inspection.status !== "usable")
@@ -370,7 +398,8 @@ function mapSchema(schema) {
370
398
  function isWritableWorldRevisionPath(path) {
371
399
  if (/^state\/.+\.(?:ya?ml|md)$/u.test(path))
372
400
  return true;
373
- return (isPackageFollowupControlPath(path) ||
401
+ return (path === gameplayPath ||
402
+ isPackageFollowupControlPath(path) ||
374
403
  path === "control/frame.yaml" ||
375
404
  path === "control/player-views.yaml" ||
376
405
  /^control\/blocks\/.+\.md$/u.test(path));
@@ -233,16 +233,18 @@ export class WorldRevisionSession {
233
233
  const worlds = await this.#worlds.listWorlds();
234
234
  const playPreset = await this.#bindPlayPreset();
235
235
  const locale = this.#locale();
236
+ const gameplay = await this.#worlds.gameplay(worldId);
236
237
  const bootstrap = this.#compiler.compileWorldRevision({
237
238
  ...(epoch === null
238
239
  ? {}
239
240
  : { epoch: { id: epoch.epochId, baseHead: epoch.baseHead } }),
240
241
  worldTitle: worlds.find((world) => world.worldId === worldId)?.title ?? worldId,
241
- runtimeContract: worldRevisionRuntimeContract(locale),
242
+ gameplay,
243
+ runtimeContract: worldRevisionRuntimeContract(locale, gameplay),
242
244
  authorPrompt: settingImprovementPromptForBinding(playPreset, locale),
243
245
  playPreset,
244
246
  modelBinding: modelBinding,
245
- tools: worldRevisionToolDefinitions(locale),
247
+ tools: worldRevisionToolDefinitions(locale, gameplay),
246
248
  });
247
249
  return { bootstrap, playPreset };
248
250
  }
@@ -253,19 +255,21 @@ export class WorldRevisionSession {
253
255
  return this.#mutate(async () => this.#view(await this.#recoverIfNeeded(await this.#store.readSession(sessionId))));
254
256
  }
255
257
  async #create(worldId, epoch, creationRequestId) {
256
- const [host, playPreset, worlds] = await Promise.all([
258
+ const [host, playPreset, worlds, gameplay] = await Promise.all([
257
259
  this.#bindModelHost(),
258
260
  this.#bindPlayPreset(),
259
261
  this.#worlds.listWorlds(),
262
+ this.#worlds.gameplay(worldId),
260
263
  ]);
261
264
  const worldTitle = worlds.find((world) => world.worldId === worldId)?.title ?? worldId;
262
265
  const locale = this.#locale();
263
266
  const modelBinding = host.binding();
264
- const tools = worldRevisionToolDefinitions(locale);
267
+ const tools = worldRevisionToolDefinitions(locale, gameplay);
265
268
  const bootstrap = this.#compiler.compileWorldRevision({
266
269
  epoch: { id: epoch.epochId, baseHead: epoch.baseHead },
267
270
  worldTitle,
268
- runtimeContract: worldRevisionRuntimeContract(locale),
271
+ gameplay,
272
+ runtimeContract: worldRevisionRuntimeContract(locale, gameplay),
269
273
  authorPrompt: settingImprovementPromptForBinding(playPreset, locale),
270
274
  playPreset,
271
275
  modelBinding,
@@ -68,6 +68,8 @@ export function registerConversationStream(server, runtime) {
68
68
  }
69
69
  }
70
70
  catch (error) {
71
+ request.log.error({ err: error, conversationTarget: target }, "conversation observation failed");
72
+ await runtime.recordConversationFailure?.(target, error);
71
73
  write(`event: failure\ndata: ${JSON.stringify({ message: error instanceof Error ? error.message : "Conversation observation failed" })}\n\n`);
72
74
  stream.end();
73
75
  cleanup();
@@ -6,6 +6,7 @@ import fastifyStatic from "@fastify/static";
6
6
  import Fastify, { LogController, } from "fastify";
7
7
  import { maxPortableContentArchiveBase64Characters } from "../protocol/contentTree.js";
8
8
  import { parseV1Envelope, V1ProtocolError, v1Protocol, } from "../protocol/v1.js";
9
+ import { WorldArchiveError } from "../runtime/archive/WorldArchiveZip.js";
9
10
  const runtimeRequestBodyLimit = maxPortableContentArchiveBase64Characters + 64 * 1024;
10
11
  const playCallChainStreamContentType = "application/x-ndjson";
11
12
  class RuntimeLogController extends LogController {
@@ -81,6 +82,53 @@ export async function createServer(input) {
81
82
  done(null, payload);
82
83
  });
83
84
  registerConversationStream(server, input.runtime);
85
+ // Keep uploads as a backpressured stream with their own archive-size limit.
86
+ // Fastify's JSON parser and content-package body limit never buffer this route.
87
+ await server.register((archives, _options, done) => {
88
+ archives.addContentTypeParser("application/zip", (_request, payload, done) => done(null, payload));
89
+ archives.post("/api/runtime/v1/world-archives", async (request, reply) => {
90
+ try {
91
+ return await input.runtime.worldArchives.upload(request.body);
92
+ }
93
+ catch (error) {
94
+ if (error instanceof WorldArchiveError)
95
+ return reply
96
+ .status(error.code === "limit_exceeded" ? 413 : 400)
97
+ .send({ error: { code: error.code, message: error.message } });
98
+ throw error;
99
+ }
100
+ });
101
+ archives.get("/api/runtime/v1/world-archives/:archiveId", async (request, reply) => {
102
+ try {
103
+ const archive = await input.runtime.worldArchives.download(request.params.archiveId);
104
+ reply.raw.once("finish", () => {
105
+ void archive.cleanup().catch(() => undefined);
106
+ });
107
+ return reply
108
+ .type("application/zip")
109
+ .header("Content-Length", archive.metadata.bytes)
110
+ .header("Content-Disposition", `attachment; filename="world.narraeon-world.zip"; filename*=UTF-8''${encodeURIComponent(archive.metadata.fileName)}`)
111
+ .send(archive.stream);
112
+ }
113
+ catch (error) {
114
+ if (error instanceof WorldArchiveError)
115
+ return reply
116
+ .status(400)
117
+ .send({ error: { code: error.code, message: error.message } });
118
+ if (error instanceof Error &&
119
+ "code" in error &&
120
+ error.code === "ENOENT")
121
+ return reply.status(404).send({
122
+ error: {
123
+ code: "invalid_archive",
124
+ message: "The archive download is unavailable",
125
+ },
126
+ });
127
+ throw error;
128
+ }
129
+ });
130
+ done();
131
+ });
84
132
  server.get("/health", () => ({
85
133
  status: "ok",
86
134
  protocol: "narraeon.runtime/v1",
@@ -234,7 +234,7 @@ NPC 可以按性格、认知、处境和目标继续做事、与别人交谈、
234
234
 
235
235
  summary 是目录简介,用于识别文档和判断是否需要读取。只写简短的身份、主题或稳定线索,不复述具体状态和事件经过;具体事实放在正文。仅在识别信息改变时更新标题与简介。
236
236
 
237
- ## 保存时机与整理检查点
237
+ ## 保存时机、整理与检查点
238
238
 
239
239
  先按玩家授权与互动需要确定本轮叙事范围和停笔位置,再保存该范围内的事实。保存进行中活动的实际进展、必要原话和待回应事项即可整理截至此刻的状态,不把尚未发生的完成结果提前写入。
240
240
 
@@ -243,9 +243,11 @@ summary 是目录简介,用于识别文档和判断是否需要读取。只写
243
243
  3. 需要保存但玩家原文和最终叙事没有记录的信息,当轮保存,包括重要场外进展、未表露的认知和隐蔽后果。工具中间文本不能替代持久记录。
244
244
  4. 其余可从玩家原文和最终叙事恢复的持续结果,允许事件收尾或合适的中间整理点归并。
245
245
 
246
- 按已确定的叙事范围完成本轮必须保存的写入。在本轮原定停笔位置判断整理时机:事件自然告一段落、实际转换场景,或持续活动经过多轮并积累了需要归并的进展时,主动集中整理,不等玩家要求,也不等整个活动完成。只有零散即时变化时,按上述保存时机继续互动;一次写入不等于一次检查点。
246
+ 按已确定的叙事范围完成本轮必须保存的写入。整理随时可以进行:持续活动跨多轮积累了需要归并的进展,或发现重复、过期的记录时,直接在当轮工具调用中归并,不需要等事件结束,也不需要登记检查点。整理不等于检查点。只有零散即时变化时,按上述保存时机继续互动。
247
247
 
248
- 整理时核对上次检查点后的交互和本轮范围内的结果,补齐必要记录,更新认知、进程、约定和界面值,结束实际已失效的待办并清理重复。此前已写回的事实核对后沿用,无需重复写入。准备好该停笔位置的记录后,调用 world_checkpoint,再输出最终叙事;进行中的活动和待回应事项按原状保存,整理不改变停笔位置。检查点覆盖该叙事,若其中又增加重要结果,继续工具调用补齐。登记回执不是语义完整性认证。
248
+ world_checkpoint 只用于事件之间的间隙:上一事件已在本轮停笔位置自然告一段落,或场景已实际转换,接下来从全新上下文开始也不会丢失仍然重要的细节。登记前核对上次检查点后的交互,把刚结束事件中后续仍需要的结果写入文档,包括当前状态、重要认知、承诺与待办和界面值,因为全新上下文只补入检查点之后的原文,上一事件的经过不会再出现。持续活动中途、对方刚给出需要玩家回应的反馈,或仍有待玩家决定的事项时,不登记检查点。此前已写回的事实核对后沿用,无需重复写入。登记后输出最终叙事;若叙事又增加重要结果,继续工具调用补齐。登记回执不是语义完整性认证。
249
+
250
+ 检查点只标记已经出现的间隙,不是本轮要达成的目标。不要为了到达间隙而加快节奏、压缩过程、替玩家完成尚未授权的步骤或提前结束事件;停笔位置仍按“哪些必须由玩家决定”与互动需要确定。一个事件持续很多轮、跨越多次回复都是正常的,期间不需要登记检查点。
249
251
 
250
252
  ## 整理与连续性
251
253
 
@@ -492,7 +494,7 @@ Prefer natural owners for durable information. Create a separate document for in
492
494
 
493
495
  summary is a catalog description for identifying a document and deciding whether to read it. Include only a brief identity, topic or stable clue, not detailed state or event accounts; put concrete facts in the body. Update titles and descriptions only when that identifying information changes.
494
496
 
495
- ## Save timing and consolidation checkpoints
497
+ ## Save timing, consolidation and checkpoints
496
498
 
497
499
  First choose this turn's narrative scope and stopping point from player authorization and interaction needs, then save facts within that scope. Recording actual progress in an ongoing activity, necessary exact wording and matters awaiting a response can complete maintenance through this moment without writing unrealized completion outcomes in advance.
498
500
 
@@ -501,9 +503,11 @@ First choose this turn's narrative scope and stopping point from player authoriz
501
503
  3. Save needed information absent from player originals and final narrative during this turn, including important offstage progress, unexpressed knowledge and hidden consequences. Intermediate tool text is not a durable substitute.
502
504
  4. Other durable outcomes recoverable from player originals and final narrative may be consolidated at event closure or a suitable intermediate point.
503
505
 
504
- Complete required writes within the chosen narrative scope. At this turn's already chosen stopping point, assess whether consolidation is due: proactively consolidate when an event naturally settles, the scene actually changes, or an ongoing activity has accumulated progress across several turns that needs consolidation. Do not wait for the player to request maintenance or for the whole activity to finish. With only isolated immediate changes, continue the interaction under the saving rules above; one write does not mean one checkpoint.
506
+ Complete required writes within the chosen narrative scope. Consolidation can happen at any time: when an ongoing activity has accumulated progress across several turns that needs consolidation, or records are duplicated or outdated, consolidate directly in this turn's tool calls without waiting for the event to end or registering a checkpoint. Consolidation is not a checkpoint. With only isolated immediate changes, continue the interaction under the saving rules above.
507
+
508
+ 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 save outcomes of the finished event that remain necessary, including current state, important knowledge, commitments, pending work and interface values. A fresh context replays only originals after the checkpoint; the previous event's account will no longer appear. Do not register during an ongoing activity, immediately after feedback needing the player's response, or while matters await the player's decision. Reuse already saved facts after checking them, without duplicate writes. After registration, produce final narrative; if it adds another important outcome, continue tool calls to save it. Registration is not certification of semantic completeness.
505
509
 
506
- When consolidating, reconcile interactions since the previous checkpoint and outcomes within this turn's scope: fill necessary records, update knowledge, processes, commitments and interface values, close actually expired pending work and remove duplicates. Reuse already saved facts after checking them, without duplicate writes. Once records for that stopping point are ready, call world_checkpoint, then produce final narrative. Preserve ongoing activities and matters awaiting a response as they stand; maintenance does not move the stopping point. The checkpoint covers that narrative; if it adds another important outcome, continue tool calls to save it. Registration is not certification of semantic completeness.
510
+ A checkpoint marks a gap that has already appeared, not a goal for this turn. Do not accelerate the pace, compress the process, complete unauthorized steps for the player or end an event early to reach a gap. Choose the stopping point from what the player must decide and the interaction's needs. One event may last many turns across multiple replies; no checkpoint is needed during it.
507
511
 
508
512
  ## Maintenance and continuity
509
513
 
@@ -1,4 +1,5 @@
1
1
  import { defaultSettingImprovementPromptForLocale } from "./default-setting-improvement-prompt.js";
2
+ import { tabletopAuthorBody } from "./tabletop-guidance.js";
2
3
  /** Authoring uses the same permissions as play, with target-specific dynamic references. */
3
4
  export function builtinAuthorPrompts(locale) {
4
5
  const zh = locale === "zh-CN";
@@ -17,6 +18,13 @@ export function builtinAuthorPrompts(locale) {
17
18
  required: false,
18
19
  body: defaultSettingImprovementPromptForLocale(locale),
19
20
  },
21
+ {
22
+ id: "author.tabletop",
23
+ name: zh ? "跑团创作建议" : "Tabletop authoring guidance",
24
+ required: false,
25
+ body: tabletopAuthorBody(locale),
26
+ gameplay: "tabletop",
27
+ },
20
28
  {
21
29
  id: "author.target",
22
30
  name: zh
@@ -1,9 +1,25 @@
1
1
  import { historyUseGuidance } from "./history-use-guidance.js";
2
2
  import { defaultNarrationPromptForLocale } from "./default-play-prompts.js";
3
3
  import { defaultPresetHostFilesForLocale } from "./default-preset-host.js";
4
+ import { tabletopPlayBody } from "./tabletop-guidance.js";
4
5
  export function isPromptMessageRole(value) {
5
6
  return value === "system" || value === "user" || value === "assistant";
6
7
  }
8
+ /**
9
+ * Unscoped prompts always apply. A scoped prompt applies only when the compiled
10
+ * world or package is tabletop, so fiction requests never see it.
11
+ */
12
+ export function promptScopeApplies(scope, gameplay) {
13
+ return scope === undefined || gameplay.mode === "tabletop";
14
+ }
15
+ /** The scope of an ordered entry: a user entry's own scope or its builtin's. */
16
+ export function orderedPromptScope(entry, catalog) {
17
+ if (entry.kind === "user")
18
+ return entry.gameplay;
19
+ if (entry.kind === "builtin")
20
+ return catalog.find((item) => item.id === entry.builtin)?.gameplay;
21
+ return undefined;
22
+ }
7
23
  /** Application-owned references. Persist references; freeze resolved text in requests. */
8
24
  export function builtinPlayPrompts(locale) {
9
25
  const zh = locale === "zh-CN";
@@ -22,7 +38,7 @@ export function builtinPlayPrompts(locale) {
22
38
 
23
39
  world_patch 的 YAML locator 可以包含 map key 和从零开始的数组下标;remove 删除一个精确位置。未提供的元数据保持原值。world_retire 从目录注入中退役文档,文档仍可读取、引用和恢复。工具响应的成功状态变化与后续最终叙事分别提交,后续失败不回滚已提交变化。成功写入回执表示当前值已改变;以真实回执的结算状态为准,模型文字不能提交世界。
24
40
 
25
- 包含工具调用的响应属于中间步,其中的文字尚未展示或提交为最终叙事。收到全部工具结果后,根据回执在最终正文中呈现本轮尚未展示的内容,并与已提交状态一致;没有工具调用的非空正文结束本次循环并由 Runtime 提交。world_checkpoint 仅登记整理声明与历史重放边界,在随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文;登记后仍可继续工具调用。登记不结束世界内事件,也不自动切换上下文,玩家自行选择。全新上下文补入边界后的已提交玩家原文与最终叙事,排除开场白、工具和推理。
41
+ 包含工具调用的响应属于中间步,其中的文字尚未展示或提交为最终叙事。收到全部工具结果后,根据回执在最终正文中呈现本轮尚未展示的内容,并与已提交状态一致;没有工具调用的非空正文结束本次循环并由 Runtime 提交。world_checkpoint 只在事件间隙登记,表示建议从此开始全新上下文,并作为历史重放边界;整理本身不需要登记。在随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文;登记后仍可继续工具调用。登记不结束世界内事件,也不自动切换上下文,玩家自行选择。全新上下文补入边界后的已提交玩家原文与最终叙事,排除开场白、工具和推理。
26
42
 
27
43
  ${historyUseGuidance(locale)}`
28
44
  : `# Tools and response settlement
@@ -31,7 +47,7 @@ Use only attached tools. Directories use @dir-* handles from the preload or tool
31
47
 
32
48
  YAML world_patch locators accept map keys and zero-based array indexes; remove deletes one exact location. Omitted metadata remains unchanged. world_retire removes a document from catalog injection while keeping it readable, referenceable and restorable. Successful tool-response state changes and later final narrative commit separately; later failure does not roll back committed changes. Successful write receipts change current values; the actual receipt determines settlement, not model prose.
33
49
 
34
- Responses containing tool calls are intermediate steps; their text has not yet been displayed or committed as final narrative. After all tool results, use the receipts to present this turn's still-unshown content in final prose consistent with committed state. A nonempty tool-free response ends this loop and Runtime commits it. world_checkpoint only registers a consolidation declaration and history-replay boundary; it takes effect after the subsequent nonempty tool-free final prose successfully commits, including that prose. Further tool calls may follow registration. Registration neither ends an in-world event nor switches context, which the player chooses. Fresh-context replay contains committed player originals and final narratives after the boundary, excluding the opening, tools and reasoning.
50
+ Responses containing tool calls are intermediate steps; their text has not yet been displayed or committed as final narrative. After all tool results, use the receipts to present this turn's still-unshown content in final prose consistent with committed state. A nonempty tool-free response ends this loop and Runtime commits it. world_checkpoint registers only at gaps between events, suggesting a fresh context from that point and defining the history-replay boundary; consolidation itself does not require registration. It takes effect after the subsequent nonempty tool-free final prose successfully commits, including that prose. Further tool calls may follow registration. Registration neither ends an in-world event nor switches context, which the player chooses. Fresh-context replay contains committed player originals and final narratives after the boundary, excluding the opening, tools and reasoning.
35
51
 
36
52
  ${historyUseGuidance(locale)}`,
37
53
  },
@@ -67,6 +83,13 @@ ${historyUseGuidance(locale)}`,
67
83
  body,
68
84
  required: false,
69
85
  })),
86
+ {
87
+ id: "play.tabletop",
88
+ name: zh ? "跑团主持" : "Tabletop GM",
89
+ body: tabletopPlayBody(locale),
90
+ required: false,
91
+ gameplay: "tabletop",
92
+ },
70
93
  ];
71
94
  }
72
95
  export function defaultOrderedPlayPrompts() {
@@ -0,0 +1,10 @@
1
+ import { rainyInnEnglish } from "./rainy-inn-en.js";
2
+ import { rainyInnChinese } from "./rainy-inn-zh.js";
3
+ /** Each caller receives an ordinary, independent content-tree seed. */
4
+ export function sampleContentPackageForLocale(locale) {
5
+ const sample = locale === "zh-CN" ? rainyInnChinese : rainyInnEnglish;
6
+ return {
7
+ title: sample.title,
8
+ files: sample.files.map((file) => ({ ...file })),
9
+ };
10
+ }
@@ -0,0 +1,194 @@
1
+ export const rainyInnEnglish = {
2
+ title: "A Letter at the Rain Lantern Inn",
3
+ files: [
4
+ {
5
+ path: "opening.md",
6
+ contents: `Rain patters from the inn's eaves. The little brass bell above you is still swaying, and the lobby smells of ginger tea. A lamp casts a warm circle across the wooden counter.
7
+
8
+ Behind it, a woman in a green apron lifts an empty blue saucer, checks underneath, and sets it down again. “Well, it hasn't grown legs.”
9
+
10
+ A guest in a grey cardigan looks up from the window table. “A pale blue envelope. It's for my sister, and I haven't sealed it yet.” Beside the counter, a young postal runner pauses with a damp ledger tucked against his chest.
11
+
12
+ The woman offers you a dry towel. “Welcome in. We're having a small mystery with a letter. Would you mind lending us a fresh pair of eyes?” The counter, umbrella stand, and window table are all within the lamplight. She waits for your answer.
13
+ `,
14
+ },
15
+ {
16
+ path: "world/current-situation.yaml",
17
+ contents: `$document:
18
+ id: situation.rainy-inn
19
+ ref: current-situation
20
+ title: An empty saucer in the lamplight
21
+ summary: A small search in the Rain Lantern Inn's lobby.
22
+ aliases: []
23
+ 背景:
24
+ Time: An autumn evening, nine o'clock, steady rain
25
+ Place: Rain Lantern Inn lobby, Brookbridge
26
+ Present:
27
+ - $ref: character.nora
28
+ - $ref: character.evelyn
29
+ - $ref: character.kit
30
+ In progress:
31
+ Letter search: Evelyn's pale blue envelope is no longer on the counter's blue saucer. Nobody has begun a methodical search of the lobby.
32
+ Awaiting a response:
33
+ Speaker: { $ref: character.nora }
34
+ Invitation: Welcome in. We're having a small mystery with a letter. Would you mind lending us a fresh pair of eyes?
35
+ Immediate circumstances:
36
+ - You have just arrived at the doorway. Nora's offered towel has not been taken.
37
+ - Kit holds the ledger and has paused wiping its damp corner.
38
+ `,
39
+ },
40
+ {
41
+ path: "world/characters/nora.yaml",
42
+ contents: `$document:
43
+ id: character.nora
44
+ ref: nora
45
+ title: Nora
46
+ summary: The Rain Lantern's attentive innkeeper, quick to offer practical kindness.
47
+ aliases: []
48
+ Appearance: Short hair, soft features, a dusting of flour on one cuff.
49
+ Clothing: A cream shirt, forest-green apron, and soft indoor shoes.
50
+ Temperament: Patient; looks for a remedy before laying blame.
51
+ Relationships:
52
+ Evelyn:
53
+ Person: { $ref: character.evelyn }
54
+ View: A familiar guest whose plant stories brighten the lobby; hopes she can rest without worrying tonight.
55
+ Kit:
56
+ Person: { $ref: character.kit }
57
+ View: A neighbour's helpful son, sometimes distracted; willing to hear him out when something goes wrong.
58
+ Knowledge:
59
+ Letter: Remembers putting Evelyn's envelope on the blue saucer, then leaving to make tea. Did not watch the counter after that.
60
+ Basis: { $ref: event.letter-entrusted }
61
+ `,
62
+ },
63
+ {
64
+ path: "world/characters/evelyn.yaml",
65
+ contents: `$document:
66
+ id: character.evelyn
67
+ ref: evelyn
68
+ title: Evelyn
69
+ summary: A travelling botanical illustrator who shares discoveries in letters.
70
+ aliases: []
71
+ Appearance: Round glasses and a faint smudge of graphite on her fingertips.
72
+ Clothing: A grey cardigan and navy trousers; her brown rain cape hangs by the umbrellas.
73
+ Temperament: Observant and deliberate; double-checks small details when worried.
74
+ Relationships:
75
+ Nora:
76
+ Person: { $ref: character.nora }
77
+ View: A trusted innkeeper; the missing letter has not made her suspect deliberate concealment.
78
+ Kit:
79
+ Person: { $ref: character.kit }
80
+ View: Met him this evening; trusts his postal expertise but knows little about his habits.
81
+ Important belongings:
82
+ Letter for her sister: An unsealed pale blue envelope containing a page of news and a hand-drawn leaf study, intended for tomorrow's post.
83
+ Knowledge:
84
+ Letter: Knows she entrusted it to Nora and now sees an empty saucer; does not know where it went afterward.
85
+ Basis: { $ref: event.letter-entrusted }
86
+ `,
87
+ },
88
+ {
89
+ path: "world/characters/kit.yaml",
90
+ contents: `$document:
91
+ id: character.kit
92
+ ref: kit
93
+ title: Kit
94
+ summary: Brookbridge's young postal runner, always willing to help a neighbour.
95
+ aliases: []
96
+ Appearance: Rain-flattened hair and a small dimple when he smiles.
97
+ Clothing: An unbuttoned blue raincoat, striped shirt, and canvas shoes with wet toes.
98
+ Temperament: Eager and easily distracted; owns up to mistakes and tries to put them right.
99
+ Relationships:
100
+ Nora:
101
+ Person: { $ref: character.nora }
102
+ View: A neighbour he has known since childhood; trusts her and wants to be useful.
103
+ Evelyn:
104
+ Person: { $ref: character.evelyn }
105
+ View: A new acquaintance with intriguing drawings; hopes to ask about the leaves she sketches.
106
+ Knowledge:
107
+ Remembered action: Moved a pale blue envelope to make room for his wet ledger, slipping it inside the back cover. Assumed the envelope was empty.
108
+ Current understanding: Has not caught what Evelyn is looking for or connected her letter with the envelope he moved.
109
+ Belongings:
110
+ Ledger: Held against his chest; a corner of the back cover is damp.
111
+ `,
112
+ },
113
+ {
114
+ path: "world/locations/rain-lantern-inn.yaml",
115
+ contents: `$document:
116
+ id: location.rain-lantern-inn
117
+ ref: rain-lantern-inn
118
+ title: Rain Lantern Inn
119
+ summary: A small inn by Brookbridge's bridge, where neighbours shelter over tea.
120
+ aliases: []
121
+ Lobby: A wooden counter faces the entrance, with a blue saucer and brass lamp on top. A small table stands by the window; the umbrellas rest beside the door.
122
+ Daily life: Guests can rest over ginger tea; the innkeeper sometimes holds letters for familiar visitors until morning.
123
+ `,
124
+ },
125
+ {
126
+ path: "world/events/letter-entrusted.yaml",
127
+ contents: `$document:
128
+ id: event.letter-entrusted
129
+ ref: letter-entrusted
130
+ title: A letter left for safekeeping
131
+ summary: Evelyn's earlier arrangement with Nora to keep a letter at the counter.
132
+ aliases: []
133
+ When: About half an hour earlier this evening
134
+ Account: Evelyn gave Nora an unsealed pale blue envelope, explaining that it held a letter and leaf drawing for her sister. Nora put it on the blue saucer while Evelyn watched.
135
+ Words: Evelyn said, “I'll seal it in the morning. Could you keep it here until then?” Nora agreed.
136
+ Source: Both Evelyn and Nora were at the counter.
137
+ `,
138
+ },
139
+ {
140
+ path: "control/frame.yaml",
141
+ contents: `format: narraeon.world-frame/v1
142
+ bindings:
143
+ currentSituation: "@current-situation"
144
+ instructions:
145
+ - markdown: blocks/world.md
146
+ context:
147
+ - slot: { kind: catalog, directory: characters, maxEntries: 3 }
148
+ - slot: { kind: catalog, directory: locations, maxEntries: 4 }
149
+ - slot: { kind: catalog, directory: events, maxEntries: 12 }
150
+ - slot: { kind: document, document: "@rain-lantern-inn" }
151
+ - slot:
152
+ kind: reference_targets
153
+ from:
154
+ document: "@current-situation"
155
+ locator: { yaml: [Present] }
156
+ maxEntries: 3
157
+ - slot: { kind: current_situation }
158
+ - slot: { kind: additional_materials }
159
+ `,
160
+ },
161
+ {
162
+ path: "control/blocks/world.md",
163
+ contents: `<!-- opening.md is the visible opening; world/current-situation.yaml holds the background and pending response; characters/ holds each person's state, relationships, and knowledge; locations/ describes places; events/ records shared past events; frame.yaml selects play material; player-views.yaml selects status values. Adapt these files to write your own story. -->
164
+
165
+ # Genre and boundaries
166
+
167
+ Focus on small-town inn life, conversation, and gentle mysteries arising from objects, memories, and misunderstandings. Keep the tone warm and suitable for all ages, without violence, adult content, or life-threatening conspiracies. Sharing a letter's private contents is up to its writer and recipient.
168
+
169
+ ## Documents and where changes belong
170
+
171
+ Keep relationships, opinions, and knowledge in each person's own world/characters/ document, without a shared truth or secret field. Record events that need shared reference in world/events/; link to the account rather than copying it into each character. Lasting changes to places belong in world/locations/. Keep the letter among its owner's important belongings until independent transfer tracking warrants a separate document.
172
+ `,
173
+ },
174
+ {
175
+ path: "control/player-views.yaml",
176
+ contents: `format: narraeon.player-views/v1
177
+ views:
178
+ - id: scene-status
179
+ title: Right now
180
+ items:
181
+ - id: time
182
+ label: Time
183
+ select:
184
+ document: "@current-situation"
185
+ locator: { yaml: [背景, Time] }
186
+ - id: place
187
+ label: Place
188
+ select:
189
+ document: "@current-situation"
190
+ locator: { yaml: [背景, Place] }
191
+ `,
192
+ },
193
+ ],
194
+ };