narraeon 0.7.10 → 0.8.1

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.
Files changed (49) hide show
  1. package/dist/node/protocol/modelConnections.js +15 -0
  2. package/dist/node/protocol/modelUsage.js +4 -0
  3. package/dist/node/protocol/sillyTavernContent.js +1 -0
  4. package/dist/node/protocol/v1.js +97 -0
  5. package/dist/node/runtime/V1Runtime.js +105 -3
  6. package/dist/node/runtime/authoring/AuthoringEditPlan.js +79 -0
  7. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
  8. package/dist/node/runtime/authoring/AuthoringRetry.js +59 -0
  9. package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
  10. package/dist/node/runtime/authoring/ModelToolConversation.js +66 -25
  11. package/dist/node/runtime/content/ContentWorkspace.js +10 -2
  12. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +14 -8
  13. package/dist/node/runtime/content/SillyTavernContent.js +796 -0
  14. package/dist/node/runtime/model/AiFailureLog.js +8 -1
  15. package/dist/node/runtime/model/DispatchFailure.js +8 -0
  16. package/dist/node/runtime/model/FileNativeModelAdapters.js +627 -18
  17. package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
  18. package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
  19. package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
  20. package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
  21. package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
  22. package/dist/node/runtime/model/ProviderStream.js +4 -2
  23. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
  24. package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
  25. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +199 -10
  26. package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
  27. package/dist/node/runtime/play/PlayAdvanceLifecycle.js +51 -23
  28. package/dist/node/runtime/play/PlayCallChain.js +92 -8
  29. package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
  30. package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
  31. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +321 -15
  32. package/dist/node/runtime/setting/SettingImprovementSession.js +285 -21
  33. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +236 -15
  34. package/dist/node/runtime/world-revision/WorldRevisionSession.js +324 -50
  35. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +47 -1
  36. package/dist/node/shared/application-preset-files.js +29 -0
  37. package/dist/node/shared/default-preset-host.js +50 -38
  38. package/dist/node/shared/first-party-action-choices.js +7 -11
  39. package/dist/node/shared/first-party-generic-panels.js +7 -11
  40. package/dist/node/shared/first-party-player-view.js +7 -11
  41. package/dist/node/shared/ordered-author-prompts.js +6 -4
  42. package/dist/node/shared/ordered-play-prompts.js +4 -2
  43. package/dist/node/shared/prompt-arrangement.js +205 -0
  44. package/dist/node/shared/sillytavern-preset.js +454 -0
  45. package/dist/web/assets/index-BthiDIQu.js +1432 -0
  46. package/dist/web/assets/{index-DH2BVsYd.css → index-wO9Y4HT1.css} +1 -1
  47. package/dist/web/index.html +2 -2
  48. package/package.json +1 -1
  49. package/dist/web/assets/index-B2kbaOGe.js +0 -1408
@@ -3,9 +3,12 @@ import { upgradeLegacyEpochs } from "./LegacyWorldRevisionEpochUpgrade.js";
3
3
  import { isPackageFollowupControlPath } from "../content/PackageFollowups.js";
4
4
  import { validAuthoringRequests, } from "../authoring/AuthoringRequestSnapshot.js";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
- import { mkdir, open, readFile, readdir, rename, rm } from "node:fs/promises";
6
+ import { readGoogleHops, writeGoogleHop } from "../model/GoogleHopJournal.js";
7
+ import { link, mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
7
8
  import { dirname, join, resolve } from "node:path";
8
9
  import { gameplayPath } from "../../protocol/gameplay.js";
10
+ import { validGoogleInteractionsSteps } from "../model/ProviderExchangeState.js";
11
+ import { hasDamagedGoogleSteps, withoutDamagedGoogleSteps, } from "../authoring/DamagedGoogleContinuation.js";
9
12
  import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
10
13
  import { isPlayPresetBinding, } from "../play/FileNativePlayPresetStore.js";
11
14
  const uuidV4Pattern = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
@@ -40,12 +43,19 @@ export class FileNativeWorldRevisionStore {
40
43
  this.#epochsRoot = join(root, "epochs");
41
44
  this.#sessionsRoot = join(root, "sessions");
42
45
  }
46
+ get sessionScope() {
47
+ return this.#sessionsRoot;
48
+ }
43
49
  createEpochId() {
44
50
  return `revision-${randomUUID()}`;
45
51
  }
46
52
  createSessionId() {
47
53
  return `world-session-${randomUUID()}`;
48
54
  }
55
+ recordGoogleHop(sessionId, requestId, hop) {
56
+ assertSessionId(sessionId);
57
+ return writeGoogleHop(join(this.#sessionsRoot, ".google-hops", sessionId), requestId, hop, () => stat(this.#sessionPath(sessionId)).then(() => true, () => false));
58
+ }
49
59
  createChangeSetId() {
50
60
  return `change-set:${randomUUID()}`;
51
61
  }
@@ -89,14 +99,108 @@ export class FileNativeWorldRevisionStore {
89
99
  });
90
100
  }
91
101
  readSession(sessionId) {
92
- return this.#afterMutations(() => this.#readSession(sessionId));
102
+ return this.#afterMutations(async () => {
103
+ try {
104
+ const session = await this.#readSession(sessionId);
105
+ if (hasDamagedGoogleSteps(session) &&
106
+ (await this.#damagedSession(sessionId)))
107
+ throw new Error("The historical conversation has damaged Google continuation; start a fresh model context");
108
+ return session;
109
+ }
110
+ catch (error) {
111
+ if (await this.#damagedSession(sessionId))
112
+ throw new Error("The historical conversation has damaged Google continuation; start a fresh model context", { cause: error });
113
+ throw error;
114
+ }
115
+ });
93
116
  }
94
117
  saveSession(session) {
95
118
  return this.#mutate(async () => {
96
119
  validateSession(session);
97
- await publishJson(this.#sessionPath(session.sessionId), session);
120
+ const path = this.#sessionPath(session.sessionId);
121
+ {
122
+ const source = await readFile(path).catch((error) => {
123
+ if (isNodeError(error) && error.code === "ENOENT")
124
+ return null;
125
+ throw error;
126
+ });
127
+ if (source !== null) {
128
+ const old = validateSession(JSON.parse(source.toString("utf8")));
129
+ if (old.schemaVersion === 3 && session.schemaVersion !== 3)
130
+ throw new Error("Cannot downgrade a migrated world-revision session");
131
+ if (old.schemaVersion !== 3) {
132
+ const backup = join(this.#sessionsRoot, ".legacy", `${session.sessionId}.json`);
133
+ await mkdir(dirname(backup), { recursive: true, mode: 0o700 });
134
+ const temporary = `${backup}.${randomUUID()}.tmp`;
135
+ try {
136
+ const handle = await open(temporary, "wx", 0o600);
137
+ try {
138
+ await handle.writeFile(source);
139
+ }
140
+ finally {
141
+ await handle.close();
142
+ }
143
+ try {
144
+ await link(temporary, backup);
145
+ }
146
+ catch (error) {
147
+ if (!isNodeError(error) || error.code !== "EEXIST")
148
+ throw error;
149
+ }
150
+ if (!(await readFile(backup)).equals(source))
151
+ throw new Error("World-revision legacy source backup does not match the original bytes");
152
+ }
153
+ finally {
154
+ await rm(temporary, { force: true });
155
+ }
156
+ session.schemaVersion = 3;
157
+ validateSession(session);
158
+ }
159
+ }
160
+ }
161
+ await publishJson(path, session);
98
162
  });
99
163
  }
164
+ async prepareEdit(input) {
165
+ validateEpoch(input.epoch);
166
+ validateSession(input.session);
167
+ if (input.epoch.worldId !== input.session.worldId ||
168
+ input.epoch.epochId !== input.session.epochId ||
169
+ !treeRevisionPattern.test(input.beforeRevision))
170
+ throw new Error("Invalid world-revision edit intent");
171
+ await publishJson(this.#editPath(input.epoch.epochId), input);
172
+ }
173
+ async readEdit(epochId) {
174
+ let value;
175
+ try {
176
+ value = JSON.parse(await readFile(this.#editPath(epochId), "utf8"));
177
+ }
178
+ catch (error) {
179
+ if (isNodeError(error) && error.code === "ENOENT")
180
+ return null;
181
+ throw error;
182
+ }
183
+ if (!isRecord(value) ||
184
+ !hasExactKeys(value, ["beforeRevision", "epoch", "session"]) ||
185
+ typeof value.beforeRevision !== "string" ||
186
+ !treeRevisionPattern.test(value.beforeRevision))
187
+ throw new Error("Invalid world-revision edit intent");
188
+ const epoch = validateEpoch(value.epoch);
189
+ const session = validateSession(value.session);
190
+ if (epoch.epochId !== epochId ||
191
+ epoch.worldId !== session.worldId ||
192
+ epoch.epochId !== session.epochId ||
193
+ epoch.lifecycle !== "active")
194
+ throw new Error("Invalid world-revision edit identity");
195
+ return { beforeRevision: value.beforeRevision, epoch, session };
196
+ }
197
+ async removeEdit(epochId) {
198
+ await rm(this.#editPath(epochId), { force: true });
199
+ }
200
+ #editPath(epochId) {
201
+ assertEpochId(epochId);
202
+ return join(this.#epochsRoot, `.edit-${epochId}.json`);
203
+ }
100
204
  async listSessions(worldId) {
101
205
  return this.#afterMutations(async () => {
102
206
  const entries = await readdir(this.#sessionsRoot, {
@@ -110,7 +214,19 @@ export class FileNativeWorldRevisionStore {
110
214
  for (const entry of entries) {
111
215
  if (!entry.isFile() || !sessionName.test(entry.name))
112
216
  continue;
113
- const session = await this.#readSession(entry.name.slice(0, -5));
217
+ const sessionId = entry.name.slice(0, -5);
218
+ let session;
219
+ try {
220
+ session = await this.#readSession(sessionId);
221
+ if (hasDamagedGoogleSteps(session))
222
+ session = (await this.#damagedSession(sessionId)) ?? session;
223
+ }
224
+ catch (error) {
225
+ const damaged = await this.#damagedSession(sessionId);
226
+ if (damaged === null)
227
+ throw error;
228
+ session = damaged;
229
+ }
114
230
  if (session.worldId === worldId)
115
231
  sessions.push(session);
116
232
  }
@@ -118,7 +234,10 @@ export class FileNativeWorldRevisionStore {
118
234
  });
119
235
  }
120
236
  async findByCreationRequest(worldId, requestId) {
121
- return ((await this.listSessions(worldId)).find(({ creationRequestId }) => creationRequestId === requestId) ?? null);
237
+ const match = (await this.listSessions(worldId)).find(({ creationRequestId }) => creationRequestId === requestId);
238
+ if (match?.damagedContinuation)
239
+ throw new Error("The historical conversation has damaged Google continuation; start a fresh model context with a new request ID");
240
+ return match ?? null;
122
241
  }
123
242
  deleteSession(worldId, sessionId) {
124
243
  return this.#mutate(async () => {
@@ -126,6 +245,10 @@ export class FileNativeWorldRevisionStore {
126
245
  if (session.worldId !== worldId)
127
246
  throw new Error("World-revision conversation belongs to another world");
128
247
  await rm(this.#sessionPath(sessionId));
248
+ await rm(join(this.#sessionsRoot, ".google-hops", sessionId), {
249
+ recursive: true,
250
+ force: true,
251
+ });
129
252
  });
130
253
  }
131
254
  async #readEpoch(epochId) {
@@ -155,8 +278,34 @@ export class FileNativeWorldRevisionStore {
155
278
  const session = validateSession(value);
156
279
  if (session.sessionId !== sessionId)
157
280
  throw new Error("World-revision conversation identity does not match its file");
281
+ for (const request of session.requests ?? [])
282
+ if (request.wireRequest !== undefined)
283
+ request.wireRequest = await readGoogleHops(join(this.#sessionsRoot, ".google-hops", sessionId), request.requestId, request.wireRequest);
158
284
  return session;
159
285
  }
286
+ async #damagedSession(sessionId) {
287
+ const raw = JSON.parse(await readFile(this.#sessionPath(sessionId), "utf8"));
288
+ const copy = withoutDamagedGoogleSteps(raw);
289
+ if (copy === null) {
290
+ if (hasDamagedGoogleSteps(raw))
291
+ throw new Error("Cannot safely identify the damaged historical conversation; settlement or identity is unverified");
292
+ return null;
293
+ }
294
+ const session = validateSession(copy);
295
+ if (session.sessionId !== sessionId ||
296
+ session.requests?.[0]?.requestId !== session.creationRequestId ||
297
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(session.creationRequestId))
298
+ throw new Error("Cannot safely identify the damaged historical conversation; its creation identity is unverified");
299
+ const epoch = await this.#readEpoch(session.epochId);
300
+ if (epoch.worldId !== session.worldId)
301
+ throw new Error("Cannot safely identify the damaged historical conversation; its world is inconsistent");
302
+ return {
303
+ ...session,
304
+ damagedContinuation: true,
305
+ runStatus: "interrupted",
306
+ lastFailure: "Google continuation is damaged; start a fresh model context.",
307
+ };
308
+ }
160
309
  async #afterMutations(read) {
161
310
  await this.#mutationTail;
162
311
  return read();
@@ -274,15 +423,51 @@ function validateSession(value) {
274
423
  "lastFailure",
275
424
  "authorization",
276
425
  "pendingSettlement",
277
- ], ["requests"]) ||
278
- (value.schemaVersion !== 1 && value.schemaVersion !== 2) ||
426
+ ], [
427
+ "requests",
428
+ "googleOutcomeUnknown",
429
+ ...(value.schemaVersion === 3 ? ["retryDispatch", "editReceipts"] : []),
430
+ ]) ||
431
+ (value.schemaVersion !== 1 &&
432
+ value.schemaVersion !== 2 &&
433
+ value.schemaVersion !== 3) ||
434
+ (value.editReceipts !== undefined &&
435
+ !validEditReceipts(value.editReceipts)) ||
436
+ (value.retryDispatch !== undefined &&
437
+ (!isRecord(value.retryDispatch) ||
438
+ !hasExactKeys(value.retryDispatch, [
439
+ "requestId",
440
+ "exchange",
441
+ "modelItemCount",
442
+ "state",
443
+ ]) ||
444
+ !nonEmptyString(value.retryDispatch.requestId) ||
445
+ !count(value.retryDispatch.exchange) ||
446
+ !count(value.retryDispatch.modelItemCount) ||
447
+ !Array.isArray(value.requests) ||
448
+ !isRecord(value.requests.at(-1)) ||
449
+ value.requests.at(-1).requestId !==
450
+ value.retryDispatch.requestId ||
451
+ (value.runStatus !== "running" && value.runStatus !== "interrupted") ||
452
+ (value.retryDispatch.state === "retryable" &&
453
+ (!isRecord(value.modelBinding) ||
454
+ value.modelBinding.provider === "google_interactions")) ||
455
+ value.pendingSettlement !== null ||
456
+ !count(value.exchange) ||
457
+ (value.retryDispatch.exchange !== value.exchange &&
458
+ value.retryDispatch.exchange !== value.exchange + 1) ||
459
+ value.retryDispatch.modelItemCount !==
460
+ (Array.isArray(value.modelItems) ? value.modelItems.length : -1) ||
461
+ !["dispatching", "retryable", "blocked"].includes(String(value.retryDispatch.state)))) ||
279
462
  (value.schemaVersion === 1
280
463
  ? value.requests !== undefined
281
- : !validAuthoringRequests(value.requests, validPromptCompilation, {
282
- modelItems: value.modelItems,
283
- activeRequestId: value.activeRequestId,
284
- completedRequestIds: value.completedRequestIds,
285
- })) ||
464
+ : value.schemaVersion === 3 && value.requests === undefined
465
+ ? value.retryDispatch !== undefined
466
+ : !validAuthoringRequests(value.requests, validPromptCompilation, {
467
+ modelItems: value.modelItems,
468
+ activeRequestId: value.activeRequestId,
469
+ completedRequestIds: value.completedRequestIds,
470
+ })) ||
286
471
  typeof value.sessionId !== "string" ||
287
472
  !nonEmptyString(value.worldId) ||
288
473
  typeof value.worldTitle !== "string" ||
@@ -311,6 +496,10 @@ function validateSession(value) {
311
496
  !Array.isArray(value.completedRequestIds) ||
312
497
  !value.completedRequestIds.every((item) => typeof item === "string") ||
313
498
  (value.lastFailure !== null && typeof value.lastFailure !== "string") ||
499
+ (value.googleOutcomeUnknown !== undefined &&
500
+ (typeof value.googleOutcomeUnknown !== "boolean" ||
501
+ !isRecord(value.modelBinding) ||
502
+ value.modelBinding.provider !== "google_interactions")) ||
314
503
  !validAuthorization(value.authorization) ||
315
504
  !validPendingSettlement(value.pendingSettlement) ||
316
505
  !validPendingForSession(value.pendingSettlement, value.epochId, value.runStatus) ||
@@ -320,6 +509,31 @@ function validateSession(value) {
320
509
  assertEpochId(value.epochId);
321
510
  return structuredClone(value);
322
511
  }
512
+ function validEditReceipts(value) {
513
+ if (!Array.isArray(value))
514
+ return false;
515
+ const ids = new Set();
516
+ return value.every((entry) => {
517
+ if (!isRecord(entry) ||
518
+ !hasExactKeys(entry, [
519
+ "requestId",
520
+ "messageId",
521
+ "message",
522
+ "filePolicy",
523
+ ]) ||
524
+ typeof entry.requestId !== "string" ||
525
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(entry.requestId) ||
526
+ ids.has(entry.requestId) ||
527
+ !nonEmptyString(entry.messageId) ||
528
+ typeof entry.message !== "string" ||
529
+ entry.message.trim().length === 0 ||
530
+ entry.message.length > 64 * 1024 ||
531
+ (entry.filePolicy !== "keep" && entry.filePolicy !== "revert"))
532
+ return false;
533
+ ids.add(entry.requestId);
534
+ return true;
535
+ });
536
+ }
323
537
  function validFiles(value) {
324
538
  if (!Array.isArray(value))
325
539
  return false;
@@ -452,6 +666,9 @@ function validModelToolCall(value) {
452
666
  function validProviderState(value) {
453
667
  if (!isRecord(value))
454
668
  return false;
669
+ if (value.protocol === "google_interactions")
670
+ return (hasExactKeys(value, ["protocol", "steps"]) &&
671
+ validGoogleInteractionsSteps(value.steps));
455
672
  if (value.protocol === "chat_completions")
456
673
  return hasExactKeys(value, ["protocol", "assistantMessage"]);
457
674
  if (value.protocol === "openai_responses")
@@ -509,7 +726,8 @@ function validModelBinding(value) {
509
726
  ], ["cacheStrategy"]) &&
510
727
  (value.provider === "chat_completions" ||
511
728
  value.provider === "openai_responses" ||
512
- value.provider === "anthropic_messages") &&
729
+ value.provider === "anthropic_messages" ||
730
+ value.provider === "google_interactions") &&
513
731
  typeof value.endpointFingerprint === "string" &&
514
732
  typeof value.modelId === "string" &&
515
733
  count(value.contextWindowTokens) &&
@@ -531,7 +749,10 @@ function validModelUsage(value) {
531
749
  "totalTokens",
532
750
  ];
533
751
  if (!isRecord(value) ||
534
- !hasExactKeys(value, [...fields, "provenance"]) ||
752
+ !hasExactKeys(value, [...fields, "provenance"], ["providerRaw", "accounting"]) ||
753
+ (value.providerRaw !== undefined && !isRecord(value.providerRaw)) ||
754
+ (value.accounting !== undefined &&
755
+ value.accounting !== "google_interactions") ||
535
756
  !fields.every((field) => value[field] === null || count(value[field])) ||
536
757
  !isRecord(value.provenance) ||
537
758
  !hasExactKeys(value.provenance, fields))
@@ -594,7 +815,7 @@ function validPendingSettlement(value) {
594
815
  function validPendingForSession(pending, epochId, runStatus) {
595
816
  return (pending === null ||
596
817
  (isRecord(pending) &&
597
- runStatus === "running" &&
818
+ (runStatus === "running" || runStatus === "interrupted") &&
598
819
  pending.epochId === epochId));
599
820
  }
600
821
  function validToolResult(value) {