Raxol.AgentClientProtocol.Router (Raxol Agent Client Protocol v0.1.0)

Copy Markdown View Source

Table-driven JSON-RPC method routing for ACP: decode/4 classifies and type-decodes an inbound wire method + params into a tagged tuple, dispatch/4 calls the matching handler callback, and result_marker/1 tells the sending side how to decode a pending outbound request's response.

Every function clause here is generated at compile time from Raxol.AgentClientProtocol.MethodTable.rows/0 by the internal Router.Codegen module (not part of the public docs; @moduledoc false) -- editing the table is the only way to change routing behavior; there is no hand-maintained parallel list of method strings anywhere in this module.

What this module does NOT do (by design)

  • No capability gating. Connection consults MethodTable's capability field and must reject a gated-and-unnegotiated method with -32601 BEFORE ever calling decode/4 -- Router has no capability awareness at all.
  • No $/cancel_request or session/cancel clauses. Both are Connection-routed session-control paths pre-filtered on the wire method string before decode/4 is called. Calling decode(side, :notification, "$/cancel_request", _) or decode(side, :notification, "session/cancel", _) falls through to the catch-all like any other unrecognized method -- callers must pre-filter, not rely on Router to special-case them.
  • No pending-map bookkeeping. result_marker/1 is a pure lookup; storing pending[id] = %{marker: ..., from: ...} and reading it back on response arrival (markers computed once at send time; ids include nil) is Connection's job.

Decoding

iex> Raxol.AgentClientProtocol.Router.decode(:agent, :request, "session/prompt", %{"sessionId" => "s1", "prompt" => []})
{:ok, {:prompt, %Raxol.AgentClientProtocol.Schema.AgentTypes.PromptRequest{session_id: "s1", prompt: []}}}

iex> Raxol.AgentClientProtocol.Router.decode(:agent, :request, "_vendor/thing", %{"x" => 1})
{:ok, {:ext_request, "_vendor/thing", %{"x" => 1}}}

iex> Raxol.AgentClientProtocol.Router.decode(:agent, :request, "no/such/method", %{})
{:error, :method_not_found}

Summary

Types

A decoded, dispatchable inbound message: {callback_tag, decoded_params_or_nil}.

What Connection should do with a pending outbound request's response.

Which side is decoding/dispatching: the side that HANDLES the row (see MethodTable.rows_for_side/1).

Functions

Classify and decode an inbound JSON-RPC method + params for side.

Dispatch a decoded message (as produced by decode/4) to handler's matching callback. Request callbacks are called with (params, ctx) (or (ctx) alone for params: nil rows); notification callbacks are called with (params, ctx) and this function always returns :ok regardless of what the callback itself returns.

Look up how the sending side should decode the response to an outbound request for wire, computed once at send time -- never re-derive this from the wire string when the response actually arrives; store the returned marker in the pending-request entry instead.

Types

decoded()

@type decoded() ::
  {atom(), struct() | nil}
  | {:ext_request, String.t(), map()}
  | {:ext_notification, String.t(), map()}

A decoded, dispatchable inbound message: {callback_tag, decoded_params_or_nil}.

result_marker()

@type result_marker() :: {:decode, module()} | :ext | :unknown

What Connection should do with a pending outbound request's response.

side()

@type side() :: :agent | :client

Which side is decoding/dispatching: the side that HANDLES the row (see MethodTable.rows_for_side/1).

Functions

decode(arg1, arg2, wire, params)

@spec decode(side(), Raxol.AgentClientProtocol.MethodTable.kind(), String.t(), term()) ::
  {:ok, decoded()} | {:error, term()}

Classify and decode an inbound JSON-RPC method + params for side.

Returns {:ok, decoded} for a recognized core/registered row or an "_"-prefixed ext passthrough, {:error, :method_not_found} for an unrecognized method (⇒ -32601 at the caller), or {:error, reason} when a recognized row's params fail to decode (⇒ -32602 at the caller).

Never creates an atom from wire -- unknown and ext methods stay binaries end-to-end.

dispatch(atom, handler, arg, ctx)

@spec dispatch(side(), module(), decoded(), term()) :: term()

Dispatch a decoded message (as produced by decode/4) to handler's matching callback. Request callbacks are called with (params, ctx) (or (ctx) alone for params: nil rows); notification callbacks are called with (params, ctx) and this function always returns :ok regardless of what the callback itself returns.

result_marker(arg1)

@spec result_marker(String.t()) :: result_marker()

Look up how the sending side should decode the response to an outbound request for wire, computed once at send time -- never re-derive this from the wire string when the response actually arrives; store the returned marker in the pending-request entry instead.