narraeon 0.5.5 → 0.5.7
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/node/protocol/appPreferences.js +4 -2
- package/dist/node/protocol/playFailure.js +13 -0
- package/dist/node/protocol/protagonistTemplates.js +13 -0
- package/dist/node/protocol/v1.js +25 -19
- package/dist/node/protocol/worldArchives.js +7 -0
- package/dist/node/runtime/V1Runtime.js +48 -6
- 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 +5 -0
- package/dist/node/runtime/history/HistoryBackground.js +84 -0
- package/dist/node/runtime/history/HistoryQuery.js +7 -2
- package/dist/node/runtime/history/HistoryRendering.js +31 -4
- 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/FileNativeContinuityCorrection.js +2 -0
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +5 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +91 -4
- package/dist/node/runtime/play/LegacyModelBindingFailure.js +10 -0
- package/dist/node/runtime/play/PlayCallChain.js +212 -61
- 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 +10 -11
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +12 -59
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +2 -2
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +7 -4
- package/dist/node/runtime/world/FileNativeHistoryProjection.js +11 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +250 -55
- package/dist/node/runtime/world/WorldActivity.js +143 -0
- package/dist/node/runtime/world/WorldYamlRendering.js +28 -0
- package/dist/node/server/createServer.js +48 -0
- package/dist/node/shared/default-preset-host.js +18 -8
- package/dist/node/shared/default-setting-improvement-prompt.js +4 -0
- package/dist/node/shared/history-use-guidance.js +2 -2
- package/dist/node/shared/ordered-play-prompts.js +2 -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/web/assets/index-DxNtxzLR.js +1255 -0
- package/dist/web/assets/{index-CVN48xrw.css → index-DzOnu9uv.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/dist/web/assets/index-DTFZQan4.js +0 -1223
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { FileNativeAuthorityV3 } from "./FileNativeAuthorityV3.js";
|
|
5
|
+
import { fileNativeHistoryMessageIdFromProjectionPath } from "./FileNativeHistoryProjection.js";
|
|
6
|
+
/** Read only the small head/cache on a hit; repair misses from the local projection. */
|
|
7
|
+
export async function readWorldActivity(root, serializeRebuild = (rebuild) => rebuild()) {
|
|
8
|
+
const head = await currentHead(root);
|
|
9
|
+
const cached = await readCache(root);
|
|
10
|
+
if (cached?.head === head)
|
|
11
|
+
return cached;
|
|
12
|
+
return serializeRebuild(async () => {
|
|
13
|
+
const latestHead = await currentHead(root);
|
|
14
|
+
const latestCache = await readCache(root);
|
|
15
|
+
if (latestCache?.head === latestHead)
|
|
16
|
+
return latestCache;
|
|
17
|
+
const empty = {
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
head: latestHead,
|
|
20
|
+
lastActivityAt: null,
|
|
21
|
+
excerpt: null,
|
|
22
|
+
playerTurns: 0,
|
|
23
|
+
};
|
|
24
|
+
// An accepted endpoint may still be awaiting recovery. Never label its old
|
|
25
|
+
// materialized history as a valid cache for the new endpoint.
|
|
26
|
+
const materialized = await readJson(join(root, "runtime/materialized-head.json"));
|
|
27
|
+
if (!isRecord(materialized) || materialized.head !== latestHead)
|
|
28
|
+
return empty;
|
|
29
|
+
const entries = await readdir(join(root, "history"), {
|
|
30
|
+
withFileTypes: true,
|
|
31
|
+
});
|
|
32
|
+
const messages = entries
|
|
33
|
+
.filter((entry) => entry.isFile())
|
|
34
|
+
.map((entry) => ({
|
|
35
|
+
path: entry.name,
|
|
36
|
+
id: fileNativeHistoryMessageIdFromProjectionPath(entry.name),
|
|
37
|
+
}))
|
|
38
|
+
.filter((message) => message.id !== null)
|
|
39
|
+
.sort((left, right) => {
|
|
40
|
+
const a = left.path.split("-");
|
|
41
|
+
const b = right.path.split("-");
|
|
42
|
+
return Number(a[0]) - Number(b[0]) || Number(a[1]) - Number(b[1]);
|
|
43
|
+
});
|
|
44
|
+
const narrator = messages.findLast(({ id }) => id.endsWith(".narrator"));
|
|
45
|
+
const activity = {
|
|
46
|
+
...empty,
|
|
47
|
+
excerpt: narrator === undefined
|
|
48
|
+
? null
|
|
49
|
+
: excerpt(await readFile(join(root, "history", narrator.path), "utf8")),
|
|
50
|
+
playerTurns: messages.filter(({ id }) => id.endsWith(".player")).length,
|
|
51
|
+
};
|
|
52
|
+
await publishCache(root, activity);
|
|
53
|
+
return activity;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Best effort: activity is never part of accepting or recovering Authority. */
|
|
57
|
+
export async function updateWorldActivity(root, head, history, lastActivityAt) {
|
|
58
|
+
const narrator = history.findLast(({ role }) => role === "narrator");
|
|
59
|
+
// Recovery cannot reconstruct a clock time, but can retain a matching cache.
|
|
60
|
+
const previous = lastActivityAt === null ? await readCache(root) : null;
|
|
61
|
+
await publishCache(root, {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
head,
|
|
64
|
+
lastActivityAt: lastActivityAt ??
|
|
65
|
+
(previous?.head === head ? previous.lastActivityAt : null),
|
|
66
|
+
excerpt: narrator === undefined ? null : excerpt(narrator.exactText),
|
|
67
|
+
playerTurns: history.filter(({ role }) => role === "player").length,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function excerpt(text) {
|
|
71
|
+
const singleLine = text.replace(/[\r\n\u2028\u2029]+/gu, " ").trim();
|
|
72
|
+
return singleLine === ""
|
|
73
|
+
? null
|
|
74
|
+
: Array.from(singleLine).slice(0, 80).join("");
|
|
75
|
+
}
|
|
76
|
+
async function currentHead(root) {
|
|
77
|
+
try {
|
|
78
|
+
return (await new FileNativeAuthorityV3(root).readHead()).head;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!isMissing(error))
|
|
82
|
+
throw error;
|
|
83
|
+
// Listing a released layout must not trigger an Authority migration/replay.
|
|
84
|
+
const checkpoint = await readJson(join(root, "runtime/materialized-head.json"));
|
|
85
|
+
return isRecord(checkpoint) && typeof checkpoint.head === "string"
|
|
86
|
+
? checkpoint.head
|
|
87
|
+
: "genesis";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function readCache(root) {
|
|
91
|
+
const value = await readJson(join(root, "runtime/activity.json")).catch(() => null);
|
|
92
|
+
return isRecord(value) &&
|
|
93
|
+
Object.keys(value).length === 5 &&
|
|
94
|
+
value.schemaVersion === 1 &&
|
|
95
|
+
typeof value.head === "string" &&
|
|
96
|
+
/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.head) &&
|
|
97
|
+
(value.lastActivityAt === null ||
|
|
98
|
+
(typeof value.lastActivityAt === "number" &&
|
|
99
|
+
Number.isSafeInteger(value.lastActivityAt) &&
|
|
100
|
+
value.lastActivityAt >= 0 &&
|
|
101
|
+
Number.isFinite(new Date(value.lastActivityAt).valueOf()))) &&
|
|
102
|
+
(value.excerpt === null ||
|
|
103
|
+
(typeof value.excerpt === "string" &&
|
|
104
|
+
Array.from(value.excerpt).length <= 80 &&
|
|
105
|
+
!/[\r\n\u2028\u2029]/u.test(value.excerpt))) &&
|
|
106
|
+
typeof value.playerTurns === "number" &&
|
|
107
|
+
Number.isSafeInteger(value.playerTurns) &&
|
|
108
|
+
value.playerTurns >= 0
|
|
109
|
+
? value
|
|
110
|
+
: null;
|
|
111
|
+
}
|
|
112
|
+
async function readJson(path) {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (isMissing(error) || error instanceof SyntaxError)
|
|
118
|
+
return null;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function publishCache(root, activity) {
|
|
123
|
+
const path = join(root, "runtime/activity.json");
|
|
124
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
125
|
+
try {
|
|
126
|
+
await writeFile(temporary, `${JSON.stringify(activity)}\n`, {
|
|
127
|
+
mode: 0o600,
|
|
128
|
+
});
|
|
129
|
+
await rename(temporary, path);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.warn("Could not write world activity cache", root, error);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function isRecord(value) {
|
|
139
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
140
|
+
}
|
|
141
|
+
function isMissing(error) {
|
|
142
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
143
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { stringify } from "yaml";
|
|
2
|
+
/** Shared public YAML rendering; references keep their handle without expansion. */
|
|
3
|
+
export function renderWorldYamlSource(value, options = {}) {
|
|
4
|
+
return stringify(publicValue(value), {
|
|
5
|
+
indent: 2,
|
|
6
|
+
lineWidth: 0,
|
|
7
|
+
...options,
|
|
8
|
+
}).trimEnd();
|
|
9
|
+
}
|
|
10
|
+
function publicValue(value) {
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.map(publicValue);
|
|
13
|
+
if (record(value)) {
|
|
14
|
+
if (Object.keys(value).length === 2 &&
|
|
15
|
+
typeof value.$ref === "string" &&
|
|
16
|
+
record(value.target) &&
|
|
17
|
+
typeof value.target.shortRef === "string")
|
|
18
|
+
return { $ref: `@${value.target.shortRef}` };
|
|
19
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, publicValue(child)]));
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function record(value) {
|
|
24
|
+
return (value !== undefined &&
|
|
25
|
+
value !== null &&
|
|
26
|
+
typeof value === "object" &&
|
|
27
|
+
!Array.isArray(value));
|
|
28
|
+
}
|
|
@@ -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,15 +234,20 @@ NPC 可以按性格、认知、处境和目标继续做事、与别人交谈、
|
|
|
234
234
|
|
|
235
235
|
summary 是目录简介,用于识别文档和判断是否需要读取。只写简短的身份、主题或稳定线索,不复述具体状态和事件经过;具体事实放在正文。仅在识别信息改变时更新标题与简介。
|
|
236
236
|
|
|
237
|
-
##
|
|
237
|
+
## 保存时机、整理与检查点
|
|
238
238
|
|
|
239
239
|
先按玩家授权与互动需要确定本轮叙事范围和停笔位置,再保存该范围内的事实。保存进行中活动的实际进展、必要原话和待回应事项即可整理截至此刻的状态,不把尚未发生的完成结果提前写入。
|
|
240
240
|
|
|
241
241
|
1. 玩家视图实际绑定的当前值改变,本轮终态叙事前写回。等待、赶路、休息或跨日后按世界粒度更新对应现值;绑定标记只说明界面读取范围。
|
|
242
|
-
2.
|
|
243
|
-
3.
|
|
242
|
+
2. 当前情境已采用顶层 \`背景\` 时,时间、地点等定位值发生变化应在最终叙事提交前写回该节点,避免在其他情境字段维护同一现值的第二份。没有该字段的世界不必创建,未变化不必写入,仅维护记录不授权推进剧情。
|
|
243
|
+
3. 需要保存但玩家原文和最终叙事没有记录的信息,当轮保存,包括重要场外进展、未表露的认知和隐蔽后果。工具中间文本不能替代持久记录。
|
|
244
|
+
4. 其余可从玩家原文和最终叙事恢复的持续结果,允许事件收尾或合适的中间整理点归并。
|
|
244
245
|
|
|
245
|
-
|
|
246
|
+
按已确定的叙事范围完成本轮必须保存的写入。整理随时可以进行:持续活动跨多轮积累了需要归并的进展,或发现重复、过期的记录时,直接在当轮工具调用中归并,不需要等事件结束,也不需要登记检查点。整理不等于检查点。只有零散即时变化时,按上述保存时机继续互动。
|
|
247
|
+
|
|
248
|
+
world_checkpoint 只用于事件之间的间隙:上一事件已在本轮停笔位置自然告一段落,或场景已实际转换,接下来从全新上下文开始也不会丢失仍然重要的细节。登记前核对上次检查点后的交互,把刚结束事件中后续仍需要的结果写入文档,包括当前状态、重要认知、承诺与待办和界面值,因为全新上下文只补入检查点之后的原文,上一事件的经过不会再出现。持续活动中途、对方刚给出需要玩家回应的反馈,或仍有待玩家决定的事项时,不登记检查点。此前已写回的事实核对后沿用,无需重复写入。登记后输出最终叙事;若叙事又增加重要结果,继续工具调用补齐。登记回执不是语义完整性认证。
|
|
249
|
+
|
|
250
|
+
检查点只标记已经出现的间隙,不是本轮要达成的目标。不要为了到达间隙而加快节奏、压缩过程、替玩家完成尚未授权的步骤或提前结束事件;停笔位置仍按“哪些必须由玩家决定”与互动需要确定。一个事件持续很多轮、跨越多次回复都是正常的,期间不需要登记检查点。
|
|
246
251
|
|
|
247
252
|
## 整理与连续性
|
|
248
253
|
|
|
@@ -489,15 +494,20 @@ Prefer natural owners for durable information. Create a separate document for in
|
|
|
489
494
|
|
|
490
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.
|
|
491
496
|
|
|
492
|
-
## Save timing and
|
|
497
|
+
## Save timing, consolidation and checkpoints
|
|
493
498
|
|
|
494
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.
|
|
495
500
|
|
|
496
501
|
1. Write changed current values actually selected by player views before this turn's final narrative. After waiting, travel, rest or a day change, update those values at the world's granularity; binding annotations describe read locations only.
|
|
497
|
-
2.
|
|
498
|
-
3.
|
|
502
|
+
2. If the current situation already uses the exact top-level \`背景\` field, write changed time, location or other positioning values there before final narrative submission; avoid maintaining a second current copy elsewhere in that situation. Worlds without the field need not create it, unchanged values need no write, and record maintenance does not authorize story advancement.
|
|
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.
|
|
504
|
+
4. Other durable outcomes recoverable from player originals and final narrative may be consolidated at event closure or a suitable intermediate point.
|
|
505
|
+
|
|
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.
|
|
499
509
|
|
|
500
|
-
|
|
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.
|
|
501
511
|
|
|
502
512
|
## Maintenance and continuity
|
|
503
513
|
|
|
@@ -27,6 +27,8 @@ export const defaultSettingImprovementPromptZhCN = `# 系统推荐的创作方
|
|
|
27
27
|
|
|
28
28
|
这些区别适用于物品、伤病、地点、组织、交易、调查与人物关系,不要求固定目录、字段或数量。独立引用、自身状态或生命周期需要独立文档;小规模信息可集中在合适载体中。当前情境只承接眼前地点、在场者、进行中的事及待回应事项,不承担全部历史或未来分支。
|
|
29
29
|
|
|
30
|
+
当前情境的受限 YAML 可选用精确顶层「背景」保存简短定位值,例如「背景: {时间: 青历三年六月初八,夜间, 地点: 南门客栈}」。Runtime 会将该节点的完整值随已提交叙事保存,用于解释旧原文的“明天”“这里”;缺失、null、空白字符串、空映射或空数组不采集,0 和 false 保留。字段内部不设固定 schema,在场名单、待办和长局面留在其他字段;采用后在背景中维护对应现值,避免同一时间地点的第二份现值。无需为未采用的既有内容创建或搬迁字段;运行中修订不会改写旧历史背景。
|
|
31
|
+
|
|
30
32
|
summary 是目录简介:只写识别文档所需的简短身份、主题或稳定线索,不复述具体状态和事件经过。具体事实放在正文,仅在识别信息改变时更新标题与简介。
|
|
31
33
|
|
|
32
34
|
## 承载与发现一起安排
|
|
@@ -65,6 +67,8 @@ Keep important event accounts in one place, using independently referenceable ev
|
|
|
65
67
|
|
|
66
68
|
These distinctions apply to objects, injuries, places, organizations, trades, investigations and personal relationships without fixed directories, fields or counts. Independent reference, evolving state or lifecycle tracking warrants a separate document; smaller records may share a suitable home. The current situation holds the immediate location, people present, ongoing matters and unanswered requests, not all history or future branches.
|
|
67
69
|
|
|
70
|
+
Restricted YAML current situations may optionally use the exact top-level field 背景 for brief positioning values, for example \`背景: {时间: night on day eight, 地点: South Gate Inn}\`. Runtime saves the complete node with committed narrative to interpret old references to tomorrow or here; missing, null, blank strings and empty maps/lists are unrecorded, while 0 and false survive. Its contents have no fixed schema. Keep attendance, pending matters and long situations elsewhere; when adopted, maintain corresponding current values in this node without a second copy. Do not create or relocate fields in existing content merely to adopt this convention. Running-world revisions never rewrite saved historical backgrounds.
|
|
71
|
+
|
|
68
72
|
summary is a catalog description: include only a brief identity, topic or stable clue needed to identify the document, not detailed state or event accounts. Concrete facts belong in the body. Update titles and descriptions only when identifying information changes.
|
|
69
73
|
|
|
70
74
|
## Arrange storage and discovery together
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Mechanical reading guidance shared by production prompts and tool help. */
|
|
2
2
|
export function historyUseGuidance(locale, legacy = false) {
|
|
3
3
|
return locale === "zh-CN"
|
|
4
|
-
? `已有对话或注入原文足够接续时,不例行检索或重读。缺失信息会影响人物行为、重要承诺、因果或玩家选择时才检索${legacy ? ";仅使用本会话冻结的旧工具定义与参数定位及读取最近原文" : ";缺少最近上下文可直接 history_read {latest:4}"}。不记得原词时提出少量候选词,${legacy ? "用已声明旧历史搜索的单个 query 做字面搜索" : "用 history_search queries 的 any/all 字面搜索"},命中后按需完整读前后文${legacy ? "" : ";all 限于同一消息"}。零命中不证明事件未发生,可改词、${legacy ? "扩大字面范围或读相邻上下文" : "放宽 all 为 any 或读相邻上下文"}。原文已足以判断时停止,不为穷尽分页而检索;重要矛盾可继续追查。玩家原文可能只是行动尝试、意图或主张,结果须结合后续叙事;主持原文也可能包含角色谎言、猜测、梦境或回忆,role=narrator
|
|
5
|
-
: `Do not routinely search or reread when conversation or injected originals already suffice. Retrieve only when missing details affect behavior, important promises, causality or player choices${legacy ? "; use only this session's frozen legacy tool definitions and arguments to locate and read recent originals" : "; missing recent context can be read directly with history_read {latest:4}"}. When exact wording is unknown, propose a few candidate terms and ${legacy ? "use the declared legacy literal search with a single query" : "use history_search queries with any/all"}, then read surrounding originals as needed${legacy ? "" : "; all applies within one message"}. Zero literal matches do not prove an event never happened: change terms, ${legacy ? "widen the literal scope or read nearby context" : "relax all to any or read nearby context"}. Stop once originals support the decision, without exhausting pages; important contradictions may warrant further investigation. Player originals may be attempts, intentions or claims; check subsequent narrative for outcomes. Narrator originals may contain character lies, guesses, dreams or memories; role=narrator does not confirm truth. Past possession is not current possession; explicit current world corrections take precedence. Host information does not automatically become in-scene character knowledge. Injected history is already committed material, not the current request; never repeat a final historical player action. Full originals can be large: use length information and expand in small windows. No fixed list→search→read ritual is required.`;
|
|
4
|
+
? `已有对话或注入原文足够接续时,不例行检索或重读。缺失信息会影响人物行为、重要承诺、因果或玩家选择时才检索${legacy ? ";仅使用本会话冻结的旧工具定义与参数定位及读取最近原文" : ";缺少最近上下文可直接 history_read {latest:4}"}。不记得原词时提出少量候选词,${legacy ? "用已声明旧历史搜索的单个 query 做字面搜索" : "用 history_search queries 的 any/all 字面搜索"},命中后按需完整读前后文${legacy ? "" : ";all 限于同一消息"}。零命中不证明事件未发生,可改词、${legacy ? "扩大字面范围或读相邻上下文" : "放宽 all 为 any 或读相邻上下文"}。原文已足以判断时停止,不为穷尽分页而检索;重要矛盾可继续追查。玩家原文可能只是行动尝试、意图或主张,结果须结合后续叙事;主持原文也可能包含角色谎言、猜测、梦境或回忆,role=narrator 不证明事实已确认。历史背景保存的是采集位置的情境,不是当前世界状态,也不认证整段每句话的发生时间;推进前后标记结合原文中的回忆、梦境和局部时间解释。背景在原文边界之外,搜索只匹配原文。过去持有不代表现在持有,明确当前世界修订优先于旧叙事。主持读到的信息不自动成为场内角色的知识。注入历史是已发生材料,不是本轮新请求,不能重复执行最后一条历史玩家行动。读取完整原文可能很大,参考原文长度并按需小步扩展;无需固定 list→search→read 流程。`
|
|
5
|
+
: `Do not routinely search or reread when conversation or injected originals already suffice. Retrieve only when missing details affect behavior, important promises, causality or player choices${legacy ? "; use only this session's frozen legacy tool definitions and arguments to locate and read recent originals" : "; missing recent context can be read directly with history_read {latest:4}"}. When exact wording is unknown, propose a few candidate terms and ${legacy ? "use the declared legacy literal search with a single query" : "use history_search queries with any/all"}, then read surrounding originals as needed${legacy ? "" : "; all applies within one message"}. Zero literal matches do not prove an event never happened: change terms, ${legacy ? "widen the literal scope or read nearby context" : "relax all to any or read nearby context"}. Stop once originals support the decision, without exhausting pages; important contradictions may warrant further investigation. Player originals may be attempts, intentions or claims; check subsequent narrative for outcomes. Narrator originals may contain character lies, guesses, dreams or memories; role=narrator does not confirm truth. Historical background records the captured situation, not current world state or the time of every sentence; interpret before/after annotations alongside memories, dreams and local times in the original. Background stays outside original-text boundaries and literal searches match only originals. Past possession is not current possession; explicit current world corrections take precedence. Host information does not automatically become in-scene character knowledge. Injected history is already committed material, not the current request; never repeat a final historical player action. Full originals can be large: use length information and expand in small windows. No fixed list→search→read ritual is required.`;
|
|
6
6
|
}
|
|
@@ -22,7 +22,7 @@ export function builtinPlayPrompts(locale) {
|
|
|
22
22
|
|
|
23
23
|
world_patch 的 YAML locator 可以包含 map key 和从零开始的数组下标;remove 删除一个精确位置。未提供的元数据保持原值。world_retire 从目录注入中退役文档,文档仍可读取、引用和恢复。工具响应的成功状态变化与后续最终叙事分别提交,后续失败不回滚已提交变化。成功写入回执表示当前值已改变;以真实回执的结算状态为准,模型文字不能提交世界。
|
|
24
24
|
|
|
25
|
-
包含工具调用的响应属于中间步,其中的文字尚未展示或提交为最终叙事。收到全部工具结果后,根据回执在最终正文中呈现本轮尚未展示的内容,并与已提交状态一致;没有工具调用的非空正文结束本次循环并由 Runtime 提交。world_checkpoint
|
|
25
|
+
包含工具调用的响应属于中间步,其中的文字尚未展示或提交为最终叙事。收到全部工具结果后,根据回执在最终正文中呈现本轮尚未展示的内容,并与已提交状态一致;没有工具调用的非空正文结束本次循环并由 Runtime 提交。world_checkpoint 只在事件间隙登记,表示建议从此开始全新上下文,并作为历史重放边界;整理本身不需要登记。在随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文;登记后仍可继续工具调用。登记不结束世界内事件,也不自动切换上下文,玩家自行选择。全新上下文补入边界后的已提交玩家原文与最终叙事,排除开场白、工具和推理。
|
|
26
26
|
|
|
27
27
|
${historyUseGuidance(locale)}`
|
|
28
28
|
: `# Tools and response settlement
|
|
@@ -31,7 +31,7 @@ Use only attached tools. Directories use @dir-* handles from the preload or tool
|
|
|
31
31
|
|
|
32
32
|
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
33
|
|
|
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
|
|
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 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
35
|
|
|
36
36
|
${historyUseGuidance(locale)}`,
|
|
37
37
|
},
|
|
@@ -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
|
+
};
|