Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
# This job builds the renderer and the electron bundle, then drives a real
# Electron app under xvfb. vite, tsc and the Playwright workers all scale
# with the core count.
runs-on: ubuntu-latest-32-core
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
review_status: ${{ steps.review-status.outputs.review_status }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
# cargo builds codegen units and test binaries in parallel across the
# cores. This lane also builds the crate from the start when Cargo.toml
# changes.
runs-on: ubuntu-latest-32-core
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests-os.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
runner: macos-latest
marker: macos_only
- name: Windows-only tests
runner: windows-latest-32-core
runner: windows-latest
marker: windows_only
steps:
- name: Checkout code
Expand Down
14 changes: 7 additions & 7 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,16 @@ def _ra():

AGENT_RUNTIME_POST_HOOK_TOOL_NAMES = frozenset(
{
"todo",
"todo_list",
"session_search",
"memory",
"clarify",
"read_terminal",
"read_preview",
"desktop_preview",
"drive_preview",
"annotate_preview",
"read_window_below",
"setup_mcp",
"tour",
"gui_tour",
"delegate_task",
"compact_context",
Expand All @@ -133,6 +130,9 @@ def agent_runtime_owns_post_tool_hook(agent: Any, function_name: str) -> bool:
"""Return True when an agent-level tool path emits its own post hook."""
if function_name in AGENT_RUNTIME_POST_HOOK_TOOL_NAMES:
return True
from model_tools import _LEGACY_TOOL_ALIASES as _lta
if _lta.get(function_name, function_name) in AGENT_RUNTIME_POST_HOOK_TOOL_NAMES:
return True
if getattr(agent, "_context_engine_tool_names", None) and function_name in agent._context_engine_tool_names:
return True
memory_manager = getattr(agent, "_memory_manager", None)
Expand Down Expand Up @@ -3746,7 +3746,7 @@ def _finish_agent_tool(result: Any, observed_args: Optional[dict] = None) -> Any
pass
return result

if function_name == "todo_list":
if function_name in ("todo_list", "todo"):
def _execute(next_args: dict) -> Any:
from tools.todo_tool import todo_tool as _todo_tool
return _finish_agent_tool(
Expand Down Expand Up @@ -3832,11 +3832,11 @@ def _execute(next_args: dict) -> Any:
),
next_args,
)
elif function_name == "desktop_preview":
elif function_name in ("desktop_preview", "read_preview"):
def _execute(next_args: dict) -> Any:
# action=read needs the GUI callback (agent-level); open/close go
# through the registry handler like any other tool.
if (next_args.get("action") or "").strip() == "read":
if function_name == "read_preview" or (next_args.get("action") or "").strip() == "read":
from tools.read_preview_tool import read_preview_tool as _read_preview_tool
return _finish_agent_tool(
_read_preview_tool(
Expand Down Expand Up @@ -3888,7 +3888,7 @@ def _execute(next_args: dict) -> Any:
),
next_args,
)
elif function_name == "gui_tour":
elif function_name in ("gui_tour", "tour"):
def _execute(next_args: dict) -> Any:
from tools.tour_tool import tour_tool as _tour_tool
return _finish_agent_tool(
Expand Down
4 changes: 3 additions & 1 deletion cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1849,7 +1849,7 @@ def _setup_worktree(repo_root: str = None, sync_base: bool = True,
import subprocess

from hermes_cli._subprocess_compat import (
noninteractive_git_env as _noninteractive_git_env,
noninteractive_git_env,
)

repo_root = repo_root or _git_repo_root()
Expand Down Expand Up @@ -2842,6 +2842,8 @@ def _prune_stale_worktrees(repo_root: str, max_age_hours: int = 24) -> None:
import subprocess
import time

from hermes_cli._subprocess_compat import noninteractive_git_env

worktrees_dir = Path(repo_root) / ".worktrees"
if not worktrees_dir.exists():
_prune_orphaned_branches(repo_root)
Expand Down
4 changes: 2 additions & 2 deletions hermes_cli/_subprocess_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ def noninteractive_git_env(
env[f"GIT_CONFIG_KEY_{idx}"] = key
env[f"GIT_CONFIG_VALUE_{idx}"] = value

env["GIT_CONFIG_PARAMETERS"] = "'core.fsmonitor=false' 'core.hooksPath=/dev/null'"

return env


Expand Down Expand Up @@ -694,8 +696,6 @@ def bounded_probe_run(
launcher instead of orphaning them.
"""
_popen_kwargs: dict = {"creationflags": windows_hide_flags()} if IS_WINDOWS else {"process_group": 0}
if env is not None:
_popen_kwargs["env"] = dict(env)
try:
proc = subprocess.Popen(
list(argv),
Expand Down
154 changes: 67 additions & 87 deletions hermes_cli/config_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,8 +799,9 @@ def _migrate_to_37(results: Dict[str, Any], quiet: bool) -> None:


def _migrate_to_38(results: Dict[str, Any], quiet: bool) -> None:
# Version 37 → 38: the bundled observability/nemo_relay plugin was
# removed when Relay lifecycle ownership moved into the agent core.
# ── Version 37 → 38: Relay cutover & legacy toolset names rewrite ──
# Part 1: the bundled observability/nemo_relay plugin was removed when
# Relay lifecycle ownership moved into the agent core.
_c = _cfg()
read_raw_config = _c.read_raw_config
_persist_migration = _c._persist_migration
Expand All @@ -809,24 +810,73 @@ def _migrate_to_38(results: Dict[str, Any], quiet: bool) -> None:

config = read_raw_config()
plugins = config.get("plugins")
if not isinstance(plugins, dict):
if isinstance(plugins, dict):
enabled = plugins.get("enabled")
removed = legacy_relay_plugin_keys(enabled)
if removed and isinstance(enabled, list):
plugins["enabled"] = [value for value in enabled if value not in removed]
config["plugins"] = plugins
_persist_migration(config)
message = (
"Removed legacy Relay plugin from plugins.enabled: "
f"{', '.join(removed)}. Configure native Relay plugins with "
"HERMES_NEMO_RELAY_PLUGINS_TOML."
)
results.setdefault("warnings", []).append(message)
if not quiet:
print(f" ⚠ {message}")

# Part 2: rewrite legacy toolset names in tools.<platform>
try:
from toolsets import TOOLSETS
except Exception:
return
enabled = plugins.get("enabled")
removed = legacy_relay_plugin_keys(enabled)
if not removed or not isinstance(enabled, list):

config = read_raw_config()
raw_tools = config.get("tools")
if not isinstance(raw_tools, dict):
return

plugins["enabled"] = [value for value in enabled if value not in removed]
config["plugins"] = plugins
_persist_migration(config)
message = (
"Removed legacy Relay plugin from plugins.enabled: "
f"{', '.join(removed)}. Configure native Relay plugins with "
"HERMES_NEMO_RELAY_PLUGINS_TOML."
)
results["warnings"].append(message)
if not quiet:
print(f" ⚠ {message}")
def _rewrite(names, platform):
if not isinstance(names, list):
return None
changed = []
for name in names:
if not isinstance(name, str):
changed.append(name)
continue
if name in TOOLSETS:
changed.append(name)
continue
suggestion = f"hermes-{platform}"
if name == "messaging" and suggestion in TOOLSETS:
changed.append(suggestion)
continue
# Unknown and unresolvable — drop (the resolver ignored it
# anyway; keeping it just re-fires the startup warning).
continue
return changed if changed != list(names) else None

any_change = False
for platform, block in raw_tools.items():
if not isinstance(platform, str) or not isinstance(block, dict):
continue
for key in ("enabled", "disabled"):
rewritten = _rewrite(block.get(key), platform)
if rewritten is not None:
block[key] = rewritten
any_change = True
results.setdefault("config_added", []).append(
f"tools.{platform}.{key}: legacy toolset names rewritten"
)
if any_change:
_persist_migration(config)
if not quiet:
print(
" ✓ Rewrote legacy toolset names (messaging → hermes-*) in "
"tools.<platform> lists — platform toolset picks that silently "
"degraded after the rename now resolve again."
)


def _migrate_to_39(results: Dict[str, Any], quiet: bool) -> None:
Expand Down Expand Up @@ -885,79 +935,9 @@ def _migrate_to_40(results: Dict[str, Any], quiet: bool) -> None:
results["config_added"].append("model_catalog.ttl_hours 1 → ttl_minutes 20 (default)")
if not quiet:
print(" ✓ Model catalog now refreshes every 20 minutes (model_catalog.ttl_minutes)")


#: Registry of (target_version, migration_fn), strictly ascending. The driver
#: applies every entry whose target version is greater than the on-disk
#: observe earlier steps' writes via read_raw_config() (filesystem state).
def _migrate_to_38(results: Dict[str, Any], quiet: bool) -> None:
# ── Version 37 → 38: rewrite legacy toolset names in tools.<platform> ──
# The toolset registry renamed the old generic names to per-platform
# `hermes-*` toolsets (messaging → hermes-telegram/hermes-cli, etc.).
# Existing configs still referencing the legacy names degrade silently:
# the platform toolset resolver drops the unknown entry and logs a
# warning at startup ("references unknown toolset"). Rewrite each
# legacy entry to the platform's own toolset when one exists
# (`messaging` under `telegram` → `hermes-telegram`), drop unknown
# per-platform spellings (`hermes-google_chat`, `hermes-teams` — those
# platforms ship no dedicated toolset), and keep everything valid
# untouched. Idempotent: a second run finds nothing to rewrite.
_c = _cfg()
read_raw_config = _c.read_raw_config
_persist_migration = _c._persist_migration

try:
from toolsets import TOOLSETS
except Exception:
return

config = read_raw_config()
raw_tools = config.get("tools")
if not isinstance(raw_tools, dict):
return

def _rewrite(names, platform):
if not isinstance(names, list):
return None
changed = []
for name in names:
if not isinstance(name, str):
changed.append(name)
continue
if name in TOOLSETS:
changed.append(name)
continue
suggestion = f"hermes-{platform}"
if name == "messaging" and suggestion in TOOLSETS:
changed.append(suggestion)
continue
# Unknown and unresolvable — drop (the resolver ignored it
# anyway; keeping it just re-fires the startup warning).
continue
return changed if changed != list(names) else None

any_change = False
for platform, block in raw_tools.items():
if not isinstance(platform, str) or not isinstance(block, dict):
continue
for key in ("enabled", "disabled"):
rewritten = _rewrite(block.get(key), platform)
if rewritten is not None:
block[key] = rewritten
any_change = True
results["config_added"].append(
f"tools.{platform}.{key}: legacy toolset names rewritten"
)
if any_change:
_persist_migration(config)
if not quiet:
print(
" ✓ Rewrote legacy toolset names (messaging → hermes-*) in "
"tools.<platform> lists — platform toolset picks that silently "
"degraded after the rename now resolve again."
)


MIGRATIONS: Tuple[Tuple[int, Callable[[Dict[str, Any], bool], None]], ...] = (
# v12 is the support floor: configs already AT v12 (or newer) still get
# every remaining step below. Only configs BELOW 12 are refused by the
Expand Down
4 changes: 4 additions & 0 deletions model_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1650,6 +1650,10 @@ def _return_bridge_result(result: Any) -> Any:
_probe_err = _ts_mod.validate_deferred_call_args(underlying_name, underlying_args)
if _probe_err is not None:
return _return_bridge_result(_probe_err)
try:
underlying_args = coerce_tool_args(underlying_name, underlying_args)
except Exception:
pass
from tools.registry import registry as _schema_registry
_ok, _type_err = _ts_mod.validate_tool_args(
underlying_name,
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ nemo-relay = false
numpy = false
onnxruntime = false
openai = false
opentelemetry-api = false
opentelemetry-exporter-otlp-proto-http = false
opentelemetry-sdk = false
openwakeword = false
Expand All @@ -535,6 +536,7 @@ pydantic = false
pyjwt = false
pytest = false
pytest-asyncio = false
pytest-timeout = false
python-dotenv = false
python-multipart = false
python-olm = false
Expand Down
8 changes: 7 additions & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2254,6 +2254,8 @@ def _spawn_background_review(
messages_snapshot=messages_snapshot,
review_memory=review_memory,
review_skills=review_skills,
correction_hint=correction_hint,
block_durable_writes=block_durable_writes,
focus=focus,
task_cfg=task_cfg,
)
Expand All @@ -2275,6 +2277,8 @@ def _spawn_background_review_now(
messages_snapshot: List[Dict],
review_memory: bool = False,
review_skills: bool = False,
correction_hint: Optional[Dict[str, Any]] = None,
block_durable_writes: bool = False,
focus: Optional[str] = None,
task_cfg: Optional[Dict[str, Any]] = None,
_requeue_attempts: int = 0,
Expand Down Expand Up @@ -2338,6 +2342,8 @@ def _target_with_requeue() -> None:
messages_snapshot=messages_snapshot,
review_memory=review_memory,
review_skills=review_skills,
correction_hint=correction_hint,
block_durable_writes=block_durable_writes,
focus=focus,
task_cfg=task_cfg,
_requeue_attempts=_requeue_attempts + 1,
Expand Down Expand Up @@ -8872,7 +8878,7 @@ def _anthropic_preserve_dots(self) -> bool:
"vertex",
}:
return True
base = self._base_url_lower
base = getattr(self, "_base_url_lower", None) or (getattr(self, "base_url", "") or "").lower()
host = base_url_hostname(base)
return (
"dashscope" in host
Expand Down
5 changes: 4 additions & 1 deletion scripts/dev-sandbox.sh
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,10 @@ if [ -n "$INSTALL_REF" ]; then
# Peel to ^{commit} in both cases: an annotated tag fetches as a tag OBJECT,
# and using it directly fails later with "trying to write non-commit object
# ... to branch 'refs/heads/main'".
if git -C "$UPSTREAM_REPO" fetch -q "$UPSTREAM_URL" "$INSTALL_REF" 2>/dev/null; then
if git -C "$GIT_ROOT" rev-parse --verify -q "$INSTALL_REF^{commit}" >/dev/null 2>&1 \
&& git -C "$UPSTREAM_REPO" fetch -q "$GIT_ROOT" "$INSTALL_REF" 2>/dev/null; then
UPSTREAM_COMMIT="$(git -C "$UPSTREAM_REPO" rev-parse "FETCH_HEAD^{commit}")"
elif git -C "$UPSTREAM_REPO" fetch -q "$UPSTREAM_URL" "$INSTALL_REF" 2>/dev/null; then
UPSTREAM_COMMIT="$(git -C "$UPSTREAM_REPO" rev-parse "FETCH_HEAD^{commit}")"
elif git -C "$UPSTREAM_REPO" fetch -q "$UPSTREAM_URL" refs/heads/main \
&& UPSTREAM_COMMIT="$(git -C "$UPSTREAM_REPO" rev-parse --verify -q "$INSTALL_REF^{commit}")"; then
Expand Down
7 changes: 5 additions & 2 deletions scripts/evolution_experience_harvest.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@
# from HERMES_HOME/scripts (outside the repo). The repo is the editable
# install source; its root must be on sys.path for `agent` imports.
_REPO_ROOT = "/root/hermes-agent-evolution"
if _REPO_ROOT not in sys.path and Path(_REPO_ROOT).is_dir():
sys.path.insert(0, _REPO_ROOT)
try:
if _REPO_ROOT not in sys.path and Path(_REPO_ROOT).is_dir():
sys.path.insert(0, _REPO_ROOT)
except OSError:
pass

try:
from agent.display import _detect_tool_failure # noqa: E402
Expand Down
Loading
Loading