narraeon 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/protocol/v1.js +4 -0
- package/dist/node/runtime/V1Runtime.js +13 -1
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +64 -163
- package/dist/node/runtime/content/FileNativeContentTree.js +16 -13
- package/dist/node/runtime/model/ToolArgumentCoercion.js +138 -0
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +34 -18
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +6 -2
- package/dist/node/runtime/play/OrderedPlayPrompts.js +3 -6
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +211 -0
- package/dist/node/runtime/play/PlayCallChain.js +276 -398
- package/dist/node/runtime/play/PlayDocumentTools.js +19 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +3 -3
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -0
- package/dist/node/runtime/prompt/ToolNameAlias.js +14 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +370 -169
- package/dist/node/runtime/world/FileNativeWorldStore.js +151 -109
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -1
- package/dist/node/runtime/world/WorldDocumentStore.js +1 -1
- package/dist/node/runtime/world/WorldLedgerAuthority.js +37 -3
- package/dist/node/runtime/world/WorldTurnLedger.js +17 -4
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -4
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +19 -241
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +8 -27
- package/dist/node/server/createServer.js +1 -4
- package/dist/node/shared/ordered-play-prompts.js +5 -0
- package/dist/node/shared/turn-guide.js +27 -2
- package/dist/web/assets/index-Cj2JYCia.js +1408 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-9N9kO8kY.js +0 -1408
|
@@ -1,89 +1,46 @@
|
|
|
1
1
|
import { isPackageFollowupControlPath } from "../content/PackageFollowups.js";
|
|
2
|
-
import { defaultAppLocale } from "../../protocol/appPreferences.js";
|
|
3
2
|
import { assertSameGameplay, gameplayPath, readGameplay, } from "../../protocol/gameplay.js";
|
|
4
3
|
import { inspectContentPackageCurrentTree } from "../content/FileNativeContentTree.js";
|
|
5
|
-
import { SettingAuthoringTransaction,
|
|
4
|
+
import { authoringToolDefinitions, SettingAuthoringTransaction, worldRevisionAuthoringSurface, } from "../setting/SettingAuthoringTransaction.js";
|
|
6
5
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
7
|
-
const syntheticOpening = {
|
|
8
|
-
path: "opening.md",
|
|
9
|
-
contents: "# Existing world\n\nThe genesis opening is immutable in a world revision.\n",
|
|
10
|
-
};
|
|
11
|
-
const settingToRevisionTool = new Map([
|
|
12
|
-
["setting_list", "world_revision_list"],
|
|
13
|
-
["setting_search", "world_revision_search"],
|
|
14
|
-
["setting_read", "world_revision_read"],
|
|
15
|
-
["setting_create", "world_revision_create"],
|
|
16
|
-
["setting_write_file", "world_revision_write_file"],
|
|
17
|
-
["setting_patch", "world_revision_patch"],
|
|
18
|
-
["setting_move", "world_revision_move"],
|
|
19
|
-
["setting_delete", "world_revision_delete"],
|
|
20
|
-
]);
|
|
21
|
-
const revisionToSettingTool = new Map([...settingToRevisionTool].map(([setting, revision]) => [revision, setting]));
|
|
22
6
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
7
|
+
* The authoring transaction over a playing world's state/control worktree.
|
|
8
|
+
* Its namespace, tool names and wording come from the world-revision surface,
|
|
9
|
+
* so nothing it returns is rewritten after the transaction renders it.
|
|
25
10
|
*/
|
|
26
11
|
export class WorldRevisionAuthoringTransaction {
|
|
27
12
|
#inner;
|
|
28
|
-
#locale;
|
|
29
13
|
constructor(input) {
|
|
30
|
-
this.#locale = input.locale ?? defaultAppLocale;
|
|
31
14
|
this.#inner = new SettingAuthoringTransaction({
|
|
32
|
-
baseFiles:
|
|
33
|
-
|
|
15
|
+
baseFiles: input.baseFiles,
|
|
16
|
+
surface: worldRevisionAuthoringSurface,
|
|
17
|
+
...(input.locale === undefined ? {} : { locale: input.locale }),
|
|
34
18
|
...(input.revision === undefined ? {} : { revision: input.revision }),
|
|
35
19
|
...(input.authorization === undefined
|
|
36
20
|
? {}
|
|
37
21
|
: { authorization: input.authorization }),
|
|
38
|
-
validateFiles: (files) =>
|
|
39
|
-
const candidate = fromProjectedFiles(files);
|
|
40
|
-
assertUsableWorldRevisionFiles(candidate, input.immutableBaseFiles);
|
|
41
|
-
},
|
|
22
|
+
validateFiles: (files) => assertUsableWorldRevisionFiles(files, input.immutableBaseFiles),
|
|
42
23
|
preview: (snapshot) => input.preview(WorldDocumentStore.open({
|
|
43
24
|
layout: "world_state",
|
|
44
|
-
files:
|
|
25
|
+
files: [...snapshot.files].sort((left, right) => left.path.localeCompare(right.path)),
|
|
45
26
|
})),
|
|
46
27
|
});
|
|
47
28
|
}
|
|
48
29
|
files() {
|
|
49
|
-
return
|
|
30
|
+
return this.#inner.files();
|
|
50
31
|
}
|
|
51
32
|
review() {
|
|
52
|
-
return
|
|
33
|
+
return this.#inner.review();
|
|
53
34
|
}
|
|
54
35
|
authorization(revision) {
|
|
55
36
|
return this.#inner.authorization(revision);
|
|
56
37
|
}
|
|
57
38
|
execute(calls) {
|
|
58
|
-
|
|
59
|
-
const executed = this.#inner.execute(mapped.flatMap((item) => ("call" in item ? [item.call] : [])));
|
|
60
|
-
let executedIndex = 0;
|
|
61
|
-
return mapped.map((item, index) => {
|
|
62
|
-
if ("error" in item)
|
|
63
|
-
return rejectedToolResult(calls[index].id, item.error, this.#locale);
|
|
64
|
-
const result = executed[executedIndex++];
|
|
65
|
-
if (result === undefined)
|
|
66
|
-
throw new Error("World-revision authoring returned no tool result");
|
|
67
|
-
return {
|
|
68
|
-
...result,
|
|
69
|
-
markdown: publicWorldRevisionMarkdown(result.markdown),
|
|
70
|
-
changes: result.changes.map(mapDiff),
|
|
71
|
-
};
|
|
72
|
-
});
|
|
39
|
+
return this.#inner.execute(calls);
|
|
73
40
|
}
|
|
74
41
|
}
|
|
75
42
|
export function worldRevisionToolDefinitions(locale, gameplay) {
|
|
76
|
-
return
|
|
77
|
-
const name = settingToRevisionTool.get(definition.name);
|
|
78
|
-
if (name === undefined)
|
|
79
|
-
throw new Error(`Unsupported setting authoring tool: ${definition.name}`);
|
|
80
|
-
return {
|
|
81
|
-
...structuredClone(definition),
|
|
82
|
-
name,
|
|
83
|
-
description: publicWorldRevisionMarkdown(definition.description),
|
|
84
|
-
inputSchema: mapSchema(definition.inputSchema),
|
|
85
|
-
};
|
|
86
|
-
});
|
|
43
|
+
return authoringToolDefinitions(worldRevisionAuthoringSurface, locale, gameplay);
|
|
87
44
|
}
|
|
88
45
|
export function worldRevisionRuntimeContract(locale, gameplay) {
|
|
89
46
|
// A world's mode and scheme are fixed, so only tabletop worlds mention the file.
|
|
@@ -170,21 +127,17 @@ export function inspectWorldRevisionFiles(files, immutableBaseFiles) {
|
|
|
170
127
|
message: "An existing world-state document must keep its document ID and short reference",
|
|
171
128
|
});
|
|
172
129
|
}
|
|
173
|
-
const
|
|
174
|
-
...structuredClone(file),
|
|
175
|
-
path: file.path.startsWith("state/")
|
|
176
|
-
? `world/${file.path.slice("state/".length)}`
|
|
177
|
-
: file.path,
|
|
178
|
-
}));
|
|
179
|
-
const inspection = inspectContentPackageCurrentTree(projected, {
|
|
130
|
+
const inspection = inspectContentPackageCurrentTree(files, {
|
|
180
131
|
requireOpening: false,
|
|
132
|
+
worldDocumentSnapshot: WorldDocumentStore.open({
|
|
133
|
+
layout: "world_revision",
|
|
134
|
+
files,
|
|
135
|
+
}),
|
|
181
136
|
});
|
|
182
137
|
if (inspection.status !== "usable")
|
|
183
138
|
diagnostics.push(...inspection.issues.map(({ code, path, message }) => ({
|
|
184
139
|
code,
|
|
185
|
-
path
|
|
186
|
-
? `state/${path.slice("world/".length)}`
|
|
187
|
-
: path,
|
|
140
|
+
path,
|
|
188
141
|
message,
|
|
189
142
|
})));
|
|
190
143
|
return {
|
|
@@ -223,178 +176,6 @@ export function assertUsableWorldRevisionFiles(files, immutableBaseFiles) {
|
|
|
223
176
|
.map(({ path, message }) => `${path}: ${message}`)
|
|
224
177
|
.join("; "));
|
|
225
178
|
}
|
|
226
|
-
function toProjectedFiles(files) {
|
|
227
|
-
return [
|
|
228
|
-
syntheticOpening,
|
|
229
|
-
...files.map((file) => ({
|
|
230
|
-
...structuredClone(file),
|
|
231
|
-
path: file.path.startsWith("state/")
|
|
232
|
-
? `world/${file.path.slice("state/".length)}`
|
|
233
|
-
: file.path,
|
|
234
|
-
})),
|
|
235
|
-
];
|
|
236
|
-
}
|
|
237
|
-
function fromProjectedFiles(files) {
|
|
238
|
-
const opening = files.find(({ path }) => path === "opening.md");
|
|
239
|
-
if (opening?.contents !== syntheticOpening.contents ||
|
|
240
|
-
opening.encoding !== undefined)
|
|
241
|
-
throw new Error("opening.md is immutable in a world revision");
|
|
242
|
-
return files
|
|
243
|
-
.filter(({ path }) => path !== "opening.md")
|
|
244
|
-
.map((file) => ({
|
|
245
|
-
...structuredClone(file),
|
|
246
|
-
path: file.path.startsWith("world/")
|
|
247
|
-
? `state/${file.path.slice("world/".length)}`
|
|
248
|
-
: file.path,
|
|
249
|
-
}))
|
|
250
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
251
|
-
}
|
|
252
|
-
function mapToolCall(call) {
|
|
253
|
-
const name = revisionToSettingTool.get(call.name);
|
|
254
|
-
if (name === undefined)
|
|
255
|
-
return {
|
|
256
|
-
call: { ...structuredClone(call), name: `unsupported:${call.name}` },
|
|
257
|
-
};
|
|
258
|
-
const args = isRecord(call.arguments)
|
|
259
|
-
? structuredClone(call.arguments)
|
|
260
|
-
: call.arguments;
|
|
261
|
-
if (isRecord(args)) {
|
|
262
|
-
for (const field of [
|
|
263
|
-
"path",
|
|
264
|
-
"directory",
|
|
265
|
-
"within",
|
|
266
|
-
"from",
|
|
267
|
-
"to",
|
|
268
|
-
"document",
|
|
269
|
-
])
|
|
270
|
-
if (typeof args[field] === "string") {
|
|
271
|
-
if (args[field] === "opening.md")
|
|
272
|
-
return { error: "opening" };
|
|
273
|
-
if (args[field] === "world" || args[field].startsWith("world/"))
|
|
274
|
-
return { error: "namespace" };
|
|
275
|
-
args[field] = toProjectedPath(args[field]);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return {
|
|
279
|
-
call: { ...structuredClone(call), name, arguments: args },
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
function toProjectedPath(path) {
|
|
283
|
-
if (path === "state")
|
|
284
|
-
return "world";
|
|
285
|
-
return path.startsWith("state/")
|
|
286
|
-
? `world/${path.slice("state/".length)}`
|
|
287
|
-
: path;
|
|
288
|
-
}
|
|
289
|
-
function mapDiff(diff) {
|
|
290
|
-
return {
|
|
291
|
-
...structuredClone(diff),
|
|
292
|
-
path: diff.path.startsWith("world/")
|
|
293
|
-
? `state/${diff.path.slice("world/".length)}`
|
|
294
|
-
: diff.path,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
function mapReview(review) {
|
|
298
|
-
return {
|
|
299
|
-
...structuredClone(review),
|
|
300
|
-
diff: review.diff.filter(({ path }) => path !== "opening.md").map(mapDiff),
|
|
301
|
-
diagnostics: review.diagnostics.map((diagnostic) => ({
|
|
302
|
-
...diagnostic,
|
|
303
|
-
path: diagnostic.path.startsWith("world/")
|
|
304
|
-
? `state/${diagnostic.path.slice("world/".length)}`
|
|
305
|
-
: diagnostic.path,
|
|
306
|
-
})),
|
|
307
|
-
playCoverage: review.playCoverage === null
|
|
308
|
-
? null
|
|
309
|
-
: {
|
|
310
|
-
...structuredClone(review.playCoverage),
|
|
311
|
-
changed: review.playCoverage.changed
|
|
312
|
-
.filter(({ path }) => path !== "opening.md")
|
|
313
|
-
.map((item) => ({ ...item, path: mapPublicPath(item.path) })),
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
function mapPublicPath(path) {
|
|
318
|
-
return path.startsWith("world/")
|
|
319
|
-
? `state/${path.slice("world/".length)}`
|
|
320
|
-
: path;
|
|
321
|
-
}
|
|
322
|
-
function publicWorldRevisionMarkdown(markdown) {
|
|
323
|
-
let fenced = false;
|
|
324
|
-
let documentBody = false;
|
|
325
|
-
let opaqueBody = false;
|
|
326
|
-
return markdown
|
|
327
|
-
.split("\n")
|
|
328
|
-
.flatMap((line) => {
|
|
329
|
-
if (line.trim() === "[body]") {
|
|
330
|
-
documentBody = true;
|
|
331
|
-
return [line];
|
|
332
|
-
}
|
|
333
|
-
if (line.trim() === "[/body]") {
|
|
334
|
-
documentBody = false;
|
|
335
|
-
return [line];
|
|
336
|
-
}
|
|
337
|
-
if (line.trimStart().startsWith("```")) {
|
|
338
|
-
fenced = !fenced;
|
|
339
|
-
return [line];
|
|
340
|
-
}
|
|
341
|
-
if (line.startsWith("# Special-file source ") ||
|
|
342
|
-
line.startsWith("# 专用文件原文 "))
|
|
343
|
-
opaqueBody = true;
|
|
344
|
-
else if (opaqueBody && line === "---")
|
|
345
|
-
opaqueBody = false;
|
|
346
|
-
if (fenced ||
|
|
347
|
-
documentBody ||
|
|
348
|
-
(opaqueBody &&
|
|
349
|
-
!line.startsWith("# Special-file source ") &&
|
|
350
|
-
!line.startsWith("# 专用文件原文 ")) ||
|
|
351
|
-
/^\s+"/u.test(line))
|
|
352
|
-
return [line];
|
|
353
|
-
const mapped = line
|
|
354
|
-
.replaceAll("setting_", "world_revision_")
|
|
355
|
-
.replaceAll("content package", "world revision")
|
|
356
|
-
.replaceAll("Content package", "World revision")
|
|
357
|
-
.replaceAll("内容包当前树", "世界修订工作树")
|
|
358
|
-
.replaceAll("内容包", "世界修订")
|
|
359
|
-
.replaceAll("opening and control", "control")
|
|
360
|
-
.replaceAll("opening/control", "control")
|
|
361
|
-
.replaceAll("opening.md, ", "")
|
|
362
|
-
.replaceAll("opening 和 control", "control")
|
|
363
|
-
.replaceAll("opening.md、", "")
|
|
364
|
-
.replaceAll("world/", "state/");
|
|
365
|
-
return /^- \[(?:special|binary)\] opening\.md$/u.test(mapped.trim())
|
|
366
|
-
? []
|
|
367
|
-
: [mapped];
|
|
368
|
-
})
|
|
369
|
-
.join("\n");
|
|
370
|
-
}
|
|
371
|
-
function rejectedToolResult(toolCallId, error, locale) {
|
|
372
|
-
const message = error === "opening"
|
|
373
|
-
? locale === "zh-CN"
|
|
374
|
-
? "世界创建后 opening.md 已成为不可变创世叙事。请改写 state/* 或 control/*。"
|
|
375
|
-
: "opening.md is immutable after a world has been created. Edit state/* or control/* instead."
|
|
376
|
-
: locale === "zh-CN"
|
|
377
|
-
? "世界修订只公开 state/* 与 control/* 逻辑路径;不能使用内容包的 world/* 命名空间。"
|
|
378
|
-
: "World revision exposes only state/* and control/* logical paths; the content-package world/* namespace is unavailable.";
|
|
379
|
-
return {
|
|
380
|
-
toolCallId,
|
|
381
|
-
markdown: `# Runtime tool rejected\n\n${message}`,
|
|
382
|
-
isError: true,
|
|
383
|
-
changes: [],
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
function mapSchema(schema) {
|
|
387
|
-
const map = (value) => {
|
|
388
|
-
if (typeof value === "string")
|
|
389
|
-
return value.replaceAll("world", "state");
|
|
390
|
-
if (Array.isArray(value))
|
|
391
|
-
return value.map(map);
|
|
392
|
-
if (!isRecord(value))
|
|
393
|
-
return value;
|
|
394
|
-
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, map(child)]));
|
|
395
|
-
};
|
|
396
|
-
return map(schema);
|
|
397
|
-
}
|
|
398
179
|
function isWritableWorldRevisionPath(path) {
|
|
399
180
|
if (/^state\/.+\.(?:ya?ml|md)$/u.test(path))
|
|
400
181
|
return true;
|
|
@@ -404,6 +185,3 @@ function isWritableWorldRevisionPath(path) {
|
|
|
404
185
|
path === "control/player-views.yaml" ||
|
|
405
186
|
/^control\/blocks\/.+\.md$/u.test(path));
|
|
406
187
|
}
|
|
407
|
-
function isRecord(value) {
|
|
408
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
409
|
-
}
|
|
@@ -190,25 +190,18 @@ export class WorldRevisionWorkspace {
|
|
|
190
190
|
layout: "world_state",
|
|
191
191
|
files: epoch.files,
|
|
192
192
|
}));
|
|
193
|
-
|
|
193
|
+
// state 与 control 的改动是同一轮 revision_apply:要么整轮成立,要么整轮不存在(#102 的账本恢复)。
|
|
194
|
+
const outcome = await this.#worlds.commitWorldRevisionApply({
|
|
194
195
|
...(worldClock === undefined ? {} : { worldClock }),
|
|
195
196
|
operationId: epoch.apply.operationId,
|
|
196
197
|
worldId: epoch.worldId,
|
|
197
198
|
parentHead: epoch.baseHead,
|
|
198
199
|
nextMaterials: binding.additionalMaterials,
|
|
199
200
|
stateChanges: stateChanges(epoch.baseFiles, epoch.files),
|
|
201
|
+
control: controlFiles(epoch.files),
|
|
200
202
|
worldRevisionLock: handle,
|
|
201
|
-
validationControl: controlFiles(epoch.files),
|
|
202
203
|
});
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
if (epoch.apply?.phase === "state_committed") {
|
|
206
|
-
await this.#worlds.replaceWorldRevisionControl({
|
|
207
|
-
handle,
|
|
208
|
-
expectedFingerprint: epoch.baseControlFingerprint,
|
|
209
|
-
files: controlFiles(epoch.files),
|
|
210
|
-
});
|
|
211
|
-
await this.#recordControlPublication(handle);
|
|
204
|
+
await this.#recordCommit(handle, outcome);
|
|
212
205
|
}
|
|
213
206
|
epoch = await this.#finishApply(handle);
|
|
214
207
|
await this.#worlds.releaseWorldRevisionLock(handle);
|
|
@@ -309,7 +302,7 @@ export class WorldRevisionWorkspace {
|
|
|
309
302
|
if (epoch.lifecycle !== "applying" || epoch.apply === null)
|
|
310
303
|
throw new WorldRevisionWorkspaceError("Apply recovery state is damaged");
|
|
311
304
|
epoch.lifecycle = "applied";
|
|
312
|
-
epoch.apply.phase = "
|
|
305
|
+
epoch.apply.phase = "committed";
|
|
313
306
|
epoch.apply.committedHead = epoch.baseHead;
|
|
314
307
|
epoch.finishedAt = Date.now();
|
|
315
308
|
epoch.updatedAt = epoch.finishedAt;
|
|
@@ -317,34 +310,22 @@ export class WorldRevisionWorkspace {
|
|
|
317
310
|
return epoch;
|
|
318
311
|
});
|
|
319
312
|
}
|
|
320
|
-
async #
|
|
313
|
+
async #recordCommit(handle, outcome) {
|
|
321
314
|
return this.#withRevisionOwnership(handle, async () => {
|
|
322
315
|
const epoch = await this.#store.readEpoch(handle.epochId);
|
|
323
316
|
if (epoch.lifecycle !== "applying" || epoch.apply === null)
|
|
324
317
|
throw new WorldRevisionWorkspaceError("Apply recovery state is damaged");
|
|
325
|
-
epoch.apply.phase = "
|
|
318
|
+
epoch.apply.phase = "committed";
|
|
326
319
|
epoch.apply.committedHead = outcome.head;
|
|
327
320
|
epoch.updatedAt = Date.now();
|
|
328
321
|
await this.#store.saveEpoch(epoch);
|
|
329
322
|
return epoch;
|
|
330
323
|
});
|
|
331
324
|
}
|
|
332
|
-
async #recordControlPublication(handle) {
|
|
333
|
-
return this.#withRevisionOwnership(handle, async () => {
|
|
334
|
-
const epoch = await this.#store.readEpoch(handle.epochId);
|
|
335
|
-
if (epoch.lifecycle !== "applying" || epoch.apply === null)
|
|
336
|
-
throw new WorldRevisionWorkspaceError("Apply recovery state is damaged");
|
|
337
|
-
epoch.apply.phase = "control_published";
|
|
338
|
-
epoch.updatedAt = Date.now();
|
|
339
|
-
await this.#store.saveEpoch(epoch);
|
|
340
|
-
return epoch;
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
325
|
async #finishApply(handle) {
|
|
344
326
|
return this.#withRevisionOwnership(handle, async () => {
|
|
345
327
|
const epoch = await this.#store.readEpoch(handle.epochId);
|
|
346
|
-
if (epoch.lifecycle !== "applying" ||
|
|
347
|
-
epoch.apply?.phase !== "control_published")
|
|
328
|
+
if (epoch.lifecycle !== "applying" || epoch.apply?.phase !== "committed")
|
|
348
329
|
throw new WorldRevisionWorkspaceError("Apply recovery state is damaged");
|
|
349
330
|
epoch.lifecycle = "applied";
|
|
350
331
|
epoch.finishedAt = Date.now();
|
|
@@ -273,8 +273,5 @@ function isPlayCallChainView(value) {
|
|
|
273
273
|
value.status === "interrupted"));
|
|
274
274
|
}
|
|
275
275
|
function hasPlayCancellation(value) {
|
|
276
|
-
|
|
277
|
-
return false;
|
|
278
|
-
const last = value.events.at(-1);
|
|
279
|
-
return isRecord(last) && last.kind === "cancellation";
|
|
276
|
+
return isRecord(value) && value.lastCancelled === true;
|
|
280
277
|
}
|
|
@@ -16,6 +16,11 @@ export function isPromptGroup(value) {
|
|
|
16
16
|
/** Soft limits for the turn guide; exceeding them warns without rejecting. */
|
|
17
17
|
export const turnGuideLineSoftLimit = 120;
|
|
18
18
|
export const turnGuideLinesSoftLimit = 10;
|
|
19
|
+
/** One turn-guide line: no line breaks, bounded length; empty clears a builtin default. */
|
|
20
|
+
const turnGuideLinePattern = /^[^\r\n\0]{0,1000}$/u;
|
|
21
|
+
export function isTurnGuideLine(value) {
|
|
22
|
+
return turnGuideLinePattern.test(value);
|
|
23
|
+
}
|
|
19
24
|
/**
|
|
20
25
|
* Unscoped prompts always apply. A scoped prompt applies only when the compiled
|
|
21
26
|
* world or package is tabletop, so fiction requests never see it.
|
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { turnGuideLineSoftLimit, turnGuideLinesSoftLimit, } from "./ordered-play-prompts.js";
|
|
2
|
+
/** The source of the player's one-turn request line. */
|
|
3
|
+
export const turnNoteSource = "player:turn";
|
|
2
4
|
/**
|
|
3
5
|
* The fixed Runtime header. It is mechanical: it tells the model what the
|
|
4
6
|
* suffix is and that the prose, not a reply to the suffix, is wanted.
|
|
5
7
|
*/
|
|
6
8
|
export function turnGuideHeader(locale) {
|
|
7
9
|
return locale === "zh-CN"
|
|
8
|
-
? "[Runtime 本轮提示]\n
|
|
9
|
-
: "[Runtime turn guide]\nThese are the
|
|
10
|
+
? "[Runtime 本轮提示]\n以下是对这一轮正文的提示,来自预设、世界设定和玩家的临时要求。不要回应本提示,直接写正文。"
|
|
11
|
+
: "[Runtime turn guide]\nThese are notes for this turn's prose, from the preset, the world, and the player's requests for this turn. Do not reply to this notice; write the prose.";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The player's one-turn request as a turn-guide line. The prefix only names
|
|
15
|
+
* the source; it adds no constraint of its own.
|
|
16
|
+
*/
|
|
17
|
+
export function turnNoteLine(locale, turnNote) {
|
|
18
|
+
return {
|
|
19
|
+
source: turnNoteSource,
|
|
20
|
+
text: locale === "zh-CN"
|
|
21
|
+
? `玩家本轮要求:${turnNote}`
|
|
22
|
+
: `Player's request for this turn: ${turnNote}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Appends the player's request line to a compiled turn guide, creating the
|
|
27
|
+
* suffix when no preset or world line exists. Without a request the guide is
|
|
28
|
+
* returned unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export function turnGuideWithTurnNote(locale, guide, turnNote) {
|
|
31
|
+
if (turnNote === undefined)
|
|
32
|
+
return guide;
|
|
33
|
+
const lines = [...(guide?.lines ?? []), turnNoteLine(locale, turnNote)];
|
|
34
|
+
return { text: turnGuideText(locale, lines), lines };
|
|
10
35
|
}
|
|
11
36
|
/** The complete suffix text: header, then one line per enabled entry in list order. */
|
|
12
37
|
export function turnGuideText(locale, lines) {
|