# MARSYS Design Principles

## DP-001: Pure Agent Logic
**Principle**: Agent `_run()` methods perform only pure model execution and return a `Message`; they must not mutate memory, execute tools, or parse actions.

**Rationale**: Centralizing side effects in coordination keeps agent logic deterministic and makes orchestration, retries, and validation consistent across all agents.

**Applies to**: `src/marsys/agents/agents.py` (BaseAgent/Agent subclasses)

**CORRECT example**:
```python
from marsys.agents.memory import Message
from marsys.agents.agents import Agent

class ResearchAgent(Agent):
    async def _run(self, messages, request_context, run_mode, **kwargs):
        raw = await self.model.arun(messages=messages, **kwargs)
        return Message.from_harmonized_response(raw, name=self.name)
```

**WRONG example**:
```python
import json

class ResearchAgent(Agent):
    async def _run(self, messages, request_context, run_mode, **kwargs):
        raw = await self.model.arun(messages=messages, **kwargs)
        # BAD: Mutates memory and parses actions inside _run
        self.memory.add(role="assistant", content=raw.content)
        return json.loads(raw.content)
```
Explanation: `_run()` must not mutate memory or parse actions; both are handled by `run_step()` and `ValidationProcessor`.

**Violations cause**: Double-counted memory, inconsistent tool ordering, and conflicting routing decisions.

**Exceptions**: None. Tests may stub `_run()` but must still return a `Message` and avoid side effects.

---

## DP-002: Centralized Validation
**Principle**: All response parsing and action validation occur in `ValidationProcessor`; agents and executors must not parse responses into actions.

**Rationale**: A single validation hub enforces topology permissions, consistent error handling, and format pluggability across the system.

**Applies to**: `src/marsys/coordination/validation/response_validator.py`, `src/marsys/coordination/execution/branch_executor.py`

**CORRECT example**:
```python
# BranchExecutor: validate after StepExecutor returns a Message
validation = await self.response_validator.process_response(
    raw_response=result.response,
    agent=agent,
    branch=branch,
    exec_state=exec_state,
)
```

**WRONG example**:
```python
# BAD: Parsing next_action inside agent or StepExecutor
import json

content = json.loads(raw_message.content)
if content["next_action"] == "invoke_agent":
    next_agent = content["action_input"]
```
Explanation: Parsing decisions outside `ValidationProcessor` bypasses topology checks and format-specific parsing rules.

**Violations cause**: Inconsistent routing, invalid transitions, and duplicated parsing logic.

**Exceptions**: Tool call extraction remains in `StepExecutor` (native tool_calls only), not action parsing.

---

## DP-003: Dynamic Branching
**Principle**: Branches are created only at runtime divergence points; parent branches wait for children when convergence is required.

**Rationale**: Dynamic branching prevents unnecessary branch creation and aligns parallelism with actual agent decisions and topology structure.

**Applies to**: `src/marsys/coordination/execution/branch_spawner.py`, `src/marsys/coordination/orchestra.py`

**CORRECT example**:
```python
# Orchestra drives dynamic branching after an agent completes
new_tasks = await self.branch_spawner.handle_agent_completion(
    last_agent,
    parsed_response,
    context,
    result.branch_id,
)
all_tasks.extend(new_tasks)
```

**WRONG example**:
```python
import asyncio
from marsys.coordination.branches.types import ExecutionBranch

# BAD: Pre-creating branches for every edge at startup
for agent in topology_graph.nodes:
    task = asyncio.create_task(
        branch_executor.execute_branch(ExecutionBranch(...), task_prompt, context)
    )
    all_tasks.append(task)
```
Explanation: Branch creation must be triggered by divergence detection, not by static pre-allocation.

**Violations cause**: Unbounded parallelism, incorrect convergence behavior, and wasted resources.

**Exceptions**: None; branching decisions must flow through `DynamicBranchSpawner`.

---

## DP-004: Branch Isolation
**Principle**: Each branch maintains its own memory, trace, metadata, and status; no cross-branch sharing or mutation is allowed.

**Rationale**: Branch isolation ensures parallel execution is deterministic and prevents leakage of context between branches.

**Applies to**: `src/marsys/coordination/execution/branch_executor.py`, `src/marsys/coordination/branches/types.py`

**CORRECT example**:
```python
from marsys.coordination.execution.branch_executor import BranchExecutionContext

exec_context = BranchExecutionContext(
    branch_id=branch.id,
    session_id=context.get("session_id", ""),
    initial_request=initial_request,
)
exec_context.add_memory(agent_name, {"role": "assistant", "content": "..."})
```

**WRONG example**:
```python
# BAD: Shared memory across branches
shared_memory.append({"agent": agent_name, "content": response})
branch.state.memory = shared_memory
```
Explanation: Branches must not share memory or state objects; use branch-local structures.

**Violations cause**: Cross-branch contamination, incorrect convergence aggregation, and nondeterministic results.

**Exceptions**: None. Aggregation should occur only through `DynamicBranchSpawner` at convergence points.

---

## DP-005: Topology-Driven Routing
**Principle**: Routing decisions must consult `TopologyGraph` for all allowed transitions; no hardcoded routes.

**Rationale**: The topology is the single source of truth for valid agent transitions and conversation loops.

**Applies to**: `src/marsys/coordination/routing/router.py`

**CORRECT example**:
```python
def _validate_transition(self, from_agent: str, to_agent: str) -> bool:
    if self.topology_graph.has_edge(from_agent, to_agent):
        return True
    if self.topology_graph.is_in_conversation_loop(from_agent, to_agent):
        return True
    return to_agent in self.topology_graph.adjacency.get(from_agent, [])
```

**WRONG example**:
```python
# BAD: Hardcoded routes
if current_agent == "Planner":
    next_agent = "Researcher"
```
Explanation: Hardcoded routing bypasses topology validation and breaks when the graph changes.

**Violations cause**: Invalid transitions, brittle workflows, and topology inconsistencies.

**Exceptions**: Error recovery may temporarily bypass topology to reach `User` when explicitly allowed in routing logic.

---

## DP-006: Adapter Pattern
**Principle**: All model providers are accessed via adapters with a unified interface; no provider-specific calls in agents.

**Rationale**: The adapter layer normalizes APIs, error handling, and response harmonization across providers.

**Applies to**: `src/marsys/models/models.py`

**CORRECT example**:
```python
# BaseAPIModel wires provider-specific adapters via a factory
from marsys.models.models import BaseAPIModel

model = BaseAPIModel(
    model_name="anthropic/claude-opus-4.6",
    api_key="...",
    base_url="...",
    provider="openrouter",
)
response = model.run(messages=[{"role": "user", "content": "Hi"}])
```

**WRONG example**:
```python
# BAD: Direct provider call inside BaseAPIModel.run()
import requests

requests.post("https://api.openai.com/v1/chat/completions", json=payload)
```
Explanation: Direct API calls bypass adapter harmonization and error handling.

**Violations cause**: Provider-specific behavior leaks into higher layers and breaks portability.

**Exceptions**: None. New providers must be added via `ProviderAdapterFactory` and adapter classes.

---

## DP-007: Format Pluggability
**Principle**: Response formats are extensible; each format provides prompt building and parsing via `BaseResponseFormat` and processors.

**Rationale**: Pluggable formats allow coordinated changes to prompts and parsing without touching core orchestration logic.

**Applies to**: `src/marsys/coordination/formats/`, `src/marsys/coordination/validation/response_validator.py`

**CORRECT example**:
```python
from marsys.coordination.formats import JSONResponseFormat, register_format, SystemPromptBuilder

class StrictJSONFormat(JSONResponseFormat):
    def get_format_name(self) -> str:
        return "strict_json"

register_format("strict_json", StrictJSONFormat)

builder = SystemPromptBuilder(response_format="strict_json")
```

**WRONG example**:
```python
# BAD: Hardcoding a new format inside ValidationProcessor
if response_format == "xml":
    parsed = parse_xml(raw_response)
```
Explanation: Format handling must live in format classes and processors, not in core validators.

**Violations cause**: Tight coupling between validation logic and formats, making new formats risky to add.

**Exceptions**: None. Format changes must be encapsulated in `coordination/formats/`.
