# `Raxol.AgentClientProtocol.Session.SteerAdapter`
[🔗](https://github.com/DROOdotFOO/raxol-agent/blob/raxol_agent_client_protocol-v0.1.0/packages/raxol_agent_client_protocol/lib/raxol/agent_client_protocol/session/steer_adapter.ex#L1)

The Session's steer seam — the one injection point through
which a live turn's compare-and-swap steer decision is driven.

`Raxol.AgentClientProtocol.Session` is the single-writer turn owner Steer's
contract demands (its `turn` field is `:idle | {:prompting, Turn} |
{:cancelling, Turn}`, mutated only from its own mailbox). A steer decision is a
read-modify-write over per-turn CAS state, and the concurrent-steer guarantee
holds ONLY if that read-modify-write is serialized by exactly one owner
process. This behaviour lets the Session BE that owner — it holds the opaque
steer state and calls these callbacks from inside its own `handle_call`, so the
serialization is satisfied by construction — WITHOUT the ACP package owning
the CAS decision itself. A host injects an adapter that makes that decision;
this package ships only the seam and a default that refuses.

## The three callbacks and how the Session drives them

The Session holds one OPAQUE `steer_state` term (the adapter's — this package
never inspects it) and threads it through:

  * `turn_started(steer_state, turn_token)` — a prompt turn began; `turn_token`
    is the Session's own per-session turn ordinal (`turn_seq`), the same value
    the durable `turn_started` journal record and the client's turn events
    carry. The adapter binds it as the running turn's CAS token so a
    subsequent steer's `expected_turn_id` can match it. Returns the next
    steer_state.
  * `turn_ended(steer_state)` — the turn drained (any outcome). The adapter
    clears the CAS token so a steer arriving in the idle window between turns
    rejects `:no_live_turn` rather than landing on a dead turn. Returns the
    next steer_state. The DEDUP index (session-lifetime window)
    is NOT cleared here — only the turn liveness.
  * `resolve(steer_state, request)` — the CAS decision itself, run from the
    Session's mailbox. `request` is a plain map `%{text, expected_turn_id,
    client_msg_id}` (see `t:request/0`). Returns `{result, next_steer_state}`;
    `result` is the honest outcome vocabulary (`t:result/0`) the Session
    returns synchronously on the wire and — on `{:ok, {:accepted, _}}` only —
    the trigger to forward the steered text to the running turn and append one
    durable steer record. Rejections change nothing and journal nothing; they
    are still the wire response.

## Default: refuse

`Raxol.AgentClientProtocol.Session.SteerAdapter.Unsupported` is the default. It
holds no state, no-ops both turn hooks, and answers every `resolve/2` with
`{:error, :no_steer_channel}` — so a base Session with no injected adapter
never mutates steer state (the turn hooks are identity) and honestly reports
that live steering is not wired. Live steering works only once a real adapter
is injected.

# `accepted_ref`

```elixir
@type accepted_ref() :: %{
  turn_id: term(),
  offset: non_neg_integer(),
  client_msg_id: term() | nil
}
```

A reference to an accepted (or de-duplicated) steer.

# `request`

```elixir
@type request() :: %{
  text: String.t() | nil,
  expected_turn_id: term(),
  client_msg_id: term() | nil
}
```

The normalized steer request the Session hands to `resolve/2`. Mirrors the
`_raxol/session.steer` wire payload:

  * `:text` — the steering text (forwarded to the turn on accept).
  * `:expected_turn_id` — the turn ordinal the client believes is running
    (the CAS target).
  * `:client_msg_id` — the client-supplied idempotency key (may be `nil`).

# `result`

```elixir
@type result() ::
  {:ok, {:accepted, accepted_ref()}}
  | {:ok, {:duplicate, accepted_ref()}}
  | {:error, {:stale_turn, term(), term()}}
  | {:error, :no_live_turn}
  | {:error, :client_msg_id_reuse}
  | {:error, :no_steer_channel}
```

The honest CAS outcome vocabulary — the same the Surface banner renders and
`Raxol.AgentClientProtocol.Ext.Schema.SteerResponse` encodes on the wire.

# `resolve`

```elixir
@callback resolve(steer_state :: term(), request()) :: {result(), term()}
```

Resolve one steer request against the live turn's CAS state (single-writer).

# `turn_ended`

```elixir
@callback turn_ended(steer_state :: term()) :: term()
```

Clear the CAS token when the turn drained; preserve the dedup index.

# `turn_started`

```elixir
@callback turn_started(steer_state :: term(), turn_token :: term()) :: term()
```

Bind the CAS token for a turn that just began (its per-session ordinal).

---

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