Skip to content
134 changes: 134 additions & 0 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ Specifies optional A2A protocol features supported by the agent.
--8<-- "types/src/types.ts:AgentCapabilities"
```

- **`idempotencySupported`**: When set to `true`, indicates that the agent supports messageId-based idempotency for new task creation. This enables the agent to detect and handle duplicate `messageId` values to prevent unintended duplicate task creation due to network failures or client retries. See [Section 7.1.2](#712-idempotency) for detailed behavior.

#### 5.5.2.1. `AgentExtension` Object

Specifies an extension to the A2A protocol supported by the agent.
Expand Down Expand Up @@ -721,6 +723,33 @@ The `error` response for all transports in case of failure is a [`JSONRPCError`]
--8<-- "types/src/types.ts:MessageSendConfiguration"
```

#### 7.1.2. Idempotency

A2A supports optional messageId-based idempotency to enable idempotent task creation, addressing scenarios where network failures or client crashes could result in duplicate tasks with unintended side effects.

**Scope**: Idempotency **ONLY** applies to new task creation (when `message.taskId` is not provided). Messages sent to continue existing tasks follow normal message uniqueness rules without task-level deduplication.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this scope limitation, given that:

scenarios where network failures or client crashes could result in duplicate tasks with unintended side effects.

are equally likely to happen with message follow-ups and might lead to unintended consequences there as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message.taskId guarantees follow-ups idempotency

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's consider an example with (messageId, taskId) tuples:

(msg1, nil) -> task1 created, hits input_required
(msg2, task1) -> continues execution of the task
(msg2, task1) -> duplicate request is idempotent, taps into the running execution / returns the current Task state
(msg3, task1) -> a new message id, the request gets executed

Re-reading this comment, more specifically:

AND the matched message is the most recent message for the Task/Message

I'd say the idempotency mechanism scope is broader than:

the new task creation

Or maybe I'm just misunderstanding this statement:

Messages sent to continue existing tasks follow normal message uniqueness rules without task-level deduplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit lost. message/send and message/stream do not accept taskId because is the server that need to create them. All the other JSONRPC methods on task use the taskId. The idempotency is guaranteed for those methods by the taskId, and the messageId is not considered anymore: It may be used in logging/tracing of course, but it doesn't have any required behaviour in the current spec and this PR is just using it for task creation idempotency, leaving all the rest unchanged

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message/send and message/stream do not accept taskId because is the server that need to create them

I'm talking about multi-turn interactions. You're right that Task IDs are generated by the server, but a Task might require a follow-up message (input-required state). And what I'm saying is Messages sent as follow-ups for a Task need to adhere to the same idempotency rules.

Idempotency ONLY applies to new task creation (when message.taskId is not provided)

So I'm suggesting to change the wording here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maeste To pile on to what @yarolegovich is saying with a concrete example. Imagine an agent playing the role of a shopping cart.

(msg1, nil) -> I want to go shopping - task1 created, hits input_required
(msg2, task1) -> Add a SuperX4 Laptop to my cart
(msg2, task1) -> Add a SuperX4 Laptop to my cart
(msg3, task1) -> Add a XtraScreen to my card
(msg4, task1) -> I'm done let's pay

Without idempotency during a task update, I get two laptops, not one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the commit 9384703 address this


**Agent Requirements:**

- Agents **MAY** support idempotency by declaring `idempotencySupported: true` in their `AgentCapabilities`.
- When `idempotencySupported` is `true`, agents **MUST** track `messageId` values for new task creation within the authenticated user/session scope.
- When `idempotencySupported` is `false` or absent, agents **MAY** not support idempotency and do not track `messageId` values.

**Server Behavior:**

- **MessageId scope**: MessageId tracking is scoped to the authenticated user/session to prevent cross-user conflicts.
- **Active task collision**: If a `messageId` (for new task creation) matches an existing task in a non-terminal state (`submitted`, `working`, `input-required`), the server **MUST** return a [`MessageIdAlreadyExistsError`](#82-a2a-specific-errors) (-32008).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call out what happens for the other transports (e.g. how is the error information propagated so that the client get the necessary task id)

Copy link
Contributor Author

@maeste maeste Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the comment. I'd like to include it in a better error handling and equivalence as proposed in #976. If that one would get merged I can rebase this one on it, including other transports error mappings for this new error in the same way I'm doing I'm that PR. Does it make sense to you?

Copy link
Contributor

@mikeas1 mikeas1 Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so sure about erroring in this case. What if the incoming message was exactly what was previously sent? It seems like a better client experience would be to just return the ongoing task, rather than having them inspect the error and fetch it themselves. That also matches with the idempotency goal: you can send the same request twice and get exactly the same response.

Getting slightly more specific, I feel the best behavior for message/send would be:

  • IF messageId matches a value previously seen, THEN:
    • IF the request message exactly matches the previous message
    • AND the matched message is the most recent message for the Task/Message
    • THEN:
      1. If the request is non-blocking, OR the task is in a terminal state, OR the response was a Message: immediately return the current state of the Task/Message.
      2. If the request is blocking and the task is in a non-terminal state: block until the Task transitions to an interrupted state, and return the Task.
    • ELSE: fail with 409 Conflict/BadRequest/MesageIdAlreadyExistsError

It looks complicated, but I think it's actually what you would naturally expect.

This allows clients to write very simple retry loops -- they can just write an outer loop that keeps sending the same request until it succeeds and not worry about handling collision errors. It also calls out more bizarre situations, like a client sending old messages that have since had follow-ups (indicating the client has seen the response, given it used details from the response in its request).

I am assuming that a client would never "accidentally" reuse a messageId (meaning: "I really wanted this to be a new task, even though the entirety of my request content is identical and this agent supports idempotency! Why did I get an old task back??"), but I actually think that's a fair assumption. We already specify that messageIds must be unique, so if you are breaking that requirement you may see strange results.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, but it is a burden on the server side because the implementation would need to store both the IDs and the full messages somewhere for some time. It's not ideal for an agent with high traffic. Moreover, we are saying we store on the server side the contents of a message sent from the client, which may have some privacy implications (it's probably out of spec's scope, but just mentioning).
Finally, consider that the retry from the client should happen only in case of network outage or severe errors on the client, so it is already handling an exception for a sporadic case. We can pretend that the standard call is not in a loop, and clients handle the retry properly (ideally by checking for the task before with getTask) in a fallback method or something like that.

Am I losing something?

In the short term, I'm worried about asking the server to store the full message content instead of just an ID.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moreover, we are saying we store on the server side the contents of a message sent from the client, which may have some privacy implications (it's probably out of spec's scope, but just mentioning).

Only message hashes can be stored.

Finally, consider that the retry from the client should happen only in case of network outage or severe errors on the client, so it is already handling an exception for a sporadic case. We can pretend that the standard call is not in a loop, and clients handle the retry properly (ideally by checking for the task before with getTask) in a fallback method or something like that.

If an a2a server is exposed to a mobile client a retry-in-the-loop becomes a pretty common thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oki I'll add the suggested behaviour, leaving to the implementation what to save (most likely the hash)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ELSE: fail with 409 Conflict

I think that'd be nice to specify something like that as the expected behavior for handling concurrent message requests with different messages. If a client sends a message and then sends a different message referencing the same task, the server should respond with an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit 9384703 addresses the original comment from @mikeas1

Regarding the latest comment from @yarolegovich, I think it is a more general problem of accepting or not following up on messages depending on the task state. I've opened #1027 and addressed it in commit ec3cdc5 in this PR

- **Terminal task reuse**: If a `messageId` matches a task in a terminal state (`completed`, `failed`, `canceled`, `rejected`), the server **MAY** allow creating a new task with the same messageId.
- **Time-based expiry**: Servers **MAY** implement time-based expiry for messageId tracking associated with terminal tasks.

**Client Responsibilities:**

- **Unique messageIds**: Clients **MUST** generate unique `messageId` values for each intended new task within their authenticated session (this is already required by the base A2A specification).
- **Error handling**: When receiving `MessageIdAlreadyExistsError`, clients **SHOULD**:
1. Use the `existingTaskId` from the error data to call `tasks/get` to retrieve the existing task. This is the correct action when retrying a request that may have already succeeded (e.g., after a network error).
2. Alternatively, if the `messageId` was reused unintentionally and a new, distinct task is desired, generate a new `messageId` and retry the request.
- **Retry safety**: Clients can safely retry `message/send` requests with the same `messageId` after network failures when creating new tasks.

### 7.2. `message/stream`

Sends a message to an agent to initiate/continue a task AND subscribes the client to real-time updates for that task via Server-Sent Events (SSE). This method requires the server to have `AgentCard.capabilities.streaming: true`. Just like `message/send`, a task which has reached a terminal state (completed, canceled, rejected, or failed) can't be restarted. Sending a message to such a task will result in an error. For more information, refer to the [Life of a Task guide](./topics/life-of-a-task.md).
Expand Down Expand Up @@ -1173,6 +1202,7 @@ These are custom error codes defined within the JSON-RPC server error range (`-3
| `-32005` | `ContentTypeNotSupportedError` | Incompatible content types | A [Media Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) provided in the request's `message.parts` (or implied for an artifact) is not supported by the agent or the specific skill being invoked. |
| `-32006` | `InvalidAgentResponseError` | Invalid agent response type | Agent generated an invalid response for the requested method |
| `-32007` | `AuthenticatedExtendedCardNotConfiguredError` | Authenticated Extended Card not configured | The agent does not have an Authenticated Extended Card configured.|
| `-32008` | `MessageIdAlreadyExistsError` | Message ID already exists for active task | The provided `messageId` is already associated with an active task in a non-terminal state. The client should either retrieve the existing task using the provided `existingTaskId` or generate a new message ID. |

Servers MAY define additional error codes within the `-32000` to `-32099` range for more specific scenarios not covered above, but they **SHOULD** document these clearly. The `data` field of the `JSONRPCError` object can be used to provide more structured details for any error.

Expand Down Expand Up @@ -1869,6 +1899,110 @@ _If the task were longer-running, the server might initially respond with `statu
}
```

### 9.8. Idempotent Task Creation with MessageId

**Scenario:** Client needs to ensure idempotent task creation to avoid duplicate operations in case of network failures.

1. **Client discovers agent supports idempotency from Agent Card:**

```json
{
"capabilities": {
"idempotencySupported": true
}
}
```

2. **Client sends initial message (new task creation):**

```json
{
"jsonrpc": "2.0",
"id": "req-008",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{
"kind": "text",
"text": "Process this critical financial transaction - charge card ending 1234 for $500"
}
],
"messageId": "client-20240315-001"
}
}
}
```

3. **Server successfully creates task and responds:**

```json
{
"jsonrpc": "2.0",
"id": "req-008",
"result": {
"id": "server-task-uuid-12345",
"contextId": "ctx-67890",
"status": { "state": "working" }
}
}
```

4. **Network failure occurs - client retries with same messageId (new task creation):**

```json
{
"jsonrpc": "2.0",
"id": "req-009",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{
"kind": "text",
"text": "Process this critical financial transaction - charge card ending 1234 for $500"
}
],
"messageId": "client-20240315-001"
}
}
}
```

5. **Server detects duplicate messageId and returns error:**

```json
{
"jsonrpc": "2.0",
"id": "req-009",
"error": {
"code": -32008,
"message": "Message ID already exists for active task",
"data": {
"messageId": "client-20240315-001",
"existingTaskId": "server-task-uuid-12345"
}
}
}
```

6. **Client retrieves existing task using the provided task ID:**

```json
{
"jsonrpc": "2.0",
"id": "req-010",
"method": "tasks/get",
"params": {
"id": "server-task-uuid-12345"
}
}
```

This pattern ensures that duplicate operations are prevented while allowing clients to safely recover from network failures.

These examples illustrate the flexibility of A2A in handling various interaction patterns and data types. Implementers should refer to the detailed object definitions for all fields and constraints.

## 10. Appendices
Expand Down
2 changes: 2 additions & 0 deletions specification/grpc/a2a.proto
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,8 @@ message AgentCapabilities {
bool push_notifications = 2;
// Extensions supported by this agent.
repeated AgentExtension extensions = 3;
// If the agent supports messageId-based idempotency for task creation
bool idempotency_supported = 4;
Copy link
Member

@yarolegovich yarolegovich Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of sync with the idempotencyEnforced renaming

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, fixing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the commit 697e334 address this

}

// A declaration of an extension supported by an Agent.
Expand Down
49 changes: 49 additions & 0 deletions specification/json/a2a.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 28 additions & 1 deletion types/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface AgentCapabilities {
pushNotifications?: boolean;
/** Indicates if the agent provides a history of state transitions for a task. */
stateTransitionHistory?: boolean;
/** Indicates if the agent supports messageId-based idempotency for task creation. */
idempotencySupported?: boolean;
/** A list of protocol extensions supported by the agent. */
extensions?: AgentExtension[];
}
Expand Down Expand Up @@ -1496,6 +1498,30 @@ export interface AuthenticatedExtendedCardNotConfiguredError
}
// --8<-- [end:AuthenticatedExtendedCardNotConfiguredError]

// --8<-- [start:MessageIdAlreadyExistsError]
/**
* An A2A-specific error indicating that the provided message ID already exists for an active task.
*/
export interface MessageIdAlreadyExistsError extends JSONRPCError {
/** The error code for when a message ID already exists for an active task. */
readonly code: -32008;
/**
* The error message.
* @default "Message ID already exists for active task"
*/
message: string;
/**
* Additional data that includes the duplicate message ID and existing task ID for client recovery.
*/
data: {
/** The message ID that already exists. */
messageId: string;
/** The ID of the existing task associated with this message ID. */
existingTaskId: string;
};
}
// --8<-- [end:MessageIdAlreadyExistsError]

// --8<-- [start:A2AError]
/**
* A discriminated union of all standard JSON-RPC and A2A-specific error types.
Expand All @@ -1512,5 +1538,6 @@ export type A2AError =
| UnsupportedOperationError
| ContentTypeNotSupportedError
| InvalidAgentResponseError
| AuthenticatedExtendedCardNotConfiguredError;
| AuthenticatedExtendedCardNotConfiguredError
| MessageIdAlreadyExistsError;
// --8<-- [end:A2AError]