refactor(run_agent): extract tool execution to agent/tool_executor.py

Move the two big tool-dispatch methods out of run_agent.py:

* execute_tool_calls_concurrent — 408-line concurrent path (interrupt
  pre-flight, guardrail+plugin block, callback fan-out, ContextVar-
  preserving ThreadPoolExecutor, periodic heartbeats for the gateway
  inactivity monitor, per-tool result handling with subdir hints +
  guardrail observations + checkpoint, /steer drain)
* execute_tool_calls_sequential — 441-line sequential path (the
  original behavior used for single-tool batches and interactive
  tools)

Both take the parent AIAgent as their first argument; AIAgent keeps
thin forwarders so call sites unchanged. handle_function_call is
routed through _ra() so tests that patch run_agent.handle_function_call
keep working. _set_interrupt likewise.

The AST guard in test_tool_executor_contextvar_propagation.py is
updated to scan both run_agent.py AND agent/tool_executor.py so it
still catches the executor.submit(_run_tool, ...) regression
regardless of which file the body lives in.

tests/run_agent/ + tests/agent/: 4313 passed (same pre-existing
test_auxiliary_client failure as before).

run_agent.py: 14309 -> 13461 lines (-848).
This commit is contained in:
teknium1 2026-05-16 18:24:05 -07:00
parent 2d2cd5e904
commit 79559214a6
No known key found for this signature in database
3 changed files with 945 additions and 855 deletions

View file

@ -152,19 +152,28 @@ def test_run_agent_concurrent_executor_wraps_submit_with_copy_context():
import inspect
import run_agent
from agent import tool_executor as tool_executor_module
src_path = inspect.getsourcefile(run_agent)
assert src_path is not None
tree = ast.parse(open(src_path, encoding="utf-8").read())
# Source for both modules — the concurrent-executor body lives in
# ``agent/tool_executor.py`` after the run_agent.py refactor (PR
# following #16660). Search both so this guard keeps firing
# regardless of where the call site lives.
sources = []
for mod in (run_agent, tool_executor_module):
src_path = inspect.getsourcefile(mod)
assert src_path is not None
sources.append((src_path, open(src_path, encoding="utf-8").read()))
submit_calls_in_agent: list[ast.Call] = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
# Match executor.submit(...) style calls.
if isinstance(func, ast.Attribute) and func.attr == "submit":
submit_calls_in_agent.append(node)
for _src_path, src_text in sources:
tree = ast.parse(src_text)
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
# Match executor.submit(...) style calls.
if isinstance(func, ast.Attribute) and func.attr == "submit":
submit_calls_in_agent.append(node)
# Filter to the submit call inside the concurrent tool executor —
# identifiable by passing `_run_tool` as its target. Other submit()