ACP from scratch

•12 min read

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 a coding 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.

Without ACP, one integration per editor-agent pair; with ACP, one implementation on each side
Without ACP, one integration per editor-agent pair; with ACP, one implementation on each side

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 id and a method: it expects a response;
  • a response carries the same id, and a result (or an error);
  • a notification carries a method but no id: 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).

An ACP session: initialization, a prompt with a permission request, a cancellation
An ACP session: initialization, a prompt with a permission request, a cancellation

Let's go through the steps one by one.

Step 1: 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:

json
{"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:

json
{"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

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:

json
{"jsonrpc":"2.0","id":1,"method":"session/new","params":{
  "cwd":"/home/me/projects/mini-me",
  "mcpServers":[]
}}
json
{"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

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:

json
{"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

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:

json
{"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:

sessionUpdateWhat it is
agent_message_chunkA piece of the model's answer.
agent_thought_chunkA piece of the model's "thinking", displayed separately.
tool_callA tool starts (for example bash: go test ./...).
tool_call_updateThe tool progresses or ends (completed or failed), with its result, some text or a diff.
planThe list of steps the agent intends to follow.
available_commands_updateThe 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

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:

json
{"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:

json
{"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.

A permission request: pending tool_call, request_permission, the dialog, then the tool allowed or rejected
A permission request: pending tool_call, request_permission, the dialog, then the tool allowed or rejected

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.

json
{"jsonrpc":"2.0","id":2,"result":{"stopReason":"end_turn"}}
stopReasonMeaning
end_turnThe model has finished talking and calls no more tools.
max_tokensThe response's token limit was reached.
max_turn_requestsThe agent reached its maximum number of model calls for this turn.
refusalThe model refused to continue.
cancelledThe client cancelled the turn.

What if we change our mind? session/cancel

If, for example, the user presses Esc, the client (the code editor) sends a notification (no id, no response expected):

json
{"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:

MessageDirectionKind
initializeeditor -> agentrequest
session/neweditor -> agentrequest
session/prompteditor -> agentrequest (the response comes at the end of the turn)
session/updateagent -> editornotification
session/request_permissionagent -> editorrequest
session/canceleditor -> agentnotification

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 declared loadSession: 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.
The editor's services: files and terminals, at the agent's request
The editor's services: files and terminals, at the agent's request

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).

ACP links the editor to the agent, MCP gives the agent tools
ACP links the editor to the agent, MCP gives the agent tools

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_call and tool_call_update;
  • asks for permission with session/request_permission, and when you choose "always", it remembers it per 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

0 Comments

No comments yet. Be the first to comment!

From other blogs

Related reading

Copyright © 2026•k33g_org's Blog•Powered by Writizzy