Using the terminal · 14
ACP
riffer acp runs riffer-rig as an Agent Client Protocol agent over stdio, so editors and other ACP clients can use it without a terminal.
Registering with an editor
Register the command riffer acp. In Zed, open Agent Settings → External Agents → Add Agent → Add Custom Agent, or add it to settings.json yourself:
{
"agent_servers": {
"Riffer": { "type": "custom", "command": "riffer", "args": ["acp"], "env": {} }
}
}
Each session loads exactly as the terminal does — settings, credentials, trusted rig.rb files, skills — for the working directory the client sends. The model comes from RIFFER_MODEL or the settings (precedence). The agent can’t ask while a session starts, so a session whose model or credentials are missing is refused with the reason: set them up in the terminal first.
What the agent supports
| ACP method | Support |
|---|---|
session/new |
starts a session; the sessionId is the riffer-rig session id |
session/prompt |
runs a turn, or a command when the text starts with / |
session/cancel |
cancels the running turn; it ends with cancelled |
session/load |
resumes a saved session, replaying its messages first; an unknown id is “resource not found” |
session/list |
lists saved sessions for the client’s cwd (all of them without one), titled by their first prompt |
session/request_permission |
sent only to relay a question; tool calls never wait for permission |
elicitation/create |
sent only to relay a free-text question |
session/resume, session/close, session/delete, authenticate |
not offered |
terminal/*, fs/* |
never requested |
Prompts may contain text and resource links; a resource link is passed to the model as its name and URI.
What the client sees
| In the session | ACP update |
|---|---|
| the reply streaming | agent_message_chunk |
| a tool call, once its arguments are complete | tool_call, in progress |
| that tool call finishing | tool_call_update: completed, or failed for an error (a blocked call included), with the result text |
| a notice or warning | agent_message_chunk, as a paragraph of its own |
| a background job exiting between prompts | agent_message_chunk: Background job <id> exited with status <status>: <command>; the model hears at the next prompt |
| the available commands | available_commands_update, after session/new, after a session/load replay, and when a reload changes them |
| How the turn ended | stopReason |
|---|---|
| completed, or a command that started no turn | end_turn |
| hit the step limit | max_turn_requests |
| output length limit, context window, or blocked output | max_tokens |
| cancelled | cancelled |
A turn that fails — a provider or network error — answers with an internal error carrying the same message the terminal would print, and the session stays usable.
Commands
A prompt starting with /name runs that command instead of prompting the model, with the rest of the text as its arguments: /reload, /trace, /auth, /model, /reasoning, /cost, every /skill:<name>, and commands from extensions. Output arrives as agent_message_chunk updates; an unknown name is reported the same way.
Questions
When riffer-rig needs an answer — trusting a project rig.rb, a missing credential during /auth, an extension’s own question — it asks the client:
| riffer-rig asks | The client gets | The answer |
|---|---|---|
| yes or no | session/request_permission with Yes and No |
yes only if Yes was selected |
| a choice | session/request_permission with one option per choice |
the selected option |
| free text | elicitation/create, one required string field named answer |
the submitted text; declined if the client doesn’t support form elicitation |
| a secret | nothing — ACP forbids asking for secrets in a form | declined |
Each question appears in the session as a pending tool call titled with the question, closed when it is answered. Anything other than a selected option or a submitted form — cancelling, an error, a disconnect — declines the question.
Questions can’t be asked while a session is starting. A project rig.rb that isn’t trusted yet is therefore skipped at session/new, and the client is asked about it the next time the file changes. To avoid that, trust it once in the terminal.
Limits
MCP servers the client sends with session/new or session/load aren’t used: stdio servers can’t be registered, because riffer-rig’s MCP client speaks only HTTPS, and servers in ACP’s http or sse shape are skipped. Each is reported.
Serving connections from your own process
riffer acp serves one connection on stdio, then exits. A Ruby process that manages its own connections builds the agent itself, once per connection:
live = {}
build = lambda do |cwd, host, servers|
Riffer::Rig::ACP.build_runtime(cwd, host, servers, env: env, home: Dir.home).tap { |runtime| live[runtime.id] = runtime }
end
Riffer::Rig::ACP.for(input: input, output: output, env: env, home: Dir.home, cwd: Dir.pwd, build: build, adopt: ->(id) { live[id] }).run do |connection|
# the client has disconnected; the process moves on when this block returns
end
| Keyword | Meaning |
|---|---|
input:, output: |
the connection’s IO pair |
env:, home: |
what each session’s Loader is built with: a Riffer::Rig::Env (or Env.load’s result) and the home directory |
cwd: |
the directory session/list uses when the client sends none |
build: |
how session/new builds a Runtime from the client’s cwd, host and MCP servers; default ACP.build_runtime |
adopt: |
called by session/load with the session id; return a still-running Runtime to hand it to this connection — a client that reconnects mid-turn reaches the same turn — or nil to restore from the store. It runs on a request thread |
run blocks until the client disconnects, yields the connection, and returns 0. It then cancels and closes the Runtimes it built or restored itself; a Runtime your build: returned or adopt: handed over is yours to close.