Skip to content

fix: add LLM-level retry for transient Bedrock errors (ServiceUnavailableException) - #18

Merged
omerhochman merged 4 commits into
salfatigroup:mainfrom
techjacker:fix/service-unavailable
Apr 23, 2026
Merged

omerhochman merged 4 commits into
salfatigroup:mainfrom
techjacker:fix/service-unavailable

Conversation

@techjacker

@techjacker techjacker commented Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary v0.3.0

  • Add transparent retry with exponential backoff for transient LLM errors (Bedrock ServiceUnavailableException, throttling, network) using LangChain's built-in .withRetry() on the chat model
  • Add llmRetryAttempts config option (default: 3, set to 0 to disable) so consumers can tune retry behavior
  • Add specific error message for ServiceUnavailableException when retries are exhausted, replacing the generic "Unexpected error" output

Problem

Bedrock intermittently returns ServiceUnavailableException (503) during the LangChain ReAct agent loop. This kills the entire agent.invoke() call, losing all accumulated agent state (tool calls, reasoning steps). The error falls through to a generic catch-all with an unhelpful message:

TEST_FAILED: Unexpected error during agent execution (ServiceUnavailableException).
  Error: Bedrock is unable to process your request.
  Suggestion: This may be a transient issue. Check the error above and retry.

The existing test-level retry (maxAttempts) restarts the entire test from scratch — wasteful for a transient infrastructure hiccup on a single LLM call mid-conversation when test suites can take 45 mins to run.

Solution

Retry at the LLM call level using model.withRetry(), which is available on all LangChain Runnable instances. This is the optimal layer because:

  1. Preserves agent state — a transient 503 on one LLM call gets retried without losing the entire multi-step conversation
  2. Exponential backoff built-in — LangChain's RunnableRetry handles backoff internally
  3. Fail-fast for non-transient errors — the onFailedAttempt callback re-throws auth errors, validation errors, etc. immediately
  4. Zero consumer config changes needed — the default llmRetryAttempts: 3 kicks in automatically

Error classification (isTransientError)

Retries only for errors classified as transient:

  • ServiceUnavailableException (Bedrock 503)
  • ThrottlingException, 429, rate limit
  • ModelStreamErrorException, overloaded
  • ECONNRESET, ETIMEDOUT, socket hang up

All other errors (401, API key, validation, recursion limit) fail immediately.

Misc Extra Fixes

App prints dynamic version from obtained from package.json.

techjacker and others added 2 commits April 21, 2026 11:23
…ableException)

Wrap chat model with LangChain's .withRetry() to transparently retry
transient errors (503, throttling, network) with exponential backoff.
This preserves agent conversation state instead of restarting the entire test.

- Add llmRetryAttempts config option (default: 3, 0 to disable)
- Add isTransientError() classifier for retryable error detection
- Add specific formatAgentError branch for ServiceUnavailableException
- Add comprehensive tests for retry logic and error classification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@techjacker
techjacker marked this pull request as ready for review April 21, 2026 12:49
Comment thread src/agent/agent-factory.ts Outdated
Comment on lines +257 to +260
errorMessage.includes("ServiceUnavailableException") ||
errorMessage.includes("503") ||
errorMessage.includes("unable to process your request")
) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we create custom errors instead of searching for text within the error message?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done with be8d653

…handler

Replace string-based error detection in formatAgentError with instanceof
check on a custom TransientLLMError class. Transient errors are now wrapped
in the catch block using isTransientError() before being passed to the
error formatter.
@omerhochman
omerhochman merged commit 1688018 into salfatigroup:main Apr 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants