.NET: Add context window size compaction strategy for harness (#5304)

* Add context window size compaction strategy for harness

* Apply suggestions from code review

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

* Address PR comments

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
westey
2026-04-16 16:44:45 +01:00
committed by GitHub
co-authored by Copilot
parent 9d89353818
commit d0ac1d83bc
6 changed files with 468 additions and 20 deletions
@@ -19,7 +19,9 @@ public static class HarnessConsole
/// <param name="agent">The agent to interact with.</param>
/// <param name="title">The title displayed in the console header.</param>
/// <param name="userPrompt">A short prompt to the user, displayed below the title.</param>
public static async Task RunAgentAsync(AIAgent agent, string title, string userPrompt)
/// <param name="maxContextWindowTokens">Optional max context window size in tokens. When set, usage is displayed as a percentage.</param>
/// <param name="maxOutputTokens">Optional max output tokens. Used with <paramref name="maxContextWindowTokens"/> to show input/output budget breakdown.</param>
public static async Task RunAgentAsync(AIAgent agent, string title, string userPrompt, int? maxContextWindowTokens = null, int? maxOutputTokens = null)
{
var todoProvider = agent.GetService<TodoProvider>();
var modeProvider = agent.GetService<AgentModeProvider>();
@@ -46,7 +48,7 @@ public static class HarnessConsole
}
else
{
await StreamAgentResponseAsync(agent, session, modeProvider, userInput);
await StreamAgentResponseAsync(agent, session, modeProvider, userInput, maxContextWindowTokens, maxOutputTokens);
}
WritePrompt(modeProvider, session);
@@ -57,7 +59,7 @@ public static class HarnessConsole
System.Console.WriteLine("Goodbye!");
}
private static async Task StreamAgentResponseAsync(AIAgent agent, AgentSession session, AgentModeProvider? modeProvider, string userInput)
private static async Task StreamAgentResponseAsync(AIAgent agent, AgentSession session, AgentModeProvider? modeProvider, string userInput, int? maxContextWindowTokens, int? maxOutputTokens)
{
string mode = modeProvider?.GetMode(session) ?? "unknown";
System.Console.ForegroundColor = GetModeColor(mode);
@@ -106,6 +108,37 @@ public static class HarnessConsole
System.Console.ForegroundColor = GetModeColor(mode);
}
else if (content is TextReasoningContent reasoning && !string.IsNullOrEmpty(reasoning.Text))
{
await spinner.StopAsync();
if (!hasTextOutput)
{
System.Console.Write("\n");
hasTextOutput = true;
hasReceivedAnyText = true;
}
System.Console.ForegroundColor = ConsoleColor.DarkMagenta;
System.Console.Write(reasoning.Text);
System.Console.ForegroundColor = GetModeColor(mode);
}
else if (content is UsageContent usage)
{
await spinner.StopAsync();
System.Console.ForegroundColor = ConsoleColor.DarkGray;
System.Console.Write("\n\n 📊 Tokens");
if (usage.Details is not null)
{
WriteUsageBreakdown(usage.Details, maxContextWindowTokens, maxOutputTokens);
}
else
{
System.Console.Write(" —");
}
System.Console.ForegroundColor = GetModeColor(mode);
hasTextOutput = false;
}
}
if (string.IsNullOrEmpty(update.Text))
@@ -136,7 +169,7 @@ public static class HarnessConsole
{
await spinner.StopAsync();
System.Console.ForegroundColor = ConsoleColor.Red;
System.Console.Write($"\n ❌ Stream error: {ex.GetType().Name}: {ex.Message}");
System.Console.Write($"\n ❌ Stream error: {ex.GetType().Name}:\n{ex}");
}
await spinner.StopAsync();
@@ -190,7 +223,7 @@ public static class HarnessConsole
catch (ArgumentException ex)
{
System.Console.ForegroundColor = ConsoleColor.Red;
System.Console.WriteLine($"\n {ex.Message}\n");
System.Console.WriteLine($"\n {ex}\n");
System.Console.ResetColor();
}
}
@@ -237,6 +270,38 @@ public static class HarnessConsole
System.Console.WriteLine();
}
private static void WriteUsageBreakdown(UsageDetails details, int? maxContextWindowTokens, int? maxOutputTokens)
{
int? inputBudget = (maxContextWindowTokens is not null && maxOutputTokens is not null)
? maxContextWindowTokens.Value - maxOutputTokens.Value
: null;
System.Console.Write(" — input: ");
WriteTokenCount(details.InputTokenCount, inputBudget);
System.Console.Write(" | output: ");
WriteTokenCount(details.OutputTokenCount, maxOutputTokens);
System.Console.Write(" | total: ");
WriteTokenCount(details.TotalTokenCount, maxContextWindowTokens);
}
private static void WriteTokenCount(long? count, int? budget)
{
if (count is null)
{
System.Console.Write("—");
return;
}
System.Console.Write($"{count.Value:N0}");
if (budget is not null && budget.Value > 0)
{
double pct = (double)count.Value / budget.Value * 100;
System.Console.Write($"/{budget.Value:N0} ({pct:F1}%)");
}
}
private static ConsoleColor GetModeColor(string mode) => mode switch
{
AgentModeProvider.PlanMode => ConsoleColor.Cyan,
@@ -13,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\Harness_Shared_Console\Harness_Shared_Console.csproj" />
</ItemGroup>
@@ -11,28 +11,40 @@
// exit — End the session.
#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage.
#pragma warning disable MAAI001 // Suppress experimental API warnings for Agents AI experiments.
using Azure.AI.Projects;
using System.ClientModel.Primitives;
using Azure.Identity;
using Harness.Shared.Console;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Responses;
using SampleApp;
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";
// Create the Azure AI Project client and get an IChatClient with stored output disabled
// Create a compaction strategy based on the model's context window.
// gpt-5.4: 1,050,000 token context window, 128,000 max output tokens.
// Defaults: tool result eviction at 50% of input budget, truncation at 80%.
var compactionStrategy = new ContextWindowCompactionStrategy(
maxContextWindowTokens: 1_050_000,
maxOutputTokens: 128_000);
// Create an OpenAIClient that communicates with the Foundry responses service and get an IChatClient with stored output disabled
// so that chat history is managed locally by the agent framework.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
IChatClient chatClient = aiProjectClient
.GetProjectOpenAIClient()
.GetProjectResponsesClient()
.AsIChatClient(deploymentName);
OpenAIClientOptions clientOptions = new() { Endpoint = new Uri(endpoint) };
IChatClient chatClient = new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), clientOptions)
.GetResponsesClient()
.AsIChatClientWithStoredOutputDisabled(deploymentName)
.AsBuilder()
.UseAIContextProviders(new CompactionProvider(compactionStrategy))
.Build();
// Create web browsing tools for downloading and converting HTML pages to markdown.
var webBrowsingTools = new WebBrowsingTools();
@@ -79,6 +91,10 @@ AIAgent agent = new ChatClientAgent(
Name = "ResearchAgent",
Description = "A research assistant that plans and executes research tasks.",
AIContextProviders = [new TodoProvider(), new AgentModeProvider()],
ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
ChatReducer = compactionStrategy.AsChatReducer(),
}),
ChatOptions = new ChatOptions
{
// Set a high token limit for long research tasks with many tool calls and long outputs.
@@ -86,9 +102,9 @@ AIAgent agent = new ChatClientAgent(
MaxOutputTokens = 128_000,
Instructions = instructions,
Reasoning = new() { Effort = ReasoningEffort.High },
Tools = [FoundryAITool.CreateWebSearchTool(), .. webBrowsingTools.Tools],
Tools = [ResponseTool.CreateWebSearchTool().AsAITool(), .. webBrowsingTools.Tools],
},
});
// Run the interactive console session using the shared HarnessConsole helper.
await HarnessConsole.RunAgentAsync(agent, title: "Research Assistant", userPrompt: "Enter a research topic to get started.");
await HarnessConsole.RunAgentAsync(agent, title: "Research Assistant", userPrompt: "Enter a research topic to get started.", maxContextWindowTokens: 1_050_000, maxOutputTokens: 128_000);
@@ -5,7 +5,7 @@ This sample demonstrates how to use a `ChatClientAgent` with the Harness `AICont
Key features showcased:
- **ChatClientAgent** — configured directly with Harness providers for planning and task management
- **Web Search** — the agent can search the web for current information via `FoundryAITool.CreateWebSearchTool()`
- **Web Search** — the agent can search the web for current information via `ResponseTool.CreateWebSearchTool()`
- **TodoProvider** — the agent creates and manages a todo list to track research questions
- **AgentModeProvider** — the agent switches between "plan" mode (breaking down the topic) and "execute" mode (answering each research question)
- **Interactive conversation** — you can review the agent's plan, provide feedback, and approve before execution begins
@@ -25,8 +25,8 @@ Before running this sample, ensure you have:
Set the following environment variables:
```bash
# Required: Your Azure AI Foundry project endpoint
export AZURE_AI_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project-name"
# Required: Your Azure AI Foundry OpenAI endpoint
export AZURE_FOUNDRY_OPENAI_ENDPOINT="https://your-project.services.ai.azure.com/openai/v1/"
# Optional: Model deployment name (defaults to gpt-5.4)
export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4"