mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
The one-shot worker's credential mapping (main89096ba56c/5fd24f47fd: _single_query_exit_code(None, credentials_rate_limited=..., credentials_terminal=...)) moved to hermes_cli/turn_exit.turn_exit_code, but the managed path never passed the flags: a provider quota wall or revoked login during agent construction crashed the worker with no worker_result, so the dispatcher's submitter exited 1 and the card spent its failure budget (or retried into a dead credential) instead of EX_TEMPFAIL requeue / EX_CONFIG park. - turn_exit.credential_failure_flags(exc): the CLI's predicates (is_rate_limited_auth_error / AuthError.relogin_required), walking the cause chain because agent construction wraps the resolver error. - session_kanban.record_start_failure + managed_worker._construct_agent: book the mapped exit before the crash; run_worker_turns maps exceptions the same way. Unknown failures keep exit 1. Also: web_routers/display.py annotated _open_rfb with Path without a module-level import (only a function-local one); import it at module scope. (cherry picked from commit 7363a92d60f312d8670cbd44115b910262d13a68)
60 lines
2.7 KiB
Python
60 lines
2.7 KiB
Python
"""Turn-result exit policy shared by CLI views and authority-owned workers.
|
|
|
|
Worker identity is supplied by the caller, not inferred from a daemon's environment.
|
|
"""
|
|
|
|
# Provider walls do not spend a Kanban task's retry budget.
|
|
TRANSIENT_PROVIDER_REASONS = frozenset({
|
|
"rate_limit", "upstream_rate_limit", "billing", "overloaded", "server_error", "timeout",
|
|
})
|
|
# These failures need an operator change; the dispatcher parks the task immediately.
|
|
TERMINAL_PROVIDER_REASONS = frozenset({
|
|
"auth", "auth_permanent", "model_not_found", "ssl_cert_verification", "upstream_blocked",
|
|
})
|
|
|
|
|
|
def turn_exit_code(
|
|
result, *, kanban_worker: bool, credentials_rate_limited: bool = False,
|
|
credentials_terminal: bool = False,
|
|
transient_reasons=TRANSIENT_PROVIDER_REASONS, terminal_reasons=TERMINAL_PROVIDER_REASONS,
|
|
) -> int:
|
|
"""Preserve completion, interruption and retryable/terminal provider outcomes."""
|
|
if not isinstance(result, dict):
|
|
if credentials_rate_limited and kanban_worker:
|
|
from hermes_cli.kanban_db import KANBAN_RATE_LIMIT_EXIT_CODE
|
|
return KANBAN_RATE_LIMIT_EXIT_CODE
|
|
if credentials_terminal and kanban_worker:
|
|
from hermes_cli.kanban_db import KANBAN_TERMINAL_PROVIDER_EXIT_CODE
|
|
return KANBAN_TERMINAL_PROVIDER_EXIT_CODE
|
|
return 1
|
|
if result.get("interrupted"):
|
|
return 130
|
|
if not (result.get("failed") or result.get("partial") or result.get("completed") is False):
|
|
return 0
|
|
if kanban_worker:
|
|
reason = result.get("failure_reason")
|
|
if reason in transient_reasons:
|
|
from hermes_cli.kanban_db import KANBAN_RATE_LIMIT_EXIT_CODE
|
|
return KANBAN_RATE_LIMIT_EXIT_CODE
|
|
if reason in terminal_reasons:
|
|
from hermes_cli.kanban_db import KANBAN_TERMINAL_PROVIDER_EXIT_CODE
|
|
return KANBAN_TERMINAL_PROVIDER_EXIT_CODE
|
|
return 1
|
|
|
|
|
|
def credential_failure_flags(exc) -> dict:
|
|
"""``turn_exit_code`` credential kwargs for an exception raised before any turn result exists.
|
|
|
|
Same predicates as the one-shot CLI's ``_ensure_runtime_credentials``: a quota/rate-limit
|
|
``AuthError`` is transient, only an explicit re-authentication requirement is terminal. The
|
|
cause chain is walked because agent construction wraps the resolver's error."""
|
|
from hermes_cli.auth import AuthError, is_rate_limited_auth_error
|
|
seen = set()
|
|
while exc is not None and id(exc) not in seen:
|
|
seen.add(id(exc))
|
|
if isinstance(exc, AuthError):
|
|
return {"credentials_rate_limited": is_rate_limited_auth_error(exc),
|
|
"credentials_terminal": bool(exc.relogin_required)}
|
|
exc = exc.__cause__ or exc.__context__
|
|
return {}
|