In this post, let's see how we can expose an agent over the
Agent-to-Agent (A2A) protocol, and then call it from another
application as if it were a local agent.
So far in this series, whoever called our agents either ran them in the same
process or knew exactly how they were hosted. A2A is an open protocol
for the case where they don't: an agent publishes a card describing itself,
and any A2A client, built with any framework, can discover it and talk to it.
Agent Framework has packages for both sides.
We'll use the same weekend-planner agent as in
Getting Started with Microsoft Agent Framework 1.0.0 in .NET, with its three tools: GetWeather, GetActivities and
GetCurrentDate.
Exposing the agent
The server is an ASP.NET Core application with these packages (latest as of
today, it will change).
<PackageVersion Include="Microsoft.Agents.AI.Foundry" Version="1.25.0-preview.261014.1" /> <PackageVersion Include="Microsoft.Agents.AI.Hosting.A2A.AspNetCore" Version="1.25.0-preview.261014.1" />
The agent itself is created the usual way.
AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) .AsAIAgent( model: deploymentName, name: "weekend-planner", description: "Plans weekends and suggests activities based on the weather.", instructions: """ You help users plan their weekends and choose the best activities for the given weather. If an activity would be unpleasant in weather, don't suggest it. Include date of the weekend in response. """, tools: [ AIFunctionFactory.Create(GetWeather, nameof(GetWeather)), AIFunctionFactory.Create(GetActivities, nameof(GetActivities)), AIFunctionFactory.Create(GetCurrentDate, nameof(GetCurrentDate)) ]);
The agent card
Then the agent card. This is what other agents read to discover ours before
they send it anything, and the client we write later starts from it.
// What other agents read to discover this one, before they send it anything AgentCard agentCard = new() { Name = agent.Name!, Description = agent.Description!, Version = "1.0.0", DefaultInputModes = ["text/plain"], DefaultOutputModes = ["text/plain"], Capabilities = new() { Streaming = false, PushNotifications = false }, Skills = [ new() { Id = "weekend-planning", Name = "Weekend planning", Description = "Suggests weekend activities for a city, based on the weather.", Examples = ["What should I do this weekend in Auckland?"] } ], SupportedInterfaces = [ new() { Url = agentUrl, ProtocolBinding = ProtocolBindingNames.JsonRpc, ProtocolVersion = "1.0" } ] };
There is no method that builds the card from the agent, so it is all ours to
fill in. What the agent already knows, its name and description, we take from
it, so they are defined in one place. SupportedInterfaces is what tells
a client where to send messages and which binding to use, so it has to match
the endpoint we map next.
Hosting it
Now we register an A2A server for the agent, map its endpoint, and serve the
card.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.AddA2AServer(agent); // Sessions are not kept by default, so every message would start a new conversation builder.Services .AddKeyedSingleton<AgentSessionStore>(agent.Name, new InMemoryAgentSessionStore()); WebApplication app = builder.Build(); app.MapA2AJsonRpc(agent, "/"); // Served at /.well-known/agent-card.json app.MapWellKnownAgentCard(agentCard); app.Run();
A few things to note here.
A2A defines two protocol bindings, JSON-RPC and HTTP+JSON, and
there is a map method for each: MapA2AJsonRpc and
MapA2AHttpJson. You can map both if you want clients to pick. Here I am
using JSON-RPC only.
MapWellKnownAgentCard comes from the A2A SDK (A2A.AspNetCore),
not from Agent Framework, and serves the card at
/.well-known/agent-card.json.
Only one card can be served per host, so if you host several agents in one
application, only one of them is discoverable this way.
The AgentSessionStore line is easy to miss. Without it, the server
doesn't keep any sessions, so a follow-up message from the same client has no
memory of the first one. InMemoryAgentSessionStore keeps them in the
process, which is fine for a sample. It is still experimental, so you need to
suppress MAAI001.
Calling it over HTTP
Before writing a client, let's see what is on the wire. Run the server, and
use the following .http file.
@host = http://localhost:5000
POST {{host}}/
Content-Type: application/json
A2A-Version: 1.0
{
"jsonrpc": "2.0",
"id": "1",
"method": "SendMessage",
"params": {
"message": {
"messageId": "1",
"role": "ROLE_USER",
"parts": [
{
"text": "What should I do this weekend in Auckland?"
}
]
}
}
}
Runs the agent, tools and all, and the answer comes back as an A2A message.
The contextId is the conversation. Send it back on the next message and
the server picks up the same session from the session store; leave it out and
a new conversation starts.
Note that the reply is a message, not a task. Agent Framework
returns every response as an immediate message for now, as background
responses aren't supported yet for A2A hosted agents.
Calling it from another agent application
The client is a console application with just one package.
<PackageVersion Include="Microsoft.Agents.AI.A2A" Version="1.25.0-preview.261014.1" />
It only needs the server's base address. It reads the card, and turns it into
an AIAgent.
using A2A; using Microsoft.Agents.AI; string agentUrl = Environment.GetEnvironmentVariable("A2A_AGENT_URL")!;
// Reads the agent card from /.well-known/agent-card.json A2ACardResolver cardResolver = new(new Uri(agentUrl)); AgentCard agentCard = await cardResolver.GetAgentCardAsync(); Console.WriteLine($"[A2A] Found '{agentCard.Name}': {agentCard.Description}"); // The remote agent becomes an AIAgent, called like any local one AIAgent agent = agentCard.AsAIAgent(); // One session, so the follow-up continues the same conversation on the remote side AgentSession session = await agent.CreateSessionAsync(); string[] userInputs = [ "What should I do this weekend in Auckland?", "What about the weekend after that?" ]; foreach (string userInput in userInputs) { Console.WriteLine(); Console.WriteLine("--- User ---"); Console.WriteLine(userInput); AgentResponse response = await agent.RunAsync(userInput, session); Console.WriteLine(); Console.WriteLine("--- Agent Response ---"); Console.WriteLine(response.ToString()); }
Console.ReadLine();
That's the nice part. Once we have the AIAgent, nothing in the rest of
the code knows it is remote. The session carries the contextId for us,
so the second question is answered in the context of the first.
[A2A] Found 'weekend-planner': Plans weekends and suggests activities based on the weather. --- User --- What should I do this weekend in Auckland? --- Agent Response --- Weekend dates: **Saturday 2026-10-24 and Sunday 2026-10-25** in **Auckland**. Forecast: **~18°C and rainy**, so I’d avoid **the beach** and **hiking** (wet, windy, slippery tracks). // Omitted for brevity --- User --- What about the weekend after that? --- Agent Response --- Weekend dates: **Saturday 2026-10-31 and Sunday 2026-11-01** in **Auckland**. Forecast: **~18°C and rainy**, so I’d skip **the beach** and **hiking** again (not great in wet weather). // Omitted for brevity
And on the server, the tools ran for both weekends.
[Tool] Getting current date [Tool] Getting weather for 'Auckland'. [Tool] Getting activities for 'Auckland' on '2026-10-24'. [Tool] Getting activities for 'Auckland' on '2026-10-25'. [Tool] Getting weather for 'Auckland'. [Tool] Getting activities for 'Auckland' on '2026-10-31'. [Tool] Getting activities for 'Auckland' on '2026-11-01'.
Note there's no second GetCurrentDate call. The agent already knew
today's date from the first turn, which shows the server really did keep the
session.
The complete sample is
here.
More read:
Microsoft Agent Framework: A2A Hosting
Microsoft Agent Framework: A2A Agent
Agent2Agent (A2A) Protocol Specification
Microsoft Agent Framework: A2A Agent
Agent2Agent (A2A) Protocol Specification
Hope this helps.
Happy Coding.
Regards,
Jaliya


No comments:
Post a Comment