merge: run reporting — token accounting + populate empty result

This commit is contained in:
Mika Kuns
2026-06-01 16:21:50 +02:00
2 changed files with 64 additions and 1 deletions

View File

@@ -44,6 +44,14 @@ public sealed class StreamAnalyzer
_structuredOutputJson = structuredProp.ToString(); _structuredOutputJson = structuredProp.ToString();
if (root.TryGetProperty("session_id", out var sessionProp)) if (root.TryGetProperty("session_id", out var sessionProp))
_sessionId = sessionProp.GetString(); _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; break;
case "assistant": case "assistant":
@@ -66,7 +74,7 @@ public sealed class StreamAnalyzer
public StreamResult GetResult() => new() public StreamResult GetResult() => new()
{ {
ResultMarkdown = _resultMarkdown, ResultMarkdown = FallbackResult(),
StructuredOutputJson = _structuredOutputJson, StructuredOutputJson = _structuredOutputJson,
SessionId = _sessionId, SessionId = _sessionId,
TurnCount = _turnCount, TurnCount = _turnCount,
@@ -75,6 +83,20 @@ public sealed class StreamAnalyzer
ApiRetryCount = _apiRetryCount, 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) private void TryAccumulateUsage(JsonElement root)
{ {
if (!root.TryGetProperty("event", out var eventProp)) return; if (!root.TryGetProperty("event", out var eventProp)) return;

View File

@@ -79,4 +79,45 @@ public sealed class StreamAnalyzerTests
Assert.Null(result.ResultMarkdown); Assert.Null(result.ResultMarkdown);
Assert.Null(result.SessionId); Assert.Null(result.SessionId);
} }
[Fact]
public void Token_Usage_From_Result_Event()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1","usage":{"input_tokens":150,"output_tokens":75,"cache_read_input_tokens":0}}""");
var result = analyzer.GetResult();
Assert.Equal(150, result.TokensIn);
Assert.Equal(75, result.TokensOut);
}
[Fact]
public void Result_Usage_Overrides_Stream_Event_Accumulation()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"stream_event","event":{"type":"message_start","message":{"usage":{"input_tokens":10,"output_tokens":5}}}}""");
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1","usage":{"input_tokens":200,"output_tokens":90}}""");
var result = analyzer.GetResult();
Assert.Equal(200, result.TokensIn);
Assert.Equal(90, result.TokensOut);
}
[Fact]
public void Empty_Result_Falls_Back_To_Structured_Output_Summary()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"","structured_output":{"summary":"Task completed successfully.","data":{}},"session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal("Task completed successfully.", result.ResultMarkdown);
Assert.Contains("summary", result.StructuredOutputJson);
}
[Fact]
public void Empty_Result_Falls_Back_To_Full_Json_When_No_Summary()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"","structured_output":{"output":"42"},"session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Contains("output", result.ResultMarkdown);
Assert.Contains("42", result.ResultMarkdown);
}
} }