# Session

> Stateful multi-turn conversations with an auto-managed conversation ID and client-side transcript replay.

Source: https://maincode.com/docs/python-agent-sdk-session
Section: Agent SDK · Matilda documentation

---

A `Session` wraps an `Agent` with auto-managed `conversation_id` and accumulates turn results. Agent runs send `persist: false`, so the server does not retain turn content — instead, the session accumulates each turn's exchange **client-side** and replays the transcript with every subsequent turn, giving the model full conversation context.

## `create_session(agent, **options)`

```python title="session.py"
import asyncio

from matilda_agent_sdk import create_session


async def main():
    session = create_session(
        {"name": "tutor", "instructions": "You are a patient programming tutor. Explain concepts simply."}
    )

    print("Conversation ID:", session.conversation_id)

    # Turn 1
    r1 = await session.run("What is a closure in Python?")
    print("Turn 1:", r1.final_output[:100], "...")

    # Turn 2 — the session replays the accumulated transcript, so the model
    # sees the previous exchange
    r2 = await session.run("Can you show me a simple example?")
    print("Turn 2:", r2.final_output[:100], "...")

    # The session accumulates all turn results
    print("Total turns:", len(session.turns))
    print("Last turn stream ID:", session.last_turn.stream_id if session.last_turn else None)


asyncio.run(main())
```

## `Session` class

### `session.run(prompt, **options)`

Runs a turn and accumulates the result in `session.turns`. Options are the `run()` options (minus `conversation_id`); per-turn options merge with session defaults. Returns `AgentRunResult`.

### `session.stream(prompt, **options)`

Streams a turn, yielding `AgentEvent` as they arrive. The result is accumulated in `session.turns` when the stream completes.

```python
async for event in session.stream("Explain decorators in one paragraph."):
    if event.type == "response.output_text.delta":
        print(event.delta, end="", flush=True)

print("\n[Turns accumulated]:", len(session.turns))
print("[Final output cached]:", (session.last_turn.final_output[:60] + "...") if session.last_turn else "")
```

### `session.conversation_id`

The auto-generated (or provided) conversation ID. Reused across all turns.

### `session.turns`

A `list[AgentRunResult]` — one per completed turn.

### `session.last_turn`

The most recent `AgentRunResult`, or `None` if no turns have run.

## Session options

| Field | Type | Description |
| - | - | - |
| `runner` | `Runner \| None` | Custom runner instance. Defaults to the shared default runner. |
| `client` | `MatildaClient \| None` | Explicit client (wrapped in a fresh Runner). Ignored when runner is set. |
| `conversation_id` | `str \| None` | Explicit conversation ID. Auto-generated when omitted. |
| `purpose` | `AgentPurpose \| None` | Fallback purpose when agent is a dict. |
| `response_mode` | `str \| None` | Session-level response mode override. |
| `metadata` | `dict \| None` | Session-level metadata merged into every turn. Defaults to {}. |
| `tool_handlers` | `ToolHandlers \| None` | Session-level tool handlers. |
| `max_tool_roundtrips` | `int` | Session-level roundtrip cap. Defaults to 25. |

## Metadata passthrough

Metadata can be set at multiple levels: `Agent` construction, `Session` construction, or per-call. Per-call metadata merges with (and overrides) session defaults.

```python title="metadata.py"
from matilda_agent_sdk import create_session


def tutor_instructions(ctx):
    env = ctx["metadata"].get("env", "unknown")
    user = ctx["metadata"].get("user", "anonymous")
    return f"Environment: {env}. User: {user}."


async def main():
    session = create_session(
        {"name": "helper", "instructions": tutor_instructions},
        metadata={"env": "staging", "user": "demo-user"},
    )

    # Session-level metadata is used by default
    await session.run("Who am I?")

    # Per-call metadata overrides session defaults
    await session.run("Who am I now?", metadata={"user": "admin"})
    # → env=staging (from session), user=admin (overridden per-call)
```

## Custom runner

A `Session` can use a custom `Runner` for dependency injection in tests or isolated configuration:

```python title="custom_runner.py"
import asyncio

from matilda_agent_sdk import Runner, Session


async def main():
    my_runner = Runner()
    session = Session(
        {"name": "custom-runner-agent", "instructions": "Be brief."},
        runner=my_runner,
    )

    result = await session.run("What is 2 + 2?")


asyncio.run(main())
```
