diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs index 4c4010f0c0..a1d8aca1b8 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs @@ -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 diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs index e8e10a3620..654c169850 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs @@ -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, diff --git a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs index 5b8d388dc8..6b88d708f2 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs @@ -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, diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs index af53443c63..908e43abd7 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs @@ -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, diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs index be66e9e635..3d5f037b2f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs @@ -16,23 +16,16 @@ public static class ChatClientHarnessExtensions { /// /// Creates a new that wraps this 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. /// /// /// The that provides access to the underlying AI model. /// - /// - /// 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. - /// - /// - /// 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. - /// /// /// Optional configuration options for the agent, including instructions override, tools, - /// additional context providers, and chat history provider. - /// When , the agent uses built-in default settings. + /// additional context providers, chat history provider, and compaction settings. + /// When , the agent uses built-in default settings with compaction disabled. /// /// /// Optional logger factory for creating loggers used by the agent and its components. @@ -43,10 +36,8 @@ public static class ChatClientHarnessExtensions /// A new instance. 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); } diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs index 6960b755ec..d6bc16d354 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs @@ -18,50 +18,65 @@ namespace Microsoft.Agents.AI; /// /// A pre-configured that wraps a 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. /// /// /// -/// assembles the following pipeline from a caller-supplied : +/// 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 so that callers +/// only need to configure the parts they want to customize. +/// +/// +/// Chat client pipeline (inner to outer): /// -/// — automatic function/tool invocation. -/// — allows external code to inject messages into the conversation mid-stream. -/// — persists chat history after every individual service call within a function-invocation loop. -/// with a — applies context-window compaction before each call so long function-invocation loops do not overflow the context window. +/// — automatic function/tool invocation with configurable iteration limits. +/// — allows external code to inject messages into the conversation mid-stream (e.g., for user interrupts). +/// — persists chat history after every individual service call within a function-invocation loop, enabling crash recovery and history inspection. +/// with a — applies context-window compaction before each call so long function-invocation loops do not overflow the context window. Only included when and are both provided. /// /// /// -/// By default, the following context providers are included (each can be disabled via ): +/// Context providers (each enabled by default, individually disableable via ): /// -/// — todo list management. -/// — agent mode tracking (plan/execute). -/// — file-based session memory. -/// — shared file access. -/// — skill discovery and loading. +/// — persistent todo list that the agent uses to track multi-step plans. Disable with . +/// — mode tracking (e.g., "plan" vs "execute") that the agent uses to structure its work. Disable with . +/// — file-based session memory allowing the agent to persist notes and artifacts across turns. Disable with . +/// — shared file access providing read/write tools for a working directory. Disable with . +/// — discovers and loads skill definitions from the file system, enabling dynamic tool sets. Disable with . /// /// /// -/// The agent is also wrapped with the following decorators by default (each can be disabled): +/// Optional context providers (enabled via ): /// -/// — "don't ask again" tool approval rules. -/// — OpenTelemetry instrumentation. +/// — enables delegation to background agents for parallel work. Enable by setting . +/// ShellEnvironmentProvider — injects OS/shell/CWD information and a shell execution tool. Enable by setting HarnessAgentOptions.ShellExecutor (.NET only). /// /// /// -/// A is added to the chat options by default (can be disabled via -/// ). +/// Agent decorators (each enabled by default, individually disableable): +/// +/// — "don't ask again" tool approval rules enabling safe unattended execution. Disable with . +/// — OpenTelemetry instrumentation following semantic conventions for generative AI. Disable with . +/// /// /// -/// The underlying is configured with -/// and -/// set to -/// to match the manually-assembled pipeline. +/// Default tools: +/// +/// — a hosted web search tool added to chat options by default. Disable with . +/// /// /// -/// When no is supplied, the agent defaults to an -/// whose chat reducer applies the same compaction strategy, -/// keeping in-memory history from growing unboundedly across sessions. +/// Chat history: When no is supplied, +/// the agent defaults to an . 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. +/// +/// +/// Default instructions: The agent includes built-in system instructions () +/// that guide general tool usage and reasoning patterns. These can be overridden via +/// and combined with agent-specific instructions via . /// /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] @@ -90,21 +105,13 @@ public sealed class HarnessAgent : DelegatingAIAgent /// /// /// The 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. - /// - /// - /// 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. - /// - /// - /// 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 , a compaction decorator is also added. /// /// /// Optional configuration options for the agent, including instructions override, tools, - /// additional context providers, and chat history provider. - /// When , the agent uses built-in default settings. + /// additional context providers, chat history provider, and compaction settings. + /// When , the agent uses built-in default settings with compaction disabled. /// /// /// Optional logger factory for creating loggers used by the agent and its components. @@ -116,23 +123,22 @@ public sealed class HarnessAgent : DelegatingAIAgent /// is . /// /// - /// is not positive, or - /// is negative or greater than or equal to . + /// is not positive, or + /// is negative or greater than or equal to + /// (when both are provided). /// - 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 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) { diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs index 85924b7c3e..291650b9b9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs @@ -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 /// public string? Description { get; set; } + /// + /// Gets or sets the maximum number of tokens the model's context window supports (e.g., 1,050,000 for gpt-5.4). + /// + /// + /// + /// When both and are provided (and no + /// custom is set), a default + /// is constructed from these values to prevent function-invocation loops from overflowing the context window. + /// + /// + /// Ignored when is provided or when is + /// . + /// + /// + public int? MaxContextWindowTokens { get; set; } + + /// + /// Gets or sets the maximum number of output tokens the model can generate per response (e.g., 128,000 for gpt-5.4). + /// + /// + /// + /// When set, this value is used as the default for . + /// when not explicitly configured. + /// + /// + /// For compaction purposes, this value is used together with to construct a + /// default — but only when no custom + /// is provided and is . + /// + /// + public int? MaxOutputTokens { get; set; } + + /// + /// Gets or sets a custom to use for in-loop context-window compaction. + /// + /// + /// + /// When provided, this strategy is used directly and and + /// are ignored for compaction purposes ( is still + /// used as the default for . if set). + /// + /// + /// When and both and + /// are provided, a default is constructed from those values. + /// + /// + /// This property is ignored when is . + /// + /// + public CompactionStrategy? CompactionStrategy { get; set; } + + /// + /// Gets or sets a value indicating whether in-loop compaction is disabled. + /// + /// + /// When , compaction is disabled regardless of , + /// , or settings. No + /// is added to the chat client pipeline, and the default + /// is configured without a chat reducer. + /// + public bool DisableCompaction { get; set; } + /// /// Gets or sets additional chat options such as tools for the agent to use. /// @@ -68,9 +131,9 @@ public sealed class HarnessAgentOptions /// Gets or sets the to use for storing chat history. /// /// - /// When , the agent defaults to an - /// configured with a compaction-based chat reducer derived from the maxContextWindowTokens - /// and maxOutputTokens constructor parameters of . + /// When , the agent defaults to an . + /// If and are both provided, + /// the default provider is configured with a compaction-based chat reducer; otherwise, no reducer is applied. /// public ChatHistoryProvider? ChatHistoryProvider { get; set; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs index f7977b595f..a6b06d2a10 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs @@ -21,9 +21,12 @@ public class HarnessAgentTests /// /// 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. /// 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(() => new HarnessAgent(null!, TestMaxContextWindowTokens, TestMaxOutputTokens)); + Assert.Throws(() => new HarnessAgent(null!)); } /// @@ -54,9 +57,10 @@ public class HarnessAgentTests { // Arrange var chatClient = new Mock().Object; + var options = new HarnessAgentOptions { MaxContextWindowTokens = 0, MaxOutputTokens = TestMaxOutputTokens }; // Act & Assert - Assert.Throws(() => new HarnessAgent(chatClient, 0, TestMaxOutputTokens)); + Assert.Throws(() => new HarnessAgent(chatClient, options)); } /// @@ -67,9 +71,10 @@ public class HarnessAgentTests { // Arrange var chatClient = new Mock().Object; + var options = new HarnessAgentOptions { MaxContextWindowTokens = 100_000, MaxOutputTokens = 100_000 }; // Act & Assert - Assert.Throws(() => new HarnessAgent(chatClient, 100_000, 100_000)); + Assert.Throws(() => new HarnessAgent(chatClient, options)); } /// @@ -82,7 +87,7 @@ public class HarnessAgentTests var chatClient = new Mock().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().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); // 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(); // 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(); // 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(); // 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(); // 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(); // 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(); // Assert @@ -289,7 +294,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); // 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(); // Assert @@ -332,7 +337,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); // 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(); // 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(); // Assert — the custom provider should appear in the inner agent's AIContextProviders. @@ -398,7 +403,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); // 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().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); // Assert Assert.Same(agent, agent.GetService()); @@ -499,7 +506,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); // Assert Assert.NotNull(agent.GetService()); @@ -524,7 +531,7 @@ public class HarnessAgentTests It.IsAny())) .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().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(); // Assert @@ -603,7 +610,7 @@ public class HarnessAgentTests public void AsHarnessAgent_ThrowsWhenChatClientIsNull() { // Act & Assert - Assert.Throws(() => ((IChatClient)null!).AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens)); + Assert.Throws(() => ((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()); @@ -638,7 +645,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); // Assert Assert.Null(agent.GetService()); @@ -678,7 +685,7 @@ public class HarnessAgentTests AutoApprovalRules = [fcc => new ValueTask(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()); @@ -812,7 +819,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); // Assert Assert.Null(agent.GetService()); @@ -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()); @@ -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, 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(); // Assert @@ -962,7 +969,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); // 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(); // Assert @@ -1007,7 +1014,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); // 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(); // 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(); // Assert @@ -1081,7 +1088,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); // 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(); // 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(); // Assert @@ -1148,7 +1155,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); // 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(); // 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(); // Assert @@ -1215,7 +1222,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); // 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(); // 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(); var ficc = innerAgent!.ChatClient.GetService(); @@ -1283,7 +1290,7 @@ public class HarnessAgentTests var chatClient = new Mock().Object; // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions()); + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); var innerAgent = agent.GetService(); var ficc = innerAgent!.ChatClient.GetService(); @@ -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(); // 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(); // 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(); // Assert @@ -1397,7 +1404,7 @@ public class HarnessAgentTests options.BackgroundAgents = Array.Empty(); // Act - var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options); + var agent = new HarnessAgent(chatClient, options); var innerAgent = agent.GetService(); // 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(); var bgProvider = innerAgent!.AIContextProviders!.OfType().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(); var bgProvider = innerAgent!.AIContextProviders!.OfType().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(); // 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(); // 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(); // Assert — provider should exist (options wiring is validated by the provider's behavior) @@ -1611,7 +1618,7 @@ public class HarnessAgentTests var loggerFactory = new Mock().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().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().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().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 + + /// + /// Verify that constructing without token values succeeds (compaction disabled). + /// + [Fact] + public void Constructor_SucceedsWithoutTokenValues() + { + // Arrange + var chatClient = new Mock().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(); + Assert.NotNull(innerAgent); + var historyProvider = innerAgent!.ChatHistoryProvider as InMemoryChatHistoryProvider; + Assert.NotNull(historyProvider); + Assert.Null(historyProvider!.ChatReducer); + } + + /// + /// Verify that when only MaxContextWindowTokens is provided (no MaxOutputTokens), compaction is disabled. + /// + [Fact] + public void Constructor_SucceedsWithOnlyMaxContextWindowTokens() + { + // Arrange + var chatClient = new Mock().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(); + Assert.NotNull(innerAgent); + var historyProvider = innerAgent!.ChatHistoryProvider as InMemoryChatHistoryProvider; + Assert.NotNull(historyProvider); + Assert.Null(historyProvider!.ChatReducer); + } + + /// + /// Verify that when both token values are provided, the agent is constructed successfully with compaction. + /// + [Fact] + public void Constructor_SucceedsWithBothTokenValues() + { + // Arrange + var chatClient = new Mock().Object; + + // Act + var agent = new HarnessAgent(chatClient, CreateAllDisabledOptions()); + + // Assert — compaction should be enabled (chat reducer configured) + var innerAgent = agent.GetService(); + Assert.NotNull(innerAgent); + var historyProvider = innerAgent!.ChatHistoryProvider as InMemoryChatHistoryProvider; + Assert.NotNull(historyProvider); + Assert.NotNull(historyProvider!.ChatReducer); + } + + #endregion }