# OTURN

**OTURN** = **Orchestrated Task Unified Reactive Nucleus**.

OTURN is a small async runtime for turn-based agent systems.

## What It Provides

- Reducer-style stream state machine (`agent_stream_state.py`)
- Queue-based multi-subscriber pub/sub (`subscribe(queue)`, `unsubscribe(queue)`)
- Turn runtime with tool execution and approval/choice callbacks
- LiteLLM streaming adapter with normalized semantic events
- Session/history persistence via `Context` + `SessionStore`

## Installation

```bash
cd oturn
uv sync
uv pip install -e .
```

Build:

```bash
uv build
```

## Core Types

- `Oturn`: high-level agent runtime
- `OTurnNucleus`: generic reducer nucleus for non-LLM state machines
- `Transition`: reducer output (`state`, `events`, `terminal`)
- `Context`: conversation history + persistence wrapper
- `BaseTool`: tool protocol/base class

## Oturn Event Stream

Published events include:

- `assistant_delta`
- `assistant_reasoning_delta`
- `assistant_final`
- `tool_call_delta`
- `tool_call`
- `tool_output`
- `tool_exit`
- `approval_request`
- `choice_request`
- `message_sent`
- `run_aborted`
- `error`
- `debug_latency` / `tool_call_debug` (when debug flags are enabled)

## Minimal `Oturn` Example

```python
import asyncio
from pathlib import Path

from oturn import (
    Oturn,
    OturnConfig,
    OturnProviderConfig,
    OturnModelConfig,
    OturnAgentConfig,
    Context,
    WeatherTool,
)


async def main() -> None:
    cfg = OturnConfig(
        model="<provider/model>",
        provider=OturnProviderConfig(api_key="<API_KEY>"),
        model_config=OturnModelConfig(max_output_tokens=4096, temperature=0.7),
        agent_config=OturnAgentConfig(max_steps=50, user_name="User"),
    )

    agent = Oturn(work_dir=Path.cwd(), config=cfg, tools=[WeatherTool])
    ctx = Context(Path.cwd() / ".oturn" / "sessions" / "demo.md", work_dir=Path.cwd())

    q: asyncio.Queue[dict] = asyncio.Queue()
    agent.subscribe(q)

    agent.enqueue_user_input_nowait("What's the weather in Tokyo?")
    task = asyncio.create_task(agent.run_queues(ctx))

    try:
        while True:
            ev = await q.get()
            print(ev)
    finally:
        task.cancel()


asyncio.run(main())
```

## Tool Injection Model

OTURN uses explicit runtime tool injection.
Pass tool classes when constructing `Oturn`:

```python
agent = Oturn(work_dir=work_dir, config=cfg, tools=[WeatherTool])
```

Example tools in `oturn.tools.examples`:

- `WeatherTool` (`get_weather`)
- `HostConfigTool` (`get_host_config`)

## Usage and Tool-Call Delta Accessors

Usage data is available via context accessors.

- `get_last_usage(ctx) -> dict | None`
- `get_last_total_tokens(ctx) -> int | None`
- `get_tool_call_deltas() -> list[tool_call_delta]`
- `get_tool_call_delta(delta_id) -> tool_call_delta | None`

## Session/History

- Session persistence is handled by `Context` + `SessionStore`.
- Records are stored as markdown event/message documents (Chron-based serialization via [`chronml`](https://pypi.org/project/chronml/)).
- Default metadata root name is `.oturn` (`DEFAULT_META_DIRNAME`).
- Session files are created under a date-partitioned global directory:
  - `~/.oturn/global_sessions/YYYYMMDD/<session_id>.md`
- When `work_dir` is available, `SessionStore` also creates a local session link:
  - `<work_dir>/.oturn/sessions/<session_id>.md` -> global session file
- Persistence is incremental:
  - `append_message(...)` appends a single serialized record
  - `append_compact(...)` appends compaction markers the same way
- `Context.restore()` loads existing session markdown into in-memory history.
- Configurable knobs:
  - `meta_dirname`: rename `.oturn` namespace for local/global paths
  - `global_sessions_root`: override `~/.<meta_dirname>/global_sessions`
- You can also import/export session markdown via:
  - `Context.export_md_as_config(...)`
  - `Context.import_md_as_config(...)`
