mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
Independent review findings on update-disabled plugins: - Recorded plugins are tried only after the enabled plugins have built, one at a time on top of them; an update that has to disable an enabled plugin re-enables nothing, so a download failure can never swap two plugins. - A resolver or build verdict records a fingerprint of what was tried (core's dependency inputs plus the plugin's) and is not built again until one side changes; a download failure is retried by the next update, and those go first because they fitted beside the rest before. - The update publishes against the config snapshot it decided from, and a rejoin needs the record to still be there, so a user's mid-update choice stands. - Recorded reasons keep their readable start, not the tail of a uv error. - An installed provider that fails to import is reported as such instead of running recovery, and the startup notices dedupe per kind.
83 lines
4.2 KiB
Python
83 lines
4.2 KiB
Python
"""Resolve the configured memory provider at agent init, telling the user once why it will not run."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger("run_agent")
|
|
|
|
|
|
# Deduped: the gateway builds a fresh AIAgent per message, so it would warn every turn.
|
|
_warned_unavailable_providers: set[tuple[str, str, str]] = set()
|
|
|
|
|
|
def _unavailable_warning_key(name: str, kind: str = "unavailable") -> tuple[str, str, str]:
|
|
"""Once per profile home, provider and kind of trouble: one multiplexed gateway/Desktop backend
|
|
serves several profiles, each one running without its memory must be told, and a provider the
|
|
user turns back on that is then unavailable is news even after the "turned off" notice."""
|
|
from hermes_constants import get_hermes_home, hermes_home_key
|
|
return hermes_home_key(get_hermes_home()), name, kind
|
|
|
|
|
|
def _warn_once(name: str, message: str, say=None, kind: str = "unavailable") -> None:
|
|
"""Log *message* and hand it to the user's sink once per home, provider and kind."""
|
|
key = _unavailable_warning_key(name, kind)
|
|
if key in _warned_unavailable_providers:
|
|
return
|
|
_warned_unavailable_providers.add(key)
|
|
logger.warning(message)
|
|
if say is not None:
|
|
say(message)
|
|
|
|
|
|
def _warn_memory_provider_unavailable(name: str, reason: str = "", say=None) -> None:
|
|
"""Warn once per home and provider that a configured memory provider is unavailable.
|
|
|
|
``is_available()`` is a side-effect-free hot-path check and can't log itself; without this
|
|
the provider is silently dropped. ``reason`` (the provider's ``unavailable_reason()`` hint)
|
|
can only reach the user here, so it is appended when present. *say* is the agent's
|
|
user-facing sink: a log line alone leaves the user running without memory unaware.
|
|
"""
|
|
_warn_once(name, (
|
|
f"⚠ Memory provider {name!r} is selected but reports unavailable — external memory "
|
|
"is disabled for this session (built-in memory still works). Check the "
|
|
"provider's credentials/config with 'hermes memory status'. Note: "
|
|
"systemd/gateway services do not inherit ~/.hermes/.env automatically; set "
|
|
f"any required variables in the service environment.{f' {reason}' if reason else ''}"
|
|
), say=say)
|
|
|
|
|
|
def resolve_memory_provider(name: str, say=None):
|
|
"""The configured provider ready to add, or None after telling the user why not (once per home)."""
|
|
from plugins.memory import find_provider_dir, load_memory_provider, parked_provider_notice
|
|
|
|
provider = load_memory_provider(name)
|
|
if provider is None:
|
|
parked = parked_provider_notice(name)
|
|
if parked:
|
|
# Installed but turned off (by the user or by an update): say so, never re-install it.
|
|
_warn_once(name, parked, say=say, kind="parked")
|
|
return None
|
|
if find_provider_dir(name) is None:
|
|
# The provider left core for the catalog (or was never installed): fetch it once.
|
|
from hermes_cli.memory_provider_migration import recover_at_startup
|
|
|
|
if recover_at_startup(name, say=say):
|
|
provider = load_memory_provider(name)
|
|
# Installed (now or before) yet not loading: neither "not installed" nor an install notice explains that.
|
|
if provider is None and find_provider_dir(name) is not None:
|
|
_warn_once(name, f"⚠ Memory provider '{name}' is installed but failed to load, so external memory is off "
|
|
"for this session (built-in memory still works). The log names the error; "
|
|
"`hermes memory status` shows the plugin's state.", say=say, kind="load")
|
|
if provider is None or provider.is_available():
|
|
return provider
|
|
if _unavailable_warning_key(name) not in _warned_unavailable_providers:
|
|
# unavailable_reason() reads config/probes importlib — skip it once warned.
|
|
reason = ""
|
|
try:
|
|
reason = provider.unavailable_reason()
|
|
except Exception: # health: allow BLE001 -- a provider's own hint must never break agent init
|
|
logger.debug("unavailable_reason() failed for %s", name, exc_info=True)
|
|
_warn_memory_provider_unavailable(name, reason, say=say)
|
|
return None
|