# `Pixir.Conversation`
[🔗](https://github.com/Ranvier-Technologies/pixir/blob/main/lib/pixir/conversation.ex#L1)

The UI-agnostic multi-turn **driver** (ADR 0008): the conversational loop over
`Session`/`Turn`/`Events`, with no presentation. Every front-end — the terminal CLI,
a future HTTP/WebSocket tier, an editor extension, or an embedding Elixir app — drives
Pixir through this module.

It is a **stateless functional module**, not a process: the `Session` GenServer already
owns turn state, History, `seq`, and interrupt (ADR 0001). Per-client state (a socket,
a pending permission reply) belongs to the transport tier, never here.

## Shape

    {:ok, sid} = Conversation.start(workspace: ".")      # new Session
    {:ok, sid} = Conversation.start(id: sid)             # resume / reattach
    {:ok, ref} = Conversation.send(sid, "do the thing")  # run one Turn (non-blocking)

**Observation is the `Events` bus** (ADR 0004), full stop — this module invents no new
streaming abstraction. An out-of-process UI subscribes via `Pixir.Events.subscribe/1`
and forwards each `{:pixir_event, event}` over its transport as JSON. For *in-process*
callers (tests, an optional terminal presenter) `await/2` consumes the bus until the
Turn reaches a terminal status, with an optional `on_event` callback.

**Permissions stay injectable** (ADR 0006): pass an `:asker` function through `send/3`;
this module implements no prompting. Async, remote permission decisions are a
transport-tier concern (an asker that blocks the Turn while round-tripping over a
socket).

# `session_id`

```elixir
@type session_id() :: String.t()
```

# `await`

```elixir
@spec await(
  session_id(),
  keyword()
) :: :done | :error | :interrupted | :timeout
```

Block until the current Turn reaches a terminal status, consuming bus events. The
caller must already be subscribed (`Pixir.Events.subscribe/1`) — typically call
`subscribe`, then `send`, then `await`.

Returns `:done | :error | :interrupted | :timeout`. `interrupted` is terminal (ADR
0008 — the Renderer's old loop hung on it). After a terminal event, `await/2` gives
the Session process a short grace period to clear its active Turn, so callers can
safely send the next prompt without seeing a transient `:busy`.

Options: `:on_event` (a 1-arity callback invoked per event, for in-process rendering),
`:idle_timeout` (ms, default 120_000), and `:cleanup_timeout` (ms, default 1_000).

# `history`

```elixir
@spec history(session_id()) :: {:ok, Pixir.Log.history()} | {:error, map()}
```

The Session's History, folded from the Log (pass-through to `Session`).

# `interrupt`

```elixir
@spec interrupt(session_id()) :: :ok | {:error, :no_turn}
```

Interrupt the running Turn, if any (pass-through to `Session`).

# `send`

```elixir
@spec send(session_id(), String.t(), keyword()) ::
  {:ok, reference()} | {:error, :busy}
```

Run one Turn for `prompt` in the Session. Non-blocking — returns `{:ok, ref}` once the
Turn task is started (`{:error, :busy}` if a Turn is already running). Observe progress
via the bus, or block with `await/2`.

Options are passed to `Turn.run/3`: `:permission_mode` (default `:auto`), `:asker`
(default deny), `:provider`, `:provider_opts`, `:dry_run`, `:max_iterations`.

# `start`

```elixir
@spec start(keyword()) :: {:ok, session_id()} | {:error, map()}
```

Start a conversation, returning its Session id.

  * no `:id` — mint a **new** Session.
  * `:id` — **resume** a persisted Session (or idempotently reattach if it is already
    running). A missing Log is a structured `:not_found` error; a corrupt Log surfaces
    as a structured error rather than crashing the caller.

Options: `:id`, `:workspace` (default cwd), `:role` (default `:build`),
`:permission_mode` (default `:auto`) and `:write_policy` (default nil) — recorded as
the new root Session's durable permission posture — plus
`:force_release_writer_lease?` and `:force_release_reason`.

# `subscribe`

```elixir
@spec subscribe(session_id()) :: :ok | {:error, map()}
```

Subscribe the calling process to the Session's event bus (pass-through).

# `unsubscribe`

```elixir
@spec unsubscribe(session_id()) :: :ok
```

Symmetric teardown for subscribe/1; effect-only per the rule-5 exception.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
