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:
2026-06-13 10:39:52 +04:00
parent fe941408a7
commit 4f6bfa85f7
14 changed files with 387 additions and 10 deletions
+68
View File
@@ -51,6 +51,7 @@ const (
TypeRouterNarration = "router.narration"
TypeArtifactList = "artifact.list"
TypeConfigSnapshot = "config.snapshot"
TypeSessionStats = "session.stats"
)
// ServerMessage is a flat decode of every server->client variant. Field names
@@ -131,6 +132,68 @@ type ServerMessage struct {
ConfigFields []ConfigFieldDto `json:"fields"`
ConfigRestartRequired []string `json:"restartRequired"`
ConfigError *string `json:"error"`
// session.stats — derived metrics for a session (reply to GetSessionStats)
Stats *StatsDto `json:"stats"`
}
// StatsDto mirrors the server's MetricsReport: a session's derived metrics
// (observability-spec §2/§3). Raw sums plus the derived ratios the pane renders.
type StatsDto struct {
SessionID string `json:"sessionId"`
EventCount int64 `json:"eventCount"`
SessionDurationMs int64 `json:"sessionDurationMs"`
InferenceCount int64 `json:"inferenceCount"`
InferenceMs int64 `json:"inferenceMs"`
PromptTokens int64 `json:"promptTokens"`
CompletionTokens int64 `json:"completionTokens"`
TokensPerSecond float64 `json:"tokensPerSecond"`
PerProvider []ProviderStatsDto `json:"perProvider"`
ToolCount int64 `json:"toolCount"`
ToolMs int64 `json:"toolMs"`
PerTool []ToolStatsDto `json:"perTool"`
ApprovalsRequested int64 `json:"approvalsRequested"`
ApprovalsResolved int64 `json:"approvalsResolved"`
ApprovalsPending int64 `json:"approvalsPending"`
ApprovalWaitMs int64 `json:"approvalWaitMs"`
AvgApprovalWaitMs int64 `json:"avgApprovalWaitMs"`
PerTier []TierApprovalStatsDto `json:"perTier"`
Failures FailureMetricsDto `json:"failures"`
InferencePct float64 `json:"inferencePct"`
ToolPct float64 `json:"toolPct"`
ApprovalWaitPct float64 `json:"approvalWaitPct"`
}
type ProviderStatsDto struct {
Provider string `json:"provider"`
CompletedCount int64 `json:"completedCount"`
TotalLatencyMs int64 `json:"totalLatencyMs"`
PromptTokens int64 `json:"promptTokens"`
CompletionTokens int64 `json:"completionTokens"`
TokensPerSecond float64 `json:"tokensPerSecond"`
}
type ToolStatsDto struct {
ToolName string `json:"toolName"`
CompletedCount int64 `json:"completedCount"`
TotalDurationMs int64 `json:"totalDurationMs"`
}
type TierApprovalStatsDto struct {
Tier string `json:"tier"`
RequestedCount int64 `json:"requestedCount"`
ResolvedCount int64 `json:"resolvedCount"`
TotalWaitMs int64 `json:"totalWaitMs"`
AvgWaitMs int64 `json:"avgWaitMs"`
}
type FailureMetricsDto struct {
InferenceFailures int64 `json:"inferenceFailures"`
InferenceTimeouts int64 `json:"inferenceTimeouts"`
ToolFailures int64 `json:"toolFailures"`
ToolRejections int64 `json:"toolRejections"`
StageFailures int64 `json:"stageFailures"`
WorkflowFailures int64 `json:"workflowFailures"`
}
type ConfigFieldDto struct {
@@ -275,6 +338,11 @@ func ListArtifacts(sessionID string) []byte {
return encode("ListArtifacts", map[string]any{"sessionId": sessionID})
}
// GetSessionStats asks the server for a session's derived metrics (replied to with session.stats).
func GetSessionStats(sessionID string) []byte {
return encode("GetSessionStats", map[string]any{"sessionId": sessionID})
}
// GetConfig requests the current editable config (replied to with a config.snapshot).
func GetConfig() []byte {
return encode("GetConfig", map[string]any{})