Skip to content

Commit 4050107

Browse files
authored
.NET: Add background agents support to HarnessAgent (microsoft#5977)
* Add background agents support to HarnessAgent * Add unit tests * Address PR comments
1 parent a12cc38 commit 4050107

5 files changed

Lines changed: 191 additions & 4 deletions

File tree

dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,7 @@ 5. Clear all completed tasks to free memory.
102102
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
103103
DisableToolApproval = true, // If enabled, this allows don't-ask-again approval functionality.
104104
DisableWebSearch = true,
105-
AIContextProviders =
106-
[
107-
new BackgroundAgentsProvider([webSearchAgent]),
108-
],
105+
BackgroundAgents = [webSearchAgent],
109106
ChatOptions = new ChatOptions
110107
{
111108
Instructions = parentInstructions,

dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using System.Diagnostics.CodeAnalysis;
66
using System.IO;
7+
using System.Linq;
78
using Microsoft.Agents.AI.Compaction;
89
using Microsoft.Extensions.AI;
910
using Microsoft.Shared.DiagnosticIds;
@@ -249,6 +250,15 @@ private static List<AIContextProvider> BuildContextProviders(HarnessAgentOptions
249250
providers.Add(skillsProvider);
250251
}
251252

253+
if (options?.BackgroundAgents is IEnumerable<AIAgent> backgroundAgents)
254+
{
255+
var materializedAgents = backgroundAgents.ToList();
256+
if (materializedAgents.Count > 0)
257+
{
258+
providers.Add(new BackgroundAgentsProvider(materializedAgents, options.BackgroundAgentsProviderOptions));
259+
}
260+
}
261+
252262
if (options?.AIContextProviders is IEnumerable<AIContextProvider> userProviders)
253263
{
254264
providers.AddRange(userProviders);

dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,26 @@ public sealed class HarnessAgentOptions
218218
/// This property is ignored when <see cref="DisableOpenTelemetry"/> is <see langword="true"/>.
219219
/// </remarks>
220220
public string? OpenTelemetrySourceName { get; set; }
221+
222+
/// <summary>
223+
/// Gets or sets the collection of background agents available for delegation via <see cref="BackgroundAgentsProvider"/>.
224+
/// </summary>
225+
/// <remarks>
226+
/// When non-null and non-empty, a <see cref="BackgroundAgentsProvider"/> is automatically included in the
227+
/// agent's context providers, enabling the agent to start, monitor, and retrieve results from background tasks.
228+
/// When <see langword="null"/> or empty, no <see cref="BackgroundAgentsProvider"/> is configured.
229+
/// Each agent in the collection must have a non-empty <see cref="AIAgent.Name"/> and names must be unique
230+
/// (case-insensitive). If these requirements are not met, <see cref="BackgroundAgentsProvider"/> will throw
231+
/// an <see cref="System.ArgumentException"/> during construction.
232+
/// </remarks>
233+
public IEnumerable<AIAgent>? BackgroundAgents { get; set; }
234+
235+
/// <summary>
236+
/// Gets or sets optional configuration for the <see cref="BackgroundAgentsProvider"/>.
237+
/// </summary>
238+
/// <remarks>
239+
/// Use this to customize instructions or agent list formatting for the background agents feature.
240+
/// This property is ignored when <see cref="BackgroundAgents"/> is <see langword="null"/> or empty.
241+
/// </remarks>
242+
public BackgroundAgentsProviderOptions? BackgroundAgentsProviderOptions { get; set; }
221243
}

dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public void DefaultPropertyValues()
3737
Assert.Null(options.FileAccessStore);
3838
Assert.Null(options.AgentModeProviderOptions);
3939
Assert.Null(options.AgentSkillsSource);
40+
Assert.Null(options.BackgroundAgents);
41+
Assert.Null(options.BackgroundAgentsProviderOptions);
4042
}
4143

4244
/// <summary>
@@ -52,6 +54,8 @@ public void PropertiesCanBeSetAndRetrieved()
5254
var fileAccessStore = new Mock<AgentFileStore>().Object;
5355
var agentModeOptions = new AgentModeProviderOptions();
5456
var skillsSource = new Mock<AgentSkillsSource>().Object;
57+
var backgroundAgents = new AIAgent[] { new Mock<AIAgent>().Object };
58+
var backgroundAgentsOptions = new BackgroundAgentsProviderOptions();
5559

5660
// Act
5761
var options = new HarnessAgentOptions
@@ -77,6 +81,8 @@ public void PropertiesCanBeSetAndRetrieved()
7781
AgentSkillsSource = skillsSource,
7882
DisableOpenTelemetry = true,
7983
OpenTelemetrySourceName = "custom-source",
84+
BackgroundAgents = backgroundAgents,
85+
BackgroundAgentsProviderOptions = backgroundAgentsOptions,
8086
};
8187

8288
// Assert
@@ -103,5 +109,7 @@ public void PropertiesCanBeSetAndRetrieved()
103109
Assert.Same(skillsSource, options.AgentSkillsSource);
104110
Assert.True(options.DisableOpenTelemetry);
105111
Assert.Equal("custom-source", options.OpenTelemetrySourceName);
112+
Assert.Same(backgroundAgents, options.BackgroundAgents);
113+
Assert.Same(backgroundAgentsOptions, options.BackgroundAgentsProviderOptions);
106114
}
107115
}

dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,4 +1197,154 @@ public async Task AllDefaults_AllFeaturesEnabledAsync()
11971197
}
11981198

11991199
#endregion
1200+
1201+
#region Feature: BackgroundAgentsProvider
1202+
1203+
/// <summary>
1204+
/// Verify that BackgroundAgentsProvider is included when BackgroundAgents are specified.
1205+
/// </summary>
1206+
[Fact]
1207+
public void BackgroundAgentsProvider_IncludedWhenAgentsSpecified()
1208+
{
1209+
// Arrange
1210+
var chatClient = new Mock<IChatClient>().Object;
1211+
var bgAgentMock = new Mock<AIAgent>();
1212+
bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent");
1213+
var options = CreateAllDisabledOptions();
1214+
options.BackgroundAgents = [bgAgentMock.Object];
1215+
1216+
// Act
1217+
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
1218+
var innerAgent = agent.GetService<ChatClientAgent>();
1219+
1220+
// Assert
1221+
Assert.NotNull(innerAgent?.AIContextProviders);
1222+
Assert.Contains(innerAgent!.AIContextProviders!, p => p is BackgroundAgentsProvider);
1223+
}
1224+
1225+
/// <summary>
1226+
/// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is null.
1227+
/// </summary>
1228+
[Fact]
1229+
public void BackgroundAgentsProvider_ExcludedWhenAgentsNull()
1230+
{
1231+
// Arrange
1232+
var chatClient = new Mock<IChatClient>().Object;
1233+
var options = CreateAllDisabledOptions();
1234+
options.BackgroundAgents = null;
1235+
1236+
// Act
1237+
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
1238+
var innerAgent = agent.GetService<ChatClientAgent>();
1239+
1240+
// Assert
1241+
Assert.NotNull(innerAgent);
1242+
if (innerAgent!.AIContextProviders != null)
1243+
{
1244+
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
1245+
}
1246+
}
1247+
1248+
/// <summary>
1249+
/// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is an empty collection.
1250+
/// </summary>
1251+
[Fact]
1252+
public void BackgroundAgentsProvider_ExcludedWhenAgentsEmpty()
1253+
{
1254+
// Arrange
1255+
var chatClient = new Mock<IChatClient>().Object;
1256+
var options = CreateAllDisabledOptions();
1257+
options.BackgroundAgents = Array.Empty<AIAgent>();
1258+
1259+
// Act
1260+
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
1261+
var innerAgent = agent.GetService<ChatClientAgent>();
1262+
1263+
// Assert
1264+
Assert.NotNull(innerAgent);
1265+
if (innerAgent!.AIContextProviders != null)
1266+
{
1267+
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
1268+
}
1269+
}
1270+
1271+
/// <summary>
1272+
/// Verify that BackgroundAgentsProviderOptions is passed through when specified.
1273+
/// </summary>
1274+
[Fact]
1275+
public async Task BackgroundAgentsProvider_UsesProvidedOptionsAsync()
1276+
{
1277+
// Arrange
1278+
var chatClient = new Mock<IChatClient>().Object;
1279+
var bgAgentMock = new Mock<AIAgent>();
1280+
bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent");
1281+
bgAgentMock.Setup(a => a.Description).Returns("A test background agent");
1282+
var providerOptions = new BackgroundAgentsProviderOptions
1283+
{
1284+
Instructions = "Custom instructions with {background_agents} list.",
1285+
};
1286+
var options = CreateAllDisabledOptions();
1287+
options.BackgroundAgents = [bgAgentMock.Object];
1288+
options.BackgroundAgentsProviderOptions = providerOptions;
1289+
1290+
// Act
1291+
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
1292+
var innerAgent = agent.GetService<ChatClientAgent>();
1293+
var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();
1294+
1295+
#pragma warning disable MAAI001
1296+
var invokingContext = new AIContextProvider.InvokingContext(
1297+
new Mock<AIAgent>().Object,
1298+
new Mock<AgentSession>().Object,
1299+
new AIContext());
1300+
#pragma warning restore MAAI001
1301+
1302+
AIContext result = await bgProvider.InvokingAsync(invokingContext);
1303+
1304+
// Assert — custom instructions template is used and agent info is included
1305+
Assert.NotNull(result.Instructions);
1306+
Assert.Contains("Custom instructions with", result.Instructions);
1307+
Assert.Contains("TestBackgroundAgent", result.Instructions);
1308+
}
1309+
1310+
/// <summary>
1311+
/// Verify that multiple background agents are all passed to the provider.
1312+
/// </summary>
1313+
[Fact]
1314+
public async Task BackgroundAgentsProvider_IncludesMultipleAgentsAsync()
1315+
{
1316+
// Arrange
1317+
var chatClient = new Mock<IChatClient>().Object;
1318+
var agent1Mock = new Mock<AIAgent>();
1319+
agent1Mock.Setup(a => a.Name).Returns("Agent1");
1320+
agent1Mock.Setup(a => a.Description).Returns("First agent");
1321+
var agent2Mock = new Mock<AIAgent>();
1322+
agent2Mock.Setup(a => a.Name).Returns("Agent2");
1323+
agent2Mock.Setup(a => a.Description).Returns("Second agent");
1324+
var options = CreateAllDisabledOptions();
1325+
options.BackgroundAgents = [agent1Mock.Object, agent2Mock.Object];
1326+
1327+
// Act
1328+
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
1329+
var innerAgent = agent.GetService<ChatClientAgent>();
1330+
var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();
1331+
1332+
#pragma warning disable MAAI001
1333+
var invokingContext = new AIContextProvider.InvokingContext(
1334+
new Mock<AIAgent>().Object,
1335+
new Mock<AgentSession>().Object,
1336+
new AIContext());
1337+
#pragma warning restore MAAI001
1338+
1339+
AIContext result = await bgProvider.InvokingAsync(invokingContext);
1340+
1341+
// Assert — both agents appear in the provider's instructions
1342+
Assert.NotNull(result.Instructions);
1343+
Assert.Contains("Agent1", result.Instructions);
1344+
Assert.Contains("First agent", result.Instructions);
1345+
Assert.Contains("Agent2", result.Instructions);
1346+
Assert.Contains("Second agent", result.Instructions);
1347+
}
1348+
1349+
#endregion
12001350
}

0 commit comments

Comments
 (0)