feat(server,tui): session stats pane — metrics over the WS bus + Bubble Tea overlay
Surfaces the observability metrics (observability-spec §3-tier-2) in the Go TUI: a session-summary pane showing duration, token throughput per provider, tool time, approval latency per tier, failure counts, and the session wall-time accounting split. Press `S` (or palette → "session stats"). Mirrors the artifacts/config fetch pattern exactly — a WS request/response, not a new transport. Server-side reuses the already-tested MetricsInspectionService (one MetricsReport definition, two wires: REST `correx stats` + WS). Server: - ClientMessage.GetSessionStats + ServerMessage.SessionStats(@SerialName session.stats), reusing MetricsReport as the nested payload. - StreamQueries.sessionStats replays via MetricsInspectionService (pure read, replay-neutral); routed in GlobalStreamHandler. - ServerMessageSerializationTest golden pins the session.stats wire format. TUI (Go/Bubble Tea): - protocol: TypeSessionStats + StatsDto/nested structs + GetSessionStats encoder + golden decode test (cross-language contract). - OverlayStats pane (statsModal), `S` key + palette entry + footer hint, loading/cache-by-session state, bounded breakdown rows. - demo `stats` preview case for serverless visual verification.
This commit is contained in:
@@ -116,6 +116,39 @@ func TestDecodeGoldenServerFrames(t *testing.T) {
|
||||
wantType: TypeSnapshotComplete,
|
||||
eventBear: false,
|
||||
},
|
||||
{
|
||||
name: "session.stats carries nested derived metrics",
|
||||
json: `{"type":"session.stats","sessionId":"s1","stats":{"sessionId":"s1","eventCount":12,` +
|
||||
`"sessionDurationMs":60000,"inferenceCount":3,"inferenceMs":6000,"promptTokens":310,` +
|
||||
`"completionTokens":170,"tokensPerSecond":28.3,` +
|
||||
`"perProvider":[{"provider":"lfm:a","completedCount":2,"totalLatencyMs":5000,` +
|
||||
`"promptTokens":300,"completionTokens":150,"tokensPerSecond":30.0}],` +
|
||||
`"toolCount":3,"toolMs":1500,"perTool":[{"toolName":"shell.read","completedCount":2,"totalDurationMs":1200}],` +
|
||||
`"approvalsRequested":2,"approvalsResolved":1,"approvalsPending":1,"approvalWaitMs":30000,` +
|
||||
`"avgApprovalWaitMs":30000,"perTier":[{"tier":"T2","requestedCount":2,"resolvedCount":1,` +
|
||||
`"totalWaitMs":30000,"avgWaitMs":30000}],` +
|
||||
`"failures":{"inferenceFailures":1,"inferenceTimeouts":0,"toolFailures":1,"toolRejections":0,` +
|
||||
`"stageFailures":0,"workflowFailures":1},"inferencePct":10.0,"toolPct":2.5,"approvalWaitPct":50.0}}`,
|
||||
wantType: TypeSessionStats,
|
||||
eventBear: false,
|
||||
check: func(t *testing.T, m ServerMessage) {
|
||||
if m.SessionID != "s1" {
|
||||
t.Fatalf("session.stats sessionId: %+v", m)
|
||||
}
|
||||
if m.Stats == nil {
|
||||
t.Fatalf("session.stats: Stats is nil")
|
||||
}
|
||||
if m.Stats.InferenceCount != 3 || m.Stats.CompletionTokens != 170 {
|
||||
t.Fatalf("session.stats inference totals: %+v", m.Stats)
|
||||
}
|
||||
if len(m.Stats.PerProvider) != 1 || m.Stats.PerProvider[0].Provider != "lfm:a" {
|
||||
t.Fatalf("session.stats perProvider: %+v", m.Stats.PerProvider)
|
||||
}
|
||||
if m.Stats.Failures.WorkflowFailures != 1 || m.Stats.ApprovalWaitPct != 50.0 {
|
||||
t.Fatalf("session.stats failures/pct: %+v", m.Stats)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
Reference in New Issue
Block a user