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

`use Raxol.AgentClientProtocol.Client` to implement the **client** side of
ACP: the editor/host process that answers permission asks, filesystem, and
terminal requests, and receives `session/update` streaming notifications.

This is a **thin ergonomic layer** over `Raxol.AgentClientProtocol.Connection`
-- see `Raxol.AgentClientProtocol.Agent`'s moduledoc for the shared rationale
(identical wiring, mirrored side). Nothing here re-implements protocol
correlation, cancellation, or the turn/permission state machine; those
live in `Connection` and (agent-side) `Session`.

## Callback surface

One `@callback` per `Raxol.AgentClientProtocol.MethodTable.rows_for_side(:client)`
row that carries an app-level callback -- `session/request_permission`,
`session/update`, `fs/write_text_file`, `fs/read_text_file`,
`terminal/create`, `terminal/output`, `terminal/release`,
`terminal/wait_for_exit`, `terminal/kill`. Naming and arity are generated
straight from the table, not hand-maintained; every current client row
has `params != nil`, so every generated callback is arity 2:
`callback(params, ctx)` (request or notification alike).

`ctx` is `Raxol.AgentClientProtocol.Connection.Ctx.t()`: read-only
per dispatch, carries `conn` (the `Connection` pid, for
`Connection.request/async_request/notify/reply/delegate_reply`),
`handler_state` (produced once by `c:init/1`), `reply_ref`, `rx_seq`,
`session_sup`, `task_sup` (client-role connections still get these two --
they are per-`Connection` infrastructure, not agent-specific).

A request callback (`request_permission`, `write_text_file`,
`read_text_file`, the four `terminal/*` methods) returns `{:ok,
result_struct}`, `{:error, %Error{}}`, or `:deferred` (legal only after
`Connection.delegate_reply/3`). `session_update/2`'s return value
is always ignored (`Router.dispatch/4`) -- it is the streaming sink for
`session/update` notifications during a prompt turn.

Call `callbacks/0` to introspect the generated `{callback, arity, wire}`
list.

## Defaults

Every generated callback (and `c:handle_ext_request/3`,
`c:handle_ext_notification/3`) has an overridable default: requests
answer `{:error, Error.method_not_found()}`, `session_update/2` is a
silent `:ok`. Override only what your client supports -- an unimplemented,
capability-gated method never needs an explicit override (the client
simply never advertises the capability at `initialize`, so a well-behaved
agent never sends it; the `-32601` default is the honest fallback if one
does anyway). `c:init/1` defaults to `{:ok, handler_arg}`.

## Extension methods

`"_"`-prefixed wire methods with no `MethodTable` row are routed by
`Connection` directly to `c:handle_ext_request/3` /
`c:handle_ext_notification/3` (never through the table-driven callbacks
above -- see `Router`'s moduledoc). Both default the same way as any other
unsupported method.

## Example

    defmodule MyClient do
      use Raxol.AgentClientProtocol.Client
      alias Raxol.AgentClientProtocol.Client.FsSandbox

      # `handler_arg:` is the project root the agent may read under.
      @impl true
      def init(root), do: {:ok, %{root: root}}

      @impl true
      def session_update(notification, _ctx) do
        IO.inspect(notification.update, label: "session/update")
      end

      @impl true
      def read_text_file(req, ctx), do: FsSandbox.read(ctx.handler_state.root, req)

      # ... only what you support.
    end

    {:ok, sup} =
      Raxol.AgentClientProtocol.Client.start_link(MyClient,
        transport: {mod, handle},
        handler_arg: "/path/to/project"
      )

## Session-consumer ergonomics: subscribe / prompt / prompt_stream

Overriding `c:session_update/2` yourself works fine, but most clients
just want "give me the updates for session X" without hand-rolling a
mailbox protocol. The generated default `c:session_update/2` (unless you
override it) forwards every decoded update to whichever processes called
`subscribe/3` for that `{conn, session_id}` pair, as
`{:acp_session_update, session_id, update}` -- `update` is always an
already-decoded `Raxol.AgentClientProtocol.Schema.SessionUpdate.t()` (see
`decode_update/1`'s doc for why an undecodable variant never reaches
this path at all). `subscribe/3` remains useful for a custom
`session_update/2` override that wants a broadcast fan-out, but it is
NOT how `prompt/3`/`prompt_stream/4` get their updates (see below).

    {:ok, {updates, response}} =
      Client.prompt(client_conn, PromptRequest.new(session_id, prompt_blocks))

    {:ok, response} =
      Client.prompt_stream(client_conn, request, fn update ->
        IO.inspect(update, label: "update")
      end)

`prompt/3` and `prompt_stream/4` drive the request through
`Connection.async_request/6` and then consume the **direct turn-delivery
channel** (`Raxol.AgentClientProtocol.Delivery`'s moduledoc):
`Connection` opens a turn at submit time and, for every `session/update`
belonging to that turn, sends the calling process
`{:acp_turn_update, tag, ordinal, notification}` directly, itself --
the same single-sender operation it uses for the terminal
`{:acp_result, tag, outcome}`. Because one process sends every update,
the turn-end companion, and the result, BEAM's pairwise FIFO guarantees
they arrive in wire order with NO reorder buffer, NO settle timer, and a
synchronous, deterministic failure (`{:error, {:delivery_gap, _}}`) if
the turn-end count and the delivered-update count ever disagree. This
is why these two functions do not consume `subscribe/3`: that path runs a
separate handler task per notification while the Connection sends the
terminal result itself -- two different senders, no cross-sender FIFO
guarantee.

## `fs_sandbox`: a working filesystem client with zero callbacks

`use Raxol.AgentClientProtocol.Client, fs_sandbox: "/path/to/root"` auto-
implements `c:read_text_file/2` and `c:write_text_file/2` (still
`defoverridable`) confined to that directory: every request path is
joined under the root, `..`-normalized, and checked against the root
using BOTH the lexical result and the fully symlink-resolved real path,
so a `../` escape or a symlink planted inside the sandbox that points
outside it are both rejected with `-32602 invalid params`. The file is
then opened and the open handle checked to be the regular file that was
resolved, and a read is capped at 16 MiB (`fs_max_read_bytes:` sets
another cap). `Raxol.AgentClientProtocol.Client.FsSandbox` states what
this does and does not guarantee against a peer that changes the
sandbox tree while requests run.
`terminal/*` handlers are deliberately NOT defaulted by any option here --
they stay `method_not_found` unless you implement them yourself.

## Library-mode wiring

`start_link/2` and `child_spec/1` build exactly this subtree
(identical shape to the agent side, `role: :client`):

    ConnectionSupervisor          Supervisor, :one_for_all,
    │                             auto_shutdown: :any_significant
    ├── Task.Supervisor           restart: :permanent
    ├── DynamicSupervisor         restart: :permanent  (Session.Supervisor; :temporary children)
    └── Connection                restart: :temporary, significant: true

See `Raxol.AgentClientProtocol.Agent`'s moduledoc for why `parent_sup` is
resolved via `self()` inside the nested supervisor's `init/1` rather than
passed in from outside. A client connection gets its own
`Session.Supervisor`/`SessionRegistry` slot too -- unused, since the client
side hosts no sessions, and kept only because the subtree shape is uniform
across roles (one less special case in `Connection`).

# `ctx`

```elixir
@type ctx() :: Raxol.AgentClientProtocol.Connection.Ctx.t()
```

Per-dispatch context. See `Raxol.AgentClientProtocol.Connection.Ctx`.

# `update`

```elixir
@type update() :: Raxol.AgentClientProtocol.Schema.SessionUpdate.t()
```

A decoded session/update payload (always successfully decoded -- see `decode_update/1`).

# `create_terminal`

```elixir
@callback create_terminal(
  Raxol.AgentClientProtocol.Schema.ClientTypes.CreateTerminalRequest.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.CreateTerminalResponse.t()}
  | {:error, Raxol.AgentClientProtocol.Error.t()}
  | :deferred
```

# `handle_ext_notification`

```elixir
@callback handle_ext_notification(wire :: String.t(), params :: map(), ctx()) :: term()
```

Handle an inbound `"_"`-prefixed extension notification. Return value is
ignored. Default: `:ok`.

# `handle_ext_request`

```elixir
@callback handle_ext_request(wire :: String.t(), params :: map(), ctx()) ::
  {:ok, map()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
```

Handle an inbound `"_"`-prefixed extension request with no `MethodTable`
row. Return shape matches the table-driven request callbacks. Default:
`{:error, Error.method_not_found()}`.

# `init`

```elixir
@callback init(handler_arg :: term()) :: {:ok, handler_state :: term()} | {:stop, term()}
```

Produce this handler's per-connection state from `handler_arg` (the value
passed as `handler_arg:` to `start_link/2` / `child_spec/1`). Called once,
at `Connection` boot; the result is threaded read-only through every
dispatch as `ctx.handler_state`. Default: `{:ok, handler_arg}`.

# `kill_terminal`

```elixir
@callback kill_terminal(
  Raxol.AgentClientProtocol.Schema.ClientTypes.KillTerminalRequest.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.KillTerminalResponse.t()}
  | {:error, Raxol.AgentClientProtocol.Error.t()}
  | :deferred
```

# `raxol_session_record`

```elixir
@callback raxol_session_record(
  Raxol.AgentClientProtocol.Ext.Schema.SessionRecordNotification.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) :: term()
```

# `read_text_file`

```elixir
@callback read_text_file(
  Raxol.AgentClientProtocol.Schema.ClientTypes.ReadTextFileRequest.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.ReadTextFileResponse.t()}
  | {:error, Raxol.AgentClientProtocol.Error.t()}
  | :deferred
```

# `release_terminal`

```elixir
@callback release_terminal(
  Raxol.AgentClientProtocol.Schema.ClientTypes.ReleaseTerminalRequest.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) ::
  {:ok,
   Raxol.AgentClientProtocol.Schema.ClientTypes.ReleaseTerminalResponse.t()}
  | {:error, Raxol.AgentClientProtocol.Error.t()}
  | :deferred
```

# `request_permission`

```elixir
@callback request_permission(
  Raxol.AgentClientProtocol.Schema.ClientTypes.RequestPermissionRequest.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) ::
  {:ok,
   Raxol.AgentClientProtocol.Schema.ClientTypes.RequestPermissionResponse.t()}
  | {:error, Raxol.AgentClientProtocol.Error.t()}
  | :deferred
```

# `session_update`

```elixir
@callback session_update(
  Raxol.AgentClientProtocol.Schema.LifecycleExtras.SessionNotification.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) :: term()
```

# `terminal_output`

```elixir
@callback terminal_output(
  Raxol.AgentClientProtocol.Schema.ClientTypes.TerminalOutputRequest.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.TerminalOutputResponse.t()}
  | {:error, Raxol.AgentClientProtocol.Error.t()}
  | :deferred
```

# `wait_for_terminal_exit`

```elixir
@callback wait_for_terminal_exit(
  Raxol.AgentClientProtocol.Schema.ClientTypes.WaitForTerminalExitRequest.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) ::
  {:ok,
   Raxol.AgentClientProtocol.Schema.ClientTypes.WaitForTerminalExitResponse.t()}
  | {:error, Raxol.AgentClientProtocol.Error.t()}
  | :deferred
```

# `write_text_file`

```elixir
@callback write_text_file(
  Raxol.AgentClientProtocol.Schema.ClientTypes.WriteTextFileRequest.t(),
  Raxol.AgentClientProtocol.Connection.Ctx.t()
) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.WriteTextFileResponse.t()}
  | {:error, Raxol.AgentClientProtocol.Error.t()}
  | :deferred
```

# `callbacks`

```elixir
@spec callbacks() :: [{atom(), arity(), String.t()}]
```

Introspect the generated callback surface: `{callback_atom, arity, wire_method}`, in table order.

# `child_spec`

```elixir
@spec child_spec(keyword()) :: Supervisor.child_spec()
```

Library-mode child spec: embed one ACP client connection subtree into the
caller's own supervision tree, e.g.

    children = [
      {Raxol.AgentClientProtocol.Client, handler: MyClient, transport: {Transport.Stdio, handle}}
    ]

Required: `:handler` (a module using this behaviour), `:transport`
(a `{module, handle}` pair per `Raxol.AgentClientProtocol.Transport`).
Optional: `:handler_arg` (passed to `c:init/1`, default `nil`), `:name`
(registers the `ConnectionSupervisor`), `:id` (child id in the parent's
supervisor, defaults to `Raxol.AgentClientProtocol.Client` -- override when
embedding more than one client connection in the same tree).

# `connection`

```elixir
@spec connection(pid()) :: {:ok, pid()} | {:error, :not_found}
```

Resolve the `Connection` pid out of a started connection subtree. `sup`
must be the **`ConnectionSupervisor`'s own pid** -- this is the ONLY
supported way to reach the pid `Connection.request/4`, `async_request/6`,
`notify/3`, and this module's own `subscribe/3`/`prompt/3`/`prompt_stream/4`
need, since `start_link/2`/`child_spec/1` deliberately return/register the
`ConnectionSupervisor`, not the `Connection` child itself (the supervisor is
the stable handle across the child's `:temporary` restart policy).

Getting `sup`:

  * `start_link/2` returns it directly -- pass that return value straight in.
  * Any starter that hands you back the STARTED CHILD's pid also works
    unchanged -- `start_supervised!/1` (ExUnit) and
    `DynamicSupervisor.start_child/2` both do this for a `child_spec/1`
    entry.
  * A plain `Supervisor.start_link(children, ...)` where a `child_spec/1`
    entry is one of several `children` does NOT hand you the
    `ConnectionSupervisor` pid directly -- that call returns YOUR OWN
    supervisor's pid, one level further out. Resolve the
    `ConnectionSupervisor` child from
    `Supervisor.which_children(your_sup)` yourself first, then pass THAT
    pid here.

Returns `{:error, :not_found}` if `sup` has no live `Connection` child
(already torn down, or `sup` isn't a `ConnectionSupervisor` at all --
e.g. you passed your own outer supervisor's pid from the last bullet
above by mistake).

# `decode_update`

```elixir
@spec decode_update(map()) ::
  {:ok, Raxol.AgentClientProtocol.Schema.SessionUpdate.t()} | {:raw, map()}
```

Tolerant `session/update` payload decode: wraps
`Raxol.AgentClientProtocol.Schema.SessionUpdate.from_json/1`. On success,
`{:ok, SessionUpdate.t()}`; on failure (an unrecognized `sessionUpdate`
discriminator, or a variant payload that fails its own required-field
decode), logs a warning and returns `{:raw, map}` instead of propagating
the error. Never raises. Every variant the pinned v1.19.0 oracle defines
is ported, `usage_update` included (`Schema.UsageUpdate`), so an
unrecognized discriminator means a wire value from outside that schema --
see `SessionUpdate`'s moduledoc for the variant list.

**Not on the live wire-dispatch path.** `Connection`/`Router` decode
`session/update` centrally, via `SessionNotification.from_json/1`
(`method_table.ex`'s `session/update` row), and DROP the whole
notification + telemetry (`[:raxol, :acp, :unknown_notification]`)
*before a handler ever runs* when that decode fails -- `c:session_update/2`,
and therefore `subscribe/3`'s broadcast, is only ever invoked with an
already-successfully-decoded payload. This
function exists for callers that decode a raw `"update"` wire map
themselves (manual/offline processing, replay tooling, tests) and want
best-effort passthrough instead of Connection's silent drop. The live
pipeline stays strict; tolerating undecodable variants there would require
a change to `SessionNotification`/`Router` decode.

# `prompt`

```elixir
@spec prompt(pid(), struct(), pos_integer()) ::
  {:ok, {[update()], struct() | {:ext, map()}}} | {:error, term()}
```

Send `session/prompt` and collect every `session/update` delivered for
the turn until the response resolves. Returns
`{:ok, {updates, response}}` -- `updates` is the list of decoded
`SessionUpdate.t()` payloads observed during the turn, oldest first;
`response` is the decoded result struct (or `{:ext, map()}` for a
non-table result marker) -- or `{:error, term()}` on timeout/transport
failure/decode failure/delivery gap, same vocabulary as
`Connection.async_request/6`'s `outcome` plus
`{:error, {:delivery_gap, %{delivered:, expected:}}}` when the
Connection's own turn-end count disagrees with what this process
actually received (a definite loss signal, not a heuristic).

No settle pass, no timer: the direct channel makes updates, the turn-end
count, and the result all arrive from the Connection itself, so mailbox
order already equals wire order.

# `prompt_stream`

```elixir
@spec prompt_stream(pid(), struct(), (update() -&gt; any()), pos_integer()) ::
  {:ok, struct() | {:ext, map()}} | {:error, term()}
```

Streaming-callback variant of `prompt/3`: `on_update.(update)` is invoked
synchronously, in this process, for each `session/update` as it is
received -- no accumulation (O(1) memory: this loop keeps no update
list at all). Returns `{:ok, response}` / `{:error, term()}` for the
terminal outcome, same vocabulary as `prompt/3` (including the
`{:error, {:delivery_gap, _}}` fail-the-turn case).

# `start_link`

```elixir
@spec start_link(module(), keyword()) :: Supervisor.on_start()
```

Standalone start: `Client.start_link(MyClient, transport: {mod, handle})`.

Returns the `ConnectionSupervisor`'s `Supervisor.on_start()` result --
link to (or monitor) the returned pid to observe the whole subtree's
lifecycle (see `Agent.start_link/2`'s doc for the crash-vs-restart
rationale, identical here).

Options: same as `child_spec/1` minus `:handler` (positional) and `:id`.

# `subscribe`

```elixir
@spec subscribe(pid(), String.t(), pid()) :: :ok
```

Subscribe `subscriber` (default `self()`) to `session_id`'s
`session/update` deliveries on `conn`. The generated default
`c:session_update/2` (unless overridden) broadcasts
`{:acp_session_update, session_id, update}` to every subscriber
registered for `{conn, session_id}` -- `update` is always an
already-decoded `Raxol.AgentClientProtocol.Schema.SessionUpdate.t()`
(see `decode_update/1`'s doc for why an undecodable variant never
reaches this path). NOT idempotent: subscribing the same pid twice for
the same key registers it twice in the underlying `:bag` -- each
registration receives its own copy of every broadcast, so the pid gets
the update twice. Subscribe once per `{conn, session_id, pid}`, or
dedupe on receipt if you can't guarantee that.

This broadcast path still fans out through a per-notification dispatch
task (one task per `session/update`), so it has no cross-notification
ordering guarantee on its own -- it is NOT what `prompt/3`/
`prompt_stream/4` use (see the moduledoc's "Session-consumer ergonomics"
section); it remains here for custom `session_update/2` overrides that
want a simple fan-out primitive.

# `unsubscribe`

```elixir
@spec unsubscribe(pid(), String.t(), pid()) :: :ok
```

Remove a subscription registered via `subscribe/3`. An unknown/already-removed entry is a no-op.

---

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