# Multi-Agent Meeting Framework - Design Spec

## Overview

A lightweight, Unix-style multi-agent collaboration system in pure Python. Two CLI tools: `agent_builder.py` generates Agent configs from natural language; `run_meeting.py` orchestrates meetings where a PM Agent routes discussion between participant Agents.

## Design Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| LLM SDK | anthropic + openai (Protocol abstraction) | User wants both; thin Protocol layer keeps it simple |
| Agent config format | JSON + YAML (auto-detect by extension) | User wants both; trivial to implement with pathlib |
| LLM call mode | Synchronous | Simpler implementation, sufficient for meeting use case |
| Output | Terminal (colored) + Markdown file in `reports/` | User preference |
| Architecture | Minimal Protocol Layer (Option A) | Unix philosophy, lowest complexity, easy to extend |

## Project Structure

```
multi-agents-meeting/
├── src/
│   ├── __init__.py
│   ├── models.py          # Pydantic: AgentConfig, MeetingState, PMDecision
│   ├── llm_client.py      # Protocol + AnthropicClient + OpenAIClient
│   ├── agent.py            # Stateless Agent executor
│   ├── pm.py               # PM Agent routing logic
│   ├── meeting.py          # Meeting event loop
│   └── config_loader.py    # Load JSON/YAML Agent configs
├── agents/                 # Agent config files
│   └── example_architect.json
├── reports/                # Meeting report output
├── agent_builder.py        # CLI 1: Generate Agent config
├── run_meeting.py          # CLI 2: Run meeting
├── tests/
│   ├── test_models.py
│   ├── test_llm_client.py
│   ├── test_agent.py
│   ├── test_pm.py
│   ├── test_meeting.py
│   └── test_config_loader.py
├── requirements.txt
└── README.md
```

## Data Models (Pydantic)

### AgentConfig

Represents a single Agent's identity and behavior.

```python
class AgentConfig(BaseModel):
    name: str                  # unique identifier, e.g. "steve_jobs_product_reviewer"
    role: str                  # human-readable role, e.g. "Chief Product Officer"
    system_prompt: str         # the Agent's system prompt
    input_schema: str = ""     # optional description of expected input
    output_schema: str = ""    # optional description of expected output
```

Stored as `.json` or `.yaml` files in `agents/` directory.

### ScratchpadEntry

A single entry on the shared whiteboard.

```python
class ScratchpadEntry(BaseModel):
    agent_name: str            # who wrote this entry
    content: str               # the entry content
    timestamp: datetime        # when it was written
```

### MeetingState

The shared whiteboard that maintains all meeting context.

```python
class MeetingState(BaseModel):
    topic: str                         # the discussion topic
    scratchpad: list[ScratchpadEntry] = []  # ordered list of entries
    current_round: int = 0             # current loop iteration
    max_rounds: int = 5                # hard limit to prevent infinite loops
```

### PMDecision

The structured output that the PM Agent must return each round.

```python
class PMDecision(BaseModel):
    analysis: str                              # brief analysis of current whiteboard
    next_action: Literal["CALL_AGENT", "FINISH"]  # what to do next
    target_agent: str | None = None            # who to call (if CALL_AGENT)
    prompt_for_agent: str | None = None        # specific question for target (if CALL_AGENT)
    final_report: str | None = None            # Markdown report (if FINISH)
```

Validation rule: if `next_action == "CALL_AGENT"`, then `target_agent` and `prompt_for_agent` must be non-None. If `next_action == "FINISH"`, then `final_report` must be non-None.

## LLM Abstraction Layer

### Protocol

```python
class LLMClient(Protocol):
    def chat(self, system: str, messages: list[dict[str, str]]) -> str:
        """Send a chat request. Returns the assistant's text response."""
        ...
```

### Implementations

- `AnthropicClient`: wraps `anthropic.Anthropic().messages.create()`
- `OpenAIClient`: wraps `openai.OpenAI().chat.completions.create()`

### Factory

```python
def create_client() -> LLMClient:
    """Create LLM client based on environment variables.

    Env vars:
        LLM_PROVIDER: "anthropic" (default) or "openai"
        LLM_MODEL: model name (optional, has sensible defaults)
        ANTHROPIC_API_KEY / OPENAI_API_KEY: API credentials
    """
```

Default models: `claude-sonnet-4-20250514` for Anthropic, `gpt-4o` for OpenAI.

## Core Modules

### agent.py - Stateless Agent Executor

```python
def run_agent(config: AgentConfig, prompt: str, scratchpad_summary: str, client: LLMClient) -> str:
    """Execute a single Agent turn.

    The Agent sees:
    1. Its own system_prompt (from config)
    2. A summary of the current whiteboard
    3. The specific prompt from PM

    Returns the Agent's text response.
    """
```

Key constraint: Agents are stateless. They receive only the PM's question and a whiteboard summary, never raw chat history from other Agents.

### pm.py - PM Agent Router

```python
PM_SYSTEM_PROMPT = """You are a meeting facilitator (PM). Your job is to:
1. Analyze the current whiteboard state
2. Decide which Agent should speak next, or if the meeting should end
3. Output ONLY valid JSON matching the PMDecision schema
...(includes available agent names and their roles)..."""

def run_pm(state: MeetingState, available_agents: dict[str, AgentConfig], client: LLMClient) -> PMDecision:
    """Run PM Agent to get next routing decision.

    Builds a message with current whiteboard state, sends to LLM,
    parses response as PMDecision via Pydantic.
    Retries up to 2 times on JSON parse failure.
    """
```

### meeting.py - Event Loop

```python
def run_meeting(topic: str, agent_configs: dict[str, AgentConfig], client: LLMClient, max_rounds: int = 5) -> str:
    """Execute the full meeting loop.

    1. Initialize MeetingState with topic
    2. Loop:
       a. Send whiteboard to PM
       b. PM returns PMDecision
       c. If CALL_AGENT: run target Agent, append result to whiteboard
       d. If FINISH: return final_report
       e. If current_round >= max_rounds: force PM to FINISH
    3. Return Markdown report
    """
```

### config_loader.py - Agent Config Loader

```python
def load_agent_config(path: Path) -> AgentConfig:
    """Load a single Agent config from JSON or YAML file.
    Auto-detects format by file extension (.json / .yaml / .yml).
    """

def load_agents(names: list[str], agents_dir: Path = Path("agents")) -> dict[str, AgentConfig]:
    """Load multiple Agent configs by name.
    Searches agents_dir for matching files (tries .json, .yaml, .yml extensions).
    """
```

## CLI Interfaces

### agent_builder.py

```
Usage: python agent_builder.py --description "..." --name NAME [--output-dir agents/] [--format json|yaml]

Behavior:
1. Takes a natural language description
2. Calls LLM to generate a structured system prompt
3. Saves AgentConfig as JSON/YAML in output directory
```

The LLM prompt for agent generation instructs it to produce:
- A clear, focused `system_prompt` based on the description
- A sensible `role` title
- The `name` from CLI argument

### run_meeting.py

```
Usage: python run_meeting.py --topic "..." --agents name1,name2,name3 [--max-rounds 5] [--agents-dir agents/] [--output-dir reports/]

Behavior:
1. Load specified Agent configs from agents directory
2. Initialize meeting with topic
3. Run event loop (PM routes between Agents)
4. Print colored output to terminal during meeting
5. Save final Markdown report to reports/ directory
```

## Error Handling

| Error | Handling |
|-------|----------|
| LLM returns invalid JSON | Pydantic `ValidationError` caught; retry up to 2 times with error feedback to LLM |
| Agent config not found | `FileNotFoundError` with friendly message listing available agents |
| API call failure | Catch SDK exceptions, print error, exit with code 1 |
| PM calls non-existent agent | Validation against available agents list; retry PM with correction |
| Max rounds reached | Force meeting end; PM gets one final call with instruction to produce FINISH |

## Testing Strategy

All tests use mocked LLM clients (no real API calls).

- **test_models.py**: Pydantic validation rules (PMDecision conditional fields, serialization/deserialization)
- **test_llm_client.py**: Factory function, environment variable handling, Protocol conformance
- **test_agent.py**: Agent executor with mock client, prompt construction
- **test_pm.py**: PM decision parsing, retry logic on invalid JSON, available-agents injection
- **test_meeting.py**: Full loop with mock: normal flow, max-rounds forced finish, error recovery
- **test_config_loader.py**: JSON loading, YAML loading, missing file handling, extension auto-detection

## Dependencies

```
pydantic>=2.0
anthropic>=0.40.0
openai>=1.0
pyyaml>=6.0
```

No other external dependencies. Standard library only for everything else.

## Demo Scenario

```bash
# 1. Create agents
python agent_builder.py --description "资深软件架构师，关注系统可扩展性和技术债务" --name architect
python agent_builder.py --description "商业分析师，关注 ROI 和市场可行性" --name business_analyst
python agent_builder.py --description "DevOps 工程师，关注部署复杂度和运维成本" --name devops

# 2. Run meeting
python run_meeting.py --topic "评估从 PostgreSQL 迁移到 MongoDB 的方案" --agents architect,business_analyst,devops
```

Expected output: Terminal shows real-time meeting progress with colored agent names. Final Markdown report saved to `reports/`.
