Are you the author? Sign in to claim
Composable Microsoft.Extensions.AI IChatClient middlewares for .NET — tool-call tracking with in-band streaming progress
Middleware extensions for Microsoft.Extensions.AI: per-request and per-tool token usage tracking, and streaming status propagation for IChatClient pipelines.
Add one line to your pipeline and get:
ChatUsageReport.ChatProgressContent updates interleaved into the live stream so your UI can show "Calling GetWeather Tool", sub-statuses reported from inside the tool ("Extracting…", "Processing…"), and completion — while the model and tools are still working.IChatProgressObserver to receive the same events and the final report without parsing the stream.dotnet add package Andes.Extensions.AI
Register the middleware before UseFunctionInvocation() — the tracker must wrap the tools that the function-invoking client executes:
using Andes.Extensions.AI;
using Microsoft.Extensions.AI;
IChatClient client = innerClient // any IChatClient (Azure OpenAI, OpenAI, Ollama, ...)
.AsBuilder()
.UseToolTracking()
.UseFunctionInvocation()
.Build();
AIFunction weather = AIFunctionFactory.Create(
(string city) =>
{
ChatProgress.Report("Extracting..."); // sub-status under "Calling GetWeather Tool"
return $"Sunny in {city}";
},
"GetWeather");
await foreach (var update in client.GetStreamingResponseAsync(
"What's the weather in Quito?",
new ChatOptions { Tools = [weather] }))
{
foreach (var content in update.Contents)
{
switch (content)
{
case ChatProgressContent progress:
Console.WriteLine($"[{progress.Progress.Kind}] {progress.Progress.Message}");
break;
case UsageReportContent usage:
Console.WriteLine($"Total tokens: {usage.Report.TotalUsage.TotalTokenCount}");
break;
}
}
Console.Write(update.Text);
}
Tools that are themselves LLM-backed (for example, agents exposed as functions) can attribute their own usage to the calling scope with ChatProgress.ReportUsage(...) — or simply run their own UseToolTracking() pipeline, whose total rolls up automatically.
Before persisting responses into conversation history, remove the synthetic content:
ChatResponse response = updates.ToChatResponse().StripProgressContent();
First-class MCP support ships as a satellite package so the core stays dependency-lean:
dotnet add package Andes.Extensions.AI.Mcp
McpClientTool instances classify as ToolKind.McpTool and render as "Calling {Server} MCP", and the server's progress notifications are bridged into ToolProgress updates with numeric Progress/ProgressTotal values:
IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync();
IChatClient client = innerClient
.AsBuilder()
.UseToolTracking(options => options.UseMcpToolClassification())
.UseFunctionInvocation()
.Build();
var chatOptions = new ChatOptions { Tools = mcpTools.WithTracking(mcpClient) };
See MCP support for details.
Microsoft Agent Framework agents run as tracked tools through their own satellite package:
dotnet add package Andes.Extensions.AI.Agent
Agents wrapped with WithTracking() classify as ToolKind.Agent and render as "Calling {Agent} Agent", and each run's AgentResponse.Usage is attributed to the calling tool's scope — a plain agent.AsAIFunction() exposes neither:
AIAgent weatherAgent = weatherChatClient.AsAIAgent(
instructions: "You answer questions about the weather.",
name: "Weather Agent",
tools: [AIFunctionFactory.Create(GetWeather)]);
IChatClient client = innerClient
.AsBuilder()
.UseToolTracking(options => options.UseAgentToolClassification())
.UseFunctionInvocation()
.Build();
var chatOptions = new ChatOptions { Tools = [weatherAgent.WithTracking()] };
See Agent support for details.
MIT
Run Claude Code as an MCP server so any agent can delegate coding tasks to it
Browser automation using accessibility snapshots instead of screenshots
Google's universal MCP server supporting PostgreSQL, MySQL, MongoDB, Redis, and 10+ databases
Official GitHub integration for repos, issues, PRs, and CI/CD workflows