6Real-time statistics tracking for the OC Chat enhanced terminal UI.
10Provides the presentation-side statistics tracker that backs the
11enhanced terminal header with real-time turn / session / context-usage
12counters. Relocated here from ``oc_chat.core.stats`` by plan 153: the
13tracker's sole owner and mutator is :class:`~oc_ui.terminal.enhanced_presenter.EnhancedPresenter`,
14so it belongs in the ``oc_ui`` adapter layer, not in the harness core.
15Keeping it here removes the ``oc_ui -> oc_chat.core.stats`` import edge
16named as a boundary residual in plan 151 §8.
20- StatisticsTracker: Main class for tracking all metrics
21- Token formatting utilities for K/MB/GB display
22- Color-coded statistics for different metric types
23- Real-time updates during sessions
35- All statistics are thread-safe and atomic
36- Token counts are formatted with proper units
37- Percentages are always displayed with % symbol
38- Time values are formatted with appropriate units
39- This module imports stdlib + oc_diagnostics only; it MUST NOT import
40 ``oc_chat`` (the plan-153 boundary the DAG guard asserts).
44- oc_diagnostics (structured debug logging via _dbg)
47from __future__
import annotations
49from typing
import Dict, List, NamedTuple
50from dataclasses
import dataclass, field
52from oc_diagnostics
import _dbg
54DEFAULT_CONTEXT_WINDOW_SIZE: int = 128_000
56"""Default context-window token cap for the enhanced UI's context meter.
58UI-local fallback that mirrors the harness default of the same name in
59``oc_chat.core.stats`` (consumed by ``cli.py``). The presenter always
60overrides it via :meth:`StatisticsTracker.set_context_window_size` right
61after construction — this constant only governs the meter before the
62first bind. Kept UI-local so this module carries no ``oc_chat`` import.
68 """Token usage statistics."""
73 cache_hit_rate: float = 0.0
78 usage_data: dict | int,
83 """Add token usage from an API response dictionary or counters."""
84 if isinstance(usage_data, dict):
85 prompt = usage_data.get(
'prompt_tokens', 0)
86 completion = usage_data.get(
'completion_tokens', 0)
87 cache_hits = usage_data.get(
'cache_hit_tokens', 0)
88 duration = usage_data.get(
'dur_ms', 0)
91 duration = duration_ms
95 self.
total += prompt + completion
103 """Format token count with appropriate units."""
106 elif count < 1_048_576:
107 return f
"{count // 1024}K"
108 elif count < 1_073_741_824:
109 return f
"{count / 1_048_576:.1f}M"
111 return f
"{count / 1_073_741_824:.1f}G"
116 """Timing and performance statistics."""
119 iteration_count: int = 0
121 def add_timing(self, duration_ms: int, tool_calls: int = 0) ->
None:
122 """Add timing information from a turn."""
129 """Session-level statistics.
131 Wall-clock session duration is NOT tracked here (plan 147 D1) — the
132 old ``update_session_totals`` accumulator wrote the same
133 ``timing.duration_ms`` field ``TimingStats.add_timing`` overwrites,
134 a genuine overwrite-vs-accumulate conflict with zero readers. Real
135 session-elapsed timing now lives on
136 :class:`~oc_ui.terminal.input.TurnStats` (monotonic, process-local)
137 and reaches the enhanced header via :class:`~oc_ui.terminal.statusline.StatusBarState`.
139 tokens: TokenStats = field(default_factory=TokenStats)
140 timing: TimingStats = field(default_factory=TimingStats)
145 """Agent/sub-loop tracking statistics."""
146 active_agents: int = 0
147 total_agents_launched: int = 0
148 sub_loops_running: int = 0
149 total_sub_loops_launched: int = 0
152 """Add a new agent to tracking."""
157 """Add a new sub-loop to tracking."""
162 """Mark an agent as finished."""
167 """Mark a sub-loop as finished."""
173 """Main statistics tracker for OC Chat enhanced UI."""
176 func =
"StatisticsTracker.__init__"
184 _dbg(
"OC_UI.STATS", func,
"initialized", 2)
191 duration_ms: int = 0,
195 """Update statistics for a completed turn."""
196 func =
"update_turn_stats"
197 prompt = aliases.pop(
"prompt_tokens", prompt)
198 completion = aliases.pop(
"completion_tokens", completion)
199 cache_hits = aliases.pop(
"cache_hit_tokens", cache_hits)
200 cache_hits = aliases.pop(
"prompt_cached", cache_hits)
202 self.
_turn_stats.add_usage(prompt, completion, cache_hits, duration_ms)
203 self.
_session_stats.tokens.add_usage(prompt, completion, cache_hits)
207 _dbg(
"OC_UI.STATS", func,
208 f
"turn: prompt={prompt}, completion={completion}, "
209 f
"cache_hits={cache_hits}, duration={duration_ms}ms, tools={tool_calls}", 3)
212 """Update context window usage percentage."""
213 func =
"update_context_usage"
217 _dbg(
"OC_UI.STATS", func, f
"context: {used_tokens}/{self._max_context_size} = {self._context_usage:.1f}%", 3)
220 """Add a new agent to tracking."""
224 _dbg(
"OC_UI.STATS", func, f
"active_agents={self._agent_stats.active_agents}", 3)
227 """Mark an agent as finished."""
228 func =
"finish_agent"
231 _dbg(
"OC_UI.STATS", func, f
"active_agents={self._agent_stats.active_agents}", 3)
234 """Add a new sub-loop to tracking."""
235 func =
"add_sub_loop"
238 _dbg(
"OC_UI.STATS", func, f
"sub_loops_running={self._agent_stats.sub_loops_running}", 3)
241 """Mark a sub-loop as finished."""
242 func =
"finish_sub_loop"
245 _dbg(
"OC_UI.STATS", func, f
"sub_loops_running={self._agent_stats.sub_loops_running}", 3)
248 """Get a snapshot of all current statistics."""
252 "total_launched": self.
_agent_stats.total_agents_launched,
253 "sub_loops_running": self.
_agent_stats.sub_loops_running,
254 "total_sub_loops": self.
_agent_stats.total_sub_loops_launched,
271 "last_turn": last_turn,
286 "last_turn": last_turn,
291 """Set the maximum context window size."""
292 func =
"set_context_window_size"
295 _dbg(
"OC_UI.STATS", func, f
"max_context_size={max_size}", 3)
int total_sub_loops_launched
int total_agents_launched
None finish_sub_loop(self)
Dict[str, any] get_stats_snapshot(self)
int _last_turn_tool_calls
None set_context_window_size(self, int max_size)
None finish_sub_loop(self)
None update_context_usage(self, int used_tokens)
None update_turn_stats(self, int prompt=0, int completion=0, int cache_hits=0, int duration_ms=0, int tool_calls=0, **aliases)
None add_timing(self, int duration_ms, int tool_calls=0)
str format_tokens(self, int count)
None add_usage(self, dict|int usage_data, int completion=0, int cache_hits=0, int duration_ms=0)