@tangle-network/agent-app 0.43.68 → 0.43.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chat-routes/index.d.ts +212 -7
- package/dist/chat-routes/index.js +220 -14
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chunk-KFTRTOT2.js +111 -0
- package/dist/chunk-KFTRTOT2.js.map +1 -0
- package/dist/failover-H0x12kY3.d.ts +100 -0
- package/dist/model-resolution/index.d.ts +2 -99
- package/dist/model-resolution/index.js +8 -101
- package/dist/model-resolution/index.js.map +1 -1
- package/dist/turn-stream/index.d.ts +185 -24
- package/dist/turn-stream/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The substrate packages — `@tangle-network/agent-runtime`, `agent-eval`, `agent
|
|
|
16
16
|
- **Bounded tool loop** — `runAppToolLoop` / `streamAppToolLoop`: stream a turn → collect tool calls → dispatch → fold results back → re-run, capped. These are 1:1 aliases of `@tangle-network/agent-runtime`'s `runToolLoop` / `streamToolLoop` (the engine owns the loop; this package adds no logic on that path). Substrate-free behind a `streamTurn` seam, so it drives a sandboxed agent, a Worker, or an in-browser copilot unchanged.
|
|
17
17
|
- **Assembled chat vertical** — `createChatTurnRoutes` wires auth → thread/message store → streaming turn with buffered replay → uploads → sidecar question answering into one route factory, over `authorize` / `produce` / `store` / `interactions` seams. No hand-rolled orchestration. See [`examples/chat-app.md`](./examples/chat-app.md).
|
|
18
18
|
- **Sandbox-optional** — the same tools, billing, eval, and loop work without a container. A `fetch`-only adapter maps any OpenAI-compatible stream (Tangle Router, tcloud) into the loop. See [`examples/browser-copilot.md`](./examples/browser-copilot.md).
|
|
19
|
-
- **Resumable turns
|
|
19
|
+
- **Resumable turns** — buffer a turn so a dropped tab loses nothing and a reconnecting client replays the tail. Two niches: a browser/edge copilot streaming the Router directly (no sandbox session to attach to), and any DETACHED sandbox run a browser must tail (`dispatchPrompt({ detach: true })` / `driveTurn` execute on a lane the session gateway never sees). **An INTERACTIVE sandbox turn doesn't need it** — drive it on the message lane (`box.session(id).sendMessage()`) and attach the tab with `box.mintScopedToken()` + `SessionGatewayClient`, or let the worker resume with `box.streamPrompt('', { executionId, lastEventId })`. See [`examples/resumable-turns.md`](./examples/resumable-turns.md).
|
|
20
20
|
- **Composes the engine, never forks it** — `/eval` re-exports `@tangle-network/agent-eval`'s verifier; `/integrations` wraps the hub; `/tangle` and `/billing` take the tcloud client as a structural contract. Engines are **peer dependencies** — you pin the version, nothing is bundled.
|
|
21
21
|
- **ESM, typed, zero runtime deps** in the substrate-free modules (`/runtime`, `/web`, `/crypto`, `/redact`, `/stream`). Ships with `.d.ts` and npm [provenance](https://www.npmjs.com/package/@tangle-network/agent-app#provenance).
|
|
22
22
|
|
|
@@ -5,6 +5,7 @@ import { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtim
|
|
|
5
5
|
import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
|
|
6
6
|
import { PersistedChatMessageForTurn } from '../stream/index.js';
|
|
7
7
|
import { d as TurnEventStore } from '../turn-buffer-DGnAPKwa.js';
|
|
8
|
+
import { M as ModelFailoverAttempt } from '../failover-H0x12kY3.js';
|
|
8
9
|
export { D as DEFAULT_STALE_TURN_LOCK_GRACE_MS, a as DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, R as ReconcileStaleTurnLockOptions, b as ReconcileStaleTurnLockResult, S as StaleTurnLockSandboxProbeResult, c as StaleTurnLockSessionProbeResult, r as reconcileStaleTurnLock } from '../stale-turn-lock-DucQzvXu.js';
|
|
9
10
|
import { J as JsonRecord } from '../stream-normalizer-QYUl4vnl.js';
|
|
10
11
|
import { SandboxExecChannel, PromptInputPart } from '../sandbox/index.js';
|
|
@@ -298,7 +299,22 @@ interface ChatTurnRouteProducer extends ChatTurnProducer {
|
|
|
298
299
|
* parts until the turn completes. */
|
|
299
300
|
draftParts?(): Array<Record<string, unknown>>;
|
|
300
301
|
usage?(): ChatTurnUsage;
|
|
302
|
+
/** The model that SERVED the turn. With failover wired this is the model that
|
|
303
|
+
* actually answered, which is not necessarily the one the caller preferred —
|
|
304
|
+
* read it after the stream drains, never before. */
|
|
301
305
|
model?: string;
|
|
306
|
+
/** Model-failover attribution, when the producer supports it. Reported onto
|
|
307
|
+
* the usage/billing receipt so a downgrade is never silent. */
|
|
308
|
+
modelFailover?(): ChatTurnModelFailover;
|
|
309
|
+
}
|
|
310
|
+
/** Which model served, and what it took to get there. */
|
|
311
|
+
interface ChatTurnModelFailover {
|
|
312
|
+
/** The model that served the turn. */
|
|
313
|
+
model?: string;
|
|
314
|
+
/** Every model tried, in order, with the reason each was abandoned. */
|
|
315
|
+
attempts: ModelFailoverAttempt[];
|
|
316
|
+
/** True when the preferred model did not serve. */
|
|
317
|
+
usedFallback: boolean;
|
|
302
318
|
}
|
|
303
319
|
/** Resolve authorization status and context for a chat turn including tenant and user identification */
|
|
304
320
|
type ChatTurnAuthorization<TContext> = {
|
|
@@ -410,6 +426,13 @@ interface ChatTurnLifecycleComplete<TContext> extends ChatTurnLifecycleBase<TCon
|
|
|
410
426
|
finalText: string;
|
|
411
427
|
usage: ChatTurnUsage;
|
|
412
428
|
durationMs: number;
|
|
429
|
+
/** The model that SERVED the turn — the fallback's id when failover moved it.
|
|
430
|
+
* `usage` is that model's, so telemetry that splits cost or quality by model
|
|
431
|
+
* must key on this and not on the requested one. */
|
|
432
|
+
model?: string;
|
|
433
|
+
/** Attribution for a downgrade: which models were tried and why each failed.
|
|
434
|
+
* `undefined` when the producer reports no failover support. */
|
|
435
|
+
modelFailover?: ChatTurnModelFailover;
|
|
413
436
|
}
|
|
414
437
|
/** Represent an error occurring during a chat turn lifecycle with context and duration information */
|
|
415
438
|
interface ChatTurnLifecycleError<TContext> extends ChatTurnLifecycleBase<TContext> {
|
|
@@ -516,6 +539,13 @@ interface CreateChatTurnRoutesOptions<TContext = void> {
|
|
|
516
539
|
context: TContext;
|
|
517
540
|
failed: boolean;
|
|
518
541
|
failureReason?: string;
|
|
542
|
+
/** The model that SERVED this turn. With failover wired it may differ from
|
|
543
|
+
* the requested one, so a product that bills or scores per model MUST read
|
|
544
|
+
* it here rather than assuming the model it asked for. */
|
|
545
|
+
model?: string;
|
|
546
|
+
/** Present when the producer supports failover: the full attempt trail, and
|
|
547
|
+
* `usedFallback` — the flag that makes a silent downgrade impossible. */
|
|
548
|
+
modelFailover?: ChatTurnModelFailover;
|
|
519
549
|
}): Promise<void>;
|
|
520
550
|
/** Per-event side channel (product broadcast). The turn-buffer tap is
|
|
521
551
|
* already wired; this runs in addition. */
|
|
@@ -566,6 +596,115 @@ interface ChatTurnRoutes {
|
|
|
566
596
|
/** Build chat turn routes to handle and validate incoming chat requests with optional logging */
|
|
567
597
|
declare function createChatTurnRoutes<TContext = void>(options: CreateChatTurnRoutesOptions<TContext>): ChatTurnRoutes;
|
|
568
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Model failover for a STREAMING turn.
|
|
601
|
+
*
|
|
602
|
+
* `/model-resolution`'s `runWithModelFailover` already owns the policy: walk a
|
|
603
|
+
* chain, classify a resolved-or-thrown signal, re-throw a non-outage failure
|
|
604
|
+
* immediately, and carry the attempt trail. This module does NOT re-implement
|
|
605
|
+
* any of that — it composes it. What it adds is the one thing a whole-call
|
|
606
|
+
* primitive cannot express, because a stream fails PARTWAY:
|
|
607
|
+
*
|
|
608
|
+
* **A turn may only fail over before its first client-visible byte.**
|
|
609
|
+
*
|
|
610
|
+
* Once a text delta, tool call, or ask has reached the browser (and the
|
|
611
|
+
* persisted transcript), restarting on another model would duplicate the
|
|
612
|
+
* answer. So each attempt is probed: open the stream, pull events into a small
|
|
613
|
+
* buffer, and decide at the first meaningful event whether this model is
|
|
614
|
+
* serving. Committing replays the buffer and hands the live iterator through;
|
|
615
|
+
* abandoning discards the buffer (those events describe the dead model's
|
|
616
|
+
* session — including its `step-finish` usage, which must never be billed) and
|
|
617
|
+
* lets `runWithModelFailover` walk to the next model.
|
|
618
|
+
*
|
|
619
|
+
* The classification itself is `isUpstreamUnavailable` verbatim, so this path
|
|
620
|
+
* inherits the measured facts from the 2026-07-25 outage — above all that an
|
|
621
|
+
* outage is NOT always a thrown error: the sandbox RESOLVES a terminal `error`
|
|
622
|
+
* event carrying `{ errorCode: 'provider_inference_unavailable' }`, which a
|
|
623
|
+
* classifier inspecting only `catch` misses entirely. That resolved shape is
|
|
624
|
+
* the whole reason the breakage went unnoticed, so it is classified here first.
|
|
625
|
+
*
|
|
626
|
+
* Conservative by construction:
|
|
627
|
+
* - A terminal failure that is NOT an outage (400, bad schema, content filter)
|
|
628
|
+
* COMMITS rather than failing over — it surfaces to the user exactly as it
|
|
629
|
+
* does today. Those fail identically on every model; walking the chain would
|
|
630
|
+
* only multiply latency and spend to reach the same error.
|
|
631
|
+
* - A clean stream that simply produced nothing is NOT retried. An empty answer
|
|
632
|
+
* is not evidence of a dead upstream, and a silent re-roll on another model is
|
|
633
|
+
* precisely the unattributable downgrade this work exists to prevent.
|
|
634
|
+
* - A chain of length 1 costs nothing: one attempt, no extra call, no added
|
|
635
|
+
* latency, byte-identical to no failover at all.
|
|
636
|
+
*/
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* True when `event` puts content in front of the user (or in the persisted
|
|
640
|
+
* transcript), making a restart on another model unsafe.
|
|
641
|
+
*
|
|
642
|
+
* Deliberately an allow-list of KNOWN-INERT types rather than a deny-list: an
|
|
643
|
+
* unrecognized event commits. Getting this wrong in the safe direction costs a
|
|
644
|
+
* missed failover; getting it wrong the other way duplicates a user's answer.
|
|
645
|
+
*/
|
|
646
|
+
declare function isCommittingSandboxEvent(event: unknown): boolean;
|
|
647
|
+
/** A terminal failure event, classified. `outage` decides failover vs surface. */
|
|
648
|
+
interface TerminalFailure {
|
|
649
|
+
outage: boolean;
|
|
650
|
+
reason: string;
|
|
651
|
+
code?: string;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Classify a terminal failure event. Returns `null` for any non-terminal event.
|
|
655
|
+
*
|
|
656
|
+
* The RESOLVED shape is checked first and deliberately: the sandbox reports an
|
|
657
|
+
* upstream outage by resolving `{ success: false, errorCode:
|
|
658
|
+
* 'provider_inference_unavailable' }` inside a terminal `error` event's `data`,
|
|
659
|
+
* never by throwing. Both `data` and the whole record are offered to
|
|
660
|
+
* `isUpstreamUnavailable` so a payload nested either way is caught.
|
|
661
|
+
*/
|
|
662
|
+
declare function classifyTerminalFailure(event: unknown): TerminalFailure | null;
|
|
663
|
+
/** Open the raw turn stream for one specific model. */
|
|
664
|
+
type OpenModelStream = (args: {
|
|
665
|
+
model: string;
|
|
666
|
+
/** 1 for the preferred model, 2 for the first fallback, and so on. */
|
|
667
|
+
attempt: number;
|
|
668
|
+
}) => AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>;
|
|
669
|
+
/** Fired when a model is abandoned and the next one is about to be tried. */
|
|
670
|
+
interface ModelFallbackInfo {
|
|
671
|
+
from: string;
|
|
672
|
+
to: string;
|
|
673
|
+
reason: string;
|
|
674
|
+
}
|
|
675
|
+
/** Define inputs for streaming a turn across a model failover chain */
|
|
676
|
+
interface ModelFailoverStreamOptions {
|
|
677
|
+
/** Preferred model first, then fallbacks in descending preference. */
|
|
678
|
+
models: readonly string[];
|
|
679
|
+
open: OpenModelStream;
|
|
680
|
+
/** Override the commit-point rule. Default {@link isCommittingSandboxEvent}. */
|
|
681
|
+
isCommitting?: (event: unknown) => boolean;
|
|
682
|
+
onFallback?: (info: ModelFallbackInfo) => void;
|
|
683
|
+
log?: (message: string, meta?: Record<string, unknown>) => void;
|
|
684
|
+
}
|
|
685
|
+
/** The failover-wrapped stream plus the attribution every consumer needs. */
|
|
686
|
+
interface ModelFailoverStreamHandle {
|
|
687
|
+
events: AsyncGenerator<unknown, void, unknown>;
|
|
688
|
+
/** The model that actually served. `undefined` until the first pull resolves it. */
|
|
689
|
+
servingModel(): string | undefined;
|
|
690
|
+
/** Every model tried, in order, with the reason each was abandoned. */
|
|
691
|
+
attempts(): ModelFailoverAttempt[];
|
|
692
|
+
/** True when the preferred model did not serve — the attributability signal. */
|
|
693
|
+
usedFallback(): boolean;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Wrap `open` in reactive model failover, streaming from the first model in
|
|
697
|
+
* `models` that reaches its commit point.
|
|
698
|
+
*
|
|
699
|
+
* Zero added latency on the happy path: the preferred model is opened first and,
|
|
700
|
+
* the moment it emits anything meaningful, its events flow straight through.
|
|
701
|
+
*
|
|
702
|
+
* @throws ModelFailoverExhaustedError when every model's upstream is down, and
|
|
703
|
+
* re-throws a non-outage error from the FIRST model without walking the
|
|
704
|
+
* chain (both behaviors inherited from `runWithModelFailover`).
|
|
705
|
+
*/
|
|
706
|
+
declare function streamWithModelFailover(options: ModelFailoverStreamOptions): ModelFailoverStreamHandle;
|
|
707
|
+
|
|
569
708
|
/**
|
|
570
709
|
* Sandbox lane: bridge a raw sandbox event stream (`streamSandboxPrompt`) into
|
|
571
710
|
* the `ChatTurnProducer` shape agent-runtime's `handleChatTurn` consumes AND
|
|
@@ -608,10 +747,46 @@ type FilePartPromotionOutcome = {
|
|
|
608
747
|
};
|
|
609
748
|
/** Define options for producing sandbox chat events with rendering and interaction controls */
|
|
610
749
|
interface SandboxChatProducerOptions {
|
|
611
|
-
/** The raw sandbox event stream (e.g. `streamSandboxPrompt(...)`).
|
|
612
|
-
|
|
613
|
-
|
|
750
|
+
/** The raw sandbox event stream (e.g. `streamSandboxPrompt(...)`).
|
|
751
|
+
*
|
|
752
|
+
* An ALREADY-OPEN stream is bound to one model and cannot be reopened, so
|
|
753
|
+
* this form can never fail over. Prefer {@link openEvents}; exactly one of
|
|
754
|
+
* the two is required. */
|
|
755
|
+
events?: AsyncIterable<unknown>;
|
|
756
|
+
/** Open the raw sandbox stream FOR A GIVEN MODEL — the failover-capable form
|
|
757
|
+
* of {@link events}. The callback receives the model to run and returns the
|
|
758
|
+
* same `AsyncIterable` `events` would have been (typically
|
|
759
|
+
* `streamSandboxPrompt(shell, box, prompt, { ...opts, model })`).
|
|
760
|
+
*
|
|
761
|
+
* Wiring this turns failover ON with no further flag: whenever the resolved
|
|
762
|
+
* chain (`model` + {@link fallbackModels}) holds more than one entry, a model
|
|
763
|
+
* whose upstream is dead is abandoned BEFORE its first client-visible byte
|
|
764
|
+
* and the next one is tried. A one-entry chain opens exactly once — no extra
|
|
765
|
+
* call, no added latency, byte-identical to today.
|
|
766
|
+
*
|
|
767
|
+
* Requires {@link model}: failover has to know which model it is running. */
|
|
768
|
+
openEvents?: OpenModelStream;
|
|
769
|
+
/** Recorded on the persisted assistant message. When failover moves the turn
|
|
770
|
+
* to another model, the producer's `model` reports the model that ACTUALLY
|
|
771
|
+
* served, not this preferred one — see {@link modelFailover}. */
|
|
614
772
|
model?: string;
|
|
773
|
+
/** Models to try, in order, when `model`'s upstream is dead. Product config,
|
|
774
|
+
* never a shell default: agent-app cannot know which ids are live, and a
|
|
775
|
+
* baked list would rot into exactly the stale-liveness bug this guards
|
|
776
|
+
* against. Empty/omitted → one attempt, today's behavior.
|
|
777
|
+
*
|
|
778
|
+
* Pick these deliberately. A same-family fallback is NOT automatically safe:
|
|
779
|
+
* `gemini-2.5-flash` produced zero persisted deliverables in 2 of 3 measured
|
|
780
|
+
* runs on a med-legal filing flow where `gemini-2.5-pro` succeeded. That is
|
|
781
|
+
* why every fallback is surfaced (persisted `model`, a transcript notice, and
|
|
782
|
+
* `modelFailover()`) instead of being applied silently. */
|
|
783
|
+
fallbackModels?: readonly string[];
|
|
784
|
+
/** Opt out of failover while still using {@link openEvents}. `false` collapses
|
|
785
|
+
* the chain to `model` alone. */
|
|
786
|
+
modelFailover?: false;
|
|
787
|
+
/** Fired when a model is abandoned mid-chain (telemetry/alerting). The user
|
|
788
|
+
* already sees a transcript notice; this is for the operator. */
|
|
789
|
+
onModelFallback?: (info: ModelFallbackInfo) => void;
|
|
615
790
|
/** Which ask kinds the product renders a card for. Anything else is
|
|
616
791
|
* auto-declined (see `declineInteraction`) so the run never hangs in the
|
|
617
792
|
* broker waiting on a card no client will show. Default: question/plan.
|
|
@@ -705,9 +880,30 @@ interface DetachedTurnOptions {
|
|
|
705
880
|
scopeId: string;
|
|
706
881
|
/** The raw sandbox event stream for this turn (e.g. `streamSandboxPrompt`).
|
|
707
882
|
* Ownership of the box, prompt, tooling, and attachments stays with the
|
|
708
|
-
* caller — this only projects the stream.
|
|
709
|
-
|
|
710
|
-
|
|
883
|
+
* caller — this only projects the stream.
|
|
884
|
+
*
|
|
885
|
+
* An already-open stream is bound to one model and cannot fail over. Prefer
|
|
886
|
+
* {@link openEvents}; exactly one of the two is required. */
|
|
887
|
+
events?: AsyncIterable<unknown>;
|
|
888
|
+
/** Open the raw sandbox stream FOR A GIVEN MODEL — the failover-capable form
|
|
889
|
+
* of {@link events}. Wiring it turns failover on with no further flag
|
|
890
|
+
* whenever {@link fallbackModels} is non-empty. Requires {@link model}.
|
|
891
|
+
*
|
|
892
|
+
* An autonomous run is the case that most needs this: nobody is watching to
|
|
893
|
+
* notice a dead upstream and retry by hand, so without failover the mission
|
|
894
|
+
* step or queue job simply fails. Forwarded to the producer. */
|
|
895
|
+
openEvents?: SandboxChatProducerOptions['openEvents'];
|
|
896
|
+
/** Models to try, in order, when `model`'s upstream is dead. Product config —
|
|
897
|
+
* see the producer's note on why a same-family fallback is not automatically
|
|
898
|
+
* safe and why every fallback is surfaced. */
|
|
899
|
+
fallbackModels?: SandboxChatProducerOptions['fallbackModels'];
|
|
900
|
+
/** Opt out of failover while still using {@link openEvents}. */
|
|
901
|
+
modelFailover?: false;
|
|
902
|
+
/** Fired when a model is abandoned mid-chain (telemetry/alerting). */
|
|
903
|
+
onModelFallback?: SandboxChatProducerOptions['onModelFallback'];
|
|
904
|
+
/** The PREFERRED model. Recorded on the persisted assistant message + usage
|
|
905
|
+
* receipt — unless failover moved the turn, in which case the model that
|
|
906
|
+
* actually served is recorded instead and surfaced on the result. */
|
|
711
907
|
model?: string;
|
|
712
908
|
/** Per-flush buffer coalescer. Default `coalesceDeltas`. */
|
|
713
909
|
coalesce?: (events: unknown[]) => unknown[];
|
|
@@ -786,6 +982,15 @@ interface DetachedTurnResult {
|
|
|
786
982
|
* `null` when the turn produced nothing and the row was retracted. Absent
|
|
787
983
|
* when the caller owns persistence (today's behavior). */
|
|
788
984
|
messageId?: string | null;
|
|
985
|
+
/** The model that SERVED the turn — the fallback's id when failover moved it.
|
|
986
|
+
* A caller that bills or scores per model must read this, not the model it
|
|
987
|
+
* requested. */
|
|
988
|
+
model?: string;
|
|
989
|
+
/** True when the preferred model did not serve. Makes an autonomous
|
|
990
|
+
* downgrade — which no human watched happen — attributable after the fact. */
|
|
991
|
+
usedModelFallback?: boolean;
|
|
992
|
+
/** Every model tried, in order, with the reason each was abandoned. */
|
|
993
|
+
modelAttempts?: ModelFailoverAttempt[];
|
|
789
994
|
}
|
|
790
995
|
/**
|
|
791
996
|
* Stream a detached turn into the live turn-event buffer, durably.
|
|
@@ -1311,4 +1516,4 @@ interface PromoteAgentFilePartOptions {
|
|
|
1311
1516
|
/** Promote a part of an agent file with optional byte limits and MIME type detection */
|
|
1312
1517
|
declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
|
|
1313
1518
|
|
|
1314
|
-
export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type FilePartPromotionOutcome, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isDraftContentEvent, promoteAgentFilePart, resolveChatAttachments, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, withDurableChatProjection };
|
|
1519
|
+
export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, type ChatTurnModelFailover, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type FilePartPromotionOutcome, type ModelFailoverStreamHandle, type ModelFailoverStreamOptions, type ModelFallbackInfo, type OpenModelStream, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, classifyTerminalFailure, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isCommittingSandboxEvent, isDraftContentEvent, promoteAgentFilePart, resolveChatAttachments, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, withDurableChatProjection };
|
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
|
|
18
18
|
reconcileStaleTurnLock
|
|
19
19
|
} from "../chunk-CCVWR33L.js";
|
|
20
|
+
import {
|
|
21
|
+
buildModelChain,
|
|
22
|
+
isUpstreamUnavailable,
|
|
23
|
+
runWithModelFailover
|
|
24
|
+
} from "../chunk-KFTRTOT2.js";
|
|
20
25
|
import {
|
|
21
26
|
parseJsonObjectBody
|
|
22
27
|
} from "../chunk-PTUMBMVH.js";
|
|
@@ -480,6 +485,7 @@ function createChatTurnRoutes(options) {
|
|
|
480
485
|
error: terminalError ?? lastFailureData ?? new Error("chat turn failed")
|
|
481
486
|
});
|
|
482
487
|
} else {
|
|
488
|
+
const failoverInfo = producer?.modelFailover?.();
|
|
483
489
|
await lifecycle.onTurnComplete?.({
|
|
484
490
|
identity,
|
|
485
491
|
executionId,
|
|
@@ -487,7 +493,9 @@ function createChatTurnRoutes(options) {
|
|
|
487
493
|
context,
|
|
488
494
|
durationMs,
|
|
489
495
|
finalText: producer?.finalText() ?? "",
|
|
490
|
-
usage: producer?.usage?.() ?? {}
|
|
496
|
+
usage: producer?.usage?.() ?? {},
|
|
497
|
+
...producer?.model ? { model: producer.model } : {},
|
|
498
|
+
...failoverInfo ? { modelFailover: failoverInfo } : {}
|
|
491
499
|
});
|
|
492
500
|
}
|
|
493
501
|
} catch (err) {
|
|
@@ -634,13 +642,18 @@ function createChatTurnRoutes(options) {
|
|
|
634
642
|
// (not a throw) still lands here — so surface `runFailed` so the
|
|
635
643
|
// product skips billing an errored turn instead of marking it
|
|
636
644
|
// complete with empty text.
|
|
637
|
-
onTurnComplete: ({ identity: turnIdentity, finalText }) =>
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
645
|
+
onTurnComplete: ({ identity: turnIdentity, finalText }) => {
|
|
646
|
+
const failoverInfo = producer?.modelFailover?.();
|
|
647
|
+
return options.onTurnComplete({
|
|
648
|
+
identity: turnIdentity,
|
|
649
|
+
finalText,
|
|
650
|
+
context,
|
|
651
|
+
failed: runFailed,
|
|
652
|
+
...runFailed ? { failureReason: failureReasonOf(lastFailureData) } : {},
|
|
653
|
+
...producer?.model ? { model: producer.model } : {},
|
|
654
|
+
...failoverInfo ? { modelFailover: failoverInfo } : {}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
644
657
|
} : {},
|
|
645
658
|
...options.traceFlush ? { traceFlush: () => options.traceFlush(context) } : {}
|
|
646
659
|
}
|
|
@@ -779,6 +792,130 @@ function concatStreams(streams) {
|
|
|
779
792
|
});
|
|
780
793
|
}
|
|
781
794
|
|
|
795
|
+
// src/chat-routes/model-failover-stream.ts
|
|
796
|
+
var TERMINAL_FAILURE_TYPES = /* @__PURE__ */ new Set(["error", "session.run.failed"]);
|
|
797
|
+
var NON_COMMITTING_PART_TYPES = /* @__PURE__ */ new Set(["step-start", "step-finish"]);
|
|
798
|
+
function isCommittingSandboxEvent(event) {
|
|
799
|
+
const record = asRecord(event);
|
|
800
|
+
if (!record) return false;
|
|
801
|
+
const type = asString(record.type) ?? "";
|
|
802
|
+
if (!type) return false;
|
|
803
|
+
if (type === "message.part.updated") {
|
|
804
|
+
const part = asRecord(asRecord(record.data)?.part);
|
|
805
|
+
const partType = asString(part?.type) ?? "";
|
|
806
|
+
if (NON_COMMITTING_PART_TYPES.has(partType)) return false;
|
|
807
|
+
if (partType === "text" || partType === "reasoning") {
|
|
808
|
+
const text = asString(part?.text) ?? asString(part?.content) ?? "";
|
|
809
|
+
return text.length > 0;
|
|
810
|
+
}
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
if (type === "start" || type === "execution.started" || type === "status" || type === "model-processing" || type === "session.created" || type === "session.updated" || type === "session.idle" || type === "step-start" || type === "step-finish" || type === "turn" || type === "warning") {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
function classifyTerminalFailure(event) {
|
|
819
|
+
const record = asRecord(event);
|
|
820
|
+
if (!record) return null;
|
|
821
|
+
const type = asString(record.type) ?? "";
|
|
822
|
+
if (!TERMINAL_FAILURE_TYPES.has(type)) return null;
|
|
823
|
+
const data = asRecord(record.data);
|
|
824
|
+
const outage = isUpstreamUnavailable(data) || isUpstreamUnavailable(record);
|
|
825
|
+
const reason = asString(data?.message) ?? asString(data?.error) ?? asString(data?.reason) ?? asString(record.message) ?? `sandbox stream reported ${type}`;
|
|
826
|
+
const code = asString(data?.errorCode) ?? asString(data?.code);
|
|
827
|
+
return { outage, reason, ...code ? { code } : {} };
|
|
828
|
+
}
|
|
829
|
+
async function closeIterator(iterator, log) {
|
|
830
|
+
try {
|
|
831
|
+
await iterator.return?.();
|
|
832
|
+
} catch (err) {
|
|
833
|
+
log?.("[chat-routes] abandoning a failed model stream threw", {
|
|
834
|
+
error: err instanceof Error ? err.message : String(err)
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function streamWithModelFailover(options) {
|
|
839
|
+
const committing = options.isCommitting ?? isCommittingSandboxEvent;
|
|
840
|
+
let serving;
|
|
841
|
+
let trail = [];
|
|
842
|
+
let fellBack = false;
|
|
843
|
+
let attemptIndex = 0;
|
|
844
|
+
const probe = async (model) => {
|
|
845
|
+
attemptIndex += 1;
|
|
846
|
+
const source = await options.open({ model, attempt: attemptIndex });
|
|
847
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
848
|
+
const buffered = [];
|
|
849
|
+
for (; ; ) {
|
|
850
|
+
const next = await iterator.next();
|
|
851
|
+
if (next.done) {
|
|
852
|
+
return { committed: true, buffered, iterator: null };
|
|
853
|
+
}
|
|
854
|
+
const event = next.value;
|
|
855
|
+
const failure = classifyTerminalFailure(event);
|
|
856
|
+
if (failure?.outage) {
|
|
857
|
+
await closeIterator(iterator, options.log);
|
|
858
|
+
return {
|
|
859
|
+
committed: false,
|
|
860
|
+
error: failure.reason,
|
|
861
|
+
...failure.code ? { errorCode: failure.code } : {}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
buffered.push(event);
|
|
865
|
+
if (failure) return { committed: true, buffered, iterator };
|
|
866
|
+
if (committing(event)) return { committed: true, buffered, iterator };
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
const events = (async function* () {
|
|
870
|
+
let handle;
|
|
871
|
+
try {
|
|
872
|
+
const outcome = await runWithModelFailover({
|
|
873
|
+
models: options.models,
|
|
874
|
+
run: probe,
|
|
875
|
+
// The probe has already classified the raw payload with
|
|
876
|
+
// `isUpstreamUnavailable`; this reads its verdict rather than
|
|
877
|
+
// re-classifying a wrapper object, so the two can never disagree.
|
|
878
|
+
isUnavailableResult: (result) => result.committed === false,
|
|
879
|
+
onFallback: (attempt, nextModel) => {
|
|
880
|
+
const info = {
|
|
881
|
+
from: attempt.model,
|
|
882
|
+
to: nextModel,
|
|
883
|
+
reason: attempt.reason ?? "upstream unavailable"
|
|
884
|
+
};
|
|
885
|
+
options.log?.("[chat-routes] model upstream unavailable; falling over", { ...info });
|
|
886
|
+
options.onFallback?.(info);
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
serving = outcome.model;
|
|
890
|
+
trail = outcome.attempts;
|
|
891
|
+
fellBack = outcome.usedFallback;
|
|
892
|
+
handle = outcome.value;
|
|
893
|
+
} catch (err) {
|
|
894
|
+
const attempts = err?.attempts;
|
|
895
|
+
if (Array.isArray(attempts)) trail = attempts;
|
|
896
|
+
throw err;
|
|
897
|
+
}
|
|
898
|
+
for (const event of handle.buffered) yield event;
|
|
899
|
+
const live = handle.iterator;
|
|
900
|
+
if (!live) return;
|
|
901
|
+
try {
|
|
902
|
+
for (; ; ) {
|
|
903
|
+
const next = await live.next();
|
|
904
|
+
if (next.done) return;
|
|
905
|
+
yield next.value;
|
|
906
|
+
}
|
|
907
|
+
} finally {
|
|
908
|
+
await closeIterator(live, options.log);
|
|
909
|
+
}
|
|
910
|
+
})();
|
|
911
|
+
return {
|
|
912
|
+
events,
|
|
913
|
+
servingModel: () => serving,
|
|
914
|
+
attempts: () => trail,
|
|
915
|
+
usedFallback: () => fellBack
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
782
919
|
// src/chat-routes/sandbox-producer.ts
|
|
783
920
|
function textDelta(tracker, key, part, rawDelta) {
|
|
784
921
|
const explicit = typeof rawDelta === "string" ? rawDelta : void 0;
|
|
@@ -885,6 +1022,40 @@ function sandboxStreamFailureDiagnostic(error) {
|
|
|
885
1022
|
function createSandboxChatProducer(options) {
|
|
886
1023
|
const log = options.log ?? ((message, meta) => console.error(message, meta ?? ""));
|
|
887
1024
|
const renderable = options.isRenderableInteraction ?? isRenderableInteractionKind;
|
|
1025
|
+
if (options.openEvents && !options.model) {
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
"createSandboxChatProducer: `openEvents` requires `model` \u2014 failover must know which model it is running"
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
if (!options.openEvents && !options.events) {
|
|
1031
|
+
throw new Error("createSandboxChatProducer: pass `openEvents` (failover-capable) or `events`");
|
|
1032
|
+
}
|
|
1033
|
+
const chain = options.model ? buildModelChain(options.model, options.modelFailover === false ? [] : options.fallbackModels ?? []) : [];
|
|
1034
|
+
const pendingModelNotices = [];
|
|
1035
|
+
let modelNoticeCount = 0;
|
|
1036
|
+
let failover;
|
|
1037
|
+
let source;
|
|
1038
|
+
if (options.openEvents) {
|
|
1039
|
+
failover = streamWithModelFailover({
|
|
1040
|
+
models: chain,
|
|
1041
|
+
open: options.openEvents,
|
|
1042
|
+
log,
|
|
1043
|
+
onFallback: (info) => {
|
|
1044
|
+
modelNoticeCount += 1;
|
|
1045
|
+
pendingModelNotices.push({
|
|
1046
|
+
id: `model-fallback-${modelNoticeCount}`,
|
|
1047
|
+
// Named models on both sides: a quality regression after a downgrade
|
|
1048
|
+
// must be attributable to the model that actually answered.
|
|
1049
|
+
text: `${info.from} was unavailable (${info.reason}) \u2014 answered with ${info.to} instead.`
|
|
1050
|
+
});
|
|
1051
|
+
options.onModelFallback?.(info);
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
source = failover.events;
|
|
1055
|
+
} else {
|
|
1056
|
+
source = options.events;
|
|
1057
|
+
}
|
|
1058
|
+
const servingModel = () => failover?.servingModel() ?? options.model;
|
|
888
1059
|
let fullText = "";
|
|
889
1060
|
const partOrder = [];
|
|
890
1061
|
const partMap = /* @__PURE__ */ new Map();
|
|
@@ -931,9 +1102,18 @@ function createSandboxChatProducer(options) {
|
|
|
931
1102
|
yield { type: "usage", usage: { promptTokens, completionTokens } };
|
|
932
1103
|
}
|
|
933
1104
|
}
|
|
1105
|
+
function* drainModelNotices() {
|
|
1106
|
+
while (pendingModelNotices.length > 0) {
|
|
1107
|
+
const queued = pendingModelNotices.shift();
|
|
1108
|
+
const notice = noticePart("warning", queued.id, queued.text);
|
|
1109
|
+
recordPersistedPart(notice, void 0, noticePartKey(notice.id));
|
|
1110
|
+
yield { type: "notice", id: notice.id, noticeKind: "warning", text: queued.text };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
934
1113
|
async function* stream() {
|
|
935
1114
|
try {
|
|
936
|
-
for await (const raw of
|
|
1115
|
+
for await (const raw of source) {
|
|
1116
|
+
yield* drainModelNotices();
|
|
937
1117
|
const record = asRecord(raw);
|
|
938
1118
|
if (!record || typeof record.type !== "string") continue;
|
|
939
1119
|
const normalized = normalizeToolEvent({ type: record.type, data: asRecord(record.data) });
|
|
@@ -1155,6 +1335,7 @@ ${errorContent}` : errorContent;
|
|
|
1155
1335
|
}
|
|
1156
1336
|
yield toProducerWireEvent(record);
|
|
1157
1337
|
}
|
|
1338
|
+
yield* drainModelNotices();
|
|
1158
1339
|
} catch (streamErr) {
|
|
1159
1340
|
const diagnostic = sandboxStreamFailureDiagnostic(streamErr);
|
|
1160
1341
|
log("[chat-routes] sandbox stream failed", {
|
|
@@ -1193,7 +1374,19 @@ ${diagnostic.userMessage}` : diagnostic.userMessage;
|
|
|
1193
1374
|
// phantom failures the final write then reverses.
|
|
1194
1375
|
draftParts: () => draftAssistantParts(partOrder, partMap, fullText),
|
|
1195
1376
|
usage: () => usage,
|
|
1196
|
-
|
|
1377
|
+
// A GETTER, not a captured value: `turn-routes` reads `producer.model` at
|
|
1378
|
+
// draft-snapshot and persist time, both of which happen after the stream
|
|
1379
|
+
// resolved which model serves. A plain property would freeze the PREFERRED
|
|
1380
|
+
// model into the row and make a downgrade unattributable — the exact
|
|
1381
|
+
// failure mode this work exists to prevent.
|
|
1382
|
+
get model() {
|
|
1383
|
+
return servingModel();
|
|
1384
|
+
},
|
|
1385
|
+
modelFailover: () => ({
|
|
1386
|
+
model: servingModel(),
|
|
1387
|
+
attempts: failover?.attempts() ?? [],
|
|
1388
|
+
usedFallback: failover?.usedFallback() ?? false
|
|
1389
|
+
})
|
|
1197
1390
|
};
|
|
1198
1391
|
}
|
|
1199
1392
|
|
|
@@ -1219,6 +1412,7 @@ function cachedResultFrom(final) {
|
|
|
1219
1412
|
async function runDetachedTurn(opts) {
|
|
1220
1413
|
const { store, turnId, scopeId } = opts;
|
|
1221
1414
|
let producer;
|
|
1415
|
+
const servingModel = () => producer?.model ?? opts.model;
|
|
1222
1416
|
let draft;
|
|
1223
1417
|
if (opts.persist) {
|
|
1224
1418
|
const { store: persistStore, threadId, messageId, transformText, ...tuning } = opts.persist;
|
|
@@ -1236,13 +1430,19 @@ async function runDetachedTurn(opts) {
|
|
|
1236
1430
|
content: producer.finalText?.() ?? "",
|
|
1237
1431
|
...producer.draftParts ? { parts: producer.draftParts() } : {},
|
|
1238
1432
|
...producer.usage ? { usage: producer.usage() } : {},
|
|
1239
|
-
...
|
|
1433
|
+
...servingModel() ? { model: servingModel() } : {}
|
|
1240
1434
|
} : null,
|
|
1241
1435
|
...transformText ? { transformText } : {},
|
|
1242
1436
|
...opts.log ? { log: opts.log } : {}
|
|
1243
1437
|
});
|
|
1244
1438
|
}
|
|
1245
|
-
const settleRow = async (
|
|
1439
|
+
const settleRow = async (base) => {
|
|
1440
|
+
const info = producer?.modelFailover?.();
|
|
1441
|
+
const result = {
|
|
1442
|
+
...base,
|
|
1443
|
+
...servingModel() ? { model: servingModel() } : {},
|
|
1444
|
+
...info ? { usedModelFallback: info.usedFallback, modelAttempts: info.attempts } : {}
|
|
1445
|
+
};
|
|
1246
1446
|
if (!draft) return result;
|
|
1247
1447
|
const transform = opts.persist?.transformText;
|
|
1248
1448
|
const content = transform ? await transform(result.text) : result.text;
|
|
@@ -1259,7 +1459,7 @@ async function runDetachedTurn(opts) {
|
|
|
1259
1459
|
const values = {
|
|
1260
1460
|
content,
|
|
1261
1461
|
...parts2.length > 0 ? { parts: parts2 } : {},
|
|
1262
|
-
...
|
|
1462
|
+
...result.model ? { model: result.model } : {},
|
|
1263
1463
|
...result.usage.inputTokens !== void 0 ? { inputTokens: result.usage.inputTokens } : {},
|
|
1264
1464
|
...result.usage.outputTokens !== void 0 ? { outputTokens: result.usage.outputTokens } : {},
|
|
1265
1465
|
...result.usage.reasoningTokens !== void 0 ? { reasoningTokens: result.usage.reasoningTokens } : {},
|
|
@@ -1310,8 +1510,11 @@ async function runDetachedTurn(opts) {
|
|
|
1310
1510
|
});
|
|
1311
1511
|
await tap.onEvent({ type: "turn", turnId });
|
|
1312
1512
|
producer = createSandboxChatProducer({
|
|
1313
|
-
events: opts.events,
|
|
1513
|
+
...opts.openEvents ? { openEvents: opts.openEvents } : { events: opts.events },
|
|
1314
1514
|
model: opts.model,
|
|
1515
|
+
...opts.fallbackModels ? { fallbackModels: opts.fallbackModels } : {},
|
|
1516
|
+
...opts.modelFailover === false ? { modelFailover: false } : {},
|
|
1517
|
+
...opts.onModelFallback ? { onModelFallback: opts.onModelFallback } : {},
|
|
1315
1518
|
isRenderableInteraction: opts.isRenderableInteraction,
|
|
1316
1519
|
declineInteraction: opts.declineInteraction,
|
|
1317
1520
|
promoteFilePart: opts.promoteFilePart,
|
|
@@ -2064,6 +2267,7 @@ export {
|
|
|
2064
2267
|
bytesToBase64,
|
|
2065
2268
|
chatTurnRequestInit,
|
|
2066
2269
|
checkAttachmentType,
|
|
2270
|
+
classifyTerminalFailure,
|
|
2067
2271
|
createAssistantDraftWriter,
|
|
2068
2272
|
createAttachmentUploadRoute,
|
|
2069
2273
|
createChatTurnRoutes,
|
|
@@ -2073,6 +2277,7 @@ export {
|
|
|
2073
2277
|
defaultValidateAttachmentPath,
|
|
2074
2278
|
fileMentionsToParts,
|
|
2075
2279
|
formatBytes,
|
|
2280
|
+
isCommittingSandboxEvent,
|
|
2076
2281
|
isDraftContentEvent,
|
|
2077
2282
|
mediaTypeForMentionPath,
|
|
2078
2283
|
mentionKindForPath,
|
|
@@ -2088,6 +2293,7 @@ export {
|
|
|
2088
2293
|
sniffBinary,
|
|
2089
2294
|
sniffMimeFromName,
|
|
2090
2295
|
storeSupportsDraftPersistence,
|
|
2296
|
+
streamWithModelFailover,
|
|
2091
2297
|
validateSandboxMentionPath,
|
|
2092
2298
|
withDurableChatProjection
|
|
2093
2299
|
};
|