{"slug": "acp-from-scratch", "title": "ACP from scratch", "summary": "Zed Industries published the Agent Client Protocol (ACP) on August 27, 2025, in partnership with Google, with Gemini CLI as the reference implementation, to let any coding agent work inside any code editor without a bespoke integration per pair. ACP runs an agent as a child process speaking JSON-RPC 2.0 over stdio, one JSON message per line, and the agent stays alive across sessions rather than restarting per question. JetBrains said on October 6, 2025 that it would co-develop the protocol with Zed across IntelliJ IDEA, PyCharm and WebStorm, and in January 2026 the two launched the ACP Registry, which now lists Gemini CLI, Claude Code, Codex CLI, GitHub Copilot, OpenCode, Goose, Cline, Qwen Code, Mistral Vibe and Cursor.", "body_md": "You have a **coding agent** you like (mine is **[Mini Me](https://mini-me.org/)**, my little coding agent side project), and a **code editor** you don't want to leave (in my case it's rather [VSCodium](https://vscodium.com/), but when I need to stay \"focused\" I use [Zed](https://zed.dev/)). 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.\n\nToday, the **Agent Client Protocol**, or **ACP**, is here to solve this problem.\n\nIn this article, I suggest we start from scratch:\n\n- what problem ACP solves,\n- how a conversation goes between a **code editor** and a**coding agent** ,\n- and where **Mini Me** fits in all this (there's no reason I shouldn't advertise my side project 😉).\n\n## [The problem: editors × agents](#the-problem-editors-agents)\n\nTake 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.\n\n**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.\n\nIf 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:\n\n\"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.\"\n\nSource: [Bring Your Own Agent to Zed](https://zed.dev/blog/bring-your-own-agent-to-zed)\n\n## [A bit of history (very short)](#a-bit-of-history-very-short)\n\n**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...).\n\nIn 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.\n\n## [The three building blocks of ACP](#the-three-building-blocks-of-acp)\n\nACP relies on three fairly simple choices:\n\n- An agent launched as a subprocess, which talks over stdio\n- JSON-RPC 2.0: requests, responses and notifications\n- Two roles (client / agent), and requests in both directions\n\n### [1. An agent launched as a subprocess, which talks over stdio](#_1-an-agent-launched-as-a-subprocess-which-talks-over-stdio)\n\nThe 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).\n\n✋ **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.\n\nPractical consequence: anything that is not a protocol message must go to the error output (`stderr`).\n\n### [2. JSON-RPC 2.0: requests, responses and notifications](#_2-json-rpc-20-requests-responses-and-notifications)\n\nEach message is a JSON object, and there are three kinds:\n\n- a **request** carries an`id` and a`method` : it expects a response;\n- a **response** carries the**same**`id` , and a`result` (or an`error` );\n- a **notification** carries a`method` but**no `id`** : it expects nothing back.\n\nA request means \"answer me\". A notification means \"FYI\". Remember this difference well, we'll find it everywhere.\n\n### [3. Two roles, and requests in both directions](#_3-two-roles-and-requests-in-both-directions)\n\n- 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).\n- The AI **agent** (coding agent) is the program that talks to the model and runs the tools.\n\nImportant 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).\n\n## [A session, from end to end](#a-session-from-end-to-end)\n\nLet'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).\n\nLet's go through the steps one by one.\n\n### [Step 1: `initialize`, introductions](#step-1-initialize-introductions)\n\n`initialize`, introductions\nThe 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:\n\n```\n{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{\n  \"protocolVersion\":1,\n  \"clientCapabilities\":{\"fs\":{\"readTextFile\":true,\"writeTextFile\":true},\"terminal\":true},\n  \"clientInfo\":{\"name\":\"Zed\",\"version\":\"0.210.0\"}\n}}\n```\n\nThe agent answers (same `id`, `0`) with its version and its own capabilities:\n\n```\n{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":{\n  \"protocolVersion\":1,\n  \"agentCapabilities\":{\"loadSession\":false,\"promptCapabilities\":{\"image\":false}},\n  \"agentInfo\":{\"name\":\"mini-me\",\"version\":\"0.11.0\"},\n  \"authMethods\":[]\n}}\n```\n\n✋ 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.\n\n### [Step 2: `session/new`, opening a conversation](#step-2-sessionnew-opening-a-conversation)\n\n`session/new`, opening a conversation\nA **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:\n\n```\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"session/new\",\"params\":{\n  \"cwd\":\"/home/me/projects/mini-me\",\n  \"mcpServers\":[]\n}}\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"sessionId\":\"bob-51234-1\"}}\n```\n\nThe **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).\n\n### [Step 3: `session/prompt`, the user asks a question](#step-3-sessionprompt-the-user-asks-a-question)\n\n`session/prompt`, the user asks a question\nThe 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:\n\n```\n{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"session/prompt\",\"params\":{\n  \"sessionId\":\"bob-51234-1\",\n  \"prompt\":[\n    {\"type\":\"text\",\"text\":\"explain this file \"},\n    {\"type\":\"resource_link\",\"name\":\"acp.go\",\"uri\":\"file:///home/me/projects/mini-me/internal/acp/acp.go\"}\n  ]\n}}\n```\n\nIt'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...\n\n### [Step 4: `session/update`, the agent tells what it's doing](#step-4-sessionupdate-the-agent-tells-what-its-doing)\n\n`session/update`, the agent tells what it's doing\nWhile 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:\n\n```\n{\"jsonrpc\":\"2.0\",\"method\":\"session/update\",\"params\":{\n  \"sessionId\":\"bob-51234-1\",\n  \"update\":{\"sessionUpdate\":\"agent_message_chunk\",\"content\":{\"type\":\"text\",\"text\":\"I'm reading the file \"}}\n}}\n```\n\nThe `sessionUpdate` field says what it is about. The most common ones:\n\n| `sessionUpdate` | What it is | \n|---|---|\n| `agent_message_chunk` | A piece of the model's answer. | \n| `agent_thought_chunk` | A piece of the model's \"thinking\", displayed separately. | \n| `tool_call` | A tool starts (for example `bash: go test ./...` ). | \n| `tool_call_update` | The tool progresses or ends ( `completed` or`failed` ), with its result, some text or a**diff** . | \n| `plan` | The list of steps the agent intends to follow. | \n| `available_commands_update` | The slash commands ( `/new` ,`/compact` ...) the agent handles itself. | \n\n🥰 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.\n\n### [Step 5: `session/request_permission`, the agent asks for permission](#step-5-sessionrequest_permission-the-agent-asks-for-permission)\n\n`session/request_permission`, the agent asks for permission\nNow 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:\n\n```\n{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"session/request_permission\",\"params\":{\n  \"sessionId\":\"bob-51234-1\",\n  \"toolCall\":{\"toolCallId\":\"call-2\",\"title\":\"bash: rm -rf build/\",\"kind\":\"execute\",\"status\":\"pending\"},\n  \"options\":[\n    {\"optionId\":\"once\",\"name\":\"Allow once\",\"kind\":\"allow_once\"},\n    {\"optionId\":\"always\",\"name\":\"Allow bash for this session\",\"kind\":\"allow_always\"},\n    {\"optionId\":\"reject\",\"name\":\"Reject\",\"kind\":\"reject_once\"}\n  ]\n}}\n```\n\nThe editor shows a dialog to the user, then answers with the chosen option:\n\n```\n{\"jsonrpc\":\"2.0\",\"id\":7,\"result\":{\"outcome\":{\"outcome\":\"selected\",\"optionId\":\"once\"}}}\n```\n\nThere are four kinds of options: `allow_once`, `allow_always`, `reject_once` and `reject_always`. What \"always\" means is up to the agent.\n\n### [Step 6: the end of the turn, and why it stopped](#step-6-the-end-of-the-turn-and-why-it-stopped)\n\nWhen 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**.\n\n```\n{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":{\"stopReason\":\"end_turn\"}}\n```\n\n| `stopReason` | Meaning | \n|---|---|\n| `end_turn` | The model has finished talking and calls no more tools. | \n| `max_tokens` | The response's token limit was reached. | \n| `max_turn_requests` | The agent reached its maximum number of model calls for this turn. | \n| `refusal` | The model refused to continue. | \n| `cancelled` | The client cancelled the turn. | \n\n### [What if we change our mind? `session/cancel`](#what-if-we-change-our-mind-sessioncancel)\n\n`session/cancel`\nIf, for example, the user presses Esc, the client (the code editor) sends a **notification** (no `id`, no response expected):\n\n```\n{\"jsonrpc\":\"2.0\",\"method\":\"session/cancel\",\"params\":{\"sessionId\":\"bob-51234-1\"}}\n```\n\nThe 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.\n\n## [Requests or notifications: the summary](#requests-or-notifications-the-summary)\n\nIf you should remember only one thing from this article, it may be this table:\n\n| Message | Direction | Kind | \n|---|---|---|\n| `initialize` | editor -> agent | request | \n| `session/new` | editor -> agent | request | \n| `session/prompt` | editor -> agent | request (the response comes at the end of the turn) | \n| `session/update` | agent -> editor | notification | \n| `session/request_permission` | agent -> editor | request | \n| `session/cancel` | editor -> agent | notification | \n\n## [What about the rest?](#what-about-the-rest)\n\nI won't go into every detail here, but know that the coding agent can also:\n\n- **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` ...);\n- **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;\n- **connect to the MCP servers** the editor passes to it in`session/new` , or ignore them if it has no use for them.\n\nSo 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).\n\n## [And where does Mini Me fit in?](#and-where-does-mini-me-fit-in)\n\n**[Mini Me](https://mini-me.org/)** 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**:\n\n- streams the model's answers with `agent_message_chunk` ;\n- 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` ;\n- asks for permission with `session/request_permission` , and when you choose \"always\", it remembers it**per tool name, for the duration of the session** ;\n- offers its own slash commands, like `/new` (clear the history) and`/compact` (compress the history), which it handles without going through the model.\n\n## [And what's coming next for the protocol?](#and-whats-coming-next-for-the-protocol)\n\nACP moves fast. A **v2** is in the works (a [first draft](https://agentclientprotocol.com/announcements/acp-v2-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.\n\n## [To conclude](#to-conclude)\n\nACP comes down to a handful of ideas:\n\n- an agent launched as a subprocess, which speaks JSON-RPC over stdio, one message per line;\n- a \"handshake\" 🤝 (`initialize` ) where each side announces what it can do;\n- sessions, and \"speaking\" turns (`session/prompt` ) that end with a stop reason;\n- a stream of notifications (`session/update` ) to show everything as it happens;\n- requests in both directions, for permissions and the editor's services.\n\nAnd the promise: you choose your agent without changing your editor, and vice versa.\n\nSee you very soon for a next article.\n\nWritten by\n\nKeep reading\n\n### Zed Editor + Docker Agent + ACP: coding with a local agent plugged into llmman\n\nPlug Zed's agent panel into Docker Agent over ACP, then point it at llmman to chat with a fully local, OpenAI-compatible coding agent.\n\nOct 3, 2026\n\n### A mini code agent with Docker Agent - Part 8: a local agent to learn Rust\n\nAssemble 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.\n\nAug 11, 2026\n\n### A mini code agent with Docker Agent - Part 5: the `mcp` toolset\n\nPlug 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.\n\nAug 6, 2026\nFrom other blogs", "url": "https://wpnews.pro/news/acp-from-scratch", "canonical_source": "https://k33g.org/p/20261031-acp-from-scratch", "published_at": "2026-10-31 00:00:00+00:00", "updated_at": "2026-10-31 20:50:10.626839+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "developer-tools", "ai-tools"], "entities": ["Agent Client Protocol", "Zed Industries", "Google", "Gemini CLI", "JetBrains", "ACP Registry", "Claude Code", "GitHub Copilot"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/acp-from-scratch", "markdown": "https://wpnews.pro/news/acp-from-scratch.md", "text": "https://wpnews.pro/news/acp-from-scratch.txt", "jsonld": "https://wpnews.pro/news/acp-from-scratch.jsonld"}}