Two agents sit in front of me. One is a C# service that reconciles invoices; the other is a Python agent that reads contracts. Each works fine alone. The moment I want the invoice agent to ask the contract agent "what are the payment terms on PO-4471?", I hit a wall that has nothing to do with models. HTTP is the easy part. The hard part is that neither agent has a shared way to say what it can do, how to reach it, or what to do when the answer takes four minutes instead of 40 milliseconds.
That gap is what the Agent2Agent (A2A) protocol targets, and the .NET SDK for it is now on NuGet. The protocol's own framing is blunt about the problem: how do independent AI agents discover, communicate, and collaborate with each other? The SDK's job is to make a .NET agent a first-class participant in that conversation — discoverable and callable by an agent written in any stack, without exposing its internal tools, memory, or prompts.
The interesting design decisions here are not where you'd expect, so let me walk through what the SDK actually gives you.
The agent card is the contract
Before any message is sent, an A2A client fetches an agent card — a JSON document describing the agent's name, description, endpoint URL, supported input/output modes, capabilities (like streaming), and a list of skills. It lives at a well-known URL, and on the client side you resolve it with one call:
var cardResolver = new A2ACardResolver(new Uri("http://localhost:5000/"));
AgentCard card = await cardResolver.GetAgentCardAsync();
Console.WriteLine($"Streaming support: {card.Capabilities?.Streaming}");
This is the piece people skim past, and it's the whole point. The card is a machine-readable capability contract. A calling agent doesn't need to be hardcoded against yours; it reads the card, sees that you speak text, that you stream, that you expose a skill, and adjusts. It's the same instinct as an OpenAPI document, aimed at agents instead of humans.
A server agent is a class and two handlers
Here's where the SDK earns its keep. A server-side agent doesn't inherit a heavy base class or implement a sprawling interface. It's a plain class that attaches two delegates to a TaskManager:
public class EchoAgent
{
public void Attach(ITaskManager taskManager)
{
taskManager.OnMessageReceived = ProcessMessageAsync;
taskManager.OnAgentCardQuery = GetAgentCardAsync;
}
private async Task<Message> ProcessMessageAsync(MessageSendParams p, CancellationToken ct)
{
string text = p.Message.Parts.OfType<TextPart>().First().Text;
return new Message
{
Role = MessageRole.Agent,
MessageId = Guid.NewGuid().ToString(),
ContextId = p.Message.ContextId,
Parts = [new TextPart { Text = $"Echo: {text}" }]
};
}
// GetAgentCardAsync returns an AgentCard with Name, Url,
// DefaultInputModes/OutputModes and Capabilities { Streaming = true }
}
TaskManager owns the protocol — the JSON-RPC binding, request routing, task lifecycle — and you own the two functions that matter: what to say when someone messages you, and what card to hand back when someone asks who you are. Wiring it into ASP.NET Core is one line:
var agent = new EchoAgent();
var taskManager = new TaskManager();
agent.Attach(taskManager);
app.MapA2A(taskManager, "/agent");
Notice the shape: messages carry a ContextId, and the handler echoes it back. That's how a multi-turn conversation stays stitched together across independent HTTP calls. Preserve it — it isn't decoration.
Messages vs tasks — the split that matters
The distinction I'd attend to most is messages versus tasks, because it's a protocol design decision, not an implementation detail.
A message is a synchronous exchange: you send, you get a Message back, done. That's the echo agent above. But a lot of real agent work — generate a report, run a data pull, wait on a human approval — doesn't finish inside one request. For that, A2A models a task: a persistent, asynchronous unit of work with its own identity and a status that moves through Running, Completed, or Failed. The client sends a message, gets back a task handle, and polls GetTaskAsync() until it resolves.
The agent decides whether a request is a quick reply or a long-running task, and the client handles both from the same call. The result of SendMessageAsync is effectively a union — check whether you got a Message or an AgentTask, and branch.
This is what separates A2A from "just POST some JSON." Long-running, resumable work is baked into the protocol, rather than being something every team reinvents with a bespoke job table and a status endpoint.
Calling an agent, with streaming
The client side mirrors the server. Resolve the card, point a client at the URL, send:
var client = new A2AClient(new Uri(card.Url));
var message = new Message
{
Role = MessageRole.User,
MessageId = Guid.NewGuid().ToString(),
Parts = [new TextPart { Text = "Hello from the A2A client!" }]
};
// non-streaming
var reply = (Message)await client.SendMessageAsync(
new MessageSendParams { Message = message });
// streaming over Server-Sent Events
await foreach (SseItem<A2AEvent> item in
client.SendMessageStreamAsync(new MessageSendParams { Message = message }))
{
var chunk = (Message)item.Data;
Console.WriteLine(((TextPart)chunk.Parts[0]).Text);
}
Streaming rides Server-Sent Events — that's why the System.Net.ServerSentEvents package comes along — so partial output flows back as the agent produces it. It's the difference between a chatbot that types and one that stalls, applied to agent-to-agent calls.
What I'd keep in mind
Two things. First, this is preview software: the core A2A and A2A.AspNetCore packages are prerelease, versions are moving, and the surface will shift before it stabilizes — so pin your versions. Second, the SDK deliberately doesn't opine on identity, authorization, or trust between agents. The card tells you what an agent can do, not whether you should let it. Putting auth in front of MapA2A is on you.
The takeaway I'd hold onto: A2A isn't an AI feature, it's an interop protocol that happens to be about agents. The moment your agent needs to be one node among many — some in .NET, some not — the value is the card and the message/task split, not the model behind it. Build the smallest possible echo agent first, point the A2A Inspector at its card to confirm the handshake, and only then wire in anything intelligent. The plumbing is the product here.
Sources: Building AI Agents with the A2A .NET SDK — Microsoft Foundry Blog, a2aproject/a2a-dotnet on GitHub