Skip to content

Conversation

@maeste
Copy link
Contributor

@maeste maeste commented Jul 30, 2025

  • Add correlationIdRequired capability to AgentCapabilities
  • Add correlationId field to MessageSendParams
  • Add CorrelationIdAlreadyExistsError (-32008) for duplicate detection
  • Update all transport formats (JSON-RPC, gRPC, REST)
  • Add comprehensive spec documentation and examples

Fixes #928
Discussed also in #925

  - Add correlationIdRequired capability to AgentCapabilities
  - Add correlationId field to MessageSendParams
  - Add CorrelationIdAlreadyExistsError (-32008) for duplicate detection
  - Update all transport formats (JSON-RPC, gRPC, REST)
  - Add comprehensive spec documentation and examples

Fixes a2aproject#928
@maeste maeste requested review from a team as code owners July 30, 2025 17:11
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @maeste, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

I've implemented support for correlation IDs to enable idempotent task creation within the A2A protocol. This enhancement addresses potential issues with duplicate operations caused by network failures or client crashes, ensuring that message/send requests can be safely retried.

Highlights

  • New Capability for Agents: Agents can now declare correlationIdRequired in their AgentCapabilities to indicate they require a correlation ID for new task creation requests.
  • Correlation ID in Message Sending: The MessageSendParams now includes an optional correlationId field, allowing clients to provide a unique identifier for idempotent operations.
  • New Error Type: A new A2A-specific error, CorrelationIdAlreadyExistsError (-32008), has been introduced to signal when a provided correlation ID is already associated with an active task.
  • Protocol Specification Updates: The A2A specification has been comprehensively updated across JSON-RPC, gRPC, and REST formats, including detailed documentation on correlation ID behavior for agents, servers, and clients, along with example scenarios.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments or fill out our survey to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for idempotent task creation using a correlationId. The changes are comprehensive, updating the specification documents, gRPC proto files, JSON schema, and TypeScript types. The implementation is well-documented with clear explanations and examples. I have a couple of minor suggestions to improve clarity in the specification and fix a style issue in the proto file.

maeste and others added 2 commits July 30, 2025 19:13
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@pstephengoogle
Copy link
Contributor

I think the same ability can be achieved by
a) using the message id for idempotent task creation on a specific request. We already provide the context id to provide correlations if the first message send request returns a result. By using the message id for idempotent task creation, if you didn't get the response due to failure, the agent that provides idempotent guarantees can return the same Task or stream it would have on the original request.
b) the only spec change, in theory, would be to add an 'is_idempotent_task' capability boolean (or similar)
c) I would recommend doing this via an extension profile initially. The biggest concern is the burden that is placed on agent developers to implement this guarantee. Doing this as an extension, and at the very least, providing a reference implementation, will go a long way toward determining if and how to support as a core spec change

@ReubenBond
Copy link

I think the same ability can be achieved by
a) using the message id for idempotent task creation on a specific request.

+1 for using the existing identifier instead of creating a new one.

Implementation-wise, I believe it's easier if we also allow users to specify contextId (the conversation thread identifier) and scope messageId to the contextId (i.e, the tuple (contextId, messageId) uniquely identifies a message). Otherwise, each messageId needs to be added to a global index rather than just each contextId. If contextId is not specified, then it can be randomly generated and idempotency is lost, which is ok since the caller doesn't support it anyway.

The biggest concern is the burden that is placed on agent developers to implement this guarantee.

My hope is that we can arrive at a behavior which is optional, straightforward to implement, and can be gradually adopted as agents & clients mature without breaking either party (or adding onerous complexity) in the meantime.

@maeste
Copy link
Contributor Author

maeste commented Jul 31, 2025

I think the same ability can be achieved by a) using the message id for idempotent task creation on a specific request. We already provide the context id to provide correlations if the first message send request returns a result. By using the message id for idempotent task creation, if you didn't get the response due to failure, the agent that provides idempotent guarantees can return the same Task or stream it would have on the original request.

@pstephengoogle I've originally created a new field to avoid semantic overload, considering messaId currently serves conversations history. But re-thinking it I see there is more minus than plus on having a new field, so I've just pushed a change to use mesageId

b) the only spec change, in theory, would be to add an 'is_idempotent_task' capability boolean (or similar)
In the end, the idempotency using messageId was already possible as implementation details. Adding the field to explicit support of it as I did in this PR adds a contract between client and server, so I've added some clarification in the spec in terms of expected behaviours and error handling too.

c) I would recommend doing this via an extension profile initially. The biggest concern is the burden that is placed on agent developers to implement this guarantee. Doing this as an extension, and at the very least, providing a reference implementation, will go a long way toward determining if and how to support as a core spec change
I like the idea of pushing a new feature via extension, but in this specific case, I would prefer to see it as part of the core spec, because it is fully optional and only norms a behaviour already possible, that agents may want to implement for critical tasks. Moreover, I believe that the correct management of tasks in a fully distributed architecture is a key distinguishing aspect of A2A compared to other agents' protocols, so it would be better not to leave the idempotency hole in the core spec.
Just my 2C

@maeste maeste force-pushed the maeste/issue928 branch from 7fb9971 to 45bdc5f Compare July 31, 2025 07:27
@holtskinner holtskinner requested review from a team and ToddSegal and removed request for a team and ToddSegal July 31, 2025 15:30
@holtskinner holtskinner added the TSC Review To be reviewed by the Technical Steering Committee label Aug 1, 2025
@izzymsft
Copy link
Contributor

izzymsft commented Aug 2, 2025

@pstephengoogle I agree with @maeste on making this an optional server capability just like streaming and push notifications is optional.

For servers that support it, then clients should expect the correlation identifier in responses from the server (including callback push notifications).

If we make it an optional capability on the agent card then only server implementations that want to support it will have to do it, thus minimizing the initial burden of one chooses to implement the spec version.

@maeste I think the push notification coming from the server after a message/send for servers that support push notifications should also include this correlation identifier in the callback operation so that it can be reconciled with the original message that triggered it.

You can call this out explicitly in your docs as the payload structure for push notifications is currently not clearly defined.

You may also want to review all the other interactions to see where this identifier could also be relevant in the response from the servers.

If idempotency is support I think we will need the identifier beyond the message/send interaction alone.

It's a great idea to use the original message identifier from the client as the correlation identifier. This was my initial thought as well.

@maeste
Copy link
Contributor Author

maeste commented Aug 3, 2025

@izzymsft Thanks for the comment, but I'm not sure I'm following: the idempotency proposed here is about task creation. And by spec (see also chapter 9.5 with the example) the client receive a message confirming the task is created as usual. Then when you have taskId the idempotency is guaranteed by the taskId. The problem of idempotency solved here is on task creation. Am I missing something?

@izzymsft
Copy link
Contributor

izzymsft commented Aug 3, 2025

@maeste I was only agreeing with you and I just added that we would include the correlation id in the push notifications as well.

This whole idempotency discussion is primarily an issue in scenarios where there is a disconnection of the client from the server before the server response was received and recorded by the client

If the client sends a message id then the server should include that in the push notifications as well as correlation id so that we can link it back to the original message id if it disconnected from the server before a response with the task id or context id was received and recorded by the client to be used in future interactions.

This will also help us to associate this correlation id to the task and context identifiers.

I hope this explains what I meant

@izzymsft
Copy link
Contributor

@maeste I know this is implicit but it would be great to update the spec so that the format for correlation id also matches the expectations from #966 and #869 explicitly.

This is going to be for versions after 0.3.0 so it makes sense for it to follow the same expectations.

**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?

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

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

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)

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


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.

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

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

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

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

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TSC Review To be reviewed by the Technical Steering Committee

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Idempotency hole when sending first message

8 participants