You have a coding agent you like (mine is Mini Me, my little coding agent side project), and a code editor you don't want to leave (in my case it's rather VSCodium, but when I need to stay "focused" I use Zed). Often, the two can only talk to each other if someone wrote an integration for this "specific pair", for example the VSCode extensions for GitHub Copilot or Claude Code; otherwise you're stuck using a CLI in your IDE's terminal.
Today, the Agent Client Protocol, or ACP, is here to solve this problem.
In this article, I suggest we start from scratch:
- what problem ACP solves,
- how a conversation goes between a code editor and acoding agent ,
- and where Mini Me fits in all this (there's no reason I shouldn't advertise my side project π).
The problem: editors Γ agents #
Take a code editor that wants to let you use several agents: it will have to write one integration per agent. Now take a coding agent that must be available in several editors: it will have to write one "plugin" per editor. The number of integrations to maintain is the number of editors multiplied by the number of agents. I'll let you imagine the number of integrations to build as the number of code editors / IDEs and agents grows.
ACP simplifies this. The code editor implements its role once (it is the ACP client, with a built-in feature, or a plugin / extension), on its side the agent implements its own role once (the agent that speaks ACP), and every combination works.
If this rings a bell, that's normal: it's exactly what the Language Server Protocol (LSP) did for completion and diagnostics. The ACP announcement post makes the comparison itself:
"Just as the Language Server Protocol unbundled language intelligence from monolithic IDEs, our goal with the Agent Client Protocol is to enable you to switch between multiple agents without switching your editor."
Source: Bring Your Own Agent to Zed
A bit of history (very short) #
ACP was created by Zed Industries (the Zed editor) and published on August 27, 2025, in partnership with Google: Gemini CLI was the reference implementation. On October 6, 2025, JetBrains announced it would co-develop the protocol with Zed and bring it to its whole range of IDEs (IntelliJ IDEA, PyCharm, WebStorm...).
In January 2026, Zed and JetBrains launched the ACP Registry, a shared registry to discover and install agents directly from the code editor / IDE (or even from any ACP client). Today, this registry lists Gemini CLI, Claude Code (an ACP wrapper for Claude Code), Codex CLI, GitHub Copilot, OpenCode, Goose, Cline, Qwen Code, Mistral Vibe, Cursor... On the code editor side, besides Zed and JetBrains, Neovim, Emacs, VS Code or Obsidian go through plugins or extensions.
The three building blocks of ACP #
ACP relies on three fairly simple choices:
- An agent launched as a subprocess, which talks over stdio
- JSON-RPC 2.0: requests, responses and notifications
- Two roles (client / agent), and requests in both directions
1. An agent launched as a subprocess, which talks over stdio
The code editor launches the agent as a child process. The agent reads the editor's messages on its standard input (stdin) and writes its own on its standard output ( stdout), and there is one JSON message per line (on a single line).
β Careful, the agent is not restarted for every question: the editor starts it only once, and the agent stays alive for the whole conversation (and even for several sessions), waiting for messages on its standard input. So it looks a bit like a server... but it is not an HTTP server: no port, no network. It has only one "interlocutor", the code editor that launched it, connected to it by two pipes (communication channels between the two processes). And when the code editor closes the conversation or stops, the agent stops too. It's like, for example, with LSP and Go: the editor starts gopls once, then "talks" to it as long as it needs to.
Practical consequence: anything that is not a protocol message must go to the error output (stderr).
2. JSON-RPC 2.0: requests, responses and notifications
Each message is a JSON object, and there are three kinds:
- a request carries an
idand amethod: it expects a response; - a response carries thesame
id, and aresult(or anerror); - a notification carries a
methodbutnoid: it expects nothing back.
A request means "answer me". A notification means "FYI". Remember this difference well, we'll find it everywhere.
3. Two roles, and requests in both directions
- The client is the code editor: it opens sessions, sends the user's questions, displays what the agent says, and provides services to the AI agent (asking the user for a permission, reading or writing a file, opening a terminal).
- The AI agent (coding agent) is the program that talks to the model and runs the tools.
Important point: requests don't only go one way. The editor asks the agent questions, but the agent can also ask the editor questions (we'll see it with permissions).
A session, from end to end #
Let's see what a complete session looks like. Solid arrows are requests (they expect a response), grey dashed arrows are their responses, and orange dashed arrows are notifications (they expect nothing).
Let's go through the steps one by one.
Step 1: initialize, introductions
initialize, introductions
The client (our code editor) speaks first. It announces the protocol version it uses, what it can do for the agent (its capabilities), and who it is:
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{
"protocolVersion":1,
"clientCapabilities":{"fs":{"readTextFile":true,"writeTextFile":true},"terminal":true},
"clientInfo":{"name":"Zed","version":"0.210.0"}
}}
The agent answers (same id, 0) with its version and its own capabilities:
{"jsonrpc":"2.0","id":0,"result":{
"protocolVersion":1,
"agentCapabilities":{"loadSession":false,"promptCapabilities":{"image":false}},
"agentInfo":{"name":"mini-me","version":"0.11.0"},
"authMethods":[]
}}
β The rule to remember: anything not declared is not supported. An agent that does not declare loadSession will never receive a request to reload a session. A client that does not declare fs will never receive a file read request. So a minimal agent can declare no optional capability at all and still work.
Step 2: session/new, opening a conversation
session/new, opening a conversation
A session is a conversation with its own history. The client asks for one, giving the working directory (always an absolute path) and, optionally, a list of MCP servers:
{"jsonrpc":"2.0","id":1,"method":"session/new","params":{
"cwd":"/home/me/projects/mini-me",
"mcpServers":[]
}}
{"jsonrpc":"2.0","id":1,"result":{"sessionId":"bob-51234-1"}}
The agent chooses the session identifier. From then on, this identifier comes with every message about this conversation (by the way, one agent can hold several sessions in parallel).
Step 3: session/prompt, the user asks a question
session/prompt, the user asks a question
The user types their question in the editor. The client sends it as content blocks: the text becomes a text block, and a file mentioned with @ becomes a resource_link block:
{"jsonrpc":"2.0","id":2,"method":"session/prompt","params":{
"sessionId":"bob-51234-1",
"prompt":[
{"type":"text","text":"explain this file "},
{"type":"resource_link","name":"acp.go","uri":"file:///home/me/projects/mini-me/internal/acp/acp.go"}
]
}}
It's a request (it has an id), so the client expects a response. But that response will only come at the end, when the agent has finished its work. In the meantime, the agent doesn't stay silent...
Step 4: session/update, the agent tells what it's doing
session/update, the agent tells what it's doing
While it works, the agent doesn't wait until the end to "talk". It sends session/update notifications (no id, so no response expected), which the editor displays "as they come". For example, a piece of the model's answer, streamed:
{"jsonrpc":"2.0","method":"session/update","params":{
"sessionId":"bob-51234-1",
"update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"I'm reading the file "}}
}}
The sessionUpdate field says what it is about. The most common ones:
sessionUpdate |
What it is |
|---|---|
agent_message_chunk |
A piece of the model's answer. |
agent_thought_chunk |
A piece of the model's "thinking", displayed separately. |
tool_call |
A tool starts (for example bash: go test ./... ). |
tool_call_update |
The tool progresses or ends ( completed orfailed ), with its result, some text or adiff . |
plan |
The list of steps the agent intends to follow. |
available_commands_update |
The slash commands ( /new ,/compact ...) the agent handles itself. |
π₯° A nice detail: when the agent modifies a file, it sends a diff (the path, the old text and the new one), and the editor takes care of displaying it nicely.
Step 5: session/request_permission, the agent asks for permission
session/request_permission, the agent asks for permission
Now let's look at the request that goes the other way, and in particular the one asking for a permission, from the agent to the code editor. Before running a somewhat sensitive command (running tools), the agent asks for the user's approval. It describes the tool call concerned and offers options:
{"jsonrpc":"2.0","id":7,"method":"session/request_permission","params":{
"sessionId":"bob-51234-1",
"toolCall":{"toolCallId":"call-2","title":"bash: rm -rf build/","kind":"execute","status":"pending"},
"options":[
{"optionId":"once","name":"Allow once","kind":"allow_once"},
{"optionId":"always","name":"Allow bash for this session","kind":"allow_always"},
{"optionId":"reject","name":"Reject","kind":"reject_once"}
]
}}
The editor shows a dialog to the user, then answers with the chosen option:
{"jsonrpc":"2.0","id":7,"result":{"outcome":{"outcome":"selected","optionId":"once"}}}
There are four kinds of options: allow_once, allow_always, reject_once and reject_always. What "always" means is up to the agent.
Step 6: the end of the turn, and why it stopped
When the agent has finished its work, it finally answers the session/prompt request from step 3 (same id, 2). And this response does not contain the text of the answer (it was already sent piece by piece): it only says why the turn stopped.
{"jsonrpc":"2.0","id":2,"result":{"stopReason":"end_turn"}}
stopReason |
Meaning |
|---|---|
end_turn |
The model has finished talking and calls no more tools. |
max_tokens |
The response's token limit was reached. |
max_turn_requests |
The agent reached its maximum number of model calls for this turn. |
refusal |
The model refused to continue. |
cancelled |
The client cancelled the turn. |
What if we change our mind? session/cancel
session/cancel
If, for example, the user presses Esc, the client (the code editor) sends a notification (no id, no response expected):
{"jsonrpc":"2.0","method":"session/cancel","params":{"sessionId":"bob-51234-1"}}
The contract on the agent side is precise: stop what it's doing, run nothing more, and still answer the pending session/prompt request, with "stopReason":"cancelled". An agent that "forgets" to answer would leave the editor in a shaky state, waiting for a response that will never come.
Requests or notifications: the summary #
If you should remember only one thing from this article, it may be this table:
| Message | Direction | Kind |
|---|---|---|
initialize |
editor -> agent | request |
session/new |
editor -> agent | request |
session/prompt |
editor -> agent | request (the response comes at the end of the turn) |
session/update |
agent -> editor | notification |
session/request_permission |
agent -> editor | request |
session/cancel |
editor -> agent | notification |
What about the rest? #
I won't go into every detail here, but know that the coding agent can also:
- use the editor's services , if the editor declared them at initialization: read or write a file (
fs/read_text_file,fs/write_text_file), which lets it take into account the editor's not-yet-saved content, or open a real terminal in the editor (terminal/create...); - reload an existing session (
session/load), if it declaredloadSession: true: it must then "replay" the whole conversation to the editor, without running the tools again; - connect to the MCP servers the editor passes to it in
session/new, or ignore them if it has no use for them.
So ACP and MCP are complementary: ACP links the editor to the agent, MCP provides tools to the agent. ACP actually reuses MCP's JSON representations where they apply (text, image and resource blocks).
And where does Mini Me fit in? #
Mini Me is an AI agent dedicated to using local language models that "speaks" ACP: it implements the agent role, with an ACP front in internal/acp, written in Go with the community SDK github.com/coder/acp-go-sdk. In practice, this means the editor launches Mini Me, talks to it in JSON-RPC over stdio, and Mini Me:
- streams the model's answers with
agent_message_chunk; - runs its tools itself (for now it doesn't use the editor's file/terminal services) and reports on them with
tool_callandtool_call_update; - asks for permission with
session/request_permission, and when you choose "always", it remembers itper tool name, for the duration of the session ; - offers its own slash commands, like
/new(clear the history) and/compact(compress the history), which it handles without going through the model.
And what's coming next for the protocol? #
ACP moves fast. A v2 is in the works (a first draft was published on July 20, 2026), where the session becomes the central unit rather than the "one prompt, one answer" round trip. But it's a draft: the maintainers ask to keep v1 and advise against making it the default behavior in production. So everything described in this article is about version 1 of the protocol, the one we use today.
To conclude #
ACP comes down to a handful of ideas:
- an agent launched as a subprocess, which speaks JSON-RPC over stdio, one message per line;
- a "handshake" π€ (
initialize) where each side announces what it can do; - sessions, and "speaking" turns (
session/prompt) that end with a stop reason; - a stream of notifications (
session/update) to show everything as it happens; - requests in both directions, for permissions and the editor's services.
And the promise: you choose your agent without changing your editor, and vice versa.
See you very soon for a next article.
Written by
Keep reading
Zed Editor + Docker Agent + ACP: coding with a local agent plugged into llmman
Plug Zed's agent panel into Docker Agent over ACP, then point it at llmman to chat with a fully local, OpenAI-compatible coding agent.
Oct 3, 2026
A mini code agent with Docker Agent - Part 8: a local agent to learn Rust
Assemble a local Rust learning companion with Docker Agent: a cookbook RAG, a rust-analyzer LSP toolset, and the context caps that keep a small model alive.
Aug 11, 2026
A mini code agent with Docker Agent - Part 5: the mcp toolset
Plug external MCP tool servers into a local Docker Agent: a local stdio server, a remote streamable HTTP one, and a tools filter to keep a small model focused.
Aug 6, 2026 From other blogs