chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Abstract base class for console observers that participate in the agent response
/// streaming lifecycle. Observers can configure run options, observe streamed content,
/// and return messages to re-invoke the agent after the stream completes.
/// All methods have default no-op implementations so subclasses only override what they need.
/// </summary>
public abstract class ConsoleObserver
{
/// <summary>
/// Configures <see cref="AgentRunOptions"/> before the agent is invoked.
/// Override to set options such as <see cref="AgentRunOptions.ResponseFormat"/>.
/// </summary>
/// <param name="options">The run options to configure.</param>
/// <param name="agent">The agent being interacted with.</param>
/// <param name="session">The current agent session.</param>
public virtual void ConfigureRunOptions(AgentRunOptions options, AIAgent agent, AgentSession session)
{
}
/// <summary>
/// Called for each <see cref="AgentResponseUpdate"/> in the response stream, regardless of
/// whether it contains content. Override to inspect update-level metadata such as
/// <see cref="AgentResponseUpdate.RawRepresentation"/> for provider-specific events.
/// </summary>
/// <param name="ux">The UX state driver, used for rendering output.</param>
/// <param name="update">The streaming response update.</param>
/// <param name="agent">The agent being interacted with.</param>
/// <param name="session">The current agent session.</param>
public virtual Task OnResponseUpdateAsync(IUXStateDriver ux, AgentResponseUpdate update, AIAgent agent, AgentSession session) => Task.CompletedTask;
/// <summary>
/// Called for each <see cref="AIContent"/> item in the response stream.
/// </summary>
/// <param name="ux">The UX state driver, used for rendering output.</param>
/// <param name="content">The content item from the stream.</param>
/// <param name="agent">The agent being interacted with.</param>
/// <param name="session">The current agent session.</param>
public virtual Task OnContentAsync(IUXStateDriver ux, AIContent content, AIAgent agent, AgentSession session) => Task.CompletedTask;
/// <summary>
/// Called for each text update in the response stream.
/// </summary>
/// <param name="ux">The UX state driver, used for rendering output.</param>
/// <param name="text">The text from the update.</param>
/// <param name="agent">The agent being interacted with.</param>
/// <param name="session">The current agent session.</param>
public virtual Task OnTextAsync(IUXStateDriver ux, string text, AIAgent agent, AgentSession session) => Task.CompletedTask;
/// <summary>
/// Called after the response stream completes. Returns a heterogeneous list of
/// follow-up actions (questions to ask the user, and/or messages to add directly to
/// the next agent invocation), or <see langword="null"/> if no follow-up is needed.
/// </summary>
/// <param name="ux">The UX state driver, used for rendering output.</param>
/// <param name="agent">The agent being interacted with.</param>
/// <param name="session">The current agent session.</param>
/// <returns>Follow-up actions to process after the stream completes, or <see langword="null"/>.</returns>
public virtual Task<IList<FollowUpAction>?> OnStreamCompleteAsync(
IUXStateDriver ux,
AIAgent agent,
AgentSession session) => Task.FromResult<IList<FollowUpAction>?>(null);
}
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Displays error content (❌) from the response stream.
/// </summary>
public sealed class ErrorDisplayObserver : ConsoleObserver
{
/// <inheritdoc/>
public override async Task OnContentAsync(IUXStateDriver ux, AIContent content, AIAgent agent, AgentSession session)
{
if (content is ErrorContent errorContent)
{
string errorText = $"❌ Error: {errorContent.Message}";
if (!string.IsNullOrWhiteSpace(errorContent.ErrorCode))
{
errorText += $" (code: {errorContent.ErrorCode})";
}
if (!string.IsNullOrWhiteSpace(errorContent.Details))
{
errorText += $" details: {errorContent.Details}";
}
await ux.WriteInfoLineAsync(errorText, ConsoleColor.Red);
}
}
}
@@ -0,0 +1,226 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
using System.Text.Json;
using Harness.ConsoleReactiveComponents;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Planning observer that is mode-aware: in planning mode it configures structured
/// JSON output, collects streamed text, and deserializes it as a <see cref="PlanningResponse"/>;
/// in execution mode it passes text straight through to <see cref="IUXStateDriver.WriteTextAsync"/>
/// for live streaming display.
/// </summary>
public sealed class PlanningOutputObserver : ConsoleObserver
{
private readonly StringBuilder _textCollector = new();
private readonly AgentModeProvider _modeProvider;
private readonly string _planModeName;
private readonly string _executionModeName;
private readonly IReadOnlyDictionary<string, ConsoleColor>? _modeColors;
private string? _lastResponseId;
private string? _lastMessageId;
/// <summary>
/// Initializes a new instance of the <see cref="PlanningOutputObserver"/> class.
/// </summary>
/// <param name="modeProvider">The mode provider for switching modes on approval.</param>
/// <param name="planModeName">The mode name that represents the planning mode.</param>
/// <param name="executionModeName">The mode name to switch to when the user approves a plan.</param>
/// <param name="modeColors">Optional mode-to-color mapping for display.</param>
public PlanningOutputObserver(AgentModeProvider modeProvider, string planModeName, string executionModeName, IReadOnlyDictionary<string, ConsoleColor>? modeColors = null)
{
this._modeProvider = modeProvider;
this._planModeName = planModeName;
this._executionModeName = executionModeName;
this._modeColors = modeColors;
}
/// <inheritdoc/>
public override void ConfigureRunOptions(AgentRunOptions options, AIAgent agent, AgentSession session)
{
if (this.IsPlanningMode(this._modeProvider.GetMode(session)))
{
options.ResponseFormat = ChatResponseFormat.ForJsonSchema<PlanningResponse>();
}
}
/// <inheritdoc/>
public override async Task OnResponseUpdateAsync(IUXStateDriver ux, AgentResponseUpdate update, AIAgent agent, AgentSession session)
{
// We aren't in planning mode, so we can just stream the output directly.
if (!this.IsPlanningMode(ux.CurrentMode))
{
if (!string.IsNullOrWhiteSpace(update.Text))
{
await ux.WriteTextAsync(update.Text).ConfigureAwait(false);
}
return;
}
// We are still accumulating the same response/message.
if (this._lastResponseId == update.ResponseId && this._lastMessageId == update.MessageId)
{
this._textCollector.Append(update.Text);
return;
}
// New response/message, write the previous response/message and
// clear the text collector for the next JSON response/message.
string collectedText = this._textCollector.ToString();
if (!string.IsNullOrWhiteSpace(collectedText))
{
await ux.WriteTextAsync(collectedText).ConfigureAwait(false);
}
this._textCollector.Clear();
this._textCollector.Append(update.Text);
this._lastResponseId = update.ResponseId;
this._lastMessageId = update.MessageId;
}
/// <inheritdoc/>
public override async Task<IList<FollowUpAction>?> OnStreamCompleteAsync(
IUXStateDriver ux,
AIAgent agent,
AgentSession session)
{
if (!this.IsPlanningMode(ux.CurrentMode))
{
// Execution mode: text was already streamed live; nothing to parse.
this._textCollector.Clear();
return null;
}
// Read collected text from our stream observation.
string collectedText = this._textCollector.ToString();
this._textCollector.Clear();
if (string.IsNullOrWhiteSpace(collectedText))
{
return null;
}
// Deserialize the structured response.
PlanningResponse? planningResponse;
try
{
planningResponse = JsonSerializer.Deserialize<PlanningResponse>(collectedText);
}
catch (JsonException)
{
// JSON parsing failed — fall back to rendering as regular text output.
await ux.WriteTextAsync(collectedText).ConfigureAwait(false);
return null;
}
if (planningResponse is null)
{
// Null result — fall back to rendering as regular text output.
await ux.WriteTextAsync(collectedText).ConfigureAwait(false);
return null;
}
if (planningResponse.Type == PlanningResponseType.Clarification)
{
return BuildClarificationActions(planningResponse);
}
if (planningResponse.Type == PlanningResponseType.Approval)
{
var question = planningResponse.Questions.FirstOrDefault();
if (question is null)
{
await ux.WriteInfoLineAsync("(approval response had no content)", ConsoleColor.DarkYellow);
return null;
}
return new List<FollowUpAction> { this.BuildApprovalAction(question, session) };
}
// Unexpected type — fall back to rendering as regular text output.
await ux.WriteTextAsync(collectedText).ConfigureAwait(false);
return null;
}
private static List<FollowUpAction> BuildClarificationActions(PlanningResponse response)
{
var actions = new List<FollowUpAction>(response.Questions.Count);
foreach (var question in response.Questions)
{
string prompt = question.Message;
async Task<ChatMessage?> Continuation(string answer, IUXStateDriver ux)
{
if (string.IsNullOrWhiteSpace(answer))
{
string noAnswer = $"🔹 {prompt}\n └─ {AnsiEscapes.SetForegroundColor(ConsoleColor.DarkGray)}(no answer){AnsiEscapes.ResetAttributes}";
await ux.WriteInfoLineAsync(noAnswer, ConsoleColor.Gray).ConfigureAwait(false);
return null;
}
string formatted = $"🔹 {prompt}\n └─ {AnsiEscapes.SetForegroundColor(ConsoleColor.Green)}{answer}{AnsiEscapes.ResetAttributes}";
await ux.WriteInfoLineAsync(formatted, ConsoleColor.Gray).ConfigureAwait(false);
return new ChatMessage(ChatRole.User, $"Q: {prompt}\nA: {answer}");
}
if (question.Choices is { Count: > 0 })
{
actions.Add(new ChoiceFollowUpQuestion(
Prompt: prompt,
Choices: question.Choices,
AllowCustomText: true,
Continuation: Continuation));
}
else
{
actions.Add(new TextFollowUpQuestion(
Prompt: prompt,
Continuation: Continuation));
}
}
return actions;
}
private ChoiceFollowUpQuestion BuildApprovalAction(PlanningQuestion question, AgentSession session)
{
const string ApproveOption = "Approve and switch to execute mode";
var choices = new List<string> { ApproveOption };
return new ChoiceFollowUpQuestion(
Prompt: question.Message,
Choices: choices,
AllowCustomText: true,
Continuation: async (selection, ux) =>
{
string formatted = $"🔹 {question.Message}\n └─ {AnsiEscapes.SetForegroundColor(ConsoleColor.Green)}{selection}{AnsiEscapes.ResetAttributes}";
await ux.WriteInfoLineAsync(formatted, ConsoleColor.Gray).ConfigureAwait(false);
if (selection == ApproveOption)
{
this._modeProvider.SetMode(session, this._executionModeName);
await ux.WriteInfoLineAsync(
$"✅ Switched to {this._executionModeName} mode.",
ModeColors.Get(this._executionModeName, this._modeColors)).ConfigureAwait(false);
return new ChatMessage(ChatRole.User, "Approved");
}
// Custom freeform input — treat as suggested changes.
return new ChatMessage(ChatRole.User, selection);
});
}
/// <summary>
/// Returns <see langword="true"/> when the current mode matches the configured plan mode name.
/// A <see langword="null"/> mode (no mode provider) is also treated as planning mode.
/// </summary>
private bool IsPlanningMode(string? currentMode) =>
currentMode is null || string.Equals(currentMode, this._planModeName, StringComparison.OrdinalIgnoreCase);
}
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Represents a structured response from the agent while in planning mode.
/// Used with structured output to enable consistent rendering of clarification
/// questions and approval requests in the console.
/// </summary>
public class PlanningResponse
{
/// <summary>
/// Gets or sets the type of planning response.
/// </summary>
[JsonPropertyName("type")]
public required PlanningResponseType Type { get; set; }
/// <summary>
/// Gets or sets the list of questions or items to present to the user.
/// For clarification, this contains one or more questions (each with choices).
/// For approval, this contains exactly one item with the plan summary.
/// </summary>
[JsonPropertyName("questions")]
[Description("For clarifications, this has one or more questions to ask the user (each with choices). For approvals, this has exactly one item containing the plan summary for the user to approve.")]
public required List<PlanningQuestion> Questions { get; set; }
}
/// <summary>
/// Represents a single question or item within a <see cref="PlanningResponse"/>.
/// </summary>
public class PlanningQuestion
{
/// <summary>
/// Gets or sets the message to display to the user.
/// For clarification, this is the question. For approval, this is the plan summary.
/// </summary>
[JsonPropertyName("message")]
[Description("For clarifications, this has the question that needs to be clarified with the user. For approvals, this would contain a summary of the execution plan that the user needs to approve.")]
public required string Message { get; set; }
/// <summary>
/// Gets or sets the list of choices for the user to pick from.
/// Only used for clarification questions. Null when no predefined choices are offered.
/// </summary>
[JsonPropertyName("choices")]
[Description("""
For clarifications, this has a list of options that the user can choose from.
null for approvals.
Note: for clarifications, the user will always also be presented with a free form input option, so make sure that each choice provided here is a valid input for the next turn.
E.g. if the question is "Which stock are you referring to?" then valid choices might be ["AAPL", "MSFT", "GOOG"], and the user could also type their own answer.
Invalid choices would be ["Enter tickers directly", "Paste tickers"], since these conflict with the already existing freeform option, and don't directly provide valid inputs for the next turn.
""")]
public List<string>? Choices { get; set; }
}
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Specifies the type of planning response from the agent.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<PlanningResponseType>))]
public enum PlanningResponseType
{
/// <summary>
/// The agent needs clarification and presents options for the user to choose from.
/// </summary>
[Description("Use this type when you need clarification around the user request and you want to present the user with options to choose from.")]
Clarification,
/// <summary>
/// The agent is seeking approval to proceed with execution.
/// </summary>
[Description("Use this type when you are ready to start execution, but need approval to start executing.")]
Approval,
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Displays reasoning content in dark magenta from the response stream.
/// </summary>
public sealed class ReasoningDisplayObserver : ConsoleObserver
{
/// <inheritdoc/>
public override async Task OnContentAsync(IUXStateDriver ux, AIContent content, AIAgent agent, AgentSession session)
{
if (content is TextReasoningContent reasoning && !string.IsNullOrEmpty(reasoning.Text))
{
await ux.WriteTextAsync(reasoning.Text, ConsoleColor.DarkMagenta);
}
}
}
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Streams agent text output directly to the console.
/// Used in normal (non-planning) mode.
/// </summary>
public sealed class TextOutputObserver : ConsoleObserver
{
/// <inheritdoc/>
public override async Task OnTextAsync(IUXStateDriver ux, string text, AIAgent agent, AgentSession session)
{
await ux.WriteTextAsync(text);
}
}
@@ -0,0 +1,111 @@
// Copyright (c) Microsoft. All rights reserved.
using Harness.ConsoleReactiveComponents;
using Harness.Shared.Console.ToolFormatters;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Collects <see cref="ToolApprovalRequestContent"/> items during the response stream,
/// displays approval-needed notifications inline, and after the stream completes returns
/// one <see cref="ChoiceFollowUpQuestion"/> per pending approval request. Each question's
/// continuation produces a separate <see cref="ChatMessage"/> carrying the approval
/// response content.
/// </summary>
public sealed class ToolApprovalObserver : ConsoleObserver
{
private readonly List<ToolApprovalRequestContent> _approvalRequests = [];
private readonly IReadOnlyList<ToolCallFormatter> _formatters;
/// <summary>
/// Initializes a new instance of the <see cref="ToolApprovalObserver"/> class.
/// </summary>
/// <param name="formatters">Optional list of tool formatters. When <see langword="null"/>,
/// the default formatters from <see cref="ToolCallFormatter.BuildDefaultToolFormatters"/> are used.</param>
public ToolApprovalObserver(IReadOnlyList<ToolCallFormatter>? formatters = null)
{
this._formatters = formatters ?? ToolCallFormatter.BuildDefaultToolFormatters();
}
/// <inheritdoc/>
public override async Task OnContentAsync(IUXStateDriver ux, AIContent content, AIAgent agent, AgentSession session)
{
if (content is ToolApprovalRequestContent approvalRequest)
{
this._approvalRequests.Add(approvalRequest);
string toolName = approvalRequest.ToolCall is FunctionCallContent fc
? ToolCallFormatter.Format(this._formatters, fc)
: approvalRequest.ToolCall?.ToString() ?? "unknown";
await ux.WriteInfoLineAsync($"⚠️ Approval needed: {toolName}", ConsoleColor.Yellow);
}
}
/// <inheritdoc/>
public override Task<IList<FollowUpAction>?> OnStreamCompleteAsync(
IUXStateDriver ux,
AIAgent agent,
AgentSession session)
{
if (this._approvalRequests.Count == 0)
{
return Task.FromResult<IList<FollowUpAction>?>(null);
}
var actions = new List<FollowUpAction>(this._approvalRequests.Count);
foreach (var request in this._approvalRequests)
{
actions.Add(this.BuildApprovalQuestion(request));
}
this._approvalRequests.Clear();
return Task.FromResult<IList<FollowUpAction>?>(actions);
}
private ChoiceFollowUpQuestion BuildApprovalQuestion(ToolApprovalRequestContent request)
{
string toolName = request.ToolCall is FunctionCallContent fc
? ToolCallFormatter.Format(this._formatters, fc)
: request.ToolCall?.ToString() ?? "unknown";
var choices = new List<string>
{
"Approve this call",
"Always approve this tool (any arguments)",
"Always approve this tool with these arguments",
"Deny",
};
string prompt = $"🔐 Tool approval: {toolName}";
return new ChoiceFollowUpQuestion(
Prompt: prompt,
Choices: choices,
AllowCustomText: false,
Continuation: async (selection, ux) =>
{
AIContent response = selection switch
{
"Always approve this tool (any arguments)" => request.CreateAlwaysApproveToolResponse("User chose to always approve this tool"),
"Always approve this tool with these arguments" => request.CreateAlwaysApproveToolWithArgumentsResponse("User chose to always approve this tool with these arguments"),
"Deny" => request.CreateResponse(approved: false, reason: "User denied"),
_ => request.CreateResponse(approved: true, reason: "User approved"),
};
string action = selection switch
{
"Always approve this tool (any arguments)" => "✅ Always approved (any args)",
"Always approve this tool with these arguments" => "✅ Always approved (these args)",
"Deny" => "❌ Denied",
_ => "✅ Approved",
};
ConsoleColor answerColor = selection == "Deny" ? ConsoleColor.Red : ConsoleColor.Green;
string formatted = $"🔹 {prompt}\n └─ {AnsiEscapes.SetForegroundColor(answerColor)}{action}{AnsiEscapes.ResetAttributes}";
await ux.WriteInfoLineAsync(formatted, ConsoleColor.Gray).ConfigureAwait(false);
return new ChatMessage(ChatRole.User, [response]);
});
}
}
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft. All rights reserved.
using Harness.Shared.Console.ToolFormatters;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Displays tool call notifications (🔧) for <see cref="FunctionCallContent"/>
/// and <see cref="ToolCallContent"/> items in the response stream.
/// </summary>
public sealed class ToolCallDisplayObserver : ConsoleObserver
{
private readonly IReadOnlyList<ToolCallFormatter> _formatters;
/// <summary>
/// Initializes a new instance of the <see cref="ToolCallDisplayObserver"/> class.
/// </summary>
/// <param name="formatters">Optional list of tool formatters. When <see langword="null"/>,
/// the default formatters from <see cref="ToolCallFormatter.BuildDefaultToolFormatters"/> are used.</param>
public ToolCallDisplayObserver(IReadOnlyList<ToolCallFormatter>? formatters = null)
{
this._formatters = formatters ?? ToolCallFormatter.BuildDefaultToolFormatters();
}
/// <inheritdoc/>
public override async Task OnContentAsync(IUXStateDriver ux, AIContent content, AIAgent agent, AgentSession session)
{
if (content is FunctionCallContent functionCall)
{
await ux.WriteInfoLineAsync($"🔧 Calling tool: {ToolCallFormatter.Format(this._formatters, functionCall)}...", ConsoleColor.DarkYellow);
}
else if (content is WebSearchToolCallContent)
{
// Handled by OpenAIResponsesWebSearchDisplayObserver when present; skip here to avoid duplication.
}
else if (content is ToolCallContent toolCall)
{
await ux.WriteInfoLineAsync($"🔧 Calling tool: {toolCall}...", ConsoleColor.DarkYellow);
}
}
}
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.Observers;
/// <summary>
/// Displays token usage statistics (📊) from the response stream.
/// </summary>
public sealed class UsageDisplayObserver : ConsoleObserver
{
private readonly int? _maxContextWindowTokens;
private readonly int? _maxOutputTokens;
/// <summary>
/// Initializes a new instance of the <see cref="UsageDisplayObserver"/> class.
/// </summary>
/// <param name="maxContextWindowTokens">Optional max context window size in tokens.</param>
/// <param name="maxOutputTokens">Optional max output tokens.</param>
public UsageDisplayObserver(int? maxContextWindowTokens, int? maxOutputTokens)
{
this._maxContextWindowTokens = maxContextWindowTokens;
this._maxOutputTokens = maxOutputTokens;
}
/// <inheritdoc/>
public override Task OnContentAsync(IUXStateDriver ux, AIContent content, AIAgent agent, AgentSession session)
{
if (content is UsageContent usage)
{
if (usage.Details is not null)
{
ux.SetUsageText(this.FormatUsageBreakdown(usage.Details));
}
else
{
ux.SetUsageText("📊 Tokens —");
}
}
return Task.CompletedTask;
}
private string FormatUsageBreakdown(UsageDetails details)
{
int? inputBudget = (this._maxContextWindowTokens is not null && this._maxOutputTokens is not null)
? this._maxContextWindowTokens.Value - this._maxOutputTokens.Value
: null;
return $"📊 Tokens — input: {FormatTokenCount(details.InputTokenCount, inputBudget)}"
+ $" | output: {FormatTokenCount(details.OutputTokenCount, this._maxOutputTokens)}"
+ $" | total: {FormatTokenCount(details.TotalTokenCount, this._maxContextWindowTokens)}";
}
private static string FormatTokenCount(long? count, int? budget)
{
if (count is null)
{
return "—";
}
if (budget is not null && budget.Value > 0)
{
double pct = (double)count.Value / budget.Value * 100;
return $"{count.Value:N0}/{budget.Value:N0} ({pct:F1}%)";
}
return $"{count.Value:N0}";
}
}