Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,68 @@ graph TD

Gemma & Gemini & Claude --> Orchestrator[Multi-Agent Orchestrator]

subgraph Agents
subgraph Agents Layer
Orchestrator --> Planner[Planner Agent]
Planner --> Coder[Coder Agent]
Coder --> Reviewer[Reviewer Agent]
Planner --> SpecDecider{Is SIMPLE task?}

SpecDecider -->|Yes| SimpleCoder[Simple Coder Agent]
SpecDecider -->|No| SpecAgent[Spec Agent]

SpecAgent --> UserReview[User Review & Approval]
UserReview -->|Modify/Approve| SpecCoder[Spec Coder Agent]

SimpleCoder & SpecCoder --> Reviewer[Reviewer Agent]
end

Reviewer --> Write[File System Writer]
Reviewer -->|Approved| Write[File System Writer]
Reviewer -->|Rejected| ReCoder[Nudge Coder to fix]
ReCoder --> SimpleCoder & SpecCoder
Write --> Workspace[(Workspace)]
```

> **Note on Complexity Classification**: The Task Router queries the local Gemma model (`gemma2:2b`) to classify prompt complexity. If Gemma is offline, it falls back to a regex-based keyword density and word count heuristic classifier.
### 1. How Task Routing Works

MACA evaluates task complexity before selecting a model to execute the coding assignment:
- **Complexity Assessment**:
- The **Task Router** queries the local Gemma model (gemma2:2b via Ollama) to analyze the user prompt and classify it into one of four categories: SIMPLE, MEDIUM, COMPLEX, or VERY_COMPLEX.
- *Fallback Heuristic*: If Gemma/Ollama is offline, the router falls back to a regex-based keyword density and word count heuristic classifier to make a prediction.
- **Model Assignment & Fallbacks**:
- **SIMPLE Tasks**: Handled entirely locally by the **Local Gemma Client** to minimize latency and token usage.
- **MEDIUM Tasks**: Routed preferentially to **Gemini Client** (Flash/Pro) for rapid, intelligent processing. If Gemini is configured but offline, it falls back to **Claude Client** (or local Gemma if no remote APIs are online).
- **COMPLEX or VERY_COMPLEX Tasks**: Routed preferentially to **Claude Client** (Opus/Sonnet) for advanced reasoning. If Claude is configured but offline, it falls back to **Gemini Client** (or local Gemma if necessary).

---

## 🛠️ Setup Instructions
### 2. How Agents Collaborate

Once a model client has been assigned, the **Multi-Agent Orchestrator** manages the coding pipeline using a suite of dedicated, specialized agents:

1. **Planner Agent**:
- Analyzes the task description and list of files in the workspace.
- Generates a structured Markdown implementation plan specifying the files to create or modify.
- Saves the plan to .maca/plan-<task>.md.

2. **Complexity Routing**:
- **For SIMPLE Tasks**: Bypasses the specification phase entirely. The orchestrator routes the task directly to the **Simple Coder Agent**, which implements the modifications based solely on the plan.
- **For MEDIUM/COMPLEX Tasks**: The orchestrator routes the task to the **Spec Agent** first.

3. **Spec Agent (Spec Flow)**:
- Generates a detailed **Technical Specification** document from the plan.
- Saves it to .maca/spec-<task>.md.
- **Interactive User Review**: The CLI pauses execution, alerting the user that the specification is ready for review. The user can open .maca/spec-<task>.md, modify it to add constraints or adjust design, and then hit Enter to approve and continue. If the user types /cancel, the task is cleanly aborted.
- Once approved, the orchestrator loads the specification and routes it to the **Spec Coder Agent**.

4. **Coder Agents**:
- Implements code changes based on either the plan (SimpleCoderAgent) or the technical specification (SpecCoderAgent). Both coder agents strictly enforce **Clean Code Guidelines** (modularity, variable naming, error handling, and type annotations).
- Generates file content blocks parsed via [FILE: path].
- *Verification Loop*: The orchestrator runs a QA check. If the code is incomplete compared to the specification or plan, it nudges the coder with feedback to continue implementation.

5. **Reviewer Agent**:
- Audits the coder output against **Clean Code Auditing Criteria** (readability, modularity, single responsibility, type safety, and correctness) and the original plan or specification (if available).
- **Approval & Verification**:
- If the code meets quality standards, it outputs APPROVED, and the orchestrator writes the changes to the disk.
- If not, it rejects the code, outputs a detailed feedback report, and nudges the coder to apply corrections. The loop repeats until approved (up to 10 attempts).
\n\n## 🛠️ Setup Instructions

### 1. Install and run the CLI
Use the local launcher scripts under [local/scripts](local/scripts):
Expand Down
15 changes: 15 additions & 0 deletions src/maca/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from maca.agents.base import BaseAgent
from maca.agents.coder import BaseCoderAgent, SimpleCoderAgent, SpecCoderAgent
from maca.agents.planner import PlannerAgent
from maca.agents.reviewer import ReviewerAgent
from maca.agents.spec import SpecAgent

__all__ = [
"BaseAgent",
"PlannerAgent",
"SpecAgent",
"BaseCoderAgent",
"SimpleCoderAgent",
"SpecCoderAgent",
"ReviewerAgent",
]
68 changes: 53 additions & 15 deletions src/maca/agents/base.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,82 @@
import abc
import os
import re
from typing import Any, Dict, List, Optional


class BaseAgent:
def __init__(self, name, model_client):
class BaseAgent(abc.ABC):
def __init__(self, name: str, model_client: Any, repo_path: str = "."):
self.name = name
self.model_client = model_client
self.repo_path = os.path.abspath(repo_path)

def run(self, *args, **kwargs):
raise NotImplementedError("Subclasses must implement run()")
@abc.abstractmethod
def _build_system_instruction(self, *args: Any, **kwargs: Any) -> str:
"""Subclasses must implement this to return the system instructions."""
pass

@abc.abstractmethod
def _build_prompt(self, *args: Any, **kwargs: Any) -> str:
"""Subclasses must implement this to return the user prompt."""
pass

def run(self, *args: Any, **kwargs: Any) -> str:
"""Consolidated run method implementing the template pattern.
Constructs instructions and prompt, then invokes model generation.
"""
system_instruction = self._build_system_instruction(*args, **kwargs)
prompt = self._build_prompt(*args, **kwargs)
try:
return str(self.model_client.generate(prompt, system_instruction))
except Exception as e:
raise RuntimeError(f"Agent {self.name} failed during generation: {e}") from e

def _is_path_safe(self, abs_path: str) -> bool:
"""Validates that a resolved path resides inside the repository root."""
try:
common = os.path.commonpath([self.repo_path, abs_path])
return common == self.repo_path
except Exception:
return False

def list_files(self, repo_path: Optional[str] = None) -> List[str]:
target_path = os.path.abspath(repo_path) if repo_path else self.repo_path
if not self._is_path_safe(target_path):
return []

# Helper tools available to the orchestrator/agents
def list_files(self, repo_path):
files_list = []
for root, dirs, files in os.walk(repo_path):
for root, dirs, files in os.walk(target_path):
# Ignore git and hidden directories
dirs[:] = [d for d in dirs if not d.startswith(".") and d != "node_modules"]
for file in files:
if not file.startswith("."):
rel_path = os.path.relpath(os.path.join(root, file), repo_path)
rel_path = os.path.relpath(os.path.join(root, file), self.repo_path)
files_list.append(rel_path)
return files_list

def read_file(self, file_path):
def read_file(self, file_path: str) -> str:
abs_path = os.path.abspath(os.path.join(self.repo_path, file_path))
if not self._is_path_safe(abs_path):
return f"Error: Path {file_path} escapes repository root {self.repo_path}"
try:
with open(file_path, "r", encoding="utf-8") as f:
with open(abs_path, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
return f"Error reading file {file_path}: {e}"

def write_file(self, file_path, content):
def write_file(self, file_path: str, content: str) -> str:
abs_path = os.path.abspath(os.path.join(self.repo_path, file_path))
if not self._is_path_safe(abs_path):
return f"Error: Path {file_path} escapes repository root {self.repo_path}"
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
with open(abs_path, "w", encoding="utf-8") as f:
f.write(content)
return f"Successfully wrote to {file_path}"
except Exception as e:
return f"Error writing to {file_path}: {e}"

def clean_code_content(self, content):
def clean_code_content(self, content: str) -> str:
content = content.strip()
while True:
cleaned = False
Expand All @@ -56,7 +94,7 @@ def clean_code_content(self, content):
break
return content

def parse_files(self, response_text):
def parse_files(self, response_text: str) -> Dict[str, str]:
pattern = r"\[FILE:\s*([^\s\]]+)\]\s*(?:\r?\n)*```\w*\s*\n(.*?)\n```"
matches = re.findall(pattern, response_text, re.DOTALL)

Expand Down
93 changes: 73 additions & 20 deletions src/maca/agents/coder.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
from typing import Any, Dict, List, Optional

from maca.agents.base import BaseAgent


class CoderAgent(BaseAgent):
def __init__(self, name, model_client):
super().__init__(name, model_client)
class BaseCoderAgent(BaseAgent):
"""Abstract base coder agent consolidating common file formatting methods."""

def __init__(self, name: str, model_client: Any, repo_path: str = "."):
super().__init__(name, model_client, repo_path=repo_path)

def _format_history(self, history: Optional[List[str]]) -> str:
if not history:
return ""
return "\n\nPrevious Conversation History:\n" + "\n".join(history)

def _format_files_context(self, repo_files_content: Optional[Dict[str, str]]) -> str:
if not repo_files_content:
return ""
formatted_files = "\n\nExisting File Contents:\n"
for filepath, content in repo_files_content.items():
formatted_files += f"--- FILE: {filepath} ---\n{content}\n\n"
return formatted_files


def run(self, task_description, plan, repo_files_content=None, history=None):
system_instruction = self._build_system_instruction()
prompt = self._build_prompt(task_description, plan, repo_files_content, history)
return self.model_client.generate(prompt, system_instruction)
class SimpleCoderAgent(BaseCoderAgent):
def __init__(self, name: str, model_client: Any, repo_path: str = "."):
super().__init__(name, model_client, repo_path=repo_path)

def _build_system_instruction(self):
def _build_system_instruction(self, *args: Any, **kwargs: Any) -> str:
return (
"You are a Software Coder Agent. Your job is to implement the changes outlined in the plan "
"while strictly adhering to clean code practices.\n\n"
Expand All @@ -32,7 +49,13 @@ def _build_system_instruction(self):
"Make sure to provide the entire, complete contents of the file. Do not use placeholders or ellipsis."
)

def _build_prompt(self, task_description, plan, repo_files_content, history):
def _build_prompt(
self,
task_description: str,
plan: str,
repo_files_content: Optional[Dict[str, str]] = None,
history: Optional[List[str]] = None,
) -> str:
history_context = self._format_history(history)
files_context = self._format_files_context(repo_files_content)

Expand All @@ -43,16 +66,46 @@ def _build_prompt(self, task_description, plan, repo_files_content, history):
"Please implement the changes using clean code principles, and output the files using the requested [FILE: path] format."
)

def _format_history(self, history):
if not history:
return ""
return "\n\nPrevious Conversation History:\n" + "\n".join(history)

def _format_files_context(self, repo_files_content):
if not repo_files_content:
return ""
class SpecCoderAgent(BaseCoderAgent):
def __init__(self, name: str, model_client: Any, repo_path: str = "."):
super().__init__(name, model_client, repo_path=repo_path)

formatted_files = "\n\nExisting File Contents:\n"
for filepath, content in repo_files_content.items():
formatted_files += f"--- FILE: {filepath} ---\n{content}\n\n"
return formatted_files
def _build_system_instruction(self, *args: Any, **kwargs: Any) -> str:
return (
"You are a Software Coder Agent. Your job is to implement the changes outlined in the specification "
"while strictly adhering to clean code practices.\n\n"
"CLEAN CODE GUIDELINES:\n"
"- Modularity: Break code into small, single-purpose functions/classes.\n"
"- Readability: Use clear, descriptive, and consistent variable/function names.\n"
"- Documentation: Include concise docstrings and inline comments explaining complex logic.\n"
"- Type Safety: Use type hints for function parameters and return types where applicable.\n"
"- Error Handling: Handle potential exceptions gracefully (no bare except blocks).\n"
"- Simplicity: Avoid over-engineering, code duplication, or spaghetti code.\n\n"
"CRITICAL: You MUST write the file identifier line in the EXACT format: [FILE: path/to/file.ext]\n"
"Do NOT use markdown headers (like '## FILE: ...' or '# FILE: ...'), bullet points, or bold text. "
"The parser WILL fail if you do not use square brackets [FILE: ...].\n\n"
"Format:\n"
"[FILE: path/to/file.ext]\n"
"```language\n"
"code contents\n"
"```\n\n"
"Make sure to provide the entire, complete contents of the file. Do not use placeholders or ellipsis."
)

def _build_prompt(
self,
task_description: str,
spec: str,
repo_files_content: Optional[Dict[str, str]] = None,
history: Optional[List[str]] = None,
) -> str:
history_context = self._format_history(history)
files_context = self._format_files_context(repo_files_content)

return (
f"User Task: {task_description}{history_context}\n\n"
f"Technical Specification:\n{spec}\n"
f"{files_context}\n"
"Please implement the changes using clean code principles, and output the files using the requested [FILE: path] format."
)
31 changes: 18 additions & 13 deletions src/maca/agents/planner.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
from typing import Any, List, Optional

from maca.agents.base import BaseAgent


class PlannerAgent(BaseAgent):
def __init__(self, model_client):
super().__init__("Planner", model_client)

def run(self, task_description, repo_files=None, history=None):
files_str = "\n".join(repo_files) if repo_files else "Empty repository"

history_str = ""
if history:
history_str = "\n\nPrevious Conversation History:\n" + "\n".join(history)
def __init__(self, model_client: Any, repo_path: str = "."):
super().__init__("Planner", model_client, repo_path=repo_path)

system_instruction = (
def _build_system_instruction(self, *args: Any, **kwargs: Any) -> str:
return (
"You are a technical Planner Agent. Your job is to analyze the user request "
"and create a structured markdown implementation plan. "
"Do NOT write any code implementation or scripts. Only write the steps and "
"identify which files need to be created or modified."
)

prompt = (
def _build_prompt(
self,
task_description: str,
repo_files: Optional[List[str]] = None,
history: Optional[List[str]] = None,
) -> str:
files_str = "\n".join(repo_files) if repo_files else "Empty repository"
history_str = ""
if history:
history_str = "\n\nPrevious Conversation History:\n" + "\n".join(history)

return (
f"User Task: {task_description}{history_str}\n\n"
f"Current Files in Repository:\n{files_str}\n\n"
"Please output a detailed implementation plan in markdown format. "
"Clearly indicate the files to be created or modified using [NEW] and [MODIFY] tags."
)

return self.model_client.generate(prompt, system_instruction)
Loading
Loading