mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: A2A agent (#520)
* add a2a agent * Update dotnet/src/Microsoft.Extensions.AI.Agents.A2A/A2AAgent.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * address pr review feedback * move unit tests for extension methods to the extensions folder * address pr review comments * address pr review comments * address pr review feedback * move a2a agent sample to console app * remove unnecessary Ids set for new projects in the solution file * remove unnecessary configuration --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
97d72c967f
commit
1d2f833122
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an <see cref="AIAgent"/> that can interact with remote agents that are exposed via the A2A protocol
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This agent supports only messages as a response from A2A agents.
|
||||
/// Support for tasks will be added later as part of the long-running
|
||||
/// executions work.
|
||||
/// </remarks>
|
||||
internal sealed class A2AAgent : AIAgent
|
||||
{
|
||||
private readonly A2AClient _a2aClient;
|
||||
private readonly string? _id;
|
||||
private readonly string? _name;
|
||||
private readonly string? _description;
|
||||
private readonly string? _displayName;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="A2AAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="a2aClient">The A2A client to use for interacting with A2A agents.</param>
|
||||
/// <param name="id">The unique identifier for the agent.</param>
|
||||
/// <param name="name">The the name of the agent.</param>
|
||||
/// <param name="description">The description of the agent.</param>
|
||||
/// <param name="displayName">The display name of the agent.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
|
||||
public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, string? description = null, string? displayName = null, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
_ = Throw.IfNull(a2aClient);
|
||||
|
||||
this._a2aClient = a2aClient;
|
||||
this._id = id;
|
||||
this._name = name;
|
||||
this._description = description;
|
||||
this._displayName = displayName;
|
||||
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<A2AAgent>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AgentRunResponse> RunAsync(IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateInputMessages(messages);
|
||||
|
||||
var a2aMessage = messages.ToA2AMessage();
|
||||
|
||||
// Linking the message to the existing conversation, if any.
|
||||
a2aMessage.ContextId = thread?.ConversationId;
|
||||
|
||||
this._logger.LogA2AAgentInvokingAgent(nameof(RunAsync), this.Id, this.Name);
|
||||
|
||||
var a2aResponse = await this._a2aClient.SendMessageAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
|
||||
|
||||
if (a2aResponse is Message message)
|
||||
{
|
||||
UpdateThreadConversationId(thread, message);
|
||||
|
||||
return new AgentRunResponse
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = message.MessageId,
|
||||
RawRepresentation = message,
|
||||
Messages = [message.ToChatMessage()],
|
||||
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
|
||||
};
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Only message responses are supported from A2A agents. Received: {a2aResponse.GetType().FullName ?? "null"}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateInputMessages(messages);
|
||||
|
||||
var a2aMessage = messages.ToA2AMessage();
|
||||
|
||||
// Linking the message to the existing conversation, if any.
|
||||
a2aMessage.ContextId = thread?.ConversationId;
|
||||
|
||||
this._logger.LogA2AAgentInvokingAgent(nameof(RunStreamingAsync), this.Id, this.Name);
|
||||
|
||||
var a2aSseEvents = this._a2aClient.SendMessageStreamAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name);
|
||||
|
||||
await foreach (var sseEvent in a2aSseEvents)
|
||||
{
|
||||
if (sseEvent.Data is not Message message)
|
||||
{
|
||||
throw new NotSupportedException($"Only message responses are supported from A2A agents. Received: {sseEvent.Data?.GetType().FullName ?? "null"}");
|
||||
}
|
||||
|
||||
UpdateThreadConversationId(thread, message);
|
||||
|
||||
yield return new AgentRunResponseUpdate
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = message.MessageId,
|
||||
RawRepresentation = message,
|
||||
Role = ChatRole.Assistant,
|
||||
MessageId = message.MessageId,
|
||||
Contents = [.. message.Parts.Select(part => part.ToAIContent())],
|
||||
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Id => this._id ?? base.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Name => this._name ?? base.Name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => this._displayName ?? base.DisplayName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Description => this._description ?? base.Description;
|
||||
|
||||
private static void ValidateInputMessages(IReadOnlyCollection<ChatMessage> messages)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message.Role != ChatRole.User)
|
||||
{
|
||||
throw new ArgumentException($"All input messages for A2A agents must have the role '{ChatRole.User}'. Found '{message.Role}'.", nameof(messages));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateThreadConversationId(AgentThread? thread, Message message)
|
||||
{
|
||||
if (thread is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Surface cases where the A2A agent responds with a message that
|
||||
// has a different context Id than the thread's conversation Id.
|
||||
if (thread.ConversationId is not null && message.ContextId is not null && thread.ConversationId != message.ContextId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The {nameof(message.ContextId)} returned from the A2A agent is different from the conversation Id of the provided {nameof(AgentThread)}.");
|
||||
}
|
||||
|
||||
// Assign a server-generated context Id to the thread if it's not already set.
|
||||
thread.ConversationId ??= message.ContextId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for logging <see cref="A2AAgent"/> invocations.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static partial class A2AAgentLogMessages
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs <see cref="A2AAgent"/> invoking agent (started).
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "[{MethodName}] A2AAgent {AgentId}/{AgentName} invoking underlying A2A agent.")]
|
||||
public static partial void LogA2AAgentInvokingAgent(
|
||||
this ILogger logger,
|
||||
string methodName,
|
||||
string agentId,
|
||||
string? agentName);
|
||||
|
||||
/// <summary>
|
||||
/// Logs <see cref="A2AAgent"/> invoked agent (complete).
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{MethodName}] A2AAgent {AgentId}/{AgentName} invoked underlying A2A agent.")]
|
||||
public static partial void LogAgentChatClientInvokedAgent(
|
||||
this ILogger logger,
|
||||
string methodName,
|
||||
string agentId,
|
||||
string? agentName);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="A2ACardResolver"/>
|
||||
/// to simplify the creation of A2A agents.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These extensions bridge the gap between A2A SDK client objects
|
||||
/// and the Microsoft Extensions AI Agent framework.
|
||||
/// <para>
|
||||
/// They allow developers to easily create AI agents that can interact
|
||||
/// with A2A agents by handling the conversion from A2A clients to
|
||||
/// <see cref="A2AAgent"/> instances that implement the <see cref="AIAgent"/> interface.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class A2ACardResolverExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves an instance of <see cref="AIAgent"/> for an existing A2A agent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method can be used to create AI agents for A2A agents whose hosts support one of the A2A discovery mechanisms:
|
||||
/// <list type="bullet">
|
||||
/// <item><see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#1-well-known-uri">Well-Known URI</see></item>
|
||||
/// <item><see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="resolver">The <see cref="A2ACardResolver" /> to use for the agent creation.</param>
|
||||
/// <param name="httpClient">The <see cref="HttpClient"/> to use for HTTP requests.</param>
|
||||
/// <param name="loggerFactory">The logger factory for enabling logging within the agent.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use when retrieving the agent card.</param>
|
||||
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
|
||||
public static async Task<AIAgent> GetAIAgentAsync(this A2ACardResolver resolver, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Obtain the agent card from the resolver.
|
||||
var agentCard = await resolver.GetAgentCardAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Create the A2A client using the agent URL from the card.
|
||||
var a2aClient = new A2AClient(new Uri(agentCard.Url), httpClient);
|
||||
|
||||
return a2aClient.GetAIAgent(name: agentCard.Name, description: agentCard.Description, loggerFactory: loggerFactory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using A2A;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="A2AClient"/>
|
||||
/// to simplify the creation of A2A agents.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These extensions bridge the gap between A2A SDK client objects
|
||||
/// and the Microsoft Extensions AI Agent framework.
|
||||
/// <para>
|
||||
/// They allow developers to easily create AI agents that can interact
|
||||
/// with A2A agents by handling the conversion from A2A clients to
|
||||
/// <see cref="A2AAgent"/> instances that implement the <see cref="AIAgent"/> interface.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class A2AClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves an instance of <see cref="AIAgent"/> for an existing A2A agent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method can be used to create AI agents for A2A agents whose hosts support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#3-direct-configuration--private-discovery">Direct Configuration / Private Discovery</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
/// <param name="client">The <see cref="A2AClient" /> to use for the agent.</param>
|
||||
/// <param name="id">The unique identifier for the agent.</param>
|
||||
/// <param name="name">The the name of the agent.</param>
|
||||
/// <param name="description">The description of the agent.</param>
|
||||
/// <param name="displayName">The display name of the agent.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
|
||||
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
|
||||
public static AIAgent GetAIAgent(this A2AClient client, string? id = null, string? name = null, string? description = null, string? displayName = null, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
return new A2AAgent(client, id, name, description, displayName, loggerFactory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using A2A;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="Message"/> class.
|
||||
/// </summary>
|
||||
internal static class A2AMessageExtensions
|
||||
{
|
||||
public static ChatMessage ToChatMessage(this Message message)
|
||||
{
|
||||
List<AIContent>? aiContents = null;
|
||||
|
||||
foreach (var part in message.Parts)
|
||||
{
|
||||
(aiContents ??= []).Add(part.ToAIContent());
|
||||
}
|
||||
|
||||
return new ChatMessage(ChatRole.Assistant, aiContents)
|
||||
{
|
||||
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
|
||||
RawRepresentation = message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for A2A metadata dictionary.
|
||||
/// </summary>
|
||||
internal static class A2AMetadataExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a dictionary of metadata to an <see cref="AdditionalPropertiesDictionary"/>.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The metadata dictionary to convert.</param>
|
||||
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
|
||||
public static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
|
||||
{
|
||||
if (metadata is not { Count: > 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var additionalProperties = new AdditionalPropertiesDictionary();
|
||||
foreach (var kvp in metadata)
|
||||
{
|
||||
additionalProperties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
return additionalProperties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using A2A;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="Part"/> class.
|
||||
/// </summary>
|
||||
internal static class A2APartExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an A2A <see cref="Part"/> to an <see cref="AIContent"/>.
|
||||
/// </summary>
|
||||
/// <param name="part">The A2A part to convert.</param>
|
||||
/// <returns>The corresponding <see cref="AIContent"/>.</returns>
|
||||
public static AIContent ToAIContent(this Part part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
TextPart textPart => new TextContent(textPart.Text)
|
||||
{
|
||||
RawRepresentation = textPart,
|
||||
AdditionalProperties = textPart.Metadata.ToAdditionalProperties()
|
||||
},
|
||||
FilePart filePart when filePart.File is FileWithUri fileWithUrl => new HostedFileContent(fileWithUrl.Uri)
|
||||
{
|
||||
RawRepresentation = filePart,
|
||||
AdditionalProperties = filePart.Metadata.ToAdditionalProperties()
|
||||
},
|
||||
_ => throw new NotSupportedException($"Part type '{part.GetType().Name}' is not supported.")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using A2A;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="AIContent"/> class.
|
||||
/// </summary>
|
||||
internal static class AIContentExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a collection of <see cref="AIContent"/> to a list of <see cref="Part"/> objects.
|
||||
/// </summary>
|
||||
/// <param name="contents">The collection of AI contents to convert.</param>"
|
||||
/// <returns>The list of A2A <see cref="Part"/> objects.</returns>
|
||||
public static List<Part>? ToA2AParts(this IEnumerable<AIContent> contents)
|
||||
{
|
||||
List<Part>? parts = null;
|
||||
|
||||
foreach (var content in contents)
|
||||
{
|
||||
(parts ??= []).Add(content.ToA2APart());
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="AIContent"/> to a <see cref="Part"/> object."/>
|
||||
/// </summary>
|
||||
/// <param name="content">AI content to convert.</param>
|
||||
/// <returns>The corresponding A2A <see cref="Part"/> object.</returns>
|
||||
public static Part ToA2APart(this AIContent content)
|
||||
{
|
||||
return content switch
|
||||
{
|
||||
TextContent textContent => new TextPart { Text = textContent.Text },
|
||||
HostedFileContent hostedFileContent => new FilePart { File = new FileWithUri { Uri = hostedFileContent.FileId } },
|
||||
_ => throw new NotSupportedException($"Unsupported content type: {content.GetType().Name}."),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using A2A;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="ChatMessage"/> class.
|
||||
/// </summary>
|
||||
internal static class ChatMessageExtensions
|
||||
{
|
||||
public static Message ToA2AMessage(this IReadOnlyCollection<ChatMessage> messages)
|
||||
{
|
||||
List<Part> allParts = [];
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message.Contents.ToA2AParts() is { Count: > 0 } ps)
|
||||
{
|
||||
allParts.AddRange(ps);
|
||||
}
|
||||
}
|
||||
|
||||
return new Message
|
||||
{
|
||||
MessageId = Guid.NewGuid().ToString(),
|
||||
Role = MessageRole.User,
|
||||
Parts = allParts,
|
||||
};
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="A2A" />
|
||||
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-preview.5.25277.114" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-preview.5.25277.114" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft.Extensions.AI.Agents.A2A</Title>
|
||||
<Description>Defines AIAgent for interacting with application-to-application (A2A) agents.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Extensions.AI.Agents.Abstractions\Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Extensions.AI.Agents.A2A.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user