@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 simply produced nothing is NOT retried. An empty answer
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 probe = async (model) => {
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
- committed: false,
876
- error: failure.reason,
877
- ...failure.code ? { errorCode: failure.code } : {}
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)) return { committed: true, buffered, iterator };
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,