Skip to content

Conversation

adamsitnik
Copy link
Collaborator

This PR does the following:

  • it removes ITaskManager.OnAgentCardQuery because TaskManager should not be responsible for Agent Cards and events should not be queries
  • introduces AgentCardProvider:
    • allows for customization (the method is virtual)
    • allows for simple configuration: the user just needs to provide AgentCard.
    • it enforces validation: all the [JsonRequired] fields are checked at creation time

So far TaskManager was by default returning an invalid AgentCard:

: Task.FromResult(new AgentCard() { Name = "Unknown", Url = agentUrl });

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

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 @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 an OnAgentCardQuery delegate. This has been removed, as TaskManager 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 validating AgentCard 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: The MapWellKnownAgentCard extension method in A2A.AspNetCore has been updated. It now directly accepts an AgentCard instance or an AgentCardProvider, making the API more intuitive and removing the previous dependency on ITaskManager for mapping agent cards.
  • Ensuring Immutability of Agent Cards: To prevent unintended modifications and ensure thread safety, AgentCardProvider now returns cloned instances of AgentCard 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 from MapHttpA2A. 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

  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.

Comment on lines -88 to -89
// /v1/card endpoint - Agent discovery
routeGroup.MapGet("/v1/card", async (HttpRequest request, CancellationToken cancellationToken) =>
Copy link
Collaborator Author

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.

Copy link
Collaborator

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?

Copy link
Collaborator

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.

Copy link
Collaborator Author

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?

Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Collaborator

@Blackhex Blackhex Aug 27, 2025

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

Copy link
Collaborator

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.

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

Comment on lines -88 to -89
// /v1/card endpoint - Agent discovery
routeGroup.MapGet("/v1/card", async (HttpRequest request, CancellationToken cancellationToken) =>
Copy link
Collaborator

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?

/// <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)
Copy link
Collaborator

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:

  1. Introduce an interface or abstract class that both AgentCardProvider and MyAgentCardProvider 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.
  2. 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.

Copy link
Collaborator

@brandonh-msft brandonh-msft Aug 1, 2025

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

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Great catch. I've added a parameter less protected constructor and a test that shows how to use it.

  1. Introduce an interface or abstract class that both AgentCardProvider and MyAgentCardProvider 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)

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

Copy link
Collaborator

@brandonh-msft brandonh-msft Aug 27, 2025

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.

/// <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)
Copy link
Collaborator

@brandonh-msft brandonh-msft Aug 1, 2025

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

brandonh-msft
brandonh-msft previously approved these changes Aug 27, 2025
@brandonh-msft brandonh-msft self-requested a review August 27, 2025 02:52
@brandonh-msft brandonh-msft dismissed their stale review August 27, 2025 03:17

wrong button

}

private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
public AgentCard Card
Copy link
Collaborator

@Blackhex Blackhex Aug 27, 2025

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.

Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Collaborator

@Blackhex Blackhex left a 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()
Copy link
Collaborator

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)
Copy link
Collaborator

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.

Copy link
Collaborator

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);
Copy link
Collaborator

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)
Copy link
Collaborator

@Blackhex Blackhex Aug 27, 2025

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
Copy link
Collaborator

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.

Comment on lines -88 to -89
// /v1/card endpoint - Agent discovery
routeGroup.MapGet("/v1/card", async (HttpRequest request, CancellationToken cancellationToken) =>
Copy link
Collaborator

@Blackhex Blackhex Aug 27, 2025

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)
Copy link
Collaborator

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?

Copy link
Collaborator

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
@brandonh-msft brandonh-msft linked an issue Sep 8, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Should ITaskManager handlers that are Task-returning Functions use the Async suffix? Allow For Dynamic Agent Card
5 participants