< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Diagnostics.AgentRunDiagnosticsTranscriptExtensions
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Diagnostics/AgentRunDiagnosticsTranscriptExtensions.cs
Line coverage
96%
Covered lines: 100
Uncovered lines: 4
Coverable lines: 104
Total lines: 193
Line coverage: 96.1%
Branch coverage
83%
Covered branches: 40
Total branches: 48
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
ToTranscriptMarkdown(...)93.33%3030100%
AppendChat(...)50%6694.73%
AppendTool(...)70%101095.65%
AppendChatMessage(...)100%22100%
SafeSerialize(...)100%1150%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Diagnostics/AgentRunDiagnosticsTranscriptExtensions.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Text;
 3using System.Text.Json;
 4using Microsoft.Extensions.AI;
 5
 6namespace NexusLabs.Needlr.AgentFramework.Diagnostics;
 7
 8/// <summary>
 9/// Extensions for rendering an <see cref="IAgentRunDiagnostics"/> as a human-readable
 10/// markdown transcript.
 11/// </summary>
 12public static class AgentRunDiagnosticsTranscriptExtensions
 13{
 114    private static readonly JsonSerializerOptions _jsonOptions = new()
 115    {
 116        WriteIndented = true,
 117    };
 18
 19    /// <summary>
 20    /// Renders a deterministic markdown transcript of the agent run, including the
 21    /// input messages, ordered timeline of chat completions and tool calls, and the
 22    /// final output response. All numeric formatting uses <see cref="CultureInfo.InvariantCulture"/>.
 23    /// </summary>
 24    /// <param name="diagnostics">The agent run diagnostics to render.</param>
 25    /// <returns>A markdown string. Never <see langword="null"/>.</returns>
 26    public static string ToTranscriptMarkdown(
 27        this IAgentRunDiagnostics diagnostics)
 28    {
 829        ArgumentNullException.ThrowIfNull(diagnostics);
 30
 731        var sb = new StringBuilder();
 732        var inv = CultureInfo.InvariantCulture;
 33
 734        sb.Append("# Agent run: ").AppendLine(diagnostics.AgentName);
 735        sb.Append("- Execution mode: ").AppendLine(diagnostics.ExecutionMode ?? "(unspecified)");
 736        sb.Append("- Succeeded: ").AppendLine(diagnostics.Succeeded ? "true" : "false");
 737        sb.Append("- Total duration: ")
 738            .Append(((long)diagnostics.TotalDuration.TotalMilliseconds).ToString(inv))
 739            .AppendLine(" ms");
 740        var tokens = diagnostics.AggregateTokenUsage;
 741        sb.Append("- Aggregate tokens: input=")
 742            .Append(tokens.InputTokens.ToString(inv))
 743            .Append(", output=")
 744            .Append(tokens.OutputTokens.ToString(inv))
 745            .Append(", total=")
 746            .AppendLine(tokens.TotalTokens.ToString(inv));
 747        sb.AppendLine();
 48
 749        if (diagnostics.InputMessages.Count > 0)
 50        {
 151            sb.AppendLine("## Input messages");
 152            sb.AppendLine();
 653            foreach (var message in diagnostics.InputMessages)
 54            {
 255                AppendChatMessage(sb, message);
 56            }
 57        }
 58
 759        sb.AppendLine("## Timeline");
 760        sb.AppendLine();
 761        var timeline = diagnostics.GetOrderedTimeline();
 762        if (timeline.Count == 0)
 63        {
 364            sb.AppendLine("_No diagnostics captured._");
 365            sb.AppendLine();
 66        }
 67        else
 68        {
 2069            foreach (var entry in timeline)
 70            {
 671                var offsetMs = (long)(entry.StartedAt - diagnostics.StartedAt).TotalMilliseconds;
 672                if (entry.Kind == DiagnosticsTimelineEntryKind.ChatCompletion
 673                    && entry.ChatCompletion is { } chat)
 74                {
 475                    AppendChat(sb, chat, offsetMs, inv);
 76                }
 277                else if (entry.Kind == DiagnosticsTimelineEntryKind.ToolCall
 278                    && entry.ToolCall is { } tool)
 79                {
 280                    AppendTool(sb, tool, offsetMs, inv);
 81                }
 82            }
 83        }
 84
 785        if (diagnostics.OutputResponse is { } output && output.Messages.Count > 0)
 86        {
 187            sb.AppendLine("## Output response");
 188            sb.AppendLine();
 489            foreach (var message in output.Messages)
 90            {
 191                AppendChatMessage(sb, message);
 92            }
 93        }
 94
 795        if (!diagnostics.Succeeded)
 96        {
 197            sb.AppendLine("## Error");
 198            sb.AppendLine();
 199            sb.AppendLine(diagnostics.ErrorMessage ?? "(no error message)");
 1100            sb.AppendLine();
 101        }
 102
 7103        return sb.ToString();
 104    }
 105
 106    private static void AppendChat(
 107        StringBuilder sb,
 108        ChatCompletionDiagnostics chat,
 109        long offsetMs,
 110        CultureInfo inv)
 111    {
 4112        sb.Append("### [+").Append(offsetMs.ToString(inv))
 4113            .Append(" ms] Chat completion #").Append(chat.Sequence.ToString(inv)).AppendLine();
 4114        sb.Append("- Model: ").AppendLine(chat.Model);
 4115        sb.Append("- Duration: ")
 4116            .Append(((long)chat.Duration.TotalMilliseconds).ToString(inv))
 4117            .AppendLine(" ms");
 4118        sb.Append("- Succeeded: ").AppendLine(chat.Succeeded ? "true" : "false");
 4119        sb.Append("- Tokens: input=")
 4120            .Append(chat.Tokens.InputTokens.ToString(inv))
 4121            .Append(", output=")
 4122            .Append(chat.Tokens.OutputTokens.ToString(inv))
 4123            .Append(", total=")
 4124            .AppendLine(chat.Tokens.TotalTokens.ToString(inv));
 4125        sb.Append("- Request chars: ").AppendLine(chat.RequestCharCount.ToString(inv));
 4126        sb.Append("- Response chars: ").AppendLine(chat.ResponseCharCount.ToString(inv));
 4127        if (!chat.Succeeded && !string.IsNullOrEmpty(chat.ErrorMessage))
 128        {
 0129            sb.Append("- Error: ").AppendLine(chat.ErrorMessage);
 130        }
 4131        sb.AppendLine();
 4132    }
 133
 134    private static void AppendTool(
 135        StringBuilder sb,
 136        ToolCallDiagnostics tool,
 137        long offsetMs,
 138        CultureInfo inv)
 139    {
 2140        sb.Append("### [+").Append(offsetMs.ToString(inv))
 2141            .Append(" ms] Tool call #").Append(tool.Sequence.ToString(inv))
 2142            .Append(": ").AppendLine(tool.ToolName);
 2143        sb.Append("- Duration: ")
 2144            .Append(((long)tool.Duration.TotalMilliseconds).ToString(inv))
 2145            .AppendLine(" ms");
 2146        sb.Append("- Succeeded: ").AppendLine(tool.Succeeded ? "true" : "false");
 2147        sb.Append("- Arguments chars: ").AppendLine(tool.ArgumentsCharCount.ToString(inv));
 2148        sb.Append("- Result chars: ").AppendLine(tool.ResultCharCount.ToString(inv));
 2149        if (tool.Arguments is not null)
 150        {
 1151            sb.AppendLine("- Arguments:");
 1152            sb.AppendLine("```json");
 1153            sb.AppendLine(SafeSerialize(tool.Arguments));
 1154            sb.AppendLine("```");
 155        }
 2156        if (tool.Result is not null)
 157        {
 1158            sb.AppendLine("- Result:");
 1159            sb.AppendLine("```json");
 1160            sb.AppendLine(SafeSerialize(tool.Result));
 1161            sb.AppendLine("```");
 162        }
 2163        if (!tool.Succeeded && !string.IsNullOrEmpty(tool.ErrorMessage))
 164        {
 0165            sb.Append("- Error: ").AppendLine(tool.ErrorMessage);
 166        }
 2167        sb.AppendLine();
 2168    }
 169
 170    private static void AppendChatMessage(StringBuilder sb, ChatMessage message)
 171    {
 3172        sb.Append("### ").AppendLine(message.Role.Value);
 3173        var text = message.Text;
 3174        if (!string.IsNullOrEmpty(text))
 175        {
 3176            sb.AppendLine();
 3177            sb.AppendLine(text);
 178        }
 3179        sb.AppendLine();
 3180    }
 181
 182    private static string SafeSerialize(object value)
 183    {
 184        try
 185        {
 2186            return JsonSerializer.Serialize(value, _jsonOptions);
 187        }
 0188        catch (Exception ex)
 189        {
 0190            return $"(serialization failed: {ex.GetType().Name}: {ex.Message})";
 191        }
 2192    }
 193}