Skip to content

refactor(run_agent): extract AIAgent internals into agent/ modules (16k→3.8k lines, 76% reduction) - #27248

Merged
teknium1 merged 37 commits into
mainfrom
hermes/hermes-27dc9cc2
May 17, 2026
Merged

refactor(run_agent): extract AIAgent internals into agent/ modules (16k→3.8k lines, 76% reduction)#27248
teknium1 merged 37 commits into
mainfrom
hermes/hermes-27dc9cc2

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

run_agent.py from 16,083 → 3,821 lines (-12,262, 76% reduction), redistributed across 14 cohesive agent/*.py modules. Behavior unchanged: every extraction keeps a thin forwarder method on AIAgent so call sites and test patches (patch("run_agent.OpenAI", ...), patch("run_agent.handle_function_call", ...), etc.) keep working.

Why

run_agent.py had grown to 16k lines. run_conversation alone was 3,877 lines. __init__ 1,381. The file was painful to read, painful to grep, and painful to test-locate. AGENTS.md called it out as one of five "do not delegate edits on this file" load-bearing files for exactly that reason.

This refactor leaves AIAgent as the orchestrator — thin forwarder methods that delegate to focused modules in agent/.

Changes

15 atomic commits, each individually green against tests/run_agent/ + tests/agent/:

Module LOC moved Contents
agent/message_sanitization.py 395 surrogate scrub, tool-arg JSON repair, ascii fallback, image-strip
agent/tool_dispatch_helpers.py 255 parallel-batch gating, multimodal envelopes, destructive-command detection
agent/process_bootstrap.py + agent/iteration_budget.py 166 lazy OpenAI proxy, broken-pipe-safe stdio, thread-safe iteration counter
agent/background_review.py 486 self-improvement loop (memory/skill review fork) + the three prompt strings
agent/conversation_compression.py 478 feasibility probe, compress+rotate-session, image-too-large recovery
agent/system_prompt.py 263 three-tier stable/context/volatile builder
agent/tool_executor.py 848 sequential + concurrent tool dispatch
agent/stream_diag.py 243 per-attempt diagnostics, exception-chain flatten, retry/drop logging
agent/chat_completion_helpers.py 1,921 7 methods incl. streaming + non-streaming API callers + build_api_kwargs
agent/codex_runtime.py 351 Codex App Server + Responses-API stream paths
agent/agent_runtime_helpers.py 2,317 22 mid-sized AIAgent helpers (switch_model, _invoke_tool, repair, sanitize, etc.)
agent/conversation_loop.py 3,856 run_conversation — the agent loop
agent/agent_init.py 1,380 __init__

Architectural pattern

Every extraction follows the same shape, by design:

  1. Pull the method body into a module-level function name(agent, ...) that takes the parent AIAgent as the first arg.
  2. self.X becomes agent.X via word-boundary regex (~163 occurrences in run_conversation alone).
  3. AIAgent keeps a thin forwarder method preserving the original signature.
  4. Symbols tests patch on run_agent.X (_set_interrupt, handle_function_call, OpenAI, cleanup_vm, logger, AIAgent.X class attrs) resolve through a _ra() lazy reference inside each extracted module so the patch contract is preserved.
  5. Source-introspection guards (inspect.getsource(AIAgent.run_conversation), etc.) updated to point at the new module location.

The _ra() indirection is doing real work — without it, dozens of test patches would silently miss the extracted code.

Validation

Before After
run_agent.py lines 16,083 3,821
tests/run_agent/ + tests/agent/ passing 4,313 4,313
tests/run_agent/ + tests/agent/ failing 1 (pre-existing) 1 (same pre-existing)
Skipped 3 3

The one persistent failure (tests/agent/test_auxiliary_client.py::TestGetTextAuxiliaryClient::test_custom_endpoint_uses_codex_wrapper_when_runtime_requests_responses_api) was confirmed failing on main before any commit in this branch — it's unrelated to this refactor.

Live E2E

End-to-end-verified from the worktree against real providers:

  • openai/gpt-5.4 via OpenRouter (chat-completions, streaming) — text response, terminal tool, read_file, write_file, web_search, multi-tool chain, session resume
  • anthropic/claude-sonnet-4.6 via OpenRouter (native Anthropic format path)
  • moonshotai/kimi-k2-thinking via OpenRouter (reasoning-content path)

Plus AIAgent direct-instantiation smoke (__init__, forwarder methods all present, IterationBudget consume/refund, sanitization roundtrips).

Test-file updates

Six tests were updated alongside the refactor — all are structural guards that scan the agent loop source for known patterns, and the patterns moved:

  • tests/run_agent/test_run_agent.py — 5 inspect.getsource(AIAgent.run_conversation) rerouted to agent.conversation_loop.run_conversation; TestAnthropicInterruptHandler rerouted to the extracted streaming/non-streaming callers; TestMemoryNudgeCounterPersistence / TestMemoryProviderTurnStart accept either self.X or agent.X
  • tests/run_agent/test_memory_nudge_counter_hydration.py — scan both run_agent.py and agent/conversation_loop.py
  • tests/run_agent/test_jsondecodeerror_retryable.py — scan both
  • tests/run_agent/test_tool_executor_contextvar_propagation.py — AST guard scans both run_agent.py and agent/tool_executor.py

Risk

  • Behaviour-preserving, but it's the largest single file edit in the project's history. Targeted suites pass; live E2E confirmed on three model paths and the streaming + tool-execution paths. The full tests/ suite was not run end-to-end (only tests/run_agent/ + tests/agent/); CI is the source of truth for cross-file regressions.
  • The _ra() lazy-reference pattern is the load-bearing piece that keeps test patches working. If a future test patches a new run_agent.X name, the extracted module's reference to that name may need routing.
  • 15 commits are atomic and individually mergeable in principle; rebase-merge preserves the history.

How to review

Easiest path: walk the commits in order. Each one tells you what was extracted and which tests it ran. The pattern is identical across all 15 — once you've reviewed the first two (sanitization, tool-dispatch helpers), the rest are mechanical applications of the same template.

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants