mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-31 06:51:29 +00:00
* fix(codex-responses): gracefully recover from invalid_encrypted_content (salvage #10144) When an OpenAI-compatible Responses API surface accepts an initial request but later rejects the replayed `codex_reasoning_items` encrypted blob with HTTP 400 `invalid_encrypted_content`, the session previously got stuck retrying the same poisoned payload. Recovery: classify the error as a dedicated FailoverReason, and on the first hit disable encrypted reasoning replay for the rest of the session, strip cached items from message history, and retry once. Changes: * error_classifier: add FailoverReason.invalid_encrypted_content branch in _classify_400 (before context_overflow so the messages that mention 'encrypted content … could not be verified' don't trip context heuristics), in _classify_by_error_code, and extend _extract_error_code to peek inside wrapped JSON in error.message and ignore the bare '400' as a code. * agent_init: initialize `_codex_reasoning_replay_enabled = True` on every agent. * run_agent: add AIAgent._disable_codex_reasoning_replay() helper that flips the flag and pops cached items. * codex_responses_adapter: thread a `replay_encrypted_reasoning` kwarg through _chat_messages_to_responses_input so that when the flag is False we don't replay codex_reasoning_items. * transports/codex.py: read `replay_encrypted_reasoning` from params, thread it into the adapter, and gate the `include=['reasoning.encrypted_content']` request hint on it. * chat_completion_helpers: pass the agent's replay flag through to the transport. * conversation_loop: in the retry loop, add an invalid_encrypted_content recovery branch that fires once per session, only when api_mode == codex_responses, only when replay is still enabled, and only when at least one assistant message in history actually carries cached reasoning items (otherwise the 400 has nothing to do with our cache and the normal retry path handles it). Tests: * test_error_classifier: new wrapped-JSON _extract_error_code case; new TestClassifyApiError cases proving the 400 is retryable with no fallback, that the broad message match doesn't catch a generic 'parsed' message, and that the error code match is case-insensitive. * test_run_agent_codex_responses: end-to-end test of the recovery branch firing once and disabling replay, plus a sibling test that proves the branch does *not* fire (and the flag stays True) when history has no cached reasoning items. Salvages PR #10144 onto the post-refactor module layout (error_classifier / codex_responses_adapter / transports/codex / conversation_loop / agent_init) since the original diff was written against the pre-refactor monolithic run_agent.py. * chore(release): map victorGPT in AUTHOR_MAP for #10144 salvage --------- Co-authored-by: victorGPT <wuxuebin1993@gmail.com>
This commit is contained in:
parent
9d3e9316f4
commit
b6ca56f651
10 changed files with 342 additions and 5 deletions
|
|
@ -50,6 +50,7 @@ class FailoverReason(enum.Enum):
|
|||
|
||||
# Request format
|
||||
format_error = "format_error" # 400 bad request — abort or strip + retry
|
||||
invalid_encrypted_content = "invalid_encrypted_content" # Responses replay blob rejected — strip replay state and retry
|
||||
multimodal_tool_content_unsupported = "multimodal_tool_content_unsupported" # Provider rejected list-type content in tool messages (e.g. Xiaomi MiMo) — downgrade to text and retry
|
||||
|
||||
# Provider-specific
|
||||
|
|
@ -865,6 +866,26 @@ def _classify_400(
|
|||
retryable=True,
|
||||
)
|
||||
|
||||
# Invalid encrypted reasoning replay blob (OpenAI Responses API). Must be
|
||||
# checked BEFORE context_overflow because some surfaces emit messages that
|
||||
# contain context-like phrasing ("encrypted content … could not be
|
||||
# verified") which could otherwise trip the context_overflow heuristics.
|
||||
# ``error_msg`` is lowercased upstream — match accordingly.
|
||||
error_code_lower = (error_code or "").lower()
|
||||
if (
|
||||
error_code_lower == "invalid_encrypted_content"
|
||||
or "invalid_encrypted_content" in error_msg
|
||||
or (
|
||||
"encrypted content for item" in error_msg
|
||||
and "could not be verified" in error_msg
|
||||
)
|
||||
):
|
||||
return result_fn(
|
||||
FailoverReason.invalid_encrypted_content,
|
||||
retryable=True,
|
||||
should_fallback=False,
|
||||
)
|
||||
|
||||
# Context overflow from 400
|
||||
if any(p in error_msg for p in _CONTEXT_OVERFLOW_PATTERNS):
|
||||
return result_fn(
|
||||
|
|
@ -974,6 +995,13 @@ def _classify_by_error_code(
|
|||
should_compress=True,
|
||||
)
|
||||
|
||||
if code_lower == "invalid_encrypted_content":
|
||||
return result_fn(
|
||||
FailoverReason.invalid_encrypted_content,
|
||||
retryable=True,
|
||||
should_fallback=False,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -1141,15 +1169,49 @@ def _extract_error_code(body: dict) -> str:
|
|||
"""Extract an error code string from the response body."""
|
||||
if not body:
|
||||
return ""
|
||||
|
||||
def _code_from_payload(payload) -> str:
|
||||
"""Extract a code/type from a nested error payload dict (defensive)."""
|
||||
if not isinstance(payload, dict):
|
||||
return ""
|
||||
payload_error = payload.get("error", {})
|
||||
if isinstance(payload_error, dict):
|
||||
nested = payload_error.get("code") or payload_error.get("type") or ""
|
||||
if isinstance(nested, str) and nested.strip() and nested.strip() != "400":
|
||||
return nested.strip()
|
||||
code = payload.get("code") or payload.get("error_code") or ""
|
||||
if isinstance(code, (str, int)):
|
||||
text = str(code).strip()
|
||||
if text and text != "400":
|
||||
return text
|
||||
return ""
|
||||
|
||||
error_obj = body.get("error", {})
|
||||
if isinstance(error_obj, dict):
|
||||
code = error_obj.get("code") or error_obj.get("type") or ""
|
||||
if isinstance(code, str) and code.strip():
|
||||
if isinstance(code, str) and code.strip() and code.strip() != "400":
|
||||
return code.strip()
|
||||
|
||||
# Some providers wrap the real JSON error body as a string inside
|
||||
# error.message — peek into it for a nested code (e.g. Responses API
|
||||
# surfaces ``invalid_encrypted_content`` this way).
|
||||
message = error_obj.get("message")
|
||||
if isinstance(message, str) and message.strip().startswith("{"):
|
||||
import json
|
||||
try:
|
||||
inner = json.loads(message)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
inner = None
|
||||
nested_code = _code_from_payload(inner)
|
||||
if nested_code:
|
||||
return nested_code
|
||||
|
||||
# Top-level code
|
||||
code = body.get("code") or body.get("error_code") or ""
|
||||
if isinstance(code, (str, int)):
|
||||
return str(code).strip()
|
||||
text = str(code).strip()
|
||||
if text and text != "400":
|
||||
return text
|
||||
return ""
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue