# Agent Orchestration

KOAN provides several agent patterns, from single ReAct agents to multi-agent orchestration.

## ReactAgent

The core agent implements a ReAct (Reason + Act) loop:

```python
from koan.agents import AgentConfig, ReactAgent
from koan.llm.client import LLMClient
from koan.tools import ToolRegistry, tool

@tool
def calculate(expression: str) -> str:
    """Evaluate a math expression."""
    return str(eval(expression))  # simplified example

async with LLMClient() as client:
    registry = ToolRegistry()
    registry.add(calculate)

    agent = ReactAgent(
        "calculator",
        client=client,
        config=AgentConfig(
            provider="openai",
            model="gpt-4o-mini",
            system_prompt="You are a calculator. Use tools to compute answers.",
            max_iterations=5,
        ),
        tools=registry,
    )

    result = await agent.run("What is 17 * 28 + 3?")
    print(result.output)        # "479"
    print(result.iterations)     # Number of think-act-observe cycles
    print(result.tool_calls_made)  # Number of tool calls
```

### AgentConfig Options

| Field | Default | Description |
|-------|---------|-------------|
| `provider` | `"openai"` | LLM provider |
| `model` | `"gpt-4o-mini"` | Model name |
| `system_prompt` | `""` | System prompt |
| `max_iterations` | `10` | Max ReAct loop iterations |
| `temperature` | `None` | Sampling temperature |
| `max_tokens` | `None` | Max output tokens |

## SequentialChain

Pipe output from one agent to the next:

```python
from koan.agents import SequentialChain

chain = SequentialChain([
    researcher_agent,    # Gathers information
    writer_agent,        # Writes a draft
    editor_agent,        # Polishes the text
])

result = await chain.run("Write about quantum computing")
# researcher output -> writer input -> editor input -> final output
```

Each agent receives the previous agent's output as its input. Streaming is supported via `chain.run_stream()`.

## ParallelFanOut

Run multiple agents concurrently and collect results:

```python
from koan.agents import ParallelFanOut

fan_out = ParallelFanOut([
    news_agent,
    weather_agent,
    stock_agent,
])

results = await fan_out.run("What's happening in Tokyo?")
# All three agents run concurrently via asyncio.gather
for r in results:
    print(f"{r.agent_name}: {r.output}")
```

Supports timeout and partial results on failure. Streaming via `fan_out.run_stream()` interleaves events from all agents.

## Orchestrator

Routes input to the best sub-agent using a configurable strategy:

```python
from koan.agents import Orchestrator, RuleRouter

orchestrator = Orchestrator(
    agents={
        "weather": weather_agent,
        "finance": finance_agent,
        "general": general_agent,
    },
    router=RuleRouter(
        {"weather": "weather", "stock": "finance", "price": "finance"},
        default="general",
    ),
)

result = await orchestrator.run("What's the weather in NYC?")
print(result.metadata["routed_to"])  # "weather"
```

### Routing Strategies

**RuleRouter** -- keyword-based routing:

```python
router = RuleRouter(
    {"weather": "weather", "forecast": "weather", "stock": "finance"},
    default="general",
)
```

**LLMRouter** -- LLM-powered classification:

```python
from koan.agents import LLMRouter

router = LLMRouter(
    client=llm_client,
    config=LLMConfig(provider="openai", model="gpt-4o-mini"),
    agent_descriptions={
        "weather": "Handles weather and forecast questions",
        "finance": "Handles stock prices and financial data",
    },
)
```

## Supervisor

Coordinates sub-agents with synthesis -- runs relevant agents and combines their outputs:

```python
from koan.agents import Supervisor

supervisor = Supervisor(
    agents={"researcher": researcher, "analyst": analyst},
    client=llm_client,
    config=AgentConfig(
        provider="openai",
        model="gpt-4o",
        system_prompt="You coordinate research and analysis tasks.",
    ),
)

result = await supervisor.run("Analyze the impact of AI on healthcare")
# Supervisor decides which agents to run and synthesizes results
```

The supervisor uses the LLM to decide which sub-agents to invoke, then synthesizes their outputs into a final response. Streaming is supported via `supervisor.run_stream()`.

## GraphAugmentedAgent

Wraps any agent with graph-informed reasoning -- reads past decisions before running, writes traces after:

```python
from koan.graph import GraphAugmentedAgent, GraphConfig, create_graph_client
from koan.agents import AgentConfig
from koan.llm.client import LLMClient

config = GraphConfig(provider="falkordb", uri="redis://localhost:6379", database="myapp")

async with create_graph_client(config) as graph, LLMClient() as llm:
    agent = GraphAugmentedAgent(
        "billing-agent",
        client=llm,
        config=AgentConfig(
            provider="openai",
            model="gpt-4o-mini",
            system_prompt="You are a billing support agent.",
        ),
        tools=registry,
        graph_client=graph,
        entity_ids=["acct-001"],       # Fetch history for these entities
        decision_type="billing",        # Filter precedents to this type
    )

    result = await agent.run("Check billing for acct-001")
    # Agent saw: precedents, entity history, policies, feedback
    # Agent wrote: new decision trace back to the graph
```

Before each run, the agent:
1. Gathers context from the graph (precedents, entity history, policies, feedback)
2. Augments the system prompt with the context block
3. Gates available tools based on active policies
4. Runs the inner agent (ReactAgent by default)
5. Records the decision trace, links entities, records outcome
6. Evolves policies (promote patterns, retire underperformers)

Both `run()` and `run_stream()` are supported. See the [Context Graphs guide](context-graphs.md) for details on policies, feedback, and the self-organizing lifecycle.

## Streaming Agents

Every agent pattern supports streaming via `run_stream()`:

```python
from koan.agents import StreamingReactAgent, TextDelta, ToolCallStart, ToolCallEnd, AgentComplete

agent = StreamingReactAgent("bot", client=client, config=config, tools=registry)

async for event in agent.run_stream("What is the weather?"):
    if isinstance(event, TextDelta):
        print(event.text, end="", flush=True)
    elif isinstance(event, ToolCallStart):
        print(f"\n[calling {event.tool_name}...]")
    elif isinstance(event, ToolCallEnd):
        print(f"[result: {event.result}]")
    elif isinstance(event, AgentComplete):
        print(f"\nDone in {event.iterations} iterations")
```

See the [Streaming Guide](streaming.md) for details on event types and SSE integration.
