mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
refactor: report plans on the experimental ACP v2 surface (#1235)
Second part of step 5 of opt-in ACP v2 support (after #1234): the v2 surface now sends the agent's plan. Nothing changes on ACP v1. ## Change - v1 `plan` becomes v2 `plan_update` with an items plan: `{ type: "items", planId, entries }`. Plan entries are identical in both versions, and a v2 items plan is replaced whole on each update, as a v1 plan is. The update's `_meta` passes through. - **One plan id per session** (`SESSION_PLAN_ID`). v1 has one plan per session, which every update replaces, whether it comes from TodoWrite or from the Task tools. Several plans, such as one per subagent (a subagent's TodoWrite replaces the session's plan today, marked by `_meta.claudeCode.parentToolUseId`), and v2's unstable markdown plans wait for step 6. ## Scenario suite `todo-write` and `task-create-update` pass, with golden files. That leaves only `session-load-replay` in the status table. ## Verification A unit test covers the mapping; build, lint and all 2383 tests pass.
This commit is contained in:
@@ -61,8 +61,6 @@ const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PASSES = "passes";
|
||||
/** Step 4 of `docs/acp-v2.md`. */
|
||||
const REPLAY = "Invalid params: The ACP v2 surface does not replay session history yet";
|
||||
/** Step 5 of `docs/acp-v2.md`. */
|
||||
const PLANS = "The ACP v2 surface does not translate plan session updates yet";
|
||||
|
||||
/**
|
||||
* How each scenario runs on the v2 surface: it passes, or stops for a reason
|
||||
@@ -91,8 +89,8 @@ const V2_STATUS: Record<string, string> = {
|
||||
"subagent-nested": PASSES,
|
||||
"subagent-late-child-update": PASSES,
|
||||
"subagent-transcript-extension": PASSES,
|
||||
"todo-write": PLANS,
|
||||
"task-create-update": PLANS,
|
||||
"todo-write": PASSES,
|
||||
"task-create-update": PASSES,
|
||||
goal: PASSES,
|
||||
"network-permission": PASSES,
|
||||
"exit-plan-approve": PASSES,
|
||||
|
||||
26
src/tests/acp-scenarios/__snapshots__/v2/task-create-update.jsonl
generated
Normal file
26
src/tests/acp-scenarios/__snapshots__/v2/task-create-update.jsonl
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{"kind":"response","method":"initialize","payload":{"_meta":{"steering":{"supported":true}},"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
|
||||
{"kind":"response","method":"session/new","payload":{"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"pending"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"pending"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"in_progress"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"completed"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"completed"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 2","type":"text"}],"messageId":"<id-2>","sessionUpdate":"user_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-2>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"All ","type":"text"},"messageId":"msg_follow","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"done.","type":"text"},"messageId":"msg_follow","sessionUpdate":"agent_message_chunk"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
|
||||
11
src/tests/acp-scenarios/__snapshots__/v2/todo-write.jsonl
generated
Normal file
11
src/tests/acp-scenarios/__snapshots__/v2/todo-write.jsonl
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
{"kind":"response","method":"initialize","payload":{"_meta":{"steering":{"supported":true}},"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
|
||||
{"kind":"response","method":"session/new","payload":{"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
|
||||
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"plan":{"entries":[{"content":"Writing tests","priority":"medium","status":"in_progress"},{"content":"Ship","priority":"medium","status":"pending"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
|
||||
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}
|
||||
@@ -1036,9 +1036,19 @@ describe("ACP v2 session translation", () => {
|
||||
});
|
||||
|
||||
it("fails on session updates that it does not translate yet", () => {
|
||||
expect(() => v2SessionUpdate({ sessionUpdate: "plan", entries: [] })).toThrow(
|
||||
"does not translate plan session updates yet",
|
||||
);
|
||||
expect(() =>
|
||||
v2SessionUpdate({ sessionUpdate: "compaction_update", compactionId: "c", status: "started" }),
|
||||
).toThrow("does not translate compaction_update session updates yet");
|
||||
});
|
||||
|
||||
it("reports the plan of the session as one v2 plan", () => {
|
||||
const entries = [{ content: "Test", priority: "high" as const, status: "pending" as const }];
|
||||
const meta = { claudeCode: { parentToolUseId: "toolu_agent" } };
|
||||
expect(v2SessionUpdate({ sessionUpdate: "plan", entries, _meta: meta })).toEqual({
|
||||
sessionUpdate: "plan_update",
|
||||
plan: { type: "items", planId: "plan", entries },
|
||||
_meta: meta,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a new tool call with an upserting tool_call_update", () => {
|
||||
|
||||
@@ -7,6 +7,13 @@ import type { AcpSessionNotification } from "../acp-subagents.js";
|
||||
import { v2AvailableCommands, v2ConfigOptions } from "./session.js";
|
||||
import { v2ToolCallUpdate } from "./tool-call.js";
|
||||
|
||||
/**
|
||||
* The id of the plan of a session. v1 has one plan per session, which every
|
||||
* plan update replaces whole, from TodoWrite and from the Task tools alike.
|
||||
* v2 tracks plans by id, so that plan gets one.
|
||||
*/
|
||||
export const SESSION_PLAN_ID = "plan";
|
||||
|
||||
/**
|
||||
* The v2 form of a v1 session update, or `undefined` when v2 has no
|
||||
* counterpart and the update tells a v2 client nothing new.
|
||||
@@ -39,6 +46,13 @@ export function v2SessionUpdate(
|
||||
case "tool_call":
|
||||
case "tool_call_update":
|
||||
return v2ToolCallUpdate(update);
|
||||
case "plan":
|
||||
// A v2 plan of items is replaced whole as well.
|
||||
return {
|
||||
sessionUpdate: "plan_update",
|
||||
plan: { type: "items", planId: SESSION_PLAN_ID, entries: update.entries },
|
||||
...(update._meta != null ? { _meta: update._meta } : {}),
|
||||
};
|
||||
case "current_mode_update":
|
||||
// v2 has no modes: the mode is the `mode` config option. Every mode
|
||||
// change also reaches the client as a `config_option_update` or in the
|
||||
|
||||
Reference in New Issue
Block a user