# Skill: Add a Single Agent to an Existing Pipeline

Use when you need to insert one new agent into an already-working pipeline — not build
a pipeline from scratch. Read the existing `agents.py` and `pipeline.py` first.

## When to Add an Agent vs Build a New Pipeline

**Add an agent** when:
- The new agent fits naturally into the existing data flow (reads `context`, writes to `result`)
- The pipeline already has the right state shape (PipelineState or your custom subclass)
- You are inserting a step before, after, or in parallel with an existing agent

**Build a new pipeline** (see `skills/build-agents/SKILL.md`) when:
- The new task has fundamentally different inputs, state, or output shape
- The new flow requires a different set of agents running in a different order
- You are adding a second independent workflow that runs separately

## Step 1 — Read the Existing Code

Before writing a single line, read:

1. `pipeline.py` — understand what nodes exist and how they connect
2. `agents/` — understand what state fields each agent reads and writes
3. `prompts/` — understand the existing prompt style
4. `state.py` (if it exists) — understand what custom state fields are available

Pay attention to:
- What does the preceding agent write into `context`? Your new agent will likely read it.
- What does the following agent expect in `context` or `result`? Your new agent must provide it.
- What is the `goto` destination in the preceding agent's `Command`? You will change it.

## Step 2 — Create the Prompt File

Create `prompts/{new_agent}.md`. Follow the existing prompt style exactly.

```markdown
# New Agent — Role Description

One sentence: what this agent does and why it exists in this pipeline.

## Instructions

1. Read the context provided (describe what the context contains)
2. Use `tool_name` to do X (list every tool this agent has access to)
3. Use `tool_name` to do Y
4. Return a JSON object when complete

## Output Format

{
  "field_one": "...",
  "field_two": "...",
  "summary": "one sentence summary"
}
Output ONLY the JSON. No prose before or after.
```

Rules:
- DO mention every tool by name — the agent needs to know what it has
- DO include the exact JSON schema it must output
- DO NOT add `{task}` placeholder anywhere — task is passed at runtime in `run_react(task=...)`
- DO NOT mention the `respond()` tool — it is optional and confusing; omit it
- DO NOT repeat rules from `arccrew/prompts/base.md` (thinking process, error handling, hard limits)

## Step 3 — Write the Agent Class

**Convention: one agent per file inside `agents/`.**

```
agents/
├── researcher.py    ← ResearcherAgent
├── writer.py        ← WriterAgent
└── new_agent.py     ← your new agent goes here
pipeline.py
prompts/
```

### Pattern A: ReAct Agent with Tools (most common)

Use this when the agent needs to call tools (web search, file ops, shell).

```python
from pathlib import Path
from langgraph.types import Command
from arccrew.agents.base import BaseAgent, track_timing
from arccrew.tools.agent_tools import get_research_tools
from arccrew.tools.workspace import create_workspace_tools

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

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

    @property
    def system_prompt(self) -> str:
        return self.get_prompt_manager().assemble_prompt("new_agent")

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

        result = await self.run_react(
            task=(
                f"Task: {task.get('description', '')}\n\n"
                f"Context from previous step:\n{context}"
            ),
            tools=get_research_tools(),
        )
        data = self.extract_json(result) or {"raw": result}
        return Command(goto="next_node_name", update={"context": data})
```

### Pattern B: Single LLM Call, No Tools

Use this when the agent only needs to reason over existing state — no external lookups.
Typical uses: reviewer, summarizer, classifier, formatter.

```python
class ReviewerAgent(BaseAgent):
    def __init__(self):
        super().__init__(name="reviewer", prompts_dir=PROMPTS_DIR)

    @property
    def system_prompt(self) -> str:
        return self.get_prompt_manager().assemble_prompt("reviewer")

    @track_timing
    async def execute(self, state: dict) -> Command:
        result = state.get("result", {})

        response = await self.generate(
            f"Review this output and determine if it meets quality standards:\n\n{result}"
        )
        data = self.extract_json(response) or {"approved": True, "feedback": response}
        return Command(goto="next_node_name", update={"review_result": data})
```

### Pattern C: Hybrid — Conditional Tool Use

Use this when the agent decides at runtime whether tools are needed.

```python
class AnalystAgent(BaseAgent):
    def __init__(self):
        super().__init__(name="analyst", prompts_dir=PROMPTS_DIR)

    @property
    def system_prompt(self) -> str:
        return self.get_prompt_manager().assemble_prompt("analyst")

    @track_timing
    async def execute(self, state: dict) -> Command:
        context = state.get("context", {})
        needs_research = not context.get("sources")  # decide based on state

        if needs_research:
            result = await self.run_react(
                task=f"Analyze and research: {context}",
                tools=get_research_tools(),
            )
        else:
            result = await self.generate(f"Analyze this data: {context}")

        data = self.extract_json(result) or {"raw": result}
        return Command(goto="writer", update={"context": data})
```

## Step 4 — Wire into the Existing Graph

Open `pipeline.py` and make the minimum changes to insert the new agent.

### Inserting Between Two Existing Nodes

Before (researcher → writer):
```python
graph.add_edge("researcher", "writer")
```

After (researcher → new_agent → writer):
```python
new_agent_instance = NewAgent()
async def new_agent_node(state): return await new_agent_instance.execute(state)

graph.add_node("new_agent", new_agent_node)
graph.add_edge("researcher", "new_agent")   # researcher now goes to new_agent
graph.add_edge("new_agent", "writer")       # new_agent continues to writer
# Remove or replace the old: graph.add_edge("researcher", "writer")
```

Also update the `ResearcherAgent.execute()` return to `Command(goto="new_agent", ...)` if
it hardcodes the destination.

### Inserting After the Last Agent (Before Increment/END)

Before:
```python
graph.add_edge("writer", "increment")
```

After:
```python
graph.add_node("new_agent", new_agent_node)
graph.add_edge("writer", "new_agent")
graph.add_edge("new_agent", "increment")
```

### Inserting at the Start (Before the First Agent)

Before:
```python
graph.add_edge(START, "researcher")
```

After:
```python
graph.add_node("new_agent", new_agent_node)
graph.add_edge(START, "new_agent")
graph.add_edge("new_agent", "researcher")
```

### Updating a Conditional Edge

If the node before yours uses `add_conditional_edges`, update the routing dict:

```python
# Before: should_continue returns "researcher" or "__end__"
graph.add_conditional_edges(
    "increment",
    should_continue,
    {"researcher": "researcher", "__end__": END}
)

# After: researcher is still the loop-back target — no change needed here
# But if you want the new agent to be the loop entry point instead:
graph.add_conditional_edges(
    "increment",
    should_continue,
    {"new_agent": "new_agent", "__end__": END}
)
```

## Step 5 — Verify the State Contract

After wiring, mentally trace the data flow:

1. What does node N-1 put in state? (look at its `Command(update={...})`)
2. What does your new agent read from state? (look at `state.get(...)` calls)
3. What does your new agent write to state? (look at its `Command(update={...})`)
4. Does node N+1 still get what it expects from state? (nothing broke downstream)

If the new agent needs state fields that do not exist in `PipelineState`, see
`skills/add-state-field/SKILL.md`.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| `goto="wrong_name"` typo | Node names in `Command(goto=...)` must exactly match `graph.add_node("name", ...)` |
| Agent reads `state["context"]` but predecessor writes `state["result"]` | Align the key names between agents |
| Old edge still exists alongside the new one | Remove `graph.add_edge("old_src", "old_dst")` when you insert a node between them |
| `@track_timing` missing | Always decorate `execute()` — it populates `agent_timings` in state |
| Forgetting `assemble_prompt` returns base.md prepended | Don't duplicate base.md rules in agent.md |

## Per-Agent Model and Round Overrides

After adding the agent, you can tune it without touching code:

```bash
# .env
NEW_AGENT_MODEL=anthropic/claude-sonnet-4-20250514   # heavier model for this agent
NEW_AGENT_MAX_ROUNDS=8                                 # fewer rounds allowed
```

The pattern `{AGENT_NAME_UPPERCASE}_MODEL` matches `BaseAgent(name="new_agent")`.
`run_react()` resolves `NEW_AGENT_MAX_ROUNDS` automatically.
