mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
A 200 whose body failed used to log a DEBUG success line before the body was read: a failed call inside the execute envelope, or a body that does not parse, had no WARNING carrying the gateway request id. - The gateway client's _request returns the 2xx reply unchecked, with its request id and route. Each caller parses it through _checked, which writes the one outcome line: a failed parse or a failed execute call is a WARNING with the request id, success is DEBUG. - The portal client logs a 2xx in _parse for the same reason. - log_connector_outcome treats a line as a failure when it carries a code, not by status, so the portal tools route's 304 (a cache refresh) stays DEBUG.
136 lines
5.0 KiB
Python
136 lines
5.0 KiB
Python
"""Caller identity for requests to the first-party gateways: who is calling, and which turn asked.
|
|
|
|
``user_agent()`` goes on every request. The correlation headers name the conversation, turn and tool
|
|
call, so they go only to connector routes and only when the profile has opted in to sending
|
|
telemetry. The gateway strips every ``x-nous-`` header before it calls a vendor.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from typing import Mapping, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
__all__ = [
|
|
"CallRef",
|
|
"connector_headers",
|
|
"correlation_headers",
|
|
"log_connector_outcome",
|
|
"request_surface",
|
|
"response_request_id",
|
|
"user_agent",
|
|
]
|
|
|
|
SESSION_HEADER = "x-nous-hermes-session"
|
|
TURN_HEADER = "x-nous-hermes-turn"
|
|
TOOL_CALL_HEADER = "x-nous-hermes-tool-call"
|
|
REQUEST_ID_HEADER = "x-request-id"
|
|
|
|
# sys.platform -> the shared-metrics OS family.
|
|
_OS_FAMILY = {"darwin": "macos", "linux": "linux", "win32": "windows"}
|
|
|
|
# The account route answers 404 with this code until the vendor has created the account.
|
|
_EXPECTED_MISSES = frozenset({"connection_not_found"})
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CallRef:
|
|
"""The model's ids for the call a request serves. Absent fields are not sent."""
|
|
|
|
turn_id: Optional[str] = None
|
|
tool_call_id: Optional[str] = None
|
|
|
|
|
|
def _platform_name() -> str:
|
|
"""The session's platform: the running turn's agent platform, else the bound session vars.
|
|
|
|
Off a turn (a desktop page action, an account operation thread) only the session vars exist.
|
|
The classic CLI and one-shot runs bind neither, so they read as ``cli``."""
|
|
from agent.relay_runtime import current_turn
|
|
from gateway.session_context import get_session_env
|
|
from tools.connectors.gateway.config import session_platform
|
|
from utils import is_truthy_value
|
|
|
|
turn = current_turn()
|
|
if turn is not None and turn.lease.platform:
|
|
return turn.lease.platform
|
|
if platform := session_platform():
|
|
return platform
|
|
return "cron" if is_truthy_value(get_session_env("HERMES_CRON_SESSION", "")) else "cli"
|
|
|
|
|
|
def request_surface() -> str:
|
|
from hermes_cli.observability.shared_metrics_contract import execution_surface
|
|
|
|
surface = execution_surface({"execution_surface": _platform_name()})
|
|
return "other" if surface == "unknown" else surface
|
|
|
|
|
|
def user_agent() -> str:
|
|
from hermes_cli.version_info import get_version_info
|
|
from hermes_platform.host.facts import os_family
|
|
|
|
os_name = _OS_FAMILY.get(os_family(), "unknown")
|
|
return f"hermes-agent/{get_version_info().base_version} ({request_surface()}; {os_name})"
|
|
|
|
|
|
def _send_consented() -> bool:
|
|
from hermes_cli.config import read_raw_config_readonly
|
|
from hermes_cli.observability.shared_metrics_send_config import resolve_send_config
|
|
|
|
return resolve_send_config(read_raw_config_readonly() or {}).send is True
|
|
|
|
|
|
def _conversation_id() -> Optional[str]:
|
|
from agent.portal_tags import get_conversation_context
|
|
from gateway.session_context import get_session_env
|
|
|
|
return get_conversation_context() or get_session_env("HERMES_SESSION_ID", "") or None
|
|
|
|
|
|
def correlation_headers(call: Optional[CallRef] = None) -> dict[str, str]:
|
|
"""``x-request-id`` (fresh per HTTP attempt) and the conversation, turn and tool call ids, or
|
|
``{}`` when the profile has not opted in to sending telemetry."""
|
|
if not _send_consented():
|
|
return {}
|
|
headers = {REQUEST_ID_HEADER: str(uuid.uuid4())}
|
|
ids = {
|
|
SESSION_HEADER: _conversation_id(),
|
|
TURN_HEADER: call.turn_id if call else None,
|
|
TOOL_CALL_HEADER: call.tool_call_id if call else None,
|
|
}
|
|
headers.update({name: str(value) for name, value in ids.items() if value})
|
|
return headers
|
|
|
|
|
|
def connector_headers(call: Optional[CallRef] = None) -> dict[str, str]:
|
|
"""Every identity header for one connector HTTP attempt. Call it once per attempt."""
|
|
return {"User-Agent": user_agent(), **correlation_headers(call)}
|
|
|
|
|
|
def response_request_id(response: object) -> Optional[str]:
|
|
headers = getattr(response, "headers", None)
|
|
if not isinstance(headers, Mapping):
|
|
return None
|
|
value = headers.get(REQUEST_ID_HEADER) or headers.get("X-Request-Id")
|
|
return str(value) if value else None
|
|
|
|
|
|
def log_connector_outcome(
|
|
route: str, status: Optional[int], *, code: Optional[str] = None, request_id: Optional[str] = None,
|
|
call: Optional[CallRef] = None,
|
|
) -> None:
|
|
"""One line per connector HTTP outcome: failures at WARNING, success and expected misses at DEBUG.
|
|
|
|
The ``code`` makes a line a failure, not the status: a 200 whose body failed carries one, and a
|
|
304 that refreshed a cached list does not."""
|
|
level = logging.WARNING if code is not None and code not in _EXPECTED_MISSES else logging.DEBUG
|
|
logger.log(
|
|
level, "connector %s %s %s request_id=%s dispatch=%s",
|
|
route, status if status is not None else "transport", code or "-", request_id or "-",
|
|
(call.tool_call_id if call else None) or "-",
|
|
)
|