# Skill: Build Agents with Multi-Agent Starter

Use when the user wants to create an agent, pipeline, tool, prompt, or API endpoint
with this template. Read this fully before writing any code.

## Core Rule

`arccrew/` is the framework library — do not modify it directly (it ships as a pip package).
All domain code goes in a new folder at the root: `my_project/agents/`, `my_project/pipeline.py`, etc.

## File Roles (strict)

| Path                          | What goes here                                                                                                                                  |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `arccrew/tools/`            | Library `@tool` functions — `web_search`, `create_workspace_tools`                                                                              |
| `my_project/tools/`           | Your custom `@tool` decorated functions                                                                                                         |
| `arccrew/utils/helpers.py`  | Plain Python helpers (no `@tool`, no LLM). Has: `truncate_text`, `extract_json_safe`, `utcnow_iso`, `slugify`, `chunk_list`, `safe_get`        |
| `arccrew/models/base.py`    | Pydantic models. Base: `AgentResult`, `TaskSummary`, `ResultStatus`                                                                             |
| `arccrew/prompts/base.md`   | Universal agent rules only — NO tool names, NO domain logic                                                                                     |
| `my_project/prompts/`         | Agent-specific prompts: role, tools available, output schema                                                                                    |
| `arccrew/api/app.py`        | Only change: add `pipeline_registry.register(...)` in `lifespan`                                                                                |

## Prompt Architecture (3 layers, non-overlapping)

```
arccrew/prompts/base.md        → universal rules (thinking, errors, limits)
my_project/prompts/agent.md      → role + tools + output schema for THIS agent
assemble_prompt(variables={...}) → dynamic values injected at runtime
```

**base.md rule:** never mention specific tool names. Tools vary per agent.
**agent.md rule:** do mention tools — the agent needs to know what it has.
**Variables:** use `{city}`, `{date}` etc. in `.md` files, pass via `variables={}`.

## Workload estimation (do this before writing code)

Every tool call costs 2 rounds in the ReAct loop (1 to call, 1 to read the result).
Estimate rounds per agent before assigning tools:

```
N items × M tool calls per item × 2 = estimated rounds needed
```

| Estimated rounds | Action |
|------------------|--------|
| ≤ 10 | Safe with default max_rounds (15) |
| 11–20 | Set `AGENTNAME_MAX_ROUNDS=25` in .env |
| > 20 | Split into two agents, or reduce tool calls per item |

Example: copywriter processing 7 posts with `validate_post` per post = 7 × 1 × 2 = 14 rounds.
That's close to the 15 default — set `COPYWRITER_MAX_ROUNDS=20` or reduce to ≤5 posts per call.

## Building a Pipeline — Step by Step

### 1. Prompts (`my_project/prompts/agent_name.md`)

Prompts are the most common source of failures. Follow these rules exactly:

**Rule 1 — JSON output is non-negotiable**

Every prompt must close with the exact output schema followed by:
```
Output ONLY the JSON object. No explanation, no markdown, no text before or after.
```
"Return JSON when done" is NOT enough — the LLM will add prose. Be literal.

**Rule 2 — Every tool must be explicitly referenced**

If a tool is in the agent's tool list, the prompt MUST name it and say exactly when to call it.
Tools not mentioned in the prompt will be ignored by the LLM.

```markdown
# Agent Name — Role

What this agent does in one sentence.

## Instructions

1. Call `web_search` with the query "{topic} trends 2025" to gather data
2. Call `validate_post_length` for EVERY post before adding it to your output
3. If `validate_post_length` returns `valid: false`, shorten the post and validate again

## Round budget

You have {N} posts to process. Make your tool calls efficiently — one `validate_post_length`
call per post is enough.

## Output Format

Return a JSON object with this exact structure:
{"posts": [{"platform": "linkedin", "copy": "...", "hashtags": [...], "valid": true}]}
Output ONLY the JSON object. No explanation, no markdown, no text before or after.
```

Do NOT add `{task}` placeholder. Do NOT mention `respond()` tool.

### 2. Agent classes (`my_project/agents/`)

**Convention: one agent per file.** Imports are relative to the project root (where `arccrew serve` is run).

```
my_project/
├── agents/
│   └── my_agent.py
├── pipeline.py
└── prompts/
    └── my_agent.md
```

```python
# agents/my_agent.py
from pathlib import Path
from langgraph.types import Command
from arccrew import BaseAgent, track_timing, get_research_tools

PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts"

class MyAgent(BaseAgent):
    def __init__(self):
        super().__init__(name="my_agent", prompts_dir=PROMPTS_DIR)

    @property
    def system_prompt(self) -> str:
        return self.get_prompt_manager().assemble_prompt(
            "my_agent",
            variables={"workspace_path": "/tmp/workspace"}
        )

    @track_timing
    async def execute(self, state: dict) -> Command:
        tasks = state.get("tasks", [])
        idx = state.get("current_task_index", 0)
        task = tasks[idx] if idx < len(tasks) else {}
        description = task.get("description", "")

        result = await self.run_react(
            task=description,
            tools=get_research_tools(),   # or create_workspace_tools(path)
            # max_rounds omitted — resolved from DEFAULT_MAX_ROUNDS in .env (default: 15)
            # Override per-agent via {AGENT_NAME}_MAX_ROUNDS in .env
        )
        data = self.extract_json(result) or {"raw": result}
        # Intermediate agent: pass data to next agent via "context" (always available in PipelineState)
        return Command(goto="next_agent", update={"context": data})
        # Final agent: also append to "results" so the API response is populated
        # return Command(goto="__end__", update={"result": data, "results": [{"task_index": idx, **data}]})
```

**Critical rules for state updates:**
- `"context"` — use this to pass data between agents (it's in `PipelineState`)
- `"results"` — the **last agent** in the flow MUST append here: `"results": [{"task_index": idx, **data}]`. This is what `POST /api/run` returns. If empty, the response will have `results: []`.
- Custom fields (`crypto_raw_data`, etc.) — only valid if you define a custom state class extending `PipelineState`. Never use a field that isn't in the state class.

Tool options:

- Research only: `get_research_tools()` → `[web_search]`
- Files + shell: `create_workspace_tools(path)` → `[read_file, write_file, list_directory, run_shell]`
- Both: `get_research_tools() + create_workspace_tools(path)`
- MCP server: `await get_mcp_tools({"server": {...}})` → tools from any MCP-compatible server

**Using MCP tools** (requires `pip install "arccrew[mcp]"`):

```python
import os
from arccrew.tools import get_research_tools, get_mcp_tools

GITHUB_MCP = {
    "transport": "sse",
    "url": "https://api.githubcopilot.com/mcp/",
    "headers": {"Authorization": f"Bearer {os.getenv('GITHUB_TOKEN')}"},
}

# In execute():
tools = get_research_tools() + await get_mcp_tools({"github": GITHUB_MCP})
result = await self.run_react(task=task, tools=tools)
```

See `skills/configure-mcp/SKILL.md` for full transport options and multi-server setup.

### 3. Pipeline (`pipeline.py`)

```python
from arccrew import create_pipeline, PipelineState
from agents.agent_a import AgentA
from agents.agent_b import AgentB

def create_my_pipeline():
    agent_a = AgentA()
    agent_b = AgentB()

    async def agent_a_node(state): return await agent_a.execute(state)
    async def agent_b_node(state): return await agent_b.execute(state)

    return create_pipeline(
        state_class=PipelineState,
        nodes={
            "agent_a": agent_a_node,
            "agent_b": agent_b_node,
        },
        flow=["agent_a", "agent_b"],
    )
```

For custom state fields beyond what `PipelineState` provides, create `state.py` at the project root:

```python
from arccrew.orchestrator.state import PipelineState, _take_last_dict
from typing import Annotated
class MyState(PipelineState):
    strategy: Annotated[dict, _take_last_dict]
```

### 4. Register with API and MCP (`pipeline.py`)

At the bottom of `pipeline.py`, register for both REST API and MCP — the server auto-loads this file:

```python
from arccrew.api.deps import pipeline_registry
from arccrew.mcp_server import register_pipeline

pipeline_registry.register("my_pipeline", create_my_pipeline)
register_pipeline("my_pipeline", create_my_pipeline)
```

Do NOT modify `arccrew/api/app.py` or `arccrew/mcp_server.py`. Registration goes in the user's `pipeline.py` only.

## Common Patterns

**Retry loop** (worker → verifier → retry on failure):

```python
from arccrew.orchestrator.graph import create_pipeline
graph = create_pipeline(
    state_class=MyState,
    nodes={"worker": worker_node, "verifier": verifier_node, "reviewer": reviewer_node},
    flow=["worker", "verifier", "reviewer"],
    retry_from="worker", verify_node="verifier", review_node="reviewer", max_retries=3,
)
```

**Single LLM call** (no tools, for reviewer/summarizer):

```python
response = await self.generate(f"Review this: {state['result']}")
data = self.extract_json(response)
```

**Per-agent models** (`.env`):

```
AGENT_MODEL=anthropic/claude-haiku-4-5-20251001
MY_AGENT_MODEL=anthropic/claude-sonnet-4-20250514
```

Pattern: `{AGENT_NAME_UPPERCASE}_MODEL` matches `BaseAgent(name="my_agent")`.

**Per-agent ReAct loop limits** (`.env`):

```
DEFAULT_MAX_ROUNDS=15
RESEARCHER_MAX_ROUNDS=10
WRITER_MAX_ROUNDS=5
```

Same pattern: `{AGENT_NAME_UPPERCASE}_MAX_ROUNDS`. `run_react()` resolves this automatically — no need to pass `max_rounds` in code unless you need a one-off override.

**Use AgentResult** for structured output:

```python
from arccrew.models.base import AgentResult, ResultStatus
result = AgentResult(status=ResultStatus.SUCCESS, confidence=0.9, summary="Done")
return Command(goto="next", update=result.to_state_update("context"))
```

## Run it

```bash
# API server (auto-loads pipeline.py and registers your pipelines)
arccrew serve
arccrew serve --reload   # dev mode with hot reload

# Call the pipeline
curl -X POST http://localhost:8000/api/runs \
  -H "Content-Type: application/json" \
  -d '{"pipeline": "my_pipeline", "input": "your task description here"}'

# MCP (Claude Desktop and other MCP clients)
arccrew serve-mcp

# Tests
pytest                             # all tests
pytest tests/test_base_agent.py -v
pytest tests/test_tools.py -v
```

## What NOT to do

- Do NOT modify `arccrew/` (it is a library package — update via pip)
- Do NOT put `{task}` in prompt files
- Do NOT mention tool names in `arccrew/prompts/base.md`
- Do NOT put business logic in `arccrew/api/routes/`
- Do NOT hardcode API keys — use `get_settings()`
- Do NOT add `@tool` to helpers — helpers go in `arccrew/utils/` (library) or `my_project/utils/` (yours)
