agy-token-stats 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agy_token_stats/__init__.py +96 -0
- agy_token_stats/__main__.py +8 -0
- agy_token_stats/aggregator.py +412 -0
- agy_token_stats/cli.py +263 -0
- agy_token_stats/discovery.py +378 -0
- agy_token_stats/formatters.py +480 -0
- agy_token_stats/models.py +402 -0
- agy_token_stats/parser.py +378 -0
- agy_token_stats/time_window.py +180 -0
- agy_token_stats-0.1.0.dist-info/METADATA +199 -0
- agy_token_stats-0.1.0.dist-info/RECORD +15 -0
- agy_token_stats-0.1.0.dist-info/WHEEL +5 -0
- agy_token_stats-0.1.0.dist-info/entry_points.txt +2 -0
- agy_token_stats-0.1.0.dist-info/licenses/LICENSE +21 -0
- agy_token_stats-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Antigravity (`agy`) token usage statistics and subagent tree analyzer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from agy_token_stats.aggregator import (
|
|
6
|
+
_collect_descendants,
|
|
7
|
+
_find_root_session_id,
|
|
8
|
+
_resolve_root_id,
|
|
9
|
+
analyze_sessions,
|
|
10
|
+
compute_stats_report,
|
|
11
|
+
)
|
|
12
|
+
from agy_token_stats.cli import (
|
|
13
|
+
_resolve_brain_sources,
|
|
14
|
+
build_arg_parser,
|
|
15
|
+
main,
|
|
16
|
+
resolve_brain_sources,
|
|
17
|
+
)
|
|
18
|
+
from agy_token_stats.discovery import (
|
|
19
|
+
DEFAULT_CLI_BRAIN_DIR,
|
|
20
|
+
DEFAULT_CLI_HISTORY_PATH,
|
|
21
|
+
DEFAULT_IDE_BRAIN_DIR,
|
|
22
|
+
SKIPPED_BRAIN_DIRS,
|
|
23
|
+
_load_subagent_descriptors,
|
|
24
|
+
_safe_is_dir,
|
|
25
|
+
discover_sessions,
|
|
26
|
+
infer_source_kind,
|
|
27
|
+
load_cli_history_metadata,
|
|
28
|
+
)
|
|
29
|
+
from agy_token_stats.formatters import (
|
|
30
|
+
format_human_report,
|
|
31
|
+
format_json_report,
|
|
32
|
+
)
|
|
33
|
+
from agy_token_stats.models import (
|
|
34
|
+
DailyStats,
|
|
35
|
+
SessionRecord,
|
|
36
|
+
SessionSummary,
|
|
37
|
+
StatsReport,
|
|
38
|
+
StepRecord,
|
|
39
|
+
SubagentLink,
|
|
40
|
+
TimeWindow,
|
|
41
|
+
TokenMetrics,
|
|
42
|
+
)
|
|
43
|
+
from agy_token_stats.parser import (
|
|
44
|
+
_clean_prompt_preview,
|
|
45
|
+
_safe_is_file,
|
|
46
|
+
_step_text_chars,
|
|
47
|
+
parse_transcript_file,
|
|
48
|
+
)
|
|
49
|
+
from agy_token_stats.time_window import (
|
|
50
|
+
get_default_local_tz,
|
|
51
|
+
parse_bound_datetime,
|
|
52
|
+
parse_iso_timestamp,
|
|
53
|
+
resolve_time_window,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__version__ = "0.1.0"
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"__version__",
|
|
60
|
+
"DEFAULT_CLI_BRAIN_DIR",
|
|
61
|
+
"DEFAULT_CLI_HISTORY_PATH",
|
|
62
|
+
"DEFAULT_IDE_BRAIN_DIR",
|
|
63
|
+
"SKIPPED_BRAIN_DIRS",
|
|
64
|
+
"DailyStats",
|
|
65
|
+
"SessionRecord",
|
|
66
|
+
"SessionSummary",
|
|
67
|
+
"StatsReport",
|
|
68
|
+
"StepRecord",
|
|
69
|
+
"SubagentLink",
|
|
70
|
+
"TimeWindow",
|
|
71
|
+
"TokenMetrics",
|
|
72
|
+
"_clean_prompt_preview",
|
|
73
|
+
"_collect_descendants",
|
|
74
|
+
"_find_root_session_id",
|
|
75
|
+
"_load_subagent_descriptors",
|
|
76
|
+
"_resolve_brain_sources",
|
|
77
|
+
"_resolve_root_id",
|
|
78
|
+
"_safe_is_dir",
|
|
79
|
+
"_safe_is_file",
|
|
80
|
+
"_step_text_chars",
|
|
81
|
+
"analyze_sessions",
|
|
82
|
+
"build_arg_parser",
|
|
83
|
+
"compute_stats_report",
|
|
84
|
+
"discover_sessions",
|
|
85
|
+
"format_human_report",
|
|
86
|
+
"format_json_report",
|
|
87
|
+
"get_default_local_tz",
|
|
88
|
+
"infer_source_kind",
|
|
89
|
+
"load_cli_history_metadata",
|
|
90
|
+
"main",
|
|
91
|
+
"parse_bound_datetime",
|
|
92
|
+
"parse_iso_timestamp",
|
|
93
|
+
"parse_transcript_file",
|
|
94
|
+
"resolve_brain_sources",
|
|
95
|
+
"resolve_time_window",
|
|
96
|
+
]
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Token aggregation, model breakdown/filtering, and subagent tree/flat rollup computation.
|
|
2
|
+
|
|
3
|
+
Invariants:
|
|
4
|
+
- Every matched step is counted once in `overall`, `by_source`, `by_role`, `by_model`,
|
|
5
|
+
and `daily`.
|
|
6
|
+
- Orphan subagents (whose `parent_id` is absent from `sessions`) and subagent-rooted
|
|
7
|
+
queries (`--session <subagent>`) are never miscounted in `active_root_sessions` or
|
|
8
|
+
`daily_roots`.
|
|
9
|
+
- Multi-level subagent trees (`Root -> SubA -> SubB`) accurately count both
|
|
10
|
+
`active_subagent_count` (only descendants with active `self_metrics` in the window)
|
|
11
|
+
and `total_subagent_count` (all descendant subagents in `allowed_ids`).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Mapping, Sequence
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
from agy_token_stats.models import (
|
|
20
|
+
DailyStats,
|
|
21
|
+
SessionRecord,
|
|
22
|
+
SessionSummary,
|
|
23
|
+
StatsReport,
|
|
24
|
+
StepRecord,
|
|
25
|
+
TimeWindow,
|
|
26
|
+
TokenMetrics,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_root_id(cid: str, sessions: Mapping[str, SessionRecord]) -> str:
|
|
31
|
+
"""Walk `parent_id` links up to the highest present ancestor in `sessions`."""
|
|
32
|
+
curr = cid
|
|
33
|
+
seen: set[str] = {curr}
|
|
34
|
+
while True:
|
|
35
|
+
rec = sessions.get(curr)
|
|
36
|
+
if rec is None or rec.parent_id is None or rec.parent_id not in sessions:
|
|
37
|
+
return curr
|
|
38
|
+
if rec.parent_id in seen:
|
|
39
|
+
return curr
|
|
40
|
+
seen.add(rec.parent_id)
|
|
41
|
+
curr = rec.parent_id
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_find_root_session_id = _resolve_root_id
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _collect_descendants(
|
|
48
|
+
root_id: str, sessions: Mapping[str, SessionRecord]
|
|
49
|
+
) -> set[str]:
|
|
50
|
+
"""Collect `root_id` and all recursive `child_ids` present in `sessions`."""
|
|
51
|
+
collected: set[str] = set()
|
|
52
|
+
queue = [root_id]
|
|
53
|
+
while queue:
|
|
54
|
+
curr = queue.pop(0)
|
|
55
|
+
if curr in collected or curr not in sessions:
|
|
56
|
+
continue
|
|
57
|
+
collected.add(curr)
|
|
58
|
+
for ch_id in sessions[curr].child_ids:
|
|
59
|
+
if ch_id not in collected:
|
|
60
|
+
queue.append(ch_id)
|
|
61
|
+
return collected
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _step_effective_model(st: StepRecord, rec: SessionRecord) -> str:
|
|
65
|
+
"""Return the effective model name for `st` in `rec`, or `'Unknown'` if none."""
|
|
66
|
+
return (st.model or rec.model or "").strip() or "Unknown"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def compute_stats_report(
|
|
70
|
+
sessions: Mapping[str, SessionRecord],
|
|
71
|
+
*,
|
|
72
|
+
window: TimeWindow | None = None,
|
|
73
|
+
by_session: bool = False,
|
|
74
|
+
flat: bool = False,
|
|
75
|
+
session_filter: str | None = None,
|
|
76
|
+
model_filter: str | None = None,
|
|
77
|
+
sources_scanned: Sequence[str] | None = None,
|
|
78
|
+
) -> StatsReport:
|
|
79
|
+
"""Compute token statistics across `sessions` under the given time window and hierarchy mode.
|
|
80
|
+
|
|
81
|
+
Filtering behavior:
|
|
82
|
+
- If `session_filter` is set, matches sessions whose `conversation_id` starts with
|
|
83
|
+
`session_filter` (case-insensitive). In tree mode (`flat=False`), matching a session
|
|
84
|
+
also includes all of its descendant subagent sessions.
|
|
85
|
+
- If `by_session=False` (default), filters individual steps by `window.contains(step.created_at_local)`.
|
|
86
|
+
- If `by_session=True`, includes all steps of a session when the session's start time
|
|
87
|
+
(or its root session's start time in tree mode) falls within `window`.
|
|
88
|
+
- If `model_filter` is set, performs a case-insensitive substring match against the
|
|
89
|
+
effective step/session model name.
|
|
90
|
+
"""
|
|
91
|
+
active_window = window or TimeWindow(label="all-time")
|
|
92
|
+
scanned = (
|
|
93
|
+
list(sources_scanned)
|
|
94
|
+
if sources_scanned is not None
|
|
95
|
+
else sorted({s.source_kind for s in sessions.values()})
|
|
96
|
+
)
|
|
97
|
+
normalized_model_filter = (
|
|
98
|
+
model_filter.strip() if model_filter and model_filter.strip() else None
|
|
99
|
+
)
|
|
100
|
+
model_needle = (
|
|
101
|
+
normalized_model_filter.lower() if normalized_model_filter else None
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Filter candidate session IDs by --session prefix if requested
|
|
105
|
+
if session_filter:
|
|
106
|
+
prefix = session_filter.strip().lower()
|
|
107
|
+
matched_roots = [cid for cid in sessions if cid.startswith(prefix)]
|
|
108
|
+
if flat:
|
|
109
|
+
allowed_ids = set(matched_roots)
|
|
110
|
+
else:
|
|
111
|
+
allowed_ids = set()
|
|
112
|
+
for mid in matched_roots:
|
|
113
|
+
allowed_ids |= _collect_descendants(mid, sessions)
|
|
114
|
+
else:
|
|
115
|
+
allowed_ids = set(sessions.keys())
|
|
116
|
+
|
|
117
|
+
# Compute filtered steps per session
|
|
118
|
+
filtered_steps_by_id: dict[str, list[StepRecord]] = {}
|
|
119
|
+
self_metrics_by_id: dict[str, TokenMetrics] = {}
|
|
120
|
+
first_active_by_id: dict[str, datetime | None] = {}
|
|
121
|
+
last_active_by_id: dict[str, datetime | None] = {}
|
|
122
|
+
active_models_by_id: dict[str, list[str]] = {}
|
|
123
|
+
|
|
124
|
+
for cid in sorted(allowed_ids):
|
|
125
|
+
rec = sessions[cid]
|
|
126
|
+
if by_session:
|
|
127
|
+
anchor_id = cid if flat else _resolve_root_id(cid, sessions)
|
|
128
|
+
anchor_rec = sessions.get(anchor_id, rec)
|
|
129
|
+
anchor_start = anchor_rec.started_at_local
|
|
130
|
+
if anchor_start is not None and active_window.contains(anchor_start):
|
|
131
|
+
matching_steps = list(rec.steps)
|
|
132
|
+
else:
|
|
133
|
+
matching_steps = []
|
|
134
|
+
else:
|
|
135
|
+
matching_steps = [
|
|
136
|
+
st for st in rec.steps if active_window.contains(st.created_at_local)
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
if model_needle is not None:
|
|
140
|
+
matching_steps = [
|
|
141
|
+
st
|
|
142
|
+
for st in matching_steps
|
|
143
|
+
if model_needle in _step_effective_model(st, rec).lower()
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
metrics = TokenMetrics()
|
|
147
|
+
first_dt: datetime | None = None
|
|
148
|
+
last_dt: datetime | None = None
|
|
149
|
+
step_models: list[str] = []
|
|
150
|
+
for st in matching_steps:
|
|
151
|
+
metrics.add_step(st)
|
|
152
|
+
if st.step_type in ("PLANNER_RESPONSE", "USER_INPUT"):
|
|
153
|
+
if first_dt is None or st.created_at_local < first_dt:
|
|
154
|
+
first_dt = st.created_at_local
|
|
155
|
+
if last_dt is None or st.created_at_local > last_dt:
|
|
156
|
+
last_dt = st.created_at_local
|
|
157
|
+
eff_m = (st.model or rec.model or "").strip()
|
|
158
|
+
if eff_m and eff_m not in step_models:
|
|
159
|
+
step_models.append(eff_m)
|
|
160
|
+
|
|
161
|
+
if metrics.has_activity and not step_models and rec.model:
|
|
162
|
+
step_models = list(rec.models_used) if rec.models_used else [rec.model]
|
|
163
|
+
|
|
164
|
+
filtered_steps_by_id[cid] = matching_steps
|
|
165
|
+
self_metrics_by_id[cid] = metrics
|
|
166
|
+
first_active_by_id[cid] = first_dt
|
|
167
|
+
last_active_by_id[cid] = last_dt
|
|
168
|
+
active_models_by_id[cid] = step_models
|
|
169
|
+
|
|
170
|
+
# Aggregate overall, by_source, by_role, by_model, and daily without double counting
|
|
171
|
+
overall = TokenMetrics()
|
|
172
|
+
by_source: dict[str, TokenMetrics] = {}
|
|
173
|
+
by_role: dict[str, TokenMetrics] = {}
|
|
174
|
+
by_model: dict[str, TokenMetrics] = {}
|
|
175
|
+
by_source_sessions: dict[str, int] = {}
|
|
176
|
+
by_role_sessions: dict[str, int] = {}
|
|
177
|
+
by_model_sessions: dict[str, int] = {}
|
|
178
|
+
|
|
179
|
+
daily_metrics: dict[str, TokenMetrics] = {}
|
|
180
|
+
daily_roots: dict[str, set[str]] = {}
|
|
181
|
+
daily_subagents: dict[str, set[str]] = {}
|
|
182
|
+
|
|
183
|
+
active_subagent_ids: set[str] = set()
|
|
184
|
+
|
|
185
|
+
for cid in sorted(allowed_ids):
|
|
186
|
+
rec = sessions[cid]
|
|
187
|
+
s_metrics = self_metrics_by_id[cid]
|
|
188
|
+
if not s_metrics.has_activity:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
is_subagent = rec.parent_id is not None or rec.agent_type != "root"
|
|
192
|
+
if is_subagent:
|
|
193
|
+
active_subagent_ids.add(cid)
|
|
194
|
+
|
|
195
|
+
overall = overall.merge(s_metrics)
|
|
196
|
+
|
|
197
|
+
src = rec.source_kind
|
|
198
|
+
by_source[src] = by_source.get(src, TokenMetrics()).merge(s_metrics)
|
|
199
|
+
by_source_sessions[src] = by_source_sessions.get(src, 0) + 1
|
|
200
|
+
|
|
201
|
+
role_key = rec.agent_type or ("subagent" if is_subagent else "root")
|
|
202
|
+
by_role[role_key] = by_role.get(role_key, TokenMetrics()).merge(s_metrics)
|
|
203
|
+
by_role_sessions[role_key] = by_role_sessions.get(role_key, 0) + 1
|
|
204
|
+
|
|
205
|
+
session_model_accs: dict[str, TokenMetrics] = {}
|
|
206
|
+
root_for_day = cid if flat else _resolve_root_id(cid, sessions)
|
|
207
|
+
for st in filtered_steps_by_id[cid]:
|
|
208
|
+
if st.step_type not in ("PLANNER_RESPONSE", "USER_INPUT"):
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
m_key = _step_effective_model(st, rec)
|
|
212
|
+
session_model_accs.setdefault(m_key, TokenMetrics()).add_step(st)
|
|
213
|
+
|
|
214
|
+
day_key = st.created_at_local.strftime("%Y-%m-%d")
|
|
215
|
+
d_acc = daily_metrics.setdefault(day_key, TokenMetrics())
|
|
216
|
+
d_acc.add_step(st)
|
|
217
|
+
if is_subagent and not flat:
|
|
218
|
+
daily_subagents.setdefault(day_key, set()).add(cid)
|
|
219
|
+
if (
|
|
220
|
+
root_for_day != cid
|
|
221
|
+
and root_for_day in allowed_ids
|
|
222
|
+
and sessions[root_for_day].parent_id is None
|
|
223
|
+
and sessions[root_for_day].agent_type == "root"
|
|
224
|
+
):
|
|
225
|
+
daily_roots.setdefault(day_key, set()).add(root_for_day)
|
|
226
|
+
elif is_subagent and flat:
|
|
227
|
+
daily_subagents.setdefault(day_key, set()).add(cid)
|
|
228
|
+
else:
|
|
229
|
+
daily_roots.setdefault(day_key, set()).add(cid)
|
|
230
|
+
|
|
231
|
+
for m_key, m_acc in session_model_accs.items():
|
|
232
|
+
if m_acc.has_activity:
|
|
233
|
+
by_model[m_key] = by_model.get(m_key, TokenMetrics()).merge(m_acc)
|
|
234
|
+
by_model_sessions[m_key] = by_model_sessions.get(m_key, 0) + 1
|
|
235
|
+
|
|
236
|
+
# Build SessionSummary objects (flat vs tree)
|
|
237
|
+
if flat:
|
|
238
|
+
summaries: list[SessionSummary] = []
|
|
239
|
+
active_root_count = 0
|
|
240
|
+
for cid in sorted(allowed_ids):
|
|
241
|
+
rec = sessions[cid]
|
|
242
|
+
s_metrics = self_metrics_by_id[cid]
|
|
243
|
+
if not s_metrics.has_activity:
|
|
244
|
+
continue
|
|
245
|
+
is_subagent = rec.parent_id is not None or rec.agent_type != "root"
|
|
246
|
+
if not is_subagent:
|
|
247
|
+
active_root_count += 1
|
|
248
|
+
s_models = active_models_by_id[cid]
|
|
249
|
+
s_model_str = ", ".join(s_models) if s_models else rec.model
|
|
250
|
+
summaries.append(
|
|
251
|
+
SessionSummary(
|
|
252
|
+
conversation_id=cid,
|
|
253
|
+
source_kind=rec.source_kind,
|
|
254
|
+
parent_id=rec.parent_id,
|
|
255
|
+
agent_type=rec.agent_type,
|
|
256
|
+
agent_role=rec.agent_role,
|
|
257
|
+
title=rec.title,
|
|
258
|
+
workspace=rec.workspace,
|
|
259
|
+
model=s_model_str,
|
|
260
|
+
first_active_local=first_active_by_id[cid],
|
|
261
|
+
last_active_local=last_active_by_id[cid],
|
|
262
|
+
self_metrics=s_metrics,
|
|
263
|
+
tree_metrics=s_metrics,
|
|
264
|
+
children=[],
|
|
265
|
+
active_subagent_count=0,
|
|
266
|
+
total_subagent_count=len(rec.child_ids),
|
|
267
|
+
models=list(s_models),
|
|
268
|
+
self_model=s_model_str,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
# Top-level roots in allowed_ids are sessions whose parent_id is not in allowed_ids
|
|
273
|
+
top_level_ids = [
|
|
274
|
+
cid
|
|
275
|
+
for cid in sorted(allowed_ids)
|
|
276
|
+
if sessions[cid].parent_id is None
|
|
277
|
+
or sessions[cid].parent_id not in allowed_ids
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
visited_tree: set[str] = set()
|
|
281
|
+
|
|
282
|
+
def _build_tree_summary(cid: str) -> SessionSummary | None:
|
|
283
|
+
if cid in visited_tree or cid not in allowed_ids:
|
|
284
|
+
return None
|
|
285
|
+
visited_tree.add(cid)
|
|
286
|
+
rec = sessions[cid]
|
|
287
|
+
self_m = self_metrics_by_id[cid]
|
|
288
|
+
tree_m = self_m
|
|
289
|
+
child_summaries: list[SessionSummary] = []
|
|
290
|
+
first_dt = first_active_by_id[cid]
|
|
291
|
+
last_dt = last_active_by_id[cid]
|
|
292
|
+
active_subs = 0
|
|
293
|
+
total_subs = len(_collect_descendants(cid, sessions) & allowed_ids) - 1
|
|
294
|
+
self_models = list(active_models_by_id[cid]) if self_m.has_activity else []
|
|
295
|
+
self_model_str = ", ".join(self_models) if self_models else rec.model
|
|
296
|
+
combined_models: list[str] = list(self_models)
|
|
297
|
+
|
|
298
|
+
for ch_id in rec.child_ids:
|
|
299
|
+
if ch_id not in allowed_ids:
|
|
300
|
+
continue
|
|
301
|
+
ch_sum = _build_tree_summary(ch_id)
|
|
302
|
+
if ch_sum is None:
|
|
303
|
+
continue
|
|
304
|
+
if ch_sum.tree_metrics.has_activity:
|
|
305
|
+
child_summaries.append(ch_sum)
|
|
306
|
+
tree_m = tree_m.merge(ch_sum.tree_metrics)
|
|
307
|
+
active_subs += (
|
|
308
|
+
1 if ch_sum.self_metrics.has_activity else 0
|
|
309
|
+
) + ch_sum.active_subagent_count
|
|
310
|
+
for cm in ch_sum.models:
|
|
311
|
+
if cm and cm not in combined_models:
|
|
312
|
+
combined_models.append(cm)
|
|
313
|
+
if ch_sum.first_active_local is not None and (
|
|
314
|
+
first_dt is None or ch_sum.first_active_local < first_dt
|
|
315
|
+
):
|
|
316
|
+
first_dt = ch_sum.first_active_local
|
|
317
|
+
if ch_sum.last_active_local is not None and (
|
|
318
|
+
last_dt is None or ch_sum.last_active_local > last_dt
|
|
319
|
+
):
|
|
320
|
+
last_dt = ch_sum.last_active_local
|
|
321
|
+
|
|
322
|
+
if not tree_m.has_activity:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
if not combined_models:
|
|
326
|
+
combined_models = list(active_models_by_id[cid])
|
|
327
|
+
model_str = ", ".join(combined_models) if combined_models else rec.model
|
|
328
|
+
|
|
329
|
+
return SessionSummary(
|
|
330
|
+
conversation_id=cid,
|
|
331
|
+
source_kind=rec.source_kind,
|
|
332
|
+
parent_id=rec.parent_id,
|
|
333
|
+
agent_type=rec.agent_type,
|
|
334
|
+
agent_role=rec.agent_role,
|
|
335
|
+
title=rec.title,
|
|
336
|
+
workspace=rec.workspace,
|
|
337
|
+
model=model_str,
|
|
338
|
+
first_active_local=first_dt,
|
|
339
|
+
last_active_local=last_dt,
|
|
340
|
+
self_metrics=self_m,
|
|
341
|
+
tree_metrics=tree_m,
|
|
342
|
+
children=child_summaries,
|
|
343
|
+
active_subagent_count=active_subs,
|
|
344
|
+
total_subagent_count=total_subs,
|
|
345
|
+
models=combined_models,
|
|
346
|
+
self_model=self_model_str,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
summaries = []
|
|
350
|
+
for top_id in top_level_ids:
|
|
351
|
+
s_sum = _build_tree_summary(top_id)
|
|
352
|
+
if s_sum is not None:
|
|
353
|
+
summaries.append(s_sum)
|
|
354
|
+
active_root_count = sum(
|
|
355
|
+
1
|
|
356
|
+
for s in summaries
|
|
357
|
+
if s.parent_id is None and s.agent_type == "root"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
summaries.sort(
|
|
361
|
+
key=lambda s: (
|
|
362
|
+
s.last_active_local.timestamp() if s.last_active_local is not None else 0.0,
|
|
363
|
+
s.tree_metrics.total_tokens,
|
|
364
|
+
s.conversation_id,
|
|
365
|
+
),
|
|
366
|
+
reverse=True,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
daily_list = [
|
|
370
|
+
DailyStats(
|
|
371
|
+
date_str=day_str,
|
|
372
|
+
active_root_sessions=len(daily_roots.get(day_str, set())),
|
|
373
|
+
active_subagent_sessions=len(daily_subagents.get(day_str, set())),
|
|
374
|
+
metrics=daily_metrics[day_str],
|
|
375
|
+
)
|
|
376
|
+
for day_str in sorted(daily_metrics.keys())
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
sorted_by_source = dict(
|
|
380
|
+
sorted(by_source.items(), key=lambda kv: (-kv[1].total_tokens, kv[0]))
|
|
381
|
+
)
|
|
382
|
+
sorted_by_role = dict(
|
|
383
|
+
sorted(by_role.items(), key=lambda kv: (-kv[1].total_tokens, kv[0]))
|
|
384
|
+
)
|
|
385
|
+
sorted_by_model = dict(
|
|
386
|
+
sorted(
|
|
387
|
+
by_model.items(),
|
|
388
|
+
key=lambda kv: (-kv[1].total_tokens, -kv[1].llm_calls, kv[0]),
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return StatsReport(
|
|
393
|
+
window=active_window,
|
|
394
|
+
by_session_mode=by_session,
|
|
395
|
+
flat_mode=flat,
|
|
396
|
+
sources_scanned=scanned,
|
|
397
|
+
active_root_sessions=active_root_count,
|
|
398
|
+
active_subagent_sessions=len(active_subagent_ids),
|
|
399
|
+
overall=overall,
|
|
400
|
+
by_source=sorted_by_source,
|
|
401
|
+
by_role=sorted_by_role,
|
|
402
|
+
by_source_sessions=by_source_sessions,
|
|
403
|
+
by_role_sessions=by_role_sessions,
|
|
404
|
+
daily=daily_list,
|
|
405
|
+
sessions=summaries,
|
|
406
|
+
by_model=sorted_by_model,
|
|
407
|
+
by_model_sessions=by_model_sessions,
|
|
408
|
+
model_filter=normalized_model_filter,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
analyze_sessions = compute_stats_report
|