Skip to content

fix(run_agent): surface traceback frame for empty-message errors in retry display - #14733

Open
AndreKurait wants to merge 1 commit into
NousResearch:mainfrom
AndreKurait:fix/summarize-api-error-empty-message
Open

fix(run_agent): surface traceback frame for empty-message errors in retry display#14733
AndreKurait wants to merge 1 commit into
NousResearch:mainfrom
AndreKurait:fix/summarize-api-error-empty-message

Conversation

@AndreKurait

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the retry loop's 📝 Error: display showing a blank line when an exception has an empty string representation. Walks the traceback and surfaces the last frame instead.

Motivation — real-world encounter

While running the agent against bedrock/global.anthropic.claude-opus-4-7, the retry loop logged:

❌ Error on attempt 1/3 after 0.63s (elapsed: 0.63s): RuntimeError: Unexpected event order, got error before "message_start"
📝 Error:

The 📝 Error: line was empty. That RuntimeError comes from anthropic/lib/streaming/_messages.py:454 when Bedrock returns an error event as the first stream event — the SDK's accumulator rejects it without forwarding the payload. There are several other paths with the same symptom:

  • Bare raise AssertionError() (no message).
  • Bare raise with no argument.
  • Third-party SDK guards that raise empty exceptions.
  • Any code doing raise RuntimeError() for flow control.

In all cases, today the user sees 📝 Error: followed by nothing — forcing them to attach a debugger to learn where it came from.

Fix

In AIAgent._summarize_api_error(), when str(error).strip() is empty, format the last traceback frame:

if not raw.strip():
    import traceback as _tb
    tb = getattr(error, "__traceback__", None)
    if tb is not None:
        frames = _tb.extract_tb(tb)
        if frames:
            last = frames[-1]
            return (
                f"{type(error).__name__} at {last.filename}:{last.lineno} "
                f"in {last.name}() — {last.line or '(no source)'}"
            )
    return f"{type(error).__name__} (no message, no traceback)"

Now the display becomes:

📝 Error: RuntimeError at /path/venv/.../anthropic/lib/streaming/_messages.py:454 in _process_event() — raise RuntimeError(f'Unexpected event order...')

…which is immediately actionable.

Which type of PR is this?

  • Bug fix (non-breaking change which fixes an issue)
  • Documentation update (none)

How has this been tested?

New test file tests/agent/test_summarize_api_error.py:

pytest tests/agent/test_summarize_api_error.py -v
# 4 passed in 4.88s

Covers:

  1. Bare AssertionError() — shows frame location + source line.
  2. Empty-message error with no traceback — falls back to "<Type> (no message, no traceback)".
  3. Whitespace-only message — treated as empty, frame shown.
  4. Non-empty message — fall-through unchanged (regression guard).

Risk

  • Zero impact on non-empty errors: not raw.strip() short-circuits immediately.
  • traceback is stdlib, imported lazily inside the guarded branch.
  • getattr(error, "__traceback__", None) handles exceptions constructed but never raised.
  • No behaviour change for any existing caller.

Related

Sibling PRs addressing Bedrock/Anthropic runtime robustness:

A follow-up PR will intercept the specific "Unexpected event order" RuntimeError at the Bedrock adapter layer and re-raise it with the discarded payload — this PR is the universal safety net that also catches the many other empty-error paths in the codebase.

Checklist

  • My changes follow the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (N/A — internal)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

…📝 Error:`

## Problem

When the retry loop's `_summarize_api_error()` receives an exception
with an empty `str(error)` (bare `raise AssertionError()`, bare `raise`
without args, or third-party SDK accumulators that discard the original
payload), users see:

    📝 Error:

...with nothing after the colon, and no way to diagnose the root cause
without re-running under a debugger.

## Real-world trigger

The anthropic SDK's streaming accumulator raises:

    RuntimeError('Unexpected event order, got error before "message_start"')

when Bedrock or Anthropic returns a service-level `error` event as the
first stream event (throttling, overload, 5xx, etc.).  The SDK has the
error payload in hand but throws it away before raising — all the user
sees is the cryptic "event order" message with zero context.  Even
worse, bare `AssertionError()`s in user-authored code paths often have
empty messages entirely.

## Fix

When the error's string representation is empty or whitespace-only,
walk `error.__traceback__` and format the last frame as:

    AssertionError at /path/file.py:123 in some_method() — source line

This gives users an actionable locator without changing the output for
any error with a real message.

## Changes

- `run_agent.py` — `_summarize_api_error()`: added empty-payload guard
  at the top of the function before the existing Cloudflare-HTML path.
- `tests/agent/test_summarize_api_error.py` — 4 new tests covering
  empty AssertionError, empty-message RuntimeError without traceback,
  whitespace-only message, and the fall-through path (non-empty errors
  must not be affected).

## Tests

    pytest tests/agent/test_summarize_api_error.py -v
    # 4 passed in 4.88s

## Risk

- Zero impact on any error with a non-empty message — the new branch
  only fires when `str(error).strip()` is falsy.
- `traceback` is a stdlib module; import is lazy (inside the branch).
- `getattr(error, "__traceback__", None)` is the documented API and
  handles manually-constructed exceptions that were never raised.

Signed-off-by: Andre Kurait <andrekurait@gmail.com>
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Apr 23, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for addressing an actionable diagnostics gap. The underlying blank-summary behavior is still present on current main: run_agent.py:2192-2195 returns an empty string for a bare empty-message exception, and agent/conversation_loop.py:2954-2971 renders that directly after 📝 Error:.

Problems

  • The proposed early fallback would now bypass current main's provider-payload recovery at run_agent.py:2171-2190. That path handles an empty SDK message by extracting and redacting response.text (added in 093f567f0), so traceback formatting must be the final fallback rather than the first branch after raw = str(error).

Suggested changes

  • Place the traceback/type fallback immediately before the final raw-string fallback at run_agent.py:2192.
  • Merge the regression coverage into tests/run_agent/test_summarize_api_error.py and add an empty-message error with useful response.text, asserting the response payload still wins.

This is an automated hermes-sweeper review.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants