.NET: Remove required token params from HarnessAgent, make compaction opt-in (#6409)

* Move token params from HarnessAgent constructor to options

Remove the required maxContextWindowTokens and maxOutputTokens
constructor parameters from HarnessAgent and AsHarnessAgent, replacing
them with optional MaxContextWindowTokens and MaxOutputTokens properties
on HarnessAgentOptions.

When both values are provided, compaction is enabled as before (in-loop
CompactionProvider and chat reducer on the default InMemoryChatHistory
Provider). When either is null, compaction is disabled entirely, making
it opt-in.

New constructor: HarnessAgent(IChatClient, HarnessAgentOptions?,
ILoggerFactory?, IServiceProvider?)

Closes #6333

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Improving comments.

* feat: Add custom CompactionStrategy and DisableCompaction to HarnessAgentOptions

Allow users to provide their own CompactionStrategy via options, with
a clear priority system:
1. DisableCompaction=true: no compaction regardless of other settings
2. Custom CompactionStrategy provided: use it (token params ignored)
3. Both MaxContextWindowTokens and MaxOutputTokens set: default strategy
4. Otherwise: no compaction

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: Address PR review comments on compaction opt-in

- Update chatClient param XML doc to reflect compaction is opt-in
- Strengthen compaction tests to assert ChatReducer is null/not-null
  rather than just asserting construction succeeds

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
westey
2026-06-09 14:06:00 +01:00
committed by GitHub
Unverified
parent 9486c76ef8
commit 96d242fa7f
8 changed files with 346 additions and 150 deletions
@@ -79,8 +79,10 @@ AIAgent agent =
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions
.AsHarnessAgent(new HarnessAgentOptions
{
MaxContextWindowTokens = MaxContextWindowTokens,
MaxOutputTokens = MaxOutputTokens,
Name = "ResearchAgent",
Description = "A research assistant that plans and executes research tasks.",
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
@@ -44,8 +44,10 @@ AIAgent webSearchAgent =
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions
.AsHarnessAgent(new HarnessAgentOptions
{
MaxContextWindowTokens = MaxContextWindowTokens,
MaxOutputTokens = MaxOutputTokens,
Name = "WebSearchAgent",
Description = "An agent that can search the web to find information.",
OpenTelemetrySourceName = TracingSourceName,
@@ -92,8 +94,10 @@ AIAgent parentAgent =
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions
.AsHarnessAgent(new HarnessAgentOptions
{
MaxContextWindowTokens = MaxContextWindowTokens,
MaxOutputTokens = MaxOutputTokens,
Name = "StockPriceResearcher",
Description = "An agent that researches stock prices using background agents.",
OpenTelemetrySourceName = TracingSourceName,
@@ -68,8 +68,10 @@ AIAgent agent =
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions
.AsHarnessAgent(new HarnessAgentOptions
{
MaxContextWindowTokens = MaxContextWindowTokens,
MaxOutputTokens = MaxOutputTokens,
Name = "DataAnalyst",
Description = "A data analyst assistant that reads, analyzes, and processes data files.",
OpenTelemetrySourceName = TracingSourceName,
@@ -89,8 +89,10 @@ AIAgent agent =
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions
.AsHarnessAgent(new HarnessAgentOptions
{
MaxContextWindowTokens = MaxContextWindowTokens,
MaxOutputTokens = MaxOutputTokens,
Name = "CodeExecutionAgent",
Description = "A technical assistant with sandboxed code execution and skill-based workflows.",
OpenTelemetrySourceName = TracingSourceName,
@@ -16,23 +16,16 @@ public static class ChatClientHarnessExtensions
{
/// <summary>
/// Creates a new <see cref="HarnessAgent"/> that wraps this <see cref="IChatClient"/> with a pre-configured
/// pipeline including function invocation, per-service-call chat history persistence, and in-loop compaction.
/// pipeline including function invocation, per-service-call chat history persistence, optional in-loop compaction, and a rich set
/// of default context providers and agent decorators.
/// </summary>
/// <param name="chatClient">
/// The <see cref="IChatClient"/> that provides access to the underlying AI model.
/// </param>
/// <param name="maxContextWindowTokens">
/// The maximum number of tokens the model's context window supports (e.g., 1,050,000 for gpt-5.4).
/// Used to configure the compaction strategy.
/// </param>
/// <param name="maxOutputTokens">
/// The maximum number of output tokens the model can generate per response (e.g., 128,000 for gpt-5.4).
/// Used to configure the compaction strategy.
/// </param>
/// <param name="options">
/// Optional configuration options for the agent, including instructions override, tools,
/// additional context providers, and chat history provider.
/// When <see langword="null"/>, the agent uses built-in default settings.
/// additional context providers, chat history provider, and compaction settings.
/// When <see langword="null"/>, the agent uses built-in default settings with compaction disabled.
/// </param>
/// <param name="loggerFactory">
/// Optional logger factory for creating loggers used by the agent and its components.
@@ -43,10 +36,8 @@ public static class ChatClientHarnessExtensions
/// <returns>A new <see cref="HarnessAgent"/> instance.</returns>
public static HarnessAgent AsHarnessAgent(
this IChatClient chatClient,
int maxContextWindowTokens,
int maxOutputTokens,
HarnessAgentOptions? options = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null) =>
new(chatClient, maxContextWindowTokens, maxOutputTokens, options, loggerFactory, services);
new(chatClient, options, loggerFactory, services);
}
@@ -18,50 +18,65 @@ namespace Microsoft.Agents.AI;
/// <summary>
/// A pre-configured <see cref="DelegatingAIAgent"/> that wraps a <see cref="ChatClientAgent"/> with
/// function invocation, per-service-call chat history persistence, in-loop compaction, and a rich set
/// function invocation, per-service-call chat history persistence, optional in-loop compaction, and a rich set
/// of default context providers and agent decorators.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="HarnessAgent"/> assembles the following pipeline from a caller-supplied <see cref="IChatClient"/>:
/// <see cref="HarnessAgent"/> provides an opinionated, batteries-included agent suitable for
/// interactive agentic scenarios such as research, coding, data analysis, and general task automation.
/// It assembles a full pipeline from a caller-supplied <see cref="IChatClient"/> so that callers
/// only need to configure the parts they want to customize.
/// </para>
/// <para>
/// <strong>Chat client pipeline (inner to outer):</strong>
/// <list type="number">
/// <item><description><see cref="FunctionInvokingChatClient"/> — automatic function/tool invocation.</description></item>
/// <item><description><see cref="MessageInjectingChatClient"/> — allows external code to inject messages into the conversation mid-stream.</description></item>
/// <item><description><see cref="PerServiceCallChatHistoryPersistingChatClient"/> — persists chat history after every individual service call within a function-invocation loop.</description></item>
/// <item><description><see cref="AIContextProviderChatClient"/> with a <see cref="CompactionProvider"/> — applies context-window compaction before each call so long function-invocation loops do not overflow the context window.</description></item>
/// <item><description><see cref="FunctionInvokingChatClient"/> — automatic function/tool invocation with configurable iteration limits.</description></item>
/// <item><description><see cref="MessageInjectingChatClient"/> — allows external code to inject messages into the conversation mid-stream (e.g., for user interrupts).</description></item>
/// <item><description><see cref="PerServiceCallChatHistoryPersistingChatClient"/> — persists chat history after every individual service call within a function-invocation loop, enabling crash recovery and history inspection.</description></item>
/// <item><description><see cref="AIContextProviderChatClient"/> with a <see cref="CompactionProvider"/> — applies context-window compaction before each call so long function-invocation loops do not overflow the context window. Only included when <see cref="HarnessAgentOptions.MaxContextWindowTokens"/> and <see cref="HarnessAgentOptions.MaxOutputTokens"/> are both provided.</description></item>
/// </list>
/// </para>
/// <para>
/// By default, the following context providers are included (each can be disabled via <see cref="HarnessAgentOptions"/>):
/// <strong>Context providers (each enabled by default, individually disableable via <see cref="HarnessAgentOptions"/>):</strong>
/// <list type="bullet">
/// <item><description><see cref="TodoProvider"/> — todo list management.</description></item>
/// <item><description><see cref="AgentModeProvider"/> — agent mode tracking (plan/execute).</description></item>
/// <item><description><see cref="FileMemoryProvider"/> — file-based session memory.</description></item>
/// <item><description><see cref="FileAccessProvider"/> — shared file access.</description></item>
/// <item><description><see cref="AgentSkillsProvider"/> — skill discovery and loading.</description></item>
/// <item><description><see cref="TodoProvider"/> — persistent todo list that the agent uses to track multi-step plans. Disable with <see cref="HarnessAgentOptions.DisableTodoProvider"/>.</description></item>
/// <item><description><see cref="AgentModeProvider"/> — mode tracking (e.g., "plan" vs "execute") that the agent uses to structure its work. Disable with <see cref="HarnessAgentOptions.DisableAgentModeProvider"/>.</description></item>
/// <item><description><see cref="FileMemoryProvider"/> — file-based session memory allowing the agent to persist notes and artifacts across turns. Disable with <see cref="HarnessAgentOptions.DisableFileMemory"/>.</description></item>
/// <item><description><see cref="FileAccessProvider"/> — shared file access providing read/write tools for a working directory. Disable with <see cref="HarnessAgentOptions.DisableFileAccess"/>.</description></item>
/// <item><description><see cref="AgentSkillsProvider"/> — discovers and loads skill definitions from the file system, enabling dynamic tool sets. Disable with <see cref="HarnessAgentOptions.DisableAgentSkillsProvider"/>.</description></item>
/// </list>
/// </para>
/// <para>
/// The agent is also wrapped with the following decorators by default (each can be disabled):
/// <strong>Optional context providers (enabled via <see cref="HarnessAgentOptions"/>):</strong>
/// <list type="bullet">
/// <item><description><see cref="ToolApprovalAgent"/> — "don't ask again" tool approval rules.</description></item>
/// <item><description><see cref="OpenTelemetryAgent"/> — OpenTelemetry instrumentation.</description></item>
/// <item><description><see cref="BackgroundAgentsProvider"/> — enables delegation to background agents for parallel work. Enable by setting <see cref="HarnessAgentOptions.BackgroundAgents"/>.</description></item>
/// <item><description><c>ShellEnvironmentProvider</c> — injects OS/shell/CWD information and a shell execution tool. Enable by setting <c>HarnessAgentOptions.ShellExecutor</c> (.NET only).</description></item>
/// </list>
/// </para>
/// <para>
/// A <see cref="HostedWebSearchTool"/> is added to the chat options by default (can be disabled via
/// <see cref="HarnessAgentOptions.DisableWebSearch"/>).
/// <strong>Agent decorators (each enabled by default, individually disableable):</strong>
/// <list type="bullet">
/// <item><description><see cref="ToolApprovalAgent"/> — "don't ask again" tool approval rules enabling safe unattended execution. Disable with <see cref="HarnessAgentOptions.DisableToolApproval"/>.</description></item>
/// <item><description><see cref="OpenTelemetryAgent"/> — OpenTelemetry instrumentation following semantic conventions for generative AI. Disable with <see cref="HarnessAgentOptions.DisableOpenTelemetry"/>.</description></item>
/// </list>
/// </para>
/// <para>
/// The underlying <see cref="ChatClientAgent"/> is configured with
/// <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> and
/// <see cref="ChatClientAgentOptions.RequirePerServiceCallChatHistoryPersistence"/> set to <see langword="true"/>
/// to match the manually-assembled pipeline.
/// <strong>Default tools:</strong>
/// <list type="bullet">
/// <item><description><see cref="HostedWebSearchTool"/> — a hosted web search tool added to chat options by default. Disable with <see cref="HarnessAgentOptions.DisableWebSearch"/>.</description></item>
/// </list>
/// </para>
/// <para>
/// When no <see cref="HarnessAgentOptions.ChatHistoryProvider"/> is supplied, the agent defaults to an
/// <see cref="InMemoryChatHistoryProvider"/> whose chat reducer applies the same compaction strategy,
/// keeping in-memory history from growing unboundedly across sessions.
/// <strong>Chat history:</strong> When no <see cref="HarnessAgentOptions.ChatHistoryProvider"/> is supplied,
/// the agent defaults to an <see cref="InMemoryChatHistoryProvider"/>. If compaction is enabled, the provider
/// is configured with a compaction-based chat reducer to keep in-memory history bounded. Otherwise, no reducer
/// is applied.
/// </para>
/// <para>
/// <strong>Default instructions:</strong> The agent includes built-in system instructions (<see cref="DefaultInstructions"/>)
/// that guide general tool usage and reasoning patterns. These can be overridden via <see cref="HarnessAgentOptions.HarnessInstructions"/>
/// and combined with agent-specific instructions via <see cref="ChatOptions.Instructions"/>.
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
@@ -90,21 +105,13 @@ public sealed class HarnessAgent : DelegatingAIAgent
/// </summary>
/// <param name="chatClient">
/// The <see cref="IChatClient"/> that provides access to the underlying AI model.
/// The agent wraps this client in a function-invocation, per-service-call persistence,
/// and compaction pipeline automatically.
/// </param>
/// <param name="maxContextWindowTokens">
/// The maximum number of tokens the model's context window supports (e.g., 1,050,000 for gpt-5.4).
/// Used to configure the compaction strategy.
/// </param>
/// <param name="maxOutputTokens">
/// The maximum number of output tokens the model can generate per response (e.g., 128,000 for gpt-5.4).
/// Used to configure the compaction strategy and to limit the model's output.
/// The agent wraps this client in a function-invocation and per-service-call persistence pipeline.
/// When compaction is enabled via <paramref name="options"/>, a compaction decorator is also added.
/// </param>
/// <param name="options">
/// Optional configuration options for the agent, including instructions override, tools,
/// additional context providers, and chat history provider.
/// When <see langword="null"/>, the agent uses built-in default settings.
/// additional context providers, chat history provider, and compaction settings.
/// When <see langword="null"/>, the agent uses built-in default settings with compaction disabled.
/// </param>
/// <param name="loggerFactory">
/// Optional logger factory for creating loggers used by the agent and its components.
@@ -116,23 +123,22 @@ public sealed class HarnessAgent : DelegatingAIAgent
/// <paramref name="chatClient"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="maxContextWindowTokens"/> is not positive, or
/// <paramref name="maxOutputTokens"/> is negative or greater than or equal to <paramref name="maxContextWindowTokens"/>.
/// <see cref="HarnessAgentOptions.MaxContextWindowTokens"/> is not positive, or
/// <see cref="HarnessAgentOptions.MaxOutputTokens"/> is negative or greater than or equal to
/// <see cref="HarnessAgentOptions.MaxContextWindowTokens"/> (when both are provided).
/// </exception>
public HarnessAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null)
public HarnessAgent(IChatClient chatClient, HarnessAgentOptions? options = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null)
: base(BuildAgent(
Throw.IfNull(chatClient),
maxContextWindowTokens,
maxOutputTokens,
options,
loggerFactory,
services))
{
}
private static AIAgent BuildAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services)
private static AIAgent BuildAgent(IChatClient chatClient, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services)
{
ChatClientAgent innerAgent = BuildInnerAgent(chatClient, maxContextWindowTokens, maxOutputTokens, options, loggerFactory, services);
ChatClientAgent innerAgent = BuildInnerAgent(chatClient, options, loggerFactory, services);
AIAgentBuilder builder = innerAgent.AsBuilder();
@@ -149,17 +155,35 @@ public sealed class HarnessAgent : DelegatingAIAgent
return builder.Build(services);
}
private static ChatClientAgent BuildInnerAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services)
private static ChatClientAgent BuildInnerAgent(IChatClient chatClient, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services)
{
var compactionStrategy = new ContextWindowCompactionStrategy(
maxContextWindowTokens: maxContextWindowTokens,
maxOutputTokens: maxOutputTokens);
// Determine compaction strategy:
// 1. DisableCompaction = true → no compaction
// 2. Custom CompactionStrategy provided → use it (ignore token params)
// 3. Both token params provided → build default ContextWindowCompactionStrategy
// 4. Otherwise → no compaction
CompactionStrategy? compactionStrategy = null;
if (options?.DisableCompaction is not true)
{
if (options?.CompactionStrategy is CompactionStrategy customStrategy)
{
compactionStrategy = customStrategy;
}
else if (options?.MaxContextWindowTokens is int maxCtx && options?.MaxOutputTokens is int maxOut)
{
compactionStrategy = new ContextWindowCompactionStrategy(
maxContextWindowTokens: maxCtx,
maxOutputTokens: maxOut);
}
}
ChatHistoryProvider chatHistoryProvider = options?.ChatHistoryProvider
?? new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
ChatReducer = compactionStrategy.AsChatReducer(),
});
?? (compactionStrategy is not null
? new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
ChatReducer = compactionStrategy.AsChatReducer(),
})
: new InMemoryChatHistoryProvider());
string harnessInstructions = options?.HarnessInstructions ?? DefaultInstructions;
string? agentInstructions = options?.ChatOptions?.Instructions;
@@ -172,9 +196,11 @@ public sealed class HarnessAgent : DelegatingAIAgent
(false, false) => $"{harnessInstructions}\n\n{agentInstructions}",
};
ChatOptions chatOptions = BuildChatOptions(options, instructions, maxOutputTokens);
ChatOptions chatOptions = BuildChatOptions(options, instructions, options?.MaxOutputTokens);
var compactionProvider = new CompactionProvider(compactionStrategy, loggerFactory: loggerFactory);
CompactionProvider? compactionProvider = compactionStrategy is not null
? new CompactionProvider(compactionStrategy, loggerFactory: loggerFactory)
: null;
IEnumerable<AIContextProvider> contextProviders = BuildContextProviders(options, loggerFactory);
@@ -185,13 +211,19 @@ public sealed class HarnessAgent : DelegatingAIAgent
chatClientBuilder.UseNonApprovalRequiredFunctionBypassing();
}
return chatClientBuilder
ChatClientBuilder pipeline = chatClientBuilder
.UseFunctionInvocation(loggerFactory, configure: options?.MaximumIterationsPerRequest is int maxIterations
? ficc => ficc.MaximumIterationsPerRequest = maxIterations
: null)
.UseMessageInjection()
.UsePerServiceCallChatHistoryPersistence()
.UseAIContextProviders(compactionProvider)
.UsePerServiceCallChatHistoryPersistence();
if (compactionProvider is not null)
{
pipeline = pipeline.UseAIContextProviders(compactionProvider);
}
return pipeline
.BuildAIAgent(new ChatClientAgentOptions
{
Id = options?.Id,
@@ -209,11 +241,15 @@ public sealed class HarnessAgent : DelegatingAIAgent
services);
}
private static ChatOptions BuildChatOptions(HarnessAgentOptions? options, string instructions, int maxOutputTokens)
private static ChatOptions BuildChatOptions(HarnessAgentOptions? options, string instructions, int? maxOutputTokens)
{
ChatOptions result = options?.ChatOptions?.Clone() ?? new ChatOptions();
result.Instructions = instructions;
result.MaxOutputTokens ??= maxOutputTokens;
if (maxOutputTokens.HasValue)
{
result.MaxOutputTokens ??= maxOutputTokens.Value;
}
if (options?.DisableWebSearch is not true)
{
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.AI.Compaction;
#if NET
using Microsoft.Agents.AI.Tools.Shell;
#endif
@@ -31,6 +32,68 @@ public sealed class HarnessAgentOptions
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the maximum number of tokens the model's context window supports (e.g., 1,050,000 for gpt-5.4).
/// </summary>
/// <remarks>
/// <para>
/// When both <see cref="MaxContextWindowTokens"/> and <see cref="MaxOutputTokens"/> are provided (and no
/// custom <see cref="CompactionStrategy"/> is set), a default <see cref="ContextWindowCompactionStrategy"/>
/// is constructed from these values to prevent function-invocation loops from overflowing the context window.
/// </para>
/// <para>
/// Ignored when <see cref="CompactionStrategy"/> is provided or when <see cref="DisableCompaction"/> is
/// <see langword="true"/>.
/// </para>
/// </remarks>
public int? MaxContextWindowTokens { get; set; }
/// <summary>
/// Gets or sets the maximum number of output tokens the model can generate per response (e.g., 128,000 for gpt-5.4).
/// </summary>
/// <remarks>
/// <para>
/// When set, this value is used as the default for <see cref="ChatOptions"/>.<see cref="ChatOptions.MaxOutputTokens"/>
/// when not explicitly configured.
/// </para>
/// <para>
/// For compaction purposes, this value is used together with <see cref="MaxContextWindowTokens"/> to construct a
/// default <see cref="ContextWindowCompactionStrategy"/> — but only when no custom <see cref="CompactionStrategy"/>
/// is provided and <see cref="DisableCompaction"/> is <see langword="false"/>.
/// </para>
/// </remarks>
public int? MaxOutputTokens { get; set; }
/// <summary>
/// Gets or sets a custom <see cref="Compaction.CompactionStrategy"/> to use for in-loop context-window compaction.
/// </summary>
/// <remarks>
/// <para>
/// When provided, this strategy is used directly and <see cref="MaxContextWindowTokens"/> and
/// <see cref="MaxOutputTokens"/> are ignored for compaction purposes (<see cref="MaxOutputTokens"/> is still
/// used as the default for <see cref="ChatOptions"/>.<see cref="ChatOptions.MaxOutputTokens"/> if set).
/// </para>
/// <para>
/// When <see langword="null"/> and both <see cref="MaxContextWindowTokens"/> and <see cref="MaxOutputTokens"/>
/// are provided, a default <see cref="ContextWindowCompactionStrategy"/> is constructed from those values.
/// </para>
/// <para>
/// This property is ignored when <see cref="DisableCompaction"/> is <see langword="true"/>.
/// </para>
/// </remarks>
public CompactionStrategy? CompactionStrategy { get; set; }
/// <summary>
/// Gets or sets a value indicating whether in-loop compaction is disabled.
/// </summary>
/// <remarks>
/// When <see langword="true"/>, compaction is disabled regardless of <see cref="CompactionStrategy"/>,
/// <see cref="MaxContextWindowTokens"/>, or <see cref="MaxOutputTokens"/> settings. No
/// <see cref="CompactionProvider"/> is added to the chat client pipeline, and the default
/// <see cref="InMemoryChatHistoryProvider"/> is configured without a chat reducer.
/// </remarks>
public bool DisableCompaction { get; set; }
/// <summary>
/// Gets or sets additional chat options such as tools for the agent to use.
/// </summary>
@@ -68,9 +131,9 @@ public sealed class HarnessAgentOptions
/// Gets or sets the <see cref="ChatHistoryProvider"/> to use for storing chat history.
/// </summary>
/// <remarks>
/// When <see langword="null"/>, the agent defaults to an <see cref="InMemoryChatHistoryProvider"/>
/// configured with a compaction-based chat reducer derived from the <c>maxContextWindowTokens</c>
/// and <c>maxOutputTokens</c> constructor parameters of <see cref="HarnessAgent"/>.
/// When <see langword="null"/>, the agent defaults to an <see cref="InMemoryChatHistoryProvider"/>.
/// If <see cref="MaxContextWindowTokens"/> and <see cref="MaxOutputTokens"/> are both provided,
/// the default provider is configured with a compaction-based chat reducer; otherwise, no reducer is applied.
/// </remarks>
public ChatHistoryProvider? ChatHistoryProvider { get; set; }
@@ -21,9 +21,12 @@ public class HarnessAgentTests
/// <summary>
/// Creates a HarnessAgent with all default features disabled to isolate tests for specific behaviors.
/// Compaction is enabled by default for backward compatibility with existing tests.
/// </summary>
private static HarnessAgentOptions CreateAllDisabledOptions() => new()
{
MaxContextWindowTokens = TestMaxContextWindowTokens,
MaxOutputTokens = TestMaxOutputTokens,
DisableToolApproval = true,
DisableOpenTelemetry = true,
DisableFileMemory = true,
@@ -43,7 +46,7 @@ public class HarnessAgentTests
public void Constructor_ThrowsWhenChatClientIsNull()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new HarnessAgent(null!, TestMaxContextWindowTokens, TestMaxOutputTokens));
Assert.Throws<ArgumentNullException>(() => new HarnessAgent(null!));
}
/// <summary>
@@ -54,9 +57,10 @@ public class HarnessAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
var options = new HarnessAgentOptions { MaxContextWindowTokens = 0, MaxOutputTokens = TestMaxOutputTokens };
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new HarnessAgent(chatClient, 0, TestMaxOutputTokens));
Assert.Throws<ArgumentOutOfRangeException>(() => new HarnessAgent(chatClient, options));
}
/// <summary>
@@ -67,9 +71,10 @@ public class HarnessAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
var options = new HarnessAgentOptions { MaxContextWindowTokens = 100_000, MaxOutputTokens = 100_000 };
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new HarnessAgent(chatClient, 100_000, 100_000));
Assert.Throws<ArgumentOutOfRangeException>(() => new HarnessAgent(chatClient, options));
}
/// <summary>
@@ -82,7 +87,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens);
var agent = new HarnessAgent(chatClient);
// Assert
Assert.NotNull(agent);
@@ -105,7 +110,7 @@ public class HarnessAgentTests
options.Description = "A test agent";
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
// Assert
Assert.Equal("TestAgent", agent.Name);
@@ -124,7 +129,7 @@ public class HarnessAgentTests
options.Id = "my-agent-id";
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
// Assert
Assert.Equal("my-agent-id", agent.Id);
@@ -144,7 +149,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -164,7 +169,7 @@ public class HarnessAgentTests
options.ChatOptions = new ChatOptions { Temperature = 0.5f };
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -184,7 +189,7 @@ public class HarnessAgentTests
options.ChatOptions = new ChatOptions { Instructions = "You are a custom assistant." };
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -205,7 +210,7 @@ public class HarnessAgentTests
options.HarnessInstructions = "Custom harness rules.";
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -226,7 +231,7 @@ public class HarnessAgentTests
options.ChatOptions = new ChatOptions { Instructions = "You are a research agent." };
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -247,7 +252,7 @@ public class HarnessAgentTests
options.ChatOptions = new ChatOptions { Instructions = "Agent only instructions." };
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -267,7 +272,7 @@ public class HarnessAgentTests
options.HarnessInstructions = string.Empty;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -289,7 +294,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -310,7 +315,7 @@ public class HarnessAgentTests
options.ChatHistoryProvider = customProvider;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -332,7 +337,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -353,7 +358,7 @@ public class HarnessAgentTests
var rawClient = mockClient.Object;
// Act
var agent = new HarnessAgent(rawClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(rawClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert — the pipeline wraps the raw client, so the outer client is not the same object.
@@ -378,7 +383,7 @@ public class HarnessAgentTests
options.AIContextProviders = [customProvider];
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert — the custom provider should appear in the inner agent's AIContextProviders.
@@ -398,7 +403,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -432,7 +437,7 @@ public class HarnessAgentTests
var options = CreateAllDisabledOptions();
options.ChatOptions = new ChatOptions { Tools = [tool] };
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(mockClient.Object, options);
var session = await agent.CreateSessionAsync();
// Act
@@ -459,8 +464,10 @@ public class HarnessAgentTests
};
// Act
_ = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions
_ = new HarnessAgent(chatClient, new HarnessAgentOptions
{
MaxContextWindowTokens = TestMaxContextWindowTokens,
MaxOutputTokens = TestMaxOutputTokens,
ChatOptions = sourceChatOptions,
});
@@ -483,7 +490,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
// Assert
Assert.Same(agent, agent.GetService<HarnessAgent>());
@@ -499,7 +506,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
// Assert
Assert.NotNull(agent.GetService<ChatClientAgent>());
@@ -524,7 +531,7 @@ public class HarnessAgentTests
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello!")));
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(mockClient.Object, CreateAllDisabledOptions());
var session = await agent.CreateSessionAsync();
// Act
@@ -565,7 +572,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens);
var agent = chatClient.AsHarnessAgent();
// Assert
Assert.NotNull(agent);
@@ -586,7 +593,7 @@ public class HarnessAgentTests
options.ChatOptions = new ChatOptions { Instructions = "Custom instructions" };
// Act
var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = chatClient.AsHarnessAgent(options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -603,7 +610,7 @@ public class HarnessAgentTests
public void AsHarnessAgent_ThrowsWhenChatClientIsNull()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => ((IChatClient)null!).AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens));
Assert.Throws<ArgumentNullException>(() => ((IChatClient)null!).AsHarnessAgent());
}
#endregion
@@ -622,7 +629,7 @@ public class HarnessAgentTests
options.DisableToolApproval = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
// Assert
Assert.NotNull(agent.GetService<ToolApprovalAgent>());
@@ -638,7 +645,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
// Assert
Assert.Null(agent.GetService<ToolApprovalAgent>());
@@ -678,7 +685,7 @@ public class HarnessAgentTests
AutoApprovalRules = [fcc => new ValueTask<bool>(fcc.Name == "ReadTool")]
};
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(mockClient.Object, options);
var session = await agent.CreateSessionAsync();
// Act
@@ -721,7 +728,7 @@ public class HarnessAgentTests
var options = CreateAllDisabledOptions();
options.ChatOptions = new ChatOptions { Tools = [normalTool, approvalTool] };
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(mockClient.Object, options);
var session = await agent.CreateSessionAsync();
// Act
@@ -763,7 +770,7 @@ public class HarnessAgentTests
options.DisableNonApprovalRequiredFunctionBypassing = true;
options.ChatOptions = new ChatOptions { Tools = [normalTool, approvalTool] };
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(mockClient.Object, options);
var session = await agent.CreateSessionAsync();
// Act
@@ -796,7 +803,7 @@ public class HarnessAgentTests
options.DisableOpenTelemetry = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
// Assert
Assert.NotNull(agent.GetService<OpenTelemetryAgent>());
@@ -812,7 +819,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
// Assert
Assert.Null(agent.GetService<OpenTelemetryAgent>());
@@ -831,7 +838,7 @@ public class HarnessAgentTests
options.OpenTelemetrySourceName = "MyApp.AgentTracing";
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
// Assert
Assert.NotNull(agent.GetService<OpenTelemetryAgent>());
@@ -858,7 +865,7 @@ public class HarnessAgentTests
var options = CreateAllDisabledOptions();
options.DisableWebSearch = false;
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(mockClient.Object, options);
var session = await agent.CreateSessionAsync();
// Act
@@ -883,7 +890,7 @@ public class HarnessAgentTests
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(mockClient.Object, CreateAllDisabledOptions());
var session = await agent.CreateSessionAsync();
// Act
@@ -916,7 +923,7 @@ public class HarnessAgentTests
options.DisableWebSearch = false;
options.ChatOptions = new ChatOptions { Tools = [userTool] };
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(mockClient.Object, options);
var session = await agent.CreateSessionAsync();
// Act
@@ -944,7 +951,7 @@ public class HarnessAgentTests
options.DisableTodoProvider = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -962,7 +969,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -989,7 +996,7 @@ public class HarnessAgentTests
options.DisableAgentModeProvider = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1007,7 +1014,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1038,7 +1045,7 @@ public class HarnessAgentTests
};
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert — AgentModeProvider should be present (we can't easily inspect its internal options,
@@ -1063,7 +1070,7 @@ public class HarnessAgentTests
options.DisableFileMemory = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1081,7 +1088,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1106,7 +1113,7 @@ public class HarnessAgentTests
options.FileMemoryStore = customStore;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert — FileMemoryProvider should be present with the custom store.
@@ -1130,7 +1137,7 @@ public class HarnessAgentTests
options.DisableFileAccess = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1148,7 +1155,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1173,7 +1180,7 @@ public class HarnessAgentTests
options.FileAccessStore = customStore;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert — FileAccessProvider should be present with the custom store.
@@ -1197,7 +1204,7 @@ public class HarnessAgentTests
options.DisableAgentSkillsProvider = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1215,7 +1222,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1240,7 +1247,7 @@ public class HarnessAgentTests
options.AgentSkillsSource = customSource;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert — AgentSkillsProvider should be present.
@@ -1264,7 +1271,7 @@ public class HarnessAgentTests
options.MaximumIterationsPerRequest = 42;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
var ficc = innerAgent!.ChatClient.GetService<FunctionInvokingChatClient>();
@@ -1283,7 +1290,7 @@ public class HarnessAgentTests
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
var innerAgent = agent.GetService<ChatClientAgent>();
var ficc = innerAgent!.ChatClient.GetService<FunctionInvokingChatClient>();
@@ -1311,7 +1318,7 @@ public class HarnessAgentTests
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));
// Act
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens);
var agent = new HarnessAgent(mockClient.Object);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert — agent wrappers
@@ -1354,7 +1361,7 @@ public class HarnessAgentTests
options.BackgroundAgents = [bgAgentMock.Object];
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1374,7 +1381,7 @@ public class HarnessAgentTests
options.BackgroundAgents = null;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1397,7 +1404,7 @@ public class HarnessAgentTests
options.BackgroundAgents = Array.Empty<AIAgent>();
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1428,7 +1435,7 @@ public class HarnessAgentTests
options.BackgroundAgentsProviderOptions = providerOptions;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();
@@ -1465,7 +1472,7 @@ public class HarnessAgentTests
options.BackgroundAgents = [agent1Mock.Object, agent2Mock.Object];
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();
@@ -1506,7 +1513,7 @@ public class HarnessAgentTests
options.ShellExecutor = executorMock.Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1526,7 +1533,7 @@ public class HarnessAgentTests
options.ShellExecutor = null;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert
@@ -1558,7 +1565,7 @@ public class HarnessAgentTests
options.ShellExecutor = executorMock.Object;
// Act
var agent = new HarnessAgent(chatClientMock.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClientMock.Object, options);
var session = await agent.CreateSessionAsync();
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);
@@ -1587,7 +1594,7 @@ public class HarnessAgentTests
options.ShellEnvironmentProviderOptions = envOptions;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var agent = new HarnessAgent(chatClient, options);
var innerAgent = agent.GetService<ChatClientAgent>();
// Assert — provider should exist (options wiring is validated by the provider's behavior)
@@ -1611,7 +1618,7 @@ public class HarnessAgentTests
var loggerFactory = new Mock<ILoggerFactory>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory);
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions(), loggerFactory);
// Assert
Assert.NotNull(agent);
@@ -1628,7 +1635,7 @@ public class HarnessAgentTests
var services = new Mock<IServiceProvider>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: services);
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions(), services: services);
// Assert
Assert.NotNull(agent);
@@ -1646,7 +1653,7 @@ public class HarnessAgentTests
var services = new Mock<IServiceProvider>().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services);
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions(), loggerFactory, services);
// Assert
Assert.NotNull(agent);
@@ -1664,7 +1671,7 @@ public class HarnessAgentTests
var services = new Mock<IServiceProvider>().Object;
// Act
var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services);
var agent = chatClient.AsHarnessAgent(CreateAllDisabledOptions(), loggerFactory, services);
// Assert
Assert.NotNull(agent);
@@ -1686,6 +1693,8 @@ public class HarnessAgentTests
// Act — use options that leave CompactionProvider and AgentSkillsProvider enabled
var options = new HarnessAgentOptions
{
MaxContextWindowTokens = TestMaxContextWindowTokens,
MaxOutputTokens = TestMaxOutputTokens,
DisableToolApproval = true,
DisableOpenTelemetry = true,
DisableFileMemory = true,
@@ -1694,7 +1703,7 @@ public class HarnessAgentTests
DisableTodoProvider = true,
DisableAgentModeProvider = true,
};
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options, mockLoggerFactory.Object);
var agent = new HarnessAgent(chatClient, options, mockLoggerFactory.Object);
// Assert — CreateLogger should have been called by one or more downstream components
Assert.NotNull(agent);
@@ -1716,7 +1725,7 @@ public class HarnessAgentTests
.Returns(null!);
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: mockServices.Object);
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions(), services: mockServices.Object);
// Assert — the service provider should have been queried during pipeline construction
Assert.NotNull(agent);
@@ -1724,4 +1733,91 @@ public class HarnessAgentTests
}
#endregion
#region Compaction Opt-in
/// <summary>
/// Verify that constructing without token values succeeds (compaction disabled).
/// </summary>
[Fact]
public void Constructor_SucceedsWithoutTokenValues()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
var options = new HarnessAgentOptions
{
DisableToolApproval = true,
DisableOpenTelemetry = true,
DisableFileMemory = true,
DisableFileAccess = true,
DisableWebSearch = true,
DisableTodoProvider = true,
DisableAgentModeProvider = true,
DisableAgentSkillsProvider = true,
};
// Act
var agent = new HarnessAgent(chatClient, options);
// Assert — compaction should be disabled (no chat reducer)
var innerAgent = agent.GetService<ChatClientAgent>();
Assert.NotNull(innerAgent);
var historyProvider = innerAgent!.ChatHistoryProvider as InMemoryChatHistoryProvider;
Assert.NotNull(historyProvider);
Assert.Null(historyProvider!.ChatReducer);
}
/// <summary>
/// Verify that when only MaxContextWindowTokens is provided (no MaxOutputTokens), compaction is disabled.
/// </summary>
[Fact]
public void Constructor_SucceedsWithOnlyMaxContextWindowTokens()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
var options = new HarnessAgentOptions
{
MaxContextWindowTokens = TestMaxContextWindowTokens,
DisableToolApproval = true,
DisableOpenTelemetry = true,
DisableFileMemory = true,
DisableFileAccess = true,
DisableWebSearch = true,
DisableTodoProvider = true,
DisableAgentModeProvider = true,
DisableAgentSkillsProvider = true,
};
// Act
var agent = new HarnessAgent(chatClient, options);
// Assert — compaction should be disabled (only one token value provided)
var innerAgent = agent.GetService<ChatClientAgent>();
Assert.NotNull(innerAgent);
var historyProvider = innerAgent!.ChatHistoryProvider as InMemoryChatHistoryProvider;
Assert.NotNull(historyProvider);
Assert.Null(historyProvider!.ChatReducer);
}
/// <summary>
/// Verify that when both token values are provided, the agent is constructed successfully with compaction.
/// </summary>
[Fact]
public void Constructor_SucceedsWithBothTokenValues()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
// Act
var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions());
// Assert — compaction should be enabled (chat reducer configured)
var innerAgent = agent.GetService<ChatClientAgent>();
Assert.NotNull(innerAgent);
var historyProvider = innerAgent!.ChatHistoryProvider as InMemoryChatHistoryProvider;
Assert.NotNull(historyProvider);
Assert.NotNull(historyProvider!.ChatReducer);
}
#endregion
}