@tangle-network/agent-app 0.44.1 → 0.44.2
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.
|
@@ -722,9 +722,14 @@ declare function createChatTurnRoutes<TContext = void>(options: CreateChatTurnRo
|
|
|
722
722
|
* COMMITS rather than failing over — it surfaces to the user exactly as it
|
|
723
723
|
* does today. Those fail identically on every model; walking the chain would
|
|
724
724
|
* only multiply latency and spend to reach the same error.
|
|
725
|
-
* - A clean stream that
|
|
725
|
+
* - A clean stream that produced nothing NEVER walks the chain. An empty answer
|
|
726
726
|
* is not evidence of a dead upstream, and a silent re-roll on another model is
|
|
727
|
-
* precisely the unattributable downgrade this work exists to prevent.
|
|
727
|
+
* precisely the unattributable downgrade this work exists to prevent. Opt-in
|
|
728
|
+
* `emptyTurnRetries` re-runs the SAME model instead, which leaves attribution
|
|
729
|
+
* untouched — measured on production 2026-07-27, an empty turn is a transient
|
|
730
|
+
* platform flake that a same-model re-run recovers (8 hard cases: 7/8
|
|
731
|
+
* delivered on the first pass, 8/8 with one re-run, at a cost of 1 extra turn
|
|
732
|
+
* in 9). Default `0`, so the behavior is unchanged unless a product asks.
|
|
728
733
|
* - A chain of length 1 costs nothing: one attempt, no extra call, no added
|
|
729
734
|
* latency, byte-identical to no failover at all.
|
|
730
735
|
*/
|
|
@@ -774,8 +779,33 @@ interface ModelFailoverStreamOptions {
|
|
|
774
779
|
/** Override the commit-point rule. Default {@link isCommittingSandboxEvent}. */
|
|
775
780
|
isCommitting?: (event: unknown) => boolean;
|
|
776
781
|
onFallback?: (info: ModelFallbackInfo) => void;
|
|
782
|
+
/**
|
|
783
|
+
* How many times to RE-RUN THE SAME MODEL when a turn completes having
|
|
784
|
+
* produced no assistant text at all. Default `0` — byte-identical to no
|
|
785
|
+
* retry.
|
|
786
|
+
*
|
|
787
|
+
* This is deliberately not a chain walk. Falling over to a different model
|
|
788
|
+
* on an empty answer is the unattributable downgrade this module refuses to
|
|
789
|
+
* do; re-running the SAME model changes nothing about attribution, because
|
|
790
|
+
* the model that serves is the model that was asked for.
|
|
791
|
+
*
|
|
792
|
+
* Bounded by the same commit rule as failover: only a turn whose ONLY
|
|
793
|
+
* committing event is a terminal receipt with no text is retried, so nothing
|
|
794
|
+
* that reached the user can ever be produced twice.
|
|
795
|
+
*/
|
|
796
|
+
emptyTurnRetries?: number;
|
|
797
|
+
/** Fired when an empty turn is discarded and the same model re-run. */
|
|
798
|
+
onEmptyTurnRetry?: (info: EmptyTurnRetryInfo) => void;
|
|
777
799
|
log?: (message: string, meta?: Record<string, unknown>) => void;
|
|
778
800
|
}
|
|
801
|
+
/** One same-model re-run of a turn that completed with no assistant text. */
|
|
802
|
+
interface EmptyTurnRetryInfo {
|
|
803
|
+
model: string;
|
|
804
|
+
/** 1 for the first re-run. */
|
|
805
|
+
retry: number;
|
|
806
|
+
/** How many re-runs remain after this one. */
|
|
807
|
+
remaining: number;
|
|
808
|
+
}
|
|
779
809
|
/** The failover-wrapped stream plus the attribution every consumer needs. */
|
|
780
810
|
interface ModelFailoverStreamHandle {
|
|
781
811
|
events: AsyncGenerator<unknown, void, unknown>;
|
|
@@ -786,6 +816,12 @@ interface ModelFailoverStreamHandle {
|
|
|
786
816
|
/** True when the preferred model did not serve — the attributability signal. */
|
|
787
817
|
usedFallback(): boolean;
|
|
788
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* Hard ceiling on same-model re-runs. A turn that comes back blank three times
|
|
821
|
+
* running is not a flake this can retry away, and each pass costs a full
|
|
822
|
+
* sandbox turn — so the budget is capped rather than trusted.
|
|
823
|
+
*/
|
|
824
|
+
declare const MAX_EMPTY_TURN_RETRIES = 3;
|
|
789
825
|
/**
|
|
790
826
|
* Wrap `open` in reactive model failover, streaming from the first model in
|
|
791
827
|
* `models` that reaches its commit point.
|
|
@@ -881,6 +917,18 @@ interface SandboxChatProducerOptions {
|
|
|
881
917
|
/** Fired when a model is abandoned mid-chain (telemetry/alerting). The user
|
|
882
918
|
* already sees a transcript notice; this is for the operator. */
|
|
883
919
|
onModelFallback?: (info: ModelFallbackInfo) => void;
|
|
920
|
+
/** Re-run the SAME model this many times when a turn completes with no
|
|
921
|
+
* assistant text at all. Default `0` (unchanged behavior). Requires
|
|
922
|
+
* {@link openEvents} — there is nothing to re-open on a fixed stream.
|
|
923
|
+
*
|
|
924
|
+
* Distinct from {@link fallbackModels} on purpose: this never changes which
|
|
925
|
+
* model answers, so it carries none of the attribution risk a downgrade
|
|
926
|
+
* does. Measured on production 2026-07-27 through gtm-agent's profile, a
|
|
927
|
+
* completed-but-blank turn is a transient platform flake — 8 hard cases went
|
|
928
|
+
* 7/8 delivered to 8/8 with one re-run, costing 1 extra turn in 9. */
|
|
929
|
+
emptyTurnRetries?: number;
|
|
930
|
+
/** Fired when a blank turn is discarded and the same model re-run. */
|
|
931
|
+
onEmptyTurnRetry?: (info: EmptyTurnRetryInfo) => void;
|
|
884
932
|
/** Which ask kinds the product renders a card for. Anything else is
|
|
885
933
|
* auto-declined (see `declineInteraction`) so the run never hangs in the
|
|
886
934
|
* broker waiting on a card no client will show. Default: question/plan.
|
|
@@ -1613,4 +1661,4 @@ interface PromoteAgentFilePartOptions {
|
|
|
1613
1661
|
/** Promote a part of an agent file with optional byte limits and MIME type detection */
|
|
1614
1662
|
declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
|
|
1615
1663
|
|
|
1616
|
-
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 ChatRouteEvent, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, type ChatTurnCompleteInput, 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, rowIdOf, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, withDurableChatProjection };
|
|
1664
|
+
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 ChatRouteEvent, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, type ChatTurnCompleteInput, 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 EmptyTurnRetryInfo, type FilePartPromotionOutcome, MAX_EMPTY_TURN_RETRIES, 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, rowIdOf, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, withDurableChatProjection };
|
|
@@ -842,6 +842,22 @@ function classifyTerminalFailure(event) {
|
|
|
842
842
|
const code = asString(data?.errorCode) ?? asString(data?.code);
|
|
843
843
|
return { outage, reason, ...code ? { code } : {} };
|
|
844
844
|
}
|
|
845
|
+
function isEmptyTerminalReceipt(event) {
|
|
846
|
+
const record = asRecord(event);
|
|
847
|
+
if (!record) return false;
|
|
848
|
+
const type = asString(record.type) ?? "";
|
|
849
|
+
if (type !== "result" && type !== "done") return false;
|
|
850
|
+
const data = asRecord(record.data);
|
|
851
|
+
const text = asString(data?.finalText) ?? asString(data?.text) ?? "";
|
|
852
|
+
return text.trim().length === 0;
|
|
853
|
+
}
|
|
854
|
+
var MAX_EMPTY_TURN_RETRIES = 3;
|
|
855
|
+
function resolveEmptyTurnRetries(value) {
|
|
856
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
857
|
+
const truncated = Math.trunc(value);
|
|
858
|
+
if (truncated <= 0) return 0;
|
|
859
|
+
return Math.min(truncated, MAX_EMPTY_TURN_RETRIES);
|
|
860
|
+
}
|
|
845
861
|
async function closeIterator(iterator, log) {
|
|
846
862
|
try {
|
|
847
863
|
await iterator.return?.();
|
|
@@ -853,11 +869,12 @@ async function closeIterator(iterator, log) {
|
|
|
853
869
|
}
|
|
854
870
|
function streamWithModelFailover(options) {
|
|
855
871
|
const committing = options.isCommitting ?? isCommittingSandboxEvent;
|
|
872
|
+
const emptyTurnRetries = resolveEmptyTurnRetries(options.emptyTurnRetries);
|
|
856
873
|
let serving;
|
|
857
874
|
let trail = [];
|
|
858
875
|
let fellBack = false;
|
|
859
876
|
let attemptIndex = 0;
|
|
860
|
-
const
|
|
877
|
+
const drainOnce = async (model) => {
|
|
861
878
|
attemptIndex += 1;
|
|
862
879
|
const source = await options.open({ model, attempt: attemptIndex });
|
|
863
880
|
const iterator = source[Symbol.asyncIterator]();
|
|
@@ -865,21 +882,39 @@ function streamWithModelFailover(options) {
|
|
|
865
882
|
for (; ; ) {
|
|
866
883
|
const next = await iterator.next();
|
|
867
884
|
if (next.done) {
|
|
868
|
-
return { committed: true, buffered, iterator: null };
|
|
885
|
+
return { outcome: { committed: true, buffered, iterator: null }, empty: true };
|
|
869
886
|
}
|
|
870
887
|
const event = next.value;
|
|
871
888
|
const failure = classifyTerminalFailure(event);
|
|
872
889
|
if (failure?.outage) {
|
|
873
890
|
await closeIterator(iterator, options.log);
|
|
874
891
|
return {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
892
|
+
outcome: {
|
|
893
|
+
committed: false,
|
|
894
|
+
error: failure.reason,
|
|
895
|
+
...failure.code ? { errorCode: failure.code } : {}
|
|
896
|
+
},
|
|
897
|
+
empty: false
|
|
878
898
|
};
|
|
879
899
|
}
|
|
880
900
|
buffered.push(event);
|
|
881
|
-
if (failure) return { committed: true, buffered, iterator };
|
|
882
|
-
if (committing(event))
|
|
901
|
+
if (failure) return { outcome: { committed: true, buffered, iterator }, empty: false };
|
|
902
|
+
if (committing(event)) {
|
|
903
|
+
return { outcome: { committed: true, buffered, iterator }, empty: isEmptyTerminalReceipt(event) };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
const probe = async (model) => {
|
|
908
|
+
for (let retry = 0; ; retry += 1) {
|
|
909
|
+
const pass = await drainOnce(model);
|
|
910
|
+
if (!pass.empty || retry >= emptyTurnRetries) return pass.outcome;
|
|
911
|
+
const iterator = pass.outcome.committed ? pass.outcome.iterator : null;
|
|
912
|
+
if (iterator) await closeIterator(iterator, options.log);
|
|
913
|
+
const info = { model, retry: retry + 1, remaining: emptyTurnRetries - retry - 1 };
|
|
914
|
+
options.log?.("[chat-routes] turn completed with no assistant text; re-running the same model", {
|
|
915
|
+
...info
|
|
916
|
+
});
|
|
917
|
+
options.onEmptyTurnRetry?.(info);
|
|
883
918
|
}
|
|
884
919
|
};
|
|
885
920
|
const events = (async function* () {
|
|
@@ -1056,6 +1091,8 @@ function createSandboxChatProducer(options) {
|
|
|
1056
1091
|
models: chain,
|
|
1057
1092
|
open: options.openEvents,
|
|
1058
1093
|
log,
|
|
1094
|
+
...options.emptyTurnRetries !== void 0 ? { emptyTurnRetries: options.emptyTurnRetries } : {},
|
|
1095
|
+
...options.onEmptyTurnRetry ? { onEmptyTurnRetry: options.onEmptyTurnRetry } : {},
|
|
1059
1096
|
onFallback: (info) => {
|
|
1060
1097
|
modelNoticeCount += 1;
|
|
1061
1098
|
pendingModelNotices.push({
|
|
@@ -2268,6 +2305,7 @@ export {
|
|
|
2268
2305
|
INLINE_PARTS_MAX_BYTES,
|
|
2269
2306
|
MAX_ATTACHMENT_TOTAL_BYTES,
|
|
2270
2307
|
MAX_BINARY_ATTACHMENT_BYTES,
|
|
2308
|
+
MAX_EMPTY_TURN_RETRIES,
|
|
2271
2309
|
MAX_TEXT_ATTACHMENT_BYTES,
|
|
2272
2310
|
MENTION_MAX_COUNT,
|
|
2273
2311
|
PROMOTE_MAX_FILE_BYTES,
|