radian acp: the Agent Client Protocol server
radian acp is the same coding agent as the full-screen interface and radian exec,
exposed over Agent Client Protocol (ACP) version 1:
newline-delimited JSON-RPC 2.0 on stdin and stdout. The client starts it as a child process,
sends prompts, answers permission requests, and receives the turn as session/update
notifications. stdout carries protocol messages only; startup lines, warnings and errors go
to stderr.
radian acp --provider openai-compatible --model gpt-5 --workspace "$PWD" --profile workspace
The key comes from the credential store, never from the command line, exactly as in every
other host: run radian login first (see providers). There is no ACP
authentication method in this version.
The flags#
radian acp accepts the conversation flags of radian exec with the
same names, values and defaults. Use that page for every one of them; this page does not
repeat their meaning.
The line-prompt and print-and-exit inputs are absent: -p/--prompt, --image,
--output, --yes, --list, --export, --search, --fork, --at, --format,
--out, --force, --include-reasoning and --include-provider-payload. Input arrives as
session/prompt, output goes over the protocol, and approvals go over the protocol. The
flag string a wrapper builds for radian exec is accepted unchanged once -p -,
--output jsonl and --yes are removed.
One process, one session#
- The first
session/newcreates the session. Itscwdmust be an absolute directory. If--workspacewas given,cwdmust name the same directory; if it was absent, the request'scwdbecomes the workspace. - The response is
{"sessionId": "<id>"}. That id is--resume, else--session-id, else a generated id, exactly as inradian exec. - A second
session/new, and asession/newwhosecwddiffers from--workspace, fail with an error that says one process holds one session. --resume <id>makes the firstsession/newcontinue that journal. A missing journal is asession/newerror, and so is a session created in another workspace:session/newapplies the same workspace rule assession/load.
Loading an existing session#
session/load opens an existing session by id, the same lookup --resume uses, and replays
its transcript to the client. initialize advertises loadSession: true.
- The request's
cwdmust satisfy the same rule assession/new: an absolute directory that equals--workspace, or, when--workspaceis absent, the workspace itself. - The session must belong to that workspace: the directory recorded in its
SessionCreatedevent must name the process workspace, the same rule--exportand--resumeuse. Both paths are compared as directories, so a workspace reached through a symlink matches the directory it names, and a session recorded under another spelling of that directory still loads. A session created in another workspace is refused with an error that names it, and nothing is written; the process can still load another session or start one withsession/new. - The reply is an empty object. Before it, radian sends the journal's transcript as
session/updatenotifications, in transcript order:user_message_chunkfor each user message,agent_thought_chunkfor reasoning,agent_message_chunkfor answer text, and atool_callfollowed by thetool_call_updateof its final status (completedorfailed, with the tool's output) for each tool call. - A prompt after the load continues the same journal: the loaded session is continued, never
forked, exactly like
radian exec --resume. The session id stays the one that was loaded. - An unknown id is an error that names the id.
- A
session/loadaftersession/new, a secondsession/load, and asession/loadnaming a different id than--resumeare refused with the one-process-one-session error. - An
allow_alwaysgrant from an earlier process is not restored: the first call that needs an approval after a load asks again.
Methods#
| Method | What happens |
|---|---|
initialize |
Negotiates protocol version 1 and advertises loadSession: true. |
session/new |
Creates (or, with --resume, continues) the one session; see above. |
session/load |
Opens an existing session by id, replays its transcript as updates, and continues it; see above. |
session/prompt |
Sends the prompt's text as a user message (a resource link becomes a text line) and answers once the end of its own turn is written to the session file. |
session/cancel |
Interrupts the running turn, like ESC in the full-screen interface; the journal keeps the interrupted prefix. |
session/request_permission |
Sent by radian for every approval and every ask_user question. |
session/update |
Sent by radian for the live turn and for a session/load replay; see below. |
session/prompt answers with stopReason: end_turn, cancelled, max_tokens,
max_turn_requests (the model-call budget ran out) or refusal (a model or hook error, or
another unfinished answer). max_tokens means the turn ended Incomplete for a truncated
answer that could not continue; see the exit-code rule for when that
happens. The answer waits for the driver to journal the turn end, so a client that reads the
session file right after the response already sees it. The turn is the one that took the prompt's
message: a background job's completion notice can wake a turn of its own before that turn runs,
and the wake turn's end answers nothing — the prompt keeps waiting for its own turn's answer. A
second prompt while a turn runs is refused. The response's _meta.radian
carries what ACP has no field for:
| Field | Meaning |
|---|---|
cost |
{ "micros", "incomplete", "estimated" }, the fee of the turn that answered the prompt, or null when no call of that turn had a price. estimated is always false: a turn this process ran priced each call as it arrived. |
text |
The last assistant message's text, in full — for a prompt, the text of the turn that answered it; empty when that turn appended none. |
metrics |
The answering turn's run metrics, in the object radian exec's Result.run_metrics carries. The keys, their types, units and when each is absent are in the Result record; the fold covers only the answering turn's records, exactly as cost and text do, and retries counts the answering turn's live retry events (an earlier turn's retry never carries over). |
error |
Present only on a refusal caused by a model or hook error of that turn: the masked error text. |
A client that does not read _meta ignores it.
Updates#
session/update carries the live events of a turn, and the replayed transcript of a
session/load; the durable transcript goes to the session journal and a full-screen session can
resume it.
agent_message_chunk: streamed answer text.agent_thought_chunk: streamed reasoning.user_message_chunk: a user message, in a replayed transcript.tool_call: a tool call started, with its id, title, kind, status, locations and raw input.tool_call_update: a tool finished, with its status and raw output.usage_update: the provider's reported usage and the context window, when the window is known.
Approvals and ask_user#
Every permission question and every ask_user question becomes session/request_permission:
- A permission question offers
allow_once,allow_alwaysandreject_once.allow_alwaysis a grant for this process only: it is never written to a profile and a new process has none. A rejection is a tool result carrying the reason. - A
cancelledoutcome denies the pending request with that reason and cancels the turn; the journal keeps the interrupted prefix. ask_useris asked one question at a time, and the question's options are the permission options.request_permissioncarries one selection and no free text, so atextquestion, a free-textOtheranswer and a multi-selection answer are not available on this surface; a text question fails the tool instead of inventing an answer.
MCP servers#
The mcpServers of session/new are merged as this session's MCP configuration at the same
level as --mcp-config; a duplicate id keeps the client's entry and
prints one warning on stderr. Only stdio servers run. An HTTP, SSE or ACP server is refused
in the session/new error.
Trust and file access#
The startup trust question is never asked: an untrusted workspace loads an untrusted project
and prints the one-line notice, exactly as radian exec with piped input does. See
permissions for --trust, --no-trust and the trust file.
radian never calls the client's fs/read_text_file, fs/write_text_file or terminal/*.
It reads and writes files with its own tools and starts its own shell under the same sandbox
and permission rules as the other hosts.
Limits#
- One input line is at most 64 MiB (67108864 bytes): enough for a prompt that carries several
images in base64. A longer line is discarded as it is read — it is never held whole — and
answered with a JSON-RPC parse error, code
-32700and idnull, whose message names the limit. The process then serves the next line normally; end of input in the middle of an over-long line is a normal end of input. - The output queue holds at most 64 messages. When it is full the producer waits instead of
dropping anything, so a client that stops reading stdout stalls the running turn until it
reads again; responses and
session/updatenotifications then arrive in order, none missing. Ending the input stream or aSIGTERMstill ends the process: a producer that is waiting for room stops waiting, and the messages that were not written yet are abandoned. - One prompt runs at a time per process. Parallel work belongs to another process or to the
subagenttool. - Background jobs are not awaited: the prompt response reports the turn, and a later job-completion wake turn streams its updates in the same session.
- A method that this version does not implement is a JSON-RPC method-not-found error.