mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
refactor(agent): remove the inter-tool delay
The 1.0s sleep between sequential tool calls has been present since the initial commit with no documented rationale. It sleeps between local tool executions — the next LLM request goes out only after the whole batch — so it rate-limits nothing, and the parallel read-only path already runs with no delay. Every multi-tool turn pays (N-1) seconds of dead time. Remove the sleep, the internal tool_delay plumbing, and dead test assignments. AIAgent.__init__ keeps tool_delay as a deprecated no-op keyword for one release so existing programmatic callers construct cleanly; passing it emits a DeprecationWarning.
This commit is contained in:
parent
1a088989bc
commit
ce9f6712ff
13 changed files with 47 additions and 24 deletions
|
|
@ -455,8 +455,7 @@ def init_agent(
|
|||
command: str = None,
|
||||
args: list[str] | None = None,
|
||||
model: str = "",
|
||||
max_iterations: int = 500, # Default tool-calling iterations (shared with subagents)
|
||||
tool_delay: float = 1.0,
|
||||
max_iterations: int = 90, # Default tool-calling iterations (shared with subagents)
|
||||
enabled_toolsets: List[str] = None,
|
||||
disabled_toolsets: List[str] = None,
|
||||
save_trajectories: bool = False,
|
||||
|
|
@ -529,8 +528,7 @@ def init_agent(
|
|||
requested_provider (str): Original provider identity before runtime canonicalization
|
||||
api_mode (str): API mode override: "chat_completions" or "codex_responses"
|
||||
model (str): Model name to use (default: "anthropic/claude-opus-4.6")
|
||||
max_iterations (int): Maximum number of tool calling iterations (default: 500)
|
||||
tool_delay (float): Delay between tool calls in seconds (default: 1.0)
|
||||
max_iterations (int): Maximum number of tool calling iterations (default: 90)
|
||||
enabled_toolsets (List[str]): Only enable tools from these toolsets (optional)
|
||||
disabled_toolsets (List[str]): Disable tools from these toolsets (optional)
|
||||
save_trajectories (bool): Whether to save conversation trajectories to JSONL files (default: False)
|
||||
|
|
@ -576,7 +574,6 @@ def init_agent(
|
|||
# Shared iteration budget — parent creates, children inherit.
|
||||
# Consumed by every LLM turn across parent + all subagents.
|
||||
agent.iteration_budget = iteration_budget or IterationBudget(max_iterations)
|
||||
agent.tool_delay = tool_delay
|
||||
agent.save_trajectories = save_trajectories
|
||||
agent.verbose_logging = verbose_logging
|
||||
agent.quiet_mode = quiet_mode
|
||||
|
|
|
|||
|
|
@ -1979,9 +1979,6 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
return
|
||||
break
|
||||
|
||||
if agent.tool_delay > 0 and i < len(assistant_message.tool_calls):
|
||||
time.sleep(agent.tool_delay)
|
||||
|
||||
# ── Per-turn aggregate budget enforcement ─────────────────────────
|
||||
num_tools_seq = len(assistant_message.tool_calls)
|
||||
if finalize and num_tools_seq > 0:
|
||||
|
|
|
|||
13
run_agent.py
13
run_agent.py
|
|
@ -45,6 +45,7 @@ import tempfile
|
|||
import time
|
||||
import threading
|
||||
import uuid
|
||||
import warnings
|
||||
from typing import List, Dict, Any, Optional, Callable
|
||||
# NOTE: `from openai import OpenAI` is deliberately NOT at module top — the
|
||||
# SDK pulls ~240 ms of imports. We expose `OpenAI` as a thin proxy object
|
||||
|
|
@ -439,8 +440,8 @@ class AIAgent:
|
|||
command: str = None,
|
||||
args: list[str] | None = None,
|
||||
model: str = "",
|
||||
max_iterations: int = 500, # Default tool-calling iterations (shared with subagents)
|
||||
tool_delay: float = 1.0,
|
||||
max_iterations: int = 90, # Default tool-calling iterations (shared with subagents)
|
||||
tool_delay: float = None, # Deprecated: accepted for compatibility, ignored
|
||||
enabled_toolsets: List[str] = None,
|
||||
disabled_toolsets: List[str] = None,
|
||||
save_trajectories: bool = False,
|
||||
|
|
@ -504,6 +505,13 @@ class AIAgent:
|
|||
requested_provider: str = None,
|
||||
):
|
||||
"""Forwarder — see ``agent.agent_init.init_agent``."""
|
||||
if tool_delay is not None:
|
||||
warnings.warn(
|
||||
"tool_delay is deprecated and ignored; sequential tool calls "
|
||||
"no longer sleep between executions.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
from agent.agent_init import init_agent
|
||||
init_agent(
|
||||
self,
|
||||
|
|
@ -518,7 +526,6 @@ class AIAgent:
|
|||
args=args,
|
||||
model=model,
|
||||
max_iterations=max_iterations,
|
||||
tool_delay=tool_delay,
|
||||
enabled_toolsets=enabled_toolsets,
|
||||
disabled_toolsets=disabled_toolsets,
|
||||
save_trajectories=save_trajectories,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ class TestGeneric400Heuristic:
|
|||
a.client = MagicMock()
|
||||
a._cached_system_prompt = "You are helpful."
|
||||
a._use_prompt_caching = False
|
||||
a.tool_delay = 0
|
||||
a.compression_enabled = False
|
||||
return a
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ def agent():
|
|||
a.client = MagicMock()
|
||||
a._cached_system_prompt = "You are helpful."
|
||||
a._use_prompt_caching = False
|
||||
a.tool_delay = 0
|
||||
# Default matches production (`compression.enabled` defaults to True).
|
||||
# Overflow-recovery tests below verify that 413 / context-overflow
|
||||
# errors DO trigger compression; the disabled-path behavior is
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ def test_substantive_tool_only_turn_invalidates_older_housekeeping_fallback():
|
|||
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
agent.valid_tool_names = {"todo", "web_search"}
|
||||
|
|
@ -147,7 +146,6 @@ def test_housekeeping_only_turn_still_sets_fallback():
|
|||
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
agent.valid_tool_names = {"memory"}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ def _make_agent() -> AIAgent:
|
|||
skip_memory=True,
|
||||
)
|
||||
agent.client = MagicMock()
|
||||
agent.tool_delay = 0
|
||||
agent._flush_messages_to_session_db = MagicMock()
|
||||
return agent
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ def _make_agent() -> AIAgent:
|
|||
a.client = MagicMock()
|
||||
a._cached_system_prompt = "You are helpful."
|
||||
a._use_prompt_caching = False
|
||||
a.tool_delay = 0
|
||||
a.compression_enabled = False
|
||||
a.save_trajectories = False
|
||||
return a
|
||||
|
|
|
|||
|
|
@ -333,7 +333,6 @@ def loop_agent():
|
|||
a.client = MagicMock()
|
||||
a._cached_system_prompt = "You are helpful."
|
||||
a._use_prompt_caching = False
|
||||
a.tool_delay = 0
|
||||
a.compression_enabled = False
|
||||
a.save_trajectories = False
|
||||
return a
|
||||
|
|
|
|||
|
|
@ -978,6 +978,25 @@ class TestInit:
|
|||
assert agent.api_mode == "anthropic_messages"
|
||||
mock_anthropic.Anthropic.assert_called_once()
|
||||
|
||||
def test_tool_delay_kwarg_is_deprecated_noop(self):
|
||||
"""tool_delay stays accepted for compatibility but warns and is ignored."""
|
||||
with (
|
||||
patch("run_agent.get_tool_definitions", return_value=[]),
|
||||
patch("run_agent.check_toolset_requirements", return_value={}),
|
||||
patch("run_agent.OpenAI"),
|
||||
):
|
||||
with pytest.warns(DeprecationWarning, match="tool_delay"):
|
||||
a = AIAgent(
|
||||
api_key="test-key-1234567890",
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
tool_delay=0,
|
||||
quiet_mode=True,
|
||||
skip_context_files=True,
|
||||
skip_memory=True,
|
||||
)
|
||||
# The value is discarded — nothing downstream reads it anymore.
|
||||
assert not hasattr(a, "tool_delay")
|
||||
|
||||
def test_prompt_caching_claude_openrouter(self):
|
||||
"""Claude model via OpenRouter should enable prompt caching."""
|
||||
with (
|
||||
|
|
@ -2397,6 +2416,22 @@ class TestExecuteToolCalls:
|
|||
assert messages[0]["role"] == "tool"
|
||||
assert "search result" in messages[0]["content"]
|
||||
|
||||
def test_sequential_tool_calls_run_without_delay(self, agent):
|
||||
"""Two sequential tool calls execute back-to-back with no sleep between them."""
|
||||
tc1 = _mock_tool_call(name="web_search", arguments="{}", call_id="c1")
|
||||
tc2 = _mock_tool_call(name="web_search", arguments="{}", call_id="c2")
|
||||
mock_msg = _mock_assistant_msg(content="", tool_calls=[tc1, tc2])
|
||||
messages = []
|
||||
with (
|
||||
patch("run_agent.handle_function_call", return_value="ok") as mock_hfc,
|
||||
patch("agent.tool_executor.time.sleep") as mock_sleep,
|
||||
):
|
||||
agent._execute_tool_calls_sequential(mock_msg, messages, "task-1")
|
||||
assert mock_hfc.call_count == 2
|
||||
mock_sleep.assert_not_called()
|
||||
tool_results = [m for m in messages if m["role"] == "tool"]
|
||||
assert [m["tool_call_id"] for m in tool_results] == ["c1", "c2"]
|
||||
|
||||
def test_sequential_memory_remove_notifies_provider_with_tool_result(self, agent):
|
||||
old_text = "stale preference entry"
|
||||
tc = _mock_tool_call(
|
||||
|
|
@ -4419,7 +4454,6 @@ class TestRunConversation:
|
|||
"""Common setup for run_conversation tests."""
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
|
||||
|
|
@ -6702,7 +6736,6 @@ class TestRetryExhaustion:
|
|||
def _setup_agent(self, agent):
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
|
||||
|
|
@ -8703,7 +8736,6 @@ class TestReasoningReplayForStrictProviders:
|
|||
def _setup_agent(self, agent):
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ def _make_agent(*tool_names: str, max_iterations: int = 10, config: dict | None
|
|||
agent.client = MagicMock()
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
return agent
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ def _make_agent():
|
|||
agent.client = MagicMock()
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
return agent
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ def _make_agent(max_iterations: int = 10, config: dict | None = None) -> AIAgent
|
|||
agent.client = MagicMock()
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
# No fallback chain so empty responses exhaust deterministically.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue