Skip to content

fix(agents,api): surface fallback model failures with classified details (#335)#383

Merged
w7-mgfcode merged 3 commits into
devfrom
fix/agents-surface-fallback-failures
Jun 11, 2026
Merged

fix(agents,api): surface fallback model failures with classified details (#335)#383
w7-mgfcode merged 3 commits into
devfrom
fix/agents-surface-fallback-failures

Conversation

@w7-mgfcode

@w7-mgfcode w7-mgfcode commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #335 · Reliability E2 of umbrella #380 (after Foundation E1 #334 / PR #382).

When every model in the PydanticAI FallbackModel chain fails (or a single configured model fails with a provider error), the client now receives a classified, secret-safe summary of each per-model failure instead of the opaque Stream error: All models from FallbackModel failed (2 sub-exceptions):

  • WS /agents/stream — one error StreamEvent with error_type="fallback_exhausted", a human-actionable per-leg summary in error (rendered verbatim by the chat UI — zero frontend changes), and an additive failures: [{model_name, status_code, reason, detail}] list.
  • REST POST /agents/sessions/{id}/chat502 application/problem+json with code="AGENT_FALLBACK_EXHAUSTED", type=/errors/agent-fallback-exhausted, and the failures extension member.

Implementation

  • app/features/agents/failures.py (new) — pure classifier: status matrix (404→model_not_found, 429→quota_exhausted, 401/403→auth_error, 5xx→provider_unavailable, other HTTP→provider_error, ResponseRejectedresponse_rejected, else→unknown), nested-ExceptionGroup recursion, secret scrubbing (AIza…/sk-…/Bearer …/api_key=…[redacted]), 300-char detail cap, deterministic human summary.
  • app/features/agents/schemas.py — new ModelFailureDetail + FailureReason; additive ErrorEvent.failures.
  • app/features/agents/service.py — new except (FallbackExceptionGroup, ModelAPIError) arms in chat() and stream_chat(), between TimeoutError and UnexpectedModelBehavior; misbehavior/salvage paths untouched; no salvage in the new arms (nothing ran).
  • app/core/problem_details.pyAGENT_FALLBACK_EXHAUSTED code + type URI; additive extensions param on problem_response merged on the serialized dict, guarded by a reserved-key frozenset.
  • app/core/exceptions.py — additive extensions kwarg on ForecastLabError (response-visible channel; details stays log-only), new AgentFallbackExhaustedError (502) mirroring EmbeddingProviderAuthError, handler pass-through.
  • docs/_base/API_CONTRACTS.md — chat row + WS error bullet updated additively.
  • websocket.py untouched — generic handler remains the backstop.

Validation

  • ruff check + ruff format --check clean
  • mypy app/ + pyright app/ (both strict) clean
  • pytest -m "not integration" — 1916 passed (incl. 20 new tests: classifier matrix, nested groups, secret scrub, truncation, summary shapes, stream 404+429 event, bare-401 event, chat raise, extensions merge + reserved-key guard)
  • pytest -m integration app/features/agents/tests/test_routes.py — 12 passed (incl. new 502 problem+json route test asserting both classified legs)
  • ✅ Live curl matrix: PATCHed a broken model pair → POST /chat returned 502 problem+json with two classified auth_error legs and request_id; operator config snapshotted and restored after
  • ✅ No secret-like material in any serialized payload (asserted on full JSON dumps)

🤖 Generated with Claude Code

Summary by Sourcery

Classify and surface detailed, secret-safe information about exhausted agent model fallbacks across REST and WebSocket surfaces, using structured failure metadata and RFC 7807 extensions.

New Features:

  • Add structured model failure details and reasons to agent error schemas and WebSocket error events for exhausted fallback chains.
  • Introduce an RFC 7807 extension channel to include classified agent fallback failures in REST problem+json responses via a dedicated AgentFallbackExhaustedError.

Enhancements:

  • Implement a reusable classifier to turn provider and fallback exception trees into sanitized, truncated, and human-readable model failure summaries.
  • Extend agent chat and streaming services to convert provider and fallback exceptions into consistent classified errors instead of exposing raw exception groups.

Documentation:

  • Document the new fallback exhaustion behavior and failure payload shape for the agents chat REST endpoint and WebSocket error events in the API contract.

Tests:

  • Add unit tests for the model-failure classifier covering status-code mapping, nested exception groups, secret scrubbing, truncation, and summary formatting.
  • Add service-level tests to verify classified fallback exhaustion handling for both chat and streaming paths, including single-model and multi-leg failures and absence of leaked secrets.
  • Add integration tests to ensure REST chat returns a 502 problem+json with the expected agent fallback exhaustion code and structured failures.
  • Add problem_details tests to confirm correct merging of RFC 7807 extensions and protection of reserved keys.

Chores:

  • Add a project requirements plan document describing the reliability epic and design for surfacing fallback model failures.

@sourcery-ai

sourcery-ai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements classified, secret-scrubbed reporting of exhausted agent model fallbacks across REST and WebSocket surfaces by introducing a model-failure classifier, extending agent error schemas and core RFC 7807 plumbing, and wiring AgentService to surface structured per-model failures via a new AgentFallbackExhaustedError and enriched error events.

Sequence diagram for REST chat fallback exhaustion handling

sequenceDiagram
    participant Client
    participant AgentsREST
    participant AgentService
    participant PydanticAIAgent
    participant FailuresClassifier
    participant ExceptionHandler
    participant ProblemDetails

    Client->>AgentsREST: POST /agents/sessions/{id}/chat
    AgentsREST->>AgentService: chat(session_id, message)
    AgentService->>PydanticAIAgent: run(...)
    PydanticAIAgent-->>AgentService: FallbackExceptionGroup / ModelAPIError
    AgentService->>FailuresClassifier: classify_model_failures(exception)
    FailuresClassifier-->>AgentService: list[ModelFailureDetail]
    AgentService->>AgentService: summarize_model_failures(failures)
    AgentService->>ExceptionHandler: raise AgentFallbackExhaustedError(message, failures)
    ExceptionHandler->>ProblemDetails: problem_response(..., error_code=AGENT_FALLBACK_EXHAUSTED, extensions={failures})
    ProblemDetails-->>Client: 502 application/problem+json
Loading

Sequence diagram for WebSocket stream fallback exhaustion handling

sequenceDiagram
    participant Client
    participant AgentsWS
    participant AgentService
    participant PydanticAIAgent
    participant FailuresClassifier

    Client->>AgentsWS: CONNECT /agents/stream
    Client->>AgentsWS: send chat message
    AgentsWS->>AgentService: stream_chat(session_id, message)
    AgentService->>PydanticAIAgent: run_stream(...)
    PydanticAIAgent-->>AgentService: FallbackExceptionGroup / ModelAPIError
    AgentService->>FailuresClassifier: classify_model_failures(exception)
    FailuresClassifier-->>AgentService: list[ModelFailureDetail]
    AgentService->>AgentService: summarize_model_failures(failures)
    AgentService-->>AgentsWS: yield StreamEvent(event_type=error, data={error_type=fallback_exhausted, failures=[...]})
    AgentsWS-->>Client: error event with fallback_exhausted and failures
Loading

File-Level Changes

Change Details Files
Introduce a reusable classifier for provider model failures and deterministic human summaries, including secret scrubbing and truncation.
  • Add app/features/agents/failures.py implementing classify_model_failures and summarize_model_failures that flatten ExceptionGroup trees into ModelFailureDetail entries.
  • Implement HTTP status-to-reason mapping, ResponseRejected handling, and a sanitization pipeline that redacts API key patterns and caps detail strings at 300 characters.
  • Add dedicated unit tests in app/features/agents/tests/test_failures.py covering the classification matrix, nested groups, secret scrubbing, truncation, and summary string formats.
app/features/agents/failures.py
app/features/agents/tests/test_failures.py
Extend agent error schemas and streaming error events to carry structured per-model failure metadata.
  • Define FailureReason and ModelFailureDetail Pydantic models to represent classified failures.
  • Augment ErrorEvent with an optional failures field to expose lists of ModelFailureDetail instances when fallback_exhausted errors occur.
  • Document the new failure payload shape and reasons in API_CONTRACTS.md for both REST and WebSocket agents APIs.
app/features/agents/schemas.py
docs/_base/API_CONTRACTS.md
Wire AgentService chat and stream_chat to classify exhausted fallbacks and surface them as structured, recoverable errors instead of leaking raw exception groups.
  • Add except (FallbackExceptionGroup, ModelAPIError) arms in chat and stream_chat that call classify_model_failures, log summarized failure reasons, and construct either an AgentFallbackExhaustedError or a single StreamEvent error.
  • Ensure stream_chat updates session.last_activity, flushes the DB, yields exactly one error event with error_type="fallback_exhausted" and failures payload, then returns without invoking salvage paths.
  • Add service-level tests for chat and stream_chat to assert status codes, error_type values, presence and classification of failures, absence of raw group strings, and secret scrubbing in serialized output.
app/features/agents/service.py
app/features/agents/tests/test_service.py
Extend core RFC 7807 error infrastructure with an extensible, response-visible channel and a dedicated AgentFallbackExhaustedError type.
  • Add an extensions dict to ForecastLabError and plumb it through forecastlab_exception_handler into problem_response.
  • Introduce AGENT_FALLBACK_EXHAUSTED_CODE and its error type URI, plus a reserved-key guard and extensions merge logic in problem_response to safely add RFC 7807 extension members.
  • Create AgentFallbackExhaustedError (502) that carries a human summary and a failures list via extensions, and add tests ensuring extensions merge correctly and cannot override reserved problem fields.
app/core/exceptions.py
app/core/problem_details.py
app/core/tests/test_problem_details.py
Expose the new fallback exhaustion behavior at the HTTP route layer with problem+json responses and integration coverage.
  • Add an integration test for POST /agents/sessions/{session_id}/chat that patches an agent to raise a FallbackExceptionGroup and asserts a 502 application/problem+json with AGENT_FALLBACK_EXHAUSTED code, correct type URI, failures list, and request_id.
  • Ensure that the opaque FallbackExceptionGroup string and any secret-like substrings are not present in the REST response body or text.
app/features/agents/tests/test_routes.py
Add a design/plan document for this reliability enhancement and its constraints.
  • Introduce PRPs/PRP-reliability-E2-surface-fallback-failures.md capturing goals, behavior changes, implementation blueprint, validation plan, and anti-patterns for surfacing classified fallback failures.
PRPs/PRP-reliability-E2-surface-fallback-failures.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfbf068e-2270-4d89-8ab8-2f442b2dedde

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agents-surface-fallback-failures

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="app/features/agents/tests/test_service.py" line_range="612" />
<code_context>
+        assert "AIza" not in json.dumps(events[0].model_dump(mode="json"))
+
+    @pytest.mark.asyncio
+    async def test_stream_chat_bare_model_api_error_classified(
+        self,
+        sample_active_session: AgentSession,
</code_context>
<issue_to_address>
**suggestion (testing):** Add an assertion that non-fallback error events do not populate the failures field

Since `ErrorEvent.failures` is meant to be set only when `error_type == "fallback_exhausted"`, we should also assert the inverse: for other error types, `failures` stays `None`. Please add a test (here or in a schema-focused test) that builds an `ErrorEvent` with `error_type="model_behavior_error"` (or similar) and verifies that `failures` is `None` in the serialized output, to protect against regressions in other error paths.

Suggested implementation:

```python
        assert "google-gla:gemini-3-flash-preview" in events[0].data["error"]
        assert "google-gla:gemini-2.5-flash" in events[0].data["error"]
        # The opaque group string must never reach the client.
        assert "sub-exceptions" not in events[0].data["error"]
        # Issue #335 hard constraint: no secret-like material anywhere.
        assert "AIza" not in json.dumps(events[0].model_dump(mode="json"))

    def test_error_event_non_fallback_has_no_failures(self) -> None:
        """Non-fallback ErrorEvents must not populate the failures field."""
        event = ErrorEvent(
            type="error",
            error_type="model_behavior_error",
            error="unexpected model behavior",
            failures=None,
        )

        serialized = event.model_dump(mode="json")
        # For non-fallback errors, failures must remain unset/None.
        assert serialized.get("failures") is None

    @pytest.mark.asyncio

```

If `ErrorEvent` is not already imported in this test module, add:
`from app.features.agents.schemas import ErrorEvent` (or the correct import path for `ErrorEvent`) to the imports at the top of `app/features/agents/tests/test_service.py`.
</issue_to_address>

### Comment 2
<location path="app/features/agents/tests/test_failures.py" line_range="18" />
<code_context>
+from app.features.agents.schemas import ModelFailureDetail
+
+
+class TestClassifyModelFailures:
+    """Classification matrix for classify_model_failures."""
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a mixed-group test that includes unexpected exception types as members

Current tests cover HTTP errors, nested `FallbackExceptionGroup`s, bare `ModelAPIError`, `ResponseRejected`, and a top-level unknown exception. To better exercise the recursion in `classify_model_failures`, please add a case where a `FallbackExceptionGroup` contains both known and unknown exceptions (e.g. `[ModelHTTPError(...), RuntimeError("boom")]`), and assert that it is flattened, order is preserved, and the unexpected member produces a `ModelFailureDetail` with `reason == "unknown"`.
</issue_to_address>

### Comment 3
<location path="app/core/tests/test_problem_details.py" line_range="37" />
<code_context>
+    assert "failures" not in body
+
+
+def test_problem_response_merges_extensions() -> None:
+    """Extension members are merged into the serialized body."""
+    response = problem_response(
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test that exercises problem_response via a ForecastLabError with extensions

Since `ForecastLabError` now carries an `extensions` dict and the handler passes it through to `problem_response`, consider adding a test that raises a minimal `ForecastLabError` (or `AgentFallbackExhaustedError`) with non-empty `extensions`, runs it through `forecastlab_exception_handler`, and asserts that the response body contains those extension fields and the expected `type`/`code`. This would exercise the full path from exception to problem+json and help catch future wiring regressions.

Suggested implementation:

```python
    body = _body(response)
    assert response.status_code == 404
    assert body["status"] == 404
    assert body["code"] == "NOT_FOUND"
    assert body["type"] == "/errors/not-found"
    assert "failures" not in body



        status=404,
        title="Not Found",
        detail="Resource not found",
        error_code="NOT_FOUND",
    )

    body = _body(response)
    assert response.status_code == 404
    assert body["status"] == 404
    assert body["code"] == "NOT_FOUND"


async def test_forecastlab_exception_handler_includes_extensions() -> None:
    """ForecastLabError extensions are propagated through the exception handler."""
    # Arrange
    request = Request(scope={"type": "http"})
    exc = ForecastLabError(
        status_code=400,
        title="Bad Request",
        detail="Something went wrong",
        error_code="BAD_REQUEST",
        extensions={"foo": "bar", "trace_id": "abc123"},
    )

    # Act
    response = await forecastlab_exception_handler(request, exc)

    # Assert
    body = _body(response)
    assert response.status_code == 400
    assert body["status"] == 400
    assert body["code"] == "BAD_REQUEST"
    assert body["title"] == "Bad Request"
    assert body["detail"] == "Something went wrong"

    # Extensions should be merged into the serialized problem+json body
    assert body["foo"] == "bar"
    assert body["trace_id"] == "abc123"

```

To fully wire this up you will likely need to:
1. Add the necessary imports at the top of `app/core/tests/test_problem_details.py`, for example:
   - `from starlette.requests import Request`
   - `from app.core.exceptions import ForecastLabError`
   - `from app.core.exception_handlers import forecastlab_exception_handler`
   Adjust module paths to match your actual project layout.
2. Ensure the async test is executed correctly:
   - If you use `pytest`, either:
     - Mark the test (or module) with `@pytest.mark.anyio` / `@pytest.mark.asyncio`, or
     - Use your existing async test configuration.
   - If `forecastlab_exception_handler` is synchronous in your codebase, remove `async` from the test definition and the `await` before calling the handler.
3. If `ForecastLabError` uses different parameter names (e.g. `status` instead of `status_code`, `error_code` vs `code`, or a different way to pass `extensions`), adjust the constructor call in the test accordingly so that:
   - The `status_code` of the HTTP response is 400.
   - The JSON body includes `"code": "BAD_REQUEST"` and the other fields.
4. If your handler derives the `"type"` field (e.g. `/errors/bad-request`) and you want to assert it as well, add:
   - `assert body["type"] == "/errors/bad-request"`
   once you confirm the exact value in your implementation.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

assert "AIza" not in json.dumps(events[0].model_dump(mode="json"))

@pytest.mark.asyncio
async def test_stream_chat_bare_model_api_error_classified(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add an assertion that non-fallback error events do not populate the failures field

Since ErrorEvent.failures is meant to be set only when error_type == "fallback_exhausted", we should also assert the inverse: for other error types, failures stays None. Please add a test (here or in a schema-focused test) that builds an ErrorEvent with error_type="model_behavior_error" (or similar) and verifies that failures is None in the serialized output, to protect against regressions in other error paths.

Suggested implementation:

        assert "google-gla:gemini-3-flash-preview" in events[0].data["error"]
        assert "google-gla:gemini-2.5-flash" in events[0].data["error"]
        # The opaque group string must never reach the client.
        assert "sub-exceptions" not in events[0].data["error"]
        # Issue #335 hard constraint: no secret-like material anywhere.
        assert "AIza" not in json.dumps(events[0].model_dump(mode="json"))

    def test_error_event_non_fallback_has_no_failures(self) -> None:
        """Non-fallback ErrorEvents must not populate the failures field."""
        event = ErrorEvent(
            type="error",
            error_type="model_behavior_error",
            error="unexpected model behavior",
            failures=None,
        )

        serialized = event.model_dump(mode="json")
        # For non-fallback errors, failures must remain unset/None.
        assert serialized.get("failures") is None

    @pytest.mark.asyncio

If ErrorEvent is not already imported in this test module, add:
from app.features.agents.schemas import ErrorEvent (or the correct import path for ErrorEvent) to the imports at the top of app/features/agents/tests/test_service.py.

from app.features.agents.schemas import ModelFailureDetail


class TestClassifyModelFailures:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding a mixed-group test that includes unexpected exception types as members

Current tests cover HTTP errors, nested FallbackExceptionGroups, bare ModelAPIError, ResponseRejected, and a top-level unknown exception. To better exercise the recursion in classify_model_failures, please add a case where a FallbackExceptionGroup contains both known and unknown exceptions (e.g. [ModelHTTPError(...), RuntimeError("boom")]), and assert that it is flattened, order is preserved, and the unexpected member produces a ModelFailureDetail with reason == "unknown".

assert "failures" not in body


def test_problem_response_merges_extensions() -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add a test that exercises problem_response via a ForecastLabError with extensions

Since ForecastLabError now carries an extensions dict and the handler passes it through to problem_response, consider adding a test that raises a minimal ForecastLabError (or AgentFallbackExhaustedError) with non-empty extensions, runs it through forecastlab_exception_handler, and asserts that the response body contains those extension fields and the expected type/code. This would exercise the full path from exception to problem+json and help catch future wiring regressions.

Suggested implementation:

    body = _body(response)
    assert response.status_code == 404
    assert body["status"] == 404
    assert body["code"] == "NOT_FOUND"
    assert body["type"] == "/errors/not-found"
    assert "failures" not in body



        status=404,
        title="Not Found",
        detail="Resource not found",
        error_code="NOT_FOUND",
    )

    body = _body(response)
    assert response.status_code == 404
    assert body["status"] == 404
    assert body["code"] == "NOT_FOUND"


async def test_forecastlab_exception_handler_includes_extensions() -> None:
    """ForecastLabError extensions are propagated through the exception handler."""
    # Arrange
    request = Request(scope={"type": "http"})
    exc = ForecastLabError(
        status_code=400,
        title="Bad Request",
        detail="Something went wrong",
        error_code="BAD_REQUEST",
        extensions={"foo": "bar", "trace_id": "abc123"},
    )

    # Act
    response = await forecastlab_exception_handler(request, exc)

    # Assert
    body = _body(response)
    assert response.status_code == 400
    assert body["status"] == 400
    assert body["code"] == "BAD_REQUEST"
    assert body["title"] == "Bad Request"
    assert body["detail"] == "Something went wrong"

    # Extensions should be merged into the serialized problem+json body
    assert body["foo"] == "bar"
    assert body["trace_id"] == "abc123"

To fully wire this up you will likely need to:

  1. Add the necessary imports at the top of app/core/tests/test_problem_details.py, for example:
    • from starlette.requests import Request
    • from app.core.exceptions import ForecastLabError
    • from app.core.exception_handlers import forecastlab_exception_handler
      Adjust module paths to match your actual project layout.
  2. Ensure the async test is executed correctly:
    • If you use pytest, either:
      • Mark the test (or module) with @pytest.mark.anyio / @pytest.mark.asyncio, or
      • Use your existing async test configuration.
    • If forecastlab_exception_handler is synchronous in your codebase, remove async from the test definition and the await before calling the handler.
  3. If ForecastLabError uses different parameter names (e.g. status instead of status_code, error_code vs code, or a different way to pass extensions), adjust the constructor call in the test accordingly so that:
    • The status_code of the HTTP response is 400.
    • The JSON body includes "code": "BAD_REQUEST" and the other fields.
  4. If your handler derives the "type" field (e.g. /errors/bad-request) and you want to assert it as well, add:
    • assert body["type"] == "/errors/bad-request"
      once you confirm the exact value in your implementation.

@w7-mgfcode w7-mgfcode merged commit fb7f84c into dev Jun 11, 2026
8 checks passed
@w7-mgfcode w7-mgfcode deleted the fix/agents-surface-fallback-failures branch June 12, 2026 15:43
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.

1 participant