refactor: clean up agent architecture and coordination loop
Squash merged refactoring of agent architecture.
This commit is contained in:
+20
-11
@@ -2,9 +2,10 @@ import asyncio
|
||||
import logging
|
||||
import lmstudio as lms
|
||||
from typing import Any, Callable
|
||||
from core.interfaces import Agent
|
||||
from .prompt import CAVEMAN_PROMPT
|
||||
|
||||
logger: logging.Logger = logging.getLogger("agent-caveman")
|
||||
logger: logging.Logger = logging.getLogger("agent-base")
|
||||
|
||||
|
||||
class _ActResponseCapture:
|
||||
@@ -53,17 +54,17 @@ class _ActResponseCapture:
|
||||
return '\n'.join(self.responses) if self.responses else "No response captured."
|
||||
|
||||
|
||||
class CavemanAgent:
|
||||
"""Caveman AI agent - minimal token usage variant."""
|
||||
class BaseAgent(Agent):
|
||||
"""Base AI agent implementing common LMStudio interaction patterns."""
|
||||
|
||||
def __init__(self, model_name: str) -> None:
|
||||
self.model_name: str = model_name
|
||||
self.model: Any | None = None
|
||||
self.system_prompt: str = CAVEMAN_PROMPT
|
||||
self.system_prompt: str = ""
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize the LM Studio model."""
|
||||
logger.info(f"Initializing CavemanAgent with model: {self.model_name}")
|
||||
logger.info(f"Initializing agent with model: {self.model_name}")
|
||||
self.model = lms.llm(self.model_name)
|
||||
|
||||
async def run(self, user_input: str) -> str:
|
||||
@@ -78,12 +79,12 @@ class CavemanAgent:
|
||||
]
|
||||
|
||||
try:
|
||||
logger.info(f"Running CavemanAgent interactively (input length: {len(user_input)})")
|
||||
logger.info(f"Running agent interactively (input length: {len(user_input)})")
|
||||
response = await self.model.respond(user_input, messages=messages)
|
||||
logger.info(f"CavemanAgent responded successfully (response length: {len(response)})")
|
||||
logger.info(f"Agent responded successfully (response length: {len(response)})")
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"CavemanAgent execution error: {e}")
|
||||
logger.error(f"Agent execution error: {e}")
|
||||
return f"Error in agent execution: {str(e)}"
|
||||
|
||||
async def run_with_tools(self, user_input: str, tools: list[Any]) -> str:
|
||||
@@ -94,14 +95,22 @@ class CavemanAgent:
|
||||
|
||||
try:
|
||||
capture = _ActResponseCapture()
|
||||
logger.info(f"Calling LMStudio act() on CavemanAgent with {len(tools)} tools...")
|
||||
logger.info(f"Calling LMStudio act() on agent with {len(tools)} tools...")
|
||||
result: lms.ActResult = self.model.act(user_input, tools=tools, on_message=capture)
|
||||
logger.info(f"act() on CavemanAgent returned: {result}")
|
||||
logger.info(f"act() on agent returned: {result}")
|
||||
response: str = capture.full_response
|
||||
if not response or response == "No response captured.":
|
||||
logger.warning(f"Act completed with {result.rounds} rounds but no response was captured.")
|
||||
return f"Act completed with {result.rounds} rounds but no response captured."
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"CavemanAgent tool execution error: {e}")
|
||||
logger.error(f"Agent tool execution error: {e}")
|
||||
return f"Error in agent tool execution: {str(e)}"
|
||||
|
||||
|
||||
class CavemanAgent(BaseAgent):
|
||||
"""Caveman AI agent - minimal token usage variant."""
|
||||
|
||||
def __init__(self, model_name: str) -> None:
|
||||
super().__init__(model_name)
|
||||
self.system_prompt = CAVEMAN_PROMPT
|
||||
|
||||
+5
-100
@@ -1,108 +1,13 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import lmstudio as lms
|
||||
from typing import Any, Callable
|
||||
from .prompt import CAVEMAN_PROMPT
|
||||
from core.agent import BaseAgent
|
||||
from .coding_prompt import CODING_AGENT_SYSTEM_PROMPT
|
||||
|
||||
logger: logging.Logger = logging.getLogger("agent-coding")
|
||||
|
||||
|
||||
class _ActResponseCapture:
|
||||
"""Captures the AI response from LMStudio act() callback."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.responses: list[str] = []
|
||||
|
||||
def __call__(self, message: Any) -> None:
|
||||
content: str = ""
|
||||
if hasattr(message, 'content'):
|
||||
content = message.content
|
||||
elif hasattr(message, 'text'):
|
||||
content = message.text
|
||||
elif hasattr(message, 'response'):
|
||||
content = message.response
|
||||
elif hasattr(message, 'message'):
|
||||
content = message.message
|
||||
else:
|
||||
return
|
||||
|
||||
if isinstance(content, list):
|
||||
parts: list[str] = []
|
||||
for item in content:
|
||||
if isinstance(item, dict):
|
||||
text: str = item.get('text', '')
|
||||
if isinstance(text, list):
|
||||
parts.extend([str(t) for t in text])
|
||||
else:
|
||||
parts.append(str(text))
|
||||
elif isinstance(item, str):
|
||||
parts.append(item)
|
||||
elif hasattr(item, 'text'):
|
||||
parts.append(str(item.text))
|
||||
elif hasattr(item, 'content'):
|
||||
parts.append(str(item.content))
|
||||
content = ''.join(parts)
|
||||
elif not isinstance(content, str):
|
||||
content = str(content)
|
||||
|
||||
if content.strip():
|
||||
self.responses.append(content.strip())
|
||||
|
||||
@property
|
||||
def full_response(self) -> str:
|
||||
return '\n'.join(self.responses) if self.responses else "No response captured."
|
||||
|
||||
|
||||
class CodingAgent:
|
||||
"""AI agent that interacts with LMStudio models and tools."""
|
||||
class CodingAgent(BaseAgent):
|
||||
"""AI agent that interacts with LMStudio models and tools for coding tasks."""
|
||||
|
||||
def __init__(self, model_name: str) -> None:
|
||||
self.model_name: str = model_name
|
||||
self.model: Any | None = None
|
||||
self.system_prompt: str = CODING_AGENT_SYSTEM_PROMPT
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize the LM Studio model."""
|
||||
logger.info(f"Initializing CodingAgent with model: {self.model_name}")
|
||||
self.model = lms.llm(self.model_name)
|
||||
|
||||
async def run(self, user_input: str) -> str:
|
||||
"""Run a single interaction with the agent."""
|
||||
if self.model is None:
|
||||
await self.initialize()
|
||||
assert self.model is not None
|
||||
|
||||
messages: list[dict[str, str]] = [
|
||||
{"role": "system", "content": self.system_prompt},
|
||||
{"role": "user", "content": user_input},
|
||||
]
|
||||
|
||||
try:
|
||||
logger.info(f"Running CodingAgent interactively (input length: {len(user_input)})")
|
||||
response = await self.model.respond(user_input, messages=messages)
|
||||
logger.info(f"CodingAgent responded successfully (response length: {len(response)})")
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"CodingAgent execution error: {e}")
|
||||
return f"Error in agent execution: {str(e)}"
|
||||
|
||||
async def run_with_tools(self, user_input: str, tools: list[Callable[..., Any]]) -> str:
|
||||
"""Run the agent with tool calling capability."""
|
||||
if self.model is None:
|
||||
await self.initialize()
|
||||
assert self.model is not None
|
||||
|
||||
try:
|
||||
capture = _ActResponseCapture()
|
||||
logger.info(f"Calling LMStudio act() on CodingAgent with {len(tools)} tools...")
|
||||
result: lms.ActResult = self.model.act(user_input, tools=tools, on_message=capture)
|
||||
logger.info(f"act() on CodingAgent returned: {result}")
|
||||
response: str = capture.full_response
|
||||
if not response or response == "No response captured.":
|
||||
logger.warning(f"Act completed with {result.rounds} rounds but no response was captured.")
|
||||
return f"Act completed with {result.rounds} rounds but no response captured."
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"CodingAgent tool execution error: {e}")
|
||||
return f"Error in agent tool execution: {str(e)}"
|
||||
super().__init__(model_name)
|
||||
self.system_prompt = CODING_AGENT_SYSTEM_PROMPT
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
from core.agent import BaseAgent
|
||||
from core.prompts import COORDINATOR_SYSTEM_PROMPT
|
||||
from core.coordinator_tools import CoordinatorTools
|
||||
|
||||
logger: logging.Logger = logging.getLogger("agent-coordinator")
|
||||
|
||||
|
||||
class CoordinatorNoToolCalledError(Exception):
|
||||
"""Raised when the Coordinator Agent completes execution without calling any routing tool."""
|
||||
pass
|
||||
|
||||
|
||||
class CoordinatorAgent(BaseAgent):
|
||||
"""AI agent that coordinates Gitea issues and decides the next action."""
|
||||
|
||||
def __init__(self, model_name: str) -> None:
|
||||
super().__init__(model_name)
|
||||
self.system_prompt = COORDINATOR_SYSTEM_PROMPT
|
||||
|
||||
async def decide_action(
|
||||
self,
|
||||
mission: str,
|
||||
planning_tools: list[Callable[..., Any]],
|
||||
coord_tools: CoordinatorTools,
|
||||
) -> str:
|
||||
"""Run the Coordinator Agent and ensure a tool is called."""
|
||||
coord_tools_list: list[Callable[..., Any]] = [
|
||||
coord_tools.propose_plan,
|
||||
coord_tools.start_implementation,
|
||||
coord_tools.answer_question,
|
||||
coord_tools.close_issue,
|
||||
coord_tools.take_no_action,
|
||||
]
|
||||
combined_tools: list[Callable[..., Any]] = planning_tools + coord_tools_list
|
||||
|
||||
logger.info("Running CoordinatorAgent to decide action...")
|
||||
response_text: str = await self.run_with_tools(mission, combined_tools)
|
||||
|
||||
if not coord_tools.tool_called:
|
||||
logger.warning("CoordinatorAgent did not call any tools!")
|
||||
raise CoordinatorNoToolCalledError(
|
||||
"CoordinatorAgent failed to call a routing tool during execution."
|
||||
)
|
||||
|
||||
return response_text
|
||||
+601
-614
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@ from core.interfaces import (
|
||||
)
|
||||
from core.coding_agent import CodingAgent
|
||||
from core.agent import CavemanAgent
|
||||
from core.coordinator_agent import CoordinatorAgent
|
||||
from core.planning_agent import PlanningAgent
|
||||
from gitea.workspace import WorkspaceManager
|
||||
|
||||
logger: logging.Logger = logging.getLogger("core-factory")
|
||||
@@ -65,6 +67,16 @@ class AgentFactory:
|
||||
logger.info(f"Factory creating CavemanAgent with model: {model_name}")
|
||||
return CavemanAgent(model_name)
|
||||
|
||||
@staticmethod
|
||||
def create_coordinator_agent(model_name: str) -> CoordinatorAgent:
|
||||
logger.info(f"Factory creating CoordinatorAgent with model: {model_name}")
|
||||
return CoordinatorAgent(model_name)
|
||||
|
||||
@staticmethod
|
||||
def create_planning_agent(model_name: str) -> PlanningAgent:
|
||||
logger.info(f"Factory creating PlanningAgent with model: {model_name}")
|
||||
return PlanningAgent(model_name)
|
||||
|
||||
|
||||
class WorkspaceFactory:
|
||||
"""Factory for creating workspace manager instances."""
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import logging
|
||||
from core.agent import BaseAgent
|
||||
from .coding_prompt import CODING_AGENT_SYSTEM_PROMPT
|
||||
|
||||
logger: logging.Logger = logging.getLogger("agent-planning")
|
||||
|
||||
|
||||
class PlanningAgent(BaseAgent):
|
||||
"""AI agent that analyzes a PR/issue and builds an implementation plan."""
|
||||
|
||||
def __init__(self, model_name: str) -> None:
|
||||
super().__init__(model_name)
|
||||
self.system_prompt = CODING_AGENT_SYSTEM_PROMPT
|
||||
@@ -94,7 +94,8 @@ def test_run_verification_failure(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_planning_and_coding_phases(mock_agent_class: MagicMock) -> None:
|
||||
@patch("core.dispatcher.PlanningAgent")
|
||||
async def test_dispatch_planning_and_coding_phases(mock_planning_class: MagicMock, mock_coding_class: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
@@ -117,14 +118,14 @@ async def test_dispatch_planning_and_coding_phases(mock_agent_class: MagicMock)
|
||||
mock_client.get_pull_request_files.return_value = []
|
||||
mock_client.get_pr_reviews.return_value = []
|
||||
|
||||
# Mock CodingAgent instances
|
||||
# Mock agent instances
|
||||
mock_planning_agent = MagicMock()
|
||||
mock_planning_agent.run_with_tools = AsyncMock(return_value="Plan: Modify file A")
|
||||
mock_planning_class.return_value = mock_planning_agent
|
||||
|
||||
mock_coding_agent = MagicMock()
|
||||
mock_coding_agent.run_with_tools = AsyncMock(return_value="PR #1 Created")
|
||||
|
||||
mock_agent_class.side_effect = [mock_planning_agent, mock_coding_agent]
|
||||
mock_coding_class.return_value = mock_coding_agent
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
|
||||
|
||||
+57
-83
@@ -38,8 +38,8 @@ async def test_dispatch_skips_issue_with_existing_pr() -> None:
|
||||
mock_client.list_repo_pull_requests.assert_called_once_with("meeks", "repo1")
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_processes_issue_without_pr(mock_agent_class: MagicMock) -> None:
|
||||
@patch("core.dispatcher.CoordinatorAgent")
|
||||
async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
@@ -52,17 +52,14 @@ async def test_dispatch_processes_issue_without_pr(mock_agent_class: MagicMock)
|
||||
mock_client.list_repo_pull_requests.return_value = [pr]
|
||||
mock_client.get_issue_comments.return_value = []
|
||||
|
||||
# Mock CodingAgent run_with_tools
|
||||
mock_agent_instance = MagicMock()
|
||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "PROPOSE_PLAN",
|
||||
"reasoning": "Plan needs to be proposed first.",
|
||||
"comment_body": "### Proposed Plan\\n- change X",
|
||||
"approved_plan": ""
|
||||
}
|
||||
```""")
|
||||
mock_agent_class.return_value = mock_agent_instance
|
||||
# Mock CoordinatorAgent invoking propose_plan tool
|
||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||
coord_tools.propose_plan(plan="- change X", issue_number=42)
|
||||
return "Agent proposed plan."
|
||||
|
||||
mock_coord_instance = MagicMock()
|
||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||
mock_coord_class.return_value = mock_coord_instance
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
|
||||
@@ -260,8 +257,8 @@ def test_is_awaiting_reply_no_marker_not_detected() -> None:
|
||||
assert dispatcher._is_awaiting_reply(comments) is False
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_proposes_plan(mock_agent_class: MagicMock) -> None:
|
||||
@patch("core.dispatcher.CoordinatorAgent")
|
||||
async def test_dispatch_proposes_plan(mock_coord_class: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
@@ -269,15 +266,12 @@ async def test_dispatch_proposes_plan(mock_agent_class: MagicMock) -> None:
|
||||
mock_client.get_issue_comments.return_value = []
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
|
||||
mock_agent_instance = MagicMock()
|
||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "PROPOSE_PLAN",
|
||||
"reasoning": "We need to add a new endpoint.",
|
||||
"comment_body": "### Proposed Plan\\n- Add endpoint\\n<!-- agent:plan-proposal -->\\n<!-- agent:awaiting-reply -->"
|
||||
}
|
||||
```""")
|
||||
mock_agent_class.return_value = mock_agent_instance
|
||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||
coord_tools.propose_plan(plan="- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->", issue_number=42)
|
||||
return "Agent proposed plan."
|
||||
mock_coord_instance = MagicMock()
|
||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||
mock_coord_class.return_value = mock_coord_instance
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
work_item = WorkItem(
|
||||
@@ -291,11 +285,11 @@ async def test_dispatch_proposes_plan(mock_agent_class: MagicMock) -> None:
|
||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||
assert len(results) == 1
|
||||
assert "POSTED_COMMENT: PROPOSE_PLAN" in results[0]
|
||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "### Proposed Plan\n- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->")
|
||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "### Proposed Implementation Plan\n\n- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->\n\nIs this plan ok for implementation or do you have any comments/changes?\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->")
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_answers_question(mock_agent_class: MagicMock) -> None:
|
||||
@patch("core.dispatcher.CoordinatorAgent")
|
||||
async def test_dispatch_answers_question(mock_coord_class: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
@@ -303,15 +297,12 @@ async def test_dispatch_answers_question(mock_agent_class: MagicMock) -> None:
|
||||
mock_client.get_issue_comments.return_value = []
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
|
||||
mock_agent_instance = MagicMock()
|
||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "ANSWER_QUESTION",
|
||||
"reasoning": "This is a question about how X works.",
|
||||
"comment_body": "X works by doing Y.\\n<!-- agent:question-response -->\\n<!-- agent:awaiting-reply -->"
|
||||
}
|
||||
```""")
|
||||
mock_agent_class.return_value = mock_agent_instance
|
||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||
coord_tools.answer_question(answer="X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->", issue_number=42)
|
||||
return "Agent answered question."
|
||||
mock_coord_instance = MagicMock()
|
||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||
mock_coord_class.return_value = mock_coord_instance
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
work_item = WorkItem(
|
||||
@@ -325,11 +316,11 @@ async def test_dispatch_answers_question(mock_agent_class: MagicMock) -> None:
|
||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||
assert len(results) == 1
|
||||
assert "POSTED_COMMENT: ANSWER_QUESTION" in results[0]
|
||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->")
|
||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->\n\nIs this answer satisfactory?\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->")
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_closes_issue_on_satisfaction(mock_agent_class: MagicMock) -> None:
|
||||
@patch("core.dispatcher.CoordinatorAgent")
|
||||
async def test_dispatch_closes_issue_on_satisfaction(mock_coord_class: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
@@ -342,15 +333,12 @@ async def test_dispatch_closes_issue_on_satisfaction(mock_agent_class: MagicMock
|
||||
_make_comment("michael", "Yes, thanks! That makes sense.")
|
||||
]
|
||||
|
||||
mock_agent_instance = MagicMock()
|
||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "CLOSE_ISSUE",
|
||||
"reasoning": "User is satisfied.",
|
||||
"comment_body": "Closing the issue now. Let me know if you need anything else!"
|
||||
}
|
||||
```""")
|
||||
mock_agent_class.return_value = mock_agent_instance
|
||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||
coord_tools.close_issue(comment="Closing the issue now. Let me know if you need anything else!", issue_number=42)
|
||||
return "Agent closed issue."
|
||||
mock_coord_instance = MagicMock()
|
||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||
mock_coord_class.return_value = mock_coord_instance
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
work_item = WorkItem(
|
||||
@@ -370,7 +358,8 @@ async def test_dispatch_closes_issue_on_satisfaction(mock_agent_class: MagicMock
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_agent_class: MagicMock, mock_run: MagicMock) -> None:
|
||||
@patch("core.dispatcher.CoordinatorAgent")
|
||||
async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_coord_class: MagicMock, mock_coding_class: MagicMock, mock_run: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
@@ -386,20 +375,13 @@ async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_agent_cla
|
||||
mock_client.create_pull_request.return_value = mock_pr
|
||||
|
||||
# Mock planning agent deciding EXECUTE_PLAN
|
||||
mock_agent_instance1 = MagicMock()
|
||||
mock_agent_instance1.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "EXECUTE_PLAN",
|
||||
"reasoning": "Plan was approved.",
|
||||
"approved_plan": "Step 1. Code X"
|
||||
}
|
||||
```""")
|
||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||
coord_tools.start_implementation(approved_plan="Step 1. Code X", issue_number=42)
|
||||
return "Agent decided execute plan."
|
||||
mock_coord_class.return_value.decide_action = AsyncMock(side_effect=mock_decide)
|
||||
|
||||
# Mock coding agent executing plan
|
||||
mock_agent_instance2 = MagicMock()
|
||||
mock_agent_instance2.run_with_tools = AsyncMock(return_value="PR Completed Successfully.")
|
||||
|
||||
mock_agent_class.side_effect = [mock_agent_instance1, mock_agent_instance2]
|
||||
mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Completed Successfully.")
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
work_item = WorkItem(
|
||||
@@ -427,7 +409,8 @@ async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_agent_cla
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_resumes_wip_pr(mock_agent_class: MagicMock, mock_run: MagicMock) -> None:
|
||||
@patch("core.dispatcher.CoordinatorAgent")
|
||||
async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_class: MagicMock, mock_run: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
@@ -450,20 +433,13 @@ async def test_dispatch_resumes_wip_pr(mock_agent_class: MagicMock, mock_run: Ma
|
||||
mock_client.get_pull_request_comments.return_value = []
|
||||
|
||||
# Mock planning agent deciding EXECUTE_PLAN
|
||||
mock_agent_instance1 = MagicMock()
|
||||
mock_agent_instance1.run_with_tools = AsyncMock(return_value="""```json
|
||||
{
|
||||
"action": "EXECUTE_PLAN",
|
||||
"reasoning": "WIP PR exists, resume coding.",
|
||||
"approved_plan": "Step 1. Resume coding"
|
||||
}
|
||||
```""")
|
||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||
coord_tools.start_implementation(approved_plan="Step 1. Resume coding", issue_number=42)
|
||||
return "Agent decided execute plan."
|
||||
mock_coord_class.return_value.decide_action = AsyncMock(side_effect=mock_decide)
|
||||
|
||||
# Mock coding agent executing plan
|
||||
mock_agent_instance2 = MagicMock()
|
||||
mock_agent_instance2.run_with_tools = AsyncMock(return_value="PR Updated Successfully.")
|
||||
|
||||
mock_agent_class.side_effect = [mock_agent_instance1, mock_agent_instance2]
|
||||
mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Updated Successfully.")
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
work_item = WorkItem(
|
||||
@@ -528,8 +504,8 @@ def test_coordinator_tools_registration() -> None:
|
||||
assert tools.arguments == {"approved_plan": "my approved plan", "issue_number": 42}
|
||||
|
||||
|
||||
@patch("core.dispatcher.CodingAgent")
|
||||
async def test_dispatch_uses_coordinator_tool_calling(mock_agent_class: MagicMock) -> None:
|
||||
@patch("core.dispatcher.CoordinatorAgent")
|
||||
async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMock) -> None:
|
||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
||||
|
||||
@@ -538,15 +514,13 @@ async def test_dispatch_uses_coordinator_tool_calling(mock_agent_class: MagicMoc
|
||||
mock_client.get_authenticated_user.return_value = UserModel(login="meeks-ai")
|
||||
|
||||
# Mock agent invoking propose_plan tool
|
||||
async def mock_run_tools(mission: str, tools: list[any]) -> str:
|
||||
for t in tools:
|
||||
if getattr(t, "__name__", "") == "propose_plan":
|
||||
t(plan="Step 1. Code X", issue_number=42)
|
||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||
coord_tools.propose_plan(plan="Step 1. Code X", issue_number=42)
|
||||
return "Agent finished turn after tool calling."
|
||||
|
||||
mock_agent_instance = MagicMock()
|
||||
mock_agent_instance.run_with_tools = AsyncMock(side_effect=mock_run_tools)
|
||||
mock_agent_class.return_value = mock_agent_instance
|
||||
mock_coord_instance = MagicMock()
|
||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||
mock_coord_class.return_value = mock_coord_instance
|
||||
|
||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
||||
work_item = WorkItem(
|
||||
|
||||
Reference in New Issue
Block a user