> ## Documentation Index
> Fetch the complete documentation index at: https://zed-685ed6d6-auto-sync-registry.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# v2 Prompt Lifecycle

Author(s): [@benbrandt](https://github.com/benbrandt)

## 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.

## 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.

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": "req_12345",
  "result": {
    "messageId": "mess_456def"
  }
}
```

This extends the [Message ID RFD](/rfds/message-id) without changing ID ownership: the agent still generates opaque message IDs, but now returns the accepted user message's ID to the submitting client as well as including it in session updates. No client-generated `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 returned `messageId` then links that submission to session updates and replay:

| Field | Identifies | Generated by |
| - | - | - |
| JSON-RPC `id` | One request/response exchange | The request sender |
| `messageId` | The accepted logical user message | The agent |

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/update`s can more freely flow from the agent, and we lost the ability to pass end\_turn 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 through `user_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](/rfds/message-id) 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:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": "req_12345",
  "method": "session/prompt",
  "params": {
    "sessionId": "sess_789xyz",
    "prompt": [
      {
        "type": "text",
        "text": "What's the capital of France?"
      }
    ]
  }
}
```

And the Agent responds with:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": "req_12345",
  "result": {
    "messageId": "mess_456def"
  }
}
```

And reports the user message with the notification:

```json theme={null}
{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "sess_789xyz",
    "update": {
      "sessionUpdate": "user_message",
      "messageId": "mess_456def",
      "content": [
        {
          "type": "text",
          "text": "What's the capital of France?"
        }
      ]
    }
  }
}
```

The submitting client uses the response's `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](./message-updates.mdx) 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.

Clients **MUST NOT** assume that every acknowledged message will appear after resuming a session. Adapters do not need a separate permanent transcript for messages their underlying runtime does not retain, or to reconstruct cleared or discarded messages solely for correlation. This does not change v1's replay rules.

A client that received the response and retained its `(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](./session-resume-replay.mdx). 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 using `user_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:

```json theme={null}
{
  "sessionUpdate": "user_message_chunk",
  "messageId": "mess_456def",
  "content": {
    "type": "text",
    "text": "What's the capital of France?"
  }
}
```

When replay reconstructs a user message from its beginning using chunks, the agent **MUST** first send a `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:

```json theme={null}
{
  "sessionUpdate": "user_message",
  "messageId": "mess_456def",
  "content": []
}
```

The agent then sends the message's chunks. Alternatively, a full-content `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:

```json theme={null}
{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "sess_789xyz",
    "update": {
      "sessionUpdate": "state_update",
      "state": "running"
    }
  }
}
```

**Idle**, whenever the agent is done, with optional data on why:

```json theme={null}
{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "sess_789xyz",
    "update": {
      "sessionUpdate": "state_update",
      "state": "idle",
      "stopReason": "end_turn"
    }
  }
}
```

**Requires Action**, for when the agent is trying to run, but needs to wait on user input to continue, it isn't just idle:

```json theme={null}
{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "sess_789xyz",
    "update": {
      "sessionUpdate": "state_update",
      "state": "requires_action"
    }
  }
}
```

We could explore adding which permission or elicitation it is waiting on if we wanted to make it clearer if we wanted.

## 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

Ultimately, after months of using the current model, I am quite excited for what these small tweaks can unlock, and also how we can build on top of this in the future!

## 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:

1. Add a required non-null `messageId` to `PromptResponse`, reusing the existing `MessageId` type. Leave `PromptRequest`, `UserMessage`, `ContentChunk`, and v1 unchanged; do not introduce `PromptId`.
2. 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.
3. Add serialization and schema coverage for valid response IDs and rejection of missing, `null`, or non-string IDs.
4. 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.
5. Update examples and generated artifacts, then run `npm run generate` and `npm run check`.

The schema and serialization tests establish the wire shape; adapters still need to implement and validate insertion timing, live reporting for local commands, and replay identity for retained messages. Broader v2 lifecycle follow-ups include post-insertion failure reporting, cancellation races, and current-state synchronization on resume. Those are separate from the insertion receipt's identity and are not resolved by adding another prompt ID. Queueing, steering, sender identity, safe retries, and idempotency guarantees remain outside this change.

Depending on how the rest of v2 testing goes, we can either:

1. Make this an opt-in "future-flag" capability on v1 so people can experiment, but it would be an unstable feature regardless.
2. We establish a preview/beta flow for v2

We definitely need 2 regardless, and can likely handle this in a similar "unstable" manner as we do for current unstabilized features. It's likely timing will work out that we can just do it that way. If for some reason the timing doesn't work out, we can start experimenting with an unstable capability or some `_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 its `messageId`, 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-generated `promptId` 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-owned `messageId` 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:

1. Clients only really had one type of notification that made sense to emit on the session: user messages
2. The Agent would still need to replay that message to show where it got accepted within the message history
3. 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.

Given that the agent is ultimately the owner of the session history, it makes sense that it is the sole notifier of the session, because it is the source of truth. By having all client interactions on the session remain requests, albeit much shorter-lived ones in theory, we still have a semantically meaningful way to communicate errors and when and how a given request was incorporated into the session.

## Revision history

2026-04-13: Initial draft
2026-04-22: Move from bidirectional notification approach to a change in the prompt request lifecycle


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.