This commit is contained in:
Dmitry 2025-11-21 02:56:30 +03:00
parent f9484d962d
commit 83e45287cc
4 changed files with 308 additions and 12 deletions

View file

@ -7,11 +7,12 @@ import subprocess
import time import time
from pathlib import Path from pathlib import Path
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional from typing import Optional, List
from config import AgentConfig, get_test_command, get_lint_command from config import AgentConfig, get_test_command, get_lint_command
from task_manager import TaskManager, GlobalLock, Task from task_manager import TaskManager, GlobalLock, Task
from cursor_cli_wrapper import CursorCLI, CursorResultStatus from cursor_cli_wrapper import CursorCLI, CursorResultStatus
from planning_agent import PlanningAgent
class AgentOrchestrator: class AgentOrchestrator:
@ -107,6 +108,28 @@ class AgentOrchestrator:
if retry_count >= self.config.max_retries: if retry_count >= self.config.max_retries:
print(f"\n❌ Task {next_task.id} failed after {retry_count} retries") print(f"\n❌ Task {next_task.id} failed after {retry_count} retries")
# Try to break down the task into subtasks
print(f"\n🔀 Attempting to break down task {next_task.id} into subtasks...")
planning_agent = PlanningAgent(self.config)
failure_reason = f"Failed after {retry_count} retry attempts. Task may be too complex."
breakdown_success = planning_agent.break_down_task(
next_task,
failure_reason
)
if breakdown_success:
print(f"\n✅ Task {next_task.id} successfully broken down into subtasks")
print(" Subtasks have been added to task_list.json")
# Mark original task as failed
self.task_manager.mark_task_failed(
next_task.id,
f"Failed after {retry_count} retries. Broken down into subtasks."
)
else:
print(f"\n⚠️ Failed to break down task {next_task.id}")
print(" Marking task as failed without subtasks")
self.task_manager.mark_task_failed( self.task_manager.mark_task_failed(
next_task.id, next_task.id,
f"Failed after {retry_count} retries" f"Failed after {retry_count} retries"
@ -114,6 +137,7 @@ class AgentOrchestrator:
# Create GitHub issue # Create GitHub issue
self._create_issue_for_failed_task(next_task) self._create_issue_for_failed_task(next_task)
break break
else: else:
print(f"\n⚠️ Task {next_task.id} failed, retrying ({retry_count}/{self.config.max_retries})") print(f"\n⚠️ Task {next_task.id} failed, retrying ({retry_count}/{self.config.max_retries})")
@ -189,8 +213,30 @@ class AgentOrchestrator:
print(f"\n📊 Agent completed:") print(f"\n📊 Agent completed:")
print(f" - Files created: {len(result.files_created)}") print(f" - Files created: {len(result.files_created)}")
print(f" - Files modified: {len(result.files_modified)}") print(f" - Files modified: {len(result.files_modified)}")
print(f" - Files read: {len(result.files_read)}")
print(f" - Tool calls: {result.tool_calls}") print(f" - Tool calls: {result.tool_calls}")
# Check if any files were actually changed
if len(result.files_created) == 0 and len(result.files_modified) == 0:
print("\n⚠️ WARNING: Agent completed but no files were created or modified!")
print(f" Agent read {len(result.files_read)} file(s) but made no changes.")
print(" This might indicate:")
print(" - Task was already completed")
print(" - Agent only analyzed files without making changes")
print(" - Agent encountered an issue but didn't report it")
# Check git status to see if there are any uncommitted changes
git_changes = self._check_git_changes()
if git_changes:
print(f"\n However, git shows {len(git_changes)} uncommitted change(s):")
for change in git_changes[:5]: # Show first 5
print(f" - {change}")
if len(git_changes) > 5:
print(f" ... and {len(git_changes) - 5} more")
else:
print("\n Git confirms: no changes detected in working directory.")
print(" Task may need to be retried or task description clarified.")
# Pull latest changes before committing # Pull latest changes before committing
print("\n🔄 Pulling latest changes...") print("\n🔄 Pulling latest changes...")
self._git_pull() self._git_pull()
@ -331,6 +377,32 @@ Changes:
print(f"❌ Test output:\n{result.stdout}") print(f"❌ Test output:\n{result.stdout}")
return False return False
def _check_git_changes(self) -> List[str]:
"""Check for uncommitted changes in git."""
# Check status for modified, added, deleted files
result = subprocess.run(
["git", "status", "--porcelain"],
cwd=self.config.project_root,
capture_output=True,
text=True
)
if result.returncode != 0:
return []
changes = []
for line in result.stdout.strip().split('\n'):
if line.strip():
# Format: "XY filename" where X is staging area, Y is working tree
# We care about files that are modified (M), added (A), deleted (D), or renamed (R)
status = line[:2]
filename = line[3:].strip()
if any(c in status for c in ['M', 'A', 'D', 'R']):
changes.append(filename)
return changes
def _git_pull(self) -> bool: def _git_pull(self) -> bool:
"""Pull latest changes from remote.""" """Pull latest changes from remote."""
result = subprocess.run( result = subprocess.run(

View file

@ -329,17 +329,21 @@ class CursorCLI:
if "success" in result: if "success" in result:
success = result["success"] success = result["success"]
path = tool_call["writeToolCall"].get("args", {}).get("path", "unknown") path = tool_call["writeToolCall"].get("args", {}).get("path", "unknown")
lines = success.get("linesCreated", 0) lines_created = success.get("linesCreated", 0)
lines_modified = success.get("linesModified", 0)
if lines > 0: if lines_created > 0:
files_created.append(path) files_created.append(path)
if self.verbose: if self.verbose:
print(f" ✅ Created {lines} lines") print(f" ✅ Created {lines_created} lines")
else: elif lines_modified > 0:
files_modified.append(path) files_modified.append(path)
if self.verbose: if self.verbose:
lines_modified = success.get("linesModified", 0)
print(f" ✅ Modified {lines_modified} lines") print(f" ✅ Modified {lines_modified} lines")
else:
# File was written but not actually changed
if self.verbose:
print(f" ⚠️ Written but no changes detected (file unchanged)")
elif "readToolCall" in tool_call: elif "readToolCall" in tool_call:
result = tool_call["readToolCall"].get("result", {}) result = tool_call["readToolCall"].get("result", {})

View file

@ -9,6 +9,7 @@ from typing import Dict, Any, List
from config import AgentConfig from config import AgentConfig
from cursor_cli_wrapper import CursorCLI, CursorResultStatus from cursor_cli_wrapper import CursorCLI, CursorResultStatus
from task_manager import Task
class PlanningAgent: class PlanningAgent:
@ -302,6 +303,225 @@ Component: {self.config.component}
else: else:
print("⚠️ No changes to commit (task list unchanged)") print("⚠️ No changes to commit (task list unchanged)")
def break_down_task(self, task: Task, failure_reason: str = "") -> bool:
"""
Break down a failed task into smaller subtasks.
Args:
task: The task that failed
failure_reason: Reason why the task failed
Returns:
True if subtasks were successfully created, False otherwise
"""
print(f"\n🔀 Breaking down task {task.id} into subtasks...")
print(f" Task: {task.title}")
print(f" Reason: {failure_reason}")
try:
# Build prompt for breaking down the task
prompt = self._build_breakdown_prompt(task, failure_reason)
# Run cursor agent to break down the task
print("\n🤖 Running Cursor Agent to break down task...")
result = self.cursor_cli.run_agent(
task_description=prompt,
force=True,
max_iterations=3
)
if result.status != CursorResultStatus.SUCCESS:
print(f"❌ Failed to break down task: {result.error}")
return False
# Load current task list before breakdown
if not self.config.task_list_path.exists():
print(f"❌ Task list not found: {self.config.task_list_path}")
return False
# Get original task list for comparison
original_task_list = self._load_and_validate_task_list()
if not original_task_list:
print("❌ Invalid task list")
return False
original_tasks = original_task_list.get("tasks", [])
original_task_ids = {t.get("id") for t in original_tasks}
original_count = len(original_tasks)
# Reload to get updated task list after agent execution
task_list = self._load_and_validate_task_list()
if not task_list:
print("❌ Failed to reload task list after breakdown")
return False
updated_tasks = task_list.get("tasks", [])
new_count = len(updated_tasks)
# Find subtasks - they should have IDs containing the original task ID
# or be new tasks added after the breakdown
# Look for tasks with IDs like "TASK-001-SUB-1" or similar patterns
subtasks = [
t for t in updated_tasks
if (task.id in t.get("id", "") and t.get("id") != task.id) or
(t.get("id", "").startswith(task.id + "-") or
t.get("id", "").startswith(task.id + "_"))
]
# If no subtasks found by ID pattern, check if task count increased
if len(subtasks) == 0:
if new_count > original_count:
# New tasks were added, assume they are subtasks
# Get tasks that weren't in the original list
subtasks = [t for t in updated_tasks if t.get("id") not in original_task_ids]
if len(subtasks) == 0:
print("⚠️ No subtasks found. Agent may not have created them.")
print(" Checking if task was updated instead...")
# Check if the original task was modified
original_task = next((t for t in updated_tasks if t.get("id") == task.id), None)
if original_task:
print(f" Task {task.id} still exists in task list")
return False
else:
print(f" Task {task.id} was removed but no subtasks found")
return False
print(f"\n✅ Task broken down into {len(subtasks)} subtask(s):")
for subtask in subtasks:
print(f" - {subtask['id']}: {subtask['title']} ({subtask.get('estimated_hours', 0)}h)")
# Mark original task as failed (if it still exists)
original_task = next((t for t in updated_tasks if t.get("id") == task.id), None)
if original_task:
original_task["status"] = "failed"
original_task["description"] += f"\n\n[FAILED] {failure_reason}\n[REPLACED BY] {', '.join([s['id'] for s in subtasks])}"
self._save_task_list(task_list)
# Commit the changes
self._commit_task_list()
return True
except Exception as e:
print(f"\n❌ Error breaking down task: {e}")
import traceback
traceback.print_exc()
return False
def _build_breakdown_prompt(self, task: Task, failure_reason: str) -> str:
"""Build prompt for breaking down a task into subtasks."""
# Load planning prompt template
prompt_file = self.config.prompts_dir / "planning_prompt.md"
if prompt_file.exists():
with open(prompt_file, 'r') as f:
template = f.read()
else:
template = "You are an AI planning agent. Break down complex tasks into smaller subtasks.\n\n"
# Load current task list for context
context = ""
if self.config.task_list_path.exists():
with open(self.config.task_list_path, 'r') as f:
current_tasks = json.load(f)
context += f"### Current task_list.json:\n```json\n{json.dumps(current_tasks, indent=2)}\n```\n\n"
prompt = f"""{template}
## TASK BREAKDOWN REQUEST
A task has failed after multiple attempts and needs to be broken down into smaller, more manageable subtasks.
### Failed Task:
```json
{json.dumps(task.to_dict(), indent=2)}
```
### Failure Reason:
{failure_reason if failure_reason else "Task was too complex and could not be completed after multiple retry attempts."}
### Current Context:
{context}
### Your Task:
1. **Analyze the failed task** - Understand why it might have been too complex
2. **Break it down** into 3-5 smaller, focused subtasks
3. **Each subtask should be:**
- 1-4 hours of work (preferably 2-3 hours)
- Independently testable and completable
- Have clear acceptance criteria
- Specify files to modify
4. **Set dependencies:**
- Subtasks should NOT depend on the original task ID (since it failed)
- Set dependencies between subtasks if there's a logical order
- If subtasks are independent, they can have empty dependencies
5. **Update task_list.json:**
- Add the new subtasks to the tasks array
- You can either:
a) Mark the original task as "failed" and add subtasks
b) Replace the original task with subtasks
- Keep all other tasks unchanged
### Subtask Structure:
Each subtask should follow this format:
```json
{{
"id": "{task.id}-SUB-1",
"title": "Clear, focused title for this subtask",
"priority": "{task.priority}",
"status": "pending",
"estimated_hours": 2.5,
"description": "Detailed description of what this subtask accomplishes",
"acceptance_criteria": [
"Specific, testable criterion 1",
"Specific, testable criterion 2",
"Comprehensive test coverage"
],
"dependencies": [],
"files_to_modify": ["path/to/specific/file.dart"],
"component": "{task.component}"
}}
```
### Guidelines:
- **Keep subtasks small**: 1-4 hours each, preferably 2-3 hours
- **Make them independent**: Each subtask should be completable on its own
- **Clear dependencies**: If subtasks depend on each other, set dependencies
- **Preserve priority**: Use the same priority as the original task
- **Specific files**: Each subtask should modify specific files, not all files from original task
- **Testable**: Each subtask should have clear acceptance criteria
### Example Breakdown:
If original task was: "Implement user authentication system (8 hours)"
Good breakdown:
- SUB-1: Create authentication models and DTOs (2h)
- SUB-2: Implement authentication service layer (2h)
- SUB-3: Create authentication API endpoints (2h)
- SUB-4: Add authentication UI components (2h)
Now break down task `{task.id}` into subtasks and update the task_list.json file!
"""
return prompt
def _save_task_list(self, task_list: Dict[str, Any]) -> None:
"""Save task list to file."""
with open(self.config.task_list_path, 'w') as f:
json.dump(task_list, f, indent=2)
def main(): def main():
"""Main entry point.""" """Main entry point."""