mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
## 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>
62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
"""Tests for AIAgent._summarize_api_error — the error-line formatter
|
|
used by the retry loop's `📝 Error:` display.
|
|
|
|
Regression tests for a gap where exceptions with empty string payloads
|
|
(bare `raise`, bare `assert`, or third-party SDK assertions that don't
|
|
carry the original error message) would surface as `📝 Error:` with no
|
|
content, forcing users to re-run under a debugger to see what failed.
|
|
"""
|
|
from run_agent import AIAgent
|
|
|
|
|
|
def _raise_and_catch(exc: Exception) -> Exception:
|
|
"""Raise and immediately re-catch so the exception has a real traceback."""
|
|
try:
|
|
raise exc
|
|
except type(exc) as caught:
|
|
return caught
|
|
|
|
|
|
class TestSummarizeApiErrorEmptyMessage:
|
|
"""Empty-message errors still surface actionable context."""
|
|
|
|
def test_bare_assertion_error_shows_traceback_frame(self):
|
|
# `raise AssertionError()` produces a truly empty-message error
|
|
# (unlike `assert False` which Python annotates with source).
|
|
def _trigger():
|
|
raise AssertionError()
|
|
|
|
try:
|
|
_trigger()
|
|
except AssertionError as e:
|
|
result = AIAgent._summarize_api_error(e)
|
|
|
|
assert "AssertionError" in result
|
|
assert "_trigger" in result
|
|
assert ".py:" in result
|
|
|
|
def test_empty_raise_shows_type_only_when_no_traceback(self):
|
|
# Construct an exception without raising it — no __traceback__
|
|
err = RuntimeError("")
|
|
result = AIAgent._summarize_api_error(err)
|
|
|
|
assert "RuntimeError" in result
|
|
assert "no message" in result
|
|
|
|
def test_whitespace_only_message_treated_as_empty(self):
|
|
err = _raise_and_catch(RuntimeError(" \n\t "))
|
|
result = AIAgent._summarize_api_error(err)
|
|
|
|
assert "RuntimeError" in result
|
|
# Must include a frame locator — not just the empty payload
|
|
assert ".py:" in result or "no message" in result
|
|
|
|
def test_non_empty_message_falls_through_to_normal_path(self):
|
|
"""Non-empty errors must not be captured by the new branch."""
|
|
err = _raise_and_catch(RuntimeError("some real error"))
|
|
result = AIAgent._summarize_api_error(err)
|
|
|
|
# The normal path returns the raw string (possibly truncated),
|
|
# NOT the traceback-frame format introduced by this fix.
|
|
assert "some real error" in result
|
|
assert ".py:" not in result
|