Multi-Agent Orchestration in .NET Using A2A
Investigation of how to orchestrate an agentic system using an A2A protocol based on .NET primitives and create a PoC. Use Case: We have existing agents that support A2A, and we want to build it into a multi-agent system. Protocol Let's start from theory. The agent-to-agent (A2A) protocol is designed to define well-known contracts for communication between agents without humans. Every agent publishes an AgentCard /.well-known/agent-card.json. An Agent client discovers the card, then sends tasks to the agent as messages. Our building blocks: AgentCard - what the agent can do Agent handler - the server-side logic A2AClient - sends messages to a remote agent graph LR User([User]) --> Orch[Orchestrator] Orch -. discover AgentCard .-> A[Assortment agent] Orch -. discover AgentCard .-> S[SupplyChain agent] Orch -- A2A SendMessage --> A Orch -- A2A SendMessage --> S A --> AT[(Catalog tools)] S --> ST[(Stock tools)] PoC setup Two agents: Assortment and SupplyChain that support A2A Orchestrator: agent that routes user requests to agents and aggregates results LLM: Ollama based on Microsoft.Extensions.AI as an abstraction that support tool calling. Can be swapped for any IChatClient (Azure OpenAI, Bedrock, OpenAI, etc.). Aspire host, which connects and runs all services together and helps to understand flow using OpenTelemetry Agent implementation Microsoft provides an abstraction for the A2A spec for ASP.NET Core var agentCard = new AgentCard { Name = "AssortmentSpecialist", Description = "Handles shop inventories, product categorizations, catalogs, and store assortments.", Skills = [ new AgentSkill { Id = "get-product", Name = "GetProduct", Description = "Look up a product's SKU, category, active status, and store coverage by name.", }, ], }; builder.Services.AddA2AAgent(agentCard); var app = builder.Build(); app.MapWellKnownAgentCard(agentCard, ""); app.MapA2A("/"); The handler processes tasks using the LLM and the agent's own tools. public async Task ExecuteAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken ct) { var responder = new MessageResponder(eventQueue, context.ContextId); var messages = new List { new(ChatRole.System, "You are the Assortment specialist. Use the tools to look up real data."), new(ChatRole.User, context.UserText ?? string.Empty), }; var options = new ChatOptions { Tools = [AIFunctionFactory.Create(tools.GetProduct)] }; var response = await chatClient.GetResponseAsync(messages, options, ct); await responder.ReplyAsync(response.Text, ct); } Orchestrator There are several ways to orchestrate agents We craft Manual explicit route Define Workflow engine Let LLM route We chose the last one because we want to have the possibility to add a new agent without any code changes. That way, we register every agent card dynamically as an orchestrator agent tool, and aggregation happens in the same LLM loop. public async Task HandleAsync(ChatThread thread, string userMessage, CancellationToken ct) { var agents = await registry.GetAgents(ct); var tools = agents.Select(ToTool).Cast().ToList(); var messages = new List { new(ChatRole.System, SystemPrompt) }; messages.Add(new ChatMessage(ChatRole.User, userMessage)); using var client = new FunctionInvokingChatClient(chatClient) { AllowConcurrentInvocation = true, }.AsBuilder().Build(); var response = await client.GetResponseAsync( messages, new ChatOptions { Tools = tools, AllowMultipleToolCalls = true }, ct); return response.Text; } We convert remote agents to AIFunction from AgentCard private AIFunction ToTool(RemoteAgent agent) { var dispatch = async (string request, CancellationToken ct) => { var response = await agent.Client!.SendMessageAsync(request, Role.User, cancellationToken: ct); return ExtractText(response); }; return AIFunctionFactory.Create(dispatch, agent.Card!.Name, $"Ask the {agent.Card.Name} specialist. {agent.Card.Description}"); } The orchestrator resolves each card with A2ACardResolver, then turns it into a tool. A request that needs both agents makes the LLM call both agents in parallel and merge their replies into one. sequenceDiagram actor User participant Orch as Orchestrator (LLM loop) participant A as Assortment agent participant S as SupplyChain agent User->>Orch: "Stores carrying the coat AND its stock?" par send both messages in parallel Orch->>A: A2A SendMessage(sub-task) and Orch->>S: A2A SendMessage(sub-task) end A-->>Orch: catalog answer S-->>Orch: stock answer Orch->>Orch: merge results Orch-->>User: one cohesive answer Conclusion The result is visualized with traces in the cover image Protocol has a stable version, but .NET libs are still in preview Communication between agents and tool calls takes time. It is better suited to long-running tasks than for immediate responses. Managing chat history is also a pain point. ohalay / a2a-poc A2A Multi-Agent Orchestrator A .NET 10 PoC for the A2A. Orchestrator discovers agents over HTTP, exposes each as a tool to one LLM loop, and aggregate to one response. All LLM inference runs locally through Ollama (llama3.2). Architecture graph TB Ollama[("Ollamallama3.2local LLM (external)")] User([User / Browser]) -->|HTTP| Orch subgraph Orch["Orchestrator"] API["Minimal API + chat UI/api/chat"] Svc["OrchestrationServiceone tool-calling LLM loop"] Reg["AgentRegistry(AgentCards + A2AClients)"] Store["ChatStore(history by threadId)"] API --> Svc Svc --> Reg Svc --> Store end Svc -->|LLM: tool loop + synthesis| Ollama subgraph Assort["AssortmentSpecialist (A2A server)"] AH["DomainAgentHandler"] AT["AssortmentToolsGetProduct / GetActiveCatalog"] AH --> AT end subgraph Supply["SupplyChainAnalyst (A2A server)"] SH["DomainAgentHandler"] ST["SupplyChainToolsGetStock / GetShipments"] SH --> ST end Reg -.->|discover AgentCard| Assort Reg -.->|discover AgentCard| Supply Svc -->|A2A SendMessage / tool call| Assort Svc -->|A2A SendMessage / tool call| Supply AH -->|LLM: tool-calling| Ollama SH -->|LLM: tool-calling| Ollama Loading See docs/architecture.md for diagrams and the full request flow, and AGENTS.md… View on GitHub
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to