BUNDLE — both fixes live in the Worker run-recording / persistence layer (where a TaskRun is written after an agent finishes), NOT in ExternalMcpService.cs. Keep this disjoint from the MCP-surface bundle so the two can run in parallel without worktree conflicts. The DTO fields (tokensIn, tokensOut, resultMarkdown) already exist and are surfaced by list_runs/get_run — the bug is at write time. 1. ClaudeDo-Task: 49a6060a-5044-4f1b-8665-5cfc064b8a82
113 lines
3.8 KiB
C#
113 lines
3.8 KiB
C#
using System.Text.Json;
|
|
|
|
namespace ClaudeDo.Worker.Runner;
|
|
|
|
public sealed class StreamResult
|
|
{
|
|
public string? ResultMarkdown { get; set; }
|
|
public string? StructuredOutputJson { get; set; }
|
|
public string? SessionId { get; set; }
|
|
public int TurnCount { get; set; }
|
|
public int TokensIn { get; set; }
|
|
public int TokensOut { get; set; }
|
|
public int ApiRetryCount { get; set; }
|
|
}
|
|
|
|
public sealed class StreamAnalyzer
|
|
{
|
|
private string? _resultMarkdown;
|
|
private string? _structuredOutputJson;
|
|
private string? _sessionId;
|
|
private int _turnCount;
|
|
private int _tokensIn;
|
|
private int _tokensOut;
|
|
private int _apiRetryCount;
|
|
|
|
public void ProcessLine(string ndjsonLine)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ndjsonLine)) return;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(ndjsonLine);
|
|
var root = doc.RootElement;
|
|
|
|
if (!root.TryGetProperty("type", out var typeProp)) return;
|
|
var type = typeProp.GetString();
|
|
|
|
switch (type)
|
|
{
|
|
case "result":
|
|
if (root.TryGetProperty("result", out var resultProp))
|
|
_resultMarkdown = resultProp.GetString();
|
|
if (root.TryGetProperty("structured_output", out var structuredProp))
|
|
_structuredOutputJson = structuredProp.ToString();
|
|
if (root.TryGetProperty("session_id", out var sessionProp))
|
|
_sessionId = sessionProp.GetString();
|
|
// Authoritative token totals live on the result event.
|
|
if (root.TryGetProperty("usage", out var resultUsage))
|
|
{
|
|
if (resultUsage.TryGetProperty("input_tokens", out var inp))
|
|
_tokensIn = inp.GetInt32();
|
|
if (resultUsage.TryGetProperty("output_tokens", out var outp))
|
|
_tokensOut = outp.GetInt32();
|
|
}
|
|
break;
|
|
|
|
case "assistant":
|
|
_turnCount++;
|
|
break;
|
|
|
|
case "system":
|
|
if (root.TryGetProperty("subtype", out var subtypeProp) &&
|
|
subtypeProp.GetString() == "api_retry")
|
|
_apiRetryCount++;
|
|
break;
|
|
|
|
case "stream_event":
|
|
TryAccumulateUsage(root);
|
|
break;
|
|
}
|
|
}
|
|
catch (JsonException) { /* Malformed JSON — skip */ }
|
|
}
|
|
|
|
public StreamResult GetResult() => new()
|
|
{
|
|
ResultMarkdown = FallbackResult(),
|
|
StructuredOutputJson = _structuredOutputJson,
|
|
SessionId = _sessionId,
|
|
TurnCount = _turnCount,
|
|
TokensIn = _tokensIn,
|
|
TokensOut = _tokensOut,
|
|
ApiRetryCount = _apiRetryCount,
|
|
};
|
|
|
|
private string? FallbackResult()
|
|
{
|
|
if (!string.IsNullOrEmpty(_resultMarkdown)) return _resultMarkdown;
|
|
if (_structuredOutputJson is null) return _resultMarkdown;
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(_structuredOutputJson);
|
|
if (doc.RootElement.TryGetProperty("summary", out var s))
|
|
return s.GetString();
|
|
}
|
|
catch { }
|
|
return _structuredOutputJson;
|
|
}
|
|
|
|
private void TryAccumulateUsage(JsonElement root)
|
|
{
|
|
if (!root.TryGetProperty("event", out var eventProp)) return;
|
|
if (eventProp.TryGetProperty("message", out var msgProp) &&
|
|
msgProp.TryGetProperty("usage", out var usageProp))
|
|
{
|
|
if (usageProp.TryGetProperty("input_tokens", out var inp))
|
|
_tokensIn += inp.GetInt32();
|
|
if (usageProp.TryGetProperty("output_tokens", out var outp))
|
|
_tokensOut += outp.GetInt32();
|
|
}
|
|
}
|
|
}
|