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
@@ -1,4 +1,5 @@
1
- import { ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, modelHostFailureRequiresFreshContext, } from "../model/ModelHost.js";
1
+ import { dispatchRequiresFreshContext } from "../model/DispatchFailure.js";
2
+ import { ModelHostCancelledError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "../model/ModelHost.js";
2
3
  import { classifyPlayFailure } from "./PlayFailure.js";
3
4
  const messages = {
4
5
  playerCommitted: "The player message is committed; the frozen model request is ready to continue.",
@@ -10,6 +11,7 @@ const messages = {
10
11
  serviceStoppedAfterHead: "The service stopped after the world was updated; use a fresh context.",
11
12
  serviceStoppedSavingPlayer: "The service stopped while saving the player message; use a fresh context.",
12
13
  serviceStoppedRetryable: "The service stopped while the model request was in progress; retry to call the model again.",
14
+ serviceStoppedUnknown: "The service stopped after the model request may have been dispatched; its outcome is unknown. Use a fresh context.",
13
15
  serviceStoppedNoRequest: "The service stopped before preparing a model request; use a fresh context.",
14
16
  dispatchInterrupted: "The model request was interrupted.",
15
17
  };
@@ -43,29 +45,52 @@ export class PlayAdvanceLifecycle {
43
45
  notify?.();
44
46
  }
45
47
  /**
46
- * Ends an advance that a process exit cut off (ADR-0044). Nothing is
47
- * replayed: the exchange is interrupted, and only the chain's own frozen
48
- * request can be retried, and only while the world has not moved on.
48
+ * Recovers an advance cut off by a process exit. A persisted ending wins;
49
+ * otherwise Google requests that may have been dispatched cannot be replayed
50
+ * (ADR-0035), while other recovery follows ADR-0044.
49
51
  */
50
52
  async recoverAbandoned(session, advance, headMoved) {
53
+ // Endings are persisted before their marker is cleared. If the process
54
+ // stopped in that gap, keep the durable ending rather than inventing a
55
+ // new retry decision for an advance that already settled.
56
+ if (advance !== null &&
57
+ session.status !== "running" &&
58
+ !(advance.advanceKind === "player" && session.status === "ready") &&
59
+ !headMoved) {
60
+ await this.#ports.clearMarker(advance);
61
+ return;
62
+ }
51
63
  const playerPhase = advance?.advanceKind === "player";
52
64
  const streaming = playerPhase
53
65
  ? undefined
54
66
  : session.events.findLast((event) => event.kind === "assistant" &&
55
67
  event.status === "streaming" &&
56
68
  (advance === null || event.id === advance.eventId));
57
- const canRetry = !headMoved && !playerPhase && session.lastRequest !== null;
69
+ const unknownGoogleDispatch = advance?.advanceKind === "response" &&
70
+ session.status === "running" &&
71
+ (session.lastRequest?.modelConfig?.provider ??
72
+ session.modelBinding?.provider) === "google_interactions";
73
+ const canRetry = !headMoved &&
74
+ !playerPhase &&
75
+ !unknownGoogleDispatch &&
76
+ session.lastRequest !== null;
58
77
  const message = headMoved
59
78
  ? messages.serviceStoppedAfterHead
60
79
  : playerPhase
61
80
  ? messages.serviceStoppedSavingPlayer
62
- : canRetry
63
- ? messages.serviceStoppedRetryable
64
- : messages.serviceStoppedNoRequest;
81
+ : unknownGoogleDispatch
82
+ ? messages.serviceStoppedUnknown
83
+ : canRetry
84
+ ? messages.serviceStoppedRetryable
85
+ : messages.serviceStoppedNoRequest;
65
86
  await this.end(session, {
66
87
  kind: "failed",
67
88
  message,
68
- failureKind: canRetry ? "retryable" : "other",
89
+ failureKind: unknownGoogleDispatch
90
+ ? "outcome_unknown"
91
+ : canRetry
92
+ ? "retryable"
93
+ : "other",
69
94
  canRetry,
70
95
  streamingEventId: streaming?.id,
71
96
  }, advance);
@@ -73,19 +98,21 @@ export class PlayAdvanceLifecycle {
73
98
  }
74
99
  /**
75
100
  * The ending of an exchange that raised an error before its response was
76
- * settled. Cancellation after dispatch is named explicitly here and is never
77
- * a retry, even though `ModelHostCancelledError` is also an outcome-unknown
78
- * error (ADR-0030).
101
+ * settled. Google dispatched outcomes remain unknown and cannot be replayed.
79
102
  */
80
- export function dispatchFailureEnding(error, streamingEventId) {
103
+ export function dispatchFailureEnding(error, streamingEventId, googleDispatched = false) {
81
104
  const message = errorMessage(error, messages.dispatchInterrupted);
82
105
  if (error instanceof ModelHostCancelledError)
83
- return { kind: "cancelled_after_dispatch", message, streamingEventId };
84
- const requiresFreshContext = error instanceof ModelHostBindingMismatchError ||
85
- error instanceof ModelHostContinuationError ||
86
- error instanceof ModelHostOutcomeUnknownError ||
87
- modelHostFailureRequiresFreshContext(error);
88
- const canRetry = !requiresFreshContext && error instanceof ModelHostFailureError;
106
+ return {
107
+ kind: "cancelled_after_dispatch",
108
+ message,
109
+ canRetry: !googleDispatched,
110
+ streamingEventId,
111
+ };
112
+ const requiresFreshContext = dispatchRequiresFreshContext(error, googleDispatched);
113
+ const canRetry = !requiresFreshContext &&
114
+ (error instanceof ModelHostFailureError ||
115
+ error instanceof ModelHostOutcomeUnknownError);
89
116
  return {
90
117
  kind: "failed",
91
118
  message,
@@ -117,12 +144,13 @@ export function noContinuationEnding() {
117
144
  };
118
145
  }
119
146
  /** A settled response with neither text nor a complete tool call. */
120
- export function emptyResponseEnding() {
147
+ export function emptyResponseEnding(canRetry = false, streamingEventId) {
121
148
  return {
122
149
  kind: "failed",
123
150
  message: messages.noOutput,
124
151
  failureKind: "other",
125
- canRetry: false,
152
+ canRetry,
153
+ streamingEventId,
126
154
  };
127
155
  }
128
156
  /**
@@ -175,7 +203,7 @@ function applyEnding(session, ending) {
175
203
  case "cancelled_after_dispatch":
176
204
  interruptStreamingEvent(session, ending.streamingEventId);
177
205
  session.status = "interrupted";
178
- session.canRetry = false;
206
+ session.canRetry = ending.canRetry;
179
207
  session.lastFailure = ending.message;
180
208
  delete session.lastFailureKind;
181
209
  session.events.push({
@@ -203,7 +231,7 @@ function interruptStreamingEvent(session, eventId) {
203
231
  if (eventId === undefined)
204
232
  return;
205
233
  const event = session.events.find(({ id }) => id === eventId);
206
- if (event?.kind === "assistant")
234
+ if (event?.kind === "assistant" && event.status === "streaming")
207
235
  event.status = "interrupted";
208
236
  }
209
237
  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,14 +1268,27 @@ 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 ?? [];
1257
- const text = durable.text ?? existing.text;
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
+ }
1284
+ const text = durable.text ?? "";
1258
1285
  const hasText = text.trim().length > 0;
1259
1286
  const responseKind = calls.length > 0 ? "tool_step" : hasText ? "narrative" : "empty";
1260
1287
  const narrative = responseKind === "narrative";
1261
1288
  const assistantEvent = {
1262
1289
  id: advance.eventId,
1263
1290
  kind: "assistant",
1264
- text,
1291
+ text: responseKind === "empty" ? existing.text : text,
1265
1292
  status: "completed",
1266
1293
  responseKind,
1267
1294
  exchange: advance.exchange,
@@ -1279,6 +1306,15 @@ export class PlayCallChain {
1279
1306
  : { stopReason: durable.stopReason }),
1280
1307
  continuation: durable.providerState === undefined ? "unavailable" : "available",
1281
1308
  };
1309
+ // No response has been settled or added to the transcript. Non-Google
1310
+ // providers may explicitly replay the frozen request without duplicating work.
1311
+ if (responseKind === "empty" &&
1312
+ (session.lastRequest?.modelConfig?.provider ??
1313
+ session.modelBinding?.provider) !== "google_interactions") {
1314
+ session.events = session.events.map((event) => event.id === advance.eventId ? assistantEvent : event);
1315
+ await this.#advances.end(session, emptyResponseEnding(true, advance.eventId), advance);
1316
+ return { kind: "terminal", status: "interrupted", narrative: false };
1317
+ }
1282
1318
  const workingTools = new Map(session.completedToolMap);
1283
1319
  const prepared = [];
1284
1320
  const trailingEvents = [];
@@ -1561,6 +1597,8 @@ export class PlayCallChain {
1561
1597
  }
1562
1598
  response = await modelHost.exchange(request, {
1563
1599
  signal,
1600
+ diagnosticAttempt: attempt,
1601
+ onGoogleHop: (hop) => this.#worlds.playTimeline.recordGoogleHop(session.worldId, session.chainId, session.promptRuns?.at(-1)?.exchange ?? 1, hop),
1564
1602
  onDelta: (delta) => {
1565
1603
  this.#observeInvocation(session.chainId, delta.kind, {
1566
1604
  ...(delta.kind === "reasoning"
@@ -1586,7 +1624,7 @@ export class PlayCallChain {
1586
1624
  },
1587
1625
  });
1588
1626
  if (signal.aborted)
1589
- throw new ModelHostCancelledError("The player cancelled the dispatched model request; its response was not committed and cannot be replayed.");
1627
+ throw new ModelHostCancelledError("The player cancelled the dispatched model request; its response was not committed.");
1590
1628
  this.#setInvocationAbortable(session.chainId, false);
1591
1629
  if ((response.toolCalls?.length ?? 0) > 0)
1592
1630
  this.#observeInvocation(session.chainId, "tool", {
@@ -1596,7 +1634,10 @@ export class PlayCallChain {
1596
1634
  await this.#failureLog?.recordExchangeIfActive(response.diagnostics);
1597
1635
  }
1598
1636
  catch (error) {
1599
- await this.#advances.end(session, dispatchFailureEnding(error, event.id), advance, () => this.#notifySnapshot(observer, session));
1637
+ await this.#advances.end(session, dispatchFailureEnding(signal.aborted && !(error instanceof ModelHostCancelledError)
1638
+ ? new ModelHostCancelledError("The player cancelled the dispatched model request; its response was not committed.")
1639
+ : error, event.id, (request.modelConfig?.provider ??
1640
+ session.modelBinding?.provider) === "google_interactions"), advance, () => this.#notifySnapshot(observer, session));
1600
1641
  return projectView(session);
1601
1642
  }
1602
1643
  try {
@@ -1661,7 +1702,7 @@ export class PlayCallChain {
1661
1702
  },
1662
1703
  ],
1663
1704
  });
1664
- await this.#advances.end(session, ending, advance, () => this.#notifySnapshot(observer, session));
1705
+ await this.#advances.end(session, { ...ending, streamingEventId: event.id }, advance, () => this.#notifySnapshot(observer, session));
1665
1706
  return projectView(session);
1666
1707
  }
1667
1708
  }
@@ -1691,6 +1732,13 @@ export class PlayCallChain {
1691
1732
  event.committedHead === session.parentHead);
1692
1733
  if (reply === undefined)
1693
1734
  throw new PlayCallChainError("Follow-up reply is not committed");
1735
+ const run = session.promptRuns?.at(-1);
1736
+ const runReplies = session.events.filter((event) => event.kind === "assistant" &&
1737
+ event.responseKind === "narrative" &&
1738
+ event.committedHead !== undefined &&
1739
+ event.id >= (run?.firstEventId ?? 1));
1740
+ // Without a new prompt run there is no distinct bounded encoding slot.
1741
+ const previewExchange = runReplies.length === 1 ? (run?.exchange ?? 1) : undefined;
1694
1742
  const observeFollowupDelta = (delta) => {
1695
1743
  this.#observeInvocation(session.chainId, "followup", {
1696
1744
  ...(delta.kind === "reasoning"
@@ -1715,6 +1763,14 @@ export class PlayCallChain {
1715
1763
  ? {}
1716
1764
  : { reasoning: outcome.reasoning }),
1717
1765
  ...(outcome.usage === undefined ? {} : { usage: outcome.usage }),
1766
+ ...(outcome.requestPreviewRecorded !== true ||
1767
+ outcome.requestPreviewStatus === undefined ||
1768
+ previewExchange === undefined
1769
+ ? {}
1770
+ : {
1771
+ requestPreviewExchange: previewExchange,
1772
+ requestPreviewStatus: outcome.requestPreviewStatus,
1773
+ }),
1718
1774
  toolCalls: structuredClone(outcome.toolCalls),
1719
1775
  ...(outcome.failure === undefined ? {} : { failure: outcome.failure }),
1720
1776
  });
@@ -1757,6 +1813,21 @@ export class PlayCallChain {
1757
1813
  ? {}
1758
1814
  : { failureLog: this.#failureLog }),
1759
1815
  observer: {
1816
+ onGoogleHop: async (id, hop) => {
1817
+ if (previewExchange !== undefined)
1818
+ await this.#worlds.playTimeline.recordGoogleHop(session.worldId, session.chainId, previewExchange, hop, id);
1819
+ },
1820
+ onRequest: async (id, request) => {
1821
+ if (previewExchange === undefined)
1822
+ return false;
1823
+ // Only Google currently has a continuation-redacting encoder.
1824
+ const preview = modelHost.binding().provider === "google_interactions"
1825
+ ? modelHost.previewAuthoringRequest?.(request)
1826
+ : undefined;
1827
+ if (preview === undefined)
1828
+ return false;
1829
+ return this.#worlds.playTimeline.recordFollowupEncoding(session.worldId, session.chainId, previewExchange, id, preview);
1830
+ },
1760
1831
  onProviderDispatch: () => {
1761
1832
  this.#setInvocationAbortable(session.chainId, true);
1762
1833
  this.#observeInvocation(session.chainId, "followup", {
@@ -1837,8 +1908,9 @@ export class PlayCallChain {
1837
1908
  }
1838
1909
  /**
1839
1910
  * An in-progress advance, or a session still marked running, means a model
1840
- * dispatch or settlement was cut off by a process exit. Nothing is replayed:
1841
- * the exchange is interrupted and the player retries (ADR-0044).
1911
+ * dispatch or settlement was cut off by a process exit. Recovery preserves
1912
+ * persisted endings and applies the protocol's retry boundary (ADR-0035,
1913
+ * ADR-0044).
1842
1914
  */
1843
1915
  async #reconcileSessionAdvance(session, loaded) {
1844
1916
  const advance = loaded === undefined
@@ -2364,6 +2436,9 @@ function assertSessionModelHostBinding(session, modelHost) {
2364
2436
  throw new PlayCallChainError("This model context predates durable provider continuation bindings; start a fresh context.");
2365
2437
  if (session.modelBinding.provider !== modelHost.binding().provider)
2366
2438
  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");
2439
+ if (session.modelBinding.provider === "google_interactions" &&
2440
+ !equalModelHostBinding(session.modelBinding, modelHost.binding()))
2441
+ throw new PlayCallChainError("The selected model connection does not match the frozen Google binding; start a fresh context.");
2367
2442
  }
2368
2443
  function validateIdentity(value, label) {
2369
2444
  if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u.test(value))
@@ -2398,6 +2473,15 @@ function turnNoteField(turnNote) {
2398
2473
  function touch(session) {
2399
2474
  session.updatedAt = Date.now();
2400
2475
  }
2476
+ /** Failed diagnostic copies cannot attach unrelated encodings to retained events. */
2477
+ function forgetFollowupPreviews(session) {
2478
+ for (const event of session.events) {
2479
+ if (event.kind !== "followup")
2480
+ continue;
2481
+ delete event.requestPreviewExchange;
2482
+ delete event.requestPreviewStatus;
2483
+ }
2484
+ }
2401
2485
  function notifyAssistantDelta(observer, delta) {
2402
2486
  try {
2403
2487
  observer?.onAssistantDelta?.(structuredClone(delta));
@@ -49,6 +49,8 @@ export async function runPlayFollowupRequests(input) {
49
49
  }
50
50
  if (signalWasAborted(input.signal)) {
51
51
  failure ??= "The follow-up request was cancelled.";
52
+ outcomes.push(outcome);
53
+ input.observer?.onOutcome?.(outcome);
52
54
  break;
53
55
  }
54
56
  outcomes.push(outcome);
@@ -91,10 +93,9 @@ async function runOne(input, followup, prefix, requestSignal) {
91
93
  try {
92
94
  await input.artifacts.beginRequestAttempt(requestContext);
93
95
  requestSignal?.throwIfAborted();
94
- input.observer?.onProviderDispatch?.();
95
96
  let response;
96
97
  try {
97
- response = await input.modelHost.exchange({
98
+ const request = {
98
99
  bootstrap: structuredClone(input.bootstrap),
99
100
  tools: structuredClone(followup.tools),
100
101
  toolUniverse: structuredClone(followup.tools),
@@ -114,8 +115,23 @@ async function runOne(input, followup, prefix, requestSignal) {
114
115
  requestAttempt: 1,
115
116
  exchange: 1,
116
117
  maxOutputTokens: input.maxOutputTokens,
117
- }, {
118
+ };
119
+ try {
120
+ outcome.requestPreviewRecorded =
121
+ (await input.observer?.onRequest?.(followup.id, request)) ?? false;
122
+ }
123
+ catch {
124
+ // Diagnostic recording must not change the provider request.
125
+ }
126
+ requestSignal?.throwIfAborted();
127
+ outcome.requestPreviewStatus = "attempted";
128
+ input.observer?.onProviderDispatch?.();
129
+ response = await input.modelHost.exchange(request, {
118
130
  ...(requestSignal === undefined ? {} : { signal: requestSignal }),
131
+ onGoogleHop: (evidence) => input.observer?.onGoogleHop?.(followup.id, evidence),
132
+ ...(input.controls !== undefined
133
+ ? { retainLateExtensionResponse: true }
134
+ : {}),
119
135
  onDelta: (delta) => input.observer?.onProviderDelta?.(delta),
120
136
  });
121
137
  if (input.signal?.aborted === true)
@@ -124,6 +140,7 @@ async function runOne(input, followup, prefix, requestSignal) {
124
140
  finally {
125
141
  input.observer?.onProviderSettled?.();
126
142
  }
143
+ outcome.requestPreviewStatus = "completed";
127
144
  responseDiagnostics = response.diagnostics;
128
145
  if (response.diagnostics !== undefined)
129
146
  await input.failureLog?.recordExchangeIfActive(response.diagnostics);
@@ -182,6 +199,9 @@ async function runOne(input, followup, prefix, requestSignal) {
182
199
  });
183
200
  }
184
201
  catch (error) {
202
+ if (outcome.requestPreviewStatus !== undefined &&
203
+ error instanceof ModelHostCancelledError)
204
+ outcome.requestPreviewStatus = "cancelled";
185
205
  outcome.failure =
186
206
  error instanceof Error
187
207
  ? error.message
@@ -315,7 +315,8 @@ function validPromptCache(value) {
315
315
  export function validProviderKind(value) {
316
316
  return (value === "chat_completions" ||
317
317
  value === "openai_responses" ||
318
- value === "anthropic_messages");
318
+ value === "anthropic_messages" ||
319
+ value === "google_interactions");
319
320
  }
320
321
  export function validCacheStrategy(value) {
321
322
  return (value === "explicit_anthropic_blocks" ||