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