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,5 +1,7 @@
|
|
|
1
|
+
import { builtinAuthorPrompts } from "../../shared/ordered-author-prompts.js";
|
|
2
|
+
import { expandRetiredPrompts, customizePromptArrangement, resolvePromptArrangement, retiredBuiltinReplacements, } from "../../shared/prompt-arrangement.js";
|
|
1
3
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { isMap,
|
|
4
|
+
import { isMap, parseDocument } from "yaml";
|
|
3
5
|
import { builtinPlayPrompts, isPromptMessageRole, isTurnGuideLine, promptGroups, } from "../../shared/ordered-play-prompts.js";
|
|
4
6
|
const promptName = /^[^\r\n\0]{1,160}$/u;
|
|
5
7
|
/** The same strict permission gate is used by portable imports and structured saves. */
|
|
@@ -86,24 +88,22 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
86
88
|
? retiredBuiltinReplacements[item.builtin]
|
|
87
89
|
: undefined;
|
|
88
90
|
if (retired !== undefined &&
|
|
89
|
-
exact(["id", "kind", "builtin", "enabled"]) &&
|
|
91
|
+
exact(["id", "kind", "builtin", "enabled"], ["turnGuide"]) &&
|
|
92
|
+
validTurnGuide &&
|
|
90
93
|
typeof item.enabled === "boolean") {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
},
|
|
105
|
-
];
|
|
106
|
-
});
|
|
94
|
+
if (builtins.has(item.builtin))
|
|
95
|
+
return fail();
|
|
96
|
+
builtins.add(item.builtin);
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
id: item.id,
|
|
100
|
+
kind: "builtin",
|
|
101
|
+
builtin: item.builtin,
|
|
102
|
+
enabled: item.enabled,
|
|
103
|
+
...messageRole,
|
|
104
|
+
...turnGuide,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
107
|
}
|
|
108
108
|
const builtin = catalog.find((entry) => entry.id === item.builtin);
|
|
109
109
|
if (item.kind !== "builtin" ||
|
|
@@ -128,10 +128,29 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
128
128
|
},
|
|
129
129
|
];
|
|
130
130
|
});
|
|
131
|
+
const expanded = expandRetiredPrompts(entries);
|
|
131
132
|
if (worlds !== (worldRequired ? 1 : 0) ||
|
|
132
|
-
catalog.some((item) =>
|
|
133
|
+
catalog.some((item) => {
|
|
134
|
+
if (!item.required)
|
|
135
|
+
return false;
|
|
136
|
+
const entry = expanded.find((entry) => entry.kind === "builtin" && entry.builtin === item.id);
|
|
137
|
+
return (entry?.kind !== "builtin" ||
|
|
138
|
+
!entry.enabled ||
|
|
139
|
+
(entry.messageRole !== undefined && entry.messageRole !== "system"));
|
|
140
|
+
}))
|
|
133
141
|
return fail();
|
|
134
|
-
return
|
|
142
|
+
return expanded;
|
|
143
|
+
}
|
|
144
|
+
/** The required identities are part of each released list-format contract.
|
|
145
|
+
* New catalog requirements apply after conversion, never while validating old bytes. */
|
|
146
|
+
export function releasedPromptRequiredIdentities(worldRequired) {
|
|
147
|
+
return worldRequired
|
|
148
|
+
? ["play.mechanics"]
|
|
149
|
+
: ["author.mechanics", "author.target"];
|
|
150
|
+
}
|
|
151
|
+
export function parseReleasedOrderedPrompts(value, catalog = builtinPlayPrompts("en"), worldRequired = true) {
|
|
152
|
+
const required = releasedPromptRequiredIdentities(worldRequired);
|
|
153
|
+
return parseOrderedPlayPrompts(value, catalog.map((item) => ({ ...item, required: required.includes(item.id) })), worldRequired);
|
|
135
154
|
}
|
|
136
155
|
function orderedPresetDocument(files) {
|
|
137
156
|
const source = files["preset.yaml"];
|
|
@@ -145,165 +164,104 @@ function orderedPresetDocument(files) {
|
|
|
145
164
|
return undefined;
|
|
146
165
|
return document;
|
|
147
166
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Insert the tabletop builtins that an ordered preset does not list yet.
|
|
150
|
-
* play.tabletop goes just before the world placeholder and author.tabletop just
|
|
151
|
-
* after author.guidance. Files that are not ordered v2 or do not parse come back
|
|
152
|
-
* unchanged, and nothing else in them is rewritten. The same object is returned
|
|
153
|
-
* when nothing needs inserting, so callers can detect a change by identity.
|
|
154
|
-
*/
|
|
155
|
-
export function withTabletopPromptBuiltins(files) {
|
|
156
|
-
const document = orderedPresetDocument(files);
|
|
157
|
-
if (document === undefined)
|
|
158
|
-
return files;
|
|
159
|
-
let changed = false;
|
|
160
|
-
const playPrompts = document.get("playPrompts", true);
|
|
161
|
-
if (isSeq(playPrompts) && !hasEntry(playPrompts, "play.tabletop")) {
|
|
162
|
-
const world = playPrompts.items.findIndex((item) => entryField(item, "kind") === "world");
|
|
163
|
-
playPrompts.items.splice(world < 0 ? playPrompts.items.length : world, 0, document.createNode({
|
|
164
|
-
id: "play.tabletop",
|
|
165
|
-
kind: "builtin",
|
|
166
|
-
builtin: "play.tabletop",
|
|
167
|
-
enabled: true,
|
|
168
|
-
}));
|
|
169
|
-
changed = true;
|
|
170
|
-
}
|
|
171
|
-
const authorPrompts = document.get("authorPrompts", true);
|
|
172
|
-
if (isSeq(authorPrompts) && !hasEntry(authorPrompts, "author.tabletop")) {
|
|
173
|
-
const guidance = authorPrompts.items.findIndex((item) => entryField(item, "id") === "author.guidance");
|
|
174
|
-
authorPrompts.items.splice(guidance < 0 ? authorPrompts.items.length : guidance + 1, 0, document.createNode({
|
|
175
|
-
id: "author.tabletop",
|
|
176
|
-
kind: "builtin",
|
|
177
|
-
builtin: "author.tabletop",
|
|
178
|
-
enabled: true,
|
|
179
|
-
}));
|
|
180
|
-
changed = true;
|
|
181
|
-
}
|
|
182
|
-
return changed ? { ...files, "preset.yaml": document.toString() } : files;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* The retired composite builtins and the blocks that replaced each of them.
|
|
186
|
-
* A stored entry for a retired builtin is replaced in place by its successors,
|
|
187
|
-
* which inherit its enabled state and message role; the unselected members of
|
|
188
|
-
* a choice group arrive disabled. User copies of the old text are not touched.
|
|
189
|
-
*/
|
|
190
|
-
const retiredBuiltinReplacements = {
|
|
191
|
-
"play.style": [{ builtin: "play.prose" }, { builtin: "play.scene" }],
|
|
192
|
-
"play.adjudication": [
|
|
193
|
-
{ builtin: "play.agency-default" },
|
|
194
|
-
{ builtin: "play.agency-strict", selected: false },
|
|
195
|
-
{ builtin: "play.agency-open", selected: false },
|
|
196
|
-
{ builtin: "play.facts" },
|
|
197
|
-
{ builtin: "play.npc" },
|
|
198
|
-
],
|
|
199
|
-
"play.narrative": [
|
|
200
|
-
{ builtin: "play.stop-beat" },
|
|
201
|
-
{ builtin: "play.stop-scene", selected: false },
|
|
202
|
-
{ builtin: "play.ending" },
|
|
203
|
-
{ builtin: "play.output" },
|
|
204
|
-
],
|
|
205
|
-
"play.state": [
|
|
206
|
-
{ builtin: "play.save-timing" },
|
|
207
|
-
{ builtin: "play.ownership" },
|
|
208
|
-
{ builtin: "play.situation" },
|
|
209
|
-
{ builtin: "play.checkpoint-policy" },
|
|
210
|
-
],
|
|
211
|
-
};
|
|
212
167
|
export const retiredPlayBuiltins = Object.keys(retiredBuiltinReplacements);
|
|
213
|
-
/**
|
|
214
|
-
|
|
215
|
-
* state) with the single-rule blocks that succeeded them, each at the old
|
|
216
|
-
* entry's position. Same identity contract as the other insertions.
|
|
217
|
-
*/
|
|
218
|
-
export function withRestructuredPromptBuiltins(files) {
|
|
168
|
+
/** One-time released v2 -> v3 encoding conversion, never a catalog migration. */
|
|
169
|
+
export function upgradeOrderedPromptFormat(files) {
|
|
219
170
|
const document = orderedPresetDocument(files);
|
|
220
|
-
if (document
|
|
171
|
+
if (!document)
|
|
221
172
|
return files;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
continue;
|
|
231
|
-
const old = playPrompts.items[at];
|
|
232
|
-
const enabled = entryField(old, "enabled") === true;
|
|
233
|
-
const messageRole = entryField(old, "messageRole");
|
|
234
|
-
const successors = replacements
|
|
235
|
-
.filter(({ builtin }) => !hasEntry(playPrompts, builtin))
|
|
236
|
-
.map(({ builtin, selected }) => document.createNode({
|
|
237
|
-
id: builtin,
|
|
238
|
-
kind: "builtin",
|
|
239
|
-
builtin,
|
|
240
|
-
enabled: selected === false ? false : enabled,
|
|
241
|
-
...(typeof messageRole === "string" ? { messageRole } : {}),
|
|
242
|
-
}));
|
|
243
|
-
playPrompts.items.splice(at, 1, ...successors);
|
|
244
|
-
changed = true;
|
|
173
|
+
try {
|
|
174
|
+
const source = document.toJS();
|
|
175
|
+
document.set("playPrompts", customizePromptArrangement(parseReleasedOrderedPrompts(source.playPrompts), builtinPlayPrompts("en")));
|
|
176
|
+
const author = document.get("authorPrompts");
|
|
177
|
+
if (author !== undefined)
|
|
178
|
+
document.set("authorPrompts", customizePromptArrangement(parseReleasedOrderedPrompts(source.authorPrompts, builtinAuthorPrompts("en"), false), builtinAuthorPrompts("en"), false));
|
|
179
|
+
document.set("format", "narraeon.play-preset/v3");
|
|
180
|
+
return { ...files, "preset.yaml": document.toString() };
|
|
245
181
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const alternativeGroups = () => {
|
|
249
|
-
const members = (group) => builtinPlayPrompts("en")
|
|
250
|
-
.filter((item) => item.group === group)
|
|
251
|
-
.map(({ id }) => id);
|
|
252
|
-
const order = promptGroups.map((group) => members(group));
|
|
253
|
-
// The default list runs mechanics, then the choice groups in `promptGroups`
|
|
254
|
-
// order, then the policy blocks: a missing group lands after the previous
|
|
255
|
-
// group (or mechanics), else ahead of the first later group or policy block.
|
|
256
|
-
return promptGroups.map((group, index) => ({
|
|
257
|
-
after: [...order.slice(0, index).flat().reverse(), "play.mechanics"],
|
|
258
|
-
before: [
|
|
259
|
-
...order.slice(index + 1).flat(),
|
|
260
|
-
"play.prose",
|
|
261
|
-
"play.scene",
|
|
262
|
-
"play.ending",
|
|
263
|
-
"play.output",
|
|
264
|
-
"play.facts",
|
|
265
|
-
"play.tabletop",
|
|
266
|
-
],
|
|
267
|
-
entries: groupPromptEntries(group),
|
|
268
|
-
}));
|
|
269
|
-
};
|
|
270
|
-
/**
|
|
271
|
-
* Insert the choice-group builtins (person, continuation, agency, stopping)
|
|
272
|
-
* that an ordered preset does not list yet. Same identity contract as the
|
|
273
|
-
* tabletop insertion.
|
|
274
|
-
*/
|
|
275
|
-
export function withAlternativePromptBuiltins(files) {
|
|
276
|
-
const document = orderedPresetDocument(files);
|
|
277
|
-
if (document === undefined)
|
|
278
|
-
return files;
|
|
279
|
-
const playPrompts = document.get("playPrompts", true);
|
|
280
|
-
if (!isSeq(playPrompts))
|
|
182
|
+
catch {
|
|
183
|
+
// Invalid drafts stay repairable, without dropping their original bytes.
|
|
281
184
|
return files;
|
|
282
|
-
let changed = false;
|
|
283
|
-
for (const group of alternativeGroups()) {
|
|
284
|
-
if (group.entries.some((entry) => hasEntry(playPrompts, entry.id)))
|
|
285
|
-
continue;
|
|
286
|
-
const anchor = group.after
|
|
287
|
-
.map((id) => playPrompts.items.findIndex((item) => entryField(item, "id") === id))
|
|
288
|
-
.find((index) => index >= 0);
|
|
289
|
-
const fallback = playPrompts.items.findIndex((item) => {
|
|
290
|
-
const id = entryField(item, "id");
|
|
291
|
-
return ((typeof id === "string" && group.before.includes(id)) ||
|
|
292
|
-
entryField(item, "kind") === "world");
|
|
293
|
-
});
|
|
294
|
-
const at = anchor !== undefined
|
|
295
|
-
? anchor + 1
|
|
296
|
-
: fallback >= 0
|
|
297
|
-
? fallback
|
|
298
|
-
: playPrompts.items.length;
|
|
299
|
-
playPrompts.items.splice(at, 0, ...group.entries.map((entry) => document.createNode(entry)));
|
|
300
|
-
changed = true;
|
|
301
185
|
}
|
|
302
|
-
return changed ? { ...files, "preset.yaml": document.toString() } : files;
|
|
303
186
|
}
|
|
304
|
-
/**
|
|
305
|
-
export function
|
|
306
|
-
|
|
187
|
+
/** Decode portable customizations through the same resolver used by every caller. */
|
|
188
|
+
export function parsePromptArrangement(value, catalog = builtinPlayPrompts("en"), worldRequired = true) {
|
|
189
|
+
const record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
190
|
+
const fail = () => {
|
|
191
|
+
throw new Error("Invalid prompt customizations");
|
|
192
|
+
};
|
|
193
|
+
if (!record(value) ||
|
|
194
|
+
Object.keys(value).some((k) => !["entries", "overrides", "order", "groups"].includes(k)))
|
|
195
|
+
return fail();
|
|
196
|
+
if (value.entries !== undefined &&
|
|
197
|
+
(!Array.isArray(value.entries) ||
|
|
198
|
+
value.entries.length > 256 ||
|
|
199
|
+
value.entries.some((e) => !record(e) || e.kind === "builtin")))
|
|
200
|
+
return fail();
|
|
201
|
+
const entries = (value.entries ?? []);
|
|
202
|
+
const entryIds = new Set(entries.map((e) => e.id));
|
|
203
|
+
if (entryIds.size !== entries.length ||
|
|
204
|
+
entries.filter((e) => e.kind === "world").length > (worldRequired ? 1 : 0))
|
|
205
|
+
return fail();
|
|
206
|
+
if (value.overrides !== undefined) {
|
|
207
|
+
if (!Array.isArray(value.overrides) || value.overrides.length > 256)
|
|
208
|
+
return fail();
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
const overrideIds = new Set();
|
|
211
|
+
for (const o of value.overrides) {
|
|
212
|
+
if (!record(o) ||
|
|
213
|
+
typeof o.builtin !== "string" ||
|
|
214
|
+
seen.has(o.builtin) ||
|
|
215
|
+
(!catalog.some((c) => c.id === o.builtin) &&
|
|
216
|
+
!retiredBuiltinReplacements[o.builtin]) ||
|
|
217
|
+
Object.keys(o).some((k) => !["builtin", "id", "enabled", "messageRole", "turnGuide"].includes(k)) ||
|
|
218
|
+
(o.enabled !== undefined && typeof o.enabled !== "boolean") ||
|
|
219
|
+
(o.messageRole !== undefined && !isPromptMessageRole(o.messageRole)) ||
|
|
220
|
+
(o.turnGuide !== undefined &&
|
|
221
|
+
(typeof o.turnGuide !== "string" || !isTurnGuideLine(o.turnGuide))) ||
|
|
222
|
+
(o.id !== undefined && typeof o.id !== "string"))
|
|
223
|
+
return fail();
|
|
224
|
+
if (typeof o.id === "string") {
|
|
225
|
+
if (entryIds.has(o.id) || overrideIds.has(o.id))
|
|
226
|
+
return fail();
|
|
227
|
+
overrideIds.add(o.id);
|
|
228
|
+
}
|
|
229
|
+
seen.add(o.builtin);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (value.order !== undefined &&
|
|
233
|
+
(!Array.isArray(value.order) ||
|
|
234
|
+
value.order.length > 512 ||
|
|
235
|
+
value.order.some((k) => typeof k !== "string") ||
|
|
236
|
+
new Set(value.order).size !== value.order.length))
|
|
237
|
+
return fail();
|
|
238
|
+
if (Array.isArray(value.order) &&
|
|
239
|
+
value.order.some((key) => {
|
|
240
|
+
if (typeof key !== "string")
|
|
241
|
+
return true;
|
|
242
|
+
if (key.startsWith("entry:"))
|
|
243
|
+
return (!entryIds.has(key.slice(6)) &&
|
|
244
|
+
!(worldRequired && key === "entry:world"));
|
|
245
|
+
return (!key.startsWith("builtin:") ||
|
|
246
|
+
(!catalog.some((c) => c.id === key.slice(8)) &&
|
|
247
|
+
!retiredBuiltinReplacements[key.slice(8)]));
|
|
248
|
+
}))
|
|
249
|
+
return fail();
|
|
250
|
+
if (value.groups !== undefined) {
|
|
251
|
+
if (!record(value.groups))
|
|
252
|
+
return fail();
|
|
253
|
+
for (const [group, ids] of Object.entries(value.groups)) {
|
|
254
|
+
if (!promptGroups.some((item) => item === group) ||
|
|
255
|
+
!Array.isArray(ids) ||
|
|
256
|
+
ids.some((id) => typeof id !== "string" ||
|
|
257
|
+
!catalog.some((c) => (c.id === id ||
|
|
258
|
+
retiredBuiltinReplacements[id]?.some((r) => r.builtin === c.id)) &&
|
|
259
|
+
c.group === group)) ||
|
|
260
|
+
new Set(ids).size !== ids.length)
|
|
261
|
+
return fail();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return parseOrderedPlayPrompts(resolvePromptArrangement(value, catalog, worldRequired), catalog, worldRequired);
|
|
307
265
|
}
|
|
308
266
|
/** A choice group's entries: the first member enabled, the rest disabled. */
|
|
309
267
|
function groupPromptEntries(group) {
|
|
@@ -316,12 +274,6 @@ function groupPromptEntries(group) {
|
|
|
316
274
|
enabled: index === 0,
|
|
317
275
|
}));
|
|
318
276
|
}
|
|
319
|
-
function hasEntry(sequence, id) {
|
|
320
|
-
return sequence.items.some((item) => entryField(item, "id") === id);
|
|
321
|
-
}
|
|
322
|
-
function entryField(item, key) {
|
|
323
|
-
return isMap(item) ? item.get(key) : undefined;
|
|
324
|
-
}
|
|
325
277
|
/** Recognized v1 content becomes user text, never a locked builtin inferred from a path. */
|
|
326
278
|
export function migrateLegacyPlayPrompts(files, narrative, excludedPaths = []) {
|
|
327
279
|
const document = parseDocument(files["frame.yaml"] ?? "", {
|
|
@@ -447,5 +399,14 @@ export function migrateLegacyPlayPrompts(files, narrative, excludedPaths = []) {
|
|
|
447
399
|
builtin: "play.tabletop",
|
|
448
400
|
enabled: true,
|
|
449
401
|
});
|
|
402
|
+
const listed = new Set(entries.flatMap((entry) => entry.kind === "builtin" ? [entry.builtin] : []));
|
|
403
|
+
entries.splice(entries.findIndex((entry) => entry.kind === "builtin" && entry.builtin === "play.tabletop"), 0, ...builtinPlayPrompts("en")
|
|
404
|
+
.filter((item) => !listed.has(item.id))
|
|
405
|
+
.map((item) => ({
|
|
406
|
+
id: item.id,
|
|
407
|
+
kind: "builtin",
|
|
408
|
+
builtin: item.id,
|
|
409
|
+
enabled: false,
|
|
410
|
+
})));
|
|
450
411
|
return entries;
|
|
451
412
|
}
|
|
@@ -10,6 +10,7 @@ const messages = {
|
|
|
10
10
|
serviceStoppedAfterHead: "The service stopped after the world was updated; use a fresh context.",
|
|
11
11
|
serviceStoppedSavingPlayer: "The service stopped while saving the player message; use a fresh context.",
|
|
12
12
|
serviceStoppedRetryable: "The service stopped while the model request was in progress; retry to call the model again.",
|
|
13
|
+
serviceStoppedUnknown: "The service stopped after the model request may have been dispatched; its outcome is unknown. Use a fresh context.",
|
|
13
14
|
serviceStoppedNoRequest: "The service stopped before preparing a model request; use a fresh context.",
|
|
14
15
|
dispatchInterrupted: "The model request was interrupted.",
|
|
15
16
|
};
|
|
@@ -43,29 +44,52 @@ export class PlayAdvanceLifecycle {
|
|
|
43
44
|
notify?.();
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* Recovers an advance cut off by a process exit. A persisted ending wins;
|
|
48
|
+
* otherwise Google requests that may have been dispatched cannot be replayed
|
|
49
|
+
* (ADR-0035), while other recovery follows ADR-0044.
|
|
49
50
|
*/
|
|
50
51
|
async recoverAbandoned(session, advance, headMoved) {
|
|
52
|
+
// Endings are persisted before their marker is cleared. If the process
|
|
53
|
+
// stopped in that gap, keep the durable ending rather than inventing a
|
|
54
|
+
// new retry decision for an advance that already settled.
|
|
55
|
+
if (advance !== null &&
|
|
56
|
+
session.status !== "running" &&
|
|
57
|
+
!(advance.advanceKind === "player" && session.status === "ready") &&
|
|
58
|
+
!headMoved) {
|
|
59
|
+
await this.#ports.clearMarker(advance);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
51
62
|
const playerPhase = advance?.advanceKind === "player";
|
|
52
63
|
const streaming = playerPhase
|
|
53
64
|
? undefined
|
|
54
65
|
: session.events.findLast((event) => event.kind === "assistant" &&
|
|
55
66
|
event.status === "streaming" &&
|
|
56
67
|
(advance === null || event.id === advance.eventId));
|
|
57
|
-
const
|
|
68
|
+
const unknownGoogleDispatch = advance?.advanceKind === "response" &&
|
|
69
|
+
session.status === "running" &&
|
|
70
|
+
(session.lastRequest?.modelConfig?.provider ??
|
|
71
|
+
session.modelBinding?.provider) === "google_interactions";
|
|
72
|
+
const canRetry = !headMoved &&
|
|
73
|
+
!playerPhase &&
|
|
74
|
+
!unknownGoogleDispatch &&
|
|
75
|
+
session.lastRequest !== null;
|
|
58
76
|
const message = headMoved
|
|
59
77
|
? messages.serviceStoppedAfterHead
|
|
60
78
|
: playerPhase
|
|
61
79
|
? messages.serviceStoppedSavingPlayer
|
|
62
|
-
:
|
|
63
|
-
? messages.
|
|
64
|
-
:
|
|
80
|
+
: unknownGoogleDispatch
|
|
81
|
+
? messages.serviceStoppedUnknown
|
|
82
|
+
: canRetry
|
|
83
|
+
? messages.serviceStoppedRetryable
|
|
84
|
+
: messages.serviceStoppedNoRequest;
|
|
65
85
|
await this.end(session, {
|
|
66
86
|
kind: "failed",
|
|
67
87
|
message,
|
|
68
|
-
failureKind:
|
|
88
|
+
failureKind: unknownGoogleDispatch
|
|
89
|
+
? "outcome_unknown"
|
|
90
|
+
: canRetry
|
|
91
|
+
? "retryable"
|
|
92
|
+
: "other",
|
|
69
93
|
canRetry,
|
|
70
94
|
streamingEventId: streaming?.id,
|
|
71
95
|
}, advance);
|
|
@@ -203,7 +227,7 @@ function interruptStreamingEvent(session, eventId) {
|
|
|
203
227
|
if (eventId === undefined)
|
|
204
228
|
return;
|
|
205
229
|
const event = session.events.find(({ id }) => id === eventId);
|
|
206
|
-
if (event?.kind === "assistant")
|
|
230
|
+
if (event?.kind === "assistant" && event.status === "streaming")
|
|
207
231
|
event.status = "interrupted";
|
|
208
232
|
}
|
|
209
233
|
function errorMessage(error, fallback) {
|
|
@@ -16,7 +16,8 @@ import { turnGuideWithTurnNote } from "../../shared/turn-guide.js";
|
|
|
16
16
|
import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepNotice, turnGuideNotice, } from "./PlayContinuity.js";
|
|
17
17
|
import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
|
|
18
18
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
19
|
-
import { equalModelHostBinding, ModelHostCancelledError, } from "../model/ModelHost.js";
|
|
19
|
+
import { equalModelHostBinding, ModelHostCancelledError, ModelHostOutcomeUnknownError, } from "../model/ModelHost.js";
|
|
20
|
+
import { validGoogleInteractionsContinuation } from "../model/ProviderExchangeState.js";
|
|
20
21
|
import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
|
|
21
22
|
import { parsePlayPresetFiles, } from "./FileNativePlayPresetStore.js";
|
|
22
23
|
import { runPlayFollowupRequests, } from "./PlayFollowupRequests.js";
|
|
@@ -234,6 +235,12 @@ export class PlayCallChain {
|
|
|
234
235
|
const session = await this.#requireChain(input.worldId, input.chainId);
|
|
235
236
|
await this.#assertCurrentHead(session);
|
|
236
237
|
assertSessionModelHostBinding(session, input.modelHost);
|
|
238
|
+
if (session.status === "ready" &&
|
|
239
|
+
session.modelBinding?.provider === "google_interactions" &&
|
|
240
|
+
session.transcript.some((item) => item.kind === "assistant" &&
|
|
241
|
+
(item.providerState?.protocol !== "google_interactions" ||
|
|
242
|
+
!validGoogleInteractionsContinuation(item.providerState.steps))))
|
|
243
|
+
throw new PlayCallChainError("The Google Interactions continuation is missing or corrupted; start a fresh context.");
|
|
237
244
|
if (input.playerText.trim().length === 0) {
|
|
238
245
|
if (session.status === "running")
|
|
239
246
|
throw new PlayCallChainError("A model request is still running.");
|
|
@@ -869,6 +876,10 @@ export class PlayCallChain {
|
|
|
869
876
|
updatedAt: Date.now(),
|
|
870
877
|
};
|
|
871
878
|
}
|
|
879
|
+
if (intent.continuation === "continue_context")
|
|
880
|
+
await this.#worlds.playTimeline
|
|
881
|
+
.copyRetainedEncodings(worldId, sourceContext.chainId, revised)
|
|
882
|
+
.catch(() => forgetFollowupPreviews(revised));
|
|
872
883
|
await this.#worlds.playTimeline.persist(revised);
|
|
873
884
|
if (intent.continuation === "fresh_context")
|
|
874
885
|
await this.#worlds.completeArchiveFreshContext(worldId);
|
|
@@ -965,6 +976,9 @@ export class PlayCallChain {
|
|
|
965
976
|
parentHead: intent.restoresHead,
|
|
966
977
|
updatedAt: Date.now(),
|
|
967
978
|
};
|
|
979
|
+
await this.#worlds.playTimeline
|
|
980
|
+
.copyRetainedEncodings(intent.worldId, sourceContext.chainId, prefix)
|
|
981
|
+
.catch(() => forgetFollowupPreviews(prefix));
|
|
968
982
|
await this.#worlds.playTimeline.persistDetached(structuredClone(prefix));
|
|
969
983
|
previousChainId = prefix.chainId;
|
|
970
984
|
}
|
|
@@ -1254,6 +1268,19 @@ export class PlayCallChain {
|
|
|
1254
1268
|
if (existing?.kind !== "assistant")
|
|
1255
1269
|
throw new PlayCallChainError("The durable Provider result no longer matches its assistant event.");
|
|
1256
1270
|
const calls = durable.toolCalls ?? [];
|
|
1271
|
+
if ((session.lastRequest?.modelConfig?.provider ??
|
|
1272
|
+
session.modelBinding?.provider) === "google_interactions" &&
|
|
1273
|
+
calls.length > 0) {
|
|
1274
|
+
const boundary = session.events.findLastIndex((event) => event.kind === "player" ||
|
|
1275
|
+
(event.kind === "assistant" &&
|
|
1276
|
+
event.status === "completed" &&
|
|
1277
|
+
event.responseKind === "narrative"));
|
|
1278
|
+
if (session.events
|
|
1279
|
+
.slice(boundary + 1)
|
|
1280
|
+
.some((event) => event.kind === "tool_call" &&
|
|
1281
|
+
calls.some((call) => call.id === event.callId)))
|
|
1282
|
+
throw new ModelHostOutcomeUnknownError("Google Interactions repeated a tool-call ID during the same send; its outcome cannot be safely settled. Use a fresh context.");
|
|
1283
|
+
}
|
|
1257
1284
|
const text = durable.text ?? existing.text;
|
|
1258
1285
|
const hasText = text.trim().length > 0;
|
|
1259
1286
|
const responseKind = calls.length > 0 ? "tool_step" : hasText ? "narrative" : "empty";
|
|
@@ -1561,6 +1588,8 @@ export class PlayCallChain {
|
|
|
1561
1588
|
}
|
|
1562
1589
|
response = await modelHost.exchange(request, {
|
|
1563
1590
|
signal,
|
|
1591
|
+
diagnosticAttempt: attempt,
|
|
1592
|
+
onGoogleHop: (hop) => this.#worlds.playTimeline.recordGoogleHop(session.worldId, session.chainId, session.promptRuns?.at(-1)?.exchange ?? 1, hop),
|
|
1564
1593
|
onDelta: (delta) => {
|
|
1565
1594
|
this.#observeInvocation(session.chainId, delta.kind, {
|
|
1566
1595
|
...(delta.kind === "reasoning"
|
|
@@ -1661,7 +1690,7 @@ export class PlayCallChain {
|
|
|
1661
1690
|
},
|
|
1662
1691
|
],
|
|
1663
1692
|
});
|
|
1664
|
-
await this.#advances.end(session, ending, advance, () => this.#notifySnapshot(observer, session));
|
|
1693
|
+
await this.#advances.end(session, { ...ending, streamingEventId: event.id }, advance, () => this.#notifySnapshot(observer, session));
|
|
1665
1694
|
return projectView(session);
|
|
1666
1695
|
}
|
|
1667
1696
|
}
|
|
@@ -1691,6 +1720,13 @@ export class PlayCallChain {
|
|
|
1691
1720
|
event.committedHead === session.parentHead);
|
|
1692
1721
|
if (reply === undefined)
|
|
1693
1722
|
throw new PlayCallChainError("Follow-up reply is not committed");
|
|
1723
|
+
const run = session.promptRuns?.at(-1);
|
|
1724
|
+
const runReplies = session.events.filter((event) => event.kind === "assistant" &&
|
|
1725
|
+
event.responseKind === "narrative" &&
|
|
1726
|
+
event.committedHead !== undefined &&
|
|
1727
|
+
event.id >= (run?.firstEventId ?? 1));
|
|
1728
|
+
// Without a new prompt run there is no distinct bounded encoding slot.
|
|
1729
|
+
const previewExchange = runReplies.length === 1 ? (run?.exchange ?? 1) : undefined;
|
|
1694
1730
|
const observeFollowupDelta = (delta) => {
|
|
1695
1731
|
this.#observeInvocation(session.chainId, "followup", {
|
|
1696
1732
|
...(delta.kind === "reasoning"
|
|
@@ -1715,6 +1751,14 @@ export class PlayCallChain {
|
|
|
1715
1751
|
? {}
|
|
1716
1752
|
: { reasoning: outcome.reasoning }),
|
|
1717
1753
|
...(outcome.usage === undefined ? {} : { usage: outcome.usage }),
|
|
1754
|
+
...(outcome.requestPreviewRecorded !== true ||
|
|
1755
|
+
outcome.requestPreviewStatus === undefined ||
|
|
1756
|
+
previewExchange === undefined
|
|
1757
|
+
? {}
|
|
1758
|
+
: {
|
|
1759
|
+
requestPreviewExchange: previewExchange,
|
|
1760
|
+
requestPreviewStatus: outcome.requestPreviewStatus,
|
|
1761
|
+
}),
|
|
1718
1762
|
toolCalls: structuredClone(outcome.toolCalls),
|
|
1719
1763
|
...(outcome.failure === undefined ? {} : { failure: outcome.failure }),
|
|
1720
1764
|
});
|
|
@@ -1757,6 +1801,21 @@ export class PlayCallChain {
|
|
|
1757
1801
|
? {}
|
|
1758
1802
|
: { failureLog: this.#failureLog }),
|
|
1759
1803
|
observer: {
|
|
1804
|
+
onGoogleHop: async (id, hop) => {
|
|
1805
|
+
if (previewExchange !== undefined)
|
|
1806
|
+
await this.#worlds.playTimeline.recordGoogleHop(session.worldId, session.chainId, previewExchange, hop, id);
|
|
1807
|
+
},
|
|
1808
|
+
onRequest: async (id, request) => {
|
|
1809
|
+
if (previewExchange === undefined)
|
|
1810
|
+
return false;
|
|
1811
|
+
// Only Google currently has a continuation-redacting encoder.
|
|
1812
|
+
const preview = modelHost.binding().provider === "google_interactions"
|
|
1813
|
+
? modelHost.previewAuthoringRequest?.(request)
|
|
1814
|
+
: undefined;
|
|
1815
|
+
if (preview === undefined)
|
|
1816
|
+
return false;
|
|
1817
|
+
return this.#worlds.playTimeline.recordFollowupEncoding(session.worldId, session.chainId, previewExchange, id, preview);
|
|
1818
|
+
},
|
|
1760
1819
|
onProviderDispatch: () => {
|
|
1761
1820
|
this.#setInvocationAbortable(session.chainId, true);
|
|
1762
1821
|
this.#observeInvocation(session.chainId, "followup", {
|
|
@@ -1837,8 +1896,9 @@ export class PlayCallChain {
|
|
|
1837
1896
|
}
|
|
1838
1897
|
/**
|
|
1839
1898
|
* An in-progress advance, or a session still marked running, means a model
|
|
1840
|
-
* dispatch or settlement was cut off by a process exit.
|
|
1841
|
-
*
|
|
1899
|
+
* dispatch or settlement was cut off by a process exit. Recovery preserves
|
|
1900
|
+
* persisted endings and applies the protocol's retry boundary (ADR-0035,
|
|
1901
|
+
* ADR-0044).
|
|
1842
1902
|
*/
|
|
1843
1903
|
async #reconcileSessionAdvance(session, loaded) {
|
|
1844
1904
|
const advance = loaded === undefined
|
|
@@ -2364,6 +2424,9 @@ function assertSessionModelHostBinding(session, modelHost) {
|
|
|
2364
2424
|
throw new PlayCallChainError("This model context predates durable provider continuation bindings; start a fresh context.");
|
|
2365
2425
|
if (session.modelBinding.provider !== modelHost.binding().provider)
|
|
2366
2426
|
throw new PlayCallChainError("The selected model protocol differs from this context. Switch back to its original protocol to continue, or start a fresh context.", "model_protocol_mismatch");
|
|
2427
|
+
if (session.modelBinding.provider === "google_interactions" &&
|
|
2428
|
+
!equalModelHostBinding(session.modelBinding, modelHost.binding()))
|
|
2429
|
+
throw new PlayCallChainError("The selected model connection does not match the frozen Google binding; start a fresh context.");
|
|
2367
2430
|
}
|
|
2368
2431
|
function validateIdentity(value, label) {
|
|
2369
2432
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u.test(value))
|
|
@@ -2398,6 +2461,15 @@ function turnNoteField(turnNote) {
|
|
|
2398
2461
|
function touch(session) {
|
|
2399
2462
|
session.updatedAt = Date.now();
|
|
2400
2463
|
}
|
|
2464
|
+
/** Failed diagnostic copies cannot attach unrelated encodings to retained events. */
|
|
2465
|
+
function forgetFollowupPreviews(session) {
|
|
2466
|
+
for (const event of session.events) {
|
|
2467
|
+
if (event.kind !== "followup")
|
|
2468
|
+
continue;
|
|
2469
|
+
delete event.requestPreviewExchange;
|
|
2470
|
+
delete event.requestPreviewStatus;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2401
2473
|
function notifyAssistantDelta(observer, delta) {
|
|
2402
2474
|
try {
|
|
2403
2475
|
observer?.onAssistantDelta?.(structuredClone(delta));
|