narraeon 0.5.7 → 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 +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/node/protocol/gameplay.js +69 -0
- package/dist/node/protocol/resolution.js +274 -0
- package/dist/node/protocol/v1.js +15 -0
- package/dist/node/runtime/RuntimeErrorLog.js +95 -0
- package/dist/node/runtime/V1Runtime.js +57 -2
- package/dist/node/runtime/content/ContentWorkspace.js +10 -2
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +43 -2
- package/dist/node/runtime/content/FileNativeContentTree.js +8 -1
- 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 +7 -0
- package/dist/node/runtime/play/OrderedPlayPrompts.js +68 -6
- package/dist/node/runtime/play/PlayCallChain.js +33 -5
- 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 +45 -10
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +35 -2
- 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 +36 -2
- 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/shared/ordered-author-prompts.js +8 -0
- package/dist/node/shared/ordered-play-prompts.js +23 -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 +1 -1
- package/dist/web/assets/index-DxNtxzLR.js +0 -1255
- package/dist/web/assets/index-DzOnu9uv.css +0 -1
|
@@ -4,6 +4,7 @@ import { readFileSync, unlinkSync } from "node:fs";
|
|
|
4
4
|
import { link, open, mkdir, readFile, readdir, rename, rm, stat, unlink, } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, posix, win32 } from "node:path";
|
|
6
6
|
import { portableContentTreePathKey } from "../../protocol/contentTree.js";
|
|
7
|
+
import { assertSameGameplayMode, gameplayFile, gameplayPath, readGameplay, } from "../../protocol/gameplay.js";
|
|
7
8
|
import { defaultContentWorkspaceLimits, } from "./ContentLimits.js";
|
|
8
9
|
import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, worldDocumentTextRequirement, } from "./FileNativeContentTree.js";
|
|
9
10
|
const localIdPattern = /^package-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
@@ -228,6 +229,29 @@ export class CurrentTreeContentLibrary {
|
|
|
228
229
|
}
|
|
229
230
|
});
|
|
230
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Change only the resolution scheme of a tabletop package. The edit reads the
|
|
234
|
+
* tree under the same lease as ordinary saves, so it publishes atomically and
|
|
235
|
+
* follows the same conflict rules.
|
|
236
|
+
*/
|
|
237
|
+
setGameplayScheme(localId, scheme, options = {}) {
|
|
238
|
+
return this.#mutate(async () => {
|
|
239
|
+
const lease = await this.acquireOperationLease(localId, {
|
|
240
|
+
wait: options.waitForLease !== false,
|
|
241
|
+
});
|
|
242
|
+
try {
|
|
243
|
+
options.signal?.throwIfAborted();
|
|
244
|
+
const current = lease.package;
|
|
245
|
+
if (current.gameplay.mode !== "tabletop")
|
|
246
|
+
throw new InvalidContentTreeError("Only a tabletop content package has a resolution scheme / 只有跑团内容包有判定方案");
|
|
247
|
+
const replacement = gameplayFile({ mode: "tabletop", scheme });
|
|
248
|
+
return await lease.replace(current.files.map((file) => file.path === gameplayPath ? replacement : file), options);
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
lease.release();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
231
255
|
async acquireOperationLease(localId, options = {}) {
|
|
232
256
|
this.#packageRoot(localId);
|
|
233
257
|
await mkdir(this.#locksRoot, { recursive: true, mode: 0o700 });
|
|
@@ -281,6 +305,7 @@ export class CurrentTreeContentLibrary {
|
|
|
281
305
|
throw new CurrentTreeContentPackageBusyError();
|
|
282
306
|
options.signal?.throwIfAborted();
|
|
283
307
|
const normalized = normalizeCurrentContentTreeFiles(files, this.#limits);
|
|
308
|
+
assertGameplayModeUnchanged(package_.files, normalized);
|
|
284
309
|
return this.#replaceNormalizedCurrentTree(localId, package_.title, normalized, options.signal);
|
|
285
310
|
},
|
|
286
311
|
release: () => {
|
|
@@ -476,7 +501,7 @@ export class CurrentTreeContentLibrary {
|
|
|
476
501
|
}
|
|
477
502
|
function detail(localId, title, files, inspection) {
|
|
478
503
|
return {
|
|
479
|
-
...summary(localId, title, inspection),
|
|
504
|
+
...summary(localId, title, readGameplay(files), inspection),
|
|
480
505
|
files: files.map((file) => ({ ...file })),
|
|
481
506
|
documents: structuredClone(inspection.documents),
|
|
482
507
|
issues: structuredClone(inspection.issues),
|
|
@@ -484,8 +509,9 @@ function detail(localId, title, files, inspection) {
|
|
|
484
509
|
control: structuredClone(inspection.control),
|
|
485
510
|
};
|
|
486
511
|
}
|
|
487
|
-
function summary(localId, title, inspection) {
|
|
512
|
+
function summary(localId, title, gameplay, inspection) {
|
|
488
513
|
return {
|
|
514
|
+
gameplay: { ...gameplay },
|
|
489
515
|
localId,
|
|
490
516
|
title,
|
|
491
517
|
status: inspection.status,
|
|
@@ -495,6 +521,7 @@ function summary(localId, title, inspection) {
|
|
|
495
521
|
}
|
|
496
522
|
function packageSummary(package_) {
|
|
497
523
|
return {
|
|
524
|
+
gameplay: { ...package_.gameplay },
|
|
498
525
|
localId: package_.localId,
|
|
499
526
|
title: package_.title,
|
|
500
527
|
status: package_.status,
|
|
@@ -547,8 +574,22 @@ export function normalizeCurrentContentTreeFiles(files, limits = defaultLimits)
|
|
|
547
574
|
...(file.encoding === undefined ? {} : { encoding: file.encoding }),
|
|
548
575
|
});
|
|
549
576
|
}
|
|
577
|
+
try {
|
|
578
|
+
readGameplay(normalized);
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
throw new InvalidContentTreeError(error instanceof Error ? error.message : "Invalid gameplay identity");
|
|
582
|
+
}
|
|
550
583
|
return normalized.sort((left, right) => compareStrings(left.path, right.path));
|
|
551
584
|
}
|
|
585
|
+
function assertGameplayModeUnchanged(before, after) {
|
|
586
|
+
try {
|
|
587
|
+
assertSameGameplayMode(before, after);
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
throw new InvalidContentTreeError(error instanceof Error ? error.message : "Gameplay mode is fixed");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
552
593
|
function decodeCanonicalBase64(contents, path) {
|
|
553
594
|
if (contents.length % 4 !== 0 ||
|
|
554
595
|
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(contents)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { gameplayPath, readGameplay } from "../../protocol/gameplay.js";
|
|
1
2
|
import { readPackageFollowups } from "./PackageFollowups.js";
|
|
2
3
|
import { parseDocument } from "yaml";
|
|
3
4
|
import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
|
|
@@ -45,7 +46,7 @@ export function inspectContentPackageCurrentTree(files, options = {}) {
|
|
|
45
46
|
}
|
|
46
47
|
const unsupportedPaths = new Set();
|
|
47
48
|
for (const file of snapshotFiles) {
|
|
48
|
-
if (/\.json$/iu.test(file.path)) {
|
|
49
|
+
if (/\.json$/iu.test(file.path) && file.path !== gameplayPath) {
|
|
49
50
|
unsupportedPaths.add(file.path);
|
|
50
51
|
issue(issues, "unsupported_content_file", file.path, "The current content package does not accept manifest, seven-field JSON, schema, record, or revision files");
|
|
51
52
|
}
|
|
@@ -116,6 +117,12 @@ export function inspectContentPackageCurrentTree(files, options = {}) {
|
|
|
116
117
|
issue(issues, "invalid_player_view", "control/player-views.yaml", `${formatProblems.length - maxReportedPlayerViewProblems} more player-view issues were omitted`);
|
|
117
118
|
}
|
|
118
119
|
}
|
|
120
|
+
try {
|
|
121
|
+
readGameplay(snapshotFiles);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
issue(issues, "invalid_gameplay", gameplayPath, error instanceof Error ? error.message : "Invalid gameplay");
|
|
125
|
+
}
|
|
119
126
|
try {
|
|
120
127
|
readPackageFollowups(snapshotFiles);
|
|
121
128
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isNarrativeOrigin, isHistoryBackground, } from "../history/HistoryBackground.js";
|
|
2
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { link, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -57,6 +57,33 @@ export class FileNativePlayAdvanceStore {
|
|
|
57
57
|
recordedAt: durableBase.createdAt,
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Return the advance's secret resolution seed, creating it on first need.
|
|
62
|
+
* Recovery reads the same fact, so every tool call in the advance draws from
|
|
63
|
+
* one persisted stream and a crashed batch cannot resample.
|
|
64
|
+
*/
|
|
65
|
+
async resolutionSeed(base) {
|
|
66
|
+
const durableBase = await this.#assertCurrent(base);
|
|
67
|
+
const path = this.#factPath(base, "resolution-seed.json");
|
|
68
|
+
if ((await readOptionalJson(path)) === null) {
|
|
69
|
+
try {
|
|
70
|
+
await publishImmutableJson(path, {
|
|
71
|
+
schemaVersion: 1,
|
|
72
|
+
stage: "resolution_seed",
|
|
73
|
+
seed: randomBytes(32).toString("hex"),
|
|
74
|
+
recordedAt: durableBase.createdAt,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// A concurrent writer may have published first; the stored fact wins.
|
|
79
|
+
if ((await readOptionalJson(path)) === null)
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const fact = await readJson(path);
|
|
84
|
+
assertResolutionSeedFact(fact);
|
|
85
|
+
return fact.seed;
|
|
86
|
+
}
|
|
60
87
|
async markSettled(base, head) {
|
|
61
88
|
const durableBase = await this.#assertCurrent(base);
|
|
62
89
|
await publishImmutableJson(this.#factPath(base, "settled.json"), {
|
|
@@ -115,6 +142,15 @@ function sameAdvanceBase(left, right) {
|
|
|
115
142
|
void rightCreatedAt;
|
|
116
143
|
return isDeepStrictEqual(leftIdentity, rightIdentity);
|
|
117
144
|
}
|
|
145
|
+
function assertResolutionSeedFact(value) {
|
|
146
|
+
if (!isRecord(value) ||
|
|
147
|
+
value.schemaVersion !== 1 ||
|
|
148
|
+
value.stage !== "resolution_seed" ||
|
|
149
|
+
typeof value.seed !== "string" ||
|
|
150
|
+
!/^[a-f0-9]{64}$/.test(value.seed) ||
|
|
151
|
+
typeof value.recordedAt !== "number")
|
|
152
|
+
throw new Error("Invalid persisted resolution seed");
|
|
153
|
+
}
|
|
118
154
|
function assertBase(value) {
|
|
119
155
|
if (!isRecord(value) ||
|
|
120
156
|
value.schemaVersion !== 1 ||
|
|
@@ -3,7 +3,7 @@ export { validatePlayPresetArtifactPayload } from "../../shared/artifact-payload
|
|
|
3
3
|
import { defaultFollowupItems, } from "../../shared/ordered-followups.js";
|
|
4
4
|
import { parseFollowupItems } from "./OrderedFollowups.js";
|
|
5
5
|
import { builtinAuthorPrompts, defaultOrderedAuthorPrompts, } from "../../shared/ordered-author-prompts.js";
|
|
6
|
-
import { parseOrderedPlayPrompts, migrateLegacyPlayPrompts, } from "./OrderedPlayPrompts.js";
|
|
6
|
+
import { parseOrderedPlayPrompts, migrateLegacyPlayPrompts, withTabletopPromptBuiltins, } from "./OrderedPlayPrompts.js";
|
|
7
7
|
import { defaultOrderedPlayPrompts, } from "../../shared/ordered-play-prompts.js";
|
|
8
8
|
import { createHash, randomUUID } from "node:crypto";
|
|
9
9
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
@@ -203,12 +203,17 @@ export class FileNativePlayPresetStore {
|
|
|
203
203
|
};
|
|
204
204
|
this.#locale = options.locale ?? (() => defaultAppLocale);
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Startup is the only place that upgrades stored presets. Reads never write:
|
|
208
|
+
* list, bind and freeze observe the library as it was last saved.
|
|
209
|
+
*/
|
|
206
210
|
async initialize() {
|
|
207
211
|
await this.#change(async () => {
|
|
208
212
|
const document = await this.#read();
|
|
209
213
|
const migrated = this.#markLegacyBuiltinDefault(document);
|
|
210
214
|
const synchronized = this.#synchronizeBuiltinDefault(document);
|
|
211
|
-
|
|
215
|
+
const tabletop = this.#migrateTabletopPromptBuiltins(document);
|
|
216
|
+
if (migrated || synchronized || tabletop)
|
|
212
217
|
await this.#write(document);
|
|
213
218
|
});
|
|
214
219
|
}
|
|
@@ -452,7 +457,8 @@ export class FileNativePlayPresetStore {
|
|
|
452
457
|
.map((path) => ({ path, contents: binding.files[path] }));
|
|
453
458
|
}
|
|
454
459
|
async importPortable(input) {
|
|
455
|
-
|
|
460
|
+
// A released preset gains the tabletop builtins at import, as at upgrade.
|
|
461
|
+
const files = withTabletopPromptBuiltins(toFileMap(input.files, this.#limits));
|
|
456
462
|
const parsed = parsePlayPresetFiles(files, this.#limits);
|
|
457
463
|
if (parsed.kind === "invalid")
|
|
458
464
|
throw parsed.error;
|
|
@@ -561,6 +567,45 @@ export class FileNativePlayPresetStore {
|
|
|
561
567
|
preset.builtinDefault = true;
|
|
562
568
|
return true;
|
|
563
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* One-time upgrade for ordered presets saved before the tabletop builtins
|
|
572
|
+
* existed. An affected revision is replaced by a new revision that inserts
|
|
573
|
+
* them; the old revision stays for frozen requests. A pending draft gets the
|
|
574
|
+
* same insertion, because a draft that kept the old list would fail the
|
|
575
|
+
* builtin-retention rule on save. The library is one atomically written file,
|
|
576
|
+
* so an interrupted upgrade leaves the released bytes and reruns cleanly.
|
|
577
|
+
*/
|
|
578
|
+
#migrateTabletopPromptBuiltins(document) {
|
|
579
|
+
let changed = false;
|
|
580
|
+
for (const stored of document.presets) {
|
|
581
|
+
const current = stored.revisions[stored.currentRevision];
|
|
582
|
+
if (current !== undefined) {
|
|
583
|
+
const migrated = withTabletopPromptBuiltins(current);
|
|
584
|
+
if (migrated !== current) {
|
|
585
|
+
const revision = revisionForFiles(migrated, this.#limits);
|
|
586
|
+
stored.revisions[revision] = cloneFiles(migrated);
|
|
587
|
+
stored.currentRevision = revision;
|
|
588
|
+
changed = true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const draft = stored.draftRevision === undefined
|
|
592
|
+
? undefined
|
|
593
|
+
: stored.revisions[stored.draftRevision];
|
|
594
|
+
if (draft !== undefined) {
|
|
595
|
+
const migrated = withTabletopPromptBuiltins(draft);
|
|
596
|
+
if (migrated !== draft) {
|
|
597
|
+
const revision = revisionForFiles(migrated, this.#limits);
|
|
598
|
+
stored.revisions[revision] = cloneFiles(migrated);
|
|
599
|
+
if (revision === stored.currentRevision)
|
|
600
|
+
delete stored.draftRevision;
|
|
601
|
+
else
|
|
602
|
+
stored.draftRevision = revision;
|
|
603
|
+
changed = true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return changed;
|
|
608
|
+
}
|
|
564
609
|
#synchronizeBuiltinDefault(document) {
|
|
565
610
|
const preset = document.presets.find(({ builtinDefault }) => builtinDefault === true);
|
|
566
611
|
if (preset === undefined || preset.draftRevision !== undefined)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isResolutionReceipt } from "../../protocol/resolution.js";
|
|
1
2
|
import { legacyModelBindingFailure, projectLegacyModelBindingFailure, } from "./LegacyModelBindingFailure.js";
|
|
2
3
|
import { isPlayFailureKind, } from "../../protocol/playFailure.js";
|
|
3
4
|
import { isNarrativeOrigin, } from "../history/HistoryBackground.js";
|
|
@@ -1028,6 +1029,9 @@ function assertPlayEvent(value) {
|
|
|
1028
1029
|
return;
|
|
1029
1030
|
}
|
|
1030
1031
|
if (value.kind === "tool_result") {
|
|
1032
|
+
if (value.resolution !== undefined &&
|
|
1033
|
+
!isResolutionReceipt(value.resolution))
|
|
1034
|
+
throw new Error("Invalid resolution receipt");
|
|
1031
1035
|
if (typeof value.callId !== "string" ||
|
|
1032
1036
|
typeof value.name !== "string" ||
|
|
1033
1037
|
typeof value.ok !== "boolean" ||
|
|
@@ -1091,6 +1095,9 @@ function assertTimelineEventSummary(value) {
|
|
|
1091
1095
|
return;
|
|
1092
1096
|
}
|
|
1093
1097
|
if (value.kind === "tool_result") {
|
|
1098
|
+
if (value.resolution !== undefined &&
|
|
1099
|
+
!isResolutionReceipt(value.resolution))
|
|
1100
|
+
throw new Error("Invalid resolution receipt");
|
|
1094
1101
|
if (typeof value.callId !== "string" ||
|
|
1095
1102
|
typeof value.name !== "string" ||
|
|
1096
1103
|
typeof value.ok !== "boolean" ||
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { parseDocument } from "yaml";
|
|
2
|
+
import { isMap, isSeq, parseDocument } from "yaml";
|
|
3
3
|
import { builtinPlayPrompts, isPromptMessageRole, } from "../../shared/ordered-play-prompts.js";
|
|
4
4
|
/** The same strict permission gate is used by portable imports and structured saves. */
|
|
5
5
|
export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"), worldRequired = true) {
|
|
@@ -24,20 +24,23 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
24
24
|
!isPromptMessageRole(item.messageRole))
|
|
25
25
|
return fail();
|
|
26
26
|
const messageRole = item.messageRole === undefined ? {} : { messageRole: item.messageRole };
|
|
27
|
-
const exact = (keys) => Object.keys(item).
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const exact = (keys, optional = []) => Object.keys(item).every((key) => keys.includes(key) ||
|
|
28
|
+
optional.includes(key) ||
|
|
29
|
+
key === "messageRole" ||
|
|
30
|
+
key === "id" ||
|
|
31
|
+
key === "kind") && keys.every((key) => Object.hasOwn(item, key));
|
|
30
32
|
if (item.kind === "world" && exact(["id", "kind"])) {
|
|
31
33
|
worlds++;
|
|
32
34
|
return { id: item.id, kind: "world", ...messageRole };
|
|
33
35
|
}
|
|
34
36
|
if (item.kind === "user" &&
|
|
35
|
-
exact(["id", "kind", "name", "enabled", "body"]) &&
|
|
37
|
+
exact(["id", "kind", "name", "enabled", "body"], ["gameplay"]) &&
|
|
36
38
|
typeof item.name === "string" &&
|
|
37
39
|
item.name.trim().length > 0 &&
|
|
38
40
|
/^[^\r\n\0]{1,160}$/u.test(item.name) &&
|
|
39
41
|
typeof item.body === "string" &&
|
|
40
|
-
typeof item.enabled === "boolean"
|
|
42
|
+
typeof item.enabled === "boolean" &&
|
|
43
|
+
(item.gameplay === undefined || item.gameplay === "tabletop"))
|
|
41
44
|
return {
|
|
42
45
|
id: item.id,
|
|
43
46
|
kind: "user",
|
|
@@ -45,6 +48,9 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
45
48
|
enabled: item.enabled,
|
|
46
49
|
body: item.body,
|
|
47
50
|
...messageRole,
|
|
51
|
+
...(item.gameplay === "tabletop"
|
|
52
|
+
? { gameplay: "tabletop" }
|
|
53
|
+
: {}),
|
|
48
54
|
};
|
|
49
55
|
const builtin = catalog.find((entry) => entry.id === item.builtin);
|
|
50
56
|
if (item.kind !== "builtin" ||
|
|
@@ -70,6 +76,54 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
70
76
|
return fail();
|
|
71
77
|
return entries;
|
|
72
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Insert the tabletop builtins that an ordered preset does not list yet.
|
|
81
|
+
* play.tabletop goes just before the world placeholder and author.tabletop just
|
|
82
|
+
* after author.guidance. Files that are not ordered v2 or do not parse come back
|
|
83
|
+
* unchanged, and nothing else in them is rewritten. The same object is returned
|
|
84
|
+
* when nothing needs inserting, so callers can detect a change by identity.
|
|
85
|
+
*/
|
|
86
|
+
export function withTabletopPromptBuiltins(files) {
|
|
87
|
+
const source = files["preset.yaml"];
|
|
88
|
+
if (source === undefined)
|
|
89
|
+
return files;
|
|
90
|
+
const document = parseDocument(source, { uniqueKeys: true });
|
|
91
|
+
if (document.errors.length > 0 ||
|
|
92
|
+
document.warnings.length > 0 ||
|
|
93
|
+
!isMap(document.contents) ||
|
|
94
|
+
document.get("format") !== "narraeon.play-preset/v2")
|
|
95
|
+
return files;
|
|
96
|
+
let changed = false;
|
|
97
|
+
const playPrompts = document.get("playPrompts", true);
|
|
98
|
+
if (isSeq(playPrompts) && !hasEntry(playPrompts, "play.tabletop")) {
|
|
99
|
+
const world = playPrompts.items.findIndex((item) => entryField(item, "kind") === "world");
|
|
100
|
+
playPrompts.items.splice(world < 0 ? playPrompts.items.length : world, 0, document.createNode({
|
|
101
|
+
id: "play.tabletop",
|
|
102
|
+
kind: "builtin",
|
|
103
|
+
builtin: "play.tabletop",
|
|
104
|
+
enabled: true,
|
|
105
|
+
}));
|
|
106
|
+
changed = true;
|
|
107
|
+
}
|
|
108
|
+
const authorPrompts = document.get("authorPrompts", true);
|
|
109
|
+
if (isSeq(authorPrompts) && !hasEntry(authorPrompts, "author.tabletop")) {
|
|
110
|
+
const guidance = authorPrompts.items.findIndex((item) => entryField(item, "id") === "author.guidance");
|
|
111
|
+
authorPrompts.items.splice(guidance < 0 ? authorPrompts.items.length : guidance + 1, 0, document.createNode({
|
|
112
|
+
id: "author.tabletop",
|
|
113
|
+
kind: "builtin",
|
|
114
|
+
builtin: "author.tabletop",
|
|
115
|
+
enabled: true,
|
|
116
|
+
}));
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
return changed ? { ...files, "preset.yaml": document.toString() } : files;
|
|
120
|
+
}
|
|
121
|
+
function hasEntry(sequence, id) {
|
|
122
|
+
return sequence.items.some((item) => entryField(item, "id") === id);
|
|
123
|
+
}
|
|
124
|
+
function entryField(item, key) {
|
|
125
|
+
return isMap(item) ? item.get(key) : undefined;
|
|
126
|
+
}
|
|
73
127
|
/** Recognized v1 content becomes user text, never a locked builtin inferred from a path. */
|
|
74
128
|
export function migrateLegacyPlayPrompts(files, narrative, excludedPaths = []) {
|
|
75
129
|
const document = parseDocument(files["frame.yaml"] ?? "", {
|
|
@@ -185,5 +239,13 @@ export function migrateLegacyPlayPrompts(files, narrative, excludedPaths = []) {
|
|
|
185
239
|
if ((path.startsWith("blocks/") || path.startsWith("prompts/")) &&
|
|
186
240
|
path.endsWith(".md"))
|
|
187
241
|
add(path, false);
|
|
242
|
+
// The tabletop builtin joins converted presets just before the world placeholder.
|
|
243
|
+
const worldIndex = entries.findIndex((entry) => entry.kind === "world");
|
|
244
|
+
entries.splice(worldIndex, 0, {
|
|
245
|
+
id: "play.tabletop",
|
|
246
|
+
kind: "builtin",
|
|
247
|
+
builtin: "play.tabletop",
|
|
248
|
+
enabled: true,
|
|
249
|
+
});
|
|
188
250
|
return entries;
|
|
189
251
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { projectLegacyModelBindingFailure } from "./LegacyModelBindingFailure.js";
|
|
2
2
|
import { classifyPlayFailure } from "./PlayFailure.js";
|
|
3
|
+
import { isResolutionTool, resolutionMarkdown, resolutionToolNames, resolveTabletop, seededRandom, } from "./TabletopResolution.js";
|
|
3
4
|
import { captureNarrativeOrigin, snapshotBackground, narrativeBackground, } from "../history/HistoryBackground.js";
|
|
4
5
|
import { isDeepStrictEqual } from "node:util";
|
|
5
6
|
import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
|
|
@@ -18,6 +19,7 @@ import { parsePlayPresetFiles, } from "./FileNativePlayPresetStore.js";
|
|
|
18
19
|
import { runPlayFollowupRequests, } from "./PlayFollowupRequests.js";
|
|
19
20
|
import { FileNativePlayDocuments, fingerprintControl, } from "./PlayDocumentTools.js";
|
|
20
21
|
const callChainToolNames = new Set([
|
|
22
|
+
...resolutionToolNames,
|
|
21
23
|
"state_list",
|
|
22
24
|
"history_list",
|
|
23
25
|
"history_search",
|
|
@@ -660,10 +662,15 @@ export class PlayCallChain {
|
|
|
660
662
|
const timelineGeneration = playerRevisionTimelineGeneration(input.source.timelineGeneration, request.operationId, request.worldId, request.continuation);
|
|
661
663
|
const requestFingerprint = playerRevisionRequestFingerprint(request, input.selectedEvent.committedHead, restoresHead);
|
|
662
664
|
const traceRetention = preparePlayerRevisionTrace(sourceContext, input.selectedEventIndex);
|
|
665
|
+
// A timeline revision appends on the current audit head and restores the
|
|
666
|
+
// selected message's logical parent, so a chain that lags the world (an
|
|
667
|
+
// out-of-story world revision, or an earlier revision whose chain was not
|
|
668
|
+
// published) must not block it. The commit still rechecks this head under
|
|
669
|
+
// the world lock.
|
|
663
670
|
const outcome = await this.#worlds.reviseTimeline({
|
|
664
671
|
operationId: request.operationId,
|
|
665
672
|
worldId: request.worldId,
|
|
666
|
-
expectedCurrentHead:
|
|
673
|
+
expectedCurrentHead: await this.#worlds.currentHead(request.worldId),
|
|
667
674
|
restoresHead,
|
|
668
675
|
replacesHead: input.selectedEvent.committedHead,
|
|
669
676
|
replacementText: request.replacementText,
|
|
@@ -1152,8 +1159,15 @@ export class PlayCallChain {
|
|
|
1152
1159
|
const prepared = [];
|
|
1153
1160
|
const trailingEvents = [];
|
|
1154
1161
|
let nextEventId = advance.eventId + 1;
|
|
1162
|
+
// One persisted seed per advance: every resolution call draws from it by
|
|
1163
|
+
// call identity, so recovery reproduces the same receipts.
|
|
1164
|
+
const resolutionSeed = calls.some(({ name }) => isResolutionTool(name))
|
|
1165
|
+
? await this.#worlds.playAdvances.resolutionSeed(advance)
|
|
1166
|
+
: undefined;
|
|
1155
1167
|
for (const call of calls) {
|
|
1156
|
-
const item = prepareTool(session, workingTools, advance.exchange, call, this.#compiler.locale);
|
|
1168
|
+
const item = prepareTool(session, workingTools, advance.exchange, call, this.#compiler.locale, resolutionSeed);
|
|
1169
|
+
if (item.result.resolution !== undefined)
|
|
1170
|
+
crashAtPlayAdvanceEdge("after_resolution");
|
|
1157
1171
|
prepared.push(item);
|
|
1158
1172
|
trailingEvents.push({
|
|
1159
1173
|
id: nextEventId++,
|
|
@@ -2068,7 +2082,7 @@ function appendCommittedHistory(session, head, messages) {
|
|
|
2068
2082
|
});
|
|
2069
2083
|
}
|
|
2070
2084
|
}
|
|
2071
|
-
function prepareTool(session, working, exchange, call, locale) {
|
|
2085
|
+
function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
2072
2086
|
const key = `${exchange}:${call.id}`;
|
|
2073
2087
|
const signature = JSON.stringify(call.arguments);
|
|
2074
2088
|
const completed = working.get(key);
|
|
@@ -2095,8 +2109,9 @@ function prepareTool(session, working, exchange, call, locale) {
|
|
|
2095
2109
|
}
|
|
2096
2110
|
else {
|
|
2097
2111
|
try {
|
|
2098
|
-
result =
|
|
2099
|
-
call.name
|
|
2112
|
+
result = isResolutionTool(call.name)
|
|
2113
|
+
? resolutionToolResult(call.name, call.arguments, resolutionSeed, key, locale)
|
|
2114
|
+
: call.name === "world_checkpoint"
|
|
2100
2115
|
? typeof call.arguments === "object" &&
|
|
2101
2116
|
call.arguments !== null &&
|
|
2102
2117
|
!Array.isArray(call.arguments) &&
|
|
@@ -2139,6 +2154,9 @@ function prepareTool(session, working, exchange, call, locale) {
|
|
|
2139
2154
|
name: call.name,
|
|
2140
2155
|
ok: result.ok,
|
|
2141
2156
|
markdown: result.markdown,
|
|
2157
|
+
...(result.resolution === undefined
|
|
2158
|
+
? {}
|
|
2159
|
+
: { resolution: structuredClone(result.resolution) }),
|
|
2142
2160
|
replayed,
|
|
2143
2161
|
};
|
|
2144
2162
|
return {
|
|
@@ -2409,6 +2427,16 @@ function notifyAssistantDelta(observer, delta) {
|
|
|
2409
2427
|
// Like snapshot projection, the Provider exchange outlives its browser stream.
|
|
2410
2428
|
}
|
|
2411
2429
|
}
|
|
2430
|
+
function resolutionToolResult(name, args, seed, identity, locale) {
|
|
2431
|
+
if (seed === undefined)
|
|
2432
|
+
throw new Error("The resolution seed for this advance is unavailable.");
|
|
2433
|
+
const resolution = resolveTabletop(name, args, seededRandom(seed, identity));
|
|
2434
|
+
return {
|
|
2435
|
+
ok: true,
|
|
2436
|
+
markdown: resolutionMarkdown(resolution, locale),
|
|
2437
|
+
resolution,
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2412
2440
|
function crashAtPlayAdvanceEdge(edge) {
|
|
2413
2441
|
if (process.env.NODE_ENV === "test" &&
|
|
2414
2442
|
process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_PLAY_ADVANCE_EDGE === edge)
|