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.
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/dist/node/protocol/appPreferences.js +4 -2
- package/dist/node/protocol/gameplay.js +69 -0
- package/dist/node/protocol/playFailure.js +13 -0
- package/dist/node/protocol/protagonistTemplates.js +13 -0
- package/dist/node/protocol/resolution.js +274 -0
- package/dist/node/protocol/v1.js +40 -19
- package/dist/node/protocol/worldArchives.js +7 -0
- package/dist/node/runtime/RuntimeErrorLog.js +95 -0
- package/dist/node/runtime/V1Runtime.js +103 -8
- package/dist/node/runtime/archive/WorldArchiveZip.js +202 -0
- package/dist/node/runtime/archive/WorldArchives.js +389 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +23 -10
- package/dist/node/runtime/config/ProtagonistTemplateStore.js +134 -0
- package/dist/node/runtime/content/ContentWorkspace.js +15 -2
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +43 -2
- package/dist/node/runtime/content/FileNativeContentTree.js +8 -1
- package/dist/node/runtime/history/StoryExport.js +33 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +34 -2
- package/dist/node/runtime/model/ModelHost.js +12 -1
- package/dist/node/runtime/model/ModelHostStream.js +11 -3
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +37 -1
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +48 -3
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +87 -4
- package/dist/node/runtime/play/LegacyModelBindingFailure.js +10 -0
- package/dist/node/runtime/play/OrderedPlayPrompts.js +68 -6
- package/dist/node/runtime/play/PlayCallChain.js +177 -57
- package/dist/node/runtime/play/PlayCallChainError.js +3 -1
- package/dist/node/runtime/play/PlayContinuity.js +3 -20
- package/dist/node/runtime/play/PlayFailure.js +20 -0
- package/dist/node/runtime/play/PlayTraceRetention.js +3 -11
- package/dist/node/runtime/play/TabletopResolution.js +372 -0
- package/dist/node/runtime/play/TabletopRules.js +141 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +49 -32
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -4
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +4 -1
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +56 -13
- package/dist/node/runtime/setting/SettingImprovementSession.js +6 -4
- package/dist/node/runtime/world/FileNativeWorldStore.js +207 -40
- package/dist/node/runtime/world/WorldActivity.js +143 -0
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -0
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +35 -6
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +9 -5
- package/dist/node/server/ConversationStream.js +2 -0
- package/dist/node/server/createServer.js +48 -0
- package/dist/node/shared/default-preset-host.js +10 -6
- package/dist/node/shared/ordered-author-prompts.js +8 -0
- package/dist/node/shared/ordered-play-prompts.js +25 -2
- package/dist/node/shared/sample-packages/index.js +10 -0
- package/dist/node/shared/sample-packages/rainy-inn-en.js +194 -0
- package/dist/node/shared/sample-packages/rainy-inn-zh.js +194 -0
- package/dist/node/shared/tabletop-guidance.js +100 -0
- package/dist/web/assets/index-BJ-1Mb0e.css +1 -0
- package/dist/web/assets/index-BgBL8G-A.js +1323 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/dist/web/assets/index-BH8NyuE4.js +0 -1233
- 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
|
|
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/*
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
+
};
|