mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-06 07:51:53 +00:00
refactor(run_agent): extract run_conversation to agent/conversation_loop.py
The 3,877-line run_conversation body — the agent loop itself — moves out of run_agent.py into a dedicated module. AIAgent.run_conversation is now a thin forwarder that delegates to agent.conversation_loop.run_conversation with the AIAgent instance as the first argument. This is the largest single extraction in the run_agent.py refactor. The body keeps all 163 self.X references intact (rewritten as agent.X), all nested closures, all retry/backoff/compression machinery. Symbols that tests or callers patch on run_agent (_set_interrupt, handle_function_call, AIAgent class attrs) are resolved through _ra() inside the extracted module so the patch surface is preserved. Five tests doing inspect.getsource(AIAgent.run_conversation) updated to scan agent.conversation_loop.run_conversation. Two source-introspection tests (TestMemoryNudgeCounterPersistence, TestMemoryProviderTurnStart) updated to accept either self.X (legacy) or agent.X (extracted form) in the matched assertions. Live E2E verified on three model paths: * openai/gpt-5.4 (OpenAI chat completions via OpenRouter) * anthropic/claude-sonnet-4.6 (Anthropic Messages via OpenRouter) * moonshotai/kimi-k2-thinking (reasoning model, reasoning_content path) Plus read_file tool execution, terminal tool, web_search. tests/run_agent/ + tests/agent/: 4313 passed, 1 pre-existing failure (test_auxiliary_client::test_custom_endpoint... — same as on main). run_agent.py: 9800 -> 5944 lines (-3856). Total reduction since baseline: 16083 -> 5944 (-10139, 63%).
This commit is contained in:
parent
d35ee7bcdd
commit
0530252384
5 changed files with 4005 additions and 3879 deletions
3964
agent/conversation_loop.py
Normal file
3964
agent/conversation_loop.py
Normal file
File diff suppressed because it is too large
Load diff
3870
run_agent.py
3870
run_agent.py
File diff suppressed because it is too large
Load diff
|
|
@ -75,7 +75,9 @@ class TestAgentLoopSourceStillHasCarveOut:
|
|||
def test_run_agent_excludes_jsondecodeerror_from_local_validation(self):
|
||||
import run_agent
|
||||
import inspect
|
||||
src = inspect.getsource(run_agent)
|
||||
from agent import conversation_loop
|
||||
# The body moved into agent/conversation_loop.py; scan both for safety.
|
||||
src = inspect.getsource(run_agent) + inspect.getsource(conversation_loop)
|
||||
# The predicate we care about must reference json.JSONDecodeError
|
||||
# in its exclusion tuple. We check for the specific co-occurrence
|
||||
# rather than the literal string so harmless reformatting doesn't
|
||||
|
|
|
|||
|
|
@ -120,10 +120,20 @@ def test_production_code_contains_hydration_block():
|
|||
"""Smoke test: confirm the hydration code is actually wired into
|
||||
run_conversation(). If someone deletes it, tests above still pass
|
||||
against the inline replica — this fails them awake.
|
||||
|
||||
The body now lives in agent/conversation_loop.py after the
|
||||
run_agent.py refactor; check both files for safety.
|
||||
"""
|
||||
from pathlib import Path
|
||||
src = Path(__file__).resolve().parents[2] / "run_agent.py"
|
||||
content = src.read_text(encoding="utf-8")
|
||||
repo = Path(__file__).resolve().parents[2]
|
||||
src_ra = (repo / "run_agent.py").read_text(encoding="utf-8")
|
||||
src_cl = (repo / "agent" / "conversation_loop.py").read_text(encoding="utf-8")
|
||||
content = src_ra + src_cl
|
||||
# Anchor on the unique comment + the modulo line.
|
||||
assert "Hydrate per-session nudge counters from persisted history" in content
|
||||
assert "self._turns_since_memory = prior_user_turns % self._memory_nudge_interval" in content
|
||||
# The line uses ``self.`` in run_agent.py form and ``agent.`` in the
|
||||
# extracted module, accept either.
|
||||
assert (
|
||||
"self._turns_since_memory = prior_user_turns % self._memory_nudge_interval" in content
|
||||
or "agent._turns_since_memory = prior_user_turns % agent._memory_nudge_interval" in content
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5205,14 +5205,19 @@ 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")
|
||||
# After the run_agent.py refactor the body uses ``agent.X`` instead
|
||||
# of ``self.X``, so accept either form.
|
||||
preamble_end = src.index("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:
|
||||
|
|
@ -5220,7 +5225,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:' "
|
||||
|
|
@ -5258,7 +5264,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
|
||||
|
||||
|
|
@ -5294,7 +5301,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(")
|
||||
|
|
@ -5304,7 +5312,13 @@ 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)
|
||||
# After the run_agent.py refactor the body uses ``agent.X`` instead
|
||||
# of ``self.X``. Accept either spelling.
|
||||
assert (
|
||||
"on_turn_start(self._user_turn_count" in src
|
||||
or "on_turn_start(agent._user_turn_count" in src
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue