Elevator pitch
What are you proposing to change?For v2 of the protocol wire format, I am proposing a change in the lifecycle of the prompt request, allowing for more dynamic session updates from the agent, and unlocking new capabilities in the process. Once a session is created, the agent will be able to send session updates at any point in time, and prompt requests will last until the user message is inserted into the conversation, not until the end of the turn. As I’ll go into later, this not only removes some current awkwardness around the prompt request lifecycle, but also provides a more flexible foundation to add features like queued messages and multi-client replay. This can even allow the agent to initiate an interaction in a session rather than requiring it to wait for a user prompt, which is becoming increasingly important for background tasks and agents which may send updates before or after a “turn” is over since its runtime might be different than the main conversation. This sketch explores returning an agent-generated
messageId in the prompt response. For session/prompt, acceptance means insertion of the user message into the ACP conversation. The same ID identifies that message in live updates and, if retained, later replay, so clients that receive the response can correlate their submissions without introducing a second identifier. Insertion is distinct from processing completion and does not require persistent history.
Because the response no longer spans the turn, it can no longer report failures that happen during it. A failure after insertion instead ends foreground work with an error stop reason on the idle state_update, carrying the same JSON-RPC error object a v1 prompt error response carries.
Status quo
How do things work today and what problems does this cause? Why would we change things?Currently, the protocol kind of assumes that all turns will be initiated by a client and ended by an agent, with a series of session update notifications in-between. While in many cases this is enough, it is becoming clear that this model is not flexible enough. It is not clear how to model queued messages for instance: would these create a new turn request lifecycle? Or fit into the existing one? What if the agent wants to submit some text at the start of a session before the user prompts? Or a status update? Also, if an agent finishes its turn, wants to wait for the next user action, but has a background subagent or task running, can it only submit updates about that status after the user prompts again? When replaying a session, the prompt request can be turned into a user message notification, but what about the end of turn response? If you call load during a currently running session, how do you know that the turn is done? Some clients handle these out-of-turn updates more gracefully than others. But it is a constant point of confusion in discussions and issues. Decoupling the prompt response from session updates also leaves a correlation gap: when the agent reports an accepted user message, how does the submitting client know which pending prompt it belongs to? Matching by content or arrival order is unreliable when users submit identical prompts, multiple clients share a session, or insertion is delayed. The same problem appears after reconnecting if the agent accepted a prompt but the client never received its response. In the spirit of allowing as much flexibility in the protocol for new paradigms and designs to emerge in the prompt lifecycle, I think imposing fewer restrictions in the protocol, whether explicitly described or just implicitly inferred because of vague wording, on when participants can make session updates will allow for more dynamic sessions, as well as make it easier to extend to new use cases in the future.
What we propose to do about it
What are you proposing to improve the situation?
Change the session/prompt response
session/prompt is still a request, but its response lifecycle will change.
For this method, acceptance means insertion: the agent has added the user message to its ACP conversation, with a messageId and a position relative to other messages. Receiving the request, assigning an ID, or holding the input for later insertion is not acceptance.
The agent MUST respond successfully once the user message has been inserted, without waiting for foreground processing to finish. It MUST NOT send a successful response before insertion. The proposed successful response MUST include the inserted message’s messageId as a non-null string; omission and explicit null are both invalid. A request rejected before insertion uses a JSON-RPC error response instead.
Insertion is a logical conversation event, not a durable-storage guarantee or a claim that the model has consumed the input. Locally handled commands follow the same rule by inserting a live-only user message into the ACP conversation, without requiring a persisted transcript entry.
promptId is added to the request or updates.
The response confirms insertion and establishes the submitting client’s association with the message. The corresponding message updates report its content and placement; state_update notifications report foreground work. These are reports of insertion and processing, not separate acceptance and insertion phases.
Correlate prompts with accepted user messages
The JSON-RPC request ID links a response to the client’s submission. The returnedmessageId then links that submission to session updates and replay:
The working assumption is one accepted submission, one logical ACP user message. The agent MUST assign a distinct
messageId to each accepted submission, even when its content is identical to another submission. IDs are unique within the session and MUST NOT be reused for a different submission.
The agent MUST use the returned ID for all updates and chunks for that user message. The response and the user-message update describe the same insertion, but clients MUST tolerate those updates arriving before or after the response. On receiving the response, the submitting client can associate its local pending input with (sessionId, messageId), reconciling with an already-received message rather than creating a second canonical message.
Only the submitting client has the request/response association. Other clients identify the message by messageId without needing the original request. Neither content equality nor notification arrival order should be used to infer which client submitted a message.
An adapter can use an underlying agent’s user-message echo or insertion event as evidence of insertion and respond with the message’s ID while processing continues. It may use a private per-submission token to match that event to the outstanding request; this does not require a new field in ACP. An ID assigned in advance is not by itself evidence of insertion. The returned ID may be native to the underlying agent or adapter-owned, provided updates and replay preserve it consistently.
Additional Agent session/update notification types
Because session/updates can more freely flow from the agent, and we lost the ability to pass end_turn, errors, and other information from the prompt response, we need to provide the agent with the affordance for a few more notification types.
User message reported to clients
The prompt response acknowledges insertion. The agent MUST report each inserted user message throughuser_message or user_message_chunk updates, using the same messageId. These updates establish its content and placement in the client-visible conversation, not a guarantee that the agent stored it for later replay.
This RFD does not specify queueing, steering, or whether agents insert new prompts while busy.
The question then turns to what makes up this notification. Which brings us to:
Who owns the user message id?
The Message ID RFD defines that the agent owns message IDs. The client sends the prompt without a message ID, and the agent returns the ID when acknowledging insertion.
The corresponding user-message update reports the agent-owned content and placement under that same ID. The response does not add content to the conversation a second time; it establishes the submitting client’s association with the inserted message.
My current proposal is that this would look like the client sending the following message:
messageId to reconcile its optimistic message with the agent-reported message. If the notification arrives first, the response later establishes that association. Other clients simply render the message under the same ID. The response does not determine the message’s position in the feed.
This is a new message type as well. Not a user_message_chunk but just a user_message that allows for sending the entire message at once. The Message Updates and Chunks RFD defines the corresponding whole-message update and streamed-chunk patterns for user messages, agent messages, and agent thoughts.
Correlation and replay
Live correlation and history retention are separate:- The response and all updates for the accepted user message MUST use the same
messageId. - Agents are NOT REQUIRED to persist every accepted input or retain it for any minimum period. A message may be live-only, or later omitted under the agent’s history-retention policy.
- If an agent retains and replays that same message within the session, it MUST reuse the returned
messageId, including after reconnecting or restarting. This is an identity guarantee for retained messages, not a requirement to retain them.
(sessionId, messageId) association can recognize the message if it is replayed. It requests retained history with session/resume and replayFrom: { "type": "start" }, as described in the Session Resume Replay RFD. Omitting replayFrom resumes without replay.
A lost response remains an ambiguous outcome. The agent may have accepted the input, but the client does not know its ID. Replay can restore retained conversation history, but cannot reliably associate a message with that uncertain submission. Clients must not guess from content or order, or assume retrying cannot create a second submission. A client-generated correlation token could address this limitation for retained messages separately.
A successful response confirms that the message was inserted. Its absence from later replay does not contradict that acknowledgment: the message may have been live-only or may no longer be retained. Imported or non-ACP messages continue to use agent-generated message IDs without a corresponding prompt response.
Streaming user messages
The agent can stream a newly inserted user message usinguser_message_chunk updates with the returned messageId. No metadata-only user_message update is required just to establish correlation, and the chunk schema remains unchanged:
user_message update with content: []. This clears any content the client already holds for that messageId, preventing the replayed chunks from being appended to an earlier copy:
user_message update replaces the retained content directly. A metadata-only update does not clear content and is not sufficient for restarting the message’s content from the beginning.
These rules use the existing message-upsert semantics without an implicit client-side reset. When reconciling an optimistic message, clients should not append streamed content to their local copy of the submitted prompt; the agent’s content stream is authoritative.
state_update notification
This would be a notification from the agent to indicate that it’s current status has changed, such as the “turn” has ended, carrying information like stopReason and usage data for that turn.
Running, to indicate that a turn has begun. Important now that turns aren’t tied necessarily to prompts:
Report failures after insertion
In v1,session/prompt lasts until the end of the turn, so its error response can report a failure at any point: an expired credential, a provider outage, an exhausted quota. Once the response arrives at insertion, that channel is gone, and a failure after insertion has to end foreground work through the same state_update that reports every other outcome.
When foreground work ends because of a failure after the user message was inserted, the Agent reports idle with the error stop reason and, when it has one, the error:
stopReason: "error"means foreground work ended because something failed, not because it finished, reached a limit, was refused, or was cancelled. It applies to any foreground work, including work no prompt started; the insertion rule below is specific tosession/prompt.erroris a field of theerrorstop reason, sitting besidestopReasonon the idle update. It is optional and nullable; omission andnullare equivalent: the Agent supplied no error details. When present, it is a JSON-RPC error object, with requiredcodeandmessageand optionaldata. The Agent SHOULD include it, and Clients may showmessageto the user.errorexists only on theerrorstop reason, the same waystopReasonexists only on theidlestate. Beside any other stop reason it is an unknown field.
stopReason and flattened into the idle state_update, so a stop reason can carry fields of its own. The existing stop reasons have none, and their wire format is unchanged.
Once the user message is inserted, the Agent MUST NOT report a later failure as an error response to session/prompt, even if it has not sent the response yet. The request succeeds with the inserted messageId, and the failure ends foreground work as above. A JSON-RPC error on session/prompt therefore always means the message was not inserted. This includes request cancellation: a $/cancel_request or internal cancellation answers session/prompt with -32800 only before insertion. Afterwards the request still succeeds with the messageId, and the work is stopped with session/cancel.
error uses the same codes as JSON-RPC error responses, so Clients can handle them the same way. For example, -32000 (authentication required) can prompt the user to authenticate before sending another prompt. More specific categories, such as rate limits, an exhausted context window, or an overloaded provider, can be added later as codes or as structured data without changing this shape.
As with every stop reason, the idle update follows all updates for the work that ended. Before sending it, the Agent SHOULD give each tool call that the failed work left unfinished a terminal status, using failed for calls that did not complete. A failure does not undo the insertion or any side effects the work already had, such as tools that ran. Sending the prompt again submits a new message; see Does returning messageId make retrying safe?.
The error stop reason is only for failures that end foreground work:
A v2 Client that predates the
error stop reason handles it like any unknown stop reason: foreground work has ended, and it shows a generic stopped state. It does not need error to know that the work ended. Because the error is part of the idle snapshot, anything that carries that snapshot carries the failure too, including an exposed subagent’s subagent_update.state and any later mechanism for synchronizing current state on resume.
v1’s session/prompt is unchanged; it already reports these failures as an error response. The one v1 surface that carries this idle snapshot, the draft subagent subagent_update.state, gains the same error stop reason, because a v1 child has no prompt response that could report its failure. A bridge from a v1 Agent to a v2 Client maps an error response that arrives after the bridge reported the message inserted to an idle state_update with the error stop reason and the same error object. A bridge from a v2 Agent to a v1 Client maps that update to an error response for the pending v1 prompt, using -32603 (internal error) when error is absent.
Shiny future
How will things play out once this feature exists?This isn’t a huge schema change, but it is a fundamental behavior change in the protocol that I believe:
- Provides agents with much more flexibility in how they want to update a client about a given session
- Solves some concrete pain points in the current model (i.e. how to integrate prompts into session replay and multi-client replays, message IDs, etc)
- Lets clients that received a prompt response reconcile optimistic submissions with live or replayed user messages without content matching or assuming a particular arrival order
Implementation details and plan
Tell me more about your implementation. What is your detailed implementation plan?Overall, this isn’t a huge lift on schema definition, but it is a large, breaking change in behavior which means we can only stabilize in protocol version 2. The v2 schema and guides define the insertion-receipt response. The implementation checklist is:
- Add a required non-null
messageIdtoPromptResponse, reusing the existingMessageIdtype. LeavePromptRequest,UserMessage,ContentChunk, and v1 unchanged; do not introducePromptId. - Update the prompt lifecycle and session replay guides with insertion as the acknowledgment point, distinct from processing completion; response/update ordering; optional retention; stable IDs when retained messages are replayed; and the content reset when replaying chunks from the beginning.
- Add serialization and schema coverage for valid response IDs and rejection of missing,
null, or non-string IDs. - Exercise an ID assigned before insertion without an early successful response, notifications before and after the response, identical prompts with distinct IDs, submissions from multiple clients, retained associations across replay, imported messages without prompt responses, and both full-content and streamed updates. Include a locally handled command reported live but omitted from later replay, and a retained command replayed with the same ID without re-executing it. Cover the lost-response limitation rather than promising recovery, and verify that streamed replay does not duplicate retained content.
- Make
StopReasona union tagged bystopReasonand flattened into the idlestate_update, keeping the existing values’ wire format and the tolerance for omitted,null, malformed, and unknown stop reasons. Add anerrorvariant with an optional, nullableerrorfield, reusing the existingErrortype. Document the failure rules in the prompt lifecycle guide’s stop reasons and cancellation sections. - Add serialization and schema coverage for the idle update with an
error, without one, and witherror: null, and verify that anerrorbeside another stop reason is treated as an unknown field. Exercise a failure after insertion but before the response is sent: the response still succeeds and the idle update reports the failure. - Update examples and generated artifacts, then run
npm run generateandnpm run check.
- Make this an opt-in “future-flag” capability on v1 so people can experiment, but it would be an unstable feature regardless.
- We establish a preview/beta flow for v2
_meta flag.
Frequently asked questions
What questions have arisen over the course of authoring this document or during subsequent discussions?I’ve hopefully addressed all of the questions and concerns for what motivated this above, but happy to engage with others on this.
Does messageId identify the client or user?
No. It identifies a logical message. The submitting client recognizes its own message through the request/response association, not by parsing the message ID. Sender attribution and authorization remain separate concerns.
Does the returned ID identify a turn or the agent’s response?
No. It identifies the accepted user message. A prompt may contribute to work already in progress, and agents may produce output without a prompt. Agent messages and tool calls retain their own IDs;state_update remains session-scoped.
Does returning messageId make retrying safe?
No. The ID is an acknowledgment, not a client-supplied idempotency key. A retry can be accepted as a distinct submission with a different ID, including after a lost response. This proposal does not promise automatic deduplication or exactly-once processing.
Do locally handled commands need persistent history?
No. Some local control commands complete without the runtime recording the submitted input. The adapter inserts a live-only user message for the command into the ACP conversation, assigns itsmessageId, and reports that insertion through the response and the user-message update. This is the command’s insertion point; it does not require a runtime insertion event that will never arrive or a permanent transcript entry.
The command may be absent from later history replay. If the agent does retain and replay it, it uses the same ID and reports the message without executing the command again. Returning a messageId promises live correlation, not archival storage.
What alternative approaches did you consider, and why did you settle on this one?
Client-generated correlation token
A client-generatedpromptId on the request, preserved with the accepted input and its replayed user message, could identify a submission even if the response is lost. It could also identify pending input before the agent assigns a history-message ID. This is separate from letting the client choose the canonical messageId.
That alternative adds a second identifier, a uniqueness requirement across clients, and a mapping between submissions and history messages. It would still need to define that mapping if several submissions could become one ACP history message, and would not by itself define queue lifecycle or safe retries.
The returned-ID approach is the simpler candidate when one submission remains one logical ACP message and its ID is returned on insertion. A client-generated correlation token remains an option if reliable lost-response recovery or a separate submission lifecycle becomes a requirement; it could be combined with a returned messageId.
Require acknowledgment before insertion
An earlier response would acknowledge receipt or reservation rather than insertion, and could require agents to reserve an ID before their runtime creates the message. This RFD instead keeps the prompt request pending until insertion or rejection. If a future queueing design needs an earlier receipt, that is a separate acknowledgment from the insertion response defined here.Reuse the JSON-RPC request ID
JSON-RPC IDs already associate the response with the submitting request. They are not suitable as persisted message identities: different clients or connections can reuse the same ID, and proxies may rewrite it. Returning the agent-ownedmessageId keeps these responsibilities separate.
Let the client choose the canonical message ID
The Message ID RFD rejected client-generated IDs as canonical message identities because they introduce multiple sources of IDs and require shared collision-avoidance rules. Returning an agent-generated ID preserves agent ownership while still giving the submitting client a correlation handle.Prompt as a notification
Early discussions revolved around having this be a bidirectional stream of notifications on the session. While this felt very symmetrical and appealing, it ran into several problems in practice:- Clients only really had one type of notification that made sense to emit on the session: user messages
- The Agent would still need to replay that message to show where it got accepted within the message history
- We would then need a notification-based way of emitting errors for invalid prompts that would need to be tied to fire-and-forget notifications.
Report failures after insertion with a failed state
state says whether the agent can accept a prompt now, and after a failure it can: it is idle. How the work ended belongs with the stop reason. A new state would also leave existing v2 clients unable to tell that foreground work ended: a client that doesn’t know failed can only present it generically, which says nothing about whether the agent is still working. An unknown stop reason always arrives on an idle update, so the client still knows the work ended.
Report failures after insertion with a separate error update
Anerror update followed by idle gives clients two signals that must agree. The protocol would need rules for an error with no idle after it and for which idle an error belongs to, and the error would not be part of the state snapshot that subagent_update.state mirrors. Putting the error on the idle update avoids those rules.
Report failures after insertion with an error-severity notice
Notices are advisory. Clients may ignore them, and they must not report fatal errors or change session state. That contract fits failures the agent recovers from, not one that ends the work.Put error beside the stop reason with a cross-field rule
Keeping stopReason a plain string and adding error as a sibling field produces the same wire format, but the schema cannot say that error belongs to one stop reason. The protocol would need a prose rule that Agents send error only with stopReason: "error" and that Clients ignore it otherwise, and every implementation would have to enforce it by hand. Making error a field of the error stop reason states the rule in the schema, the same way stopReason belongs to the idle state.
Define a new error type for failures after insertion
Reusing the JSON-RPC error object keeps failures before and after insertion in one shape, lets clients reuse the code handling they already have, and lets v1 and v2 bridges convert in both directions without losing information. Finer categories can be added later as codes or indata.
Codex’s app-server makes the same split. A failed turn ends with turn/completed whose status is failed and whose details are in turn.error, while a separate error notification with willRetry reports transient errors that don’t end the turn, which notice covers here.
Revision history
2026-04-13: Initial draft 2026-04-22: Move from bidirectional notification approach to a change in the prompt request lifecycle 2026-10-01: Report failures after insertion with anerror stop reason that carries an optional JSON-RPC error