編輯器的 agent 面板上,轉圈圈一直在轉。剛才送出的那段話,Claude Code 正拿著它讀檔、跑工具、改 diff。這時候編輯器得回答一個很樸素的問題:它現在是在忙,還是在等我?

在 Agent Client Protocol 的 v1 裡,這個問題的答案藏在一個沒回來的 response 裡。請求還 pending,就代表還在忙;response 回來了,turn 就結束了。

這裡的 ACP 是編輯器跟 coding agent 之間的協定,repo 描述寫的是「A protocol for connecting any editor to any agent」。它跟我 10 月 8 日寫的IBM 那個 ACP只是縮寫撞名,那個管 agent 跟 agent 之間,已經宣布併入 A2A、repo 也封存了。

送出之後,誰告訴你結束了

v1 的做法很直覺。session/prompt 送出去之後,整個 turn 期間這個請求都掛著;agent 一邊做事一邊丟 session/update 通知,最後才用 response 帶一個 stopReason 收尾:

1
{ "jsonrpc": "2.0", "id": 2, "result": { "stopReason": "end_turn" } }

這就像打電話給餐廳訂位,對方說「您稍等」,然後你拿著話筒等到他把整件事辦完才掛斷。電話沒掛,就代表事情還沒完。簡單,好懂,一次只辦一件事的時候完全夠用。

問題出在「話筒」被賦予了兩個意思。官方的 v2 遷移文件講得很直白,v1 這樣做「entangled prompt acceptance with turn completion」,把「請求被接受」跟「這一輪做完了」綁在同一個 response 上。後果是 replay、多個 client 看同一個 session、背景工作、排隊訊息,這幾件事都很彆扭。

想一下排隊訊息。agent 還在跑,你又送了第二句話。在 v1 的模型裡,第二句的 response 要代表什麼?它被收下了,還是它跑完了?兩個意思擠在同一個欄位,總有一個要犧牲。

draft v2 把話筒換成號碼牌。session/prompt 的 response 只做一件事:確認你的訊息已經插進對話裡,給你一個 messageId。

1
{ "jsonrpc": "2.0", "id": 2, "result": { "messageId": "msg_user_8f7a1" } }

至於在忙還是在等,改由叫號螢幕負責。agent 用 session/update 的 state_update 回報狀態,三個值:running、idle、requires_action。turn 結束時是一則 idle 通知,stopReason 搬到這裡:

1
2
3
4
5
6
7
8
9
10
11
12
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "sess_abc123",
"update": {
"sessionUpdate": "state_update",
"state": "idle",
"stopReason": "end_turn"
}
}
}

requires_action 是我覺得最值得看的那個狀態。前景工作整體卡在等你動作時(例如等你回權限確認),agent 回報它;之後進度恢復,文件規定一定要再報一次 running,沒恢復就結束則報 idle。開場那個「在忙還是在等我」,v2 給的是一個明寫的值,不用從 response 有沒有回來去推。

錯誤的語意也跟著乾淨了。v1 的 turn 跑到一半失敗,是用 session/prompt 的 error response 回報;v2 加了一個 error stop reason 放在 idle 通知裡,文件寫「An error response to session/prompt now always means the message was not inserted.」。error response 只剩一個意思:你的話沒進去。

取消也是同一個邏輯。session/cancel 本來就是通知,v2 沒改;改的是確認方式,從 prompt response 帶 cancelled,變成 idle 的 state_update 帶 stopReason: "cancelled"。

官方給的理由我覺得是整份文件最有份量的一段:前景進度全部用通知表達之後,同一套訊息流就能拿來做 session/resume 的歷史重播,也能表達 agent 自己發起的工作,未來要支援排隊也不用再發明新的東西。一條管子,三種用途。

切模式:從專用方法變成一個設定值

Claude Code 使用者對「模式」不陌生,plan、acceptEdits 這類切換天天在用。ACP 這邊也有模式。

v1 有一整套專用 API。session/new 的 response 帶 modes,client 用 session/set_mode 切,agent 用 current_mode_update 回報:

1
2
3
4
5
6
7
8
9
{
"jsonrpc": "2.0",
"id": 2,
"method": "session/set_mode",
"params": {
"sessionId": "sess_abc123def456",
"modeId": "code"
}
}

v2 把這整套拿掉了,session/set_mode、modes 欄位、current_mode_update、所有 SessionMode* 型別都移除。模式變成 session config options 裡的一個選項,category 叫 mode,跟 model、model_config、thought_level 平起平坐:

1
2
3
4
5
6
7
8
9
10
11
{
"jsonrpc": "2.0",
"id": 2,
"method": "session/set_config_option",
"params": {
"sessionId": "sess_abc123def456",
"configId": "mode",
"type": "id",
"value": "code"
}
}

這個轉向其實早有預告。Session Config Options 在 2026 年 2 月 4 日的 v0.10.8 就標為穩定,v1 的 session modes 文件頂端也掛了一句「Dedicated session mode methods will be removed in a future version of the protocol.」。v2 只是把話兌現。

拿 Claude Code 自己的 permission mode 來對照,是我的類比,不是協定文件說的:兩邊都在處理「agent 現在用什麼規矩做事」,但層次不同。ACP 的選擇是「模式沒有特別到需要自己的方法」,它就是一個設定,跟選哪個模型同一個等級。

接回 session:兩扇門合成一扇

v1 有兩種方式回到一個舊 session。session/load 會重播完整歷史,由 loadSession 能力開關;session/resume 不重播,由 sessionCapabilities.resume 開關。兩個方法、兩個能力旗標,做的是同一件事的兩種口味。

v2 只留 session/resume,加一個選填的 replayFrom:

1
2
3
4
5
6
7
8
9
10
{
"jsonrpc": "2.0",
"id": 3,
"method": "session/resume",
"params": {
"sessionId": "sess_abc123",
"cwd": "/home/user/project",
"replayFrom": { "type": "start" }
}
}

省略 replayFrom 就等於 v1 的 resume。replayFrom 是 tagged union,文件說這樣設計是為了讓未來的版本能加上「從某個點開始重播」,到時加一個 type 就好,不必再開新方法。這段跟前面的 prompt 生命週期是同一根線串起來的:重播能做成 resume 的一個參數,前提就是前景進度已經全部走通知了。

一條死掉的路:讓編輯器幫 agent 讀檔、跑指令

v1 讓 Client 端提供檔案系統跟 terminal:fs/read_text_file、fs/write_text_file,再加上 terminal/create、output、release、wait_for_exit、kill 五個方法。想法很合理,編輯器手上有未存檔的 buffer、有自己的終端機,讓 agent 透過編輯器讀寫,看到的才是你眼前的東西。

v2 整組拿掉。官方理由是「In practice this surface was inconsistently implemented outside of a few IDEs」,而且 agent 反正得為不提供這些的 client 準備自己的檔案與執行處理,等於每個 agent 都在養兩條路。替代方案是讓 Client 透過 mcpServers 提供一個 MCP server,把檔案存取、未存檔狀態、指令執行都變成跟其他工具同等地位的 tool。

這個理由是官方說法,各家 IDE 實際實作到什麼程度,我沒有獨立查證。

同一條路上還有兩具屍體。MCP 的 HTTP+SSE("type": "sse")在 v2 移除,每個 server config 都要明寫 type,stdio 也變成要宣告的能力;Next Edit Suggestions 更乾脆,文件寫「Removed from both v1 and v2 … No replacement.」,連 v1 都一起拿掉。

能力旗標也順手整理了。v1 混用 boolean("image": true)跟 object("list": {}),v2 全部改成 object,判斷式從 promptCapabilities.image === true 變成 capabilities.session.prompt.image != null。initialize 的欄位也不再分 clientCapabilities/agentCapabilities,雙方都叫 capabilities 跟必填的 info。

這些事是照什麼順序發生的

版本號這裡有個很容易寫錯的坑。2026 年 6 月 24 日的 v1.0.0 是 Rust crate/schema 的版本,不是協定從 0 升到 1。CHANGELOG 0.7.0 寫得很清楚:「The Protocol version is already, and remains, 1」。README 也提醒「Consumers should not infer wire compatibility from the crate or schema release version alone.」。所以你看到 v1.12.0,那是套件;協定的 protocolVersion 一直是 1,draft 的是 2。

Agent Client Protocol 的版本演進

2025-06-23

agentclientprotocol/agent-client-protocol repo 建立。

2025-09-17

v0.4.0,GitHub release 列表上最早的一版。

2025-11-25

0.7.0,schema 為了對 SDK 友善大改,CHANGELOG 註明協定版本一直是 1。

2026-02-04

v0.10.8,Session Config Options 標為穩定。

2026-06-01

v0.13.5,協定文件搬進 v1/,新增 draft v2 文件與 schema,v2 移除 modes/models 專用 API。

2026-06-05

v0.13.6,v2 移除 Client 的 fs/terminal,移除 MCP SSE。

2026-06-24

v1.0.0,crate/schema 的版本,不是協定版本。

2026-07-20

schema-v2.0.0-alpha.1。

2026-10-09

v1.11.0,session notices 與 compaction 標為穩定。

2026-10-16

v1.12.0 與 schema-v2.0.0-alpha.9,仍在補 v2 的 foreground state transitions、cancellation 等條文;同日 claude-agent-acp 發 v0.93.0。

從 alpha.1 到 alpha.9 不到三個月。repo 截至 10 月 18 日是 4,463 顆星(gh api 當天取得),當天還有 push,Apache-2.0。誰最早提出這個協定,我找不到第一手敘述;能看到的線索是 README 橫幅圖來自 zed.dev,以及 Claude Code 轉接層的舊路徑 zed-industries/claude-code-acp 現在會導到新組織。

你的 Claude Code 站在哪一邊

你在 JetBrains AIR 這類支援 ACP 的編輯器裡用 Claude Code,中間隔著 @agentclientprotocol/claude-agent-acp 這個轉接層(它的舊 repo 名是 zed-industries/claude-code-acp)。它用官方的 Claude Agent SDK 實作 ACP agent,package.json 裡依賴的是 @anthropic-ai/claude-agent-sdk 0.3.300 跟 @agentclientprotocol/sdk 1.9.0。之前寫 OpenAB 那篇時接的也是它。

它的 README 有一節 Experimental ACP v2:用環境變數 CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1 啟動後,在 initialize 要求 protocol version 2 以上的 client 拿到 v2,其他 client 照舊拿 v1。README 自己也寫了「The draft can still change, and this surface changes with it.」,而 Steering 與 AIR extensions 目前只有 v1。

這種「同一個 agent 對不同 client 講不同版本」的做法,正是 v2 文件建議的協商方式:client 送它支援的最高版本,agent 選不高於它的最高版本,沒得選就回 -32022。官方建議 agent 兩版都支援、逐連線協商。

我的立場是:日常用的編輯器,現在不要開那個環境變數。README 寫的 stable 是 1,遷移文件自己也說 v2 整體「still labeled draft」,要用 feature flag 擋著。這個月 v2 還在補取消與狀態轉換的條文,開了等於幫草案做 beta 測試。等 v2 拿掉 draft 標籤、schema 離開 alpha,或者你常用的那個編輯器在 release notes 明寫預設協商 v2,三件事出現任何一件,再打開也不遲。如果你在寫 ACP client 或 agent,那就反過來,現在就該照 v2 的 prompt 生命週期設計狀態機,因為這部分的方向已經很清楚。

要說清楚的是,這篇我只讀了官方 repo 的文件、CHANGELOG、release 列表跟 gh api 的元資料,沒有安裝 claude-agent-acp,也沒有在 Zed 或 JetBrains 實際跑過。文中的 JSON 都是文件範例,不是我抓到的線上流量。轉接層預設協商哪一版,我也只依 README 的描述,沒有讀原始碼確認。

真正換掉的是那支話筒

被刪掉的東西有個共同點:都是「某個請求的回應,順便代表了某個狀態」。prompt 的 response 順便代表 turn 結束,load 跟 resume 的分家順便代表要不要重播,Client 端的 fs 順便代表 agent 看得到你的 buffer。

v2 做的事,是把這些「順便」一個一個拆出來,變成明寫的狀態或明寫的參數。

v1 的心智是打一通電話,等對方把事辦完才掛。v2 的心智是走進一間有叫號螢幕的店,你遞出單子拿到號碼牌,接下來看螢幕。功能多寡反而是次要的,上面那幾項改動有好幾個是在刪東西、併東西。換掉的是 session 的樣子:從「一連串我在等的請求」,變成「一個我隨時可以抬頭看一眼的共享狀態」。多個 client 同時看、斷線重連後重播、agent 自己開工,在後一種心智裡都只是「多看一眼螢幕」而已。