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

# Conflicts:
#	tui_gateway/server.py
This commit is contained in:
Brooklyn Nicholson
2026-05-30 13:19:27 -05:00
157 changed files with 10059 additions and 831 deletions
+168 -5
View File
@@ -2543,6 +2543,122 @@ class TestConcurrentToolExecution:
assert json.loads(result) == {"error": "Blocked"}
assert agent._turns_since_memory == 5
def test_concurrent_blocked_write_skips_checkpoint(self, agent, monkeypatch):
"""Concurrent path: blocked write_file should not trigger checkpoint."""
tc1 = _mock_tool_call(name="write_file",
arguments='{"path":"test.txt","content":"hello"}',
call_id="c1")
tc2 = _mock_tool_call(name="read_file",
arguments='{"path":"other.py"}',
call_id="c2")
mock_msg = _mock_assistant_msg(content="", tool_calls=[tc1, tc2])
messages = []
monkeypatch.setattr(
"hermes_cli.plugins.get_pre_tool_call_block_message",
lambda *args, **kwargs: "Blocked" if args[0] == "write_file" else None,
)
agent._checkpoint_mgr.enabled = True
def fake_handle(name, args, task_id, **kwargs):
return f"result_{name}"
with patch("run_agent.handle_function_call", side_effect=fake_handle):
with patch.object(agent._checkpoint_mgr, "ensure_checkpoint") as cp_mock:
agent._execute_tool_calls_concurrent(mock_msg, messages, "task-1")
cp_mock.assert_not_called()
def test_concurrent_blocked_patch_skips_checkpoint(self, agent, monkeypatch):
"""Concurrent path: blocked patch should not trigger checkpoint."""
tc1 = _mock_tool_call(name="patch",
arguments='{"path":"f.py","old":"a","new":"b"}',
call_id="c1")
tc2 = _mock_tool_call(name="read_file",
arguments='{"path":"other.py"}',
call_id="c2")
mock_msg = _mock_assistant_msg(content="", tool_calls=[tc1, tc2])
messages = []
monkeypatch.setattr(
"hermes_cli.plugins.get_pre_tool_call_block_message",
lambda *args, **kwargs: "Blocked" if args[0] == "patch" else None,
)
agent._checkpoint_mgr.enabled = True
def fake_handle(name, args, task_id, **kwargs):
return f"result_{name}"
with patch("run_agent.handle_function_call", side_effect=fake_handle):
with patch.object(agent._checkpoint_mgr, "ensure_checkpoint") as cp_mock:
agent._execute_tool_calls_concurrent(mock_msg, messages, "task-1")
cp_mock.assert_not_called()
def test_concurrent_blocked_terminal_skips_checkpoint(self, agent, monkeypatch):
"""Concurrent path: blocked terminal should not trigger checkpoint."""
tc1 = _mock_tool_call(name="terminal",
arguments='{"command":"rm -rf /tmp/foo"}',
call_id="c1")
tc2 = _mock_tool_call(name="read_file",
arguments='{"path":"other.py"}',
call_id="c2")
mock_msg = _mock_assistant_msg(content="", tool_calls=[tc1, tc2])
messages = []
monkeypatch.setattr(
"hermes_cli.plugins.get_pre_tool_call_block_message",
lambda *args, **kwargs: "Blocked" if args[0] == "terminal" else None,
)
agent._checkpoint_mgr.enabled = True
def fake_handle(name, args, task_id, **kwargs):
return f"result_{name}"
with patch("run_agent.handle_function_call", side_effect=fake_handle):
with patch.object(agent._checkpoint_mgr, "ensure_checkpoint") as cp_mock:
with patch("agent.tool_executor._is_destructive_command", return_value=True):
agent._execute_tool_calls_concurrent(mock_msg, messages, "task-1")
cp_mock.assert_not_called()
def test_concurrent_blocked_write_does_not_steal_slot_from_allowed_write(self, agent, monkeypatch):
"""When write_file is blocked, its dedup slot must not be consumed,
so a subsequent allowed write_file for the same path still checkpoints."""
tc1 = _mock_tool_call(name="write_file",
arguments='{"path":"dup.txt","content":"blocked"}',
call_id="c1")
tc2 = _mock_tool_call(name="write_file",
arguments='{"path":"dup.txt","content":"allowed"}',
call_id="c2")
mock_msg = _mock_assistant_msg(content="", tool_calls=[tc1, tc2])
messages = []
call_count = {"n": 0}
def block_first_only(*args, **kwargs):
call_count["n"] += 1
return "Blocked" if call_count["n"] == 1 else None
monkeypatch.setattr(
"hermes_cli.plugins.get_pre_tool_call_block_message",
block_first_only,
)
agent._checkpoint_mgr.enabled = True
def fake_handle(name, args, task_id, **kwargs):
return f"result_{name}"
with patch("run_agent.handle_function_call", side_effect=fake_handle):
with patch.object(agent._checkpoint_mgr, "ensure_checkpoint") as cp_mock:
agent._execute_tool_calls_concurrent(mock_msg, messages, "task-1")
# Second (allowed) write must checkpoint even though first was blocked.
cp_mock.assert_called_once()
class TestPathsOverlap:
"""Unit tests for the _paths_overlap helper."""
@@ -2756,6 +2872,40 @@ class TestHandleMaxIterations:
]
assert len(stub_ids) >= 1, f"No stub result for assistant tool_call: {stub_ids}"
def test_summary_strips_strict_schema_foreign_fields(self, agent):
"""Regression: the max-iterations summary request must NOT carry
Chat-Completions-schema-foreign keys — tool_name (SQLite FTS
bookkeeping), codex_* reasoning carriers, or internal _-prefixed
scaffolding. Strict gateways (Fireworks-backed OpenCode Go, Mistral,
Kimi) reject these with 'Extra inputs are not permitted, field:
messages[N].tool_name'. The transport's convert_messages() strips
them on the main loop; this hand-built summary path must mirror it."""
agent.client.chat.completions.create.return_value = _mock_response(content="Summary")
agent._cached_system_prompt = "You are helpful."
messages = [
{"role": "user", "content": "do stuff"},
{
"role": "assistant",
"tool_calls": [{"id": "call_1", "function": {"name": "execute_code", "arguments": "{}"}}],
"codex_reasoning_items": [{"id": "rs_1"}],
},
{"role": "tool", "tool_call_id": "call_1", "content": "result", "tool_name": "execute_code"},
{"role": "assistant", "content": "Done.", "_empty_recovery_synthetic": True},
]
result = agent._handle_max_iterations(messages, 60)
assert result == "Summary"
sent_msgs = agent.client.chat.completions.create.call_args.kwargs.get("messages", [])
for m in sent_msgs:
assert "tool_name" not in m, m
assert "codex_reasoning_items" not in m, m
assert "codex_message_items" not in m, m
assert not any(isinstance(k, str) and k.startswith("_") for k in m), m
# Internal history is untouched — the path copies each message.
assert messages[2]["tool_name"] == "execute_code"
assert messages[1]["codex_reasoning_items"] == [{"id": "rs_1"}]
def test_summary_omits_provider_preferences_for_non_openrouter(self, agent):
agent.base_url = "https://api.openai.com/v1"
agent._base_url_lower = agent.base_url.lower()
@@ -3046,7 +3196,11 @@ class TestRunConversation:
mock_compress.assert_not_called() # no compression triggered
assert result["completed"] is True
assert result["final_response"] == "(empty)"
# #34452: the bare "(empty)" sentinel is now replaced by a
# user-visible end-of-turn explanation so the failure isn't silent.
assert result["final_response"] != "(empty)"
assert "No reply:" in result["final_response"]
assert result["turn_exit_reason"] == "empty_response_exhausted"
assert result["api_calls"] == 6 # 1 original + 2 prefill + 3 retries
def test_reasoning_only_response_prefill_then_empty(self, agent):
@@ -3066,7 +3220,9 @@ class TestRunConversation:
):
result = agent.run_conversation("answer me")
assert result["completed"] is True
assert result["final_response"] == "(empty)"
# #34452: explanation replaces the bare "(empty)" sentinel.
assert result["final_response"] != "(empty)"
assert "No reply:" in result["final_response"]
assert result["api_calls"] == 6 # 1 original + 2 prefill + 3 retries
def test_reasoning_only_prefill_succeeds_on_continuation(self, agent):
@@ -3113,7 +3269,9 @@ class TestRunConversation:
):
result = agent.run_conversation("answer me")
assert result["completed"] is True
assert result["final_response"] == "(empty)"
# #34452: explanation replaces the bare "(empty)" sentinel.
assert result["final_response"] != "(empty)"
assert "No reply:" in result["final_response"]
assert result["api_calls"] == 4 # 1 original + 3 retries
def test_truly_empty_response_succeeds_on_nudge(self, agent):
@@ -3209,7 +3367,9 @@ class TestRunConversation:
):
result = agent.run_conversation("answer me")
assert result["completed"] is True
assert result["final_response"] == "(empty)"
# #34452: explanation replaces the bare "(empty)" sentinel.
assert result["final_response"] != "(empty)"
assert "No reply:" in result["final_response"]
def test_empty_response_emits_status_for_gateway(self, agent):
"""_emit_status is called during empty retries so gateway users see feedback."""
@@ -3235,7 +3395,10 @@ class TestRunConversation:
):
result = agent.run_conversation("answer me")
assert result["final_response"] == "(empty)"
# #34452: explanation replaces the bare "(empty)" sentinel, but the
# status emissions during retries are unchanged.
assert result["final_response"] != "(empty)"
assert "No reply:" in result["final_response"]
# Should have emitted retry statuses (3 retries) + final failure
retry_msgs = [m for m in status_messages if "retrying" in m.lower()]
assert len(retry_msgs) == 3, f"Expected 3 retry status messages, got {len(retry_msgs)}: {status_messages}"