mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Merge 38dab6dff4 into 00c3d848d8
This commit is contained in:
commit
14c697fb3a
9 changed files with 53 additions and 13 deletions
|
|
@ -27,6 +27,7 @@ def build_write_denied_paths(home: str) -> set[str]:
|
|||
os.path.join(home, ".ssh", "id_ed25519"),
|
||||
os.path.join(home, ".ssh", "config"),
|
||||
str(hermes_home / ".env"),
|
||||
os.path.join(home, ".hermes", ".env"),
|
||||
os.path.join(home, ".bashrc"),
|
||||
os.path.join(home, ".zshrc"),
|
||||
os.path.join(home, ".profile"),
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@ _CATEGORY_MERGE: Dict[str, str] = {
|
|||
"human_delay": "display",
|
||||
"dashboard": "display",
|
||||
"code_execution": "agent",
|
||||
"prompt_caching": "compression",
|
||||
}
|
||||
|
||||
# Display order for tabs — unlisted categories sort alphabetically after these.
|
||||
|
|
|
|||
23
run_agent.py
23
run_agent.py
|
|
@ -12221,6 +12221,29 @@ class AIAgent:
|
|||
_has_structured
|
||||
and self._thinking_prefill_retries >= 2
|
||||
)
|
||||
if _truly_empty:
|
||||
_raw_chars = len(final_response or "")
|
||||
_has_think_blocks = bool(
|
||||
final_response
|
||||
and re.search(
|
||||
r'<(?:think|thinking|reasoning|thought|REASONING_SCRATCHPAD)\b',
|
||||
final_response,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
"Empty response diagnostics: finish_reason=%r "
|
||||
"raw_content_chars=%d has_think_blocks=%s "
|
||||
"structured_reasoning=%s tool_calls=%s model=%s "
|
||||
"raw_preview=%r",
|
||||
finish_reason,
|
||||
_raw_chars,
|
||||
_has_think_blocks,
|
||||
_has_structured,
|
||||
bool(getattr(assistant_message, "tool_calls", None)),
|
||||
self.model,
|
||||
(final_response or "")[:300],
|
||||
)
|
||||
if _truly_empty and (not _has_structured or _prefill_exhausted) and self._empty_content_retries < 3:
|
||||
self._empty_content_retries += 1
|
||||
logger.warning(
|
||||
|
|
|
|||
|
|
@ -969,6 +969,7 @@ class TestAgentCacheIdleResume:
|
|||
session_id="hard-session",
|
||||
)
|
||||
|
||||
import run_agent as _ra
|
||||
vm_calls: list = []
|
||||
# AIAgent.close() calls the ``cleanup_vm`` name bound into
|
||||
# ``run_agent`` at import time, not ``tools.terminal_tool.cleanup_vm``
|
||||
|
|
|
|||
|
|
@ -266,33 +266,33 @@ class TestFindAllSkillsFiltering:
|
|||
skills = _find_all_skills()
|
||||
assert not any(s["name"] == "my-skill" for s in skills)
|
||||
|
||||
@patch("agent.skill_utils.iter_skill_index_files")
|
||||
@patch("tools.skills_tool._get_disabled_skill_names", return_value=set())
|
||||
@patch("tools.skills_tool.skill_matches_platform", return_value=True)
|
||||
def test_enabled_skill_included(self, mock_platform, mock_disabled, tmp_path, monkeypatch):
|
||||
@patch("tools.skills_tool.SKILLS_DIR")
|
||||
def test_enabled_skill_included(self, mock_dir, mock_platform, mock_disabled, mock_iter, tmp_path):
|
||||
skill_dir = tmp_path / "my-skill"
|
||||
skill_dir.mkdir()
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
skill_md.write_text("---\nname: my-skill\ndescription: A test skill\n---\nContent")
|
||||
import tools.skills_tool as _st
|
||||
import agent.skill_utils as _su
|
||||
monkeypatch.setattr(_st, "SKILLS_DIR", tmp_path)
|
||||
monkeypatch.setattr(_su, "get_external_skills_dirs", lambda: [])
|
||||
mock_dir.exists.return_value = True
|
||||
mock_iter.return_value = [skill_md]
|
||||
from tools.skills_tool import _find_all_skills
|
||||
skills = _find_all_skills()
|
||||
assert any(s["name"] == "my-skill" for s in skills)
|
||||
|
||||
@patch("agent.skill_utils.iter_skill_index_files")
|
||||
@patch("tools.skills_tool._get_disabled_skill_names", return_value={"my-skill"})
|
||||
@patch("tools.skills_tool.skill_matches_platform", return_value=True)
|
||||
def test_skip_disabled_returns_all(self, mock_platform, mock_disabled, tmp_path, monkeypatch):
|
||||
@patch("tools.skills_tool.SKILLS_DIR")
|
||||
def test_skip_disabled_returns_all(self, mock_dir, mock_platform, mock_disabled, mock_iter, tmp_path):
|
||||
"""skip_disabled=True ignores the disabled set (for config UI)."""
|
||||
skill_dir = tmp_path / "my-skill"
|
||||
skill_dir.mkdir()
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
skill_md.write_text("---\nname: my-skill\ndescription: A test skill\n---\nContent")
|
||||
import tools.skills_tool as _st
|
||||
import agent.skill_utils as _su
|
||||
monkeypatch.setattr(_st, "SKILLS_DIR", tmp_path)
|
||||
monkeypatch.setattr(_su, "get_external_skills_dirs", lambda: [])
|
||||
mock_dir.exists.return_value = True
|
||||
mock_iter.return_value = [skill_md]
|
||||
from tools.skills_tool import _find_all_skills
|
||||
skills = _find_all_skills(skip_disabled=True)
|
||||
assert any(s["name"] == "my-skill" for s in skills)
|
||||
|
|
|
|||
|
|
@ -217,8 +217,14 @@ class FileToolsIntegrationTests(unittest.TestCase):
|
|||
def setUp(self) -> None:
|
||||
file_state.get_registry().clear()
|
||||
self._tmpdir = tempfile.mkdtemp(prefix="hermes_file_state_int_")
|
||||
self._orig_terminal_env = os.environ.get("TERMINAL_ENV")
|
||||
os.environ["TERMINAL_ENV"] = "local"
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self._orig_terminal_env is None:
|
||||
os.environ.pop("TERMINAL_ENV", None)
|
||||
else:
|
||||
os.environ["TERMINAL_ENV"] = self._orig_terminal_env
|
||||
import shutil
|
||||
shutil.rmtree(self._tmpdir, ignore_errors=True)
|
||||
file_state.get_registry().clear()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import os
|
|||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
# Ensure repo root is importable
|
||||
_repo_root = Path(__file__).resolve().parent.parent.parent
|
||||
|
|
@ -33,6 +34,7 @@ except ImportError:
|
|||
class TestToolResolution:
|
||||
"""Verify get_tool_definitions returns all expected tools for eval."""
|
||||
|
||||
@patch.dict("os.environ", {"TERMINAL_ENV": "local"})
|
||||
def test_terminal_and_file_toolsets_resolve_all_tools(self):
|
||||
"""enabled_toolsets=['terminal', 'file'] should produce 6 tools."""
|
||||
from model_tools import get_tool_definitions
|
||||
|
|
@ -44,6 +46,7 @@ class TestToolResolution:
|
|||
expected = {"terminal", "process", "read_file", "write_file", "search_files", "patch"}
|
||||
assert expected == names, f"Expected {expected}, got {names}"
|
||||
|
||||
@patch.dict("os.environ", {"TERMINAL_ENV": "local"})
|
||||
def test_terminal_tool_present(self):
|
||||
"""The terminal tool must be present (not silently dropped)."""
|
||||
from model_tools import get_tool_definitions
|
||||
|
|
|
|||
|
|
@ -157,6 +157,10 @@ def _check_sensitive_path(filepath: str, task_id: str = "default") -> str | None
|
|||
except (OSError, ValueError):
|
||||
resolved = filepath
|
||||
normalized = os.path.normpath(os.path.expanduser(filepath))
|
||||
# /private/var/folders is the macOS per-user temporary directory tree; it is
|
||||
# safe to write and must not be treated as a sensitive system path.
|
||||
if resolved.startswith("/private/var/folders/"):
|
||||
return None
|
||||
_err = (
|
||||
f"Refusing to write to sensitive system path: {filepath}\n"
|
||||
"Use the terminal tool with sudo if you need to modify system files."
|
||||
|
|
@ -428,7 +432,7 @@ def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str =
|
|||
with _read_tracker_lock:
|
||||
task_data = _read_tracker.setdefault(task_id, {
|
||||
"last_key": None, "consecutive": 0,
|
||||
"read_history": set(), "dedup": {},
|
||||
"read_history": set(), "dedup": {}, "read_timestamps": {},
|
||||
})
|
||||
cached_mtime = task_data.get("dedup", {}).get(dedup_key)
|
||||
|
||||
|
|
|
|||
|
|
@ -2988,8 +2988,9 @@ def _kill_orphaned_mcp_children() -> None:
|
|||
except (ProcessLookupError, PermissionError, OSError):
|
||||
pass
|
||||
|
||||
# Phase 2: Wait for graceful exit
|
||||
_time.sleep(2)
|
||||
# Phase 2: Wait for graceful exit (skip if nothing was signalled)
|
||||
if pids:
|
||||
_time.sleep(2)
|
||||
|
||||
# Phase 3: SIGKILL any survivors
|
||||
_sigkill = getattr(_signal, "SIGKILL", _signal.SIGTERM)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue