Skip to content

AI scenarios

Errors and retry

Bit.BlazorUI.AI Scripted transport

The first attempt is rate limited. The failure is retryable, keeps the partial answer, and recovers on the second try.

Live demo

Send anything: the first request fails on purpose. Press Try again.

\n**A retryable failure**: ```razor @implements IAsyncDisposable
``` ```csharp private readonly BitAiChatSession session = new(AiDemoInteractiveTransports.FailsThenRecovers()); private readonly string[] suggestions = ["Summarise the release notes"]; public ValueTask DisposeAsync() => session.DisposeAsync(); ``` `AiDemoInteractiveTransports.cs`: ```csharp // AiScriptedTransport and AiScript are the demo's helpers - see the Basic chat scenario. public static IBitAiTransport FailsThenRecovers() { var attempts = 0; return new AiScriptedTransport((request, token) => AiScript.Stream(token, async yielder => { await yielder(new BitAiMessageStartedEvent()); var attempt = Interlocked.Increment(ref attempts); if (attempt == 1) { // Some of the answer streams, then the request fails - and what streamed is kept. await foreach (var evt in AiScript.Write("Looking that up", 4, 25, token)) await yielder(evt); await yielder(new BitAiErrorEvent(new BitAiError("The model is rate limited right now.") { Code = "rate_limit", Kind = BitAiErrorKind.RateLimit, IsRetryable = true, RetryAfter = TimeSpan.FromSeconds(3), Detail = "HTTP 429\nx-ratelimit-remaining: 0\nretry-after: 3", })); return; } // Only the retry right after the failure talks about it; every later turn is an ordinary one. var answer = attempt == 2 ? "That worked on the second attempt." : $"You said: {AiScript.LastUserText(request)}"; await foreach (var evt in AiScript.Write(answer, 4, 12, token)) await yielder(evt); await yielder(new BitAiMessageCompletedEvent()); })); } ```

How it works

Failures are data. A transport reports one as a BitAiErrorEvent carrying a BitAiError: a message safe to show, a machine-readable Code, a Kind, whether retrying could help (IsRetryable), how long to wait (RetryAfter) and the technical Detail. Components branch on that rather than on any provider's exception type.

Here the first request writes a few words, then sends a terminal BitAiErrorEvent: a rate limit (BitAiErrorKind.RateLimit) that is retryable after three seconds. The reply fails with the error on it, the session keeps it as LastError, and the error view shows the message, how long to wait (RetryAfter, until that time is up) and, since it is retryable, Try again - which calls BitAiChatSession.RetryAsync(): the failed reply is removed and the same user message is sent again, and this time the transport answers.

The session words two failures itself, with the same shape: a turn that runs out of BitAiChatSessionOptions.Timeout fails as a retryable Timeout, and a stopped turn keeps what it had written and is marked cancelled. An error sent with isTerminal: false does not end the reply: it adds a BitAiErrorPart and the stream goes on.

An exception that escapes the transport is translated into the same shape (unless BitAiChatSessionOptions.CatchTransportExceptions is turned off), so an unhandled failure still renders as a retryable error rather than tearing down the page. Whatever had already streamed is kept: losing a half-written answer to a network blip is worse than showing it with a retry button underneath.

Components used

  • BitAiErrorView - what went wrong, the retry when it can help, and the detail behind a disclosure.
  • BitAiChatMessage - the message that keeps its partial answer above the error.

Feedback

Found a mistake, a gap, or something that could be clearer? Every page and every component is one click from its source.