Skip to content

Commit 1ca504e

Browse files
committed
Wire AgentLoop into JSON executor (AC_run_agent) and MCP (ac_run_agent)
1 parent 60482ba commit 1ca504e

4 files changed

Lines changed: 175 additions & 0 deletions

File tree

je_auto_control/utils/executor/action_executor.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,63 @@ def _presence_clear() -> Dict[str, Any]:
474474
return {"cleared": True}
475475

476476

477+
def _run_agent(goal: str,
478+
backend: str = "anthropic",
479+
max_steps: int = 25,
480+
wall_seconds: float = 300.0,
481+
model: Optional[str] = None,
482+
max_tokens: int = 1024) -> Dict[str, Any]:
483+
"""Executor adapter: drive the closed-loop ``AgentLoop`` against ``goal``.
484+
485+
``backend`` selects between the production backends (Anthropic /
486+
OpenAI). The Anthropic computer-use raw path remains available
487+
via :func:`_computer_use` / ``AC_computer_use``.
488+
"""
489+
from je_auto_control.utils.agent import AgentBudget, AgentLoop
490+
from je_auto_control.utils.agent.backends import (
491+
AgentBackendError, AnthropicAgentBackend, OpenAIAgentBackend,
492+
)
493+
from je_auto_control.utils.tool_use_schema import (
494+
export_anthropic_tools, export_openai_tools,
495+
)
496+
name = (backend or "anthropic").strip().lower()
497+
if name == "anthropic":
498+
tools = export_anthropic_tools()
499+
backend_obj = AnthropicAgentBackend(
500+
tools=tools,
501+
model=model or "claude-opus-4-7",
502+
max_tokens=int(max_tokens),
503+
)
504+
elif name == "openai":
505+
tools = export_openai_tools()
506+
backend_obj = OpenAIAgentBackend(
507+
tools=tools,
508+
model=model or "gpt-4o",
509+
max_tokens=int(max_tokens),
510+
)
511+
else:
512+
raise ValueError(f"unknown agent backend: {backend!r}")
513+
budget = AgentBudget(
514+
max_steps=int(max_steps), wall_seconds=float(wall_seconds),
515+
)
516+
result = AgentLoop(backend_obj, budget=budget).run(goal)
517+
return {
518+
"succeeded": bool(result.succeeded),
519+
"elapsed_s": float(result.elapsed_s),
520+
"final_message": result.final_message,
521+
"steps": [
522+
{
523+
"index": step.index,
524+
"tool": step.tool,
525+
"arguments": step.arguments,
526+
"error": step.error,
527+
"stop_reason": step.stop_reason,
528+
}
529+
for step in result.steps
530+
],
531+
}
532+
533+
477534
def _computer_use(goal: str,
478535
display_width_px: Optional[int] = None,
479536
display_height_px: Optional[int] = None,
@@ -1478,6 +1535,9 @@ def __init__(self):
14781535
# Computer-use (Anthropic computer_20250124 closed-loop agent)
14791536
"AC_computer_use": _computer_use,
14801537

1538+
# Generic plan→act→verify→retry agent loop (Anthropic / OpenAI)
1539+
"AC_run_agent": _run_agent,
1540+
14811541
# Cross-host DAG orchestrator
14821542
"AC_run_dag": _run_dag,
14831543

je_auto_control/utils/mcp_server/tools/_factories.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,26 @@ def computer_use_tools() -> List[MCPTool]:
762762
handler=h.computer_use,
763763
annotations=DESTRUCTIVE,
764764
),
765+
MCPTool(
766+
name="ac_run_agent",
767+
description=("Drive the generic plan→act→verify→retry "
768+
"AgentLoop against goal. backend='anthropic' "
769+
"uses tool-use messages; 'openai' uses the "
770+
"Responses API. Returns {succeeded, "
771+
"final_message, elapsed_s, steps[]}. Requires "
772+
"the matching SDK + API key."),
773+
input_schema=schema({
774+
"goal": {"type": "string"},
775+
"backend": {"type": "string",
776+
"enum": ["anthropic", "openai"]},
777+
"max_steps": {"type": "integer"},
778+
"wall_seconds": {"type": "number"},
779+
"model": {"type": "string"},
780+
"max_tokens": {"type": "integer"},
781+
}, required=["goal"]),
782+
handler=h.run_agent,
783+
annotations=DESTRUCTIVE,
784+
),
765785
]
766786

767787

je_auto_control/utils/mcp_server/tools/_handlers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,21 @@ def computer_use(goal: str,
10931093
return result_to_dict(result)
10941094

10951095

1096+
def run_agent(goal: str,
1097+
backend: str = "anthropic",
1098+
max_steps: int = 25,
1099+
wall_seconds: float = 300.0,
1100+
model: Optional[str] = None,
1101+
max_tokens: int = 1024) -> Dict[str, Any]:
1102+
"""Drive the generic plan→act→verify→retry AgentLoop against ``goal``."""
1103+
from je_auto_control.utils.executor.action_executor import _run_agent
1104+
return _run_agent(
1105+
goal=goal, backend=backend,
1106+
max_steps=int(max_steps), wall_seconds=float(wall_seconds),
1107+
model=model, max_tokens=int(max_tokens),
1108+
)
1109+
1110+
10961111
# === Scheduler / triggers / hotkey daemon ===================================
10971112

10981113
def _job_to_dict(job: Any) -> Dict[str, Any]:
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Wire-up tests for ``AC_run_agent`` + ``ac_run_agent``.
2+
3+
The closed-loop AgentLoop already has direct-API tests in
4+
``test_agent_loop.py``. These tests cover the new executor + MCP
5+
adapters: they verify both surfaces register, dispatch to AgentLoop,
6+
and faithfully return the structured result. A ``FakeAgentBackend``
7+
is patched in so the tests never hit a real LLM.
8+
"""
9+
from __future__ import annotations
10+
11+
from typing import Any, Dict, List
12+
13+
from je_auto_control.utils.agent import FakeAgentBackend
14+
15+
16+
def _stub_backend_factory(decisions: List[Dict[str, Any]]):
17+
"""Return an Anthropic-/OpenAI-backend stub that ignores tools kwargs."""
18+
def factory(*_args, **_kwargs):
19+
return FakeAgentBackend(decisions)
20+
return factory
21+
22+
23+
def _patch_backends(monkeypatch, decisions):
24+
"""Replace both production backends with the FakeAgentBackend stub."""
25+
factory = _stub_backend_factory(decisions)
26+
import je_auto_control.utils.agent.backends as backends_pkg
27+
monkeypatch.setattr(backends_pkg, "AnthropicAgentBackend", factory)
28+
monkeypatch.setattr(backends_pkg, "OpenAIAgentBackend", factory)
29+
# Disable the screenshot helper so the loop doesn't try to grab a
30+
# real frame on the CI runner.
31+
from je_auto_control.utils.agent import agent_loop as loop_mod
32+
monkeypatch.setattr(loop_mod, "_default_screenshot", lambda: None)
33+
34+
35+
def test_executor_registers_ac_run_agent():
36+
from je_auto_control.utils.executor.action_executor import executor
37+
assert "AC_run_agent" in executor.known_commands()
38+
39+
40+
def test_mcp_registry_exposes_ac_run_agent():
41+
from je_auto_control.utils.mcp_server.tools import (
42+
build_default_tool_registry,
43+
)
44+
names = {tool.name for tool in build_default_tool_registry()}
45+
assert "ac_run_agent" in names
46+
47+
48+
def test_executor_path_runs_agent_loop(monkeypatch):
49+
_patch_backends(monkeypatch, [
50+
{"stop": True, "message": "done by stub"},
51+
])
52+
# Stop AgentLoop from trying to dispatch a real AC_* tool.
53+
from je_auto_control.utils.executor.action_executor import _run_agent
54+
result = _run_agent(
55+
goal="probe", backend="anthropic",
56+
max_steps=2, wall_seconds=5.0,
57+
)
58+
assert result["succeeded"] is True
59+
assert result["final_message"] == "done by stub"
60+
assert len(result["steps"]) == 1
61+
62+
63+
def test_mcp_handler_round_trips(monkeypatch):
64+
_patch_backends(monkeypatch, [
65+
{"stop": True, "message": "mcp-ok"},
66+
])
67+
from je_auto_control.utils.mcp_server.tools._handlers import run_agent
68+
record = run_agent(
69+
goal="probe-mcp", backend="openai",
70+
max_steps=2, wall_seconds=5.0,
71+
)
72+
assert record["succeeded"] is True
73+
assert record["final_message"] == "mcp-ok"
74+
75+
76+
def test_unknown_backend_raises():
77+
from je_auto_control.utils.executor.action_executor import _run_agent
78+
import pytest
79+
with pytest.raises(ValueError, match="unknown agent backend"):
80+
_run_agent(goal="x", backend="bogus")

0 commit comments

Comments
 (0)