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
@@ -6,6 +6,15 @@ export function hasCLIProxyThinkingSuffix(modelId) {
6
6
  export function modelReasoningPolicyIssue(input) {
7
7
  const thinking = input.thinking ?? "provider_default";
8
8
  const thinkingBudgetTokens = input.thinkingBudgetTokens ?? null;
9
+ if (input.provider === "google_interactions") {
10
+ if (input.dialect !== "standard")
11
+ return "Google Interactions requires the standard dialect";
12
+ if (input.effort !== "provider_default" ||
13
+ input.summary !== "provider_default" ||
14
+ thinking !== "provider_default" ||
15
+ thinkingBudgetTokens !== null)
16
+ return "Google Interactions model capabilities are unverified; use provider defaults";
17
+ }
9
18
  const thinkingControlledByCLIProxySuffix = input.provider === "anthropic_messages" &&
10
19
  input.dialect === "cliproxyapi" &&
11
20
  hasCLIProxyThinkingSuffix(input.modelId);
@@ -54,6 +63,12 @@ export const modelProviderPresets = [
54
63
  provider: "chat_completions",
55
64
  baseUrl: "",
56
65
  },
66
+ {
67
+ id: "google-interactions",
68
+ name: "Google Gemini · Interactions",
69
+ provider: "google_interactions",
70
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta",
71
+ },
57
72
  {
58
73
  id: "openai",
59
74
  name: "OpenAI · Responses API",
@@ -24,6 +24,10 @@ export function aggregateModelUsage(current, next) {
24
24
  }
25
25
  return {
26
26
  ...result,
27
+ ...(next?.accounting === "google_interactions" ||
28
+ current.accounting === "google_interactions"
29
+ ? { accounting: "google_interactions" }
30
+ : {}),
27
31
  provenance: Object.fromEntries(modelUsageKeys.map((key) => [
28
32
  key,
29
33
  result[key] === null ? "unavailable" : "derived_provider_fields",
@@ -0,0 +1 @@
1
+ export {};
@@ -73,6 +73,14 @@ const requiredFields = {
73
73
  message: "string",
74
74
  continuation: "object",
75
75
  },
76
+ "setting-improvement.message.edit": {
77
+ packageId: "string",
78
+ sessionId: "string",
79
+ requestId: "string",
80
+ messageId: "string",
81
+ message: "string",
82
+ filePolicy: "string",
83
+ },
76
84
  "setting-improvement.cancel": { sessionId: "string" },
77
85
  "setting-improvement.resume": {
78
86
  packageId: "string",
@@ -89,6 +97,28 @@ const requiredFields = {
89
97
  "play.select": { presetId: "string" },
90
98
  "play.export": { presetId: "string" },
91
99
  "play.import": { name: "string", files: "array" },
100
+ "content.sillytavern.inspect": { source: "string" },
101
+ "content.sillytavern.preview": {
102
+ source: "string",
103
+ name: "string",
104
+ selection: "object",
105
+ },
106
+ "content.sillytavern.import": {
107
+ source: "string",
108
+ name: "string",
109
+ selection: "object",
110
+ },
111
+ "play.sillytavern.inspect": { source: "string" },
112
+ "play.sillytavern.preview": {
113
+ source: "string",
114
+ selected: "array",
115
+ name: "string",
116
+ },
117
+ "play.sillytavern.import": {
118
+ source: "string",
119
+ selected: "array",
120
+ name: "string",
121
+ },
92
122
  "play.workbench.read": {},
93
123
  "prompt.preview": {
94
124
  packageId: "string",
@@ -130,6 +160,14 @@ const requiredFields = {
130
160
  message: "string",
131
161
  continuation: "object",
132
162
  },
163
+ "world.revision.message.edit": {
164
+ worldId: "string",
165
+ sessionId: "string",
166
+ requestId: "string",
167
+ messageId: "string",
168
+ message: "string",
169
+ filePolicy: "string",
170
+ },
133
171
  "world.revision.cancel": { sessionId: "string" },
134
172
  "world.revision.resume": {
135
173
  worldId: "string",
@@ -229,6 +267,10 @@ function validateRequestFields(request) {
229
267
  if (!valid || (kind === "string" && value === "" && !allowsEmptyString))
230
268
  throw new V1ProtocolError("invalid_request", `${String(request.type)}.${field} is invalid`);
231
269
  }
270
+ if (request.type === "setting-improvement.message.edit" &&
271
+ request.filePolicy !== "keep" &&
272
+ request.filePolicy !== "revert")
273
+ throw new V1ProtocolError("invalid_request", "setting-improvement.message.edit.filePolicy is invalid");
232
274
  if (request.type === "world.story.export" &&
233
275
  request.format !== "markdown" &&
234
276
  request.format !== "text")
@@ -326,6 +368,53 @@ function validateRequestFields(request) {
326
368
  if (request.revision !== undefined && request.presetId === undefined)
327
369
  throw new V1ProtocolError("invalid_request", "play.workbench.read.revision must be provided with presetId");
328
370
  }
371
+ if (typeof request.type === "string" &&
372
+ request.type.startsWith("content.sillytavern.")) {
373
+ const inspect = request.type === "content.sillytavern.inspect";
374
+ const allowed = inspect
375
+ ? ["type", "source"]
376
+ : ["type", "source", "name", "selection"];
377
+ if (Object.keys(request).some((key) => !allowed.includes(key)))
378
+ throw new V1ProtocolError("invalid_request", "Invalid SillyTavern content fields");
379
+ if (!inspect) {
380
+ const selection = request.selection;
381
+ if (!isRecord(selection) ||
382
+ Object.keys(selection).some((key) => ![
383
+ "character",
384
+ "scenario",
385
+ "opening",
386
+ "example",
387
+ "systemPrompt",
388
+ "attachments",
389
+ "entries",
390
+ ].includes(key)) ||
391
+ (!Array.isArray(selection.attachments) &&
392
+ selection.attachments !== undefined) ||
393
+ (Array.isArray(selection.attachments) &&
394
+ !selection.attachments.every((value) => typeof value === "string")) ||
395
+ !isRecord(selection.entries) ||
396
+ Object.values(selection.entries).some((value) => value !== "world" && value !== "attachment") ||
397
+ ["character", "scenario", "example", "systemPrompt"].some((key) => selection[key] !== undefined && typeof selection[key] !== "boolean") ||
398
+ (selection.opening !== undefined &&
399
+ typeof selection.opening !== "string"))
400
+ throw new V1ProtocolError("invalid_request", "Invalid SillyTavern content selection");
401
+ }
402
+ }
403
+ if (typeof request.type === "string" &&
404
+ request.type.startsWith("play.sillytavern.")) {
405
+ const inspect = request.type === "play.sillytavern.inspect";
406
+ const allowed = inspect
407
+ ? ["type", "source", "groupId"]
408
+ : ["type", "source", "groupId", "selected", "name"];
409
+ if (Object.keys(request).some((key) => !allowed.includes(key)) ||
410
+ (request.groupId !== undefined &&
411
+ (typeof request.groupId !== "string" ||
412
+ request.groupId.length === 0)) ||
413
+ (!inspect &&
414
+ (!Array.isArray(request.selected) ||
415
+ !request.selected.every((value) => typeof value === "string" && value.length > 0))))
416
+ throw new V1ProtocolError("invalid_request", `${request.type} has invalid fields`);
417
+ }
329
418
  if (request.type === "play.import") {
330
419
  if (!request.files.every((file) => isRecord(file) &&
331
420
  typeof file.path === "string" &&
@@ -441,6 +530,7 @@ const requestTypes = new Set([
441
530
  "setting-improvement.session.delete",
442
531
  "setting-improvement.rollback",
443
532
  "setting-improvement.message",
533
+ "setting-improvement.message.edit",
444
534
  "setting-improvement.cancel",
445
535
  "setting-improvement.resume",
446
536
  "play.read",
@@ -454,6 +544,12 @@ const requestTypes = new Set([
454
544
  "play.select",
455
545
  "play.export",
456
546
  "play.import",
547
+ "content.sillytavern.inspect",
548
+ "content.sillytavern.preview",
549
+ "content.sillytavern.import",
550
+ "play.sillytavern.inspect",
551
+ "play.sillytavern.preview",
552
+ "play.sillytavern.import",
457
553
  "play.workbench.read",
458
554
  "prompt.preview",
459
555
  "world.create",
@@ -476,6 +572,7 @@ const requestTypes = new Set([
476
572
  "world.revision.session.read",
477
573
  "world.revision.session.delete",
478
574
  "world.revision.message",
575
+ "world.revision.message.edit",
479
576
  "world.revision.cancel",
480
577
  "world.revision.resume",
481
578
  "world.revision.files.replace",
@@ -1,3 +1,5 @@
1
+ import { inspectSillyTavernContent, convertSillyTavernContent, } from "./content/SillyTavernContent.js";
2
+ import { inspectSillyTavernPreset, convertSillyTavernPreset, } from "../shared/sillytavern-preset.js";
1
3
  import { WorldArchives } from "./archive/WorldArchives.js";
2
4
  import { WorldArchiveError } from "./archive/WorldArchiveZip.js";
3
5
  import { FileNativeWorldCreationError } from "./world/FileNativeWorldStore.js";
@@ -16,14 +18,14 @@ import { ModelConnectionStore } from "./model/ModelConnectionStore.js";
16
18
  import { FileNativeContinuityCorrection } from "./play/FileNativeContinuityCorrection.js";
17
19
  import { fingerprintControl } from "./play/PlayDocumentTools.js";
18
20
  import { PlayCallChain, PlayCallChainError, } from "./play/PlayCallChain.js";
19
- import { FileNativePlayPresetError, FileNativePlayPresetStore, presetHostBinding, } from "./play/FileNativePlayPresetStore.js";
21
+ import { FileNativePlayPresetError, FileNativePlayPresetStore, presetHostBinding, parsePlayPresetFiles, toPlayPresetStructuredEditor, } from "./play/FileNativePlayPresetStore.js";
20
22
  import { buildPlayPresetWorkbenchSnapshot } from "./play/PlayPresetWorkbench.js";
21
23
  import { FileNativeArtifactStore } from "./artifact/FileNativeArtifactStore.js";
22
24
  import { projectArtifactForFrontend, projectDebugArtifactForFrontend, } from "./extension/FrontendExtensionBundle.js";
23
25
  import { projectPlayerViewPanels, } from "./extension/PlayerViewPanelProjector.js";
24
26
  import { FileNativePromptCompiler } from "./prompt/FileNativePromptCompiler.js";
25
27
  import { FileNativeSettingImprovementStore } from "./setting/FileNativeSettingImprovementStore.js";
26
- import { SettingImprovementRollbackError, SettingImprovementSession, } from "./setting/SettingImprovementSession.js";
28
+ import { SettingImprovementRollbackError, SettingImprovementSession, SettingMessageEditError, } from "./setting/SettingImprovementSession.js";
27
29
  import { FileNativeWorldStore } from "./world/FileNativeWorldStore.js";
28
30
  import { WorldDocumentStore } from "./world/WorldDocumentStore.js";
29
31
  import { FileNativeAiFailureLog } from "./model/AiFailureLog.js";
@@ -31,7 +33,7 @@ import { FileNativeRuntimeErrorLog } from "./RuntimeErrorLog.js";
31
33
  import { ProtagonistTemplateStore } from "./config/ProtagonistTemplateStore.js";
32
34
  import { AppPreferencesStore } from "./config/AppPreferencesStore.js";
33
35
  import { FileNativeWorldRevisionStore } from "./world-revision/FileNativeWorldRevisionStore.js";
34
- import { WorldRevisionSession } from "./world-revision/WorldRevisionSession.js";
36
+ import { WorldRevisionSession, WorldRevisionMessageEditError, } from "./world-revision/WorldRevisionSession.js";
35
37
  import { WorldRevisionWorkspace, WorldRevisionWorkspaceError, } from "./world-revision/WorldRevisionWorkspace.js";
36
38
  import { ConversationChanges } from "./ConversationChanges.js";
37
39
  // 先取得当前端点再按它读取的请求,在端点被回退丢弃时最多重做这么多次(#94)。
@@ -363,6 +365,33 @@ export class V1Runtime {
363
365
  throw error;
364
366
  }
365
367
  }
368
+ case "content.sillytavern.inspect":
369
+ case "content.sillytavern.preview":
370
+ case "content.sillytavern.import": {
371
+ try {
372
+ if (request.source.length > 12 * 1024 * 1024 ||
373
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(request.source))
374
+ throw new Error("Invalid source encoding");
375
+ const source = Buffer.from(request.source, "base64");
376
+ if (source.toString("base64") !== request.source)
377
+ throw new Error("Invalid source encoding");
378
+ if (request.type === "content.sillytavern.inspect")
379
+ return inspectSillyTavernContent(source);
380
+ const converted = convertSillyTavernContent(source, request.selection, this.#locale);
381
+ this.#content.validateCurrentTreeContentPackage(converted.files);
382
+ if (request.type === "content.sillytavern.preview")
383
+ return {
384
+ ...converted,
385
+ inspection: this.#content.inspectCurrentTreeContentPackage(converted.files),
386
+ };
387
+ return await this.#content.importSillyTavernContent(request.name, converted.files);
388
+ }
389
+ catch (error) {
390
+ throw new V1ProtocolError("invalid_request", error instanceof Error
391
+ ? error.message
392
+ : "Invalid SillyTavern content");
393
+ }
394
+ }
366
395
  case "content.export": {
367
396
  const exported = await this.#content.exportCurrentTreeContentPackage(request.packageId);
368
397
  return {
@@ -395,6 +424,22 @@ export class V1Runtime {
395
424
  }
396
425
  case "setting-improvement.message":
397
426
  return this.#settingImprovements.send(request);
427
+ case "setting-improvement.message.edit":
428
+ try {
429
+ return await this.#settingImprovements.editMessage(request);
430
+ }
431
+ catch (error) {
432
+ if (error instanceof SettingMessageEditError)
433
+ throw new V1ProtocolError("invalid_request", error.message, {
434
+ cause: error,
435
+ details: {
436
+ kind: "setting_message_edit",
437
+ reason: error.reason,
438
+ paths: error.paths,
439
+ },
440
+ });
441
+ throw error;
442
+ }
398
443
  case "setting-improvement.cancel":
399
444
  return this.#settingImprovements.cancel(request.sessionId);
400
445
  case "setting-improvement.resume":
@@ -423,6 +468,47 @@ export class V1Runtime {
423
468
  };
424
469
  case "play.import":
425
470
  return this.#playPresets.importPortable(request);
471
+ case "play.sillytavern.inspect":
472
+ case "play.sillytavern.preview":
473
+ case "play.sillytavern.import": {
474
+ // Do not trust the browser to enforce the import limit or supply
475
+ // already-converted files. Preview and import both parse from source.
476
+ if (Buffer.byteLength(request.source, "utf8") > 2 * 1024 * 1024)
477
+ throw new V1ProtocolError("invalid_request", "SillyTavern source exceeds the size limit");
478
+ if (request.type !== "play.sillytavern.inspect" &&
479
+ (request.selected.length > 4096 ||
480
+ request.selected.reduce((bytes, id) => bytes + Buffer.byteLength(id, "utf8"), 0) >
481
+ 2 * 1024 * 1024))
482
+ throw new V1ProtocolError("invalid_request", "SillyTavern selection exceeds the size limit");
483
+ try {
484
+ if (request.type === "play.sillytavern.inspect")
485
+ return inspectSillyTavernPreset(request.source, request.groupId);
486
+ const converted = convertSillyTavernPreset(request.source, {
487
+ groupId: request.groupId,
488
+ selected: request.selected,
489
+ name: request.name,
490
+ locale: this.#locale,
491
+ });
492
+ if (request.type === "play.sillytavern.preview") {
493
+ const parsed = parsePlayPresetFiles(converted.files);
494
+ if (parsed.kind === "invalid")
495
+ throw parsed.error;
496
+ toPlayPresetStructuredEditor(parsed.definition);
497
+ return converted;
498
+ }
499
+ return await this.#playPresets.importPortable({
500
+ name: request.name,
501
+ files: converted.files,
502
+ });
503
+ }
504
+ catch (error) {
505
+ if (error instanceof V1ProtocolError)
506
+ throw error;
507
+ throw new V1ProtocolError("invalid_request", error instanceof Error
508
+ ? error.message
509
+ : "Invalid SillyTavern preset");
510
+ }
511
+ }
426
512
  case "play.workbench.read": {
427
513
  const binding = request.presetId === undefined
428
514
  ? await this.#playPresets.bindCurrent()
@@ -638,6 +724,22 @@ export class V1Runtime {
638
724
  return this.#worldRevisionCall(() => this.#worldRevisions.deleteSession(request.worldId, request.sessionId));
639
725
  case "world.revision.message":
640
726
  return this.#worldRevisionCall(() => this.#worldRevisions.send(request));
727
+ case "world.revision.message.edit":
728
+ try {
729
+ return await this.#worldRevisionCall(() => this.#worldRevisions.editMessage(request));
730
+ }
731
+ catch (error) {
732
+ if (error instanceof WorldRevisionMessageEditError)
733
+ throw new V1ProtocolError("invalid_request", error.message, {
734
+ cause: error,
735
+ details: {
736
+ kind: "world_revision_message_edit",
737
+ reason: error.reason,
738
+ paths: error.paths,
739
+ },
740
+ });
741
+ throw error;
742
+ }
641
743
  case "world.revision.cancel":
642
744
  return this.#worldRevisionCall(() => this.#worldRevisions.cancel(request.sessionId));
643
745
  case "world.revision.resume":
@@ -0,0 +1,79 @@
1
+ import { validAuthoringNativeContinuation } from "../model/FileNativeModelAdapters.js";
2
+ /** Checks idempotency before a target claims its own publication lease. */
3
+ export function completedAuthoringEdit(session, edit) {
4
+ const prior = session.editReceipts?.find(({ requestId }) => requestId === edit.requestId);
5
+ if (prior !== undefined &&
6
+ (prior.messageId !== edit.messageId ||
7
+ prior.message !== edit.message ||
8
+ prior.filePolicy !== edit.filePolicy))
9
+ throw new Error("This edit request ID was already used for different content");
10
+ if (!session.completedRequestIds.includes(edit.requestId))
11
+ return false;
12
+ if (prior === undefined)
13
+ throw new Error("This request ID was already used for another authoring operation");
14
+ return true;
15
+ }
16
+ /** Locates and validates the native prefix and prepares the replacement transcript. */
17
+ export function planAuthoringEdit(session, edit) {
18
+ const messageIndex = session.messages.findIndex((entry) => entry.role === "user" && entry.id === edit.messageId);
19
+ if (messageIndex < 0)
20
+ throw new Error("The selected authoring message no longer exists");
21
+ const userIndex = session.messages
22
+ .slice(0, messageIndex)
23
+ .filter((entry) => entry.role === "user").length;
24
+ const itemIndex = session.modelItems
25
+ .map((item, index) => (item.kind === "user" ? index : -1))
26
+ .filter((index) => index >= 0)[userIndex];
27
+ if (itemIndex === undefined ||
28
+ session.modelItems[itemIndex]?.kind !== "user" ||
29
+ session.modelItems[itemIndex].text !== session.messages[messageIndex].text)
30
+ throw new Error("The selected authoring message cannot be matched to the native transcript");
31
+ const prefix = session.modelItems.slice(0, itemIndex);
32
+ if (!validAuthoringNativeContinuation(prefix, session.modelBinding.provider))
33
+ throw new Error("The native continuation before this message is damaged; start a fresh context");
34
+ const next = structuredClone(session);
35
+ next.editReceipts = [...(next.editReceipts ?? []), edit];
36
+ delete next.retryDispatch;
37
+ next.modelItems = prefix;
38
+ next.messages = next.messages.slice(0, messageIndex);
39
+ next.requests = (next.requests ?? []).filter((entry) => entry.modelItemStart < itemIndex);
40
+ return {
41
+ itemIndex,
42
+ tail: session.modelItems.slice(itemIndex),
43
+ next: { ...next, requests: next.requests },
44
+ };
45
+ }
46
+ /** Returns a candidate tree and conflicts; the target validates and publishes it. */
47
+ export function revertAuthoringChanges(files, items) {
48
+ const result = new Map(files.map((file) => [file.path, structuredClone(file)]));
49
+ const conflicts = new Set();
50
+ const changes = items.flatMap((item) => item.kind === "tool" && !item.isError ? item.changes : []);
51
+ for (const change of changes.reverse()) {
52
+ if (conflicts.has(change.path))
53
+ continue;
54
+ const malformed = (change.kind === "create" &&
55
+ (change.before !== null || change.after === null)) ||
56
+ (change.kind === "delete" &&
57
+ (change.before === null || change.after !== null)) ||
58
+ (change.kind === "modify" &&
59
+ (change.before === null ||
60
+ change.after === null ||
61
+ change.before === change.after));
62
+ const current = result.get(change.path);
63
+ if (malformed ||
64
+ (change.after === null
65
+ ? current !== undefined
66
+ : current?.contents !== change.after || current.encoding !== undefined)) {
67
+ conflicts.add(change.path);
68
+ continue;
69
+ }
70
+ if (change.before === null)
71
+ result.delete(change.path);
72
+ else
73
+ result.set(change.path, { path: change.path, contents: change.before });
74
+ }
75
+ return {
76
+ files: [...result.values()].sort((a, b) => a.path.localeCompare(b.path)),
77
+ conflicts: [...conflicts].sort(),
78
+ };
79
+ }
@@ -1,4 +1,5 @@
1
1
  import { isPlayPresetBinding, } from "../play/FileNativePlayPresetStore.js";
2
+ import { isSafeGoogleWireRequest } from "../model/GoogleRequestPreview.js";
2
3
  export function isAuthoringPreloadRecord(value) {
3
4
  if (typeof value !== "object" || value === null || Array.isArray(value))
4
5
  return false;
@@ -25,9 +26,17 @@ export function validAuthoringRequests(value, validPrompt, session) {
25
26
  if (typeof raw !== "object" || raw === null || Array.isArray(raw))
26
27
  return false;
27
28
  const item = raw;
28
- const keys = Object.keys(item).length;
29
- if ((keys !== 4 && !(keys === 5 && "preload" in item)) ||
29
+ if (!Object.keys(item).every((key) => [
30
+ "requestId",
31
+ "modelItemStart",
32
+ "bootstrap",
33
+ "playPreset",
34
+ "preload",
35
+ "wireRequest",
36
+ ].includes(key)) ||
30
37
  (item.preload !== undefined && !isAuthoringPreloadRecord(item.preload)) ||
38
+ (item.wireRequest !== undefined &&
39
+ !isSafeGoogleWireRequest(item.wireRequest)) ||
31
40
  typeof item.requestId !== "string" ||
32
41
  !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(item.requestId) ||
33
42
  ids.has(item.requestId) ||
@@ -67,9 +76,12 @@ export function authoringRequestPreviews(session) {
67
76
  },
68
77
  ]
69
78
  : []),
70
- ...requests.map(({ requestId, bootstrap }) => ({
79
+ ...requests.map(({ requestId, bootstrap, wireRequest }) => ({
71
80
  requestId,
72
81
  compilation: structuredClone(bootstrap),
82
+ ...(wireRequest === undefined
83
+ ? {}
84
+ : { wireRequest: structuredClone(wireRequest) }),
73
85
  legacyBootstrap: false,
74
86
  })),
75
87
  ];
@@ -0,0 +1,59 @@
1
+ import { validAuthoringNativeContinuation } from "../model/FileNativeModelAdapters.js";
2
+ import { dispatchRequiresFreshContext } from "../model/DispatchFailure.js";
3
+ export function prepareAuthoringDispatch(session, request) {
4
+ const retry = session.retryDispatch;
5
+ if (retry?.state === "retryable") {
6
+ session.retryDispatch = { ...retry, state: "dispatching" };
7
+ return { ...request, requestId: retry.requestId, exchange: retry.exchange };
8
+ }
9
+ session.retryDispatch = {
10
+ requestId: request.requestId,
11
+ exchange: request.exchange,
12
+ modelItemCount: session.modelItems.length,
13
+ state: "dispatching",
14
+ };
15
+ return request;
16
+ }
17
+ export function checkpointNextAuthoringDispatch(session, state) {
18
+ const request = session.requests?.at(-1);
19
+ if (session.modelBinding.provider !== "google_interactions" &&
20
+ request !== undefined)
21
+ session.retryDispatch = {
22
+ requestId: request.requestId,
23
+ exchange: session.exchange + 1,
24
+ modelItemCount: session.modelItems.length,
25
+ state,
26
+ };
27
+ }
28
+ export function failAuthoringDispatch(session, error, phase) {
29
+ const checkpoint = session.retryDispatch;
30
+ if (checkpoint === undefined)
31
+ return;
32
+ // Acceptance clears the previous checkpoint. During settlement only a
33
+ // completed tool receipt can create the next one, before its final save.
34
+ const settledNextRequest = phase === "settling" &&
35
+ session.pendingSettlement === null &&
36
+ checkpoint.state === "dispatching" &&
37
+ checkpoint.modelItemCount === session.modelItems.length;
38
+ checkpoint.state =
39
+ session.modelBinding.provider !== "google_interactions" &&
40
+ (phase === "preparing" || phase === "dispatched" || settledNextRequest) &&
41
+ !dispatchRequiresFreshContext(error, false)
42
+ ? "retryable"
43
+ : "blocked";
44
+ }
45
+ export function recoverAuthoringDispatch(session) {
46
+ if (session.retryDispatch?.state === "dispatching")
47
+ session.retryDispatch.state =
48
+ session.modelBinding.provider === "google_interactions"
49
+ ? "blocked"
50
+ : "retryable";
51
+ }
52
+ export function canRetryAuthoringDispatch(session) {
53
+ return (session.runStatus === "interrupted" &&
54
+ session.modelBinding.provider !== "google_interactions" &&
55
+ session.googleOutcomeUnknown !== true &&
56
+ session.pendingSettlement === null &&
57
+ session.retryDispatch?.state === "retryable" &&
58
+ validAuthoringNativeContinuation(session.modelItems, session.modelBinding.provider));
59
+ }
@@ -0,0 +1,68 @@
1
+ import { validGoogleInteractionsContinuation } from "../model/ProviderExchangeState.js";
2
+ export function hasDamagedGoogleSteps(value) {
3
+ return (isRecord(value) &&
4
+ isRecord(value.modelBinding) &&
5
+ value.modelBinding.provider === "google_interactions" &&
6
+ Array.isArray(value.modelItems) &&
7
+ value.modelItems.some((item) => isRecord(item) &&
8
+ item.kind === "assistant" &&
9
+ damagedGoogleState(item.providerState)));
10
+ }
11
+ /** A disposable copy for discovery only. Callers must still validate the complete schema. */
12
+ export function withoutDamagedGoogleSteps(value) {
13
+ if (!isRecord(value) ||
14
+ !isRecord(value.modelBinding) ||
15
+ value.modelBinding.provider !== "google_interactions" ||
16
+ value.pendingSettlement !== null ||
17
+ value.googleOutcomeUnknown === true ||
18
+ value.activeRequestId !== null ||
19
+ (value.runStatus !== "ready" && value.runStatus !== "interrupted") ||
20
+ !Array.isArray(value.modelItems))
21
+ return null;
22
+ const copy = structuredClone(value);
23
+ if (!Array.isArray(copy.modelItems))
24
+ return null;
25
+ const items = copy.modelItems;
26
+ for (const [index, item] of items.entries()) {
27
+ if (isRecord(item) &&
28
+ item.kind === "assistant" &&
29
+ isRecord(item.providerState) &&
30
+ item.providerState.protocol !== "google_interactions")
31
+ return null;
32
+ if (!isRecord(item) ||
33
+ item.kind !== "assistant" ||
34
+ !Array.isArray(item.toolCalls))
35
+ continue;
36
+ for (const [offset, call] of item.toolCalls.entries()) {
37
+ const result = items[index + offset + 1];
38
+ if (!isRecord(call) ||
39
+ typeof call.id !== "string" ||
40
+ !isRecord(result) ||
41
+ result.kind !== "tool" ||
42
+ result.toolCallId !== call.id)
43
+ return null;
44
+ }
45
+ }
46
+ let damaged = false;
47
+ for (const item of items) {
48
+ if (!isRecord(item) ||
49
+ item.kind !== "assistant" ||
50
+ !damagedGoogleState(item.providerState))
51
+ continue;
52
+ if (isRecord(item.providerState))
53
+ item.providerState.steps = [];
54
+ else
55
+ item.providerState = { protocol: "google_interactions", steps: [] };
56
+ damaged = true;
57
+ }
58
+ return damaged ? copy : null;
59
+ }
60
+ function damagedGoogleState(value) {
61
+ return (value == null ||
62
+ (isRecord(value) &&
63
+ value.protocol === "google_interactions" &&
64
+ !validGoogleInteractionsContinuation(value.steps)));
65
+ }
66
+ function isRecord(value) {
67
+ return typeof value === "object" && value !== null && !Array.isArray(value);
68
+ }