Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui

# Conflicts:
#	cli.py
#	hermes_cli/main.py
#	run_agent.py
#	tests/hermes_cli/test_cmd_update.py
#	tools/mcp_tool.py
#	web/src/lib/gatewayClient.ts
This commit is contained in:
Brooklyn Nicholson
2026-05-18 01:26:56 -05:00
260 changed files with 24547 additions and 13573 deletions
+45 -20
View File
@@ -2282,9 +2282,11 @@ class TestMcpParallelToolBatch:
def test_mcp_tools_parallel_when_server_opted_in(self):
"""MCP tools from a parallel-safe server can run concurrently."""
from run_agent import _should_parallelize_tool_batch
from tools.mcp_tool import _parallel_safe_servers, _lock
from tools.mcp_tool import _mcp_tool_server_names, _parallel_safe_servers, _lock
with _lock:
_parallel_safe_servers.add("github")
_mcp_tool_server_names["mcp_github_list_repos"] = "github"
_mcp_tool_server_names["mcp_github_search_code"] = "github"
try:
tc1 = _mock_tool_call(name="mcp_github_list_repos", arguments='{"org":"openai"}', call_id="c1")
tc2 = _mock_tool_call(name="mcp_github_search_code", arguments='{"q":"test"}', call_id="c2")
@@ -2292,13 +2294,16 @@ class TestMcpParallelToolBatch:
finally:
with _lock:
_parallel_safe_servers.discard("github")
_mcp_tool_server_names.pop("mcp_github_list_repos", None)
_mcp_tool_server_names.pop("mcp_github_search_code", None)
def test_mixed_mcp_and_builtin_parallel(self):
"""MCP parallel tools mixed with built-in parallel-safe tools."""
from run_agent import _should_parallelize_tool_batch
from tools.mcp_tool import _parallel_safe_servers, _lock
from tools.mcp_tool import _mcp_tool_server_names, _parallel_safe_servers, _lock
with _lock:
_parallel_safe_servers.add("docs")
_mcp_tool_server_names["mcp_docs_search"] = "docs"
try:
tc1 = _mock_tool_call(name="mcp_docs_search", arguments='{"query":"api"}', call_id="c1")
tc2 = _mock_tool_call(name="web_search", arguments='{"query":"test"}', call_id="c2")
@@ -2306,14 +2311,17 @@ class TestMcpParallelToolBatch:
finally:
with _lock:
_parallel_safe_servers.discard("docs")
_mcp_tool_server_names.pop("mcp_docs_search", None)
def test_mixed_parallel_and_serial_mcp_servers(self):
"""One parallel MCP server + one non-parallel MCP server = sequential."""
from run_agent import _should_parallelize_tool_batch
from tools.mcp_tool import _parallel_safe_servers, _lock
from tools.mcp_tool import _mcp_tool_server_names, _parallel_safe_servers, _lock
with _lock:
_parallel_safe_servers.add("docs")
# "github" is NOT in _parallel_safe_servers
_mcp_tool_server_names["mcp_docs_search"] = "docs"
_mcp_tool_server_names["mcp_github_list_repos"] = "github"
try:
tc1 = _mock_tool_call(name="mcp_docs_search", arguments='{"query":"api"}', call_id="c1")
tc2 = _mock_tool_call(name="mcp_github_list_repos", arguments='{"org":"openai"}', call_id="c2")
@@ -2321,6 +2329,8 @@ class TestMcpParallelToolBatch:
finally:
with _lock:
_parallel_safe_servers.discard("docs")
_mcp_tool_server_names.pop("mcp_docs_search", None)
_mcp_tool_server_names.pop("mcp_github_list_repos", None)
class TestHandleMaxIterations:
@@ -3657,7 +3667,7 @@ class TestNousCredentialRefresh:
assert ok is True
assert closed["value"] is True
assert captured["force_mint"] is True
assert captured["inference_auth_mode"] == "legacy"
assert rebuilt["kwargs"]["api_key"] == "new-nous-key"
assert (
rebuilt["kwargs"]["base_url"] == "https://inference-api.nousresearch.com/v1"
@@ -4832,23 +4842,26 @@ class TestAnthropicInterruptHandler:
def test_interruptible_has_anthropic_branch(self):
"""The interrupt handler must check api_mode == 'anthropic_messages'."""
import inspect
source = inspect.getsource(AIAgent._interruptible_api_call)
from agent.chat_completion_helpers import interruptible_api_call
source = inspect.getsource(interruptible_api_call)
assert "anthropic_messages" in source, \
"_interruptible_api_call must handle Anthropic interrupt (api_mode check)"
"interruptible_api_call must handle Anthropic interrupt (api_mode check)"
def test_interruptible_rebuilds_anthropic_client(self):
"""After interrupting, the Anthropic client should be rebuilt."""
import inspect
source = inspect.getsource(AIAgent._interruptible_api_call)
from agent.chat_completion_helpers import interruptible_api_call
source = inspect.getsource(interruptible_api_call)
assert "build_anthropic_client" in source, \
"_interruptible_api_call must rebuild Anthropic client after interrupt"
"interruptible_api_call must rebuild Anthropic client after interrupt"
def test_streaming_has_anthropic_branch(self):
"""_streaming_api_call must also handle Anthropic interrupt."""
import inspect
source = inspect.getsource(AIAgent._interruptible_streaming_api_call)
from agent.chat_completion_helpers import interruptible_streaming_api_call
source = inspect.getsource(interruptible_streaming_api_call)
assert "anthropic_messages" in source, \
"_streaming_api_call must handle Anthropic interrupt"
"interruptible_streaming_api_call must handle Anthropic interrupt"
# ---------------------------------------------------------------------------
@@ -5257,14 +5270,20 @@ class TestMemoryNudgeCounterPersistence:
def test_counters_not_reset_in_preamble(self):
"""The run_conversation preamble must not zero the nudge counters."""
import inspect
src = inspect.getsource(AIAgent.run_conversation)
from agent.conversation_loop import run_conversation as _rc
src = inspect.getsource(_rc)
# The preamble resets many fields (retry counts, budget, etc.)
# before the main loop. Find that reset block and verify our
# counters aren't in it. The reset block ends at iteration_budget.
preamble_end = src.index("self.iteration_budget = IterationBudget")
# The extracted body uses ``agent.X`` (not ``self.X``). Anchor
# exactly on ``agent.iteration_budget = IterationBudget`` so an
# unrelated identifier ending in ``iteration_budget`` (e.g.
# ``_iteration_budget`` or ``shared_iteration_budget``) can't
# match the boundary.
preamble_end = src.index("agent.iteration_budget = IterationBudget")
preamble = src[:preamble_end]
assert "self._turns_since_memory = 0" not in preamble
assert "self._iters_since_skill = 0" not in preamble
assert "agent._turns_since_memory = 0" not in preamble
assert "agent._iters_since_skill = 0" not in preamble
class TestDeadRetryCode:
@@ -5272,7 +5291,8 @@ class TestDeadRetryCode:
def test_no_unreachable_max_retries_after_backoff(self):
import inspect
source = inspect.getsource(AIAgent.run_conversation)
from agent.conversation_loop import run_conversation as _rc
source = inspect.getsource(_rc)
occurrences = source.count("if retry_count >= max_retries:")
assert occurrences == 2, (
f"Expected 2 occurrences of 'if retry_count >= max_retries:' "
@@ -5310,7 +5330,8 @@ class TestMemoryContextSanitization:
a literal <memory-context> tag we don't silently delete their text.
The streaming scrubber + plugin-side scrub cover real leak paths."""
import inspect
src = inspect.getsource(AIAgent.run_conversation)
from agent.conversation_loop import run_conversation as _rc
src = inspect.getsource(_rc)
assert "sanitize_context(user_message)" not in src
assert "sanitize_context(persist_user_message)" not in src
@@ -5346,7 +5367,8 @@ class TestMemoryProviderTurnStart:
def test_on_turn_start_called_before_prefetch(self):
"""Source-level check: on_turn_start appears before prefetch_all in run_conversation."""
import inspect
src = inspect.getsource(AIAgent.run_conversation)
from agent.conversation_loop import run_conversation as _rc
src = inspect.getsource(_rc)
# Find the actual method calls, not comments
idx_turn_start = src.index(".on_turn_start(")
idx_prefetch = src.index(".prefetch_all(")
@@ -5356,7 +5378,10 @@ class TestMemoryProviderTurnStart:
)
def test_on_turn_start_uses_user_turn_count(self):
"""Source-level check: on_turn_start receives self._user_turn_count."""
"""Source-level check: on_turn_start receives the user_turn_count."""
import inspect
src = inspect.getsource(AIAgent.run_conversation)
assert "on_turn_start(self._user_turn_count" in src
from agent.conversation_loop import run_conversation as _rc
src = inspect.getsource(_rc)
# The extracted body uses ``agent.X`` rather than ``self.X``;
# assert the extracted-form spelling directly.
assert "on_turn_start(agent._user_turn_count" in src