mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
Follow-up to #59332 targeting the remaining PERCEIVED first-token latency
(the wire streaming was already per-token; these fix what the user sees):
1. display.show_reasoning default ON. On thinking models the reasoning
phase streams for tens of seconds; with the display off users stare
at a spinner the whole time and read it as a stall. Flipped in
DEFAULT_CONFIG, load_cli_config defaults, tui_gateway raw-YAML
fallbacks, and the hermes setup status line (all four read sites kept
in sync). Gateway per-platform defaults intentionally stay off —
messaging chats shouldn't fill with thinking text. /reasoning hide
still turns it off and persists.
2. Response box force-flushes long partial lines. _emit_stream_text only
painted on newline, so a response opening with a long paragraph
stayed invisible until the first \n — seconds of blank box. Now
partial lines wrap at terminal width and paint as tokens arrive
(mirrors the reasoning box's 80-char force-flush that existed since
day one). Table blocks remain batch-aligned; no content loss at wrap
boundaries (regression tests added).
3. hermes_time timezone resolution uses read_raw_config (mtime-cached +
libyaml C loader) instead of a raw yaml.safe_load of config.yaml
(~110-140ms measured) inside the FIRST system prompt build. First
build drops 320ms -> ~155ms on a 200-skill install.
4. Stale docs: configuration.md (en+zh) still documented the 70%/90%
[BUDGET WARNING] tool-result injections. Those were removed in April
2026 (c8aff7463) precisely because they hurt task completion; current
behavior is exhaustion-message + one grace call, no mid-loop
injection, no cache impact. Docs now describe reality.
Verified: token-count compression decisions already use API-reported
last_prompt_tokens (rough estimators are preflight-only and cost ~1.7ms
even on 1.7MB histories — not worth touching).
135 lines
4.4 KiB
Python
135 lines
4.4 KiB
Python
"""
|
|
Timezone-aware clock for Hermes.
|
|
|
|
Provides a single ``now()`` helper that returns a timezone-aware datetime
|
|
based on the user's configured IANA timezone (e.g. ``Asia/Kolkata``).
|
|
|
|
Resolution order:
|
|
1. ``HERMES_TIMEZONE`` environment variable
|
|
2. ``timezone`` key in ``~/.hermes/config.yaml``
|
|
3. Falls back to the server's local time (``datetime.now().astimezone()``)
|
|
|
|
Invalid timezone values log a warning and fall back safely — Hermes never
|
|
crashes due to a bad timezone string.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from datetime import datetime
|
|
from hermes_constants import get_config_path
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from zoneinfo import ZoneInfo
|
|
except ImportError:
|
|
# Python 3.8 fallback (shouldn't be needed — Hermes requires 3.9+)
|
|
from backports.zoneinfo import ZoneInfo # type: ignore[no-redef]
|
|
|
|
# Cached state — resolved once, reused on every call.
|
|
# Call reset_cache() to force re-resolution (e.g. after config changes).
|
|
_cached_tz: Optional[ZoneInfo] = None
|
|
_cached_tz_name: Optional[str] = None
|
|
_cache_resolved: bool = False
|
|
|
|
|
|
def _resolve_timezone_name() -> str:
|
|
"""Read the configured IANA timezone string (or empty string).
|
|
|
|
This does file I/O when falling through to config.yaml, so callers
|
|
should cache the result rather than calling on every ``now()``.
|
|
"""
|
|
# 1. Environment variable (highest priority — set by Supervisor, etc.)
|
|
tz_env = os.getenv("HERMES_TIMEZONE", "").strip()
|
|
if tz_env:
|
|
return tz_env
|
|
|
|
# 2. config.yaml ``timezone`` key
|
|
try:
|
|
# Prefer the shared cached raw-config reader (mtime/size-keyed cache +
|
|
# libyaml C loader) — a direct yaml.safe_load of a large config.yaml
|
|
# costs ~100ms+ and this used to run inside the FIRST system prompt
|
|
# build, on the time-to-first-token critical path.
|
|
try:
|
|
from hermes_cli.config import read_raw_config
|
|
cfg = read_raw_config() or {}
|
|
except Exception:
|
|
import yaml
|
|
config_path = get_config_path()
|
|
if config_path.exists():
|
|
with open(config_path, encoding="utf-8") as f:
|
|
cfg = yaml.safe_load(f) or {}
|
|
else:
|
|
cfg = {}
|
|
if cfg:
|
|
# Managed scope: an administrator can pin ``timezone`` too. Overlay
|
|
# via the shared helper (fail-open) since this reads config.yaml directly.
|
|
try:
|
|
from hermes_cli import managed_scope
|
|
cfg = managed_scope.apply_managed_overlay(cfg)
|
|
except Exception:
|
|
pass
|
|
tz_cfg = cfg.get("timezone", "")
|
|
if isinstance(tz_cfg, str) and tz_cfg.strip():
|
|
return tz_cfg.strip()
|
|
except Exception:
|
|
pass
|
|
|
|
return ""
|
|
|
|
|
|
def _get_zoneinfo(name: str) -> Optional[ZoneInfo]:
|
|
"""Validate and return a ZoneInfo, or None if invalid."""
|
|
if not name:
|
|
return None
|
|
try:
|
|
return ZoneInfo(name)
|
|
except (KeyError, Exception) as exc:
|
|
logger.warning(
|
|
"Invalid timezone '%s': %s. Falling back to server local time.",
|
|
name, exc,
|
|
)
|
|
return None
|
|
|
|
|
|
def get_timezone() -> Optional[ZoneInfo]:
|
|
"""Return the user's configured ZoneInfo, or None (meaning server-local).
|
|
|
|
Resolved once and cached. Call ``reset_cache()`` after config changes.
|
|
"""
|
|
global _cached_tz, _cached_tz_name, _cache_resolved
|
|
if not _cache_resolved:
|
|
_cached_tz_name = _resolve_timezone_name()
|
|
_cached_tz = _get_zoneinfo(_cached_tz_name)
|
|
_cache_resolved = True
|
|
return _cached_tz
|
|
|
|
|
|
def reset_cache() -> None:
|
|
"""Clear the cached timezone so the next call re-resolves it.
|
|
|
|
Call this after the configured timezone may have changed (e.g. after a
|
|
config edit or ``HERMES_TIMEZONE`` update) to force ``get_timezone()`` /
|
|
``now()`` to read the new value instead of the value cached at first use.
|
|
"""
|
|
global _cached_tz, _cached_tz_name, _cache_resolved
|
|
_cached_tz = None
|
|
_cached_tz_name = None
|
|
_cache_resolved = False
|
|
|
|
|
|
def now() -> datetime:
|
|
"""
|
|
Return the current time as a timezone-aware datetime.
|
|
|
|
If a valid timezone is configured, returns wall-clock time in that zone.
|
|
Otherwise returns the server's local time (via ``astimezone()``).
|
|
"""
|
|
tz = get_timezone()
|
|
if tz is not None:
|
|
return datetime.now(tz)
|
|
# No timezone configured — use server-local (still tz-aware)
|
|
return datetime.now().astimezone()
|
|
|
|
|