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

Copy Markdown View Source

Ergonomic DX layer for agent handler code running inside session/prompt turn tasks (the functions a turn_runner closure or one of its subagent tasks actually calls). This module introduces no new runtime semantics: every function here is a thin, documented wrapper over Raxol.AgentClientProtocol.Connection.request/4 (the agent->client filesystem/terminal/permission requests) or Raxol.AgentClientProtocol.Session.post_update/2 / Session.request_permission/2 (the Session-owned turn primitives).

Why this isn't built on Connection.Ctx

Raxol.AgentClientProtocol.Connection.Ctx is the per-DISPATCH struct handed to the request/notification handler callback that answers session/prompt itself (it carries conn, reply_ref, rx_seq, etc.). It does not reach the turn task the handler spawns — turn_runner is injected at Session start with a fixed (session_pid, req) -> ... arity-2 shape (session.ex moduledoc, "Turn runner seam"), and a subagent's task closure only ever receives what its parent closed over. There is deliberately one ctx concept in this package (Connection.Ctx); this module does not introduce a second, competing one. Instead every function below takes the plain pid a turn task actually has in hand — the Connection pid (closed over from the original dispatch's ctx.conn when the app builds its turn_runner fun) for the outbound protocol requests, or the Session pid (the task's own session argument) for the two Session-owned primitives.

What's here

  • read_text_file/3,4, write_text_file/4,5 — fs/read_text_file / fs/write_text_file, sent via Connection.request/4. request/4 blocks its caller, so it is for plain handler-task code; a process that must stay responsive uses async_request/6 instead. A turn task is plain handler-task code, not the Session itself.
  • create_terminal/3,4, terminal_output/3,4, wait_for_terminal_exit/3,4, kill_terminal/3,4, release_terminal/3,4 — the five terminal/* methods, same Connection.request/4 wrapper.
  • request_permission/3,4 — builds a RequestPermissionRequest and delegates to Session.request_permission/2. Fail-closed, always: the ONLY way to get an allow back is a literal {:selected, %SelectedPermissionOutcome{}} decoded from the client's reply; every other terminal (timeout, client error, decode failure, disconnect, a session/cancel racing the ask) resolves {:ok, :cancelled} — deny is the zero value everywhere. This wrapper adds no new fail-closed logic; it is documentation + ergonomics over the Session's existing guarantee.
  • post_update/2 — delegates to Session.post_update/2 verbatim (same arity, same contract): forwards a session/update for the live turn, surfacing {:error, :turn_over} for a straggler post after drain and {:error, :empty_chunk} for an empty-text agent_message_chunk/agent_thought_chunk (never sent to the wire — see session.ex's empty_chunk?/1).

Timeouts

Connection.request/4's timeout_ms is pos_integer() — not :infinity (only async_request/6 accepts that). Every function below takes a :timeout option (default 30_000ms, except wait_for_terminal_exit/3,4 which defaults to 30 minutes since waiting for a command to exit is expected to take a while); pass a larger :timeout explicitly for a longer-running command rather than relying on a library-invented unbounded wait.

Summary

Functions

terminal/create — create a terminal and execute command. opts: :args ([String.t()]), :env (list of {name, value} string pairs or pre-built EnvVariable.t() structs), :cwd, :output_byte_limit, :timeout (default 30000ms). Only sent if the client declared the terminal capability.

terminal/kill — kill the terminal's command without releasing the terminal. opts: :timeout.

Emit a session/update for the live turn — a verbatim pass-through to Session.post_update/2 (same arity, same contract): forwards notification via the Connection's notify/3 on the Session's own FIFO lane, returns {:error, :turn_over} for a straggler post after the turn has drained, and {:error, :empty_chunk} for an empty-text agent_message_chunk/ agent_thought_chunk — the streaming guard lives in session.ex (empty_chunk?/1); this wrapper adds no logic of its own, only the re-export so handler code has one module to alias.

fs/read_text_file — read a text file on the client side. opts: :line (1-based start line), :limit (max lines), :timeout (default 30000ms). Only sent if the client declared the fs.readTextFile capability (the client answers -32601 otherwise — MethodTable's capability gate, unrelated to this wrapper).

terminal/release — release a terminal and free its resources. opts: :timeout.

Ask the client for permission to perform tool_call (session/request_permission), offering options. Delegates to Session.request_permission/2 — session is the turn's own Session pid (the task's session argument), NOT the Connection. Fail-closed: returns {:ok, {:selected, %SelectedPermissionOutcome{}}} ONLY on a decoded selected outcome; every other terminal — timeout, client error reply, undecodable reply, transport/connection death, or a session/cancel racing the ask — returns {:ok, :cancelled}. There is no third outcome and no exception path: deny is the zero value everywhere. The Session arms no timer of its own for this wait; Connection.async_request/6 is the single timeout authority, configured per-connection via the Session's :permission_timeout (default 600s).

terminal/output — the output captured so far, and exit status if the command has completed. opts: :timeout.

terminal/wait_for_exit — block until the terminal command exits. opts: :timeout (default 1800000ms = 30 minutes, since waiting for a command is expected to take a while; request/4 does not accept :infinity — pass an explicit larger :timeout for a longer-running command instead).

fs/write_text_file — write content to a text file on the client side. opts: :timeout (default 30000ms). Only sent if the client declared the fs.writeTextFile capability.

Functions

create_terminal(conn, session_id, command, opts \\ [])

@spec create_terminal(pid(), String.t(), String.t(), keyword()) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.CreateTerminalResponse.t()}
  | {:error, term()}

terminal/create — create a terminal and execute command. opts: :args ([String.t()]), :env (list of {name, value} string pairs or pre-built EnvVariable.t() structs), :cwd, :output_byte_limit, :timeout (default 30000ms). Only sent if the client declared the terminal capability.

kill_terminal(conn, session_id, terminal_id, opts \\ [])

@spec kill_terminal(pid(), String.t(), String.t(), keyword()) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.KillTerminalResponse.t()}
  | {:error, term()}

terminal/kill — kill the terminal's command without releasing the terminal. opts: :timeout.

post_update(session, notification)

@spec post_update(
  GenServer.server(),
  Raxol.AgentClientProtocol.Schema.LifecycleExtras.SessionNotification.t()
) :: :ok | {:error, :turn_over} | {:error, :empty_chunk}

Emit a session/update for the live turn — a verbatim pass-through to Session.post_update/2 (same arity, same contract): forwards notification via the Connection's notify/3 on the Session's own FIFO lane, returns {:error, :turn_over} for a straggler post after the turn has drained, and {:error, :empty_chunk} for an empty-text agent_message_chunk/ agent_thought_chunk — the streaming guard lives in session.ex (empty_chunk?/1); this wrapper adds no logic of its own, only the re-export so handler code has one module to alias.

read_text_file(conn, session_id, path, opts \\ [])

@spec read_text_file(pid(), String.t(), String.t(), keyword()) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.ReadTextFileResponse.t()}
  | {:error, term()}

fs/read_text_file — read a text file on the client side. opts: :line (1-based start line), :limit (max lines), :timeout (default 30000ms). Only sent if the client declared the fs.readTextFile capability (the client answers -32601 otherwise — MethodTable's capability gate, unrelated to this wrapper).

release_terminal(conn, session_id, terminal_id, opts \\ [])

@spec release_terminal(pid(), String.t(), String.t(), keyword()) ::
  {:ok,
   Raxol.AgentClientProtocol.Schema.ClientTypes.ReleaseTerminalResponse.t()}
  | {:error, term()}

terminal/release — release a terminal and free its resources. opts: :timeout.

request_permission(session, session_id, tool_call, options \\ [])

Ask the client for permission to perform tool_call (session/request_permission), offering options. Delegates to Session.request_permission/2 — session is the turn's own Session pid (the task's session argument), NOT the Connection. Fail-closed: returns {:ok, {:selected, %SelectedPermissionOutcome{}}} ONLY on a decoded selected outcome; every other terminal — timeout, client error reply, undecodable reply, transport/connection death, or a session/cancel racing the ask — returns {:ok, :cancelled}. There is no third outcome and no exception path: deny is the zero value everywhere. The Session arms no timer of its own for this wait; Connection.async_request/6 is the single timeout authority, configured per-connection via the Session's :permission_timeout (default 600s).

terminal_output(conn, session_id, terminal_id, opts \\ [])

@spec terminal_output(pid(), String.t(), String.t(), keyword()) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.TerminalOutputResponse.t()}
  | {:error, term()}

terminal/output — the output captured so far, and exit status if the command has completed. opts: :timeout.

wait_for_terminal_exit(conn, session_id, terminal_id, opts \\ [])

@spec wait_for_terminal_exit(pid(), String.t(), String.t(), keyword()) ::
  {:ok,
   Raxol.AgentClientProtocol.Schema.ClientTypes.WaitForTerminalExitResponse.t()}
  | {:error, term()}

terminal/wait_for_exit — block until the terminal command exits. opts: :timeout (default 1800000ms = 30 minutes, since waiting for a command is expected to take a while; request/4 does not accept :infinity — pass an explicit larger :timeout for a longer-running command instead).

write_text_file(conn, session_id, path, content, opts \\ [])

@spec write_text_file(pid(), String.t(), String.t(), String.t(), keyword()) ::
  {:ok, Raxol.AgentClientProtocol.Schema.ClientTypes.WriteTextFileResponse.t()}
  | {:error, term()}

fs/write_text_file — write content to a text file on the client side. opts: :timeout (default 30000ms). Only sent if the client declared the fs.writeTextFile capability.