Feat/tool based coordinator routing (#5)

Co-authored-by: Michael <michael@example.com>
Reviewed-on: #5
This commit is contained in:
2026-06-29 21:17:42 +02:00
parent e38a80532b
commit 7f66d09d9e
10 changed files with 301 additions and 87 deletions
+91 -79
View File
@@ -10,10 +10,11 @@ from gitea.tools.research_tools import ResearchTools
from gitea.tools.gitea_tools import GiteaTools
from gitea.client import GiteaClient
from core.coding_prompt import CODING_AGENT_SYSTEM_PROMPT
from core.coordinator_tools import CoordinatorTools
from core.prompts import COORDINATOR_SYSTEM_PROMPT
from gitea.config import AGENT_MODEL_ID
from gitea.workspace import WorkspaceManager
from gitea.models import CommentModel, PullRequestFileModel, PullRequestModel, IssueModel
logger: logging.Logger = logging.getLogger("agent-dispatcher")
AGENT_USERNAMES: frozenset[str] = frozenset({"meeks-ai", "agent-bot"})
@@ -319,44 +320,6 @@ class AgentDispatcher:
f"PR Reviews:\n{reviews_str}"
)
STATE_ANALYSIS_SYSTEM_PROMPT = (
"You are an AI Coordinator. Your job is to analyze Gitea issues and pull requests, "
"read the conversation history, and determine the next action for the agent.\n\n"
"You must choose one of the following actions:\n"
"1. `PROPOSE_PLAN`: Choose this if code changes are needed to resolve the issue, and either:\n"
" - No plan has been proposed yet by the AI agent.\n"
" - Or a plan was proposed, but the human replied with feedback, corrections, or requests for changes, so we need to propose a revised plan.\n"
" You will write a detailed, step-by-step implementation plan (listing files to modify/create, specific changes to make, verification/test commands).\n"
" Your output must include a comment to post, starting with the plan and ending with a question asking if the plan is OK or if they have comments.\n"
" CRITICAL: The comment in the `comment_body` field must contain ONLY the implementation plan and the trailing confirmation question. DO NOT include your thought process, reasoning, research notes, or file analysis in the comment. Keep it concise, professional, and limited strictly to the plan itself. Any internal reasoning should be placed in the `reasoning` field of the JSON instead.\n"
" CRITICAL: The comment must contain the tags `<!-- agent:plan-proposal -->` and `<!-- agent:awaiting-reply -->` on separate lines at the very end of the comment.\n\n"
"2. `ANSWER_QUESTION`: Choose this if the issue is just a question or request for information (no code changes needed), and either:\n"
" - No answer has been provided yet by the AI agent.\n"
" - Or the agent answered, but the human replied with follow-up questions or clarifications.\n"
" You will formulate a clear, helpful answer to the question.\n"
" Your output must include a comment to post, starting with the answer and ending with a question asking if this was a good enough answer.\n"
" CRITICAL: The comment in the `comment_body` field must contain ONLY the actual answer to the question and the trailing confirmation question. DO NOT include your thought process, reasoning, research notes, or file analysis in the comment. Keep it clean, concise, helpful, and limited strictly to the answer itself. Any internal reasoning should be placed in the `reasoning` field of the JSON instead.\n"
" CRITICAL: The comment must contain the tags `<!-- agent:question-response -->` and `<!-- agent:awaiting-reply -->` on separate lines at the very end of the comment.\n\n"
"3. `EXECUTE_PLAN`: Choose this if:\n"
" - A plan was previously proposed (check the comment history) AND the human has clearly replied with approval/greenlight/go-ahead (e.g. 'yes', 'looks good', 'ok', 'go ahead', etc.).\n"
" - OR there is an existing WIP PR or a PR with requested changes, and we need to continue/resume implementing the changes.\n"
" You will extract or summarize the approved plan, incorporating any feedback the human gave in their approval/reviews.\n\n"
"4. `CLOSE_ISSUE`: Choose this if the AI agent previously answered a question (using `<!-- agent:question-response -->`) and the human has replied confirming they are satisfied or giving approval to close (e.g., 'yes', 'looks good', 'thanks', 'close it', etc.).\n"
" You will write a polite final comment to post on the issue.\n\n"
"5. `NO_ACTION`: Choose this if the issue/PR is already resolved, or if we cannot proceed for another reason.\n\n"
"You MUST respond ONLY with a JSON object inside a ```json markdown code block. Do not include other text.\n"
"CRITICAL: The `comment_body` field in the JSON must contain ONLY the implementation plan or the answer itself, and MUST NOT contain any thought process, reasoning, or internal details. Place all thought process and internal reasoning in the `reasoning` field.\n"
"Example:\n"
"```json\n"
"{\n"
" \"action\": \"PROPOSE_PLAN\",\n"
" \"reasoning\": \"No plan has been proposed yet. We need to implement ...\",\n"
" \"comment_body\": \"### Proposed Implementation Plan\\n1. Modify X\\n2. Run Y\\n\\nIs this ok for implementation?\\n<!-- agent:plan-proposal -->\\n<!-- agent:awaiting-reply -->\",\n"
" \"approved_plan\": \"\"\n"
"}\n"
"```"
)
state_analysis_mission = (
f"Analyzing issue #{item.task_number} in '{repo}'.\n\n"
f"Issue Title: {title}\n"
@@ -365,55 +328,103 @@ class AgentDispatcher:
f"Existing PR Details:\n{pr_info_str}\n"
)
# Run Coordinator Agent with CoordinatorTools
coord_tools = CoordinatorTools()
coord_tools_list = [
coord_tools.propose_plan,
coord_tools.start_implementation,
coord_tools.answer_question,
coord_tools.close_issue,
coord_tools.take_no_action,
]
combined_tools = planning_tools + coord_tools_list
logger.info(f"Analyzing conversation state for issue #{item.task_number}...")
planning_agent = CodingAgent(self._model_name)
planning_agent.system_prompt = STATE_ANALYSIS_SYSTEM_PROMPT
plan_response = await planning_agent.run_with_tools(state_analysis_mission, planning_tools)
logger.info(f"State analyzer returned: {plan_response}")
import json
decision = {}
json_match = re.search(r"```json\s*(.*?)\s*```", plan_response, re.DOTALL)
if json_match:
json_str = json_match.group(1).strip()
else:
json_str = plan_response.strip()
try:
decision = json.loads(json_str)
except Exception as e:
logger.error(f"Failed to parse planning agent decision JSON: {e}. Attempting manual extraction.")
planning_agent.system_prompt = COORDINATOR_SYSTEM_PROMPT
# Let the coordinator analyze and route
response_text = await planning_agent.run_with_tools(state_analysis_mission, combined_tools)
# Fallback if no tool was called
if not coord_tools.tool_called:
logger.info(f"Coordinator agent did not call any tools. Falling back to JSON text parsing.")
import json
decision = {}
json_match = re.search(r"```json\s*(.*?)\s*```", response_text, re.DOTALL)
if json_match:
json_str = json_match.group(1).strip()
else:
json_str = response_text.strip()
try:
start_idx = json_str.find('{')
end_idx = json_str.rfind('}')
if start_idx != -1 and end_idx != -1:
decision = json.loads(json_str[start_idx:end_idx+1])
except Exception:
pass
decision = json.loads(json_str)
except Exception as e:
try:
start_idx = json_str.find('{')
end_idx = json_str.rfind('}')
if start_idx != -1 and end_idx != -1:
decision = json.loads(json_str[start_idx:end_idx+1])
except Exception:
pass
if decision and "action" in decision:
coord_tools.action = decision["action"]
if coord_tools.action == "PROPOSE_PLAN":
coord_tools.arguments = {
"comment_body": decision.get("comment_body", "") or decision.get("reasoning", ""),
"issue_number": item.task_number
}
elif coord_tools.action == "ANSWER_QUESTION":
coord_tools.arguments = {
"comment_body": decision.get("comment_body", "") or decision.get("reasoning", ""),
"issue_number": item.task_number
}
elif coord_tools.action == "CLOSE_ISSUE":
coord_tools.arguments = {
"comment": decision.get("comment_body", "Closing the issue as resolved."),
"issue_number": item.task_number
}
elif coord_tools.action == "EXECUTE_PLAN":
coord_tools.arguments = {
"approved_plan": decision.get("approved_plan", ""),
"issue_number": item.task_number
}
if not decision or "action" not in decision:
logger.info("Fallback: assuming PROPOSE_PLAN and using raw plan_response")
decision = {
"action": "PROPOSE_PLAN",
"comment_body": f"### Proposed Implementation Plan\n\n{plan_response}\n\nIs this plan ok for implementation or do you have any comments/changes?\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->",
"approved_plan": ""
}
action = coord_tools.action
logger.info(f"Coordinator Decided Action: {action} (tool_called={coord_tools.tool_called})")
action = decision.get("action", "NO_ACTION")
reasoning = decision.get("reasoning", "")
logger.info(f"Decided Action: {action}. Reasoning: {reasoning}")
if action in ("PROPOSE_PLAN", "ANSWER_QUESTION"):
comment_body = decision.get("comment_body", "")
if action == "PROPOSE_PLAN":
comment_body = coord_tools.arguments.get("comment_body", "")
if not comment_body:
comment_body = decision.get("reasoning", "No details provided.")
plan = coord_tools.arguments.get("plan", "")
comment_body = (
f"### Proposed Implementation Plan\n\n"
f"{plan}\n\n"
f"Is this plan ok for implementation or do you have any comments/changes?\n"
f"<!-- agent:plan-proposal -->\n"
f"<!-- agent:awaiting-reply -->"
)
self._client.add_comment(owner, repo_name, item.task_number, comment_body)
results.append(f"POSTED_COMMENT: {action} comment posted to issue #{item.task_number}.")
results.append(f"POSTED_COMMENT: PROPOSE_PLAN comment posted to issue #{item.task_number}.")
break
elif action == "ANSWER_QUESTION":
comment_body = coord_tools.arguments.get("comment_body", "")
if not comment_body:
answer = coord_tools.arguments.get("answer", "")
comment_body = (
f"{answer}\n\n"
f"Is this answer satisfactory?\n"
f"<!-- agent:question-response -->\n"
f"<!-- agent:awaiting-reply -->"
)
self._client.add_comment(owner, repo_name, item.task_number, comment_body)
results.append(f"POSTED_COMMENT: ANSWER_QUESTION comment posted to issue #{item.task_number}.")
break
elif action == "CLOSE_ISSUE":
comment_body = decision.get("comment_body", "Closing the issue as resolved.")
self._client.add_comment(owner, repo_name, item.task_number, comment_body)
comment = coord_tools.arguments.get("comment", "Closing the issue as resolved.")
self._client.add_comment(owner, repo_name, item.task_number, comment)
self._client.close_issue(owner, repo_name, item.task_number)
results.append(f"CLOSED_ISSUE: Issue #{item.task_number} closed.")
break
@@ -423,6 +434,7 @@ class AgentDispatcher:
break
elif action == "EXECUTE_PLAN":
approved_plan = coord_tools.arguments.get("approved_plan", "")
pr_to_use = existing_pr
branch_name = ""
@@ -472,7 +484,7 @@ class AgentDispatcher:
f"PHASE 2: EXECUTION/CODING PHASE\n\n"
f"You are implementing changes for issue #{item.task_number} in repository '{repo}'.\n"
f"You are working on the existing Pull Request #{pr_to_use.number} on branch '{branch_name}'.\n\n"
f"--- APPROVED PLAN ---\n{decision.get('approved_plan', '')}\n--- APPROVED PLAN END ---\n\n"
f"--- APPROVED PLAN ---\n{approved_plan}\n--- APPROVED PLAN END ---\n\n"
f"Original Mission details:\n{base_mission}\n\n"
f"DIRECTIONS:\n"
f"1. Checkout the branch '{branch_name}' (it should already be checked out, or run `git checkout {branch_name}`).\n"