-
Notifications
You must be signed in to change notification settings - Fork 38
feat: introduce AgentCardProvider #130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…kManager.OnAgentCardQuery
There was a problem hiding this 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 @adamsitnik, 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!
This pull request significantly refactors the mechanism for agents to expose their capabilities and metadata within the A2A framework. My goal was to improve clarity, configurability, and adherence to best practices in API design by centralizing agent card management and decoupling it from task management responsibilities. This change makes it more intuitive for users to configure agent cards and ensures robust validation of agent metadata.
Highlights
- Decoupling Agent Card Provision from TaskManager: I've refactored the responsibility of providing
AgentCard
information. Previously,ITaskManager
had anOnAgentCardQuery
delegate. This has been removed, asTaskManager
should not be responsible for agent card queries. - Introduction of AgentCardProvider: I've introduced a new class,
AgentCardProvider
, which is now solely responsible for handling and validatingAgentCard
instances. This centralizes the logic for agent card configuration and ensures that all required fields are present at creation time. - Simplified Agent Card Exposure: Agents now expose their
AgentCard
as a public property (e.g.,public AgentCard Card { get; }
) instead of implementing a method to query it. This simplifies how agents define and provide their metadata. - Improved
MapWellKnownAgentCard
API: TheMapWellKnownAgentCard
extension method inA2A.AspNetCore
has been updated. It now directly accepts anAgentCard
instance or anAgentCardProvider
, making the API more intuitive and removing the previous dependency onITaskManager
for mapping agent cards. - Ensuring Immutability of Agent Cards: To prevent unintended modifications and ensure thread safety,
AgentCardProvider
now returns cloned instances ofAgentCard
when queried. This ensures that each request receives a fresh copy of the agent's metadata. - Standardization of Agent Discovery Endpoint: The
/v1/card
endpoint, which was previously used for agent discovery, has been removed fromMapHttpA2A
. The standard.well-known/agent.json
endpoint is now the sole mechanism for agent card discovery, streamlining the API surface.
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
-
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. ↩
// /v1/card endpoint - Agent discovery | ||
routeGroup.MapGet("/v1/card", async (HttpRequest request, CancellationToken cancellationToken) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like #110 missed it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this code is removed, how can the agent card be requested?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right; there are 2 ways to get at the card in our impl; one is via an http route, and the other is via the well known endpoint (a la json RPC)
if we remove this, we're forcing only well-known support. could be ok, but just so we're aware.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on #105 I have assumed that we want the AgentCard
to be exposed optionally. I am fine reverting this particular change, but I want to make sure that it's desired.
@darrelmiller thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with removing that (maybe in a separate PR) as long as there's a way to configure getting the agent card over an HTTP route if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The specification is a bit challenging in that it says an Agent MUST expose an Agent Card, but doesn't mandate how the agent must be discovered (which is good). It feels to me like the introduction of the /v1/card endpoint is one way to guarantee that if someone gives you a URL to an agent, then at least you have a guaranteed way to find the card. Except JSON-RPC doesn't yet implement that endpoint so, again not guaranteed.
I think priority one is that we make it possible to make compliant agents. Creating a library that helps people fall into the pit of success can come over time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding of the specification is that this endpoint should implement Extended Authenticated Agent Card https://a2a-protocol.org/dev/specification/#356-method-mapping-reference-table
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Blackhex Thanks for that pointer. That's news to me and a somewhat perplexing decision in my opinion.
There was a problem hiding this 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 a significant architectural improvement by creating the AgentCardProvider
. This change effectively centralizes agent card creation and validation, removing this responsibility from the ITaskManager
. The implementation is well-executed across the sample agents and ASP.NET Core extensions, enhancing modularity and maintainability. My review includes a critical suggestion to ensure deep cloning in the AgentCard
copy constructor to prevent potential data corruption issues, and a refactoring suggestion in AgentCardProvider
to improve code readability.
// /v1/card endpoint - Agent discovery | ||
routeGroup.MapGet("/v1/card", async (HttpRequest request, CancellationToken cancellationToken) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this code is removed, how can the agent card be requested?
src/A2A/Server/AgentCardProvider.cs
Outdated
/// <param name="agentCard">The valid agent card to initialize with.</param> | ||
/// <exception cref="ArgumentNullException">Thrown when agentCard is null.</exception> | ||
/// <exception cref="ArgumentException">Thrown when any required property of agentCard is null or empty.</exception> | ||
public AgentCardProvider(AgentCard agentCard) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case I want to build my agent card dynamically, I'll need to inherit from the class to override the GetAgentCardAsync
method. However, in my class, I'll also have to add the constructor public MyAgentCardProvider(AgentCard agentCard) : base(agentCard)
that would force me to provide an agent card I don't have yet, and the class is supposed to build it for me.
I can see two options to fix this:
- Introduce an interface or abstract class that both
AgentCardProvider
andMyAgentCardProvider
will implement or inherit from. Pros: DI friendly. Cons: A new, unnecessary model class will need to be added increasing the public API surface of the SDK, resulting in more potentially breaking changes when it needs to change. - Model the agent card provider as a callback function and refactor the
MapWellKnownAgentCard(this IEndpointRouteBuilder endpoints, Func<Task<AgentCard>> agentCardProvider, [StringSyntax("Route")] string agentPath)
to use it. Pros: Simple. Cons: Not DI friendly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree w/ the above; I'm - of course - for Opt 1 (it follows what I had proposed) but I hear the concern about public surface area as well...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can start simply with option 2 and implement option 1 later, if and when needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case I want to build my agent card dynamically, I'll need to inherit from the class to override the
GetAgentCardAsync
method. However, in my class, I'll also have to add the constructorpublic MyAgentCardProvider(AgentCard agentCard) : base(agentCard)
that would force me to provide an agent card I don't have yet, and the class is supposed to build it for me.
Great catch. I've added a parameter less protected constructor and a test that shows how to use it.
- Introduce an interface or abstract class that both
AgentCardProvider
andMyAgentCardProvider
will implement or inherit from. Pros: DI friendly. Cons: A new, unnecessary model class will need to be added increasing the public API surface of the SDK, resulting in more potentially breaking changes when it needs to change.
With the updated form the implementation gives us the benefits of the abstract
class without its disadvantages (the need of providing an implementation and users discovering its existence)
- Model the agent card provider as a callback function and refactor the
MapWellKnownAgentCard(this IEndpointRouteBuilder endpoints, Func<Task<AgentCard>> agentCardProvider, [StringSyntax("Route")] string agentPath)
to use it. Pros: Simple. Cons: Not DI friendly.
Func
is very simple, but on the other hand it limits us to only one method. What if we need a new one? With a spec evolving as quickly as A2A I would prefer to have future-proof solutions from the beginning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not seeing where the latest rev handles the scenario where I want to build my card dynamically (call MCP servers to set up skills collection, etc.)... I'd have to do all that before creating the Provider instance, and then use the provider in the extension method call. Since all this happens during DI container setup, doing async
to build my card first is not fun.
I think this is why an Interface is a better choice here, with - perhaps - a default impl that looks like what's in this PR now, but named something like StaticAgentCardProvider
.
src/A2A/Server/AgentCardProvider.cs
Outdated
/// <param name="agentCard">The valid agent card to initialize with.</param> | ||
/// <exception cref="ArgumentNullException">Thrown when agentCard is null.</exception> | ||
/// <exception cref="ArgumentException">Thrown when any required property of agentCard is null or empty.</exception> | ||
public AgentCardProvider(AgentCard agentCard) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree w/ the above; I'm - of course - for Opt 1 (it follows what I had proposed) but I hear the concern about public surface area as well...
} | ||
|
||
private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken) | ||
public AgentCard Card |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the library hint its users how agent implementations should look like by providing abstract agent class demonstrating what are the common patterns? Such class could either have AgentCard Card
as abstract property or implement AgentCardProvider
. app.MapWellKnownAgentCard()
could then accept agent instance instead of card, attaching agent instance to TaskManager
instance could be simplified, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the original design goals was to impose no requirements on the implementation of the agent. An existing agent should be able to adapted to "talk A2A". Requiring an agent to implement a specific base class would force people to create a "proxy agent" to adapt existing agents. That feels pretty intrusive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for explaining the original intent. The suggestion was meant as an option or a showcase of "good practice" not a requirement though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea in general. I just think it can go one step further.
get | ||
{ | ||
return Task.FromCanceled<AgentCard>(cancellationToken); | ||
var capabilities = new AgentCapabilities() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not inlining it into AgentCard
instance initializer? (The same applies to other agent implementations in samples/
folder.)
/// <exception cref="ArgumentException">Thrown when any required property of <paramref name="agentCard"/> is invalid.</exception> | ||
public static AgentCard Validate(AgentCard agentCard) | ||
{ | ||
if (agentCard is null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, consider moving this logic into a separate class used here (that can even be set as a parameter of the provider constructor). Technically speaking having it here breaks SRP.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could these validations be done in the agent card itself?
/// <param name="agentCard">The valid agent card to initialize with.</param> | ||
/// <exception cref="ArgumentNullException">Thrown when agentCard is null.</exception> | ||
/// <exception cref="ArgumentException">Thrown when any required property of agentCard is null or empty.</exception> | ||
public AgentCardProvider(AgentCard agentCard) => _agentCard = Validate(agentCard); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why two constructors are needed? Wouldn't single constructor with optional argument defaulting to null be more conscise?
{ | ||
return Results.Ok(await agentCardProvider.GetAgentCardAsync(uriBuilder.ToString(), cancellationToken)); | ||
} | ||
catch (Exception ex) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the example agent implementation implementing the AgentCardProvider
mentioned in the previous comment would be utilized, this error handling (and this application-specific message display) could be offloaded to the user buisiness logic.
} | ||
|
||
private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken) | ||
public AgentCard Card |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for explaining the original intent. The suggestion was meant as an option or a showcase of "good practice" not a requirement though.
// /v1/card endpoint - Agent discovery | ||
routeGroup.MapGet("/v1/card", async (HttpRequest request, CancellationToken cancellationToken) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding of the specification is that this endpoint should implement Extended Authenticated Agent Card https://a2a-protocol.org/dev/specification/#356-method-mapping-reference-table
{ | ||
throw new ArgumentException("Agent card must define a non-empty ProtocolVersion.", nameof(agentCard)); | ||
} | ||
else if (agentCard.Capabilities is null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a second thought, shouln't for example this validation be handled by the nullability of the property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i had noticed we are doing null checks elsewhere in the codebase as well; I believe this is because netstandard 2 doesn't support nullable.
…agentCardProvider # Conflicts: # src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs
This PR does the following:
ITaskManager.OnAgentCardQuery
becauseTaskManager
should not be responsible for Agent Cards and events should not be queriesAgentCardProvider
:virtual
)AgentCard
.[JsonRequired]
fields are checked at creation timeSo far
TaskManager
was by default returning an invalidAgentCard
:a2a-dotnet/src/A2A/Server/TaskManager.cs
Line 42 in 5b6cdd8
And the user had to find out how to configure it properly. Now I hope it's more intuitive and kind of obvious.
fixes #88