mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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:
@@ -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,
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user