mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add background agents support to HarnessAgent (#5977)
* Add background agents support to HarnessAgent * Add unit tests * Address PR comments
This commit is contained in:
committed by
GitHub
Unverified
parent
a12cc3878e
commit
4050107942
+1
-4
@@ -102,10 +102,7 @@ AIAgent parentAgent =
|
||||
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
|
||||
DisableToolApproval = true, // If enabled, this allows don't-ask-again approval functionality.
|
||||
DisableWebSearch = true,
|
||||
AIContextProviders =
|
||||
[
|
||||
new BackgroundAgentsProvider([webSearchAgent]),
|
||||
],
|
||||
BackgroundAgents = [webSearchAgent],
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
Instructions = parentInstructions,
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
@@ -249,6 +250,15 @@ public sealed class HarnessAgent : DelegatingAIAgent
|
||||
providers.Add(skillsProvider);
|
||||
}
|
||||
|
||||
if (options?.BackgroundAgents is IEnumerable<AIAgent> backgroundAgents)
|
||||
{
|
||||
var materializedAgents = backgroundAgents.ToList();
|
||||
if (materializedAgents.Count > 0)
|
||||
{
|
||||
providers.Add(new BackgroundAgentsProvider(materializedAgents, options.BackgroundAgentsProviderOptions));
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.AIContextProviders is IEnumerable<AIContextProvider> userProviders)
|
||||
{
|
||||
providers.AddRange(userProviders);
|
||||
|
||||
@@ -218,4 +218,26 @@ public sealed class HarnessAgentOptions
|
||||
/// This property is ignored when <see cref="DisableOpenTelemetry"/> is <see langword="true"/>.
|
||||
/// </remarks>
|
||||
public string? OpenTelemetrySourceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of background agents available for delegation via <see cref="BackgroundAgentsProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When non-null and non-empty, a <see cref="BackgroundAgentsProvider"/> is automatically included in the
|
||||
/// agent's context providers, enabling the agent to start, monitor, and retrieve results from background tasks.
|
||||
/// When <see langword="null"/> or empty, no <see cref="BackgroundAgentsProvider"/> is configured.
|
||||
/// Each agent in the collection must have a non-empty <see cref="AIAgent.Name"/> and names must be unique
|
||||
/// (case-insensitive). If these requirements are not met, <see cref="BackgroundAgentsProvider"/> will throw
|
||||
/// an <see cref="System.ArgumentException"/> during construction.
|
||||
/// </remarks>
|
||||
public IEnumerable<AIAgent>? BackgroundAgents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets optional configuration for the <see cref="BackgroundAgentsProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this to customize instructions or agent list formatting for the background agents feature.
|
||||
/// This property is ignored when <see cref="BackgroundAgents"/> is <see langword="null"/> or empty.
|
||||
/// </remarks>
|
||||
public BackgroundAgentsProviderOptions? BackgroundAgentsProviderOptions { get; set; }
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ public class HarnessAgentOptionsTests
|
||||
Assert.Null(options.FileAccessStore);
|
||||
Assert.Null(options.AgentModeProviderOptions);
|
||||
Assert.Null(options.AgentSkillsSource);
|
||||
Assert.Null(options.BackgroundAgents);
|
||||
Assert.Null(options.BackgroundAgentsProviderOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -52,6 +54,8 @@ public class HarnessAgentOptionsTests
|
||||
var fileAccessStore = new Mock<AgentFileStore>().Object;
|
||||
var agentModeOptions = new AgentModeProviderOptions();
|
||||
var skillsSource = new Mock<AgentSkillsSource>().Object;
|
||||
var backgroundAgents = new AIAgent[] { new Mock<AIAgent>().Object };
|
||||
var backgroundAgentsOptions = new BackgroundAgentsProviderOptions();
|
||||
|
||||
// Act
|
||||
var options = new HarnessAgentOptions
|
||||
@@ -77,6 +81,8 @@ public class HarnessAgentOptionsTests
|
||||
AgentSkillsSource = skillsSource,
|
||||
DisableOpenTelemetry = true,
|
||||
OpenTelemetrySourceName = "custom-source",
|
||||
BackgroundAgents = backgroundAgents,
|
||||
BackgroundAgentsProviderOptions = backgroundAgentsOptions,
|
||||
};
|
||||
|
||||
// Assert
|
||||
@@ -103,5 +109,7 @@ public class HarnessAgentOptionsTests
|
||||
Assert.Same(skillsSource, options.AgentSkillsSource);
|
||||
Assert.True(options.DisableOpenTelemetry);
|
||||
Assert.Equal("custom-source", options.OpenTelemetrySourceName);
|
||||
Assert.Same(backgroundAgents, options.BackgroundAgents);
|
||||
Assert.Same(backgroundAgentsOptions, options.BackgroundAgentsProviderOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1197,4 +1197,154 @@ public class HarnessAgentTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Feature: BackgroundAgentsProvider
|
||||
|
||||
/// <summary>
|
||||
/// Verify that BackgroundAgentsProvider is included when BackgroundAgents are specified.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BackgroundAgentsProvider_IncludedWhenAgentsSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var bgAgentMock = new Mock<AIAgent>();
|
||||
bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent");
|
||||
var options = CreateAllDisabledOptions();
|
||||
options.BackgroundAgents = [bgAgentMock.Object];
|
||||
|
||||
// Act
|
||||
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
||||
var innerAgent = agent.GetService<ChatClientAgent>();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(innerAgent?.AIContextProviders);
|
||||
Assert.Contains(innerAgent!.AIContextProviders!, p => p is BackgroundAgentsProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BackgroundAgentsProvider_ExcludedWhenAgentsNull()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var options = CreateAllDisabledOptions();
|
||||
options.BackgroundAgents = null;
|
||||
|
||||
// Act
|
||||
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
||||
var innerAgent = agent.GetService<ChatClientAgent>();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(innerAgent);
|
||||
if (innerAgent!.AIContextProviders != null)
|
||||
{
|
||||
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is an empty collection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BackgroundAgentsProvider_ExcludedWhenAgentsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var options = CreateAllDisabledOptions();
|
||||
options.BackgroundAgents = Array.Empty<AIAgent>();
|
||||
|
||||
// Act
|
||||
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
||||
var innerAgent = agent.GetService<ChatClientAgent>();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(innerAgent);
|
||||
if (innerAgent!.AIContextProviders != null)
|
||||
{
|
||||
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that BackgroundAgentsProviderOptions is passed through when specified.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task BackgroundAgentsProvider_UsesProvidedOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var bgAgentMock = new Mock<AIAgent>();
|
||||
bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent");
|
||||
bgAgentMock.Setup(a => a.Description).Returns("A test background agent");
|
||||
var providerOptions = new BackgroundAgentsProviderOptions
|
||||
{
|
||||
Instructions = "Custom instructions with {background_agents} list.",
|
||||
};
|
||||
var options = CreateAllDisabledOptions();
|
||||
options.BackgroundAgents = [bgAgentMock.Object];
|
||||
options.BackgroundAgentsProviderOptions = providerOptions;
|
||||
|
||||
// Act
|
||||
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
||||
var innerAgent = agent.GetService<ChatClientAgent>();
|
||||
var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();
|
||||
|
||||
#pragma warning disable MAAI001
|
||||
var invokingContext = new AIContextProvider.InvokingContext(
|
||||
new Mock<AIAgent>().Object,
|
||||
new Mock<AgentSession>().Object,
|
||||
new AIContext());
|
||||
#pragma warning restore MAAI001
|
||||
|
||||
AIContext result = await bgProvider.InvokingAsync(invokingContext);
|
||||
|
||||
// Assert — custom instructions template is used and agent info is included
|
||||
Assert.NotNull(result.Instructions);
|
||||
Assert.Contains("Custom instructions with", result.Instructions);
|
||||
Assert.Contains("TestBackgroundAgent", result.Instructions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that multiple background agents are all passed to the provider.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task BackgroundAgentsProvider_IncludesMultipleAgentsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var agent1Mock = new Mock<AIAgent>();
|
||||
agent1Mock.Setup(a => a.Name).Returns("Agent1");
|
||||
agent1Mock.Setup(a => a.Description).Returns("First agent");
|
||||
var agent2Mock = new Mock<AIAgent>();
|
||||
agent2Mock.Setup(a => a.Name).Returns("Agent2");
|
||||
agent2Mock.Setup(a => a.Description).Returns("Second agent");
|
||||
var options = CreateAllDisabledOptions();
|
||||
options.BackgroundAgents = [agent1Mock.Object, agent2Mock.Object];
|
||||
|
||||
// Act
|
||||
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
||||
var innerAgent = agent.GetService<ChatClientAgent>();
|
||||
var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();
|
||||
|
||||
#pragma warning disable MAAI001
|
||||
var invokingContext = new AIContextProvider.InvokingContext(
|
||||
new Mock<AIAgent>().Object,
|
||||
new Mock<AgentSession>().Object,
|
||||
new AIContext());
|
||||
#pragma warning restore MAAI001
|
||||
|
||||
AIContext result = await bgProvider.InvokingAsync(invokingContext);
|
||||
|
||||
// Assert — both agents appear in the provider's instructions
|
||||
Assert.NotNull(result.Instructions);
|
||||
Assert.Contains("Agent1", result.Instructions);
|
||||
Assert.Contains("First agent", result.Instructions);
|
||||
Assert.Contains("Agent2", result.Instructions);
|
||||
Assert.Contains("Second agent", result.Instructions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user