Create A2A flows with Microsoft Agent Framework and multiple services

You know what’s cool? Having agents talk to each other and letting them figure out how to get to the answer you’re looking for.
One way to do this is by using the Agent-to-Agent protocol in your application. Version 0.3.0 is the latest released version, and there’s an RC v1.0 available already.
The Microsoft Agent Framework (MAF) also has an implementation of this protocol available. The current version of the MAF packages, 1.0.0b260130 at the time of writing, isn’t compatible with the proposed changes of 1.0, but I’m pretty sure this will be supported in upcoming releases. The team is adding and changing features quite quickly. There are also newer versions of MAF available now, but I have yet to validate those.

In my current project, we’re creating a dozen agents, each doing its own little thing. What we could do is create some workflow or state machine, invoking each agent in turn, much like the good old days. However, we don’t always need to run every agent or run them in a specific order. While it is possible to add this dynamic nature to an application, we can also leverage the power of a language model for this. Based on the knowledge of what an agent can do, the language model can figure out which agents to invoke and in what order. The A2A protocol can help here.

Project setup

Most of the agents I’m working with are created with Python and use MAF.
Because our engineers are fluent in the .NET ecosystem, the main application is created with C# and also uses MAF.
Both projects expose an API that can be leveraged.

For ease of use, I’m using Aspire as it offers some quality-of-life features that make a developer’s life easier.

Other than that, these are just two normal applications we all know and love. For those interested, I’ve got a repository with some trial-and-error work on GitHub.

Setup the Python application

As mentioned, this project contains some important agents that I want to use in my A2A flow.
To enable this, you need to add one or more AgentCards, one for every agent. This is just a definition of your agent, providing metadata to the consuming party of the card and the endpoint that should be invoked to use the agent.

Currently, I have two agents in the backend Python solution: count-letters and large-data-analysis. For this post, I’ll focus on the first one as it’s the simplest.

Create the agent card

The endpoint for the agent card is /agents/count-letters/.well-known/agent-card.json. As mentioned, every agent needs one of these agent-card.json endpoints in the .well-known path.
Mine contains the following information:

{
  "name": "CountLettersAgent",
  "description": "Analyzes text to count letters and provide detailed reasoning about letter counts in questions.",
  "version": "1.0.0",
  "url": "https://localhost:9443/agents/count-letters-a2a",
  "protocolVersion": "1.0",
  "preferredTransport": "HTTP",
  "supportedInterfaces": [
    {
      "url": "https://localhost:9443/agents/count-letters-a2a",
      "protocolBinding": "HTTP+JSON",
      "protocolVersion": "1.0"
    }
  ],
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "capabilities": {
    "streaming": false,
    "pushNotifications": false
  },
  "skills": [
    {
      "id": "id_count_letters_agent",
      "name": "CountLettersAgent",
      "description": "Analyzes text to count letters and provide detailed reasoning about letter counts in questions.",
      "tags": ["calculator", "letter-counting", "text-analysis"],
      "examples": [
        "How many letters are in the word 'hello'?",
        "Count the letters in this sentence",
        "What is the letter count of 'Python'?"
      ]
    }
  ]
}

As you can see, it contains information about the agent and what it can do. The exact format depends on what version of the A2A protocol you’re using. The contents of the above should be compatible with both 0.3.0 and the proposed 1.0.0 version of the protocol, but the specs might change.

Implement the A2A endpoint in Python

Messages to an A2A endpoint require a specific format that looks like the following sample:

{
  "jsonrpc": "2.0",
  "method": "run",
  "params": {
    "message": {
      "kind": "message",
      "messageId": "msg-1",
      "role": "User",
      "parts": [
        {
          "kind": "text",
          "text": "How many r are there in the word strawberry?"
        }
      ]
    }
  },
  "id": 1
}

I like working with strongly typed variables, so I created the following classes in Python to support these messages:

# A2A Protocol Models - Request/Response structures
class A2AMessagePart(BaseModel):
    """A2A Message Part - represents content within a message"""

    kind: str
    text: str

class A2AMessage(BaseModel):
    """A2A Message - the core message structure"""

    kind: str = "message"
    role: str
    parts: list[A2AMessagePart]
    messageId: str

class A2ASendMessageParams(BaseModel):
    """A2A SendMessage parameters"""

    message: A2AMessage

class A2AJsonRpcRequest(BaseModel):
    """A2A JSON-RPC request with typed params"""

    jsonrpc: str = "2.0"
    method: str
    params: A2ASendMessageParams
    id: str | int

There’s a lot going on in my sample project, but the important parts are the following pieces of code:

@router.post("/count-letters-a2a")
async def count_letters_a2a(request: A2AJsonRpcRequest) -> JSONResponse:
    # Fetch the question from the request
    question = None
        for part in request.params.message.parts:
            if part.kind == "text":
                question = part.text
                break

    # Run the calculator, which in turn creates an agent for AI Foundry
    subject = calculator()
    results = await subject.run(question)

    # Parse the response and put it in a nice answer that gets returned to the client
    answer_text = (
        f"Answer: {results.answer}\n"
        f"Final Number: {results.final_number}\n"
        f"Reasoning: {results.reasoning}\n"
        f"Chain of Thought: {results.chain_of_thought}"
    )

    a2a_message = {
        "kind": "message",
        "messageId": message_id,
        "role": "Agent",  # .NET expects PascalCase role
        "parts": [{"kind": "text", "text": answer_text}],
    }

    # Return JSON-RPC response with A2A Message object
    jsonrpc_response = JsonRpcResponse(result=a2a_message, id=request.id)
    response_data = jsonrpc_response.model_dump()

    return JSONResponse(content=response_data)

Here, I’m invoking my calculator, which performs some calculations in an AI Foundry agent, but you can do whatever you need to do here. If you’re interested in the details of the calculator, check the GitHub repository or one of my previous posts.

This is just about all you need to set up on the Python backend.

Setup the .NET service

I like to keep things simple from a user perspective and not bother them with internals. Therefore, I opted for a simple REST endpoint to ask questions:

POST {{ApiService_HostAddress}}/countLetters-a2a
Accept: application/json
Content-Type: application/json

{
    "question": "I want to know the square root of the number of times the letter `r` is found in `Strawberry`?"
}

This question is then forwarded to my AgentCollaboration class, which is responsible for invoking my backend agents.

It’s good to note that you need several packages to get this working. As mentioned, I’m using preview packages at the moment:

<PackageReference Include="Microsoft.Agents.AI.AzureAI" Version="1.0.0-preview.260209.1" />
<PackageReference Include="Microsoft.Agents.AI.A2A">
  <Version>1.0.0-preview.260209.1</Version>
</PackageReference>
<PackageReference Include="Microsoft.Agents.AI.Abstractions">
  <Version>1.0.0-preview.260209.1</Version>
</PackageReference>

With this in place, you can now retrieve the agent card from the Python backend we created, add this agent as a tool to your new agent, and invoke it as such.

Setup the agent using A2A in .NET

Once you know how to set this all up, it’s fairly easy. I’ll focus on the most important parts here:

private const string Instructions =
    @"Your job is to orchestrate the use of agents made available to you.
    You will receive a question that needs to be solved and require the use of these agents.
    If an agent can't answer or provides the wrong answer, feel free to ask it again with an updated question and remarks on
    why it should try again.
    ";

public async Task<A2aResult> Ask(string question)
{
    // Get the agent card from the A2A endpoint
    var agentCardResolver = new A2ACardResolver(new Uri(endpoint), httpClient);
    var agentCard = await agentCardResolver.GetAgentCardAsync();

    // Create the A2A agent from the card
    var agent = agentCard.AsAIAgent();

    // Create function tools from the agent's skills
    var tools = CreateFunctionTools(agent, agentCard, logger).ToList();

    AIProjectClient aiProjectClient = new(new Uri(aiOptions.ProjectEndpoint), new DefaultAzureCredential());
    var newAgent = await aiProjectClient.CreateAIAgentAsync(
        name: "A2ATestClient",
        model: aiOptions.ModelDeploymentName,
        instructions: Instructions,
        tools: tools);

    var session = await newAgent.CreateSessionAsync();

    var answer = await newAgent.RunAsync(question, session);
    logger.LogInformation("Received answer from agent");

    return new A2aResult(question, answer.Text);
}

private static IEnumerable<AITool> CreateFunctionTools(AIAgent a2aAgent, AgentCard agentCard, ILogger logger)
{
    // Useful if multiple skills are defined. This way the agent over here knows what the added agent can do.
    foreach (var skill in agentCard.Skills)
    {
        AIFunctionFactoryOptions options = new()
        {
            Name = skill.Name,
            Description = $$"""
            {
                "description": "{{skill.Description}}",
                "tags": "[{{string.Join(", ", skill.Tags ?? [])}}]",
                "examples": "[{{string.Join(", ", skill.Examples ?? [])}}]",
                "inputModes": "[{{string.Join(", ", skill.InputModes ?? [])}}]",
                "outputModes": "[{{string.Join(", ", skill.OutputModes ?? [])}}]"
            }
            """,
        };

        yield return AIFunctionFactory.Create(RunAgentAsync, options);
    }

If you have ever worked with Semantic Kernel or MAF a lot of the above will look familiar. The only new part is the parsing of the agent card and add the agent as a tool to the agent you create over here.
Over here there is only 1 agent added. I’ve also tested this with multiple agents, using the A2A protocol, and am happy with the result. This orchestrator is able to determine what agent to invoke and if it needs to invoke multiple it can do that too.

To conclude

When your application grows, so will the number of agents you have in the code. I think it makes sense to start using the A2A-protocol from the start, so you can extend the workflows with ease. It does bring a bit of overhead, but I think it’s worth it in the long run. If you implement it smart, there’s probably a lot of code you can reuse across multiple agents so the overhead & maintenance is minimal.