Adapter behaviour for a coding-agent CLI.
A session talks to exactly one CLI, and every CLI differs in two places:
the argv it is launched with, and the newline-delimited JSON it speaks on
stdin/stdout. Everything else in CrowdControl.Session -- buffering,
cursors, backpressure, retention, backends -- is agent-agnostic.
Built-in adapters:
CrowdControl.Agent.ClaudeCode(:claude,:claude_code,:open_code) -- Claude Code's--output-format stream-jsonwire format, also spoken by theopen-codeCLI.CrowdControl.Agent.Omp(:omp) -- Oh My Pi in--mode rpc, its newline-delimited JSON-RPC protocol.
Adapters normalize their wire format into CrowdControl.Protocol.message/0,
so a subscriber written against Claude Code works unchanged against omp: the
session id still arrives as {:system_init, %{"session_id" => id}} and the
end of a turn still arrives as {:result, subtype, map}.
Selecting an adapter
Pass :agent (an alias atom or a module implementing this behaviour):
CrowdControl.run("hi", agent: :omp)When :agent is omitted it is inferred from the :executable basename
("omp" => CrowdControl.Agent.Omp), defaulting to
CrowdControl.Agent.ClaudeCode.
Summary
Types
One file an adapter needs inside the sandbox: an absolute in-sandbox path, its bytes, and its octal mode.
Callbacks
Builds {executable, args, env} for launching the CLI.
Decodes one line of CLI stdout into a tagged message.
Encodes a user prompt as a stdin frame.
Frames written to the CLI's stdin immediately after exec, before any prompt.
Files this adapter needs written inside the sandbox before its CLI starts.
Functions
Resolves the adapter module for a set of session options.
The sandbox files module needs, or [] when it declares none.
Types
@type sandbox_file() :: {Path.t(), iodata(), non_neg_integer()}
One file an adapter needs inside the sandbox: an absolute in-sandbox path, its bytes, and its octal mode.
Callbacks
Builds {executable, args, env} for launching the CLI.
Raises ArgumentError on options the adapter cannot express.
@callback decode_line(binary()) :: CrowdControl.Protocol.message()
Decodes one line of CLI stdout into a tagged message.
@callback encode_prompt(prompt :: binary(), seq :: non_neg_integer(), keyword()) :: binary()
Encodes a user prompt as a stdin frame.
seq is a per-session counter starting at 0, so adapters that need request
ids can mint stable ones without extra state.
Frames written to the CLI's stdin immediately after exec, before any prompt.
Used for protocol handshakes; return [] when the CLI needs none.
@callback sandbox_files(keyword()) :: [sandbox_file()]
Files this adapter needs written inside the sandbox before its CLI starts.
Optional; an adapter that does not implement it stages nothing, which is why
CrowdControl.Agent.ClaudeCode needs no change.
A CLI that reads configuration from disk — CrowdControl.Agent.Omp resolving
a custom provider's baseUrl out of models.yml — has a problem no argv or
environment variable solves on a remote substrate: the file has to exist on
the sandbox's filesystem, and a host temp directory is not visible there.
Rendering is the adapter's job; writing is the backend's, since only the
backend knows how bytes cross into its substrate.
Must be pure and deterministic: it is called once per exec, after
CrowdControl.Backend.provision/1 (there is no sandbox to write to before
that) and again on nothing else. Refuse a malformed option in
build_command/1 instead, which runs before a sandbox has been created
and billed.
Functions
Resolves the adapter module for a set of session options.
Explicit :agent wins; otherwise the :executable basename decides; otherwise
CrowdControl.Agent.ClaudeCode.
iex> CrowdControl.Agent.resolve(agent: :omp)
CrowdControl.Agent.Omp
iex> CrowdControl.Agent.resolve(executable: "/opt/homebrew/bin/omp")
CrowdControl.Agent.Omp
iex> CrowdControl.Agent.resolve([])
CrowdControl.Agent.ClaudeCode
@spec sandbox_files( module(), keyword() ) :: [sandbox_file()]
The sandbox files module needs, or [] when it declares none.
Probed rather than required, so an adapter opts in by defining
sandbox_files/1 and every other adapter is unaffected.