{"slug": "multi-agent-orchestration-in-net-using-a2a", "title": "Multi-Agent Orchestration in .NET Using A2A", "summary": "A developer demonstrated how to orchestrate a multi-agent system in .NET using the Agent-to-Agent (A2A) protocol, converting remote agents into AIFunction tools for an LLM-based orchestrator. The proof-of-concept dynamically registers agent cards, enabling parallel calls to specialized agents like an assortment specialist and a supply chain agent without code changes.", "body_md": "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.\n\nLet'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. \n\n  Our building blocks:\n\n``` php\ngraph LR\n    User([User]) --> Orch[Orchestrator]\n    Orch -. discover AgentCard .-> A[Assortment agent]\n    Orch -. discover AgentCard .-> S[SupplyChain agent]\n    Orch -- A2A SendMessage --> A\n    Orch -- A2A SendMessage --> S\n    A --> AT[(Catalog tools)]\n    S --> ST[(Stock tools)]\n```\n\n`IChatClient` (Azure OpenAI, Bedrock, OpenAI, etc.).\nMicrosoft provides an abstraction for the A2A spec for ASP.NET Core\n\n``` js\nvar agentCard = new AgentCard\n{\n  Name = \"AssortmentSpecialist\",\n  Description = \"Handles shop inventories, product categorizations, catalogs, and store assortments.\",\n  Skills =\n  [\n    new AgentSkill\n    {\n       Id = \"get-product\",\n       Name = \"GetProduct\",\n       Description = \"Look up a product's SKU, category, active status, and store coverage by name.\",\n    },\n  ],\n};\n\nbuilder.Services.AddA2AAgent<DomainAgentHandler>(agentCard);\n\nvar app = builder.Build();\napp.MapWellKnownAgentCard(agentCard, \"\");\napp.MapA2A(\"/\");\n```\n\nThe handler processes tasks using the LLM and the agent's own tools.\n\n```\npublic async Task ExecuteAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken ct)\n{\n  var responder = new MessageResponder(eventQueue, context.ContextId);\n\n  var messages = new List<ChatMessage>\n  {\n    new(ChatRole.System, \"You are the Assortment specialist. Use the tools to look up real data.\"),\n    new(ChatRole.User, context.UserText ?? string.Empty),\n  };\n\n  var options = new ChatOptions { Tools = [AIFunctionFactory.Create(tools.GetProduct)] };\n  var response = await chatClient.GetResponseAsync(messages, options, ct);\n\n  await responder.ReplyAsync(response.Text, ct);\n}\n```\n\nThere are several ways to orchestrate agents\n\nWe 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.\n\n```\npublic async Task<string> HandleAsync(ChatThread thread, string userMessage, CancellationToken ct)\n{\n  var agents = await registry.GetAgents(ct);\n  var tools = agents.Select(ToTool).Cast<AITool>().ToList();\n\n  var messages = new List<ChatMessage> { new(ChatRole.System, SystemPrompt) };\n  messages.Add(new ChatMessage(ChatRole.User, userMessage));\n\n  using var client = new FunctionInvokingChatClient(chatClient)\n  {\n     AllowConcurrentInvocation = true,\n  }.AsBuilder().Build();\n\n  var response = await client.GetResponseAsync(\n    messages,\n    new ChatOptions { Tools = tools, AllowMultipleToolCalls = true },\n    ct);\n\n  return response.Text;\n}\n```\n\nWe convert remote agents to `AIFunction` from AgentCard\n\n``` js\nprivate AIFunction ToTool(RemoteAgent agent)\n{\n  var dispatch = async (string request, CancellationToken ct) =>\n  {\n     var response = await agent.Client!.SendMessageAsync(request, Role.User, cancellationToken: ct);\n     return ExtractText(response);\n  };\n\n  return AIFunctionFactory.Create(dispatch, agent.Card!.Name,\n        $\"Ask the {agent.Card.Name} specialist. {agent.Card.Description}\");\n}\n```\n\nThe orchestrator resolves each card with A2ACardResolver, then turns it into a tool.\n\nA request that needs both agents makes the LLM call both agents in parallel and merge their replies into one.\n\n```\nsequenceDiagram\n    actor User\n    participant Orch as Orchestrator (LLM loop)\n    participant A as Assortment agent\n    participant S as SupplyChain agent\n\n    User->>Orch: \"Stores carrying the coat AND its stock?\"\n    par send both messages in parallel\n        Orch->>A: A2A SendMessage(sub-task)\n    and\n        Orch->>S: A2A SendMessage(sub-task)\n    end\n    A-->>Orch: catalog answer\n    S-->>Orch: stock answer\n    Orch->>Orch: merge results\n    Orch-->>User: one cohesive answer\n```\n\nA .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`).\n\n```\ngraph TB\n    Ollama[(\"Ollama<br/>llama3.2<br/>local LLM (external)\")]\n\n    User([User / Browser]) -->|HTTP| Orch\n\n    subgraph Orch[\"Orchestrator\"]\n        API[\"Minimal API + chat UI<br/>/api/chat\"]\n        Svc[\"OrchestrationService<br/>one tool-calling LLM loop\"]\n        Reg[\"AgentRegistry<br/>(AgentCards + A2AClients)\"]\n        Store[\"ChatStore<br/>(history by threadId)\"]\n        API --> Svc\n        Svc --> Reg\n        Svc --> Store\n    end\n\n    Svc -->|LLM: tool loop + synthesis| Ollama\n\n    subgraph Assort[\"AssortmentSpecialist (A2A server)\"]\n        AH[\"DomainAgentHandler\"]\n        AT[\"AssortmentTools<br/>GetProduct / GetActiveCatalog\"]\n        AH --> AT\n    end\n    subgraph Supply[\"SupplyChainAnalyst (A2A server)\"]\n        SH[\"DomainAgentHandler\"]\n        ST[\"SupplyChainTools<br/>GetStock / GetShipments\"]\n        SH --> ST\n    end\n\n    Reg -.->|discover AgentCard| Assort\n    Reg -.->|discover AgentCard| Supply\n    Svc -->|A2A SendMessage / tool call| Assort\n    Svc -->|A2A SendMessage / tool call| Supply\n    AH -->|LLM: tool-calling| Ollama\n    SH -->|LLM: tool-calling| Ollama\n```\n\nSee [`docs/architecture.md`](https://github.com/ohalay/a2a-poc/docs/architecture.md) for diagrams and the full request flow, and\n[`AGENTS.md`](https://github.com/ohalay/a2a-poc/AGENTS.md)…", "url": "https://wpnews.pro/news/multi-agent-orchestration-in-net-using-a2a", "canonical_source": "https://dev.to/ohalay/multi-agent-orchestration-in-net-using-a2a-4m5f", "published_at": "2026-09-08 20:10:36+00:00", "updated_at": "2026-09-08 20:22:26.444374+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "artificial-intelligence"], "entities": ["Microsoft", "Azure OpenAI", "Bedrock", "OpenAI", "A2A", ".NET", "AgentCard"], "alternates": {"html": "https://wpnews.pro/news/multi-agent-orchestration-in-net-using-a2a", "markdown": "https://wpnews.pro/news/multi-agent-orchestration-in-net-using-a2a.md", "text": "https://wpnews.pro/news/multi-agent-orchestration-in-net-using-a2a.txt", "jsonld": "https://wpnews.pro/news/multi-agent-orchestration-in-net-using-a2a.jsonld"}}