diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index fa54be567c..1678e4b2ae 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -101,8 +101,8 @@
-
-
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index af7ca9f0be..e6c842494a 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -281,6 +281,7 @@
+
diff --git a/dotnet/samples/04-hosting/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj b/dotnet/samples/04-hosting/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj
index 1bccc99d4f..d91b20e34b 100644
--- a/dotnet/samples/04-hosting/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj
+++ b/dotnet/samples/04-hosting/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj
@@ -2,7 +2,7 @@
Exe
- net10.0
+ net10.0
enable
enable
@@ -13,7 +13,6 @@
-
diff --git a/dotnet/samples/04-hosting/A2A/A2AAgent_PollingForTaskCompletion/Program.cs b/dotnet/samples/04-hosting/A2A/A2AAgent_PollingForTaskCompletion/Program.cs
index e1731604a9..9410785c39 100644
--- a/dotnet/samples/04-hosting/A2A/A2AAgent_PollingForTaskCompletion/Program.cs
+++ b/dotnet/samples/04-hosting/A2A/A2AAgent_PollingForTaskCompletion/Program.cs
@@ -18,8 +18,12 @@
AgentSession session = await agent.CreateSessionAsync();
+// AllowBackgroundResponses must be true so the server returns immediately with a continuation token
+// instead of blocking until the task is complete.
+AgentRunOptions options = new() { AllowBackgroundResponses = true };
+
// Start the initial run with a long-running task.
-AgentResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", session);
+AgentResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", session, options: options);
// Poll until the response is complete.
while (response.ContinuationToken is { } token)
diff --git a/dotnet/samples/04-hosting/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj b/dotnet/samples/04-hosting/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj
new file mode 100644
index 0000000000..e75368ea99
--- /dev/null
+++ b/dotnet/samples/04-hosting/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/A2A/A2AAgent_StreamReconnection/Program.cs b/dotnet/samples/04-hosting/A2A/A2AAgent_StreamReconnection/Program.cs
new file mode 100644
index 0000000000..0829faf8ed
--- /dev/null
+++ b/dotnet/samples/04-hosting/A2A/A2AAgent_StreamReconnection/Program.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens,
+// allowing recovery from stream interruptions without losing progress.
+
+using A2A;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
+
+// Initialize an A2ACardResolver to get an A2A agent card.
+A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
+
+// Get the agent card
+AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
+
+// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
+AIAgent agent = agentCard.AsAIAgent();
+
+AgentSession session = await agent.CreateSessionAsync();
+
+ResponseContinuationToken? continuationToken = null;
+
+await foreach (var update in agent.RunStreamingAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", session))
+{
+ // Saving the continuation token to be able to reconnect to the same response stream later.
+ // Note: Continuation tokens are only returned for long-running tasks. If the underlying A2A agent
+ // returns a message instead of a task, the continuation token will not be initialized.
+ // A2A agents do not support stream resumption from a specific point in the stream,
+ // but only reconnection to obtain the same response stream from the beginning.
+ // So, A2A agents will return an initialized continuation token in the first update
+ // representing the beginning of the stream, and it will be null in all subsequent updates.
+ if (update.ContinuationToken is { } token)
+ {
+ continuationToken = token;
+ }
+
+ // Imitating stream interruption
+ break;
+}
+
+// Reconnect to the same response stream using the continuation token obtained from the previous stream.
+// As a first update, the agent will return an update representing the current state of the response at the moment of calling
+// RunStreamingAsync with the same continuation token, followed by other updates until the end of the stream is reached.
+if (continuationToken is not null)
+{
+ await foreach (var update in agent.RunStreamingAsync(session, options: new() { ContinuationToken = continuationToken }))
+ {
+ if (!string.IsNullOrEmpty(update.Text))
+ {
+ Console.WriteLine(update);
+ }
+ }
+}
diff --git a/dotnet/samples/04-hosting/A2A/A2AAgent_StreamReconnection/README.md b/dotnet/samples/04-hosting/A2A/A2AAgent_StreamReconnection/README.md
new file mode 100644
index 0000000000..ca5b0b66ad
--- /dev/null
+++ b/dotnet/samples/04-hosting/A2A/A2AAgent_StreamReconnection/README.md
@@ -0,0 +1,29 @@
+# A2A Agent Stream Reconnection
+
+This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens, allowing recovery from stream interruptions without losing progress.
+
+The sample:
+
+- Connects to an A2A agent server specified in the `A2A_AGENT_HOST` environment variable
+- Sends a request to the agent and begins streaming the response
+- Captures a continuation token from the stream for later reconnection
+- Simulates a stream interruption by breaking out of the streaming loop
+- Reconnects to the same response stream using the captured continuation token
+- Displays the response received after reconnection
+
+This pattern is useful when network interruptions or other failures may disrupt an ongoing streaming response, and you need to recover and continue processing.
+
+> **Note:** Continuation tokens are only available when the underlying A2A agent returns a task. If the agent returns a message instead, the continuation token will not be initialized and stream reconnection is not applicable.
+
+# Prerequisites
+
+Before you begin, ensure you have the following prerequisites:
+
+- .NET 10.0 SDK or later
+- An A2A agent server running and accessible via HTTP
+
+Set the following environment variable:
+
+```powershell
+$env:A2A_AGENT_HOST="http://localhost:5000" # Replace with your A2A agent server host
+```
diff --git a/dotnet/samples/04-hosting/A2A/README.md b/dotnet/samples/04-hosting/A2A/README.md
index 55539a8322..2f161748df 100644
--- a/dotnet/samples/04-hosting/A2A/README.md
+++ b/dotnet/samples/04-hosting/A2A/README.md
@@ -15,6 +15,7 @@ See the README.md for each sample for the prerequisites for that sample.
|---|---|
|[A2A Agent As Function Tools](./A2AAgent_AsFunctionTools/)|This sample demonstrates how to represent an A2A agent as a set of function tools, where each function tool corresponds to a skill of the A2A agent, and register these function tools with another AI agent so it can leverage the A2A agent's skills.|
|[A2A Agent Polling For Task Completion](./A2AAgent_PollingForTaskCompletion/)|This sample demonstrates how to poll for long-running task completion using continuation tokens with an A2A agent.|
+|[A2A Agent Stream Reconnection](./A2AAgent_StreamReconnection/)|This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens, allowing recovery from stream interruptions.|
## Running the samples from the console
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/Program.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/Program.cs
index 0b9696e3a1..32cb743de9 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/Program.cs
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/Program.cs
@@ -62,12 +62,10 @@ private static async Task HandleCommandsAsync(CancellationToken cancellationToke
}
var agentResponse = await hostAgent.Agent!.RunAsync(message, session, cancellationToken: cancellationToken);
- foreach (var chatMessage in agentResponse.Messages)
- {
- Console.ForegroundColor = ConsoleColor.Cyan;
- Console.WriteLine($"\nAgent: {chatMessage.Text}");
- Console.ResetColor();
- }
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine($"\nAgent: {agentResponse.Text}");
+ Console.ResetColor();
}
}
catch (Exception ex)
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/HostAgentFactory.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/HostAgentFactory.cs
index 584b7db422..f3517e5d62 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/HostAgentFactory.cs
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/HostAgentFactory.cs
@@ -12,7 +12,7 @@ namespace A2AServer;
internal static class HostAgentFactory
{
- internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string agentName, IList? tools = null)
+ internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string agentName, string[] agentUrls, IList? tools = null)
{
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
@@ -24,16 +24,16 @@ internal static class HostAgentFactory
AgentCard agentCard = agentType.ToUpperInvariant() switch
{
- "INVOICE" => GetInvoiceAgentCard(),
- "POLICY" => GetPolicyAgentCard(),
- "LOGISTICS" => GetLogisticsAgentCard(),
+ "INVOICE" => GetInvoiceAgentCard(agentUrls),
+ "POLICY" => GetPolicyAgentCard(agentUrls),
+ "LOGISTICS" => GetLogisticsAgentCard(agentUrls),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
return new(agent, agentCard);
}
- internal static async Task<(AIAgent, AgentCard)> CreateChatCompletionHostAgentAsync(string agentType, string model, string apiKey, string name, string instructions, IList? tools = null)
+ internal static async Task<(AIAgent, AgentCard)> CreateChatCompletionHostAgentAsync(string agentType, string model, string apiKey, string name, string instructions, string[] agentUrls, IList? tools = null)
{
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(model)
@@ -41,9 +41,9 @@ internal static class HostAgentFactory
AgentCard agentCard = agentType.ToUpperInvariant() switch
{
- "INVOICE" => GetInvoiceAgentCard(),
- "POLICY" => GetPolicyAgentCard(),
- "LOGISTICS" => GetLogisticsAgentCard(),
+ "INVOICE" => GetInvoiceAgentCard(agentUrls),
+ "POLICY" => GetPolicyAgentCard(agentUrls),
+ "LOGISTICS" => GetLogisticsAgentCard(agentUrls),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
@@ -51,7 +51,7 @@ internal static class HostAgentFactory
}
#region private
- private static AgentCard GetInvoiceAgentCard()
+ private static AgentCard GetInvoiceAgentCard(string[] agentUrls)
{
var capabilities = new AgentCapabilities()
{
@@ -80,10 +80,11 @@ private static AgentCard GetInvoiceAgentCard()
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [invoiceQuery],
+ SupportedInterfaces = CreateAgentInterfaces(agentUrls)
};
}
- private static AgentCard GetPolicyAgentCard()
+ private static AgentCard GetPolicyAgentCard(string[] agentUrls)
{
var capabilities = new AgentCapabilities()
{
@@ -112,10 +113,11 @@ private static AgentCard GetPolicyAgentCard()
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [policyQuery],
+ SupportedInterfaces = CreateAgentInterfaces(agentUrls)
};
}
- private static AgentCard GetLogisticsAgentCard()
+ private static AgentCard GetLogisticsAgentCard(string[] agentUrls)
{
var capabilities = new AgentCapabilities()
{
@@ -144,7 +146,18 @@ private static AgentCard GetLogisticsAgentCard()
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [logisticsQuery],
+ SupportedInterfaces = CreateAgentInterfaces(agentUrls)
};
}
+
+ private static List CreateAgentInterfaces(string[] agentUrls)
+ {
+ return agentUrls.Select(url => new AgentInterface
+ {
+ Url = url,
+ ProtocolBinding = "JSONRPC",
+ ProtocolVersion = "1.0",
+ }).ToList();
+ }
#endregion
}
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs
index f1c0b966fe..a7680a2f88 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs
@@ -38,14 +38,15 @@
string? apiKey = configuration["OPENAI_API_KEY"];
string model = configuration["OPENAI_CHAT_MODEL_NAME"] ?? "gpt-4o-mini";
string? endpoint = configuration["AZURE_AI_PROJECT_ENDPOINT"];
+string[] agentUrls = (app.Configuration["urls"] ?? "http://localhost:5000").Split(';');
var invoiceQueryPlugin = new InvoiceQuery();
IList tools =
- [
+[
AIFunctionFactory.Create(invoiceQueryPlugin.QueryInvoices),
AIFunctionFactory.Create(invoiceQueryPlugin.QueryByTransactionId),
AIFunctionFactory.Create(invoiceQueryPlugin.QueryByInvoiceId)
- ];
+];
AIAgent hostA2AAgent;
AgentCard hostA2AAgentCard;
@@ -54,9 +55,9 @@
{
(hostA2AAgent, hostA2AAgentCard) = agentType.ToUpperInvariant() switch
{
- "INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName, tools),
- "POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName),
- "LOGISTICS" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName),
+ "INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName, agentUrls, tools),
+ "POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName, agentUrls),
+ "LOGISTICS" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName, agentUrls),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
}
@@ -68,7 +69,7 @@
agentType, model, apiKey, "InvoiceAgent",
"""
You specialize in handling queries related to invoices.
- """, tools),
+ """, agentUrls, tools),
"POLICY" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, model, apiKey, "PolicyAgent",
"""
@@ -84,7 +85,7 @@ You specialize in handling queries related to policies and customer communicatio
resolution in SAP CRM and notify the customer via email within 2 business days, referencing the
original invoice and the credit memo number. Use the 'Formal Credit Notification' email
template."
- """),
+ """, agentUrls),
"LOGISTICS" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, model, apiKey, "LogisticsAgent",
"""
@@ -95,7 +96,7 @@ You specialize in handling queries related to logistics.
Shipment number: SHPMT-SAP-001
Item: TSHIRT-RED-L
Quantity: 900
- """),
+ """, agentUrls),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
}
@@ -104,10 +105,9 @@ You specialize in handling queries related to logistics.
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentName must be provided");
}
-var a2aTaskManager = app.MapA2A(
+app.MapA2A(
hostA2AAgent,
path: "/",
- agentCard: hostA2AAgentCard,
- taskManager => app.MapWellKnownAgentCard(taskManager, "/"));
+ agentCard: hostA2AAgentCard);
await app.RunAsync();
diff --git a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/A2AAgentClient.cs b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/A2AAgentClient.cs
index f790ec0daa..d2c67d0ca5 100644
--- a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/A2AAgentClient.cs
+++ b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/A2AAgentClient.cs
@@ -43,20 +43,21 @@ public override async IAsyncEnumerable RunStreamingAsync(
{
// Convert all messages to A2A parts and create a single message
var parts = messages.ToParts();
- var a2aMessage = new AgentMessage
+ var a2aMessage = new Message
{
MessageId = Guid.NewGuid().ToString("N"),
ContextId = contextId,
- Role = MessageRole.User,
+ Role = Role.User,
Parts = parts
};
- var messageSendParams = new MessageSendParams { Message = a2aMessage };
+ var messageSendParams = new SendMessageRequest { Message = a2aMessage };
var a2aResponse = await a2aClient.SendMessageAsync(messageSendParams, cancellationToken);
// Handle different response types
- if (a2aResponse is AgentMessage message)
+ if (a2aResponse.PayloadCase == SendMessageResponseCase.Message)
{
+ var message = a2aResponse.Message!;
var responseMessage = message.ToChatMessage();
if (responseMessage is { Contents.Count: > 0 })
{
@@ -67,9 +68,10 @@ public override async IAsyncEnumerable RunStreamingAsync(
});
}
}
- else if (a2aResponse is AgentTask agentTask)
+ else if (a2aResponse.PayloadCase == SendMessageResponseCase.Task)
{
// Manually convert AgentTask artifacts to ChatMessages since the extension method is internal
+ var agentTask = a2aResponse.Task!;
if (agentTask.Artifacts is not null)
{
foreach (var artifact in agentTask.Artifacts)
diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
index 9d98857e9b..f2eaada02f 100644
--- a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Net.ServerSentEvents;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
@@ -100,64 +99,47 @@ protected override async Task RunCoreAsync(IEnumerable 0 } taskMessages)
- {
- response.Messages = taskMessages;
- }
+ UpdateSession(typedSession, agentTask.ContextId, agentTask.Id);
- return response;
+ return this.ConvertToAgentResponse(agentTask);
}
- throw new NotSupportedException($"Only Message and AgentTask responses are supported from A2A agents. Received: {a2aResponse.GetType().FullName ?? "null"}");
+ throw new NotSupportedException($"Only Message and AgentTask responses are supported from A2A agents. Received: {a2aResponse.PayloadCase}");
}
///
@@ -169,59 +151,61 @@ protected override async IAsyncEnumerable RunCoreStreamingA
this._logger.LogA2AAgentInvokingAgent(nameof(RunStreamingAsync), this.Id, this.Name);
- ConfiguredCancelableAsyncEnumerable> a2aSseEvents;
+ ConfiguredCancelableAsyncEnumerable streamEvents;
- if (options?.ContinuationToken is not null)
+ if (GetContinuationToken(messages, options) is { } token)
{
- // Task stream resumption is not well defined in the A2A v2.* specification, leaving it to the agent implementations.
- // The v3.0 specification improves this by defining task stream reconnection that allows obtaining the same stream
- // from the beginning, but it does not define stream resumption from a specific point in the stream.
- // Therefore, the code should be updated once the A2A .NET library supports the A2A v3.0 specification,
- // and AF has the necessary model to allow consumers to know whether they need to resume the stream and add new updates to
- // the existing ones or reconnect the stream and obtain all updates again.
- // For more details, see the following issue: https://github.com/microsoft/agent-framework/issues/1764
- throw new InvalidOperationException("Reconnecting to task streams using continuation tokens is not supported yet.");
- // a2aSseEvents = this._a2aClient.SubscribeToTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
+ streamEvents = this._a2aClient.SubscribeToTaskAsync(new SubscribeToTaskRequest { Id = token.TaskId }, cancellationToken).ConfigureAwait(false);
}
-
- MessageSendParams sendParams = new()
+ else
{
- Message = CreateA2AMessage(typedSession, messages),
- Metadata = options?.AdditionalProperties?.ToA2AMetadata()
- };
+ SendMessageRequest sendParams = new()
+ {
+ Message = CreateA2AMessage(typedSession, messages),
+ Metadata = options?.AdditionalProperties?.ToA2AMetadata()
+ };
- a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(sendParams, cancellationToken).ConfigureAwait(false);
+ streamEvents = this._a2aClient.SendStreamingMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
+ }
this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name);
string? contextId = null;
string? taskId = null;
- await foreach (var sseEvent in a2aSseEvents)
+ await foreach (var streamResponse in streamEvents)
{
- if (sseEvent.Data is AgentMessage message)
- {
- contextId = message.ContextId;
-
- yield return this.ConvertToAgentResponseUpdate(message);
- }
- else if (sseEvent.Data is AgentTask task)
+ switch (streamResponse.PayloadCase)
{
- contextId = task.ContextId;
- taskId = task.Id;
-
- yield return this.ConvertToAgentResponseUpdate(task);
- }
- else if (sseEvent.Data is TaskUpdateEvent taskUpdateEvent)
- {
- contextId = taskUpdateEvent.ContextId;
- taskId = taskUpdateEvent.TaskId;
-
- yield return this.ConvertToAgentResponseUpdate(taskUpdateEvent);
- }
- else
- {
- throw new NotSupportedException($"Only message, task, task update events are supported from A2A agents. Received: {sseEvent.Data.GetType().FullName ?? "null"}");
+ case StreamResponseCase.Message:
+ var message = streamResponse.Message!;
+ contextId = message.ContextId;
+ yield return this.ConvertToAgentResponseUpdate(message);
+ break;
+
+ case StreamResponseCase.Task:
+ var task = streamResponse.Task!;
+ contextId = task.ContextId;
+ taskId = task.Id;
+ yield return this.ConvertToAgentResponseUpdate(task);
+ break;
+
+ case StreamResponseCase.StatusUpdate:
+ var statusUpdate = streamResponse.StatusUpdate!;
+ contextId = statusUpdate.ContextId;
+ taskId = statusUpdate.TaskId;
+ yield return this.ConvertToAgentResponseUpdate(statusUpdate);
+ break;
+
+ case StreamResponseCase.ArtifactUpdate:
+ var artifactUpdate = streamResponse.ArtifactUpdate!;
+ contextId = artifactUpdate.ContextId;
+ taskId = artifactUpdate.TaskId;
+ yield return this.ConvertToAgentResponseUpdate(artifactUpdate);
+ break;
+
+ default:
+ throw new NotSupportedException($"Only message, task, task update events are supported from A2A agents. Received: {streamResponse.PayloadCase}");
}
}
@@ -284,7 +268,7 @@ private static void UpdateSession(A2AAgentSession? session, string? contextId, s
session.TaskId = taskId;
}
- private static AgentMessage CreateA2AMessage(A2AAgentSession typedSession, IEnumerable messages)
+ private static Message CreateA2AMessage(A2AAgentSession typedSession, IEnumerable messages)
{
var a2aMessage = messages.ToA2AMessage();
@@ -324,7 +308,34 @@ private static AgentMessage CreateA2AMessage(A2AAgentSession typedSession, IEnum
return null;
}
- private AgentResponseUpdate ConvertToAgentResponseUpdate(AgentMessage message)
+ private AgentResponse ConvertToAgentResponse(Message message)
+ {
+ return new AgentResponse
+ {
+ AgentId = this.Id,
+ ResponseId = message.MessageId,
+ FinishReason = ChatFinishReason.Stop,
+ RawRepresentation = message,
+ Messages = [message.ToChatMessage()],
+ AdditionalProperties = message.Metadata?.ToAdditionalProperties(),
+ };
+ }
+
+ private AgentResponse ConvertToAgentResponse(AgentTask task)
+ {
+ return new AgentResponse
+ {
+ AgentId = this.Id,
+ ResponseId = task.Id,
+ FinishReason = MapTaskStateToFinishReason(task.Status.State),
+ RawRepresentation = task,
+ Messages = task.ToChatMessages() ?? [],
+ ContinuationToken = CreateContinuationToken(task.Id, task.Status.State),
+ AdditionalProperties = task.Metadata?.ToAdditionalProperties(),
+ };
+ }
+
+ private AgentResponseUpdate ConvertToAgentResponseUpdate(Message message)
{
return new AgentResponseUpdate
{
@@ -349,32 +360,35 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AgentTask task)
RawRepresentation = task,
Role = ChatRole.Assistant,
Contents = task.ToAIContents(),
+ ContinuationToken = CreateContinuationToken(task.Id, task.Status.State),
AdditionalProperties = task.Metadata?.ToAdditionalProperties(),
};
}
- private AgentResponseUpdate ConvertToAgentResponseUpdate(TaskUpdateEvent taskUpdateEvent)
+ private AgentResponseUpdate ConvertToAgentResponseUpdate(TaskStatusUpdateEvent statusUpdateEvent)
{
- AgentResponseUpdate responseUpdate = new()
+ return new AgentResponseUpdate
{
AgentId = this.Id,
- ResponseId = taskUpdateEvent.TaskId,
- RawRepresentation = taskUpdateEvent,
+ ResponseId = statusUpdateEvent.TaskId,
+ RawRepresentation = statusUpdateEvent,
Role = ChatRole.Assistant,
- AdditionalProperties = taskUpdateEvent.Metadata?.ToAdditionalProperties() ?? [],
+ FinishReason = MapTaskStateToFinishReason(statusUpdateEvent.Status.State),
+ AdditionalProperties = statusUpdateEvent.Metadata?.ToAdditionalProperties() ?? [],
};
+ }
- if (taskUpdateEvent is TaskArtifactUpdateEvent artifactUpdateEvent)
- {
- responseUpdate.Contents = artifactUpdateEvent.Artifact.ToAIContents();
- responseUpdate.RawRepresentation = artifactUpdateEvent;
- }
- else if (taskUpdateEvent is TaskStatusUpdateEvent statusUpdateEvent)
+ private AgentResponseUpdate ConvertToAgentResponseUpdate(TaskArtifactUpdateEvent artifactUpdateEvent)
+ {
+ return new AgentResponseUpdate
{
- responseUpdate.FinishReason = MapTaskStateToFinishReason(statusUpdateEvent.Status.State);
- }
-
- return responseUpdate;
+ AgentId = this.Id,
+ ResponseId = artifactUpdateEvent.TaskId,
+ RawRepresentation = artifactUpdateEvent,
+ Role = ChatRole.Assistant,
+ Contents = artifactUpdateEvent.Artifact.ToAIContents(),
+ AdditionalProperties = artifactUpdateEvent.Metadata?.ToAdditionalProperties() ?? [],
+ };
}
private static ChatFinishReason? MapTaskStateToFinishReason(TaskState state)
diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs
index 1998d020b5..bda0290af5 100644
--- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Net.Http;
using Microsoft.Agents.AI;
using Microsoft.Extensions.Logging;
@@ -26,11 +28,27 @@ public static class A2AAgentCardExtensions
/// The to use for the agent creation.
/// The to use for HTTP requests.
/// The logger factory for enabling logging within the agent.
+ ///
+ /// An optional callback to select which to use from the card's
+ /// . When not provided, the first interface is used.
+ ///
/// An instance backed by the A2A agent.
- public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null)
+ public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, Func, AgentInterface>? interfaceSelector = null)
{
+ var interfaces = card.SupportedInterfaces
+ ?? throw new InvalidOperationException("The AgentCard does not have any SupportedInterfaces.");
+
+ // Use the provided selector or default to the first interface.
+ var selectedInterface = interfaceSelector is not null
+ ? interfaceSelector(interfaces)
+ : interfaces.FirstOrDefault()
+ ?? throw new InvalidOperationException("The AgentCard does not have any SupportedInterfaces with a URL.");
+
+ var url = selectedInterface.Url
+ ?? throw new InvalidOperationException("The selected AgentInterface does not have a URL.");
+
// Create the A2A client using the agent URL from the card.
- var a2aClient = new A2AClient(new Uri(card.Url), httpClient);
+ var a2aClient = new A2AClient(new Uri(url), httpClient);
return a2aClient.AsAIAgent(name: card.Name, description: card.Description, loggerFactory: loggerFactory);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/ChatMessageExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/ChatMessageExtensions.cs
index b1f1bd643a..dd0749ecc9 100644
--- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/ChatMessageExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/ChatMessageExtensions.cs
@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.AI;
///
internal static class ChatMessageExtensions
{
- internal static AgentMessage ToA2AMessage(this IEnumerable messages)
+ internal static Message ToA2AMessage(this IEnumerable messages)
{
List allParts = [];
@@ -23,10 +23,10 @@ internal static AgentMessage ToA2AMessage(this IEnumerable messages
}
}
- return new AgentMessage
+ return new Message
{
MessageId = Guid.NewGuid().ToString("N"),
- Role = MessageRole.User,
+ Role = Role.User,
Parts = allParts,
};
}
diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj b/dotnet/src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj
index b1b9ba7671..4e92826f56 100644
--- a/dotnet/src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj
@@ -1,6 +1,7 @@
+ $(TargetFrameworksCore)
preview
$(NoWarn);MEAI001
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs
index af3ff093ee..ffd82f50b6 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs
@@ -27,14 +27,17 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// The to add the A2A endpoints to.
/// The configuration builder for .
/// The route group to use for A2A endpoints.
- /// Configured for A2A integration.
+ /// An for further endpoint configuration.
///
/// This method can be used to access A2A agents that support the
/// Curated Registries (Catalog-Based Discovery)
/// discovery mechanism.
///
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path)
- => endpoints.MapA2A(agentBuilder, path, _ => { });
+ {
+ ArgumentNullException.ThrowIfNull(agentBuilder);
+ return endpoints.MapA2A(agentBuilder.Name, path);
+ }
///
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
@@ -43,7 +46,7 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The configuration builder for .
/// The route group to use for A2A endpoints.
/// Controls the response behavior of the agent run.
- /// Configured for A2A integration.
+ /// An for further endpoint configuration.
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentRunMode agentRunMode)
{
ArgumentNullException.ThrowIfNull(agentBuilder);
@@ -56,42 +59,12 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The to add the A2A endpoints to.
/// The name of the agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
- /// Configured for A2A integration.
+ /// An for further endpoint configuration.
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
- => endpoints.MapA2A(agentName, path, _ => { });
-
- ///
- /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
- ///
- /// The to add the A2A endpoints to.
- /// The name of the agent to use for A2A protocol integration.
- /// The route group to use for A2A endpoints.
- /// Controls the response behavior of the agent run.
- /// Configured for A2A integration.
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentRunMode agentRunMode)
{
ArgumentNullException.ThrowIfNull(endpoints);
var agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentName);
- return endpoints.MapA2A(agent, path, _ => { }, agentRunMode);
- }
-
- ///
- /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
- ///
- /// The to add the A2A endpoints to.
- /// The configuration builder for .
- /// The route group to use for A2A endpoints.
- /// The callback to configure .
- /// Configured for A2A integration.
- ///
- /// This method can be used to access A2A agents that support the
- /// Curated Registries (Catalog-Based Discovery)
- /// discovery mechanism.
- ///
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, Action configureTaskManager)
- {
- ArgumentNullException.ThrowIfNull(agentBuilder);
- return endpoints.MapA2A(agentBuilder.Name, path, configureTaskManager);
+ return endpoints.MapA2A(agent, path);
}
///
@@ -100,13 +73,13 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The to add the A2A endpoints to.
/// The name of the agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
- /// The callback to configure .
- /// Configured for A2A integration.
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, Action configureTaskManager)
+ /// Controls the response behavior of the agent run.
+ /// An for further endpoint configuration.
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentRunMode agentRunMode)
{
ArgumentNullException.ThrowIfNull(endpoints);
var agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentName);
- return endpoints.MapA2A(agent, path, configureTaskManager);
+ return endpoints.MapA2A(agent, path, agentRunMode);
}
///
@@ -115,15 +88,18 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The to add the A2A endpoints to.
/// The configuration builder for .
/// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
- /// Configured for A2A integration.
+ /// Agent card describing the agent's capabilities for discovery.
+ /// An for further endpoint configuration.
///
/// This method can be used to access A2A agents that support the
/// Curated Registries (Catalog-Based Discovery)
/// discovery mechanism.
///
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentCard agentCard)
- => endpoints.MapA2A(agentBuilder, path, agentCard, _ => { });
+ {
+ ArgumentNullException.ThrowIfNull(agentBuilder);
+ return endpoints.MapA2A(agentBuilder.Name, path, agentCard);
+ }
///
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
@@ -131,15 +107,19 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The to add the A2A endpoints to.
/// The name of the agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
- /// Configured for A2A integration.
+ /// Agent card describing the agent's capabilities for discovery.
+ /// An for further endpoint configuration.
///
/// This method can be used to access A2A agents that support the
/// Curated Registries (Catalog-Based Discovery)
/// discovery mechanism.
///
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
- => endpoints.MapA2A(agentName, path, agentCard, _ => { });
+ {
+ ArgumentNullException.ThrowIfNull(endpoints);
+ var agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentName);
+ return endpoints.MapA2A(agent, path, agentCard);
+ }
///
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
@@ -147,9 +127,9 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The to add the A2A endpoints to.
/// The configuration builder for .
/// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
+ /// Agent card describing the agent's capabilities for discovery.
/// Controls the response behavior of the agent run.
- /// Configured for A2A integration.
+ /// An for further endpoint configuration.
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentCard agentCard, AgentRunMode agentRunMode)
{
ArgumentNullException.ThrowIfNull(agentBuilder);
@@ -162,9 +142,9 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The to add the A2A endpoints to.
/// The name of the agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
+ /// Agent card describing the agent's capabilities for discovery.
/// Controls the response behavior of the agent run.
- /// Configured for A2A integration.
+ /// An for further endpoint configuration.
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, AgentRunMode agentRunMode)
{
ArgumentNullException.ThrowIfNull(endpoints);
@@ -172,74 +152,15 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
return endpoints.MapA2A(agent, path, agentCard, agentRunMode);
}
- ///
- /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
- ///
- /// The to add the A2A endpoints to.
- /// The configuration builder for .
- /// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
- /// The callback to configure .
- /// Configured for A2A integration.
- ///
- /// This method can be used to access A2A agents that support the
- /// Curated Registries (Catalog-Based Discovery)
- /// discovery mechanism.
- ///
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentCard agentCard, Action configureTaskManager)
- {
- ArgumentNullException.ThrowIfNull(agentBuilder);
- return endpoints.MapA2A(agentBuilder.Name, path, agentCard, configureTaskManager);
- }
-
- ///
- /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
- ///
- /// The to add the A2A endpoints to.
- /// The name of the agent to use for A2A protocol integration.
- /// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
- /// The callback to configure .
- /// Configured for A2A integration.
- ///
- /// This method can be used to access A2A agents that support the
- /// Curated Registries (Catalog-Based Discovery)
- /// discovery mechanism.
- ///
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action configureTaskManager)
- => endpoints.MapA2A(agentName, path, agentCard, configureTaskManager, AgentRunMode.DisallowBackground);
-
- ///
- /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
- ///
- /// The to add the A2A endpoints to.
- /// The name of the agent to use for A2A protocol integration.
- /// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
- /// The callback to configure .
- /// Controls the response behavior of the agent run.
- /// Configured for A2A integration.
- ///
- /// This method can be used to access A2A agents that support the
- /// Curated Registries (Catalog-Based Discovery)
- /// discovery mechanism.
- ///
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action configureTaskManager, AgentRunMode agentRunMode)
- {
- ArgumentNullException.ThrowIfNull(endpoints);
- var agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentName);
- return endpoints.MapA2A(agent, path, agentCard, configureTaskManager, agentRunMode);
- }
-
///
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
///
/// The to add the A2A endpoints to.
/// The agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
- /// Configured for A2A integration.
+ /// An for further endpoint configuration.
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
- => endpoints.MapA2A(agent, path, _ => { });
+ => endpoints.MapA2A(agent, path, AgentRunMode.DisallowBackground);
///
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
@@ -248,42 +169,16 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
/// Controls the response behavior of the agent run.
- /// Configured for A2A integration.
+ /// An for further endpoint configuration.
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentRunMode agentRunMode)
- => endpoints.MapA2A(agent, path, _ => { }, agentRunMode);
-
- ///
- /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
- ///
- /// The to add the A2A endpoints to.
- /// The agent to use for A2A protocol integration.
- /// The route group to use for A2A endpoints.
- /// The callback to configure .
- /// Configured for A2A integration.
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action configureTaskManager)
- => endpoints.MapA2A(agent, path, configureTaskManager, AgentRunMode.DisallowBackground);
-
- ///
- /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
- ///
- /// The to add the A2A endpoints to.
- /// The agent to use for A2A protocol integration.
- /// The route group to use for A2A endpoints.
- /// The callback to configure .
- /// Controls the response behavior of the agent run.
- /// Configured for A2A integration.
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action configureTaskManager, AgentRunMode agentRunMode)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(agent);
var loggerFactory = endpoints.ServiceProvider.GetRequiredService();
var agentSessionStore = endpoints.ServiceProvider.GetKeyedService(agent.Name);
- var taskManager = agent.MapA2A(loggerFactory: loggerFactory, agentSessionStore: agentSessionStore, runMode: agentRunMode);
- var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
-
- configureTaskManager(taskManager);
- return endpointConventionBuilder;
+ var handler = agent.MapA2A(loggerFactory: loggerFactory, agentSessionStore: agentSessionStore, runMode: agentRunMode);
+ return A2ARouteBuilderExtensions.MapA2A(endpoints, handler, path);
}
///
@@ -292,15 +187,15 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The to add the A2A endpoints to.
/// The agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
- /// Configured for A2A integration.
+ /// Agent card describing the agent's capabilities for discovery.
+ /// An for further endpoint configuration.
///
/// This method can be used to access A2A agents that support the
/// Curated Registries (Catalog-Based Discovery)
/// discovery mechanism.
///
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
- => endpoints.MapA2A(agent, path, agentCard, _ => { });
+ => endpoints.MapA2A(agent, path, agentCard, AgentRunMode.DisallowBackground);
///
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
@@ -308,78 +203,52 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo
/// The to add the A2A endpoints to.
/// The agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
+ /// Agent card describing the agent's capabilities for discovery.
/// Controls the response behavior of the agent run.
- /// Configured for A2A integration.
+ /// An for further endpoint configuration.
///
/// This method can be used to access A2A agents that support the
/// Curated Registries (Catalog-Based Discovery)
/// discovery mechanism.
///
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, AgentRunMode agentRunMode)
- => endpoints.MapA2A(agent, path, agentCard, _ => { }, agentRunMode);
-
- ///
- /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
- ///
- /// The to add the A2A endpoints to.
- /// The agent to use for A2A protocol integration.
- /// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
- /// The callback to configure .
- /// Configured for A2A integration.
- ///
- /// This method can be used to access A2A agents that support the
- /// Curated Registries (Catalog-Based Discovery)
- /// discovery mechanism.
- ///
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action configureTaskManager)
- => endpoints.MapA2A(agent, path, agentCard, configureTaskManager, AgentRunMode.DisallowBackground);
-
- ///
- /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
- ///
- /// The to add the A2A endpoints to.
- /// The agent to use for A2A protocol integration.
- /// The route group to use for A2A endpoints.
- /// Agent card info to return on query.
- /// The callback to configure .
- /// Controls the response behavior of the agent run.
- /// Configured for A2A integration.
- ///
- /// This method can be used to access A2A agents that support the
- /// Curated Registries (Catalog-Based Discovery)
- /// discovery mechanism.
- ///
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action configureTaskManager, AgentRunMode agentRunMode)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(agent);
var loggerFactory = endpoints.ServiceProvider.GetRequiredService();
var agentSessionStore = endpoints.ServiceProvider.GetKeyedService(agent.Name);
- var taskManager = agent.MapA2A(agentCard: agentCard, agentSessionStore: agentSessionStore, loggerFactory: loggerFactory, runMode: agentRunMode);
- var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
-
- configureTaskManager(taskManager);
+ var handler = agent.MapA2A(loggerFactory: loggerFactory, agentSessionStore: agentSessionStore, runMode: agentRunMode);
+ A2ARouteBuilderExtensions.MapA2A(endpoints, handler, path);
+ endpoints.MapWellKnownAgentCard(agentCard);
+ return endpoints.MapHttpA2A(handler, agentCard);
+ }
- return endpointConventionBuilder;
+ ///
+ /// Maps A2A JSON-RPC communication endpoints to the specified path using a pre-configured request handler.
+ ///
+ /// The to add the A2A endpoints to.
+ /// Pre-configured for handling A2A requests.
+ /// The route group to use for A2A endpoints.
+ /// An for further endpoint configuration.
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IA2ARequestHandler handler, string path)
+ {
+ return A2ARouteBuilderExtensions.MapA2A(endpoints, handler, path);
}
///
- /// Maps HTTP A2A communication endpoints to the specified path using the provided TaskManager.
- /// TaskManager should be preconfigured before calling this method.
+ /// Maps A2A communication endpoints including JSON-RPC, well-known agent card, and REST API
+ /// to the specified path using a pre-configured request handler.
///
/// The to add the A2A endpoints to.
- /// Pre-configured A2A TaskManager to use for A2A endpoints handling.
+ /// Pre-configured for handling A2A requests.
/// The route group to use for A2A endpoints.
- /// Configured for A2A integration.
- public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, ITaskManager taskManager, string path)
+ /// Agent card describing the agent's capabilities for discovery.
+ /// An for further endpoint configuration.
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IA2ARequestHandler handler, string path, AgentCard agentCard)
{
- // note: current SDK version registers multiple `.well-known/agent.json` handlers here.
- // it makes app return HTTP 500, but will be fixed once new A2A SDK is released.
- // see https://github.com/microsoft/agent-framework/issues/476 for details
- A2ARouteBuilderExtensions.MapA2A(endpoints, taskManager, path);
- return endpoints.MapHttpA2A(taskManager, path);
+ A2ARouteBuilderExtensions.MapA2A(endpoints, handler, path);
+ endpoints.MapWellKnownAgentCard(agentCard);
+ return endpoints.MapHttpA2A(handler, agentCard);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2ARunDecisionContext.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2ARunDecisionContext.cs
index 6ff49f6ecb..9928aaf2f8 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2ARunDecisionContext.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2ARunDecisionContext.cs
@@ -9,13 +9,13 @@ namespace Microsoft.Agents.AI.Hosting.A2A;
///
public sealed class A2ARunDecisionContext
{
- internal A2ARunDecisionContext(MessageSendParams messageSendParams)
+ internal A2ARunDecisionContext(SendMessageRequest sendMessageRequest)
{
- this.MessageSendParams = messageSendParams;
+ this.SendMessageRequest = sendMessageRequest;
}
///
- /// Gets the parameters of the incoming A2A message that triggered this run.
+ /// Gets the that triggered this run.
///
- public MessageSendParams MessageSendParams { get; }
+ public SendMessageRequest SendMessageRequest { get; }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
index 31c520755f..8ad7dab668 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
@@ -10,6 +11,7 @@
using Microsoft.Agents.AI.Hosting.A2A.Converters;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI.Hosting.A2A;
@@ -28,15 +30,15 @@ public static class AIAgentExtensions
/// Attaches A2A (Agent2Agent) messaging capabilities via Message processing to the specified .
///
/// Agent to attach A2A messaging processing capabilities to.
- /// Instance of to configure for A2A messaging. New instance will be created if not passed.
+ /// Instance of for task persistence. A new will be created if not passed.
/// The logger factory to use for creating instances.
/// The store to store session contents and metadata.
/// Controls the response behavior of the agent run.
/// Optional for serializing and deserializing continuation tokens. Use this when the agent's continuation token contains custom types not registered in the default options. Falls back to if not provided.
- /// The configured .
- public static ITaskManager MapA2A(
+ /// The configured .
+ public static IA2ARequestHandler MapA2A(
this AIAgent agent,
- ITaskManager? taskManager = null,
+ ITaskStore? taskStore = null,
ILoggerFactory? loggerFactory = null,
AgentSessionStore? agentSessionStore = null,
AgentRunMode? runMode = null,
@@ -46,264 +48,282 @@ public static ITaskManager MapA2A(
ArgumentNullException.ThrowIfNull(agent.Name);
runMode ??= AgentRunMode.DisallowBackground;
+ taskStore ??= new InMemoryTaskStore();
var hostAgent = new AIHostAgent(
innerAgent: agent,
sessionStore: agentSessionStore ?? new NoopAgentSessionStore());
- taskManager ??= new TaskManager();
-
// Resolve the JSON serializer options for continuation token serialization. May be custom for the user's agent.
JsonSerializerOptions continuationTokenJsonOptions = jsonSerializerOptions ?? A2AHostingJsonUtilities.DefaultOptions;
- // OnMessageReceived handles both message-only and task-based flows.
- // The A2A SDK prioritizes OnMessageReceived over OnTaskCreated when both are set,
- // so we consolidate all initial message handling here and return either
- // an AgentMessage or AgentTask depending on the agent response.
- // When the agent returns a ContinuationToken (long-running operation), a task is
- // created for stateful tracking. Otherwise a lightweight AgentMessage is returned.
- // See https://github.com/a2aproject/a2a-dotnet/issues/275
- taskManager.OnMessageReceived += (p, ct) => OnMessageReceivedAsync(p, hostAgent, runMode, taskManager, continuationTokenJsonOptions, ct);
-
- // Task flow for subsequent updates and cancellations
- taskManager.OnTaskUpdated += (t, ct) => OnTaskUpdatedAsync(t, hostAgent, taskManager, continuationTokenJsonOptions, ct);
- taskManager.OnTaskCancelled += OnTaskCancelledAsync;
+ // Wrap the task store to inject pending metadata (continuation tokens, history) during task
+ // materialization. The A2AServer runs the handler concurrently with event materialization,
+ // so the handler cannot directly access/modify tasks via the store during execution.
+ var wrappedTaskStore = new MetadataInjectingTaskStore(taskStore);
- return taskManager;
+ var handler = new AIAgentA2AHandler(hostAgent, runMode, wrappedTaskStore, continuationTokenJsonOptions);
+ var logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance;
+ return new A2AServer(handler, wrappedTaskStore, new ChannelEventNotifier(), logger, null);
}
- ///
- /// Attaches A2A (Agent2Agent) messaging capabilities via Message processing to the specified .
- ///
- /// Agent to attach A2A messaging processing capabilities to.
- /// The agent card to return on query.
- /// Instance of to configure for A2A messaging. New instance will be created if not passed.
- /// The logger factory to use for creating instances.
- /// The store to store session contents and metadata.
- /// Controls the response behavior of the agent run.
- /// Optional for serializing and deserializing continuation tokens. Use this when the agent's continuation token contains custom types not registered in the default options. Falls back to if not provided.
- /// The configured .
- public static ITaskManager MapA2A(
- this AIAgent agent,
- AgentCard agentCard,
- ITaskManager? taskManager = null,
- ILoggerFactory? loggerFactory = null,
- AgentSessionStore? agentSessionStore = null,
- AgentRunMode? runMode = null,
- JsonSerializerOptions? jsonSerializerOptions = null)
- {
- taskManager = agent.MapA2A(taskManager, loggerFactory, agentSessionStore, runMode, jsonSerializerOptions);
-
- taskManager.OnAgentCardQuery += (context, query) =>
+ private static Message CreateMessageFromResponse(string contextId, AgentResponse response) =>
+ new()
{
- // A2A SDK assigns the url on its own
- // we can help user if they did not set Url explicitly.
- if (string.IsNullOrEmpty(agentCard.Url))
- {
- agentCard.Url = context.TrimEnd('/');
- }
+ MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
+ ContextId = contextId,
+ Role = Role.Agent,
+ Parts = response.Messages.ToParts(),
+ Metadata = response.AdditionalProperties?.ToA2AMetadata()
+ };
- return Task.FromResult(agentCard);
+ // Task outputs should be returned as artifacts rather than messages:
+ // https://a2a-protocol.org/latest/specification/#37-messages-and-artifacts
+ private static Artifact CreateArtifactFromResponse(AgentResponse response) =>
+ new()
+ {
+ ArtifactId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
+ Parts = response.Messages.ToParts(),
+ Metadata = response.AdditionalProperties?.ToA2AMetadata()
};
- return taskManager;
+
+ private static void StoreContinuationToken(
+ AgentTask agentTask,
+ ResponseContinuationToken token,
+ JsonSerializerOptions continuationTokenJsonOptions)
+ {
+ agentTask.Metadata ??= [];
+ agentTask.Metadata[ContinuationTokenMetadataKey] = JsonSerializer.SerializeToElement(
+ token,
+ continuationTokenJsonOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
}
- private static async Task OnMessageReceivedAsync(
- MessageSendParams messageSendParams,
- AIHostAgent hostAgent,
- AgentRunMode runMode,
- ITaskManager taskManager,
- JsonSerializerOptions continuationTokenJsonOptions,
- CancellationToken cancellationToken)
+ private static List ExtractChatMessagesFromTaskHistory(AgentTask? agentTask)
{
- // AIAgent does not support resuming from arbitrary prior tasks.
- // Throw explicitly so the client gets a clear error rather than a response
- // that silently ignores the referenced task context.
- // Follow-ups on the *same* task are handled via OnTaskUpdated instead.
- if (messageSendParams.Message.ReferenceTaskIds is { Count: > 0 })
+ if (agentTask?.History is not { Count: > 0 })
{
- throw new NotSupportedException("ReferenceTaskIds is not supported. AIAgent cannot resume from arbitrary prior task context. Use OnTaskUpdated for follow-ups on the same task.");
+ return [];
}
- var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
- var session = await hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
-
- // Decide whether to run in background based on user preferences and agent capabilities
- var decisionContext = new A2ARunDecisionContext(messageSendParams);
- var allowBackgroundResponses = await runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false);
-
- var options = messageSendParams.Metadata is not { Count: > 0 }
- ? new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses }
- : new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = messageSendParams.Metadata.ToAdditionalProperties() };
-
- var response = await hostAgent.RunAsync(
- messageSendParams.ToChatMessages(),
- session: session,
- options: options,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- await hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
-
- if (response.ContinuationToken is null)
+ var chatMessages = new List(agentTask.History.Count);
+ foreach (var message in agentTask.History)
{
- return CreateMessageFromResponse(contextId, response);
+ chatMessages.Add(message.ToChatMessage());
}
- var agentTask = await InitializeTaskAsync(contextId, messageSendParams.Message, taskManager, cancellationToken).ConfigureAwait(false);
- StoreContinuationToken(agentTask, response.ContinuationToken, continuationTokenJsonOptions);
- await TransitionToWorkingAsync(agentTask.Id, contextId, response, taskManager, cancellationToken).ConfigureAwait(false);
- return agentTask;
+ return chatMessages;
}
- private static async Task OnTaskUpdatedAsync(
- AgentTask agentTask,
- AIHostAgent hostAgent,
- ITaskManager taskManager,
- JsonSerializerOptions continuationTokenJsonOptions,
- CancellationToken cancellationToken)
+ ///
+ /// Wraps an to apply pending modifications (continuation tokens,
+ /// message history) when the A2AServer materializes task events. This is needed because
+ /// the A2AServer runs handler execution concurrently with event materialization, so the
+ /// handler cannot directly modify tasks in the store during execution.
+ ///
+ private sealed class MetadataInjectingTaskStore : ITaskStore
{
- var contextId = agentTask.ContextId ?? Guid.NewGuid().ToString("N");
- var session = await hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
+ private readonly ITaskStore _inner;
+ private readonly ConcurrentDictionary> _pendingModifications = new();
- try
- {
- // Discard any stale continuation token — the incoming user message supersedes
- // any previous background operation. AF agents don't support updating existing
- // background responses (long-running operations); we start a fresh run from the
- // existing session using the full chat history (which includes the new message).
- agentTask.Metadata?.Remove(ContinuationTokenMetadataKey);
+ internal MetadataInjectingTaskStore(ITaskStore inner) => this._inner = inner;
- await taskManager.UpdateStatusAsync(agentTask.Id, TaskState.Working, cancellationToken: cancellationToken).ConfigureAwait(false);
-
- var response = await hostAgent.RunAsync(
- ExtractChatMessagesFromTaskHistory(agentTask),
- session: session,
- options: new AgentRunOptions { AllowBackgroundResponses = true },
- cancellationToken: cancellationToken).ConfigureAwait(false);
+ internal void RegisterModification(string taskId, Action modification)
+ => this._pendingModifications.AddOrUpdate(taskId, modification, (_, existing) => task =>
+ {
+ existing(task);
+ modification(task);
+ });
- await hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
+ internal void ClearModification(string taskId)
+ => this._pendingModifications.TryRemove(taskId, out _);
- if (response.ContinuationToken is not null)
- {
- StoreContinuationToken(agentTask, response.ContinuationToken, continuationTokenJsonOptions);
- await TransitionToWorkingAsync(agentTask.Id, contextId, response, taskManager, cancellationToken).ConfigureAwait(false);
- }
- else
+ public async Task SaveTaskAsync(string taskId, AgentTask task, CancellationToken cancellationToken)
+ {
+ if (this._pendingModifications.TryRemove(taskId, out var modification))
{
- await CompleteWithArtifactAsync(agentTask.Id, response, taskManager, cancellationToken).ConfigureAwait(false);
+ modification(task);
}
+
+ await this._inner.SaveTaskAsync(taskId, task, cancellationToken).ConfigureAwait(false);
}
- catch (OperationCanceledException)
- {
- throw;
- }
- catch (Exception)
+
+ public Task GetTaskAsync(string taskId, CancellationToken cancellationToken)
+ => this._inner.GetTaskAsync(taskId, cancellationToken);
+
+ public Task DeleteTaskAsync(string taskId, CancellationToken cancellationToken)
{
- await taskManager.UpdateStatusAsync(
- agentTask.Id,
- TaskState.Failed,
- final: true,
- cancellationToken: cancellationToken).ConfigureAwait(false);
- throw;
+ this._pendingModifications.TryRemove(taskId, out _);
+ return this._inner.DeleteTaskAsync(taskId, cancellationToken);
}
+
+ public Task ListTasksAsync(ListTasksRequest request, CancellationToken cancellationToken)
+ => this._inner.ListTasksAsync(request, cancellationToken);
}
- private static Task OnTaskCancelledAsync(AgentTask agentTask, CancellationToken cancellationToken)
+ ///
+ /// Private handler implementation that bridges AIAgent to the A2A v1 IAgentHandler interface.
+ ///
+ private sealed class AIAgentA2AHandler : IAgentHandler
{
- // Remove the continuation token from metadata if present.
- // The task has already been marked as cancelled by the TaskManager.
- agentTask.Metadata?.Remove(ContinuationTokenMetadataKey);
- return Task.CompletedTask;
- }
+ private readonly AIHostAgent _hostAgent;
+ private readonly AgentRunMode _runMode;
+ private readonly MetadataInjectingTaskStore _taskStore;
+ private readonly JsonSerializerOptions _continuationTokenJsonOptions;
+
+ internal AIAgentA2AHandler(
+ AIHostAgent hostAgent,
+ AgentRunMode runMode,
+ MetadataInjectingTaskStore taskStore,
+ JsonSerializerOptions continuationTokenJsonOptions)
+ {
+ this._hostAgent = hostAgent;
+ this._runMode = runMode;
+ this._taskStore = taskStore;
+ this._continuationTokenJsonOptions = continuationTokenJsonOptions;
+ }
- private static AgentMessage CreateMessageFromResponse(string contextId, AgentResponse response) =>
- new()
+ public async Task ExecuteAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken)
{
- MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
- ContextId = contextId,
- Role = MessageRole.Agent,
- Parts = response.Messages.ToParts(),
- Metadata = response.AdditionalProperties?.ToA2AMetadata()
- };
+ if (context.IsContinuation)
+ {
+ await this.HandleTaskUpdateAsync(context, eventQueue, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ await this.HandleNewMessageAsync(context, eventQueue, cancellationToken).ConfigureAwait(false);
+ }
+ }
- // Task outputs should be returned as artifacts rather than messages:
- // https://a2a-protocol.org/latest/specification/#37-messages-and-artifacts
- private static Artifact CreateArtifactFromResponse(AgentResponse response) =>
- new()
+ public Task CancelAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken)
{
- ArtifactId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
- Parts = response.Messages.ToParts(),
- Metadata = response.AdditionalProperties?.ToA2AMetadata()
- };
+ // Remove the continuation token from metadata if present.
+ // The task has already been marked as cancelled by the A2AServer.
+ context.Task?.Metadata?.Remove(ContinuationTokenMetadataKey);
+ return Task.CompletedTask;
+ }
- private static async Task InitializeTaskAsync(
- string contextId,
- AgentMessage originalMessage,
- ITaskManager taskManager,
- CancellationToken cancellationToken)
- {
- AgentTask agentTask = await taskManager.CreateTaskAsync(contextId, cancellationToken: cancellationToken).ConfigureAwait(false);
+ private async Task HandleNewMessageAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken)
+ {
+ // AIAgent does not support resuming from arbitrary prior tasks.
+ // Follow-ups on the *same* task are handled via IsContinuation instead.
+ if (context.Message?.ReferenceTaskIds is { Count: > 0 })
+ {
+ throw new NotSupportedException("ReferenceTaskIds is not supported. AIAgent cannot resume from arbitrary prior task context.");
+ }
- // Add the original user message to the task history.
- // The A2A SDK does this internally when it creates tasks via OnTaskCreated.
- agentTask.History ??= [];
- agentTask.History.Add(originalMessage);
+ var contextId = context.ContextId;
+ var session = await this._hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
- // Notify subscribers of the Submitted state per the A2A spec: https://a2a-protocol.org/latest/specification/#413-taskstate
- await taskManager.UpdateStatusAsync(agentTask.Id, TaskState.Submitted, cancellationToken: cancellationToken).ConfigureAwait(false);
+ var sendRequest = new SendMessageRequest { Message = context.Message!, Metadata = context.Metadata };
+ var decisionContext = new A2ARunDecisionContext(sendRequest);
+ var allowBackgroundResponses = await this._runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false);
- return agentTask;
- }
+ var options = context.Metadata is not { Count: > 0 }
+ ? new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses }
+ : new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = context.Metadata.ToAdditionalProperties() };
- private static void StoreContinuationToken(
- AgentTask agentTask,
- ResponseContinuationToken token,
- JsonSerializerOptions continuationTokenJsonOptions)
- {
- // Serialize the continuation token into the task's metadata so it survives
- // across requests and is cleaned up with the task itself.
- agentTask.Metadata ??= [];
- agentTask.Metadata[ContinuationTokenMetadataKey] = JsonSerializer.SerializeToElement(
- token,
- continuationTokenJsonOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
- }
+ var chatMessages = new List();
+ if (context.Message?.Parts is not null)
+ {
+ chatMessages.Add(context.Message.ToChatMessage());
+ }
- private static async Task TransitionToWorkingAsync(
- string taskId,
- string contextId,
- AgentResponse response,
- ITaskManager taskManager,
- CancellationToken cancellationToken)
- {
- // Include any intermediate progress messages from the response as a status message.
- AgentMessage? progressMessage = response.Messages.Count > 0 ? CreateMessageFromResponse(contextId, response) : null;
- await taskManager.UpdateStatusAsync(taskId, TaskState.Working, message: progressMessage, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
+ var response = await this._hostAgent.RunAsync(
+ chatMessages,
+ session: session,
+ options: options,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
- private static async Task CompleteWithArtifactAsync(
- string taskId,
- AgentResponse response,
- ITaskManager taskManager,
- CancellationToken cancellationToken)
- {
- var artifact = CreateArtifactFromResponse(response);
- await taskManager.ReturnArtifactAsync(taskId, artifact, cancellationToken).ConfigureAwait(false);
- await taskManager.UpdateStatusAsync(taskId, TaskState.Completed, final: true, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
+ await this._hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
- private static List ExtractChatMessagesFromTaskHistory(AgentTask agentTask)
- {
- if (agentTask.History is not { Count: > 0 })
- {
- return [];
+ if (response.ContinuationToken is null)
+ {
+ // Simple message response — enqueue a full Message with metadata.
+ var replyMessage = CreateMessageFromResponse(contextId, response);
+ await eventQueue.EnqueueMessageAsync(replyMessage, cancellationToken).ConfigureAwait(false);
+ eventQueue.Complete(null);
+ }
+ else
+ {
+ // Long-running task — use TaskUpdater for stateful tracking.
+ // Register a pending modification so that when A2AServer materializes
+ // the task, the continuation token and original message are injected.
+ var continuationToken = response.ContinuationToken;
+ var continuationJsonOptions = this._continuationTokenJsonOptions;
+ var originalMessage = context.Message;
+ this._taskStore.RegisterModification(context.TaskId, task =>
+ {
+ StoreContinuationToken(task, continuationToken, continuationJsonOptions);
+ task.History ??= [];
+ task.History.Add(originalMessage!);
+ });
+
+ var taskUpdater = new TaskUpdater(eventQueue, context.TaskId, contextId);
+ await taskUpdater.SubmitAsync(cancellationToken).ConfigureAwait(false);
+
+ Message? progressMessage = response.Messages.Count > 0 ? CreateMessageFromResponse(contextId, response) : null;
+ await taskUpdater.StartWorkAsync(progressMessage, cancellationToken).ConfigureAwait(false);
+ }
}
- var chatMessages = new List(agentTask.History.Count);
- foreach (var message in agentTask.History)
+ private async Task HandleTaskUpdateAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken)
{
- chatMessages.Add(message.ToChatMessage());
- }
+ var contextId = context.ContextId;
+ var session = await this._hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
+ var taskUpdater = new TaskUpdater(eventQueue, context.TaskId, contextId);
- return chatMessages;
+ try
+ {
+ // Discard any stale continuation token — the incoming user message supersedes
+ // any previous background operation.
+ var agentTask = context.Task;
+ agentTask?.Metadata?.Remove(ContinuationTokenMetadataKey);
+
+ // Emit the existing task so the materializer has a non-null response.
+ // TaskUpdater status/artifact events alone use EnqueueStatusUpdateAsync
+ // and EnqueueArtifactUpdateAsync, which set StreamResponse.StatusUpdate
+ // and StreamResponse.ArtifactUpdate respectively. The materializer only
+ // initializes the response from events with StreamResponse.Task or
+ // StreamResponse.Message set. EnqueueTaskAsync sets StreamResponse.Task.
+ await eventQueue.EnqueueTaskAsync(agentTask!, cancellationToken).ConfigureAwait(false);
+
+ await taskUpdater.StartWorkAsync(null, cancellationToken).ConfigureAwait(false);
+
+ var response = await this._hostAgent.RunAsync(
+ ExtractChatMessagesFromTaskHistory(agentTask),
+ session: session,
+ options: new AgentRunOptions { AllowBackgroundResponses = true },
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ await this._hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
+
+ if (response.ContinuationToken is not null)
+ {
+ // Register continuation token injection for the next task save.
+ var continuationToken = response.ContinuationToken;
+ var continuationJsonOptions = this._continuationTokenJsonOptions;
+ this._taskStore.RegisterModification(context.TaskId, task =>
+ StoreContinuationToken(task, continuationToken, continuationJsonOptions));
+
+ Message? progressMessage = response.Messages.Count > 0 ? CreateMessageFromResponse(contextId, response) : null;
+ await taskUpdater.StartWorkAsync(progressMessage, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ var artifact = CreateArtifactFromResponse(response);
+ await taskUpdater.AddArtifactAsync(artifact.Parts ?? [], artifact.ArtifactId, artifact.Name, artifact.Description, true, false, cancellationToken).ConfigureAwait(false);
+ await taskUpdater.CompleteAsync(null, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception)
+ {
+ await taskUpdater.FailAsync(null, cancellationToken).ConfigureAwait(false);
+ throw;
+ }
+ }
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs
index 087df96aae..58c58d1d0b 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AgentRunMode.cs
@@ -29,13 +29,13 @@ private AgentRunMode(string value, Func
/// Dissallows the background responses from the agent. Is equivalent to configuring as false.
- /// In the A2A protocol terminology will make responses be returned as AgentMessage.
+ /// In the A2A protocol terminology will make responses be returned as a Message.
///
public static AgentRunMode DisallowBackground => new(MessageValue);
///
/// Allows the background responses from the agent. Is equivalent to configuring as true.
- /// In the A2A protocol terminology will make responses be returned as AgentTask if the agent supports background responses, and as AgentMessage otherwise.
+ /// In the A2A protocol terminology will make responses be returned as AgentTask if the agent supports background responses, and as a Message otherwise.
///
public static AgentRunMode AllowBackgroundIfSupported => new(TaskValue);
@@ -44,9 +44,9 @@ private AgentRunMode(string value, Func with the incoming
/// message and returns a boolean specifying whether to run the agent in background mode.
/// indicates that the agent should run in background mode and return an
- /// AgentTask if the agent supports background mode; otherwise, it returns an AgentMessage
+ /// AgentTask if the agent supports background mode; otherwise, it returns a Message
/// if the mode is not supported. indicates that the agent should run in
- /// non-background mode and return an AgentMessage.
+ /// non-background mode and return a Message.
///
///
/// An async delegate that decides whether the response should be wrapped in an AgentTask.
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/MessageConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/MessageConverter.cs
index 5d2381a235..80f0a60986 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/MessageConverter.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/MessageConverter.cs
@@ -30,22 +30,23 @@ public static List ToParts(this IList chatMessages)
return parts;
}
+
///
- /// Converts A2A MessageSendParams to a collection of Microsoft.Extensions.AI ChatMessage objects.
+ /// Converts an A2A SendMessageRequest to a collection of Microsoft.Extensions.AI ChatMessage objects.
///
- /// The A2A message send parameters to convert.
- /// A read-only collection of ChatMessage objects.
- public static List ToChatMessages(this MessageSendParams messageSendParams)
+ /// The A2A send message request to convert.
+ /// A list of ChatMessage objects.
+ public static List ToChatMessages(this SendMessageRequest sendMessageRequest)
{
- if (messageSendParams is null)
+ if (sendMessageRequest is null)
{
return [];
}
var result = new List();
- if (messageSendParams.Message?.Parts is not null)
+ if (sendMessageRequest.Message?.Parts is not null)
{
- result.Add(messageSendParams.Message.ToChatMessage());
+ result.Add(sendMessageRequest.Message.ToChatMessage());
}
return result;
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
index 514922dd26..3e984db83a 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs
@@ -6,9 +6,7 @@
using System.Linq;
using System.Net;
using System.Net.Http;
-using System.Net.ServerSentEvents;
using System.Text;
-using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -89,14 +87,17 @@ public async Task RunAsync_AllowsNonUserRoleMessagesAsync()
public async Task RunAsync_WithValidUserMessage_RunsSuccessfullyAsync()
{
// Arrange
- this._handler.ResponseToReturn = new AgentMessage
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- MessageId = "response-123",
- Role = MessageRole.Agent,
- Parts =
- [
- new TextPart { Text = "Hello! How can I help you today?" }
- ]
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts =
+ [
+ new Part { Text = "Hello! How can I help you today?" }
+ ]
+ }
};
var inputMessages = new List
@@ -108,11 +109,11 @@ public async Task RunAsync_WithValidUserMessage_RunsSuccessfullyAsync()
var result = await this._agent.RunAsync(inputMessages);
// Assert input message sent to A2AClient
- var inputMessage = this._handler.CapturedMessageSendParams?.Message;
+ var inputMessage = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(inputMessage);
Assert.Single(inputMessage.Parts);
- Assert.Equal(MessageRole.User, inputMessage.Role);
- Assert.Equal("Hello, world!", ((TextPart)inputMessage.Parts[0]).Text);
+ Assert.Equal(Role.User, inputMessage.Role);
+ Assert.Equal("Hello, world!", inputMessage.Parts[0].Text);
// Assert response from A2AClient is converted correctly
Assert.NotNull(result);
@@ -120,8 +121,8 @@ public async Task RunAsync_WithValidUserMessage_RunsSuccessfullyAsync()
Assert.Equal("response-123", result.ResponseId);
Assert.NotNull(result.RawRepresentation);
- Assert.IsType(result.RawRepresentation);
- Assert.Equal("response-123", ((AgentMessage)result.RawRepresentation).MessageId);
+ Assert.IsType(result.RawRepresentation);
+ Assert.Equal("response-123", ((Message)result.RawRepresentation).MessageId);
Assert.Single(result.Messages);
Assert.Equal(ChatRole.Assistant, result.Messages[0].Role);
@@ -133,15 +134,18 @@ public async Task RunAsync_WithValidUserMessage_RunsSuccessfullyAsync()
public async Task RunAsync_WithNewSession_UpdatesSessionConversationIdAsync()
{
// Arrange
- this._handler.ResponseToReturn = new AgentMessage
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- MessageId = "response-123",
- Role = MessageRole.Agent,
- Parts =
- [
- new TextPart { Text = "Response" }
- ],
- ContextId = "new-context-id"
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts =
+ [
+ new Part { Text = "Response" }
+ ],
+ ContextId = "new-context-id"
+ }
};
var inputMessages = new List
@@ -177,7 +181,7 @@ public async Task RunAsync_WithExistingSession_SetConversationIdToMessageAsync()
await this._agent.RunAsync(inputMessages, session);
// Assert
- var message = this._handler.CapturedMessageSendParams?.Message;
+ var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(message);
Assert.Equal("existing-context-id", message.ContextId);
}
@@ -191,15 +195,18 @@ public async Task RunAsync_WithSessionHavingDifferentContextId_ThrowsInvalidOper
new(ChatRole.User, "Test message")
};
- this._handler.ResponseToReturn = new AgentMessage
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- MessageId = "response-123",
- Role = MessageRole.Agent,
- Parts =
- [
- new TextPart { Text = "Response" }
- ],
- ContextId = "different-context"
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts =
+ [
+ new Part { Text = "Response" }
+ ],
+ ContextId = "different-context"
+ }
};
var session = await this._agent.CreateSessionAsync();
@@ -219,12 +226,15 @@ public async Task RunStreamingAsync_WithValidUserMessage_YieldsAgentResponseUpda
new(ChatRole.User, "Hello, streaming!")
};
- this._handler.StreamingResponseToReturn = new AgentMessage()
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- MessageId = "stream-1",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Hello" }],
- ContextId = "stream-context"
+ Message = new Message
+ {
+ MessageId = "stream-1",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Hello" }],
+ ContextId = "stream-context"
+ }
};
// Act
@@ -238,11 +248,11 @@ public async Task RunStreamingAsync_WithValidUserMessage_YieldsAgentResponseUpda
Assert.Single(updates);
// Assert input message sent to A2AClient
- var inputMessage = this._handler.CapturedMessageSendParams?.Message;
+ var inputMessage = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(inputMessage);
Assert.Single(inputMessage.Parts);
- Assert.Equal(MessageRole.User, inputMessage.Role);
- Assert.Equal("Hello, streaming!", ((TextPart)inputMessage.Parts[0]).Text);
+ Assert.Equal(Role.User, inputMessage.Role);
+ Assert.Equal("Hello, streaming!", inputMessage.Parts[0].Text);
// Assert response from A2AClient is converted correctly
Assert.Equal(ChatRole.Assistant, updates[0].Role);
@@ -251,8 +261,8 @@ public async Task RunStreamingAsync_WithValidUserMessage_YieldsAgentResponseUpda
Assert.Equal(this._agent.Id, updates[0].AgentId);
Assert.Equal("stream-1", updates[0].ResponseId);
Assert.Equal(ChatFinishReason.Stop, updates[0].FinishReason);
- Assert.IsType(updates[0].RawRepresentation);
- Assert.Equal("stream-1", ((AgentMessage)updates[0].RawRepresentation!).MessageId);
+ Assert.IsType(updates[0].RawRepresentation);
+ Assert.Equal("stream-1", ((Message)updates[0].RawRepresentation!).MessageId);
}
[Fact]
@@ -264,12 +274,15 @@ public async Task RunStreamingAsync_WithSession_UpdatesSessionConversationIdAsyn
new(ChatRole.User, "Test streaming")
};
- this._handler.StreamingResponseToReturn = new AgentMessage()
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- MessageId = "stream-1",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response" }],
- ContextId = "new-stream-context"
+ Message = new Message
+ {
+ MessageId = "stream-1",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Response" }],
+ ContextId = "new-stream-context"
+ }
};
var session = await this._agent.CreateSessionAsync();
@@ -294,7 +307,7 @@ public async Task RunStreamingAsync_WithExistingSession_SetConversationIdToMessa
new(ChatRole.User, "Test streaming")
};
- this._handler.StreamingResponseToReturn = new AgentMessage();
+ this._handler.StreamingResponseToReturn = new StreamResponse { Message = new Message() };
var session = await this._agent.CreateSessionAsync();
var a2aSession = (A2AAgentSession)session;
@@ -307,7 +320,7 @@ public async Task RunStreamingAsync_WithExistingSession_SetConversationIdToMessa
}
// Assert
- var message = this._handler.CapturedMessageSendParams?.Message;
+ var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(message);
Assert.Equal("existing-context-id", message.ContextId);
}
@@ -325,12 +338,15 @@ public async Task RunStreamingAsync_WithSessionHavingDifferentContextId_ThrowsIn
new(ChatRole.User, "Test streaming")
};
- this._handler.StreamingResponseToReturn = new AgentMessage()
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- MessageId = "stream-1",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response" }],
- ContextId = "different-context"
+ Message = new Message
+ {
+ MessageId = "stream-1",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Response" }],
+ ContextId = "different-context"
+ }
};
// Act
@@ -346,12 +362,15 @@ await Assert.ThrowsAsync(async () =>
public async Task RunStreamingAsync_AllowsNonUserRoleMessagesAsync()
{
// Arrange
- this._handler.StreamingResponseToReturn = new AgentMessage()
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- MessageId = "stream-1",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response" }],
- ContextId = "new-stream-context"
+ Message = new Message
+ {
+ MessageId = "stream-1",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Response" }],
+ ContextId = "new-stream-context"
+ }
};
var inputMessages = new List
@@ -385,13 +404,13 @@ public async Task RunAsync_WithHostedFileContent_ConvertsToFilePartAsync()
await this._agent.RunAsync(inputMessages);
// Assert
- var message = this._handler.CapturedMessageSendParams?.Message;
+ var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(message);
Assert.Equal(2, message.Parts.Count);
- Assert.IsType(message.Parts[0]);
- Assert.Equal("Check this file:", ((TextPart)message.Parts[0]).Text);
- Assert.IsType(message.Parts[1]);
- Assert.Equal("https://example.com/file.pdf", ((FilePart)message.Parts[1]).File.Uri?.ToString());
+ Assert.Equal(PartContentCase.Text, message.Parts[0].ContentCase);
+ Assert.Equal("Check this file:", message.Parts[0].Text);
+ Assert.Equal(PartContentCase.Url, message.Parts[1].ContentCase);
+ Assert.Equal("https://example.com/file.pdf", message.Parts[1].Url);
}
[Fact]
@@ -413,10 +432,11 @@ public async Task RunAsync_WithContinuationTokenAndMessages_ThrowsInvalidOperati
public async Task RunAsync_WithContinuationToken_CallsGetTaskAsyncAsync()
{
// Arrange
- this._handler.ResponseToReturn = new AgentTask
+ this._handler.AgentTaskToReturn = new AgentTask
{
Id = "task-123",
- ContextId = "context-123"
+ ContextId = "context-123",
+ Status = new() { State = TaskState.Submitted }
};
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-123") };
@@ -425,19 +445,22 @@ public async Task RunAsync_WithContinuationToken_CallsGetTaskAsyncAsync()
await this._agent.RunAsync([], options: options);
// Assert
- Assert.Equal("tasks/get", this._handler.CapturedJsonRpcRequest?.Method);
- Assert.Equal("task-123", this._handler.CapturedTaskIdParams?.Id);
+ Assert.Equal("GetTask", this._handler.CapturedJsonRpcRequest?.Method);
+ Assert.Equal("task-123", this._handler.CapturedGetTaskRequest?.Id);
}
[Fact]
public async Task RunAsync_WithTaskInSessionAndMessage_AddTaskAsReferencesToMessageAsync()
{
// Arrange
- this._handler.ResponseToReturn = new AgentMessage
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- MessageId = "response-123",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response to task" }]
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Response to task" }]
+ }
};
var session = (A2AAgentSession)await this._agent.CreateSessionAsync();
@@ -449,7 +472,7 @@ public async Task RunAsync_WithTaskInSessionAndMessage_AddTaskAsReferencesToMess
await this._agent.RunAsync(inputMessage, session);
// Assert
- var message = this._handler.CapturedMessageSendParams?.Message;
+ var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.Null(message?.TaskId);
Assert.NotNull(message?.ReferenceTaskIds);
Assert.Contains("task-123", message.ReferenceTaskIds);
@@ -459,11 +482,14 @@ public async Task RunAsync_WithTaskInSessionAndMessage_AddTaskAsReferencesToMess
public async Task RunAsync_WithAgentTask_UpdatesSessionTaskIdAsync()
{
// Arrange
- this._handler.ResponseToReturn = new AgentTask
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- Id = "task-456",
- ContextId = "context-789",
- Status = new() { State = TaskState.Submitted }
+ Task = new AgentTask
+ {
+ Id = "task-456",
+ ContextId = "context-789",
+ Status = new() { State = TaskState.Submitted }
+ }
};
var session = await this._agent.CreateSessionAsync();
@@ -480,16 +506,19 @@ public async Task RunAsync_WithAgentTask_UpdatesSessionTaskIdAsync()
public async Task RunAsync_WithAgentTaskResponse_ReturnsTaskResponseCorrectlyAsync()
{
// Arrange
- this._handler.ResponseToReturn = new AgentTask
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- Id = "task-789",
- ContextId = "context-456",
- Status = new() { State = TaskState.Submitted },
- Metadata = new Dictionary
+ Task = new AgentTask
+ {
+ Id = "task-789",
+ ContextId = "context-456",
+ Status = new() { State = TaskState.Submitted },
+ Metadata = new Dictionary
{
{ "key1", JsonSerializer.SerializeToElement("value1") },
{ "count", JsonSerializer.SerializeToElement(42) }
}
+ }
};
var session = await this._agent.CreateSessionAsync();
@@ -532,11 +561,14 @@ public async Task RunAsync_WithAgentTaskResponse_ReturnsTaskResponseCorrectlyAsy
public async Task RunAsync_WithVariousTaskStates_ReturnsCorrectTokenAsync(TaskState taskState)
{
// Arrange
- this._handler.ResponseToReturn = new AgentTask
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- Id = "task-123",
- ContextId = "context-123",
- Status = new() { State = taskState }
+ Task = new AgentTask
+ {
+ Id = "task-123",
+ ContextId = "context-123",
+ Status = new() { State = taskState }
+ }
};
// Act
@@ -583,15 +615,76 @@ await Assert.ThrowsAsync(async () =>
});
}
+ [Fact]
+ public async Task RunStreamingAsync_WithContinuationToken_UsesSubscribeToTaskMethodAsync()
+ {
+ // Arrange
+ this._handler.StreamingResponseToReturn = new StreamResponse
+ {
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Continuation response" }]
+ }
+ };
+
+ var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-456") };
+
+ // Act
+ await foreach (var _ in this._agent.RunStreamingAsync([], null, options))
+ {
+ // Just iterate through to trigger the logic
+ }
+
+ // Assert - verify SubscribeToTask was called (not SendStreamingMessage)
+ Assert.Single(this._handler.CapturedJsonRpcRequests);
+ Assert.Equal("SubscribeToTask", this._handler.CapturedJsonRpcRequests[0].Method);
+ }
+
+ [Fact]
+ public async Task RunStreamingAsync_WithContinuationToken_PassesCorrectTaskIdAsync()
+ {
+ // Arrange
+ this._handler.StreamingResponseToReturn = new StreamResponse
+ {
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Continuation response" }]
+ }
+ };
+
+ const string ExpectedTaskId = "my-task-789";
+ var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken(ExpectedTaskId) };
+
+ // Act
+ await foreach (var _ in this._agent.RunStreamingAsync([], null, options))
+ {
+ // Just iterate through to trigger the logic
+ }
+
+ // Assert - verify the task ID was passed correctly
+ Assert.NotEmpty(this._handler.CapturedJsonRpcRequests);
+ var subscribeRequest = this._handler.CapturedJsonRpcRequests[0];
+ var subscribeParams = subscribeRequest.Params?.Deserialize(A2AJsonUtilities.DefaultOptions);
+ Assert.NotNull(subscribeParams);
+ Assert.Equal(ExpectedTaskId, subscribeParams.Id);
+ }
+
[Fact]
public async Task RunStreamingAsync_WithTaskInSessionAndMessage_AddTaskAsReferencesToMessageAsync()
{
// Arrange
- this._handler.StreamingResponseToReturn = new AgentMessage
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- MessageId = "response-123",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response to task" }]
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Response to task" }]
+ }
};
var session = (A2AAgentSession)await this._agent.CreateSessionAsync();
@@ -604,7 +697,7 @@ public async Task RunStreamingAsync_WithTaskInSessionAndMessage_AddTaskAsReferen
}
// Assert
- var message = this._handler.CapturedMessageSendParams?.Message;
+ var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.Null(message?.TaskId);
Assert.NotNull(message?.ReferenceTaskIds);
Assert.Contains("task-123", message.ReferenceTaskIds);
@@ -614,11 +707,14 @@ public async Task RunStreamingAsync_WithTaskInSessionAndMessage_AddTaskAsReferen
public async Task RunStreamingAsync_WithAgentTask_UpdatesSessionTaskIdAsync()
{
// Arrange
- this._handler.StreamingResponseToReturn = new AgentTask
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- Id = "task-456",
- ContextId = "context-789",
- Status = new() { State = TaskState.Submitted }
+ Task = new AgentTask
+ {
+ Id = "task-456",
+ ContextId = "context-789",
+ Status = new() { State = TaskState.Submitted }
+ }
};
var session = await this._agent.CreateSessionAsync();
@@ -642,15 +738,18 @@ public async Task RunStreamingAsync_WithAgentMessage_YieldsResponseUpdateAsync()
const string ContextId = "ctx-456";
const string MessageText = "Hello from agent!";
- this._handler.StreamingResponseToReturn = new AgentMessage
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- MessageId = MessageId,
- Role = MessageRole.Agent,
- ContextId = ContextId,
- Parts =
- [
- new TextPart { Text = MessageText }
- ]
+ Message = new Message
+ {
+ MessageId = MessageId,
+ Role = Role.Agent,
+ ContextId = ContextId,
+ Parts =
+ [
+ new Part { Text = MessageText }
+ ]
+ }
};
// Act
@@ -670,8 +769,8 @@ public async Task RunStreamingAsync_WithAgentMessage_YieldsResponseUpdateAsync()
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.Equal(MessageText, update0.Text);
Assert.Equal(ChatFinishReason.Stop, update0.FinishReason);
- Assert.IsType(update0.RawRepresentation);
- Assert.Equal(MessageId, ((AgentMessage)update0.RawRepresentation!).MessageId);
+ Assert.IsType(update0.RawRepresentation);
+ Assert.Equal(MessageId, ((Message)update0.RawRepresentation!).MessageId);
}
[Fact]
@@ -681,18 +780,21 @@ public async Task RunStreamingAsync_WithAgentTask_YieldsResponseUpdateAsync()
const string TaskId = "task-789";
const string ContextId = "ctx-012";
- this._handler.StreamingResponseToReturn = new AgentTask
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- Id = TaskId,
- ContextId = ContextId,
- Status = new() { State = TaskState.Submitted },
- Artifacts = [
+ Task = new AgentTask
+ {
+ Id = TaskId,
+ ContextId = ContextId,
+ Status = new() { State = TaskState.Submitted },
+ Artifacts = [
new()
{
ArtifactId = "art-123",
- Parts = [new TextPart { Text = "Task artifact content" }]
+ Parts = [new Part { Text = "Task artifact content" }]
}
]
+ }
};
var session = await this._agent.CreateSessionAsync();
@@ -728,11 +830,14 @@ public async Task RunStreamingAsync_WithTaskStatusUpdateEvent_YieldsResponseUpda
const string TaskId = "task-status-123";
const string ContextId = "ctx-status-456";
- this._handler.StreamingResponseToReturn = new TaskStatusUpdateEvent
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- TaskId = TaskId,
- ContextId = ContextId,
- Status = new() { State = TaskState.Working }
+ StatusUpdate = new TaskStatusUpdateEvent
+ {
+ TaskId = TaskId,
+ ContextId = ContextId,
+ Status = new() { State = TaskState.Working }
+ }
};
var session = await this._agent.CreateSessionAsync();
@@ -768,14 +873,17 @@ public async Task RunStreamingAsync_WithTaskArtifactUpdateEvent_YieldsResponseUp
const string ContextId = "ctx-artifact-456";
const string ArtifactContent = "Task artifact data";
- this._handler.StreamingResponseToReturn = new TaskArtifactUpdateEvent
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- TaskId = TaskId,
- ContextId = ContextId,
- Artifact = new()
+ ArtifactUpdate = new TaskArtifactUpdateEvent
{
- ArtifactId = "artifact-789",
- Parts = [new TextPart { Text = ArtifactContent }]
+ TaskId = TaskId,
+ ContextId = ContextId,
+ Artifact = new()
+ {
+ ArtifactId = "artifact-789",
+ Parts = [new Part { Text = ArtifactContent }]
+ }
}
};
@@ -848,15 +956,18 @@ await Assert.ThrowsAsync(async () =>
public async Task RunAsync_WithAgentMessageResponseMetadata_ReturnsMetadataAsAdditionalPropertiesAsync()
{
// Arrange
- this._handler.ResponseToReturn = new AgentMessage
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- MessageId = "response-123",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response with metadata" }],
- Metadata = new Dictionary
+ Message = new Message
{
- { "responseKey1", JsonSerializer.SerializeToElement("responseValue1") },
- { "responseCount", JsonSerializer.SerializeToElement(99) }
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Response with metadata" }],
+ Metadata = new Dictionary
+ {
+ { "responseKey1", JsonSerializer.SerializeToElement("responseValue1") },
+ { "responseCount", JsonSerializer.SerializeToElement(99) }
+ }
}
};
@@ -877,14 +988,17 @@ public async Task RunAsync_WithAgentMessageResponseMetadata_ReturnsMetadataAsAdd
}
[Fact]
- public async Task RunAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync()
+ public async Task RunAsync_WithAdditionalProperties_PropagatesThemAsMetadataToSendMessageRequestAsync()
{
// Arrange
- this._handler.ResponseToReturn = new AgentMessage
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- MessageId = "response-123",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response" }]
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Response" }]
+ }
};
var inputMessages = new List
@@ -906,22 +1020,25 @@ public async Task RunAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMe
await this._agent.RunAsync(inputMessages, null, options);
// Assert
- Assert.NotNull(this._handler.CapturedMessageSendParams);
- Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata);
- Assert.Equal("value1", this._handler.CapturedMessageSendParams.Metadata["key1"].GetString());
- Assert.Equal(42, this._handler.CapturedMessageSendParams.Metadata["key2"].GetInt32());
- Assert.True(this._handler.CapturedMessageSendParams.Metadata["key3"].GetBoolean());
+ Assert.NotNull(this._handler.CapturedSendMessageRequest);
+ Assert.NotNull(this._handler.CapturedSendMessageRequest.Metadata);
+ Assert.Equal("value1", this._handler.CapturedSendMessageRequest.Metadata["key1"].GetString());
+ Assert.Equal(42, this._handler.CapturedSendMessageRequest.Metadata["key2"].GetInt32());
+ Assert.True(this._handler.CapturedSendMessageRequest.Metadata["key3"].GetBoolean());
}
[Fact]
public async Task RunAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync()
{
// Arrange
- this._handler.ResponseToReturn = new AgentMessage
+ this._handler.ResponseToReturn = new SendMessageResponse
{
- MessageId = "response-123",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response" }]
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Response" }]
+ }
};
var inputMessages = new List
@@ -938,19 +1055,22 @@ public async Task RunAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync(
await this._agent.RunAsync(inputMessages, null, options);
// Assert
- Assert.NotNull(this._handler.CapturedMessageSendParams);
- Assert.Null(this._handler.CapturedMessageSendParams.Metadata);
+ Assert.NotNull(this._handler.CapturedSendMessageRequest);
+ Assert.Null(this._handler.CapturedSendMessageRequest.Metadata);
}
[Fact]
- public async Task RunStreamingAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync()
+ public async Task RunStreamingAsync_WithAdditionalProperties_PropagatesThemAsMetadataToSendMessageRequestAsync()
{
// Arrange
- this._handler.StreamingResponseToReturn = new AgentMessage
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- MessageId = "stream-123",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Streaming response" }]
+ Message = new Message
+ {
+ MessageId = "stream-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Streaming response" }]
+ }
};
var inputMessages = new List
@@ -974,22 +1094,25 @@ public async Task RunStreamingAsync_WithAdditionalProperties_PropagatesThemAsMet
}
// Assert
- Assert.NotNull(this._handler.CapturedMessageSendParams);
- Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata);
- Assert.Equal("streamValue1", this._handler.CapturedMessageSendParams.Metadata["streamKey1"].GetString());
- Assert.Equal(100, this._handler.CapturedMessageSendParams.Metadata["streamKey2"].GetInt32());
- Assert.False(this._handler.CapturedMessageSendParams.Metadata["streamKey3"].GetBoolean());
+ Assert.NotNull(this._handler.CapturedSendMessageRequest);
+ Assert.NotNull(this._handler.CapturedSendMessageRequest.Metadata);
+ Assert.Equal("streamValue1", this._handler.CapturedSendMessageRequest.Metadata["streamKey1"].GetString());
+ Assert.Equal(100, this._handler.CapturedSendMessageRequest.Metadata["streamKey2"].GetInt32());
+ Assert.False(this._handler.CapturedSendMessageRequest.Metadata["streamKey3"].GetBoolean());
}
[Fact]
public async Task RunStreamingAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync()
{
// Arrange
- this._handler.StreamingResponseToReturn = new AgentMessage
+ this._handler.StreamingResponseToReturn = new StreamResponse
{
- MessageId = "stream-123",
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Streaming response" }]
+ Message = new Message
+ {
+ MessageId = "stream-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Streaming response" }]
+ }
};
var inputMessages = new List
@@ -1008,8 +1131,115 @@ public async Task RunStreamingAsync_WithNullAdditionalProperties_DoesNotSetMetad
}
// Assert
- Assert.NotNull(this._handler.CapturedMessageSendParams);
- Assert.Null(this._handler.CapturedMessageSendParams.Metadata);
+ Assert.NotNull(this._handler.CapturedSendMessageRequest);
+ Assert.Null(this._handler.CapturedSendMessageRequest.Metadata);
+ }
+
+ [Fact]
+ public async Task RunAsync_WithDefaultOptions_SetsBlockingToTrueAsync()
+ {
+ // Arrange
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test message")
+ };
+
+ // Act
+ await this._agent.RunAsync(inputMessages);
+
+ // Assert
+ Assert.NotNull(this._handler.CapturedSendMessageRequest);
+ Assert.NotNull(this._handler.CapturedSendMessageRequest.Configuration);
+ Assert.True(this._handler.CapturedSendMessageRequest.Configuration.Blocking);
+ }
+
+ [Fact]
+ public async Task RunAsync_WithAllowBackgroundResponsesTrue_SetsBlockingToFalseAsync()
+ {
+ // Arrange
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test message")
+ };
+
+ var session = await this._agent.CreateSessionAsync();
+ var options = new AgentRunOptions { AllowBackgroundResponses = true };
+
+ // Act
+ await this._agent.RunAsync(inputMessages, session, options);
+
+ // Assert
+ Assert.NotNull(this._handler.CapturedSendMessageRequest);
+ Assert.NotNull(this._handler.CapturedSendMessageRequest.Configuration);
+ Assert.False(this._handler.CapturedSendMessageRequest.Configuration.Blocking);
+ }
+
+ [Fact]
+ public async Task RunAsync_WithAllowBackgroundResponsesFalse_SetsBlockingToTrueAsync()
+ {
+ // Arrange
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test message")
+ };
+
+ var options = new AgentRunOptions { AllowBackgroundResponses = false };
+
+ // Act
+ await this._agent.RunAsync(inputMessages, null, options);
+
+ // Assert
+ Assert.NotNull(this._handler.CapturedSendMessageRequest);
+ Assert.NotNull(this._handler.CapturedSendMessageRequest.Configuration);
+ Assert.True(this._handler.CapturedSendMessageRequest.Configuration.Blocking);
+ }
+
+ [Fact]
+ public async Task RunAsync_WithNullOptions_SetsBlockingToTrueAsync()
+ {
+ // Arrange
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test message")
+ };
+
+ // Act
+ await this._agent.RunAsync(inputMessages, null, null);
+
+ // Assert
+ Assert.NotNull(this._handler.CapturedSendMessageRequest);
+ Assert.NotNull(this._handler.CapturedSendMessageRequest.Configuration);
+ Assert.True(this._handler.CapturedSendMessageRequest.Configuration.Blocking);
+ }
+
+ [Fact]
+ public async Task RunStreamingAsync_SendMessageRequest_DoesNotSetBlockingConfigurationAsync()
+ {
+ // Arrange
+ this._handler.StreamingResponseToReturn = new StreamResponse
+ {
+ Message = new Message
+ {
+ MessageId = "response-123",
+ Role = Role.Agent,
+ Parts = [new Part { Text = "Streaming response" }]
+ }
+ };
+
+ var inputMessages = new List
+ {
+ new(ChatRole.User, "Test message")
+ };
+
+ // Act
+ await foreach (var _ in this._agent.RunStreamingAsync(inputMessages))
+ {
+ // Just iterate through to trigger the logic
+ }
+
+ // Assert
+ Assert.NotNull(this._handler.CapturedSendMessageRequest);
+ Assert.Null(this._handler.CapturedSendMessageRequest.Configuration);
}
[Fact]
@@ -1256,6 +1486,7 @@ await Assert.ThrowsAnyAsync(async () =>
public void Dispose()
{
+ this._a2aClient.Dispose();
this._handler.Dispose();
this._httpClient.Dispose();
}
@@ -1269,13 +1500,17 @@ internal sealed class A2AClientHttpMessageHandlerStub : HttpMessageHandler
{
public JsonRpcRequest? CapturedJsonRpcRequest { get; set; }
- public MessageSendParams? CapturedMessageSendParams { get; set; }
+ public List CapturedJsonRpcRequests { get; } = [];
+
+ public SendMessageRequest? CapturedSendMessageRequest { get; set; }
- public TaskIdParams? CapturedTaskIdParams { get; set; }
+ public GetTaskRequest? CapturedGetTaskRequest { get; set; }
- public A2AEvent? ResponseToReturn { get; set; }
+ public SendMessageResponse? ResponseToReturn { get; set; }
- public A2AEvent? StreamingResponseToReturn { get; set; }
+ public AgentTask? AgentTaskToReturn { get; set; }
+
+ public StreamResponse? StreamingResponseToReturn { get; set; }
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
@@ -1286,22 +1521,46 @@ protected override async Task SendAsync(HttpRequestMessage
this.CapturedJsonRpcRequest = JsonSerializer.Deserialize(content);
+ if (this.CapturedJsonRpcRequest is not null)
+ {
+ this.CapturedJsonRpcRequests.Add(this.CapturedJsonRpcRequest);
+ }
+
try
{
- this.CapturedMessageSendParams = this.CapturedJsonRpcRequest?.Params?.Deserialize();
+ this.CapturedSendMessageRequest = this.CapturedJsonRpcRequest?.Params?.Deserialize(A2AJsonUtilities.DefaultOptions);
}
- catch { /* Ignore deserialization errors for non-MessageSendParams requests */ }
+ catch { /* Ignore deserialization errors for non-SendMessageRequest requests */ }
try
{
- this.CapturedTaskIdParams = this.CapturedJsonRpcRequest?.Params?.Deserialize();
+ this.CapturedGetTaskRequest = this.CapturedJsonRpcRequest?.Params?.Deserialize(A2AJsonUtilities.DefaultOptions);
+ }
+ catch { /* Ignore deserialization errors for non-GetTaskRequest requests */ }
+
+ // Return the pre-configured AgentTask response (for tasks/get)
+ if (this.AgentTaskToReturn is not null && this.CapturedJsonRpcRequest?.Method == "GetTask")
+ {
+ var jsonRpcResponse = new JsonRpcResponse
+ {
+ Id = "response-id",
+ Result = JsonSerializer.SerializeToNode(this.AgentTaskToReturn, A2AJsonUtilities.DefaultOptions)
+ };
+
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse), Encoding.UTF8, "application/json")
+ };
}
- catch { /* Ignore deserialization errors for non-TaskIdParams requests */ }
// Return the pre-configured non-streaming response
if (this.ResponseToReturn is not null)
{
- var jsonRpcResponse = JsonRpcResponse.CreateJsonRpcResponse("response-id", this.ResponseToReturn);
+ var jsonRpcResponse = new JsonRpcResponse
+ {
+ Id = "response-id",
+ Result = JsonSerializer.SerializeToNode(this.ResponseToReturn, A2AJsonUtilities.DefaultOptions)
+ };
return new HttpResponseMessage(HttpStatusCode.OK)
{
@@ -1311,22 +1570,18 @@ protected override async Task SendAsync(HttpRequestMessage
// Return the pre-configured streaming response
else if (this.StreamingResponseToReturn is not null)
{
- var stream = new MemoryStream();
-
- await SseFormatter.WriteAsync(
- new SseItem[]
- {
- new(JsonRpcResponse.CreateJsonRpcResponse("response-id", this.StreamingResponseToReturn!))
- }.ToAsyncEnumerable(),
- stream,
- (item, writer) =>
- {
- using Utf8JsonWriter json = new(writer, new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
- JsonSerializer.Serialize(json, item.Data);
- },
- cancellationToken
- );
+ var jsonRpcResponse = new JsonRpcResponse
+ {
+ Id = "response-id",
+ Result = JsonSerializer.SerializeToNode(this.StreamingResponseToReturn, A2AJsonUtilities.DefaultOptions)
+ };
+ var stream = new MemoryStream();
+ var writer = new StreamWriter(stream);
+ await writer.WriteAsync($"data: {JsonSerializer.Serialize(jsonRpcResponse, A2AJsonUtilities.DefaultOptions)}\n\n");
+#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods; overload doesn't exist downlevel
+ await writer.FlushAsync();
+#pragma warning restore CA2016
stream.Position = 0;
return new HttpResponseMessage(HttpStatusCode.OK)
@@ -1339,7 +1594,11 @@ await SseFormatter.WriteAsync(
}
else
{
- var jsonRpcResponse = JsonRpcResponse.CreateJsonRpcResponse("response-id", new AgentMessage());
+ var jsonRpcResponse = new JsonRpcResponse
+ {
+ Id = "response-id",
+ Result = JsonSerializer.SerializeToNode(new SendMessageResponse { Message = new Message() }, A2AJsonUtilities.DefaultOptions)
+ };
return new HttpResponseMessage(HttpStatusCode.OK)
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAIContentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAIContentExtensionsTests.cs
index 358bdfb152..c2e704833a 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAIContentExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAIContentExtensionsTests.cs
@@ -42,14 +42,14 @@ public void ToA2AParts_WithMultipleContents_ReturnsListWithAllParts()
Assert.NotNull(result);
Assert.Equal(3, result.Count);
- var firstTextPart = Assert.IsType(result[0]);
- Assert.Equal("First text", firstTextPart.Text);
+ Assert.Equal(PartContentCase.Text, result[0].ContentCase);
+ Assert.Equal("First text", result[0].Text);
- var filePart = Assert.IsType(result[1]);
- Assert.Equal("https://example.com/file1.txt", filePart.File.Uri?.ToString());
+ Assert.Equal(PartContentCase.Url, result[1].ContentCase);
+ Assert.Equal("https://example.com/file1.txt", result[1].Url);
- var secondTextPart = Assert.IsType(result[2]);
- Assert.Equal("Second text", secondTextPart.Text);
+ Assert.Equal(PartContentCase.Text, result[2].ContentCase);
+ Assert.Equal("Second text", result[2].Text);
}
[Fact]
@@ -72,14 +72,14 @@ public void ToA2AParts_WithMixedSupportedAndUnsupportedContent_IgnoresUnsupporte
Assert.NotNull(result);
Assert.Equal(3, result.Count);
- var firstTextPart = Assert.IsType(result[0]);
- Assert.Equal("First text", firstTextPart.Text);
+ Assert.Equal(PartContentCase.Text, result[0].ContentCase);
+ Assert.Equal("First text", result[0].Text);
- var filePart = Assert.IsType(result[1]);
- Assert.Equal("https://example.com/file.txt", filePart.File.Uri?.ToString());
+ Assert.Equal(PartContentCase.Url, result[1].ContentCase);
+ Assert.Equal("https://example.com/file.txt", result[1].Url);
- var secondTextPart = Assert.IsType(result[2]);
- Assert.Equal("Second text", secondTextPart.Text);
+ Assert.Equal(PartContentCase.Text, result[2].ContentCase);
+ Assert.Equal("Second text", result[2].Text);
}
// Mock class for testing unsupported scenarios
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs
index f644109b38..abf1aa2325 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
@@ -26,7 +27,7 @@ public A2AAgentCardExtensionsTests()
{
Name = "Test Agent",
Description = "A test agent for unit testing",
- Url = "http://test-endpoint/agent"
+ SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }]
};
}
@@ -50,10 +51,10 @@ public async Task RunIAgentAsync_SendsRequestToTheUrlSpecifiedInAgentCardAsync()
using var handler = new HttpMessageHandlerStub();
using var httpClient = new HttpClient(handler, false);
- handler.ResponsesToReturn.Enqueue(new AgentMessage
+ handler.ResponsesToReturn.Enqueue(new Message
{
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response" }],
+ Role = Role.Agent,
+ Parts = [Part.FromText("Response")],
});
var agent = this._agentCard.AsAIAgent(httpClient);
@@ -66,6 +67,42 @@ public async Task RunIAgentAsync_SendsRequestToTheUrlSpecifiedInAgentCardAsync()
Assert.Equal(new Uri("http://test-endpoint/agent"), handler.CapturedUris[0]);
}
+ [Fact]
+ public async Task AsAIAgent_WithInterfaceSelector_UsesSelectedInterfaceAsync()
+ {
+ // Arrange
+ var card = new AgentCard
+ {
+ Name = "Multi-Interface Agent",
+ Description = "An agent with multiple interfaces",
+ SupportedInterfaces =
+ [
+ new AgentInterface { Url = "http://first/agent" },
+ new AgentInterface { Url = "http://second/agent", ProtocolBinding = "grpc" },
+ new AgentInterface { Url = "http://third/agent", ProtocolBinding = "http" },
+ ]
+ };
+
+ using var handler = new HttpMessageHandlerStub();
+ using var httpClient = new HttpClient(handler, false);
+
+ handler.ResponsesToReturn.Enqueue(new Message
+ {
+ Role = Role.Agent,
+ Parts = [Part.FromText("Response")],
+ });
+
+ var agent = card.AsAIAgent(httpClient, interfaceSelector: interfaces =>
+ interfaces.First(i => i.ProtocolBinding == "http"));
+
+ // Act
+ await agent.RunAsync("Test input");
+
+ // Assert
+ Assert.Single(handler.CapturedUris);
+ Assert.Equal(new Uri("http://third/agent"), handler.CapturedUris[0]);
+ }
+
internal sealed class HttpMessageHandlerStub : HttpMessageHandler
{
public Queue ResponsesToReturn { get; } = new();
@@ -86,13 +123,18 @@ protected override async Task SendAsync(HttpRequestMessage
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
}
- else if (response is AgentMessage message)
+ else if (response is Message message)
{
- var jsonRpcResponse = JsonRpcResponse.CreateJsonRpcResponse("response-id", message);
+ var sendMessageResponse = new SendMessageResponse { Message = message };
+ var jsonRpcResponse = new JsonRpcResponse
+ {
+ Id = "response-id",
+ Result = JsonSerializer.SerializeToNode(sendMessageResponse, A2AJsonUtilities.DefaultOptions)
+ };
return new HttpResponseMessage(HttpStatusCode.OK)
{
- Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse), Encoding.UTF8, "application/json")
+ Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse, A2AJsonUtilities.DefaultOptions), Encoding.UTF8, "application/json")
};
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentTaskExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentTaskExtensionsTests.cs
index 97c9ca7c05..5fdfb1ff89 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentTaskExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentTaskExtensionsTests.cs
@@ -40,7 +40,7 @@ public void ToChatMessages_WithEmptyArtifactsAndNoUserInputRequests_ReturnsNull(
{
Id = "task1",
Artifacts = [],
- Status = new AgentTaskStatus { State = TaskState.Completed },
+ Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -58,7 +58,7 @@ public void ToChatMessages_WithNullArtifactsAndNoUserInputRequests_ReturnsNull()
{
Id = "task1",
Artifacts = null,
- Status = new AgentTaskStatus { State = TaskState.Completed },
+ Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -76,7 +76,7 @@ public void ToAIContents_WithEmptyArtifactsAndNoUserInputRequests_ReturnsNull()
{
Id = "task1",
Artifacts = [],
- Status = new AgentTaskStatus { State = TaskState.Completed },
+ Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -94,7 +94,7 @@ public void ToAIContents_WithNullArtifactsAndNoUserInputRequests_ReturnsNull()
{
Id = "task1",
Artifacts = null,
- Status = new AgentTaskStatus { State = TaskState.Completed },
+ Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -110,14 +110,14 @@ public void ToChatMessages_WithValidArtifact_ReturnsChatMessages()
// Arrange
var artifact = new Artifact
{
- Parts = [new TextPart { Text = "response" }],
+ Parts = [Part.FromText("response")],
};
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [artifact],
- Status = new AgentTaskStatus { State = TaskState.Completed },
+ Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -136,15 +136,15 @@ public void ToAIContents_WithMultipleArtifacts_FlattenAllContents()
// Arrange
var artifact1 = new Artifact
{
- Parts = [new TextPart { Text = "content1" }],
+ Parts = [Part.FromText("content1")],
};
var artifact2 = new Artifact
{
Parts =
[
- new TextPart { Text = "content2" },
- new TextPart { Text = "content3" }
+ Part.FromText("content2"),
+ Part.FromText("content3")
],
};
@@ -152,7 +152,7 @@ public void ToAIContents_WithMultipleArtifacts_FlattenAllContents()
{
Id = "task1",
Artifacts = [artifact1, artifact2],
- Status = new AgentTaskStatus { State = TaskState.Completed },
+ Status = new TaskStatus { State = TaskState.Completed },
};
// Act
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AArtifactExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AArtifactExtensionsTests.cs
index b18abd4485..1f6cfa65f0 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AArtifactExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AArtifactExtensionsTests.cs
@@ -22,9 +22,9 @@ public void ToChatMessage_WithMultiplePartsMetadataAndRawRepresentation_ReturnsC
Name = "comprehensive-artifact",
Parts =
[
- new TextPart { Text = "First part" },
- new TextPart { Text = "Second part" },
- new TextPart { Text = "Third part" }
+ Part.FromText("First part"),
+ Part.FromText("Second part"),
+ Part.FromText("Third part")
],
Metadata = new Dictionary
{
@@ -66,9 +66,9 @@ public void ToAIContents_WithMultipleParts_ReturnsCorrectList()
Name = "test",
Parts =
[
- new TextPart { Text = "Part 1" },
- new TextPart { Text = "Part 2" },
- new TextPart { Text = "Part 3" }
+ Part.FromText("Part 1"),
+ Part.FromText("Part 2"),
+ Part.FromText("Part 3")
],
Metadata = null
};
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs
index dcc45e8fce..95cb2a67d2 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs
@@ -37,7 +37,7 @@ public async Task GetAIAgentAsync_WithValidAgentCard_ReturnsAIAgentAsync()
{
Name = "Test Agent",
Description = "A test agent for unit testing",
- Url = "http://test-endpoint/agent"
+ SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }]
});
// Act
@@ -60,12 +60,12 @@ public async Task RunIAgentAsync_WithUrlFromAgentCard_SendsRequestToTheUrlAsync(
// Arrange
this._handler.ResponsesToReturn.Enqueue(new AgentCard
{
- Url = "http://test-endpoint/agent"
+ SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }]
});
- this._handler.ResponsesToReturn.Enqueue(new AgentMessage
+ this._handler.ResponsesToReturn.Enqueue(new Message
{
- Role = MessageRole.Agent,
- Parts = [new TextPart { Text = "Response" }],
+ Role = Role.Agent,
+ Parts = [Part.FromText("Response")],
});
var agent = await this._resolver.GetAIAgentAsync(this._httpClient);
@@ -104,13 +104,18 @@ protected override async Task SendAsync(HttpRequestMessage
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
}
- else if (response is AgentMessage message)
+ else if (response is Message message)
{
- var jsonRpcResponse = JsonRpcResponse.CreateJsonRpcResponse("response-id", message);
+ var sendMessageResponse = new SendMessageResponse { Message = message };
+ var jsonRpcResponse = new JsonRpcResponse
+ {
+ Id = "response-id",
+ Result = JsonSerializer.SerializeToNode(sendMessageResponse, A2AJsonUtilities.DefaultOptions)
+ };
return new HttpResponseMessage(HttpStatusCode.OK)
{
- Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse), Encoding.UTF8, "application/json")
+ Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse, A2AJsonUtilities.DefaultOptions), Encoding.UTF8, "application/json")
};
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/ChatMessageExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/ChatMessageExtensionsTests.cs
index 8d771c679c..bb502bbea0 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/ChatMessageExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/ChatMessageExtensionsTests.cs
@@ -32,20 +32,19 @@ public void ToA2AMessage_WithMessageContainingMultipleContents_AddsAllContentsAs
Assert.NotNull(a2aMessage.MessageId);
Assert.NotEmpty(a2aMessage.MessageId);
- Assert.Equal(MessageRole.User, a2aMessage.Role);
+ Assert.Equal(Role.User, a2aMessage.Role);
Assert.NotNull(a2aMessage.Parts);
Assert.Equal(3, a2aMessage.Parts.Count);
- var filePart = Assert.IsType(a2aMessage.Parts[0]);
- Assert.NotNull(filePart.File);
- Assert.Equal("https://example.com/report.pdf", filePart.File.Uri?.ToString());
+ Assert.Equal(PartContentCase.Url, a2aMessage.Parts[0].ContentCase);
+ Assert.Equal("https://example.com/report.pdf", a2aMessage.Parts[0].Url);
- var secondTextPart = Assert.IsType(a2aMessage.Parts[1]);
- Assert.Equal("please summarize the file content", secondTextPart.Text);
+ Assert.Equal(PartContentCase.Text, a2aMessage.Parts[1].ContentCase);
+ Assert.Equal("please summarize the file content", a2aMessage.Parts[1].Text);
- var thirdTextPart = Assert.IsType(a2aMessage.Parts[2]);
- Assert.Equal("and send it to me over email", thirdTextPart.Text);
+ Assert.Equal(PartContentCase.Text, a2aMessage.Parts[2].ContentCase);
+ Assert.Equal("and send it to me over email", a2aMessage.Parts[2].Text);
}
[Fact]
@@ -71,19 +70,18 @@ public void ToA2AMessage_WithMixedMessages_AddsAllContentsAsParts()
Assert.NotNull(a2aMessage.MessageId);
Assert.NotEmpty(a2aMessage.MessageId);
- Assert.Equal(MessageRole.User, a2aMessage.Role);
+ Assert.Equal(Role.User, a2aMessage.Role);
Assert.NotNull(a2aMessage.Parts);
Assert.Equal(3, a2aMessage.Parts.Count);
- var filePart = Assert.IsType(a2aMessage.Parts[0]);
- Assert.NotNull(filePart.File);
- Assert.Equal("https://example.com/report.pdf", filePart.File.Uri?.ToString());
+ Assert.Equal(PartContentCase.Url, a2aMessage.Parts[0].ContentCase);
+ Assert.Equal("https://example.com/report.pdf", a2aMessage.Parts[0].Url);
- var secondTextPart = Assert.IsType(a2aMessage.Parts[1]);
- Assert.Equal("please summarize the file content", secondTextPart.Text);
+ Assert.Equal(PartContentCase.Text, a2aMessage.Parts[1].ContentCase);
+ Assert.Equal("please summarize the file content", a2aMessage.Parts[1].Text);
- var thirdTextPart = Assert.IsType(a2aMessage.Parts[2]);
- Assert.Equal("and send it to me over email", thirdTextPart.Text);
+ Assert.Equal(PartContentCase.Text, a2aMessage.Parts[2].ContentCase);
+ Assert.Equal("and send it to me over email", a2aMessage.Parts[2].Text);
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj
index d33de0613b..97541f6a94 100644
--- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj
+++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj
@@ -1,5 +1,9 @@
+
+ $(TargetFrameworksCore)
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AIntegrationTests.cs
index f8604c7eac..e7d77fed38 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AIntegrationTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AIntegrationTests.cs
@@ -16,9 +16,17 @@ namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
public sealed class A2AIntegrationTests
{
///
- /// Verifies that calling the A2A card endpoint with MapA2A returns an agent card with a URL populated.
+ /// Verifies that calling the A2A well-known agent card endpoint returns the configured agent card.
///
+ ///
+ /// Skipped on .NET 8 because the A2A.AspNetCore SDK's MapWellKnownAgentCard uses
+ /// PipeWriter.UnflushedBytes which requires .NET 9+.
+ ///
+#if NET9_0_OR_GREATER
[Fact]
+#else
+ [Fact(Skip = "A2A.AspNetCore MapWellKnownAgentCard requires .NET 9+ (PipeWriter.UnflushedBytes)")]
+#endif
public async Task MapA2A_WithAgentCard_CardEndpointReturnsCardWithUrlAsync()
{
// Arrange
@@ -36,7 +44,11 @@ public async Task MapA2A_WithAgentCard_CardEndpointReturnsCardWithUrlAsync()
{
Name = "Test Agent",
Description = "A test agent for A2A communication",
- Version = "1.0"
+ Version = "1.0",
+ SupportedInterfaces =
+ [
+ new AgentInterface { Url = "http://localhost/a2a/test-agent" }
+ ]
};
// Map A2A with the agent card
@@ -51,10 +63,11 @@ public async Task MapA2A_WithAgentCard_CardEndpointReturnsCardWithUrlAsync()
?? throw new InvalidOperationException("TestServer not found");
var httpClient = testServer.CreateClient();
- // Act - Query the agent card endpoint
- var requestUri = new Uri("/a2a/test-agent/v1/card", UriKind.Relative);
+ // Act - Query the well-known agent card endpoint
+ var requestUri = new Uri("/.well-known/agent-card.json", UriKind.Relative);
var response = await httpClient.GetAsync(requestUri);
+ // Assert
// Assert
Assert.True(response.IsSuccessStatusCode, $"Expected successful response but got {response.StatusCode}");
@@ -69,17 +82,17 @@ public async Task MapA2A_WithAgentCard_CardEndpointReturnsCardWithUrlAsync()
Assert.True(root.TryGetProperty("description", out var descProperty));
Assert.Equal("A test agent for A2A communication", descProperty.GetString());
- // Verify the card has a URL property and it's not null/empty
- Assert.True(root.TryGetProperty("url", out var urlProperty));
- Assert.NotEqual(JsonValueKind.Null, urlProperty.ValueKind);
+ // Verify the card has a supportedInterfaces property with a URL
+ Assert.True(root.TryGetProperty("supportedInterfaces", out var interfacesProp));
+ Assert.NotEqual(JsonValueKind.Null, interfacesProp.ValueKind);
+ Assert.True(interfacesProp.GetArrayLength() > 0);
+ var firstInterface = interfacesProp[0];
+ Assert.True(firstInterface.TryGetProperty("url", out var urlProperty));
var url = urlProperty.GetString();
Assert.NotNull(url);
Assert.NotEmpty(url);
- Assert.StartsWith("http", url, StringComparison.OrdinalIgnoreCase);
-
- // agentCard's URL matches the agent endpoint
- Assert.Equal($"{testServer.BaseAddress.ToString().TrimEnd('/')}/a2a/test-agent", url);
+ Assert.Equal("http://localhost/a2a/test-agent", url);
}
finally
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs
index 87de6e52cd..5ce7a0f7c9 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs
@@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
public sealed class AIAgentExtensionsTests
{
///
- /// Verifies that when messageSendParams.Metadata is null, the options passed to RunAsync have
+ /// Verifies that when sendMessageRequest.Metadata is null, the options passed to RunAsync have
/// AllowBackgroundResponses enabled and no AdditionalProperties.
///
[Fact]
@@ -27,14 +27,14 @@ public async Task MapA2A_WhenMetadataIsNull_PassesOptionsWithNoAdditionalPropert
{
// Arrange
AgentRunOptions? capturedOptions = null;
- ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
// Act
- await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] },
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] },
Metadata = null
- });
+ }, CancellationToken.None);
// Assert
Assert.NotNull(capturedOptions);
@@ -43,25 +43,25 @@ public async Task MapA2A_WhenMetadataIsNull_PassesOptionsWithNoAdditionalPropert
}
///
- /// Verifies that when messageSendParams.Metadata has values, the options.AdditionalProperties contains the converted values.
+ /// Verifies that when sendMessageRequest.Metadata has values, the options.AdditionalProperties contains the converted values.
///
[Fact]
public async Task MapA2A_WhenMetadataHasValues_PassesOptionsWithAdditionalPropertiesToRunAsync()
{
// Arrange
AgentRunOptions? capturedOptions = null;
- ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
// Act
- await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] },
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] },
Metadata = new Dictionary
{
["key1"] = JsonSerializer.SerializeToElement("value1"),
["key2"] = JsonSerializer.SerializeToElement(42)
}
- });
+ }, CancellationToken.None);
// Assert
Assert.NotNull(capturedOptions);
@@ -72,7 +72,7 @@ public async Task MapA2A_WhenMetadataHasValues_PassesOptionsWithAdditionalProper
}
///
- /// Verifies that when messageSendParams.Metadata is an empty dictionary, the options passed to RunAsync have
+ /// Verifies that when sendMessageRequest.Metadata is an empty dictionary, the options passed to RunAsync have
/// AllowBackgroundResponses enabled and no AdditionalProperties.
///
[Fact]
@@ -80,14 +80,14 @@ public async Task MapA2A_WhenMetadataIsEmptyDictionary_PassesOptionsWithNoAdditi
{
// Arrange
AgentRunOptions? capturedOptions = null;
- ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMock(options => capturedOptions = options).Object.MapA2A();
// Act
- await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] },
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] },
Metadata = []
- });
+ }, CancellationToken.None);
// Assert
Assert.NotNull(capturedOptions);
@@ -96,10 +96,10 @@ public async Task MapA2A_WhenMetadataIsEmptyDictionary_PassesOptionsWithNoAdditi
}
///
- /// Verifies that when the agent response has AdditionalProperties, the returned AgentMessage.Metadata contains the converted values.
+ /// Verifies that when the agent response has AdditionalProperties, the returned Message.Metadata contains the converted values.
///
[Fact]
- public async Task MapA2A_WhenResponseHasAdditionalProperties_ReturnsAgentMessageWithMetadataAsync()
+ public async Task MapA2A_WhenResponseHasAdditionalProperties_ReturnsMessageWithMetadataAsync()
{
// Arrange
AdditionalPropertiesDictionary additionalProps = new()
@@ -111,16 +111,18 @@ public async Task MapA2A_WhenResponseHasAdditionalProperties_ReturnsAgentMessage
{
AdditionalProperties = additionalProps
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- AgentMessage agentMessage = Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Message, a2aResponse.PayloadCase);
+ Message agentMessage = a2aResponse.Message!;
+ Assert.NotNull(agentMessage);
Assert.NotNull(agentMessage.Metadata);
Assert.Equal(2, agentMessage.Metadata.Count);
Assert.True(agentMessage.Metadata.ContainsKey("responseKey1"));
@@ -130,121 +132,128 @@ public async Task MapA2A_WhenResponseHasAdditionalProperties_ReturnsAgentMessage
}
///
- /// Verifies that when the agent response has null AdditionalProperties, the returned AgentMessage.Metadata is null.
+ /// Verifies that when the agent response has null AdditionalProperties, the returned Message.Metadata is null.
///
[Fact]
- public async Task MapA2A_WhenResponseHasNullAdditionalProperties_ReturnsAgentMessageWithNullMetadataAsync()
+ public async Task MapA2A_WhenResponseHasNullAdditionalProperties_ReturnsMessageWithNullMetadataAsync()
{
// Arrange
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")])
{
AdditionalProperties = null
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- AgentMessage agentMessage = Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Message, a2aResponse.PayloadCase);
+ Message agentMessage = a2aResponse.Message!;
+ Assert.NotNull(agentMessage);
Assert.Null(agentMessage.Metadata);
}
///
- /// Verifies that when the agent response has empty AdditionalProperties, the returned AgentMessage.Metadata is null.
+ /// Verifies that when the agent response has empty AdditionalProperties, the returned Message.Metadata is null.
///
[Fact]
- public async Task MapA2A_WhenResponseHasEmptyAdditionalProperties_ReturnsAgentMessageWithNullMetadataAsync()
+ public async Task MapA2A_WhenResponseHasEmptyAdditionalProperties_ReturnsMessageWithNullMetadataAsync()
{
// Arrange
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")])
{
AdditionalProperties = []
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- AgentMessage agentMessage = Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Message, a2aResponse.PayloadCase);
+ Message agentMessage = a2aResponse.Message!;
+ Assert.NotNull(agentMessage);
Assert.Null(agentMessage.Metadata);
}
///
- /// Verifies that when runMode is Message, the result is always an AgentMessage even when
+ /// Verifies that when runMode is Message, the result is always a Message even when
/// the agent would otherwise support background responses.
///
[Fact]
- public async Task MapA2A_MessageMode_AlwaysReturnsAgentMessageAsync()
+ public async Task MapA2A_MessageMode_AlwaysReturnsMessageAsync()
{
// Arrange
AgentRunOptions? capturedOptions = null;
- ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options)
+ IA2ARequestHandler handler = CreateAgentMock(options => capturedOptions = options)
.Object.MapA2A(runMode: AgentRunMode.DisallowBackground);
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Message, a2aResponse.PayloadCase);
+ Assert.NotNull(a2aResponse.Message);
Assert.NotNull(capturedOptions);
Assert.False(capturedOptions.AllowBackgroundResponses);
}
///
/// Verifies that in BackgroundIfSupported mode when the agent completes immediately (no ContinuationToken),
- /// the result is an AgentMessage because the response type is determined solely by ContinuationToken presence.
+ /// the result is a Message because the response type is determined solely by ContinuationToken presence.
///
[Fact]
- public async Task MapA2A_BackgroundIfSupportedMode_WhenNoContinuationToken_ReturnsAgentMessageAsync()
+ public async Task MapA2A_BackgroundIfSupportedMode_WhenNoContinuationToken_ReturnsMessageAsync()
{
// Arrange
AgentRunOptions? capturedOptions = null;
- ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options)
+ IA2ARequestHandler handler = CreateAgentMock(options => capturedOptions = options)
.Object.MapA2A(runMode: AgentRunMode.AllowBackgroundIfSupported);
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Message, a2aResponse.PayloadCase);
+ Assert.NotNull(a2aResponse.Message);
Assert.NotNull(capturedOptions);
Assert.True(capturedOptions.AllowBackgroundResponses);
}
///
- /// Verifies that a custom Dynamic delegate returning false produces an AgentMessage
+ /// Verifies that a custom Dynamic delegate returning false produces a Message
/// even when the agent completes immediately (no ContinuationToken).
///
[Fact]
- public async Task MapA2A_DynamicMode_WithFalseCallback_ReturnsAgentMessageAsync()
+ public async Task MapA2A_DynamicMode_WithFalseCallback_ReturnsMessageAsync()
{
// Arrange
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, "Quick reply")]);
- ITaskManager taskManager = CreateAgentMockWithResponse(response)
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response)
.Object.MapA2A(runMode: AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(false)));
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Message, a2aResponse.PayloadCase);
+ Assert.NotNull(a2aResponse.Message);
}
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@@ -260,16 +269,18 @@ public async Task MapA2A_WhenResponseHasContinuationToken_ReturnsAgentTaskInWork
{
ContinuationToken = CreateTestContinuationToken()
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- AgentTask agentTask = Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Task, a2aResponse.PayloadCase);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
Assert.Equal(TaskState.Working, agentTask.Status.State);
}
@@ -285,19 +296,21 @@ public async Task MapA2A_WhenResponseHasContinuationToken_TaskStatusHasIntermedi
{
ContinuationToken = CreateTestContinuationToken()
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- AgentTask agentTask = Assert.IsType(a2aResponse);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
Assert.NotNull(agentTask.Status.Message);
- TextPart textPart = Assert.IsType(Assert.Single(agentTask.Status.Message.Parts));
- Assert.Equal("Starting work...", textPart.Text);
+ Part part = Assert.Single(agentTask.Status.Message.Parts);
+ Assert.Equal(PartContentCase.Text, part.ContentCase);
+ Assert.Equal("Starting work...", part.Text);
}
///
@@ -312,18 +325,20 @@ public async Task MapA2A_WhenResponseHasContinuationToken_StoresTokenInTaskMetad
{
ContinuationToken = CreateTestContinuationToken()
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- AgentTask agentTask = Assert.IsType(a2aResponse);
- Assert.NotNull(agentTask.Metadata);
- Assert.True(agentTask.Metadata.ContainsKey("__a2a__continuationToken"));
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
+ AgentTask storedTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
+ Assert.NotNull(storedTask.Metadata);
+ Assert.True(storedTask.Metadata.ContainsKey("__a2a__continuationToken"));
}
///
@@ -338,51 +353,55 @@ public async Task MapA2A_WhenTaskIsCreated_OriginalMessageIsInHistoryAsync()
{
ContinuationToken = CreateTestContinuationToken()
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
- AgentMessage originalMessage = new() { MessageId = "user-msg-1", Role = MessageRole.User, Parts = [new TextPart { Text = "Do something" }] };
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
+ Message originalMessage = new() { MessageId = "user-msg-1", Role = Role.User, Parts = [Part.FromText("Do something")] };
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
Message = originalMessage
- });
+ }, CancellationToken.None);
// Assert
- AgentTask agentTask = Assert.IsType(a2aResponse);
- Assert.NotNull(agentTask.History);
- Assert.Contains(agentTask.History, m => m.MessageId == "user-msg-1" && m.Role == MessageRole.User);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
+ AgentTask storedTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
+ Assert.NotNull(storedTask.History);
+ Assert.Contains(storedTask.History, m => m.MessageId == "user-msg-1" && m.Role == Role.User);
}
///
/// Verifies that in BackgroundIfSupported mode when the agent completes immediately (no ContinuationToken),
- /// the returned AgentMessage preserves the original context ID.
+ /// the returned Message preserves the original context ID.
///
[Fact]
- public async Task MapA2A_BackgroundIfSupportedMode_WhenNoContinuationToken_ReturnsAgentMessageWithContextIdAsync()
+ public async Task MapA2A_BackgroundIfSupportedMode_WhenNoContinuationToken_ReturnsMessageWithContextIdAsync()
{
// Arrange
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, "Done!")]);
- ITaskManager taskManager = CreateAgentMockWithResponse(response)
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response)
.Object.MapA2A(runMode: AgentRunMode.AllowBackgroundIfSupported);
- AgentMessage originalMessage = new() { MessageId = "user-msg-2", ContextId = "ctx-123", Role = MessageRole.User, Parts = [new TextPart { Text = "Quick task" }] };
+ Message originalMessage = new() { MessageId = "user-msg-2", ContextId = "ctx-123", Role = Role.User, Parts = [Part.FromText("Quick task")] };
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
Message = originalMessage
- });
+ }, CancellationToken.None);
// Assert
- AgentMessage agentMessage = Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Message, a2aResponse.PayloadCase);
+ Message agentMessage = a2aResponse.Message!;
+ Assert.NotNull(agentMessage);
Assert.Equal("ctx-123", agentMessage.ContextId);
}
///
- /// Verifies that when OnTaskUpdated is invoked on a task with a pending continuation token
+ /// Verifies that when a continuation is triggered on a task with a pending continuation token
/// and the agent returns a completed response (null ContinuationToken), the task is updated to Completed.
///
[Fact]
- public async Task MapA2A_OnTaskUpdated_WhenBackgroundOperationCompletes_TaskIsCompletedAsync()
+ public async Task MapA2A_OnContinuation_WhenBackgroundOperationCompletes_TaskIsCompletedAsync()
{
// Arrange
int callCount = 0;
@@ -392,38 +411,40 @@ public async Task MapA2A_OnTaskUpdated_WhenBackgroundOperationCompletes_TaskIsCo
{
ContinuationToken = CreateTestContinuationToken()
},
- // Second call (via OnTaskUpdated): return completed response
+ // Second call (via continuation): return completed response
new AgentResponse([new ChatMessage(ChatRole.Assistant, "Done!")]),
ref callCount);
- ITaskManager taskManager = agentMock.Object.MapA2A();
+ IA2ARequestHandler handler = agentMock.Object.MapA2A();
- // Act — trigger OnMessageReceived to create the task
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ // Act — send initial message to create the task
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
- AgentTask agentTask = Assert.IsType(a2aResponse);
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
Assert.Equal(TaskState.Working, agentTask.Status.State);
- // Act — invoke OnTaskUpdated to check on the background operation
- await InvokeOnTaskUpdatedAsync(taskManager, agentTask);
+ // Act — send continuation message to check on the background operation
+ await SendTaskContinuationAsync(handler, agentTask);
// Assert — task should now be completed
- AgentTask? updatedTask = await taskManager.GetTaskAsync(new TaskQueryParams { Id = agentTask.Id }, CancellationToken.None);
+ AgentTask updatedTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
Assert.NotNull(updatedTask);
Assert.Equal(TaskState.Completed, updatedTask.Status.State);
Assert.NotNull(updatedTask.Artifacts);
Artifact artifact = Assert.Single(updatedTask.Artifacts);
- TextPart textPart = Assert.IsType(Assert.Single(artifact.Parts));
- Assert.Equal("Done!", textPart.Text);
+ Part part = Assert.Single(artifact.Parts);
+ Assert.Equal(PartContentCase.Text, part.ContentCase);
+ Assert.Equal("Done!", part.Text);
}
///
- /// Verifies that when OnTaskUpdated is invoked on a task with a pending continuation token
+ /// Verifies that when a continuation is triggered on a task with a pending continuation token
/// and the agent returns another ContinuationToken, the task stays in Working state.
///
[Fact]
- public async Task MapA2A_OnTaskUpdated_WhenBackgroundOperationStillWorking_TaskRemainsWorkingAsync()
+ public async Task MapA2A_OnContinuation_WhenBackgroundOperationStillWorking_TaskRemainsWorkingAsync()
{
// Arrange
int callCount = 0;
@@ -433,26 +454,27 @@ public async Task MapA2A_OnTaskUpdated_WhenBackgroundOperationStillWorking_TaskR
{
ContinuationToken = CreateTestContinuationToken()
},
- // Second call (via OnTaskUpdated): still working, return another token
+ // Second call (via continuation): still working, return another token
new AgentResponse([new ChatMessage(ChatRole.Assistant, "Still working...")])
{
ContinuationToken = CreateTestContinuationToken()
},
ref callCount);
- ITaskManager taskManager = agentMock.Object.MapA2A();
+ IA2ARequestHandler handler = agentMock.Object.MapA2A();
- // Act — trigger OnMessageReceived to create the task
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ // Act — send initial message to create the task
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
- AgentTask agentTask = Assert.IsType(a2aResponse);
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
- // Act — invoke OnTaskUpdated; agent still working
- await InvokeOnTaskUpdatedAsync(taskManager, agentTask);
+ // Act — send continuation; agent still working
+ await SendTaskContinuationAsync(handler, agentTask);
// Assert — task should still be in Working state
- AgentTask? updatedTask = await taskManager.GetTaskAsync(new TaskQueryParams { Id = agentTask.Id }, CancellationToken.None);
+ AgentTask updatedTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
Assert.NotNull(updatedTask);
Assert.Equal(TaskState.Working, updatedTask.Status.State);
}
@@ -462,7 +484,7 @@ public async Task MapA2A_OnTaskUpdated_WhenBackgroundOperationStillWorking_TaskR
/// second poll returns completed.
///
[Fact]
- public async Task MapA2A_OnTaskUpdated_MultiplePolls_EventuallyCompletesAsync()
+ public async Task MapA2A_OnContinuation_MultiplePolls_EventuallyCompletesAsync()
{
// Arrange
int callCount = 0;
@@ -484,35 +506,35 @@ public async Task MapA2A_OnTaskUpdated_MultiplePolls_EventuallyCompletesAsync()
_ => new AgentResponse([new ChatMessage(ChatRole.Assistant, "All done!")])
};
});
- ITaskManager taskManager = agentMock.Object.MapA2A();
+ IA2ARequestHandler handler = agentMock.Object.MapA2A();
// Act — create the task
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Do work" }] }
- });
- AgentTask agentTask = Assert.IsType(a2aResponse);
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Do work")] }
+ }, CancellationToken.None);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
Assert.Equal(TaskState.Working, agentTask.Status.State);
// Act — first poll: still working
- AgentTask? currentTask = await taskManager.GetTaskAsync(new TaskQueryParams { Id = agentTask.Id }, CancellationToken.None);
- Assert.NotNull(currentTask);
- await InvokeOnTaskUpdatedAsync(taskManager, currentTask);
- currentTask = await taskManager.GetTaskAsync(new TaskQueryParams { Id = agentTask.Id }, CancellationToken.None);
+ await SendTaskContinuationAsync(handler, agentTask);
+ AgentTask currentTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
Assert.NotNull(currentTask);
Assert.Equal(TaskState.Working, currentTask.Status.State);
// Act — second poll: completed
- await InvokeOnTaskUpdatedAsync(taskManager, currentTask);
- currentTask = await taskManager.GetTaskAsync(new TaskQueryParams { Id = agentTask.Id }, CancellationToken.None);
+ await SendTaskContinuationAsync(handler, agentTask);
+ currentTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
Assert.NotNull(currentTask);
Assert.Equal(TaskState.Completed, currentTask.Status.State);
// Assert — final output as artifact
Assert.NotNull(currentTask.Artifacts);
Artifact artifact = Assert.Single(currentTask.Artifacts);
- TextPart textPart = Assert.IsType(Assert.Single(artifact.Parts));
- Assert.Equal("All done!", textPart.Text);
+ Part part = Assert.Single(artifact.Parts);
+ Assert.Equal(PartContentCase.Text, part.ContentCase);
+ Assert.Equal("All done!", part.Text);
}
///
@@ -520,7 +542,7 @@ public async Task MapA2A_OnTaskUpdated_MultiplePolls_EventuallyCompletesAsync()
/// the task is updated to Failed state.
///
[Fact]
- public async Task MapA2A_OnTaskUpdated_WhenAgentThrows_TaskIsFailedAsync()
+ public async Task MapA2A_OnContinuation_WhenAgentThrows_TaskIsFailedAsync()
{
// Arrange
int callCount = 0;
@@ -536,20 +558,21 @@ public async Task MapA2A_OnTaskUpdated_WhenAgentThrows_TaskIsFailedAsync()
throw new InvalidOperationException("Agent failed");
});
- ITaskManager taskManager = agentMock.Object.MapA2A();
+ IA2ARequestHandler handler = agentMock.Object.MapA2A();
// Act — create the task
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
- AgentTask agentTask = Assert.IsType(a2aResponse);
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
// Act — poll the task; agent throws
- await Assert.ThrowsAsync(() => InvokeOnTaskUpdatedAsync(taskManager, agentTask));
+ await Assert.ThrowsAsync(() => SendTaskContinuationAsync(handler, agentTask));
// Assert — task should be Failed
- AgentTask? updatedTask = await taskManager.GetTaskAsync(new TaskQueryParams { Id = agentTask.Id }, CancellationToken.None);
+ AgentTask updatedTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
Assert.NotNull(updatedTask);
Assert.Equal(TaskState.Failed, updatedTask.Status.State);
}
@@ -565,20 +588,23 @@ public async Task MapA2A_TaskMode_WhenContinuationToken_ReturnsWorkingAgentTaskA
{
ContinuationToken = CreateTestContinuationToken()
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response)
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response)
.Object.MapA2A(runMode: AgentRunMode.AllowBackgroundIfSupported);
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- AgentTask agentTask = Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Task, a2aResponse.PayloadCase);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
Assert.Equal(TaskState.Working, agentTask.Status.State);
- Assert.NotNull(agentTask.Metadata);
- Assert.True(agentTask.Metadata.ContainsKey("__a2a__continuationToken"));
+ AgentTask storedTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
+ Assert.NotNull(storedTask.Metadata);
+ Assert.True(storedTask.Metadata.ContainsKey("__a2a__continuationToken"));
}
///
@@ -593,26 +619,28 @@ public async Task MapA2A_WhenContinuationTokenWithNoMessages_TaskStatusHasNullMe
{
ContinuationToken = CreateTestContinuationToken()
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
// Assert
- AgentTask agentTask = Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Task, a2aResponse.PayloadCase);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
Assert.Equal(TaskState.Working, agentTask.Status.State);
Assert.Null(agentTask.Status.Message);
}
///
- /// Verifies that when OnTaskUpdated is invoked on a completed task with a follow-up message
- /// and no continuation token in metadata, the task processes history and completes with a new artifact.
+ /// Verifies that when a continuation completes a task, the task transitions to Completed state
+ /// with an artifact, and sending a follow-up to the terminal task is rejected by A2AServer.
///
[Fact]
- public async Task MapA2A_OnTaskUpdated_WhenNoContinuationToken_ProcessesHistoryAndCompletesAsync()
+ public async Task MapA2A_OnContinuation_WhenCompleted_FollowUpToTerminalTaskThrowsAsync()
{
// Arrange
int callCount = 0;
@@ -625,71 +653,64 @@ public async Task MapA2A_OnTaskUpdated_WhenNoContinuationToken_ProcessesHistoryA
{
ContinuationToken = CreateTestContinuationToken()
},
- // Second call (via OnTaskUpdated): complete the background operation
- 2 => new AgentResponse([new ChatMessage(ChatRole.Assistant, "Done!")]),
- // Third call (follow-up via OnTaskUpdated): complete follow-up
- _ => new AgentResponse([new ChatMessage(ChatRole.Assistant, "Follow-up done!")])
+ // Second call (via continuation): complete the background operation
+ _ => new AgentResponse([new ChatMessage(ChatRole.Assistant, "Done!")])
};
});
- ITaskManager taskManager = agentMock.Object.MapA2A();
+ IA2ARequestHandler handler = agentMock.Object.MapA2A();
// Act — create a working task (with continuation token)
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
- {
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
- AgentTask agentTask = Assert.IsType(a2aResponse);
-
- // Act — first OnTaskUpdated: completes the background operation
- await InvokeOnTaskUpdatedAsync(taskManager, agentTask);
- agentTask = (await taskManager.GetTaskAsync(new TaskQueryParams { Id = agentTask.Id }, CancellationToken.None))!;
- Assert.Equal(TaskState.Completed, agentTask.Status.State);
-
- // Simulate a follow-up message by adding it to history and re-submitting via OnTaskUpdated
- agentTask.History ??= [];
- agentTask.History.Add(new AgentMessage { MessageId = "follow-up", Role = MessageRole.User, Parts = [new TextPart { Text = "Follow up" }] });
-
- // Act — invoke OnTaskUpdated without a continuation token in metadata
- await InvokeOnTaskUpdatedAsync(taskManager, agentTask);
-
- // Assert
- AgentTask? updatedTask = await taskManager.GetTaskAsync(new TaskQueryParams { Id = agentTask.Id }, CancellationToken.None);
- Assert.NotNull(updatedTask);
- Assert.Equal(TaskState.Completed, updatedTask.Status.State);
- Assert.NotNull(updatedTask.Artifacts);
- Assert.Equal(2, updatedTask.Artifacts.Count);
- Artifact artifact = updatedTask.Artifacts[1];
- TextPart textPart = Assert.IsType(Assert.Single(artifact.Parts));
- Assert.Equal("Follow-up done!", textPart.Text);
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
+ {
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
+
+ // Act — first continuation: completes the background operation
+ await SendTaskContinuationAsync(handler, agentTask);
+ AgentTask storedTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
+ Assert.Equal(TaskState.Completed, storedTask.Status.State);
+ Assert.NotNull(storedTask.Artifacts);
+ Artifact artifact = Assert.Single(storedTask.Artifacts);
+ Part part = Assert.Single(artifact.Parts);
+ Assert.Equal(PartContentCase.Text, part.ContentCase);
+ Assert.Equal("Done!", part.Text);
+
+ // Assert — follow-up to a terminal (Completed) task is rejected
+ A2AException ex = await Assert.ThrowsAsync(() => SendTaskContinuationAsync(handler, agentTask, "Follow up"));
+ Assert.Contains("terminal state", ex.Message, StringComparison.OrdinalIgnoreCase);
}
///
- /// Verifies that when a task is cancelled, the continuation token is removed from metadata.
+ /// Verifies that when a task is cancelled via CancelTaskAsync, the A2AServer
+ /// invokes the handler's CancelAsync and returns the task from the store.
///
[Fact]
- public async Task MapA2A_OnTaskCancelled_RemovesContinuationTokenFromMetadataAsync()
+ public async Task MapA2A_OnTaskCancelled_CancelTaskAsyncReturnsTaskAsync()
{
// Arrange
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, "Starting...")])
{
ContinuationToken = CreateTestContinuationToken()
};
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
// Act — create a working task with a continuation token
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
- AgentTask agentTask = Assert.IsType(a2aResponse);
- Assert.NotNull(agentTask.Metadata);
- Assert.True(agentTask.Metadata.ContainsKey("__a2a__continuationToken"));
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
+ Assert.Equal(TaskState.Working, agentTask.Status.State);
// Act — cancel the task
- await taskManager.CancelTaskAsync(new TaskIdParams { Id = agentTask.Id }, CancellationToken.None);
+ AgentTask cancelledTask = await handler.CancelTaskAsync(new CancelTaskRequest { Id = agentTask.Id }, CancellationToken.None);
- // Assert — continuation token should be removed from metadata
- Assert.False(agentTask.Metadata.ContainsKey("__a2a__continuationToken"));
+ // Assert — CancelTaskAsync returns the task from the store
+ Assert.NotNull(cancelledTask);
+ Assert.Equal(agentTask.Id, cancelledTask.Id);
}
///
@@ -697,7 +718,7 @@ public async Task MapA2A_OnTaskCancelled_RemovesContinuationTokenFromMetadataAsy
/// it is re-thrown without marking the task as Failed.
///
[Fact]
- public async Task MapA2A_OnTaskUpdated_WhenOperationCancelled_DoesNotMarkFailedAsync()
+ public async Task MapA2A_OnContinuation_WhenOperationCancelled_DoesNotMarkFailedAsync()
{
// Arrange
int callCount = 0;
@@ -713,20 +734,21 @@ public async Task MapA2A_OnTaskUpdated_WhenOperationCancelled_DoesNotMarkFailedA
throw new OperationCanceledException("Cancelled");
});
- ITaskManager taskManager = agentMock.Object.MapA2A();
+ IA2ARequestHandler handler = agentMock.Object.MapA2A();
// Act — create the task
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }
- });
- AgentTask agentTask = Assert.IsType(a2aResponse);
+ Message = new Message { MessageId = "test-id", Role = Role.User, Parts = [Part.FromText("Hello")] }
+ }, CancellationToken.None);
+ AgentTask agentTask = a2aResponse.Task!;
+ Assert.NotNull(agentTask);
// Act — poll the task; agent throws OperationCanceledException
- await Assert.ThrowsAsync(() => InvokeOnTaskUpdatedAsync(taskManager, agentTask));
+ await Assert.ThrowsAsync(() => SendTaskContinuationAsync(handler, agentTask));
// Assert — task should still be Working, not Failed
- AgentTask? updatedTask = await taskManager.GetTaskAsync(new TaskQueryParams { Id = agentTask.Id }, CancellationToken.None);
+ AgentTask updatedTask = (await handler.GetTaskAsync(new GetTaskRequest { Id = agentTask.Id }, CancellationToken.None))!;
Assert.NotNull(updatedTask);
Assert.Equal(TaskState.Working, updatedTask.Status.State);
}
@@ -740,22 +762,24 @@ public async Task MapA2A_WhenMessageHasContextId_UsesProvidedContextIdAsync()
{
// Arrange
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, "Reply")]);
- ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A();
+ IA2ARequestHandler handler = CreateAgentMockWithResponse(response).Object.MapA2A();
// Act
- A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams
+ SendMessageResponse a2aResponse = await handler.SendMessageAsync(new SendMessageRequest
{
- Message = new AgentMessage
+ Message = new Message
{
MessageId = "test-id",
ContextId = "my-context-123",
- Role = MessageRole.User,
- Parts = [new TextPart { Text = "Hello" }]
+ Role = Role.User,
+ Parts = [Part.FromText("Hello")]
}
- });
+ }, CancellationToken.None);
// Assert
- AgentMessage agentMessage = Assert.IsType(a2aResponse);
+ Assert.Equal(SendMessageResponseCase.Message, a2aResponse.PayloadCase);
+ Message agentMessage = a2aResponse.Message!;
+ Assert.NotNull(agentMessage);
Assert.Equal("my-context-123", agentMessage.ContextId);
}
@@ -803,18 +827,28 @@ private static Mock CreateAgentMockWithResponse(AgentResponse response)
return agentMock;
}
- private static async Task InvokeOnMessageReceivedAsync(ITaskManager taskManager, MessageSendParams messageSendParams)
- {
- Func>? handler = taskManager.OnMessageReceived;
- Assert.NotNull(handler);
- return await handler.Invoke(messageSendParams, CancellationToken.None);
- }
-
- private static async Task InvokeOnTaskUpdatedAsync(ITaskManager taskManager, AgentTask agentTask)
+ ///
+ /// Sends a continuation message for an existing task using the streaming API.
+ /// The streaming path is required because HandleTaskUpdateAsync produces only
+ /// TaskStatusUpdateEvent and TaskArtifactUpdateEvent events, which are not
+ /// recognized by A2AServer.MaterializeResponseAsync (used by SendMessageAsync).
+ ///
+ private static async Task SendTaskContinuationAsync(IA2ARequestHandler handler, AgentTask agentTask, string text = "continue")
{
- Func? handler = taskManager.OnTaskUpdated;
- Assert.NotNull(handler);
- await handler.Invoke(agentTask, CancellationToken.None);
+ await foreach (StreamResponse _ in handler.SendStreamingMessageAsync(new SendMessageRequest
+ {
+ Message = new Message
+ {
+ MessageId = Guid.NewGuid().ToString("N"),
+ ContextId = agentTask.ContextId,
+ TaskId = agentTask.Id,
+ Role = Role.User,
+ Parts = [Part.FromText(text)]
+ }
+ }, CancellationToken.None))
+ {
+ // Consume all events to ensure they are applied to the task store.
+ }
}
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/MessageConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/MessageConverterTests.cs
index 69eaf3a535..198f9f6446 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/MessageConverterTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/MessageConverterTests.cs
@@ -10,66 +10,66 @@ namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Converters;
public class MessageConverterTests
{
[Fact]
- public void ToChatMessages_MessageSendParams_Null_ReturnsEmptyCollection()
+ public void ToChatMessages_SendMessageRequest_Null_ReturnsEmptyCollection()
{
- MessageSendParams? messageSendParams = null;
+ SendMessageRequest? sendMessageRequest = null;
- var result = messageSendParams!.ToChatMessages();
+ var result = sendMessageRequest!.ToChatMessages();
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
- public void ToChatMessages_MessageSendParams_WithNullMessage_ReturnsEmptyCollection()
+ public void ToChatMessages_SendMessageRequest_WithNullMessage_ReturnsEmptyCollection()
{
- var messageSendParams = new MessageSendParams
+ var sendMessageRequest = new SendMessageRequest
{
Message = null!
};
- var result = messageSendParams.ToChatMessages();
+ var result = sendMessageRequest.ToChatMessages();
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
- public void ToChatMessages_MessageSendParams_WithMessageWithoutParts_ReturnsEmptyCollection()
+ public void ToChatMessages_SendMessageRequest_WithMessageWithoutParts_ReturnsEmptyCollection()
{
- var messageSendParams = new MessageSendParams
+ var sendMessageRequest = new SendMessageRequest
{
- Message = new AgentMessage
+ Message = new Message
{
MessageId = "test-id",
- Role = MessageRole.User,
+ Role = Role.User,
Parts = null!
}
};
- var result = messageSendParams.ToChatMessages();
+ var result = sendMessageRequest.ToChatMessages();
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
- public void ToChatMessages_MessageSendParams_WithValidTextMessage_ReturnsCorrectChatMessage()
+ public void ToChatMessages_SendMessageRequest_WithValidTextMessage_ReturnsCorrectChatMessage()
{
- var messageSendParams = new MessageSendParams
+ var sendMessageRequest = new SendMessageRequest
{
- Message = new AgentMessage
+ Message = new Message
{
MessageId = "test-id",
- Role = MessageRole.User,
+ Role = Role.User,
Parts =
[
- new TextPart { Text = "Hello, world!" }
+ Part.FromText("Hello, world!")
]
}
};
- var result = messageSendParams.ToChatMessages();
+ var result = sendMessageRequest.ToChatMessages();
Assert.NotNull(result);
Assert.Single(result);
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/EndpointRouteA2ABuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/EndpointRouteA2ABuilderExtensionsTests.cs
index a848528888..3aae126c5e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/EndpointRouteA2ABuilderExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/EndpointRouteA2ABuilderExtensionsTests.cs
@@ -57,7 +57,7 @@ public void MapA2A_WithAgentBuilder_NullAgentBuilder_ThrowsArgumentNullException
}
///
- /// Verifies that MapA2A with IHostedAgentBuilder correctly maps the agent with default task manager configuration.
+ /// Verifies that MapA2A with IHostedAgentBuilder correctly maps the agent with default configuration.
///
[Fact]
public void MapA2A_WithAgentBuilder_DefaultConfiguration_Succeeds()
@@ -76,26 +76,6 @@ public void MapA2A_WithAgentBuilder_DefaultConfiguration_Succeeds()
Assert.NotNull(app);
}
- ///
- /// Verifies that MapA2A with IHostedAgentBuilder and custom task manager configuration succeeds.
- ///
- [Fact]
- public void MapA2A_WithAgentBuilder_CustomTaskManagerConfiguration_Succeeds()
- {
- // Arrange
- WebApplicationBuilder builder = WebApplication.CreateBuilder();
- IChatClient mockChatClient = new DummyChatClient();
- builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
- IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
- builder.Services.AddLogging();
- using WebApplication app = builder.Build();
-
- // Act & Assert - Should not throw
- var result = app.MapA2A(agentBuilder, "/a2a", taskManager => { });
- Assert.NotNull(result);
- Assert.NotNull(app);
- }
-
///
/// Verifies that MapA2A with IHostedAgentBuilder and agent card succeeds.
///
@@ -123,10 +103,10 @@ public void MapA2A_WithAgentBuilder_WithAgentCard_Succeeds()
}
///
- /// Verifies that MapA2A with IHostedAgentBuilder, agent card, and custom task manager configuration succeeds.
+ /// Verifies that MapA2A with IHostedAgentBuilder, agent card, and custom run mode succeeds.
///
[Fact]
- public void MapA2A_WithAgentBuilder_WithAgentCardAndCustomConfiguration_Succeeds()
+ public void MapA2A_WithAgentBuilder_WithAgentCardAndRunMode_Succeeds()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
@@ -143,7 +123,7 @@ public void MapA2A_WithAgentBuilder_WithAgentCardAndCustomConfiguration_Succeeds
};
// Act & Assert - Should not throw
- var result = app.MapA2A(agentBuilder, "/a2a", agentCard, taskManager => { });
+ var result = app.MapA2A(agentBuilder, "/a2a", agentCard, AgentRunMode.AllowBackgroundIfSupported);
Assert.NotNull(result);
Assert.NotNull(app);
}
@@ -184,26 +164,6 @@ public void MapA2A_WithAgentName_DefaultConfiguration_Succeeds()
Assert.NotNull(app);
}
- ///
- /// Verifies that MapA2A with string agent name and custom task manager configuration succeeds.
- ///
- [Fact]
- public void MapA2A_WithAgentName_CustomTaskManagerConfiguration_Succeeds()
- {
- // Arrange
- WebApplicationBuilder builder = WebApplication.CreateBuilder();
- IChatClient mockChatClient = new DummyChatClient();
- builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
- builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
- builder.Services.AddLogging();
- using WebApplication app = builder.Build();
-
- // Act & Assert - Should not throw
- var result = app.MapA2A("agent", "/a2a", taskManager => { });
- Assert.NotNull(result);
- Assert.NotNull(app);
- }
-
///
/// Verifies that MapA2A with string agent name and agent card succeeds.
///
@@ -231,10 +191,10 @@ public void MapA2A_WithAgentName_WithAgentCard_Succeeds()
}
///
- /// Verifies that MapA2A with string agent name, agent card, and custom task manager configuration succeeds.
+ /// Verifies that MapA2A with string agent name, agent card, and custom run mode succeeds.
///
[Fact]
- public void MapA2A_WithAgentName_WithAgentCardAndCustomConfiguration_Succeeds()
+ public void MapA2A_WithAgentName_WithAgentCardAndRunMode_Succeeds()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
@@ -251,7 +211,7 @@ public void MapA2A_WithAgentName_WithAgentCardAndCustomConfiguration_Succeeds()
};
// Act & Assert - Should not throw
- var result = app.MapA2A("agent", "/a2a", agentCard, taskManager => { });
+ var result = app.MapA2A("agent", "/a2a", agentCard, AgentRunMode.DisallowBackground);
Assert.NotNull(result);
Assert.NotNull(app);
}
@@ -293,27 +253,6 @@ public void MapA2A_WithAIAgent_DefaultConfiguration_Succeeds()
Assert.NotNull(app);
}
- ///
- /// Verifies that MapA2A with AIAgent and custom task manager configuration succeeds.
- ///
- [Fact]
- public void MapA2A_WithAIAgent_CustomTaskManagerConfiguration_Succeeds()
- {
- // Arrange
- WebApplicationBuilder builder = WebApplication.CreateBuilder();
- IChatClient mockChatClient = new DummyChatClient();
- builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
- builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
- builder.Services.AddLogging();
- using WebApplication app = builder.Build();
- AIAgent agent = app.Services.GetRequiredKeyedService("agent");
-
- // Act & Assert - Should not throw
- var result = app.MapA2A(agent, "/a2a", taskManager => { });
- Assert.NotNull(result);
- Assert.NotNull(app);
- }
-
///
/// Verifies that MapA2A with AIAgent and agent card succeeds.
///
@@ -342,10 +281,10 @@ public void MapA2A_WithAIAgent_WithAgentCard_Succeeds()
}
///
- /// Verifies that MapA2A with AIAgent, agent card, and custom task manager configuration succeeds.
+ /// Verifies that MapA2A with AIAgent, agent card, and custom run mode succeeds.
///
[Fact]
- public void MapA2A_WithAIAgent_WithAgentCardAndCustomConfiguration_Succeeds()
+ public void MapA2A_WithAIAgent_WithAgentCardAndRunMode_Succeeds()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
@@ -363,26 +302,24 @@ public void MapA2A_WithAIAgent_WithAgentCardAndCustomConfiguration_Succeeds()
};
// Act & Assert - Should not throw
- var result = app.MapA2A(agent, "/a2a", agentCard, taskManager => { });
+ var result = app.MapA2A(agent, "/a2a", agentCard, AgentRunMode.AllowBackgroundIfSupported);
Assert.NotNull(result);
Assert.NotNull(app);
}
///
- /// Verifies that MapA2A throws ArgumentNullException for null endpoints when using ITaskManager.
+ /// Verifies that MapA2A with IA2ARequestHandler correctly maps the handler.
///
[Fact]
- public void MapA2A_WithTaskManager_NullEndpoints_ThrowsArgumentNullException()
+ public void MapA2A_WithHandler_NullEndpoints_ThrowsArgumentNullException()
{
// Arrange
AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
- ITaskManager taskManager = null!;
+ IA2ARequestHandler handler = null!;
// Act & Assert
- ArgumentNullException exception = Assert.Throws(() =>
- endpoints.MapA2A(taskManager, "/a2a"));
-
- Assert.Equal("endpoints", exception.ParamName);
+ Assert.Throws(() =>
+ endpoints.MapA2A(handler, "/a2a"));
}
///
@@ -425,33 +362,6 @@ public void MapA2A_WithCustomPath_AcceptsValidPath()
Assert.NotNull(app);
}
- ///
- /// Verifies that task manager configuration callback is invoked correctly.
- ///
- [Fact]
- public void MapA2A_WithAgentBuilder_TaskManagerConfigurationCallbackInvoked()
- {
- // Arrange
- WebApplicationBuilder builder = WebApplication.CreateBuilder();
- IChatClient mockChatClient = new DummyChatClient();
- builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
- IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
- builder.Services.AddLogging();
- using WebApplication app = builder.Build();
-
- bool configureCallbackInvoked = false;
-
- // Act
- app.MapA2A(agentBuilder, "/a2a", taskManager =>
- {
- configureCallbackInvoked = true;
- Assert.NotNull(taskManager);
- });
-
- // Assert
- Assert.True(configureCallbackInvoked);
- }
-
///
/// Verifies that agent card with all properties is accepted.
///