Raxol. AgentClientProtocol. Agent behaviour
(Raxol Agent Client Protocol v0.1.0)
Copy Markdown
View Source
use Raxol.AgentClientProtocol.Agent to implement the agent side of
ACP: the process that answers initialize, creates/loads sessions, and
serves session/prompt turns.
This is a thin ergonomic layer over Raxol.AgentClientProtocol.Connection
and Raxol.AgentClientProtocol.Session:
it only generates the callback surface and wires the supervision tree.
Protocol correlation, cancellation, permission fan-out, and turn/streaming
semantics live entirely in those two modules -- nothing here re-implements
or shortcuts them.
Callback surface
One @callback per Raxol.AgentClientProtocol.MethodTable.rows_for_side(:agent)
row that carries an app-level callback (layer: :app -- everything except
session/cancel, which is Connection-routed session control with no
handler callback at all). Naming and arity are
generated straight from the table, not hand-maintained:
params: nilrows (currently onlylogout) get callback arity 1:callback(ctx).- every other row gets arity 2:
callback(params, ctx)for requests,callback(notification, ctx)for notifications.
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 init/1), reply_ref, rx_seq,
session_sup, task_sup.
A request callback returns {:ok, result_struct}, {:error, %Error{}},
or :deferred (legal only after Connection.delegate_reply/3; a
:deferred return without a prior delegation is a contract breach the
Connection turns into -32603). A notification callback's return value is
always ignored (Router.dispatch/4).
Call callbacks/0 to introspect the generated {callback, arity, wire}
list.
Defaults
Every generated callback (and handle_ext_request/3,
handle_ext_notification/3) has an overridable default: requests answer
{:error, Error.method_not_found()}, notifications are a silent :ok.
Override only what your agent supports -- an unimplemented,
capability-gated method (e.g. session/load without loadSession
advertised) never needs an explicit override, the wire-correct
-32601 falls out for free. init/1 defaults to {:ok, handler_arg}.
Extension methods
"_"-prefixed wire methods with no MethodTable row are routed by
Connection directly to handle_ext_request/3 /
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 MyAgent do
use Raxol.AgentClientProtocol.Agent
alias Raxol.AgentClientProtocol.Schema.AgentTypes.InitializeResponse
@impl true
def init(_arg), do: {:ok, %{sessions: %{}}}
@impl true
def initialize(_req, _ctx), do: {:ok, InitializeResponse.new(1)}
# ... new_session/2, prompt/2, etc. -- only what you support.
end
{:ok, sup} = Raxol.AgentClientProtocol.Agent.start_link(MyAgent, transport: {mod, handle})Library-mode wiring
start_link/2 and child_spec/1 build exactly this subtree:
ConnectionSupervisor Supervisor, :one_for_all,
│ auto_shutdown: :any_significant
├── Task.Supervisor restart: :permanent
├── DynamicSupervisor restart: :permanent (Session.Supervisor; :temporary children)
└── Connection restart: :temporary, significant: trueConnection resolves its task_sup/session_sup siblings via
Supervisor.which_children/1 in handle_continue/2 (never in init/1),
so this layer does not need to name or pre-register them; it only
needs to hand Connection the ConnectionSupervisor's own pid as
parent_sup, which the nested ConnectionSupervisor.init/1 callback
captures via self() (that callback body runs in the supervisor
process, before any child starts -- the standard OTP way to hand a
supervisor's own pid to a child it is about to start).
Summary
Types
Per-dispatch context. See Raxol.AgentClientProtocol.Connection.Ctx.
Callbacks
Handle an inbound "_"-prefixed extension notification. Return value is
ignored. Default: :ok.
Handle an inbound "_"-prefixed extension request with no MethodTable
row. Return shape matches the table-driven request callbacks. Default:
{:error, Error.method_not_found()}.
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 -- handlers run as concurrent Tasks, so
it is never mutated after init. Default: {:ok, handler_arg}.
Functions
Introspect the generated callback surface: {callback_atom, arity, wire_method}, in table order.
Library-mode child spec: embed one ACP agent connection subtree into the caller's own supervision tree, e.g.
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,
and notify/3 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).
Standalone start: Agent.start_link(MyAgent, transport: {mod, handle}).
Types
@type ctx() :: Raxol.AgentClientProtocol.Connection.Ctx.t()
Per-dispatch context. See Raxol.AgentClientProtocol.Connection.Ctx.
Callbacks
@callback authenticate( Raxol.AgentClientProtocol.Schema.AgentTypes.AuthenticateRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.AgentTypes.AuthenticateResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback close_session( Raxol.AgentClientProtocol.Schema.LifecycleExtras.CloseSessionRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.LifecycleExtras.CloseSessionResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback delete_session( Raxol.AgentClientProtocol.Schema.LifecycleExtras.DeleteSessionRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.LifecycleExtras.DeleteSessionResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
Handle an inbound "_"-prefixed extension notification. Return value is
ignored. Default: :ok.
@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()}.
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 -- handlers run as concurrent Tasks, so
it is never mutated after init. Default: {:ok, handler_arg}.
@callback initialize( Raxol.AgentClientProtocol.Schema.AgentTypes.InitializeRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.AgentTypes.InitializeResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback list_sessions( Raxol.AgentClientProtocol.Schema.Unstable.ListSessionsRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.Unstable.ListSessionsResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback load_session( Raxol.AgentClientProtocol.Schema.AgentTypes.LoadSessionRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.AgentTypes.LoadSessionResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback logout(Raxol.AgentClientProtocol.Connection.Ctx.t()) :: {:ok, Raxol.AgentClientProtocol.Schema.LifecycleExtras.LogoutResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback new_session( Raxol.AgentClientProtocol.Schema.AgentTypes.NewSessionRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.AgentTypes.NewSessionResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback prompt( Raxol.AgentClientProtocol.Schema.AgentTypes.PromptRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.AgentTypes.PromptResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback raxol_load_session( Raxol.AgentClientProtocol.Schema.AgentTypes.LoadSessionRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.AgentTypes.LoadSessionResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback raxol_steer_session( Raxol.AgentClientProtocol.Ext.Schema.SteerRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Ext.Schema.SteerResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback resume_session( Raxol.AgentClientProtocol.Schema.Unstable.ResumeSessionRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.Unstable.ResumeSessionResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback set_session_config_option( Raxol.AgentClientProtocol.Schema.Unstable.SetSessionConfigOptionRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.Unstable.SetSessionConfigOptionResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
@callback set_session_mode( Raxol.AgentClientProtocol.Schema.AgentTypes.SetSessionModeRequest.t(), Raxol.AgentClientProtocol.Connection.Ctx.t() ) :: {:ok, Raxol.AgentClientProtocol.Schema.AgentTypes.SetSessionModeResponse.t()} | {:error, Raxol.AgentClientProtocol.Error.t()} | :deferred
Functions
Introspect the generated callback surface: {callback_atom, arity, wire_method}, in table order.
@spec child_spec(keyword()) :: Supervisor.child_spec()
Library-mode child spec: embed one ACP agent connection subtree into the caller's own supervision tree, e.g.
children = [
{Raxol.AgentClientProtocol.Agent, handler: MyAgent, 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 init/1, default nil), :name
(registers the ConnectionSupervisor), :id (child id in the parent's
supervisor, defaults to Raxol.AgentClientProtocol.Agent -- override when
embedding more than one agent connection in the same tree).
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,
and notify/3 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/2returns 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) andDynamicSupervisor.start_child/2both do this for achild_spec/1entry. - A plain
Supervisor.start_link(children, ...)where achild_spec/1entry is one of severalchildrendoes NOT hand you theConnectionSupervisorpid directly -- that call returns YOUR OWN supervisor's pid, one level further out. Resolve theConnectionSupervisorchild fromSupervisor.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).
@spec start_link(module(), keyword()) :: Supervisor.on_start()
Standalone start: Agent.start_link(MyAgent, 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. A Connection/Session crash
is never silently swallowed: it tears the subtree down
(auto_shutdown: :any_significant), it does not restart in place.
Options: same as child_spec/1 minus :handler (positional) and :id
(not meaningful outside a parent supervisor's children list).