# Streaming

KOAN supports token-by-token streaming at every level: raw LLM calls, tool-calling streams, agent runs, and orchestrators.

## Install

```bash
pip install koan[llm]
```

## Text Streaming

Stream raw text from an LLM:

```python
from koan.llm.client import LLMClient
from koan.llm.config import LLMConfig
from koan.streaming import stream_text
from koan.types import Message

async with LLMClient() as client:
    config = LLMConfig(provider="openai", model="gpt-4o-mini")
    messages = [Message(role="user", content="Tell me a story")]

    async for chunk in stream_text(client, config, messages):
        print(chunk, end="", flush=True)
```

## Tool Call Streaming

Stream with tool call accumulation -- tool call deltas are collected and yielded as complete tool calls:

```python
from koan.streaming import stream_with_tools

async for event in stream_with_tools(client, config, messages):
    if event.has_tool_calls:
        for tc in event.tool_calls:
            print(f"Tool: {tc.name}({tc.arguments})")
    elif event.text:
        print(event.text, end="")
```

## Structured Streaming

Stream with final Pydantic validation:

```python
from pydantic import BaseModel
from koan.streaming import stream_structured

class Summary(BaseModel):
    title: str
    points: list[str]

async for partial in stream_structured(client, config, messages, Summary):
    print(partial)  # Partial JSON chunks
# Final chunk is validated against the Pydantic model
```

## Agent Streaming

### StreamingReactAgent

The streaming agent yields typed `AgentEvent` objects during the ReAct loop:

```python
from koan.agents import (
    StreamingReactAgent,
    AgentStart,
    IterationStart,
    TextDelta,
    ToolCallStart,
    ToolCallEnd,
    AgentComplete,
    AgentError,
)

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

async for event in agent.run_stream("What is 17 + 28?"):
    match event:
        case IterationStart(iteration=n):
            print(f"\n--- Iteration {n} ---")
        case TextDelta(text=t):
            print(t, end="", flush=True)
        case ToolCallStart(tool_name=name):
            print(f"\n[calling {name}...]")
        case ToolCallEnd(tool_name=name, result=r):
            print(f"[{name} returned: {r}]")
        case AgentComplete(output=out, tool_calls_made=tc):
            print(f"\nDone ({tc} tool calls)")
        case AgentError(error=err):
            print(f"\nError: {err}")
```

### Event Types

| Event | Fields | When |
|-------|--------|------|
| `AgentStart` | `agent_name`, `input_text` | Agent begins processing |
| `IterationStart` | `iteration` | Each ReAct loop cycle |
| `TextDelta` | `text` | Token-by-token LLM output |
| `ToolCallStart` | `tool_name`, `tool_call_id`, `arguments` | Tool call begins |
| `ToolCallEnd` | `tool_name`, `tool_call_id`, `result`, `error` | Tool call completes |
| `AgentComplete` | `output`, `iterations`, `tool_calls_made`, `metadata` | Agent finished successfully |
| `AgentError` | `error`, `output`, `iterations`, `tool_calls_made` | Agent encountered an error |

### Non-Streaming Fallback

`StreamingReactAgent` also supports `run()` which collects all events and returns an `AgentResult`:

```python
result = await agent.run("What is 17 + 28?")
# Equivalent to consuming all events from run_stream()
```

## Orchestrator Streaming

All orchestration patterns support `run_stream()`:

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

orchestrator = Orchestrator(
    agents={"weather": weather_agent, "math": math_agent},
    router=RuleRouter({"weather": "weather", "add": "math"}),
)

async for event in orchestrator.run_stream("What's the weather?"):
    if isinstance(event, AgentStart):
        print(f"Routed to: {event.agent_name}")
    elif isinstance(event, TextDelta):
        print(event.text, end="")
```

`SequentialChain.run_stream()` and `ParallelFanOut.run_stream()` also yield `AgentEvent` objects.

## SSE (Server-Sent Events)

Wrap any async event stream as an SSE response for FastAPI/Starlette:

```python
from koan.streaming import sse_response

# In a FastAPI endpoint:
@app.get("/chat/stream")
async def chat_stream(query: str):
    events = agent.run_stream(query)
    return sse_response(events)
```

The `sse_response()` helper formats events as `text/event-stream` with proper `data:` framing.
