narraeon 0.7.10 → 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.
Files changed (44) 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 +75 -0
  5. package/dist/node/runtime/V1Runtime.js +71 -1
  6. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
  7. package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
  8. package/dist/node/runtime/authoring/ModelToolConversation.js +47 -22
  9. package/dist/node/runtime/content/ContentWorkspace.js +3 -0
  10. package/dist/node/runtime/content/SillyTavernContent.js +796 -0
  11. package/dist/node/runtime/model/AiFailureLog.js +8 -1
  12. package/dist/node/runtime/model/FileNativeModelAdapters.js +610 -18
  13. package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
  14. package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
  15. package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
  16. package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
  17. package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
  18. package/dist/node/runtime/model/ProviderStream.js +4 -2
  19. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
  20. package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
  21. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +150 -10
  22. package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
  23. package/dist/node/runtime/play/PlayAdvanceLifecycle.js +33 -9
  24. package/dist/node/runtime/play/PlayCallChain.js +76 -4
  25. package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
  26. package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
  27. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +112 -8
  28. package/dist/node/runtime/setting/SettingImprovementSession.js +48 -7
  29. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +84 -8
  30. package/dist/node/runtime/world-revision/WorldRevisionSession.js +125 -17
  31. package/dist/node/shared/application-preset-files.js +29 -0
  32. package/dist/node/shared/default-preset-host.js +50 -38
  33. package/dist/node/shared/first-party-action-choices.js +7 -11
  34. package/dist/node/shared/first-party-generic-panels.js +7 -11
  35. package/dist/node/shared/first-party-player-view.js +7 -11
  36. package/dist/node/shared/ordered-author-prompts.js +6 -4
  37. package/dist/node/shared/ordered-play-prompts.js +4 -2
  38. package/dist/node/shared/prompt-arrangement.js +205 -0
  39. package/dist/node/shared/sillytavern-preset.js +454 -0
  40. package/dist/web/assets/{index-DH2BVsYd.css → index-BKA1D0Rz.css} +1 -1
  41. package/dist/web/assets/index-C_Qta8UL.js +1432 -0
  42. package/dist/web/index.html +2 -2
  43. package/package.json +1 -1
  44. 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 {};
@@ -89,6 +89,28 @@ const requiredFields = {
89
89
  "play.select": { presetId: "string" },
90
90
  "play.export": { presetId: "string" },
91
91
  "play.import": { name: "string", files: "array" },
92
+ "content.sillytavern.inspect": { source: "string" },
93
+ "content.sillytavern.preview": {
94
+ source: "string",
95
+ name: "string",
96
+ selection: "object",
97
+ },
98
+ "content.sillytavern.import": {
99
+ source: "string",
100
+ name: "string",
101
+ selection: "object",
102
+ },
103
+ "play.sillytavern.inspect": { source: "string" },
104
+ "play.sillytavern.preview": {
105
+ source: "string",
106
+ selected: "array",
107
+ name: "string",
108
+ },
109
+ "play.sillytavern.import": {
110
+ source: "string",
111
+ selected: "array",
112
+ name: "string",
113
+ },
92
114
  "play.workbench.read": {},
93
115
  "prompt.preview": {
94
116
  packageId: "string",
@@ -326,6 +348,53 @@ function validateRequestFields(request) {
326
348
  if (request.revision !== undefined && request.presetId === undefined)
327
349
  throw new V1ProtocolError("invalid_request", "play.workbench.read.revision must be provided with presetId");
328
350
  }
351
+ if (typeof request.type === "string" &&
352
+ request.type.startsWith("content.sillytavern.")) {
353
+ const inspect = request.type === "content.sillytavern.inspect";
354
+ const allowed = inspect
355
+ ? ["type", "source"]
356
+ : ["type", "source", "name", "selection"];
357
+ if (Object.keys(request).some((key) => !allowed.includes(key)))
358
+ throw new V1ProtocolError("invalid_request", "Invalid SillyTavern content fields");
359
+ if (!inspect) {
360
+ const selection = request.selection;
361
+ if (!isRecord(selection) ||
362
+ Object.keys(selection).some((key) => ![
363
+ "character",
364
+ "scenario",
365
+ "opening",
366
+ "example",
367
+ "systemPrompt",
368
+ "attachments",
369
+ "entries",
370
+ ].includes(key)) ||
371
+ (!Array.isArray(selection.attachments) &&
372
+ selection.attachments !== undefined) ||
373
+ (Array.isArray(selection.attachments) &&
374
+ !selection.attachments.every((value) => typeof value === "string")) ||
375
+ !isRecord(selection.entries) ||
376
+ Object.values(selection.entries).some((value) => value !== "world" && value !== "attachment") ||
377
+ ["character", "scenario", "example", "systemPrompt"].some((key) => selection[key] !== undefined && typeof selection[key] !== "boolean") ||
378
+ (selection.opening !== undefined &&
379
+ typeof selection.opening !== "string"))
380
+ throw new V1ProtocolError("invalid_request", "Invalid SillyTavern content selection");
381
+ }
382
+ }
383
+ if (typeof request.type === "string" &&
384
+ request.type.startsWith("play.sillytavern.")) {
385
+ const inspect = request.type === "play.sillytavern.inspect";
386
+ const allowed = inspect
387
+ ? ["type", "source", "groupId"]
388
+ : ["type", "source", "groupId", "selected", "name"];
389
+ if (Object.keys(request).some((key) => !allowed.includes(key)) ||
390
+ (request.groupId !== undefined &&
391
+ (typeof request.groupId !== "string" ||
392
+ request.groupId.length === 0)) ||
393
+ (!inspect &&
394
+ (!Array.isArray(request.selected) ||
395
+ !request.selected.every((value) => typeof value === "string" && value.length > 0))))
396
+ throw new V1ProtocolError("invalid_request", `${request.type} has invalid fields`);
397
+ }
329
398
  if (request.type === "play.import") {
330
399
  if (!request.files.every((file) => isRecord(file) &&
331
400
  typeof file.path === "string" &&
@@ -454,6 +523,12 @@ const requestTypes = new Set([
454
523
  "play.select",
455
524
  "play.export",
456
525
  "play.import",
526
+ "content.sillytavern.inspect",
527
+ "content.sillytavern.preview",
528
+ "content.sillytavern.import",
529
+ "play.sillytavern.inspect",
530
+ "play.sillytavern.preview",
531
+ "play.sillytavern.import",
457
532
  "play.workbench.read",
458
533
  "prompt.preview",
459
534
  "world.create",
@@ -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,7 +18,7 @@ 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";
@@ -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 {
@@ -423,6 +452,47 @@ export class V1Runtime {
423
452
  };
424
453
  case "play.import":
425
454
  return this.#playPresets.importPortable(request);
455
+ case "play.sillytavern.inspect":
456
+ case "play.sillytavern.preview":
457
+ case "play.sillytavern.import": {
458
+ // Do not trust the browser to enforce the import limit or supply
459
+ // already-converted files. Preview and import both parse from source.
460
+ if (Buffer.byteLength(request.source, "utf8") > 2 * 1024 * 1024)
461
+ throw new V1ProtocolError("invalid_request", "SillyTavern source exceeds the size limit");
462
+ if (request.type !== "play.sillytavern.inspect" &&
463
+ (request.selected.length > 4096 ||
464
+ request.selected.reduce((bytes, id) => bytes + Buffer.byteLength(id, "utf8"), 0) >
465
+ 2 * 1024 * 1024))
466
+ throw new V1ProtocolError("invalid_request", "SillyTavern selection exceeds the size limit");
467
+ try {
468
+ if (request.type === "play.sillytavern.inspect")
469
+ return inspectSillyTavernPreset(request.source, request.groupId);
470
+ const converted = convertSillyTavernPreset(request.source, {
471
+ groupId: request.groupId,
472
+ selected: request.selected,
473
+ name: request.name,
474
+ locale: this.#locale,
475
+ });
476
+ if (request.type === "play.sillytavern.preview") {
477
+ const parsed = parsePlayPresetFiles(converted.files);
478
+ if (parsed.kind === "invalid")
479
+ throw parsed.error;
480
+ toPlayPresetStructuredEditor(parsed.definition);
481
+ return converted;
482
+ }
483
+ return await this.#playPresets.importPortable({
484
+ name: request.name,
485
+ files: converted.files,
486
+ });
487
+ }
488
+ catch (error) {
489
+ if (error instanceof V1ProtocolError)
490
+ throw error;
491
+ throw new V1ProtocolError("invalid_request", error instanceof Error
492
+ ? error.message
493
+ : "Invalid SillyTavern preset");
494
+ }
495
+ }
426
496
  case "play.workbench.read": {
427
497
  const binding = request.presetId === undefined
428
498
  ? await this.#playPresets.bindCurrent()
@@ -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,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
+ }
@@ -1,8 +1,28 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { aggregateModelUsage, } from "../../protocol/modelUsage.js";
3
3
  import { ModelHostCancelledError } from "../model/ModelHost.js";
4
+ import { withGoogleHop } from "../model/GoogleRequestPreview.js";
4
5
  const maximumExchangesPerMessage = 64;
5
6
  const maximumStreamTailCharacters = 240;
7
+ export function authoringExchange(session, requestId, boundaries) {
8
+ const bootstrap = session.requests?.at(-1)?.bootstrap ?? session.bootstrap;
9
+ return {
10
+ bootstrap,
11
+ toolUniverse: bootstrap.toolUniverse,
12
+ allowedTools: bootstrap.toolUniverse.map(({ name }) => name),
13
+ toolStrategy: bootstrap.toolStrategy,
14
+ tools: bootstrap.tools,
15
+ appended: session.modelItems,
16
+ ...(boundaries === undefined
17
+ ? {}
18
+ : { worldRevisionBoundaries: boundaries }),
19
+ requestId,
20
+ operationId: session.sessionId,
21
+ requestAttempt: 1,
22
+ exchange: session.exchange + 1,
23
+ maxOutputTokens: session.modelBinding.maxOutputTokens,
24
+ };
25
+ }
6
26
  /**
7
27
  * Deep Module for the shared authoring-conversation loop. Its Interface leaves
8
28
  * content-package and world-revision publication behind the settlement seam.
@@ -56,28 +76,25 @@ export class ModelToolConversation {
56
76
  }
57
77
  async #execute(input, live) {
58
78
  const { session } = input;
59
- const bootstrap = session.requests?.at(-1)?.bootstrap ?? session.bootstrap;
60
79
  try {
61
80
  for (let round = 0; round < maximumExchangesPerMessage; round += 1) {
81
+ const request = authoringExchange(session, live.requestId, input.worldRevisionBoundaries);
62
82
  session.exchange += 1;
83
+ if (session.modelBinding.provider === "google_interactions")
84
+ session.googleOutcomeUnknown = true;
63
85
  live.streaming = emptyStreaming();
64
86
  session.updatedAt = Date.now();
65
87
  await input.save(session);
66
88
  input.changed?.(true);
67
- const response = await input.host.exchange({
68
- bootstrap,
69
- toolUniverse: bootstrap.toolUniverse,
70
- allowedTools: bootstrap.toolUniverse.map(({ name }) => name),
71
- toolStrategy: bootstrap.toolStrategy,
72
- tools: bootstrap.tools,
73
- appended: session.modelItems,
74
- requestId: live.requestId,
75
- operationId: session.sessionId,
76
- requestAttempt: 1,
77
- exchange: session.exchange,
78
- maxOutputTokens: session.modelBinding.maxOutputTokens,
79
- }, {
89
+ const response = await input.host.exchange(request, {
80
90
  signal: live.controller.signal,
91
+ onGoogleHop: (hop) => {
92
+ const snapshot = session.requests?.findLast((entry) => entry.requestId === live.requestId);
93
+ if (snapshot?.wireRequest === undefined)
94
+ return;
95
+ snapshot.wireRequest = withGoogleHop(snapshot.wireRequest, hop);
96
+ return input.recordGoogleHop?.(live.requestId, hop);
97
+ },
81
98
  onDelta: (delta) => {
82
99
  recordDelta(live, delta);
83
100
  input.changed?.(false);
@@ -100,6 +117,8 @@ export class ModelToolConversation {
100
117
  providerState: response.providerState,
101
118
  toolCalls: structuredClone(toolCalls),
102
119
  });
120
+ if (session.modelBinding.provider === "google_interactions")
121
+ session.googleOutcomeUnknown = false;
103
122
  session.updatedAt = Date.now();
104
123
  if (toolCalls.length === 0) {
105
124
  session.messages.push({
@@ -134,13 +153,15 @@ export class ModelToolConversation {
134
153
  session.activeRequestId = null;
135
154
  session.completedRequestIds = rememberRequest(session.completedRequestIds, live.requestId);
136
155
  session.lastFailure =
137
- error instanceof ModelHostCancelledError
138
- ? (input.failure?.cancelled ??
139
- "The authoring response was cancelled. Complete tool responses already settled were retained.")
140
- : error instanceof Error
141
- ? error.message
142
- : (input.failure?.interrupted ??
143
- "The authoring response was interrupted");
156
+ session.googleOutcomeUnknown === true
157
+ ? "The Google request outcome is unknown; start a fresh model context before sending again."
158
+ : error instanceof ModelHostCancelledError
159
+ ? (input.failure?.cancelled ??
160
+ "The authoring response was cancelled. Complete tool responses already settled were retained.")
161
+ : error instanceof Error
162
+ ? error.message
163
+ : (input.failure?.interrupted ??
164
+ "The authoring response was interrupted");
144
165
  session.updatedAt = Date.now();
145
166
  await input.save(session);
146
167
  return input.view(session);
@@ -170,7 +191,11 @@ export function appendAuthoringUserMessage(session, message, requestId) {
170
191
  export function resumePausedAuthoringConversation(session) {
171
192
  if (session.runStatus !== "paused")
172
193
  throw new Error("Only a paused authoring conversation can be continued");
173
- if (session.modelItems.at(-1)?.kind !== "tool")
194
+ const last = session.modelItems.at(-1);
195
+ if (last?.kind !== "tool" &&
196
+ !(session.modelBinding.provider === "google_interactions" &&
197
+ session.googleOutcomeUnknown === false &&
198
+ last?.kind === "user"))
174
199
  throw new Error("The paused authoring conversation does not end with settled tool results");
175
200
  session.runStatus = "running";
176
201
  session.activeRequestId = session.requests?.at(-1)?.requestId ?? null;
@@ -96,6 +96,9 @@ export class ContentWorkspace {
96
96
  }, options.signal === undefined ? {} : { signal: options.signal });
97
97
  });
98
98
  }
99
+ importSillyTavernContent(title, files) {
100
+ return this.#mutate(() => this.#currentTreeLibrary.importPackage({ title, files }));
101
+ }
99
102
  copyCurrentTreeContentPackage(localId, options = {}) {
100
103
  return this.#mutate(() => this.#currentTreeLibrary.copyPackage(localId, options.signal === undefined ? {} : { signal: options.signal }));
101
104
  }