ACP v2: Prompt Response Success Only Confirms Message Insertion, Not Turn Completion
The article explains how ACP v2 decouples message insertion from turn completion, making session/prompt responses only acknowledge insertion while deferring completion, errors, and cancellations to session-level state_update notifications, requiring clients to maintain separate tracking for submission receipt and session activity.
The article analyzes a critical semantic shift in the Agent Client Protocol (ACP) v2 draft (schema-v2.0.0-alpha.9, tagged 2026-10-16) compared to v1 (schema-v1.26.0, v1.12.0 crate). In v1, a session/prompt request blocks until the entire turn finishes, returning a stopReason that lets the client close loading state and end streaming. In v2, the same request returns quickly with only a messageId; model output, tool calls, and termination reasons arrive later via session/update notifications, with running possibly preceding the insertion response.
v1: Response Equals One Turn
v1 defines a turn as the complete interaction from user message to agent reply. When no tool calls remain, the agent must respond to the original session/prompt with a StopReason; cancellation uses the same channel with cancelled. This binds message acceptance, work progress, and turn completion into a single return value, making history replay, multi-client sessions, background work, and queued messages difficult to express.
"Acceptance" Is Defined as Insertion
v2 removes ambiguity: acceptance means the agent has logically added the user message to the ACP session, giving it a messageId and a position relative to other messages. Mere receipt, queuing, or ID assignment do not count. For local slash commands without runtime records, the adapter may insert a user message that exists only in the live stream. The response must occur after insertion and includes the mandatory non-empty messageId (agent-generated). The agent must also emit a user_message update with full content or fragments, which cannot precede insertion. The spec acknowledges no fixed order between response and update; clients must tolerate either arriving first. When the response arrives first, it uses the JSON-RPC request ID to correlate the submission with (sessionId, messageId); when the update arrives first, it must merge with the existing message rather than create a duplicate. Different submissions must have distinct IDs even if content is identical; clients cannot guess ownership by content or arrival order. This makes the session/prompt response a mere receipt for the submission, not the message's data.
Insertion Splits Errors into Two Channels
Insertion is the commit point. Before insertion, rejection returns a JSON-RPC error (including -32800 for cancellation or session close) and no user_message update is sent. The spec does not guarantee absence of side effects, so clients must not assume blind retry is safe. After insertion, the agent must return success even if failure or cancellation occurred before the response was sent; the request cannot be turned into an error response. The migration guide states: session/prompt errors now always mean the message was not inserted. Work failures are reported via state_update with idle, stopReason: "error", and a JSON-RPC error object. Failure does not undo insertion or prior tool side effects; a new prompt creates a new message, not a retry. Cancellation follows the same pattern: after insertion, request-level cancellation cannot use -32800. The agent should stop model requests and tool calls, then emit idle with stopReason: "cancelled" if foreground work was active. Clients must respond to pending permission requests with cancelled and continue accepting tool-call updates arriving after session/cancel. The draft warns that many client libraries throw on aborted operations; agents must catch those and report cancelled, not error, to avoid misclassifying normal cancellations as faults. Session-close races follow the same line: if insertion is blocked, the request gets -32800; if already inserted, it gets a successful messageId. The prompt response need not precede the close response.
Completion Is a Session Snapshot, Not a Single Reply
v2 uses state_update to report foreground activity with three stable states: running, idle, and requires_action. This is defined as a session-level aggregate snapshot, not an event tied to a specific prompt, model call, or permission request. Foreground work includes model processing, tool execution, and waiting for user action; background tasks may continue and emit notifications during idle without changing foreground state. The idle rule is session-scoped: it must be reported when no foreground activity exists, including between consecutive turns. Repeated snapshots are idempotent and do not signal another completion. stopReason is "should" not "must"; clients must treat idle as foreground end even if stopReason is missing, null, malformed, or unrecognized, but without a recognizable stop reason they cannot infer how work ended. A key consequence: idle does not guarantee "your prompt completed." A prompt may start foreground work, join ongoing foreground work, or the agent may start work without a prompt (e.g., following up a completed background task). When session-level foreground activity ends, the client only knows "no foreground work now," not which submission brought it there. To account per message, clients must maintain their own mapping of submissions to foreground start/end; the spec defines state_update as a session snapshot, not a per-prompt completion event. The migration guide lists this as a v2 motivation: moving progress and completion into the notification stream lets the same message stream serve history replay and agent-initiated work, and leaves room for future queued work. Effectively, session-level snapshots decouple "who initiated work" from "how completion is expressed."
Clients Must Change Their State Machines
The most direct impact is on SDK and client state machines. After the session/prompt RPC completes, notification processing must not stop; the migration guide requires SDKs to keep the session's processing and notifications alive. Treating the RPC return as a cleanup point loses subsequent output, tool updates, and end states. Submissions and messages must be stored separately: submissions keyed by JSON-RPC request ID, messages by (sessionId, messageId); both paths may arrive first, and either must be mergeable. running may arrive before insertion; it reports foreground work, not insertion confirmation, and cannot be used as a receipt. UIs need at least four distinct moments: submitted but no receipt, inserted, foreground running or waiting for user, session idle. Merging "submitted" and "inserted" hides whether a lost response means the message never entered; merging "inserted" and "completed" gives false success on post-insertion failures. Retry must be a product decision, not an automatic transport-layer action. The draft admits response loss leaves the submission's fate uncertain; resending creates a new message. Either make resends visible in the UI or maintain an application-level deduplication key; do not translate RPC timeout into a transparent retry. Middleware must respect the same boundary: gateways or proxies must not synthesize a completion state upon seeing a successful prompt response, nor stop forwarding session/update after the response. Tests must assert on notification sequences; checking only RPC return values misses post-insertion failure, cancellation, and late-update paths.
What This Trade-Off Buys
v2 trades "response confirms insertion only" for decoupling request from work. Requests can finish early while work progresses in the notification stream; replay and background work reuse the same state representation; agent-initiated work no longer needs a fake user message. The cost is that clients must maintain two simultaneous truths: whether the message was inserted, and what the session is doing now. Collapsing these two truths into one return value worked in v1; in v2 it fails in both directions. The draft is still evolving; documentation and tags may diverge. When citing specific behaviors, pin to schema-v2.0.0-alpha.9 rather than the default branch.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architecture Development Notes
Focused on architecture design, technology trend analysis, and practical development experience sharing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
