Agent Client Protocol (ACP)¶
ai-elements-acp makes the app an Agent Client Protocol
client, like an editor such as Zed or JetBrains IDEs. Coding agents that speak ACP become chat
providers: Claude Code, Codex and Gemini CLI through their ACP adapters, or any agent built on an
ACP SDK, such as Pydantic AI Harness's run_acp_stdio. It is built on the official
ACP Kotlin SDK.
class CodingAgentViewModel : ViewModel() {
private val agent = AcpAgent.webSocket("wss://dev-box.example.com/acp", cwd = "/home/me/project")
// Each conversation is one ACP session; permission requests show as approvals.
val chat = ChatController(backend = { approver -> AcpBackend(agent, approver) }, scope = viewModelScope)
override fun onCleared() = agent.close()
}
Connecting¶
// An agent on another machine, over the ACP Kotlin SDK's WebSocket transport
AcpAgent.webSocket("wss://dev-box.example.com/acp", cwd = "/home/me/project"),
// An agent the app starts itself, over stdio (the transport the ACP spec defines)
AcpAgent.process(listOf("npx", "@zed-industries/claude-code-acp"), cwd = "/workspace"),
- stdio is the transport the ACP specification defines.
AcpAgent.processstarts the agent as a subprocess. That suits a desktop JVM, or a device that can run the agent, for example in the harness sandbox. - WebSocket reaches an agent on another machine. ACP's remote transport (Streamable HTTP) is
still a draft, so this uses the ACP Kotlin SDK's WebSocket transport: one JSON-RPC message per
text frame, the same messages as stdio. The reference server
serves it at
ws://…/acp.
Security
An ACP agent can read and change files and run commands on the machine it runs on. Only connect
to agents you trust, over wss:// with authentication outside your own network.
cwd is the session's working directory on the agent's machine (an absolute path, as ACP
session/new requires). One connection is shared by all conversations using the agent and reopens
after a failure. Close the agent when you are done with it.
Sessions¶
Each conversation is one ACP session. The session id is kept in the reply's Message.metadata
under acp, so the next turn continues the same session and only the new user message is sent:
the agent keeps the history. When a session is gone, for example after the app restarted, it is
resumed with session/load if the agent supports it; otherwise a new session starts with the
earlier turns as context.
Mapping¶
| ACP | In the chat |
|---|---|
agent_message_chunk |
text; images as files, resource links as sources |
agent_thought_chunk |
reasoning |
tool_call, tool_call_update |
a tool call: title as its name, rawInput as input; content (text, diffs as unified diffs, terminals) or rawOutput as output; failed as an error |
plan |
the Plan element |
session/request_permission |
an approval: approve picks the agent's allow-once option, always allow its allow-always option, deny its reject-once option |
| stop | session/cancel |
PromptResponse |
usage; refusal, max_tokens, max_turn_requests end the turn with an error |
ACP permission answers carry only the chosen option, so the approval offers yes / no and always allow, without a reason or edited arguments. See Human in the loop.
Offering files¶
The client advertises no file system by default. Pass an AcpFileSystem to answer
fs/read_text_file (and fs/write_text_file unless it is read-only), for example over the harness
workspace or a folder the user shared:
AcpAgent.webSocket(url, files = object : AcpFileSystem {
override suspend fun read(path: String, line: Int?, limit: Int?) = workspaceFile(path).readText()
})
Terminals and ACP elicitation (still unstable in ACP) are not offered.