update a2a agent to the latest a2a sdk (#5257)

This commit is contained in:
SergeyMenshykh
2026-04-15 11:08:05 +01:00
committed by GitHub
parent 485af07b8c
commit 6173e63f0b
18 changed files with 728 additions and 390 deletions
+109 -96
View File
@@ -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 @@ public sealed class A2AAgent : AIAgent
this._logger.LogA2AAgentInvokingAgent(nameof(RunAsync), this.Id, this.Name);
A2AResponse? a2aResponse = null;
if (GetContinuationToken(messages, options) is { } token)
{
a2aResponse = await this._a2aClient.GetTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
}
else
{
MessageSendParams sendParams = new()
{
Message = CreateA2AMessage(typedSession, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
};
AgentTask agentTask = await this._a2aClient.GetTaskAsync(new GetTaskRequest { Id = token.TaskId }, cancellationToken).ConfigureAwait(false);
a2aResponse = await this._a2aClient.SendMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
UpdateSession(typedSession, agentTask.ContextId, agentTask.Id);
return this.ConvertToAgentResponse(agentTask);
}
SendMessageRequest sendParams = new()
{
Message = CreateA2AMessage(typedSession, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata(),
Configuration = new SendMessageConfiguration { ReturnImmediately = options?.AllowBackgroundResponses is true }
};
SendMessageResponse a2aResponse = await this._a2aClient.SendMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
if (a2aResponse is AgentMessage message)
if (a2aResponse.PayloadCase == SendMessageResponseCase.Message)
{
var message = a2aResponse.Message!;
UpdateSession(typedSession, message.ContextId);
return new AgentResponse
{
AgentId = this.Id,
ResponseId = message.MessageId,
FinishReason = ChatFinishReason.Stop,
RawRepresentation = message,
Messages = [message.ToChatMessage()],
AdditionalProperties = message.Metadata?.ToAdditionalProperties(),
};
return this.ConvertToAgentResponse(message);
}
if (a2aResponse is AgentTask agentTask)
if (a2aResponse.PayloadCase == SendMessageResponseCase.Task)
{
var agentTask = a2aResponse.Task!;
UpdateSession(typedSession, agentTask.ContextId, agentTask.Id);
var response = new AgentResponse
{
AgentId = this.Id,
ResponseId = agentTask.Id,
FinishReason = MapTaskStateToFinishReason(agentTask.Status.State),
RawRepresentation = agentTask,
Messages = agentTask.ToChatMessages() ?? [],
ContinuationToken = CreateContinuationToken(agentTask.Id, agentTask.Status.State),
AdditionalProperties = agentTask.Metadata?.ToAdditionalProperties(),
};
if (agentTask.ToChatMessages() is { Count: > 0 } taskMessages)
{
response.Messages = taskMessages;
}
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}");
}
/// <inheritdoc/>
@@ -169,59 +151,61 @@ public sealed class A2AAgent : AIAgent
this._logger.LogA2AAgentInvokingAgent(nameof(RunStreamingAsync), this.Id, this.Name);
ConfiguredCancelableAsyncEnumerable<SseItem<A2AEvent>> a2aSseEvents;
ConfiguredCancelableAsyncEnumerable<StreamResponse> 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)
switch (streamResponse.PayloadCase)
{
contextId = message.ContextId;
case StreamResponseCase.Message:
var message = streamResponse.Message!;
contextId = message.ContextId;
yield return this.ConvertToAgentResponseUpdate(message);
break;
yield return this.ConvertToAgentResponseUpdate(message);
}
else if (sseEvent.Data is AgentTask task)
{
contextId = task.ContextId;
taskId = task.Id;
case StreamResponseCase.Task:
var task = streamResponse.Task!;
contextId = task.ContextId;
taskId = task.Id;
yield return this.ConvertToAgentResponseUpdate(task);
break;
yield return this.ConvertToAgentResponseUpdate(task);
}
else if (sseEvent.Data is TaskUpdateEvent taskUpdateEvent)
{
contextId = taskUpdateEvent.ContextId;
taskId = taskUpdateEvent.TaskId;
case StreamResponseCase.StatusUpdate:
var statusUpdate = streamResponse.StatusUpdate!;
contextId = statusUpdate.ContextId;
taskId = statusUpdate.TaskId;
yield return this.ConvertToAgentResponseUpdate(statusUpdate);
break;
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.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 @@ public sealed class A2AAgent : AIAgent
session.TaskId = taskId;
}
private static AgentMessage CreateA2AMessage(A2AAgentSession typedSession, IEnumerable<ChatMessage> messages)
private static Message CreateA2AMessage(A2AAgentSession typedSession, IEnumerable<ChatMessage> messages)
{
var a2aMessage = messages.ToA2AMessage();
@@ -324,7 +308,34 @@ public sealed class A2AAgent : AIAgent
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 agentTask)
{
return new AgentResponse
{
AgentId = this.Id,
ResponseId = agentTask.Id,
FinishReason = MapTaskStateToFinishReason(agentTask.Status.State),
RawRepresentation = agentTask,
Messages = agentTask.ToChatMessages() ?? [],
ContinuationToken = CreateContinuationToken(agentTask.Id, agentTask.Status.State),
AdditionalProperties = agentTask.Metadata?.ToAdditionalProperties(),
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(Message message)
{
return new AgentResponseUpdate
{
@@ -353,28 +364,30 @@ public sealed class A2AAgent : AIAgent
};
}
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)
private AgentResponseUpdate ConvertToAgentResponseUpdate(TaskArtifactUpdateEvent artifactUpdateEvent)
{
return new AgentResponseUpdate
{
responseUpdate.Contents = artifactUpdateEvent.Artifact.ToAIContents();
responseUpdate.RawRepresentation = artifactUpdateEvent;
}
else if (taskUpdateEvent is TaskStatusUpdateEvent statusUpdateEvent)
{
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)
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Net.Http;
using Microsoft.Agents.AI;
using Microsoft.Extensions.Logging;
@@ -29,8 +30,12 @@ public static class A2AAgentCardExtensions
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null)
{
// TODO: Refactor to support interface selection from card.SupportedInterfaces.
var url = card.SupportedInterfaces?.FirstOrDefault()?.Url
?? throw new InvalidOperationException("The AgentCard does not have any SupportedInterfaces with 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);
}
@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.AI;
/// </summary>
internal static class ChatMessageExtensions
{
internal static AgentMessage ToA2AMessage(this IEnumerable<ChatMessage> messages)
internal static Message ToA2AMessage(this IEnumerable<ChatMessage> messages)
{
List<Part> allParts = [];
@@ -23,10 +23,10 @@ internal static class ChatMessageExtensions
}
}
return new AgentMessage
return new Message
{
MessageId = Guid.NewGuid().ToString("N"),
Role = MessageRole.User,
Role = Role.User,
Parts = allParts,
};
}
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<VersionSuffix>preview</VersionSuffix>
<NoWarn>$(NoWarn);MEAI001</NoWarn>
</PropertyGroup>