diff --git a/docs/specification.md b/docs/specification.md
index 03787f825..9a63f5a30 100644
--- a/docs/specification.md
+++ b/docs/specification.md
@@ -322,6 +322,8 @@ Specifies optional A2A protocol features supported by the agent.
--8<-- "types/src/types.ts:AgentCapabilities"
```
+- **`idempotencyEnforced`**: When set to `true`, indicates that the agent enforces messageId-based idempotency for task creation and follow-up. This enables the agent to detect and handle duplicate `messageId` values to prevent unintended duplicate task creation or wrong behavior in multi turn messages 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.
@@ -659,7 +661,11 @@ The A2A Server's HTTP response body **MUST** be a `JSONRPCResponse` object (or,
### 7.1. `message/send`
-Sends a message to an agent to initiate a new interaction or to continue an existing one. This method is suitable for synchronous request/response interactions or when client-side polling (using `tasks/get`) is acceptable for monitoring longer-running tasks. 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).
+Sends a message to an agent to initiate a new interaction or to continue an existing one. This method is suitable for synchronous request/response interactions or when client-side polling (using `tasks/get`) is acceptable for monitoring longer-running tasks.
+
+**Multi-turn message acceptance**: When a message contains a `taskId` (follow-up message), the server **MUST** only accept the message if the referenced task is in the `input-required` state. Messages sent to tasks in other non-terminal states (`submitted`, `working`, `auth-required`) **MUST** result in an [`UnsupportedOperationError`](#82-a2a-specific-errors) (-32004) with message "Task not accepting input".
+
+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).
@@ -721,6 +727,38 @@ 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 applies to new task creation (when `message.taskId` is not provided) and on messages sent to continue existing tasks in a multi turn workflow.
+
+**Agent Requirements:**
+
+- Agents **MAY** enforce idempotency by declaring `idempotencyEnforced: true` in their `AgentCapabilities`.
+- When `idempotencyEnforced` is `true`, agents **MUST** track `messageId` values for new task creation and multi-turn message continuation within the authenticated user/session scope.
+- When `idempotencyEnforced` is `false` or absent, agents **MAY** not enforce 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.
+- **MessageId collision handling**: If a `messageId` matches a value previously seen, the server **MUST**:
+ - **Content hash verification**: If the SHA256 hash of the request message content matches the hash of the previous message AND the matched message is the most recent message for the Task/Message, then:
+ - 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.
+ - If the request is blocking and the task is in a non-terminal state: block until the Task transitions to a terminal state, and return the Task.
+ - **Content mismatch**: Otherwise, return a [`MessageIdAlreadyExistsError`](#82-a2a-specific-errors) (-32008) indicating the `messageId` is already associated with different content.
+- **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).
+- **Simple retry loops**: Clients can implement simple retry loops that keep sending the same request until it succeeds, without worrying about handling collision errors for genuine retries.
+- **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 indicates the client has sent a different message with the same `messageId`.
+ 2. If a new, distinct task is desired, generate a new `messageId` and retry the request.
+- **Retry safety**: Clients can safely retry `message/send` requests with identical content and the same `messageId` after network failures, as the server will return the appropriate response without creating duplicate 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).
@@ -1173,6 +1211,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.
@@ -1869,6 +1908,370 @@ _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": {
+ "idempotencyEnforced": 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.
+
+### 9.9. Idempotent Multi-Turn Interaction
+
+**Scenario:** Client wants to shop for items, and network failures cause message retransmission during the multi-turn workflow. The agent enforces idempotency to prevent duplicate orders.
+
+1. **Client discovers agent supports idempotency from Agent Card:**
+
+ ```json
+ {
+ "capabilities": {
+ "idempotencyEnforced": true
+ }
+ }
+ ```
+
+2. **Client sends initial shopping request (new task creation):**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": "req-001",
+ "method": "message/send",
+ "params": {
+ "message": {
+ "role": "user",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "I want to go shopping"
+ }
+ ],
+ "messageId": "msg1-shopping-start"
+ }
+ }
+ }
+ ```
+
+3. **Server creates task and requests input:**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": "req-001",
+ "result": {
+ "id": "task1-shopping-session",
+ "contextId": "ctx-shopping-001",
+ "status": {
+ "state": "input-required",
+ "message": {
+ "role": "agent",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "Welcome to our store! What would you like to add to your cart?"
+ }
+ ],
+ "messageId": "agent-msg1",
+ "taskId": "task1-shopping-session",
+ "contextId": "ctx-shopping-001"
+ }
+ },
+ "history": [
+ {
+ "role": "user",
+ "parts": [{"kind": "text", "text": "I want to go shopping"}],
+ "messageId": "msg1-shopping-start",
+ "taskId": "task1-shopping-session",
+ "contextId": "ctx-shopping-001"
+ }
+ ],
+ "kind": "task"
+ }
+ }
+ ```
+
+4. **Client adds first item to cart:**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": "req-002",
+ "method": "message/send",
+ "params": {
+ "message": {
+ "role": "user",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "Add a SuperX4 Laptop to my cart"
+ }
+ ],
+ "taskId": "task1-shopping-session",
+ "contextId": "ctx-shopping-001",
+ "messageId": "msg2-add-laptop"
+ }
+ }
+ }
+ ```
+
+5. **Server processes request and responds:**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": "req-002",
+ "result": {
+ "id": "task1-shopping-session",
+ "contextId": "ctx-shopping-001",
+ "status": {
+ "state": "input-required",
+ "message": {
+ "role": "agent",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "Added SuperX4 Laptop ($1,299) to your cart. Anything else?"
+ }
+ ]
+ }
+ },
+ "kind": "task"
+ }
+ }
+ ```
+
+6. **Network failure occurs - client retries the same laptop addition:**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": "req-003",
+ "method": "message/send",
+ "params": {
+ "message": {
+ "role": "user",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "Add a SuperX4 Laptop to my cart"
+ }
+ ],
+ "taskId": "task1-shopping-session",
+ "contextId": "ctx-shopping-001",
+ "messageId": "msg2-add-laptop"
+ }
+ }
+ }
+ ```
+
+7. **Server detects identical messageId and content hash, returns current state without duplicate action:**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": "req-003",
+ "result": {
+ "id": "task1-shopping-session",
+ "contextId": "ctx-shopping-001",
+ "status": {
+ "state": "input-required",
+ "message": {
+ "role": "agent",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "Added SuperX4 Laptop ($1,299) to your cart. Anything else?"
+ }
+ ]
+ }
+ },
+ "kind": "task"
+ }
+ }
+ ```
+
+8. **Client adds second item (different messageId):**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": "req-004",
+ "method": "message/send",
+ "params": {
+ "message": {
+ "role": "user",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "Add a XtraScreen to my cart"
+ }
+ ],
+ "taskId": "task1-shopping-session",
+ "contextId": "ctx-shopping-001",
+ "messageId": "msg3-add-screen"
+ }
+ }
+ }
+ ```
+
+9. **Client finalizes order:**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": "req-005",
+ "method": "message/send",
+ "params": {
+ "message": {
+ "role": "user",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "I'm done let's pay"
+ }
+ ],
+ "taskId": "task1-shopping-session",
+ "contextId": "ctx-shopping-001",
+ "messageId": "msg4-checkout"
+ }
+ }
+ }
+ ```
+
+10. **Server completes transaction with correct single laptop:**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": "req-005",
+ "result": {
+ "id": "task1-shopping-session",
+ "contextId": "ctx-shopping-001",
+ "status": {
+ "state": "completed"
+ },
+ "artifacts": [
+ {
+ "artifactId": "order-confirmation",
+ "name": "Order Receipt",
+ "parts": [
+ {
+ "kind": "data",
+ "data": {
+ "orderId": "ORD-12345",
+ "items": [
+ {"name": "SuperX4 Laptop", "price": 1299, "quantity": 1},
+ {"name": "XtraScreen", "price": 299, "quantity": 1}
+ ],
+ "total": 1598
+ }
+ }
+ ]
+ }
+ ],
+ "kind": "task"
+ }
+ }
+ ```
+
+This example demonstrates how idempotency in multi-turn workflows prevents duplicate operations (only one laptop was ordered despite the retry) while allowing the conversation to continue naturally.
+
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
diff --git a/specification/grpc/a2a.proto b/specification/grpc/a2a.proto
index 4a5e126d9..a68c05e2f 100644
--- a/specification/grpc/a2a.proto
+++ b/specification/grpc/a2a.proto
@@ -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 message send
+ bool idempotency_enforced = 4;
}
// A declaration of an extension supported by an Agent.
diff --git a/specification/json/a2a.json b/specification/json/a2a.json
index 7e0da25e6..2f3f5fc0d 100644
--- a/specification/json/a2a.json
+++ b/specification/json/a2a.json
@@ -38,6 +38,9 @@
},
{
"$ref": "#/definitions/AuthenticatedExtendedCardNotConfiguredError"
+ },
+ {
+ "$ref": "#/definitions/MessageIdAlreadyExistsError"
}
],
"description": "A discriminated union of all standard JSON-RPC and A2A-specific error types."
@@ -120,6 +123,10 @@
},
"type": "array"
},
+ "idempotencyEnforced": {
+ "description": "Indicates if the agent enforces messageId-based idempotency for task creation.",
+ "type": "boolean"
+ },
"pushNotifications": {
"description": "Indicates if the agent supports sending push notifications for asynchronous task updates.",
"type": "boolean"
@@ -1369,6 +1376,9 @@
},
{
"$ref": "#/definitions/AuthenticatedExtendedCardNotConfiguredError"
+ },
+ {
+ "$ref": "#/definitions/MessageIdAlreadyExistsError"
}
],
"description": "An object describing the error that occurred."
@@ -1666,6 +1676,45 @@
],
"type": "object"
},
+ "MessageIdAlreadyExistsError": {
+ "description": "An A2A-specific error indicating that the provided message ID already exists for an active task.",
+ "properties": {
+ "code": {
+ "const": -32008,
+ "description": "The error code for when a message ID already exists for an active task.",
+ "type": "integer"
+ },
+ "data": {
+ "description": "Additional data that includes the duplicate message ID and existing task ID for client recovery.",
+ "properties": {
+ "existingTaskId": {
+ "description": "The ID of the existing task associated with this message ID.",
+ "type": "string"
+ },
+ "messageId": {
+ "description": "The message ID that already exists.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "existingTaskId",
+ "messageId"
+ ],
+ "type": "object"
+ },
+ "message": {
+ "default": "Message ID already exists for active task",
+ "description": "The error message.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "data",
+ "message"
+ ],
+ "type": "object"
+ },
"MessageSendConfiguration": {
"description": "Defines configuration options for a `message/send` or `message/stream` request.",
"properties": {
diff --git a/types/src/types.ts b/types/src/types.ts
index 4f284ddd4..c285dee4b 100644
--- a/types/src/types.ts
+++ b/types/src/types.ts
@@ -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 enforces messageId-based idempotency for task creation. */
+ idempotencyEnforced?: boolean;
/** A list of protocol extensions supported by the agent. */
extensions?: AgentExtension[];
}
@@ -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.
@@ -1512,5 +1538,6 @@ export type A2AError =
| UnsupportedOperationError
| ContentTypeNotSupportedError
| InvalidAgentResponseError
- | AuthenticatedExtendedCardNotConfiguredError;
+ | AuthenticatedExtendedCardNotConfiguredError
+ | MessageIdAlreadyExistsError;
// --8<-- [end:A2AError]