narraeon 0.7.9 → 0.8.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/dist/node/protocol/modelConnections.js +15 -0
- package/dist/node/protocol/modelUsage.js +4 -0
- package/dist/node/protocol/sillyTavernContent.js +1 -0
- package/dist/node/protocol/v1.js +75 -0
- package/dist/node/runtime/V1Runtime.js +71 -1
- package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
- package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +47 -22
- package/dist/node/runtime/content/ContentWorkspace.js +3 -0
- package/dist/node/runtime/content/SillyTavernContent.js +796 -0
- package/dist/node/runtime/model/AiFailureLog.js +8 -1
- package/dist/node/runtime/model/FileNativeModelAdapters.js +610 -18
- package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
- package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
- package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
- package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
- package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
- package/dist/node/runtime/model/ProviderStream.js +4 -2
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +150 -10
- package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +33 -9
- package/dist/node/runtime/play/PlayCallChain.js +76 -4
- package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +112 -8
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +100 -41
- package/dist/node/runtime/setting/SettingImprovementSession.js +48 -7
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +84 -8
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +125 -17
- package/dist/node/shared/application-preset-files.js +29 -0
- package/dist/node/shared/default-preset-host.js +50 -38
- package/dist/node/shared/first-party-action-choices.js +7 -11
- package/dist/node/shared/first-party-generic-panels.js +7 -11
- package/dist/node/shared/first-party-player-view.js +7 -11
- package/dist/node/shared/ordered-author-prompts.js +6 -4
- package/dist/node/shared/ordered-play-prompts.js +4 -2
- package/dist/node/shared/prompt-arrangement.js +205 -0
- package/dist/node/shared/sillytavern-preset.js +454 -0
- package/dist/web/assets/{index-DH2BVsYd.css → index-BKA1D0Rz.css} +1 -1
- package/dist/web/assets/index-C_Qta8UL.js +1432 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-B2kbaOGe.js +0 -1408
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { applicationPresetFilesForLocale } from "../../shared/application-preset-files.js";
|
|
1
2
|
import { parseDisplayRegex, DisplayRegexError, } from "../../shared/display-regex.js";
|
|
2
3
|
export { validatePlayPresetArtifactPayload } from "../../shared/artifact-payload-contract.js";
|
|
3
4
|
import { defaultFollowupItems, } from "../../shared/ordered-followups.js";
|
|
4
5
|
import { parseFollowupItems } from "./OrderedFollowups.js";
|
|
5
6
|
import { builtinAuthorPrompts, defaultOrderedAuthorPrompts, } from "../../shared/ordered-author-prompts.js";
|
|
6
|
-
import { parseOrderedPlayPrompts, migrateLegacyPlayPrompts,
|
|
7
|
-
import {
|
|
7
|
+
import { parseOrderedPlayPrompts, parseReleasedOrderedPrompts, releasedPromptRequiredIdentities, parsePromptArrangement, migrateLegacyPlayPrompts, upgradeOrderedPromptFormat, } from "./OrderedPlayPrompts.js";
|
|
8
|
+
import { builtinPlayPrompts, } from "../../shared/ordered-play-prompts.js";
|
|
9
|
+
import { customizePromptArrangement, retiredBuiltinReplacements, } from "../../shared/prompt-arrangement.js";
|
|
8
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
9
11
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
10
12
|
import { join } from "node:path";
|
|
11
13
|
import { isDeepStrictEqual } from "node:util";
|
|
12
|
-
import { parseDocument,
|
|
14
|
+
import { parseDocument, visit, isAlias } from "yaml";
|
|
13
15
|
import { defaultPortableContentTreeLimits, portableContentTreePathKey, } from "../../protocol/contentTree.js";
|
|
14
16
|
import { defaultAppLocale, } from "../../protocol/appPreferences.js";
|
|
15
17
|
import { defaultNarrationPromptForLocale } from "../../shared/default-play-prompts.js";
|
|
@@ -95,27 +97,78 @@ export function isPlayPresetBinding(value) {
|
|
|
95
97
|
const builtInDefault = value.id === "builtin-default" &&
|
|
96
98
|
value.name === "default" &&
|
|
97
99
|
value.revision === "builtin-default-v1" &&
|
|
98
|
-
value.scriptsEnabled === true
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
isDeepStrictEqual(files, legacyDefaultPlayPresetFilesForLocale("en")) ||
|
|
102
|
-
isDeepStrictEqual(files, legacyDefaultPlayPresetFilesForLocale("zh-CN")));
|
|
100
|
+
value.scriptsEnabled === true;
|
|
101
|
+
// This internal sentinel predates content-hash revisions. Its original
|
|
102
|
+
// snapshot must validate on its own, never against today's seed template.
|
|
103
103
|
return (parsed.kind === "valid" &&
|
|
104
|
-
|
|
104
|
+
frozenDefinitionMatches(value.definition, parsed.definition, files) &&
|
|
105
105
|
(builtInDefault || value.revision === revisionForPlayPresetFiles(files)));
|
|
106
106
|
}
|
|
107
|
+
/** A frozen v3 binding validates against the catalog identities it captured.
|
|
108
|
+
* Newly shipped defaults belong to new sends, never to a saved request. */
|
|
109
|
+
function frozenDefinitionMatches(snapshot, current, files) {
|
|
110
|
+
if (current.format !== "narraeon.play-preset/v3")
|
|
111
|
+
return isDeepStrictEqual(normalizedFrozenDefinition(snapshot), current);
|
|
112
|
+
try {
|
|
113
|
+
const source = readYaml(files, "preset.yaml", "preset.yaml");
|
|
114
|
+
const expected = { ...current };
|
|
115
|
+
const normalizedSnapshot = { ...snapshot };
|
|
116
|
+
for (const key of ["playPrompts", "authorPrompts"]) {
|
|
117
|
+
if (source[key] === undefined && snapshot[key] === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
const author = key === "authorPrompts";
|
|
120
|
+
const catalog = author
|
|
121
|
+
? builtinAuthorPrompts("en")
|
|
122
|
+
: builtinPlayPrompts("en");
|
|
123
|
+
const raw = snapshot[key];
|
|
124
|
+
if (!Array.isArray(raw))
|
|
125
|
+
return false;
|
|
126
|
+
const identities = new Set(raw.flatMap((e) => isRecord(e) && e.kind === "builtin" && typeof e.builtin === "string"
|
|
127
|
+
? (retiredBuiltinReplacements[e.builtin]?.map((r) => r.builtin) ?? [
|
|
128
|
+
e.builtin,
|
|
129
|
+
])
|
|
130
|
+
: []));
|
|
131
|
+
// v3's original mandatory identities are part of its validation contract.
|
|
132
|
+
// Later required additions are mandatory only for newly compiled requests.
|
|
133
|
+
const requiredAtRelease = releasedPromptRequiredIdentities(!author);
|
|
134
|
+
const capturedCatalog = catalog.filter((c) => identities.has(c.id) || requiredAtRelease.includes(c.id));
|
|
135
|
+
const captured = parseOrderedPlayPrompts(raw, capturedCatalog, !author);
|
|
136
|
+
const frozenCatalog = captured.flatMap((entry) => {
|
|
137
|
+
if (entry.kind !== "builtin")
|
|
138
|
+
return [];
|
|
139
|
+
const item = capturedCatalog.find((item) => item.id === entry.builtin);
|
|
140
|
+
return item === undefined
|
|
141
|
+
? []
|
|
142
|
+
: [{ ...item, defaultEnabled: entry.enabled }];
|
|
143
|
+
});
|
|
144
|
+
if (!isDeepStrictEqual(captured, parsePromptArrangement(source[key], frozenCatalog, !author)))
|
|
145
|
+
return false;
|
|
146
|
+
expected[key] = captured;
|
|
147
|
+
normalizedSnapshot[key] = captured;
|
|
148
|
+
}
|
|
149
|
+
return isDeepStrictEqual(normalizedSnapshot, expected);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
107
155
|
/**
|
|
108
156
|
* A frozen definition captured before a builtin was retired still lists the
|
|
109
|
-
* old entry; reading
|
|
110
|
-
* successors the files
|
|
157
|
+
* old entry; reading both ordered lists through their respective parsers yields
|
|
158
|
+
* the same successors as the files, so the binding stays consistent.
|
|
111
159
|
*/
|
|
112
160
|
function normalizedFrozenDefinition(definition) {
|
|
113
|
-
if (!Array.isArray(definition.playPrompts))
|
|
114
|
-
return definition;
|
|
115
161
|
try {
|
|
116
162
|
return {
|
|
117
163
|
...definition,
|
|
118
|
-
|
|
164
|
+
...(Array.isArray(definition.playPrompts)
|
|
165
|
+
? { playPrompts: parseReleasedOrderedPrompts(definition.playPrompts) }
|
|
166
|
+
: {}),
|
|
167
|
+
...(Array.isArray(definition.authorPrompts)
|
|
168
|
+
? {
|
|
169
|
+
authorPrompts: parseReleasedOrderedPrompts(definition.authorPrompts, builtinAuthorPrompts("en"), false),
|
|
170
|
+
}
|
|
171
|
+
: {}),
|
|
119
172
|
};
|
|
120
173
|
}
|
|
121
174
|
catch {
|
|
@@ -151,14 +204,7 @@ followups: []
|
|
|
151
204
|
};
|
|
152
205
|
}
|
|
153
206
|
export function defaultPlayPresetFilesForLocale(locale) {
|
|
154
|
-
|
|
155
|
-
const preset = parseDocument(files["preset.yaml"]);
|
|
156
|
-
preset.set("format", "narraeon.play-preset/v2");
|
|
157
|
-
preset.set("playPrompts", defaultOrderedPlayPrompts());
|
|
158
|
-
preset.set("authorPrompts", defaultOrderedAuthorPrompts());
|
|
159
|
-
preset.set("mergeConsecutiveMessages", false);
|
|
160
|
-
files["preset.yaml"] = stringify(preset.toJS());
|
|
161
|
-
return files;
|
|
207
|
+
return applicationPresetFilesForLocale(locale);
|
|
162
208
|
}
|
|
163
209
|
export const defaultPlayPresetFiles = defaultPlayPresetFilesForLocale(defaultAppLocale);
|
|
164
210
|
/**
|
|
@@ -230,8 +276,8 @@ export class FileNativePlayPresetStore {
|
|
|
230
276
|
const document = await this.#read();
|
|
231
277
|
const migrated = this.#markLegacyBuiltinDefault(document);
|
|
232
278
|
const synchronized = this.#synchronizeBuiltinDefault(document);
|
|
233
|
-
const
|
|
234
|
-
if (migrated || synchronized ||
|
|
279
|
+
const customizations = this.#migratePromptCustomizations(document);
|
|
280
|
+
if (migrated || synchronized || customizations)
|
|
235
281
|
await this.#write(document);
|
|
236
282
|
});
|
|
237
283
|
}
|
|
@@ -291,10 +337,18 @@ export class FileNativePlayPresetStore {
|
|
|
291
337
|
const stored = findStored(document, input.presetId);
|
|
292
338
|
delete stored.builtinDefault;
|
|
293
339
|
let files = cloneFiles(input.files);
|
|
340
|
+
const previous = parsePlayPresetFiles(stored.revisions[stored.currentRevision]);
|
|
294
341
|
if (input.structure !== undefined) {
|
|
295
|
-
|
|
342
|
+
const structure = parsePlayPresetStructuredEditor(input.structure);
|
|
343
|
+
if (previous.kind === "valid") {
|
|
344
|
+
for (const key of ["playPrompts", "authorPrompts"]) {
|
|
345
|
+
if (structure[key] === undefined)
|
|
346
|
+
continue;
|
|
347
|
+
assertReadonlyPromptsRetained(previous.definition[key] ?? [], structure[key]);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
files = applyPlayPresetStructuredEditor(input.files, structure);
|
|
296
351
|
}
|
|
297
|
-
const previous = parsePlayPresetFiles(stored.revisions[stored.currentRevision]);
|
|
298
352
|
if (previous.kind === "valid" &&
|
|
299
353
|
previous.definition.followupItems !== undefined) {
|
|
300
354
|
const candidate = readYaml(files, "call-chain.yaml", "call-chain.yaml");
|
|
@@ -305,31 +359,23 @@ export class FileNativePlayPresetStore {
|
|
|
305
359
|
if (previous.kind === "valid" &&
|
|
306
360
|
previous.definition.playPrompts !== undefined) {
|
|
307
361
|
const candidate = readYaml(files, "preset.yaml", "preset.yaml");
|
|
308
|
-
if (candidate.format !== "narraeon.play-preset/v2"
|
|
362
|
+
if (candidate.format !== "narraeon.play-preset/v2" &&
|
|
363
|
+
candidate.format !== "narraeon.play-preset/v3")
|
|
309
364
|
throw new FileNativePlayPresetError("readonly_prompt_removed", "Ordered presets cannot downgrade to v1");
|
|
310
|
-
const candidates =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
(entry.kind === "builtin" &&
|
|
315
|
-
retained.kind === "builtin" &&
|
|
316
|
-
retained.builtin !== entry.builtin))
|
|
317
|
-
throw new FileNativePlayPresetError("readonly_prompt_removed", "System prompts and the world placeholder cannot be deleted or replaced; disable optional prompts or clone them instead");
|
|
318
|
-
}
|
|
365
|
+
const candidates = candidate.format === "narraeon.play-preset/v3"
|
|
366
|
+
? parsePromptArrangement(candidate.playPrompts)
|
|
367
|
+
: parseOrderedPlayPrompts(candidate.playPrompts);
|
|
368
|
+
assertReadonlyPromptsRetained(previous.definition.playPrompts, candidates);
|
|
319
369
|
}
|
|
320
370
|
if (previous.kind === "valid" &&
|
|
321
371
|
previous.definition.authorPrompts !== undefined) {
|
|
322
372
|
const candidate = readYaml(files, "preset.yaml", "preset.yaml");
|
|
323
|
-
const entries =
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const retained = entries.find((item) => item.id === entry.id);
|
|
328
|
-
if (retained?.kind !== "builtin" ||
|
|
329
|
-
retained.builtin !== entry.builtin)
|
|
330
|
-
throw new FileNativePlayPresetError("readonly_prompt_removed", "Authoring system prompts cannot be deleted or replaced; disable or clone optional prompts");
|
|
331
|
-
}
|
|
373
|
+
const entries = candidate.format === "narraeon.play-preset/v3"
|
|
374
|
+
? parsePromptArrangement(candidate.authorPrompts, builtinAuthorPrompts("en"), false)
|
|
375
|
+
: parseOrderedAuthorPrompts(candidate.authorPrompts);
|
|
376
|
+
assertReadonlyPromptsRetained(previous.definition.authorPrompts, entries);
|
|
332
377
|
}
|
|
378
|
+
files = upgradeValidOrderedPromptFormat(files, this.#limits);
|
|
333
379
|
const revision = revisionForFiles(files, this.#limits);
|
|
334
380
|
stored.name = normalizeName(input.name);
|
|
335
381
|
stored.revisions[revision] = cloneFiles(files);
|
|
@@ -472,14 +518,16 @@ export class FileNativePlayPresetStore {
|
|
|
472
518
|
}
|
|
473
519
|
/** Portable export deliberately contains no id, revision, world, operation or provider state. */
|
|
474
520
|
async exportPortable(id) {
|
|
475
|
-
|
|
476
|
-
|
|
521
|
+
await this.#tail;
|
|
522
|
+
const stored = findStored(await this.#read(), id);
|
|
523
|
+
const files = stored.revisions[stored.currentRevision];
|
|
524
|
+
return Object.keys(files)
|
|
477
525
|
.sort()
|
|
478
|
-
.map((path) => ({ path, contents:
|
|
526
|
+
.map((path) => ({ path, contents: files[path] }));
|
|
479
527
|
}
|
|
480
528
|
async importPortable(input) {
|
|
481
529
|
// A released preset gains the current builtins at import, as at upgrade.
|
|
482
|
-
const files =
|
|
530
|
+
const files = upgradeOrderedPromptFormat(toFileMap(input.files, this.#limits));
|
|
483
531
|
const parsed = parsePlayPresetFiles(files, this.#limits);
|
|
484
532
|
if (parsed.kind === "invalid")
|
|
485
533
|
throw parsed.error;
|
|
@@ -537,7 +585,7 @@ export class FileNativePlayPresetStore {
|
|
|
537
585
|
return document;
|
|
538
586
|
}
|
|
539
587
|
try {
|
|
540
|
-
// Validation
|
|
588
|
+
// Validation checks the structure and hash of every stored revision, so a library
|
|
541
589
|
// whose bytes were already validated is served from that result. Each
|
|
542
590
|
// caller gets its own copy because callers edit what they read.
|
|
543
591
|
if (this.#validated !== undefined && raw === this.#validated.raw)
|
|
@@ -597,19 +645,17 @@ export class FileNativePlayPresetStore {
|
|
|
597
645
|
return true;
|
|
598
646
|
}
|
|
599
647
|
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
* builtin-retention rule on save. The library is one atomically written file,
|
|
605
|
-
* so an interrupted upgrade leaves the released bytes and reruns cleanly.
|
|
648
|
+
* Convert released ordered lists to sparse customizations exactly once.
|
|
649
|
+
* Original revisions remain immutable. Current and valid draft endpoints
|
|
650
|
+
* advance together in one atomic library replacement; invalid drafts retain
|
|
651
|
+
* their original bytes for repair. Catalog evolution never changes v3 files.
|
|
606
652
|
*/
|
|
607
|
-
#
|
|
653
|
+
#migratePromptCustomizations(document) {
|
|
608
654
|
let changed = false;
|
|
609
655
|
for (const stored of document.presets) {
|
|
610
656
|
const current = stored.revisions[stored.currentRevision];
|
|
611
657
|
if (current !== undefined) {
|
|
612
|
-
const migrated =
|
|
658
|
+
const migrated = upgradeValidOrderedPromptFormat(current, this.#limits);
|
|
613
659
|
if (migrated !== current) {
|
|
614
660
|
const revision = revisionForFiles(migrated, this.#limits);
|
|
615
661
|
stored.revisions[revision] = cloneFiles(migrated);
|
|
@@ -621,7 +667,7 @@ export class FileNativePlayPresetStore {
|
|
|
621
667
|
? undefined
|
|
622
668
|
: stored.revisions[stored.draftRevision];
|
|
623
669
|
if (draft !== undefined) {
|
|
624
|
-
const migrated =
|
|
670
|
+
const migrated = upgradeValidOrderedPromptFormat(draft, this.#limits);
|
|
625
671
|
if (migrated !== draft) {
|
|
626
672
|
const revision = revisionForFiles(migrated, this.#limits);
|
|
627
673
|
stored.revisions[revision] = cloneFiles(migrated);
|
|
@@ -653,7 +699,7 @@ export class FileNativePlayPresetStore {
|
|
|
653
699
|
}
|
|
654
700
|
#stored(id, rawName, rawFiles) {
|
|
655
701
|
const name = normalizeName(rawName);
|
|
656
|
-
const files = cloneFiles(rawFiles);
|
|
702
|
+
const files = cloneFiles(upgradeValidOrderedPromptFormat(rawFiles, this.#limits));
|
|
657
703
|
const revision = revisionForFiles(files, this.#limits);
|
|
658
704
|
return {
|
|
659
705
|
id,
|
|
@@ -784,7 +830,7 @@ export function toPlayPresetStructuredEditor(definition) {
|
|
|
784
830
|
* public structure and never bypasses the normal codec.
|
|
785
831
|
*/
|
|
786
832
|
export function applyPlayPresetStructuredEditor(files, input) {
|
|
787
|
-
const next = cloneFiles(files);
|
|
833
|
+
const next = cloneFiles(upgradeOrderedPromptFormat(files));
|
|
788
834
|
const presetSource = next["preset.yaml"];
|
|
789
835
|
const callChainSource = next[input.callChainPath];
|
|
790
836
|
if (presetSource === undefined || callChainSource === undefined)
|
|
@@ -805,10 +851,10 @@ export function applyPlayPresetStructuredEditor(files, input) {
|
|
|
805
851
|
if (input.mergeConsecutiveMessages !== undefined)
|
|
806
852
|
preset.set("mergeConsecutiveMessages", parseMessageMerging(input.mergeConsecutiveMessages));
|
|
807
853
|
if (input.authorPrompts !== undefined)
|
|
808
|
-
preset.set("authorPrompts", parseOrderedAuthorPrompts(input.authorPrompts));
|
|
854
|
+
preset.set("authorPrompts", customizePromptArrangement(parseOrderedAuthorPrompts(input.authorPrompts), builtinAuthorPrompts("en"), false));
|
|
809
855
|
if (input.playPrompts !== undefined) {
|
|
810
|
-
preset.set("format", "narraeon.play-preset/
|
|
811
|
-
preset.set("playPrompts", parseOrderedPlayPrompts(input.playPrompts));
|
|
856
|
+
preset.set("format", "narraeon.play-preset/v3");
|
|
857
|
+
preset.set("playPrompts", customizePromptArrangement(parseOrderedPlayPrompts(input.playPrompts), builtinPlayPrompts("en")));
|
|
812
858
|
}
|
|
813
859
|
preset.set("callChain", input.callChainPath);
|
|
814
860
|
if (input.settingImprovementPrompt === undefined)
|
|
@@ -965,7 +1011,8 @@ export function parsePlayPresetFiles(files, limits = {}) {
|
|
|
965
1011
|
if (callChainPath !== "call-chain.yaml")
|
|
966
1012
|
invalid("call_chain_path_invalid", "Entry callChain must be call-chain.yaml", "preset.yaml");
|
|
967
1013
|
if (preset.format !== "narraeon.play-preset/v1" &&
|
|
968
|
-
preset.format !== "narraeon.play-preset/v2"
|
|
1014
|
+
preset.format !== "narraeon.play-preset/v2" &&
|
|
1015
|
+
preset.format !== "narraeon.play-preset/v3")
|
|
969
1016
|
invalid("preset_format_invalid", "preset.yaml format must be narraeon.play-preset/v1", "preset.yaml");
|
|
970
1017
|
if (preset.format === "narraeon.play-preset/v1" &&
|
|
971
1018
|
(preset.playPrompts !== undefined ||
|
|
@@ -1009,12 +1056,21 @@ export function parsePlayPresetFiles(files, limits = {}) {
|
|
|
1009
1056
|
: {
|
|
1010
1057
|
mergeConsecutiveMessages: parseMessageMerging(preset.mergeConsecutiveMessages),
|
|
1011
1058
|
}),
|
|
1012
|
-
...(preset.format === "narraeon.play-preset/v2"
|
|
1013
|
-
|
|
1059
|
+
...(preset.format === "narraeon.play-preset/v2" ||
|
|
1060
|
+
preset.format === "narraeon.play-preset/v3"
|
|
1061
|
+
? {
|
|
1062
|
+
playPrompts: preset.format === "narraeon.play-preset/v3"
|
|
1063
|
+
? parsePromptArrangement(preset.playPrompts)
|
|
1064
|
+
: parseReleasedOrderedPrompts(preset.playPrompts),
|
|
1065
|
+
}
|
|
1014
1066
|
: {}),
|
|
1015
1067
|
...(preset.authorPrompts === undefined
|
|
1016
1068
|
? {}
|
|
1017
|
-
: {
|
|
1069
|
+
: {
|
|
1070
|
+
authorPrompts: preset.format === "narraeon.play-preset/v3"
|
|
1071
|
+
? parsePromptArrangement(preset.authorPrompts, builtinAuthorPrompts("en"), false)
|
|
1072
|
+
: parseReleasedOrderedPrompts(preset.authorPrompts, builtinAuthorPrompts("en"), false),
|
|
1073
|
+
}),
|
|
1018
1074
|
name: name.trim(),
|
|
1019
1075
|
callChainPath,
|
|
1020
1076
|
...(settingImprovementPrompt === undefined
|
|
@@ -1819,6 +1875,13 @@ function toFileMap(input, limits) {
|
|
|
1819
1875
|
validateFileTree(files, limits);
|
|
1820
1876
|
return files;
|
|
1821
1877
|
}
|
|
1878
|
+
function upgradeValidOrderedPromptFormat(files, limits) {
|
|
1879
|
+
const upgraded = upgradeOrderedPromptFormat(files);
|
|
1880
|
+
return upgraded !== files &&
|
|
1881
|
+
validatePlayPresetFiles(upgraded, limits).status === "valid"
|
|
1882
|
+
? upgraded
|
|
1883
|
+
: files;
|
|
1884
|
+
}
|
|
1822
1885
|
function parseStoredDocument(value, limits = defaultLimits) {
|
|
1823
1886
|
if (!isRecord(value) ||
|
|
1824
1887
|
!hasExactKeys(value, ["schemaVersion", "currentPresetId", "presets"]) ||
|
|
@@ -1875,14 +1938,6 @@ function parseStoredDocument(value, limits = defaultLimits) {
|
|
|
1875
1938
|
}
|
|
1876
1939
|
if (expectedRevision !== revision)
|
|
1877
1940
|
throw new FileNativePlayPresetError("store_invalid", "Play-preset revision content hash does not match");
|
|
1878
|
-
// A revision saved by an earlier release may still name builtins that
|
|
1879
|
-
// the catalog has since replaced; it counts as current-format when the
|
|
1880
|
-
// load-time migration makes it so, and its bytes stay as written.
|
|
1881
|
-
if (revision !== raw.draftRevision &&
|
|
1882
|
-
validatePlayPresetFiles(clone, limits).status !== "valid" &&
|
|
1883
|
-
validatePlayPresetFiles(withCurrentPromptBuiltins(clone), limits)
|
|
1884
|
-
.status !== "valid")
|
|
1885
|
-
throw new FileNativePlayPresetError("store_invalid", "Play-preset revision does not match the current format");
|
|
1886
1941
|
revisions[revision] = clone;
|
|
1887
1942
|
}
|
|
1888
1943
|
if (revisions[raw.currentRevision] === undefined)
|
|
@@ -1919,6 +1974,18 @@ function findStored(document, id) {
|
|
|
1919
1974
|
throw new FileNativePlayPresetError("not_found", "Requested play preset was not found");
|
|
1920
1975
|
return preset;
|
|
1921
1976
|
}
|
|
1977
|
+
function assertReadonlyPromptsRetained(previous, candidate) {
|
|
1978
|
+
for (const entry of previous) {
|
|
1979
|
+
if (entry.kind !== "builtin" && entry.kind !== "world")
|
|
1980
|
+
continue;
|
|
1981
|
+
const retained = candidate.find((item) => item.id === entry.id);
|
|
1982
|
+
if (retained?.kind !== entry.kind ||
|
|
1983
|
+
(entry.kind === "builtin" &&
|
|
1984
|
+
retained.kind === "builtin" &&
|
|
1985
|
+
retained.builtin !== entry.builtin))
|
|
1986
|
+
throw new FileNativePlayPresetError("readonly_prompt_removed", "System prompts and the world placeholder cannot be deleted or replaced; disable optional prompts or clone them instead");
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1922
1989
|
function normalizeName(raw) {
|
|
1923
1990
|
const name = raw.trim();
|
|
1924
1991
|
if (!namePattern.test(name) || name.includes(String.fromCharCode(0)))
|
|
@@ -1959,13 +2026,18 @@ export function legacyAuthorPrompts(definition) {
|
|
|
1959
2026
|
const entries = defaultOrderedAuthorPrompts();
|
|
1960
2027
|
const path = definition.settingImprovementPrompt?.path;
|
|
1961
2028
|
if (path !== undefined)
|
|
1962
|
-
entries
|
|
2029
|
+
entries.splice(1, 1, {
|
|
2030
|
+
id: "author.guidance",
|
|
2031
|
+
kind: "builtin",
|
|
2032
|
+
builtin: "author.guidance",
|
|
2033
|
+
enabled: false,
|
|
2034
|
+
}, {
|
|
1963
2035
|
id: "legacy-author",
|
|
1964
2036
|
kind: "user",
|
|
1965
2037
|
name: "Authoring",
|
|
1966
2038
|
body: definition.files[path],
|
|
1967
2039
|
enabled: true,
|
|
1968
|
-
};
|
|
2040
|
+
});
|
|
1969
2041
|
return entries;
|
|
1970
2042
|
}
|
|
1971
2043
|
function parseMessageMerging(value) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isResolutionReceipt } from "../../protocol/resolution.js";
|
|
2
|
+
import { isSafeGoogleWireRequest } from "../model/GoogleRequestPreview.js";
|
|
3
|
+
import { readGoogleHops, writeGoogleHop } from "../model/GoogleHopJournal.js";
|
|
2
4
|
import { legacyModelBindingFailure, projectLegacyModelBindingFailure, } from "./LegacyModelBindingFailure.js";
|
|
3
5
|
import { isPlayFailureKind, } from "../../protocol/playFailure.js";
|
|
4
6
|
import { isNarrativeOrigin, } from "../history/HistoryBackground.js";
|
|
@@ -43,11 +45,7 @@ export class FileNativePlayTimelineStore {
|
|
|
43
45
|
const transcriptCount = prefixLength(input.source.transcript, input.target.transcript);
|
|
44
46
|
const eventCount = prefixLength(input.source.events, input.target.events);
|
|
45
47
|
const completedToolCount = prefixLength(input.source.completedTools, input.target.completedTools);
|
|
46
|
-
|
|
47
|
-
const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId, exchange).catch(() => null);
|
|
48
|
-
if (encoding !== null)
|
|
49
|
-
await publishImmutableJson(join(targetRoot, encodingFileName(exchange)), encoding);
|
|
50
|
-
}
|
|
48
|
+
await this.#copyRetainedEncodings(input.sourceWorldId, input.source.chainId, targetRoot, input.target);
|
|
51
49
|
await Promise.all([
|
|
52
50
|
cloneNumberedPrefix({
|
|
53
51
|
sourceRoot,
|
|
@@ -346,18 +344,96 @@ export class FileNativePlayTimelineStore {
|
|
|
346
344
|
await pruneEncodings(root);
|
|
347
345
|
}
|
|
348
346
|
async readInitialEncoding(worldId, chainId, exchange = 1) {
|
|
349
|
-
const
|
|
347
|
+
const root = this.#contextRoot(worldId, chainId);
|
|
348
|
+
if (!(await retainedEncodingExchanges(root)).includes(exchange))
|
|
349
|
+
return null;
|
|
350
|
+
const value = await readOptionalJson(join(root, encodingFileName(exchange)));
|
|
350
351
|
if (value === null)
|
|
351
352
|
return null;
|
|
353
|
+
if (isRecord(value) &&
|
|
354
|
+
value.provider === "google_interactions" &&
|
|
355
|
+
!isSafeGoogleWireRequest(value))
|
|
356
|
+
return null;
|
|
352
357
|
if (!isRecord(value) ||
|
|
353
358
|
value.method !== "POST" ||
|
|
354
|
-
![
|
|
359
|
+
![
|
|
360
|
+
"chat_completions",
|
|
361
|
+
"openai_responses",
|
|
362
|
+
"anthropic_messages",
|
|
363
|
+
"google_interactions",
|
|
364
|
+
].includes(String(value.provider)) ||
|
|
355
365
|
!isRecord(value.body) ||
|
|
356
366
|
typeof value.endpointPath !== "string" ||
|
|
357
367
|
!Array.isArray(value.headerNames) ||
|
|
358
368
|
!value.headerNames.every((name) => typeof name === "string"))
|
|
359
369
|
throw new Error("Initial request encoding has an invalid shape");
|
|
360
|
-
|
|
370
|
+
const wire = value;
|
|
371
|
+
return wire.provider === "google_interactions"
|
|
372
|
+
? readGoogleHops(join(root, "google-hops", String(exchange)), "main", wire)
|
|
373
|
+
: wire;
|
|
374
|
+
}
|
|
375
|
+
async recordGoogleHop(worldId, chainId, runExchange, hop, followupId) {
|
|
376
|
+
const root = this.#contextRoot(worldId, chainId);
|
|
377
|
+
await writeGoogleHop(join(root, "google-hops", String(runExchange)), followupId === undefined ? "main" : `followup:${followupId}`, hop, async () => (await retainedEncodingExchanges(root)).includes(runExchange));
|
|
378
|
+
}
|
|
379
|
+
async copyRetainedEncodings(worldId, sourceChainId, target) {
|
|
380
|
+
await this.#copyRetainedEncodings(worldId, sourceChainId, this.#contextRoot(worldId, target.chainId), target);
|
|
381
|
+
}
|
|
382
|
+
async #copyRetainedEncodings(worldId, sourceChainId, targetRoot, target) {
|
|
383
|
+
const sourceRoot = this.#contextRoot(worldId, sourceChainId);
|
|
384
|
+
const retainedResponses = new Set(target.events.flatMap((event) => event.kind === "assistant"
|
|
385
|
+
? [`${event.exchange}:${event.attempt}`]
|
|
386
|
+
: []));
|
|
387
|
+
const targetFollowups = new Map(target.events.flatMap((event) => event.kind === "followup" && event.requestPreviewExchange !== undefined
|
|
388
|
+
? [
|
|
389
|
+
[
|
|
390
|
+
followupEncodingFileName(event.requestPreviewExchange, event.followupId),
|
|
391
|
+
event.followupId,
|
|
392
|
+
],
|
|
393
|
+
]
|
|
394
|
+
: []));
|
|
395
|
+
for (const exchange of (await retainedEncodingExchanges(sourceRoot)).filter((value) => value <= target.exchange)) {
|
|
396
|
+
const encoding = await this.readInitialEncoding(worldId, sourceChainId, exchange).catch(() => null);
|
|
397
|
+
if (encoding === null)
|
|
398
|
+
continue;
|
|
399
|
+
if (encoding.googleHops !== undefined)
|
|
400
|
+
encoding.googleHops = encoding.googleHops.filter((hop) => retainedResponses.has(`${hop.exchange}:${hop.attempt}`));
|
|
401
|
+
await publishImmutableJson(join(targetRoot, encodingFileName(exchange)), encoding);
|
|
402
|
+
for (const name of await followupEncodingNames(sourceRoot, exchange)) {
|
|
403
|
+
const followupId = targetFollowups.get(name);
|
|
404
|
+
if (followupId === undefined)
|
|
405
|
+
continue;
|
|
406
|
+
try {
|
|
407
|
+
const value = await this.readFollowupEncoding(worldId, sourceChainId, exchange, followupId);
|
|
408
|
+
if (safeFollowupEncoding(value))
|
|
409
|
+
await publishImmutableJson(join(targetRoot, name), value);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// A single optional diagnostic must not prevent other evidence or a fork.
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async recordFollowupEncoding(worldId, chainId, exchange, requestId, encoding) {
|
|
418
|
+
const root = this.#contextRoot(worldId, chainId);
|
|
419
|
+
// The main encoding owns the bounded run slot; never create an orphan slot.
|
|
420
|
+
if (!(await retainedEncodingExchanges(root)).includes(exchange) ||
|
|
421
|
+
!safeFollowupEncoding(encoding))
|
|
422
|
+
return false;
|
|
423
|
+
await publishImmutableJson(join(root, followupEncodingFileName(exchange, requestId)), encoding);
|
|
424
|
+
await pruneEncodings(root);
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
async readFollowupEncoding(worldId, chainId, exchange, requestId) {
|
|
428
|
+
const root = this.#contextRoot(worldId, chainId);
|
|
429
|
+
if (!(await retainedEncodingExchanges(root)).includes(exchange))
|
|
430
|
+
return null;
|
|
431
|
+
const value = await readOptionalJson(join(root, followupEncodingFileName(exchange, requestId)));
|
|
432
|
+
if (value === null)
|
|
433
|
+
return null;
|
|
434
|
+
if (!safeFollowupEncoding(value))
|
|
435
|
+
throw new Error("Follow-up request encoding has an invalid shape");
|
|
436
|
+
return readGoogleHops(join(root, "google-hops", String(exchange)), `followup:${requestId}`, value);
|
|
361
437
|
}
|
|
362
438
|
async readAllContexts(worldId) {
|
|
363
439
|
const current = await this.readCurrent(worldId);
|
|
@@ -538,6 +614,18 @@ export class FileNativePlayTimelineStore {
|
|
|
538
614
|
assertPlayEvent(event);
|
|
539
615
|
if (event.id !== eventId)
|
|
540
616
|
throw new Error("Play timeline event identity is inconsistent");
|
|
617
|
+
if (event.kind === "followup") {
|
|
618
|
+
const wireRequest = event.requestPreviewExchange === undefined ||
|
|
619
|
+
event.requestPreviewStatus === undefined
|
|
620
|
+
? null
|
|
621
|
+
: await this.readFollowupEncoding(worldId, chainId, event.requestPreviewExchange, event.followupId).catch(() => null);
|
|
622
|
+
return {
|
|
623
|
+
...event,
|
|
624
|
+
requestPreview: wireRequest === null || event.requestPreviewStatus === undefined
|
|
625
|
+
? { status: "unavailable" }
|
|
626
|
+
: { status: event.requestPreviewStatus, wireRequest },
|
|
627
|
+
};
|
|
628
|
+
}
|
|
541
629
|
return projectLegacyModelBindingFailure(event);
|
|
542
630
|
}
|
|
543
631
|
newGeneration() {
|
|
@@ -900,7 +988,8 @@ function validModelHostBinding(value) {
|
|
|
900
988
|
return (isRecord(value) &&
|
|
901
989
|
(value.provider === "chat_completions" ||
|
|
902
990
|
value.provider === "openai_responses" ||
|
|
903
|
-
value.provider === "anthropic_messages"
|
|
991
|
+
value.provider === "anthropic_messages" ||
|
|
992
|
+
value.provider === "google_interactions") &&
|
|
904
993
|
typeof value.endpointFingerprint === "string" &&
|
|
905
994
|
value.endpointFingerprint.length > 0 &&
|
|
906
995
|
typeof value.modelId === "string" &&
|
|
@@ -972,6 +1061,13 @@ function assertPlayEvent(value) {
|
|
|
972
1061
|
typeof value.text !== "string" ||
|
|
973
1062
|
!validOptionalString(value.reasoning) ||
|
|
974
1063
|
!validOptionalUsage(value.usage) ||
|
|
1064
|
+
(value.requestPreviewExchange !== undefined &&
|
|
1065
|
+
(!Number.isSafeInteger(value.requestPreviewExchange) ||
|
|
1066
|
+
Number(value.requestPreviewExchange) < 1)) ||
|
|
1067
|
+
(value.requestPreviewStatus !== undefined &&
|
|
1068
|
+
value.requestPreviewStatus !== "attempted" &&
|
|
1069
|
+
value.requestPreviewStatus !== "completed" &&
|
|
1070
|
+
value.requestPreviewStatus !== "cancelled") ||
|
|
975
1071
|
!Array.isArray(value.toolCalls) ||
|
|
976
1072
|
!value.toolCalls.every(validFollowupToolCall) ||
|
|
977
1073
|
!validOptionalString(value.failure))
|
|
@@ -1073,6 +1169,11 @@ function validOptionalUsage(value) {
|
|
|
1073
1169
|
function validFullUsage(value) {
|
|
1074
1170
|
if (!isRecord(value) || !isRecord(value.provenance))
|
|
1075
1171
|
return false;
|
|
1172
|
+
if (value.providerRaw !== undefined && !isRecord(value.providerRaw))
|
|
1173
|
+
return false;
|
|
1174
|
+
if (value.accounting !== undefined &&
|
|
1175
|
+
value.accounting !== "google_interactions")
|
|
1176
|
+
return false;
|
|
1076
1177
|
const provenance = value.provenance;
|
|
1077
1178
|
const fields = [
|
|
1078
1179
|
"inputTokens",
|
|
@@ -1192,6 +1293,31 @@ function encodingFileName(exchange) {
|
|
|
1192
1293
|
? "initial-request-encoding.json"
|
|
1193
1294
|
: `request-encoding-${exchange}.json`;
|
|
1194
1295
|
}
|
|
1296
|
+
function followupEncodingFileName(exchange, requestId) {
|
|
1297
|
+
return `followup-encoding-${exchange}-${createHash("sha256").update(requestId).digest("hex")}.json`;
|
|
1298
|
+
}
|
|
1299
|
+
function safeFollowupEncoding(value) {
|
|
1300
|
+
if (!isSafeGoogleWireRequest(value) ||
|
|
1301
|
+
value.headerNames.length !== 3 ||
|
|
1302
|
+
value.headerNames[0] !== "Accept" ||
|
|
1303
|
+
value.headerNames[1] !== "x-goog-api-key" ||
|
|
1304
|
+
value.headerNames[2] !== "Content-Type" ||
|
|
1305
|
+
!isRecord(value.body) ||
|
|
1306
|
+
!Array.isArray(value.body.input))
|
|
1307
|
+
return false;
|
|
1308
|
+
// Provider function calls must have been redacted; only the Runtime's
|
|
1309
|
+
// explicitly marked bootstrap call can remain in the projected input.
|
|
1310
|
+
return value.body.input.every((item) => !isRecord(item) ||
|
|
1311
|
+
item.type !== "function_call" ||
|
|
1312
|
+
(item.name === "runtime_load_context" &&
|
|
1313
|
+
isRecord(item.arguments) &&
|
|
1314
|
+
Object.keys(item.arguments).length === 0));
|
|
1315
|
+
}
|
|
1316
|
+
async function followupEncodingNames(root, exchange) {
|
|
1317
|
+
const prefix = `followup-encoding-${exchange}-`;
|
|
1318
|
+
return (await readDirectoryNames(root)).filter((name) => name.startsWith(prefix) &&
|
|
1319
|
+
/^[a-f0-9]{64}\.json$/u.test(name.slice(prefix.length)));
|
|
1320
|
+
}
|
|
1195
1321
|
/** The exchanges with a recorded encoding in a context, latest first. */
|
|
1196
1322
|
async function recordedEncodingExchanges(contextRoot) {
|
|
1197
1323
|
return (await readDirectoryNames(contextRoot))
|
|
@@ -1204,12 +1330,26 @@ async function recordedEncodingExchanges(contextRoot) {
|
|
|
1204
1330
|
.filter((exchange) => exchange !== null)
|
|
1205
1331
|
.sort((left, right) => right - left);
|
|
1206
1332
|
}
|
|
1333
|
+
async function retainedEncodingExchanges(contextRoot) {
|
|
1334
|
+
return (await recordedEncodingExchanges(contextRoot)).slice(0, 2);
|
|
1335
|
+
}
|
|
1207
1336
|
/**
|
|
1208
1337
|
* Keep the encodings of the two latest exchanges, the pair the AI-reading
|
|
1209
1338
|
* diagnostics compare. A context without runs is read at exchange 1.
|
|
1210
1339
|
*/
|
|
1211
1340
|
async function pruneEncodings(contextRoot) {
|
|
1212
|
-
|
|
1341
|
+
const exchanges = await recordedEncodingExchanges(contextRoot);
|
|
1342
|
+
const retained = new Set(exchanges.slice(0, 2));
|
|
1343
|
+
const hopRoot = join(contextRoot, "google-hops");
|
|
1344
|
+
for (const name of await readDirectoryNames(hopRoot))
|
|
1345
|
+
if (/^[1-9]\d*$/u.test(name) && !retained.has(Number(name)))
|
|
1346
|
+
await rm(join(hopRoot, name), { recursive: true, force: true });
|
|
1347
|
+
for (const name of await readDirectoryNames(contextRoot)) {
|
|
1348
|
+
const match = /^followup-encoding-([1-9][0-9]*)-[a-f0-9]{64}\.json$/u.exec(name);
|
|
1349
|
+
if (match !== null && !retained.has(Number(match[1])))
|
|
1350
|
+
await rm(join(contextRoot, name), { force: true });
|
|
1351
|
+
}
|
|
1352
|
+
for (const exchange of exchanges.slice(2))
|
|
1213
1353
|
await rm(join(contextRoot, encodingFileName(exchange)), { force: true });
|
|
1214
1354
|
}
|
|
1215
1355
|
/**
|