refactor(hclib): remaining hermes_cli library modules — dead code, unified helpers, flattened branches
This commit is contained in:
+52
-96
@@ -6,6 +6,12 @@ import threading
|
||||
from contextlib import nullcontext
|
||||
from typing import Optional
|
||||
|
||||
from hermes_constants import (
|
||||
get_hermes_home_override,
|
||||
reset_hermes_home_override,
|
||||
set_hermes_home_override,
|
||||
)
|
||||
|
||||
_mcp_discovery_lock = threading.Lock()
|
||||
_mcp_discovery_started = False
|
||||
_mcp_discovery_thread: Optional[threading.Thread] = None
|
||||
@@ -18,8 +24,7 @@ def _has_configured_mcp_servers() -> bool:
|
||||
from hermes_cli.config import read_raw_config
|
||||
|
||||
raw_config = read_raw_config() or {}
|
||||
mcp_servers = raw_config.get("mcp_servers")
|
||||
if isinstance(mcp_servers, dict) and len(mcp_servers) > 0:
|
||||
if isinstance(raw_config.get("mcp_servers"), dict) and raw_config["mcp_servers"]:
|
||||
return True
|
||||
from hermes_cli.agent_plugins import has_enabled_agent_plugin_mcp
|
||||
|
||||
@@ -30,13 +35,18 @@ def _has_configured_mcp_servers() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _any_mcp_connected() -> bool:
|
||||
from tools.mcp_tool import get_mcp_status
|
||||
|
||||
return any(entry.get("connected") for entry in (get_mcp_status() or []))
|
||||
|
||||
|
||||
def start_background_mcp_discovery(*, logger, thread_name: str) -> None:
|
||||
"""Spawn one shared background MCP discovery thread for this process.
|
||||
|
||||
If the first background discovery run exits without connecting any MCP
|
||||
server (for example after startup cancellation / OOM restart), later calls
|
||||
are allowed to retry instead of permanently pinning the process in a
|
||||
"discovery already started" state with zero MCP tools.
|
||||
If the first background discovery run exits without connecting any MCP server (for example after
|
||||
startup cancellation / OOM restart), later calls are allowed to retry instead of permanently
|
||||
pinning the process in a "discovery already started" state with zero MCP tools.
|
||||
"""
|
||||
global _mcp_discovery_started, _mcp_discovery_thread
|
||||
|
||||
@@ -46,10 +56,7 @@ def start_background_mcp_discovery(*, logger, thread_name: str) -> None:
|
||||
if thread is not None and thread.is_alive():
|
||||
return
|
||||
try:
|
||||
from tools.mcp_tool import get_mcp_status
|
||||
|
||||
status = get_mcp_status() or []
|
||||
if any(entry.get("connected") for entry in status):
|
||||
if _any_mcp_connected():
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
@@ -71,27 +78,14 @@ def start_background_mcp_discovery(*, logger, thread_name: str) -> None:
|
||||
# "switched" to profile X would discover the LAUNCH profile's
|
||||
# mcp_servers instead (#67605). The config gate above already runs on
|
||||
# the caller's thread, so it sees the same override.
|
||||
try:
|
||||
from hermes_constants import get_hermes_home_override
|
||||
|
||||
home_override = get_hermes_home_override()
|
||||
except Exception:
|
||||
home_override = None
|
||||
home_override = get_hermes_home_override()
|
||||
|
||||
def _discover() -> None:
|
||||
token = None
|
||||
try:
|
||||
from hermes_constants import set_hermes_home_override
|
||||
|
||||
token = set_hermes_home_override(home_override)
|
||||
except Exception:
|
||||
token = None
|
||||
token = set_hermes_home_override(home_override)
|
||||
try:
|
||||
_discover_mcp_tools_without_interactive_oauth()
|
||||
try:
|
||||
from tools.mcp_tool import get_mcp_status
|
||||
status = get_mcp_status() or []
|
||||
if not any(entry.get("connected") for entry in status):
|
||||
if not _any_mcp_connected():
|
||||
logger.warning(
|
||||
"Background MCP discovery completed with zero connected servers"
|
||||
)
|
||||
@@ -100,15 +94,9 @@ def start_background_mcp_discovery(*, logger, thread_name: str) -> None:
|
||||
except Exception:
|
||||
logger.debug("Background MCP tool discovery failed", exc_info=True)
|
||||
finally:
|
||||
if token is not None:
|
||||
try:
|
||||
from hermes_constants import reset_hermes_home_override
|
||||
|
||||
reset_hermes_home_override(token)
|
||||
except Exception:
|
||||
pass
|
||||
reset_hermes_home_override(token)
|
||||
with _mcp_discovery_lock:
|
||||
global _mcp_discovery_thread, _mcp_discovery_started
|
||||
global _mcp_discovery_thread
|
||||
_mcp_discovery_thread = None
|
||||
|
||||
thread = threading.Thread(
|
||||
@@ -125,18 +113,9 @@ def _resolve_discovery_timeout(
|
||||
) -> float:
|
||||
"""Resolve the MCP discovery wait bound: explicit arg > config > default.
|
||||
|
||||
Reads ``mcp_discovery_timeout`` from config.yaml, defaulting to the value in
|
||||
``DEFAULT_CONFIG`` (single source of truth) when the key is absent. Kept lazy
|
||||
and fail-safe — a missing/invalid value or a broken config falls back to a
|
||||
short safe bound so startup can never hang or crash.
|
||||
|
||||
When ``single_query`` is True (``hermes -z "..."`` / ``-q``), the larger
|
||||
``mcp_single_query_discovery_timeout`` bound is used instead. In single-query
|
||||
mode there is only ONE turn, so the between-turns late-binding refresh never
|
||||
runs — a server that misses the small interactive bound would be invisible to
|
||||
the LLM for the whole session. The wait still returns the instant discovery
|
||||
completes (see ``wait_for_mcp_discovery``), so fast servers pay ~0s; the
|
||||
larger bound only caps how long a genuinely slow cold-start may block.
|
||||
Reads ``mcp_discovery_timeout`` from config.yaml, defaulting to the value in ``DEFAULT_CONFIG``
|
||||
(single source of truth) when the key is absent. Kept lazy and fail-safe — a missing/invalid
|
||||
value or a broken config falls back to a short safe bound so startup can never hang or crash.
|
||||
"""
|
||||
if explicit is not None:
|
||||
return explicit
|
||||
@@ -150,14 +129,13 @@ def _resolve_discovery_timeout(
|
||||
from hermes_cli.config import load_config, DEFAULT_CONFIG
|
||||
|
||||
default = float(DEFAULT_CONFIG.get(key, fallback))
|
||||
try:
|
||||
raw = (load_config() or {}).get(key, default)
|
||||
val = float(raw)
|
||||
return val if val > 0 else default
|
||||
except Exception:
|
||||
return default
|
||||
except Exception:
|
||||
return fallback
|
||||
try:
|
||||
val = float((load_config() or {}).get(key, default))
|
||||
return val if val > 0 else default
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _discover_mcp_tools_without_interactive_oauth() -> None:
|
||||
@@ -176,13 +154,10 @@ def _discover_mcp_tools_without_interactive_oauth() -> None:
|
||||
def defer_background_mcp_discovery(*, logger, thread_name: str, delay: float) -> None:
|
||||
"""Arm ``start_background_mcp_discovery`` to run ``delay`` seconds from now.
|
||||
|
||||
Used by the Desktop ``serve`` backend after its socket is announced: the
|
||||
discovery thread's first act is the ~350ms ``mcp`` SDK import, which holds
|
||||
the GIL against the renderer's connect + first hydration reads if it starts
|
||||
at bind time, and against the web_server import if it starts before. Any
|
||||
consumer that needs discovery sooner (``wait_for_mcp_discovery`` from an
|
||||
agent build) fires the deferred start immediately, so the bounded join and
|
||||
the late-binding refresh behave exactly as if it had been started eagerly.
|
||||
Used by the Desktop ``serve`` backend after its socket is announced: the discovery thread's
|
||||
first act is the ~350ms ``mcp`` SDK import, which holds the GIL against the renderer's connect +
|
||||
first hydration reads if it starts at bind time, and against the web_server import if it starts
|
||||
before.
|
||||
"""
|
||||
global _mcp_discovery_deferred
|
||||
with _mcp_discovery_lock:
|
||||
@@ -217,16 +192,13 @@ def wait_for_mcp_discovery(
|
||||
) -> None:
|
||||
"""Wait for background MCP discovery before the first tool snapshot.
|
||||
|
||||
``thread.join(timeout)`` returns the INSTANT discovery completes, so this
|
||||
only ever blocks for the real connect time of a still-pending server —
|
||||
users with no MCP servers or fast servers pay ~0s. The bound (from
|
||||
``mcp_discovery_timeout`` in config) just caps the wait so a dead server
|
||||
can't freeze startup; servers that miss it are picked up by the automatic
|
||||
late-binding refresh.
|
||||
``thread.join(timeout)`` returns the INSTANT discovery completes, so this only ever blocks for
|
||||
the real connect time of a still-pending server — users with no MCP servers or fast servers pay
|
||||
~0s.
|
||||
|
||||
When ``single_query`` is True, the bound comes from
|
||||
``mcp_single_query_discovery_timeout`` instead (default 15s vs 1.5s
|
||||
interactive) because one-shot sessions have no second turn to recover.
|
||||
When ``single_query`` is True, the bound comes from ``mcp_single_query_discovery_timeout``
|
||||
instead (default 15s vs 1.5s interactive) because one-shot sessions have no second turn to
|
||||
recover.
|
||||
"""
|
||||
_start_deferred_mcp_discovery_now()
|
||||
thread = _mcp_discovery_thread
|
||||
@@ -238,13 +210,9 @@ def wait_for_mcp_discovery(
|
||||
def mcp_discovery_in_flight() -> bool:
|
||||
"""Return True if THIS module's background discovery thread is still running.
|
||||
|
||||
Mirrors ``tui_gateway.entry.mcp_discovery_in_flight`` for the surfaces that
|
||||
start discovery through ``start_background_mcp_discovery`` here (the desktop
|
||||
app + dashboard WebSocket sidecar via ``tui_gateway/ws.py``, and
|
||||
``hermes dashboard``). Those processes populate THIS module's
|
||||
``_mcp_discovery_thread``, not ``tui_gateway.entry``'s, so the late-refresh
|
||||
scheduler must consult both to decide whether a slow server's tools are
|
||||
still pending (see #51587).
|
||||
Mirrors ``tui_gateway.entry.mcp_discovery_in_flight`` for surfaces that start discovery here
|
||||
(desktop app, dashboard WebSocket sidecar, ``hermes dashboard``). Those populate THIS module's
|
||||
thread, so the late-refresh scheduler must consult both to know if a slow server is pending.
|
||||
"""
|
||||
thread = _mcp_discovery_thread
|
||||
return thread is not None and thread.is_alive()
|
||||
@@ -253,10 +221,9 @@ def mcp_discovery_in_flight() -> bool:
|
||||
def join_mcp_discovery(timeout: "float | None" = None) -> bool:
|
||||
"""Block until THIS module's background discovery finishes, up to ``timeout``.
|
||||
|
||||
Returns True if discovery has completed (thread absent or no longer alive),
|
||||
False if it is still running after the timeout. Unlike
|
||||
``wait_for_mcp_discovery`` this accepts an unbounded/long wait and reports
|
||||
the outcome, for the off-critical-path late-refresh waiter.
|
||||
Returns True once discovery has completed, False if still running after the timeout. Unlike
|
||||
``wait_for_mcp_discovery`` this accepts a long wait and reports the outcome, for the
|
||||
off-critical-path late-refresh waiter.
|
||||
"""
|
||||
thread = _mcp_discovery_thread
|
||||
if thread is None:
|
||||
@@ -274,24 +241,13 @@ def ensure_mcp_discovery_before_agent_build(
|
||||
) -> None:
|
||||
"""Give configured MCP tools a bounded chance to register before AIAgent.
|
||||
|
||||
Non-interactive first turns (``chat -q``, ``hermes -z``) can construct
|
||||
``AIAgent`` before the normal banner or tool-list paths touch
|
||||
``get_tool_definitions()``. Because the agent snapshots its tool
|
||||
registry at construction time, the first and only model turn can miss
|
||||
native ``mcp__...`` tools even when the MCP server is healthy.
|
||||
Non-interactive first turns (``chat -q``, ``hermes -z``) can construct ``AIAgent`` before the
|
||||
normal banner or tool-list paths touch ``get_tool_definitions()``.
|
||||
|
||||
``wait_for_mcp_discovery()`` only joins an already-created discovery
|
||||
thread, so it no-ops if a direct/single-query path reaches agent
|
||||
construction before MCP startup created that thread. This helper makes
|
||||
the construction site self-sufficient: start discovery if needed, then
|
||||
wait up to the configured bound.
|
||||
|
||||
When ``single_query`` is True, the larger
|
||||
``mcp_single_query_discovery_timeout`` bound is used (default 15s vs 1.5s
|
||||
interactive) because one-shot sessions have no second turn to recover.
|
||||
|
||||
Failures are swallowed so a broken MCP config never aborts agent
|
||||
construction — the agent runs without MCP tools, same as before.
|
||||
``wait_for_mcp_discovery()`` only joins an already-created discovery thread, so it no-ops if a
|
||||
direct/single-query path reaches agent construction before MCP startup created that thread. This
|
||||
helper makes the construction site self-sufficient: start discovery if needed, then wait up to
|
||||
the configured bound.
|
||||
"""
|
||||
try:
|
||||
start_background_mcp_discovery(
|
||||
|
||||
Reference in New Issue
Block a user