> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unpod.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Sessions

> Manage multiple concurrent conversations, persist state across process restarts, and use non-DM agent brains.

## When do you need sessions?

Sessions add a lifecycle and persistence layer on top of **any `Agent`-protocol
brain** - the default `PlaybookAgent` and the legacy `DialogMachine` alike. A
bare agent holds its conversation state in memory for the lifetime of the
instance. This works perfectly for:

* Voice calls (one machine per call, ephemeral)
* CLI testing (single conversation, short-lived)
* Simple single-user demos

You need `SessionWorker` when:

* **Multi-user:** multiple concurrent conversations hit the same process
* **Multi-worker:** requests are load-balanced across FastAPI workers or pods
* **Long-lived chat:** conversations span hours or days and must survive restarts

## The Agent Protocol

`SessionWorker` works with any brain that implements this Protocol:

```python theme={null}
class Agent(Protocol):
    async def turn(text: str, *, stream: bool = False) -> TurnResult | AsyncIterator[StreamChunk]
    def assist(text: str) -> None
    @property
    def chat_ctx(self) -> ChatContext
    def load_chat_ctx(ctx: ChatContext) -> None
```

The Playbook engine's `PlaybookAgent`, the legacy `DialogMachine`, `LLMAgent`,
and `LangChainAgent` all implement this.

## SessionWorker

The `agent_factory` returns a fresh agent per session. Point `DialogMachine` at
a playbook and you get the default engine; the factory is identical whatever the
artifact:

```python theme={null}
from superdialog import DialogMachine, SessionWorker, InMemorySessionStore

worker = SessionWorker(
    agent_factory=lambda: DialogMachine("kyc.yaml", llm="openai/gpt-4.1-mini"),
    store=InMemorySessionStore(),
    max_sessions=1000,               # optional cap
)
```

Use it with an async context manager:

```python theme={null}
async with worker.acquire("user-42") as h:
    result = await h.turn("Hello")
    h.assist("The customer is a VIP - be especially warm.")
    print(result.text)
```

What `acquire` does:

1. Loads or creates the session for `session_id`
2. Acquires a per-session lock (serialises concurrent requests for the same id)
3. Yields a `SessionHandle`
4. On exit: persists state to the store and releases the lock

Requests for **different** `session_id`s run fully in parallel.

## FastAPI multi-user example

```python theme={null}
from contextlib import asynccontextmanager
from fastapi import FastAPI
from superdialog import DialogMachine, SessionWorker, InMemorySessionStore

worker: SessionWorker

@asynccontextmanager
async def lifespan(app: FastAPI):
    global worker
    worker = SessionWorker(
        agent_factory=lambda: DialogMachine("kyc.yaml", llm="openai/gpt-4.1-mini"),
        store=InMemorySessionStore(),
    )
    yield

app = FastAPI(lifespan=lifespan)

@app.post("/turn")
async def turn(payload: dict):
    async with worker.acquire(payload["session_id"]) as h:
        result = await h.turn(payload["text"])
    return {"reply": result.text}
```

## Session stores

| Store                  | Status  | Use case                          |
| ---------------------- | ------- | --------------------------------- |
| `InMemorySessionStore` | ✅ v0.2  | Development, single-process       |
| `NullSessionStore`     | ✅ v0.2  | Voice (ephemeral, no persistence) |
| `RedisSessionStore`    | 🔜 v0.3 | Multi-process, distributed        |
| `FileSessionStore`     | 🔜 v0.3 | Lightweight persistence           |
| `SQLiteSessionStore`   | 🔜 v0.3 | Local single-server               |

Switch stores by changing one line - the rest of the code stays the same:

```python theme={null}
# Development
store = InMemorySessionStore()

# Production (v0.3)
# store = RedisSessionStore(url="redis://localhost:6379")
```

## Lock backends

| Backend              | Status  | Use case                    |
| -------------------- | ------- | --------------------------- |
| `AsyncioLockBackend` | ✅ v0.2  | Single-process (default)    |
| `RedisLockBackend`   | 🔜 v0.3 | Multi-process / distributed |

## Other agent brains

When you want sessions and persistence but no checkpoint or flow opinion at all -
a raw chat brain:

```python theme={null}
from superdialog import LLMAgent, SessionWorker, InMemorySessionStore

# Raw chat brain - no flow, no slots
worker = SessionWorker(
    agent_factory=lambda: LLMAgent(
        llm="openai/gpt-5.1",
        system_prompt="You are a helpful customer support assistant.",
    ),
    store=InMemorySessionStore(),
)
```

Or with LangChain (requires `pip install superdialog[langchain]`):

```python theme={null}
from superdialog import LangChainAgent, SessionWorker, InMemorySessionStore

worker = SessionWorker(
    agent_factory=lambda: LangChainAgent(runnable=my_langchain_chain),
    store=InMemorySessionStore(),
)
```

## Conversation state

Internally, each session stores a `ChatContext` - LiveKit-aligned message
history:

```python theme={null}
@dataclass
class ChatMessage:
    role: Literal["system", "user", "assistant", "tool"]
    content: str

@dataclass
class ChatContext:
    items: list[ChatMessage]
```

Beyond the transcript, each engine carries its own runtime state:

* **Playbook engine** - the source of truth is the **event-sourced log**
  (`agent.event_log`); `ConversationState.fold(log, playbook)` derives the
  checkpoint, slots, and outcome. Persist `event_log.to_jsonl()` and restore via
  `load_event_log` for full fidelity.
* **Legacy DialogMachine** - `FlowState` (current node, slot values). Present
  only when the brain is a `DialogMachine`; `None` otherwise.

<Warning>
  `SessionWorker`'s built-in `SessionRecord` persists `chat_ctx` / `flow_state`
  only. For durable or multi-worker resume on the Playbook engine, persist
  `agent.event_log.to_jsonl()` yourself and restore with `load_event_log` -
  otherwise playbook state fidelity (provisional vs confirmed slots, tool results)
  is lost.
</Warning>

## Voice (ephemeral) pattern

For voice calls where each call is a fresh conversation and no persistence is needed:

```python theme={null}
worker = SessionWorker(
    agent_factory=lambda: DialogMachine("kyc.yaml", llm="anthropic/claude-haiku-4-5"),
    store=NullSessionStore(),   # writes are dropped; reads always return empty
)
```

`NullSessionStore` keeps the single-call lifecycle of a bare agent while still giving you the multiplexing and locking of `SessionWorker`.
