agentburn 0.2.0__tar.gz
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.
- agentburn-0.2.0/LICENSE +21 -0
- agentburn-0.2.0/PKG-INFO +130 -0
- agentburn-0.2.0/README.md +112 -0
- agentburn-0.2.0/agentburn/__init__.py +8 -0
- agentburn-0.2.0/agentburn/adapters/__init__.py +14 -0
- agentburn-0.2.0/agentburn/adapters/hermes.py +239 -0
- agentburn-0.2.0/agentburn/analyze.py +180 -0
- agentburn-0.2.0/agentburn/baseline.py +92 -0
- agentburn-0.2.0/agentburn/benchmarks.py +41 -0
- agentburn-0.2.0/agentburn/cli.py +136 -0
- agentburn-0.2.0/agentburn/doctor.py +97 -0
- agentburn-0.2.0/agentburn/model.py +71 -0
- agentburn-0.2.0/agentburn/recommend.py +100 -0
- agentburn-0.2.0/agentburn/report.py +191 -0
- agentburn-0.2.0/agentburn/share.py +107 -0
- agentburn-0.2.0/agentburn.egg-info/PKG-INFO +130 -0
- agentburn-0.2.0/agentburn.egg-info/SOURCES.txt +20 -0
- agentburn-0.2.0/agentburn.egg-info/dependency_links.txt +1 -0
- agentburn-0.2.0/agentburn.egg-info/entry_points.txt +2 -0
- agentburn-0.2.0/agentburn.egg-info/top_level.txt +1 -0
- agentburn-0.2.0/pyproject.toml +29 -0
- agentburn-0.2.0/setup.cfg +4 -0
agentburn-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ion
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
agentburn-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentburn
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Where does your AI agent burn money? Local token/cost profiler for always-on agents (Hermes Agent first). Zero dependencies.
|
|
5
|
+
Author: Ion
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Socialpranker/agentburn
|
|
8
|
+
Keywords: hermes-agent,tokens,cost,profiler,ai-agents,openrouter
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# agentburn
|
|
20
|
+
|
|
21
|
+
> **Where does your AI agent burn money — while you sleep?**
|
|
22
|
+
|
|
23
|
+
Always-on agents bill you around the clock. Hermes Agent users wake up to
|
|
24
|
+
[**$47 overnight bills**](https://dev.to/chintanonweb/hermes-agent-gets-smarter-every-day-so-does-the-bill-4i8o)
|
|
25
|
+
from recursive subagent runs; one user measured that
|
|
26
|
+
[**73% of every API call is fixed overhead**](https://github.com/NousResearch/hermes-agent/issues/4379)
|
|
27
|
+
(tool definitions + system prompt, resent every time); chained delegation means
|
|
28
|
+
*"step 3 costs 4× step 1 — no alert, just a bill."* Built-in `/usage` shows totals.
|
|
29
|
+
Nothing shows **where** it burns.
|
|
30
|
+
|
|
31
|
+
agentburn is a local profiler for your agent's own accounting database. One command, zero dependencies, nothing leaves your machine:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uvx agentburn # or: pipx run agentburn / pip install agentburn
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
🔥 agentburn — hermes · last 30d
|
|
39
|
+
|
|
40
|
+
~$45.50 total · 1.75M tokens · 7 sessions · 123 API calls
|
|
41
|
+
≈ ~$431.24/month at the current pace
|
|
42
|
+
|
|
43
|
+
WHERE IT BURNS (by source)
|
|
44
|
+
cron ██████████████···· 79% ~$36.00 1.24M 2 sess
|
|
45
|
+
cli ██················ 9% ~$4.00 185K 1 sess
|
|
46
|
+
gateway:telegram █················· 7% ~$3.00 210K 1 sess
|
|
47
|
+
subagent █················· 5% ~$2.50 113K 2 sess
|
|
48
|
+
|
|
49
|
+
🌙 WHILE YOU SLEPT (00:00–08:00): ~$36.00 (79% of spend) · 2 sessions
|
|
50
|
+
mostly: cron
|
|
51
|
+
|
|
52
|
+
FIXED OVERHEAD (avg input tokens per API call)
|
|
53
|
+
gateway:telegram 20,000 ← heavy
|
|
54
|
+
cron 15,000 ← heavy
|
|
55
|
+
input composition (sampled from 3 request dumps): system 30% · tools 58% · history 12%
|
|
56
|
+
|
|
57
|
+
💡 DO THIS
|
|
58
|
+
1. 79% of spend happens at night — that's ≈$341/mo while you sleep. Route night work to a cheaper model.
|
|
59
|
+
2. Scheduled (cron) sessions run on anthropic/claude-opus-x — maintenance rarely needs a frontier model.
|
|
60
|
+
3. 20,000 input tokens per call on telegram: trim per-platform toolsets, prune unused skills.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## What it answers
|
|
64
|
+
|
|
65
|
+
- **Where it burns** — by source: `cron` / `subagent` / `gateway:telegram|discord|whatsapp` / `cli`. Always-on ≠ free: scheduled jobs and gateways spend without you.
|
|
66
|
+
- **🌙 While you slept** — the overnight bill, isolated and named (configurable window: `--night 23-7`).
|
|
67
|
+
- **Fixed overhead** — average input tokens per API call per source. The "73% overhead" pattern is visible in one glance; with request dumps enabled, you get the sampled composition (system prompt vs tool definitions vs history).
|
|
68
|
+
- **Subagent rollups** — delegation cost chained back to the session that spawned it. Recursion compounds; here is the receipt.
|
|
69
|
+
- **Top tools** — which tool results weigh most in your context.
|
|
70
|
+
- **What to do** — up to 4 conservative, named recommendations with monthly estimates.
|
|
71
|
+
|
|
72
|
+
## Why trust these numbers
|
|
73
|
+
|
|
74
|
+
Most token trackers quietly disagree with each other (2–91× in public issue threads). agentburn takes the opposite stance:
|
|
75
|
+
|
|
76
|
+
- Numbers come from **the agent's own accounting** (`~/.hermes/state.db`: per-session token counters and cost fields). No scraping, no proxies, no guessing.
|
|
77
|
+
- Provider-billed costs are shown as-is; Hermes estimates are marked with `~`. Mixed data is labeled mixed.
|
|
78
|
+
- Sessions with messages but **zero recorded tokens** (known Hermes accounting gaps, e.g. [#12023](https://github.com/NousResearch/hermes-agent/issues/12023)) are detected and reported: totals are then explicitly a **lower bound** — and fixing the accounting becomes recommendation #1.
|
|
79
|
+
- Input composition from request dumps is char-proportional and labeled *sampled estimate*, not truth.
|
|
80
|
+
|
|
81
|
+
## Privacy
|
|
82
|
+
|
|
83
|
+
Everything runs locally and reads your database **read-only**. No network calls. No telemetry. The report is yours.
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
agentburn # autodetect agent, last 30 days
|
|
89
|
+
agentburn --days 7
|
|
90
|
+
agentburn --db /path/to/state.db
|
|
91
|
+
agentburn --night 23-7 # custom overnight window (local time)
|
|
92
|
+
agentburn --json # machine-readable, pipe it anywhere
|
|
93
|
+
agentburn --no-color
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Mechanics
|
|
97
|
+
|
|
98
|
+
**📤 Share your burn (`--share`).** An anonymized card — categories, models and totals only; session titles, paths and content are excluded *by construction*. Safe to paste into a post; `--svg card.svg` renders the same card as an image:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
🔥 my hermes agent · last 30d
|
|
102
|
+
~$45.50 · 1.75M tokens → ~$430/mo pace
|
|
103
|
+
cron 79% · cli 9% · telegram 7% · subagent 5%
|
|
104
|
+
🌙 while I slept (00–08): ~$36.00 (79%)
|
|
105
|
+
heaviest overhead: telegram 20,000 tokens/call (community baseline ≈8k/call: +150%)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**📏 Calibration against public benchmarks.** "Is 15k input tokens per call normal?" The report compares your fixed overhead with community-measured references embedded as dated constants (e.g. the [Phala always-on-agent benchmark](https://phala.com/posts/understanding-openclaws-token-usage), 2026-03: ≈8k/call baseline). No network — sources are cited inline.
|
|
109
|
+
|
|
110
|
+
**📐 Optimize → prove it (`--save-baseline` / `--compare`).** Snapshot your pace, change the config (cheaper cron model, trimmed toolsets), then `agentburn --compare` shows the delta in $/month — pace-normalized, so a 7-day baseline compares honestly with a 30-day window. Every recommendation becomes a testable promise.
|
|
111
|
+
|
|
112
|
+
**🩺 `agentburn doctor`.** Trackers disagree because the agent's own accounting has gaps. doctor names the broken combinations (provider × model × source) for zero-usage and unpriced sessions, and generates a ready-to-paste upstream bug report — counters only, no message content.
|
|
113
|
+
|
|
114
|
+
## Supported agents
|
|
115
|
+
|
|
116
|
+
| Agent | Status | Data source |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| **Hermes Agent** | ✅ v0.1 | `~/.hermes/state.db` (+ optional `request_dump_*.json` for input composition) |
|
|
119
|
+
| OpenClaw | roadmap | session JSONL |
|
|
120
|
+
| Claude Code | roadmap | `~/.claude/projects/**.jsonl` |
|
|
121
|
+
|
|
122
|
+
The core is agent-agnostic (normalized session/event model); adapters are ~150 lines each. PRs welcome.
|
|
123
|
+
|
|
124
|
+
## Related
|
|
125
|
+
|
|
126
|
+
[token-history](https://github.com/Socialpranker/token-history) — the macro view: daily archive of *which agents the world uses* (OpenRouter rankings). agentburn is the micro view: *where yours burns*.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# agentburn
|
|
2
|
+
|
|
3
|
+
> **Where does your AI agent burn money — while you sleep?**
|
|
4
|
+
|
|
5
|
+
Always-on agents bill you around the clock. Hermes Agent users wake up to
|
|
6
|
+
[**$47 overnight bills**](https://dev.to/chintanonweb/hermes-agent-gets-smarter-every-day-so-does-the-bill-4i8o)
|
|
7
|
+
from recursive subagent runs; one user measured that
|
|
8
|
+
[**73% of every API call is fixed overhead**](https://github.com/NousResearch/hermes-agent/issues/4379)
|
|
9
|
+
(tool definitions + system prompt, resent every time); chained delegation means
|
|
10
|
+
*"step 3 costs 4× step 1 — no alert, just a bill."* Built-in `/usage` shows totals.
|
|
11
|
+
Nothing shows **where** it burns.
|
|
12
|
+
|
|
13
|
+
agentburn is a local profiler for your agent's own accounting database. One command, zero dependencies, nothing leaves your machine:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uvx agentburn # or: pipx run agentburn / pip install agentburn
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
🔥 agentburn — hermes · last 30d
|
|
21
|
+
|
|
22
|
+
~$45.50 total · 1.75M tokens · 7 sessions · 123 API calls
|
|
23
|
+
≈ ~$431.24/month at the current pace
|
|
24
|
+
|
|
25
|
+
WHERE IT BURNS (by source)
|
|
26
|
+
cron ██████████████···· 79% ~$36.00 1.24M 2 sess
|
|
27
|
+
cli ██················ 9% ~$4.00 185K 1 sess
|
|
28
|
+
gateway:telegram █················· 7% ~$3.00 210K 1 sess
|
|
29
|
+
subagent █················· 5% ~$2.50 113K 2 sess
|
|
30
|
+
|
|
31
|
+
🌙 WHILE YOU SLEPT (00:00–08:00): ~$36.00 (79% of spend) · 2 sessions
|
|
32
|
+
mostly: cron
|
|
33
|
+
|
|
34
|
+
FIXED OVERHEAD (avg input tokens per API call)
|
|
35
|
+
gateway:telegram 20,000 ← heavy
|
|
36
|
+
cron 15,000 ← heavy
|
|
37
|
+
input composition (sampled from 3 request dumps): system 30% · tools 58% · history 12%
|
|
38
|
+
|
|
39
|
+
💡 DO THIS
|
|
40
|
+
1. 79% of spend happens at night — that's ≈$341/mo while you sleep. Route night work to a cheaper model.
|
|
41
|
+
2. Scheduled (cron) sessions run on anthropic/claude-opus-x — maintenance rarely needs a frontier model.
|
|
42
|
+
3. 20,000 input tokens per call on telegram: trim per-platform toolsets, prune unused skills.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What it answers
|
|
46
|
+
|
|
47
|
+
- **Where it burns** — by source: `cron` / `subagent` / `gateway:telegram|discord|whatsapp` / `cli`. Always-on ≠ free: scheduled jobs and gateways spend without you.
|
|
48
|
+
- **🌙 While you slept** — the overnight bill, isolated and named (configurable window: `--night 23-7`).
|
|
49
|
+
- **Fixed overhead** — average input tokens per API call per source. The "73% overhead" pattern is visible in one glance; with request dumps enabled, you get the sampled composition (system prompt vs tool definitions vs history).
|
|
50
|
+
- **Subagent rollups** — delegation cost chained back to the session that spawned it. Recursion compounds; here is the receipt.
|
|
51
|
+
- **Top tools** — which tool results weigh most in your context.
|
|
52
|
+
- **What to do** — up to 4 conservative, named recommendations with monthly estimates.
|
|
53
|
+
|
|
54
|
+
## Why trust these numbers
|
|
55
|
+
|
|
56
|
+
Most token trackers quietly disagree with each other (2–91× in public issue threads). agentburn takes the opposite stance:
|
|
57
|
+
|
|
58
|
+
- Numbers come from **the agent's own accounting** (`~/.hermes/state.db`: per-session token counters and cost fields). No scraping, no proxies, no guessing.
|
|
59
|
+
- Provider-billed costs are shown as-is; Hermes estimates are marked with `~`. Mixed data is labeled mixed.
|
|
60
|
+
- Sessions with messages but **zero recorded tokens** (known Hermes accounting gaps, e.g. [#12023](https://github.com/NousResearch/hermes-agent/issues/12023)) are detected and reported: totals are then explicitly a **lower bound** — and fixing the accounting becomes recommendation #1.
|
|
61
|
+
- Input composition from request dumps is char-proportional and labeled *sampled estimate*, not truth.
|
|
62
|
+
|
|
63
|
+
## Privacy
|
|
64
|
+
|
|
65
|
+
Everything runs locally and reads your database **read-only**. No network calls. No telemetry. The report is yours.
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
agentburn # autodetect agent, last 30 days
|
|
71
|
+
agentburn --days 7
|
|
72
|
+
agentburn --db /path/to/state.db
|
|
73
|
+
agentburn --night 23-7 # custom overnight window (local time)
|
|
74
|
+
agentburn --json # machine-readable, pipe it anywhere
|
|
75
|
+
agentburn --no-color
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Mechanics
|
|
79
|
+
|
|
80
|
+
**📤 Share your burn (`--share`).** An anonymized card — categories, models and totals only; session titles, paths and content are excluded *by construction*. Safe to paste into a post; `--svg card.svg` renders the same card as an image:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
🔥 my hermes agent · last 30d
|
|
84
|
+
~$45.50 · 1.75M tokens → ~$430/mo pace
|
|
85
|
+
cron 79% · cli 9% · telegram 7% · subagent 5%
|
|
86
|
+
🌙 while I slept (00–08): ~$36.00 (79%)
|
|
87
|
+
heaviest overhead: telegram 20,000 tokens/call (community baseline ≈8k/call: +150%)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**📏 Calibration against public benchmarks.** "Is 15k input tokens per call normal?" The report compares your fixed overhead with community-measured references embedded as dated constants (e.g. the [Phala always-on-agent benchmark](https://phala.com/posts/understanding-openclaws-token-usage), 2026-03: ≈8k/call baseline). No network — sources are cited inline.
|
|
91
|
+
|
|
92
|
+
**📐 Optimize → prove it (`--save-baseline` / `--compare`).** Snapshot your pace, change the config (cheaper cron model, trimmed toolsets), then `agentburn --compare` shows the delta in $/month — pace-normalized, so a 7-day baseline compares honestly with a 30-day window. Every recommendation becomes a testable promise.
|
|
93
|
+
|
|
94
|
+
**🩺 `agentburn doctor`.** Trackers disagree because the agent's own accounting has gaps. doctor names the broken combinations (provider × model × source) for zero-usage and unpriced sessions, and generates a ready-to-paste upstream bug report — counters only, no message content.
|
|
95
|
+
|
|
96
|
+
## Supported agents
|
|
97
|
+
|
|
98
|
+
| Agent | Status | Data source |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| **Hermes Agent** | ✅ v0.1 | `~/.hermes/state.db` (+ optional `request_dump_*.json` for input composition) |
|
|
101
|
+
| OpenClaw | roadmap | session JSONL |
|
|
102
|
+
| Claude Code | roadmap | `~/.claude/projects/**.jsonl` |
|
|
103
|
+
|
|
104
|
+
The core is agent-agnostic (normalized session/event model); adapters are ~150 lines each. PRs welcome.
|
|
105
|
+
|
|
106
|
+
## Related
|
|
107
|
+
|
|
108
|
+
[token-history](https://github.com/Socialpranker/token-history) — the macro view: daily archive of *which agents the world uses* (OpenRouter rankings). agentburn is the micro view: *where yours burns*.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""agentburn — where does your AI agent burn money?
|
|
2
|
+
|
|
3
|
+
Local, zero-dependency token/cost profiler for always-on AI agents.
|
|
4
|
+
Adapter #1: Hermes Agent (~/.hermes/state.db). Honest methodology:
|
|
5
|
+
numbers come from the agent's own accounting; gaps are surfaced, not hidden.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Adapter registry. v0.1 ships Hermes; OpenClaw and Claude Code are next."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from . import hermes
|
|
6
|
+
|
|
7
|
+
ADAPTERS = {
|
|
8
|
+
"hermes": hermes,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def detect() -> list[str]:
|
|
13
|
+
"""Return adapter names whose data is present on this machine."""
|
|
14
|
+
return [name for name, mod in ADAPTERS.items() if mod.available()]
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Hermes Agent adapter: reads ~/.hermes/state.db (SQLite) read-only.
|
|
2
|
+
|
|
3
|
+
Schema observed in NousResearch/hermes-agent `hermes_state.py` (June 2026):
|
|
4
|
+
sessions(id, source, model, parent_session_id, started_at, ended_at,
|
|
5
|
+
message_count, tool_call_count, api_call_count,
|
|
6
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
7
|
+
reasoning_tokens, estimated_cost_usd, actual_cost_usd, cost_status,
|
|
8
|
+
title, archived, ...)
|
|
9
|
+
messages(session_id, role, tool_name, tool_calls, timestamp, token_count, ...)
|
|
10
|
+
|
|
11
|
+
Known upstream accounting gaps (hermes-agent #12023, #6775, #8337): some
|
|
12
|
+
providers/streams record zero tokens. We DETECT and REPORT those gaps instead
|
|
13
|
+
of silently presenting totals as truth.
|
|
14
|
+
|
|
15
|
+
Optional precision layer: request_dump_*.json files (written when request
|
|
16
|
+
dumping is enabled) contain the full API body; we sample them to estimate the
|
|
17
|
+
input composition (system prompt vs tool definitions vs history).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import glob
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sqlite3
|
|
26
|
+
import time
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
from ..model import DumpComposition, SessionRec, Snapshot, ToolStat
|
|
30
|
+
|
|
31
|
+
GATEWAY_SOURCES = {
|
|
32
|
+
"telegram",
|
|
33
|
+
"whatsapp",
|
|
34
|
+
"discord",
|
|
35
|
+
"slack",
|
|
36
|
+
"signal",
|
|
37
|
+
"imessage",
|
|
38
|
+
"email",
|
|
39
|
+
"api",
|
|
40
|
+
"api_server",
|
|
41
|
+
"web",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def default_db_path() -> str:
|
|
46
|
+
return os.path.join(os.path.expanduser("~"), ".hermes", "state.db")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def available() -> bool:
|
|
50
|
+
return os.path.exists(default_db_path())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize_source(raw: Optional[str]) -> str:
|
|
54
|
+
s = (raw or "unknown").strip().lower()
|
|
55
|
+
if s in ("cli", "cron", "subagent"):
|
|
56
|
+
return s
|
|
57
|
+
if s in GATEWAY_SOURCES:
|
|
58
|
+
return f"gateway:{s}"
|
|
59
|
+
if s.startswith(("gateway:", "other:")):
|
|
60
|
+
return s
|
|
61
|
+
return f"other:{s}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _columns(con: sqlite3.Connection, table: str) -> set:
|
|
65
|
+
try:
|
|
66
|
+
return {r[1] for r in con.execute(f"PRAGMA table_info({table})")}
|
|
67
|
+
except sqlite3.Error:
|
|
68
|
+
return set()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _col(cols: set, name: str, default_sql: str = "NULL") -> str:
|
|
72
|
+
return name if name in cols else f"{default_sql} AS {name}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load(
|
|
76
|
+
db_path: Optional[str] = None,
|
|
77
|
+
days: Optional[int] = 30,
|
|
78
|
+
dumps_dir: Optional[str] = None,
|
|
79
|
+
now: Optional[float] = None,
|
|
80
|
+
) -> Snapshot:
|
|
81
|
+
path = db_path or default_db_path()
|
|
82
|
+
if not os.path.exists(path):
|
|
83
|
+
raise FileNotFoundError(
|
|
84
|
+
f"Hermes state not found at {path}. Pass --db /path/to/state.db "
|
|
85
|
+
"or run on the machine where Hermes Agent lives."
|
|
86
|
+
)
|
|
87
|
+
now = now or time.time()
|
|
88
|
+
since = now - days * 86400 if days else 0
|
|
89
|
+
|
|
90
|
+
snap = Snapshot(
|
|
91
|
+
agent="hermes",
|
|
92
|
+
source_path=path,
|
|
93
|
+
generated_at=now,
|
|
94
|
+
days=days,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
con = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
98
|
+
try:
|
|
99
|
+
con.row_factory = sqlite3.Row
|
|
100
|
+
scols = _columns(con, "sessions")
|
|
101
|
+
if "id" not in scols:
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
"sessions table not found — is this really a Hermes state.db? "
|
|
104
|
+
"(schema may have changed; please open an issue with `PRAGMA table_info(sessions)` output)"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
fields = ", ".join(
|
|
108
|
+
[
|
|
109
|
+
"id",
|
|
110
|
+
_col(scols, "source", "'unknown'"),
|
|
111
|
+
_col(scols, "model"),
|
|
112
|
+
_col(scols, "parent_session_id"),
|
|
113
|
+
_col(scols, "started_at"),
|
|
114
|
+
_col(scols, "ended_at"),
|
|
115
|
+
_col(scols, "title"),
|
|
116
|
+
_col(scols, "message_count", "0"),
|
|
117
|
+
_col(scols, "api_call_count", "0"),
|
|
118
|
+
_col(scols, "input_tokens", "0"),
|
|
119
|
+
_col(scols, "output_tokens", "0"),
|
|
120
|
+
_col(scols, "cache_read_tokens", "0"),
|
|
121
|
+
_col(scols, "cache_write_tokens", "0"),
|
|
122
|
+
_col(scols, "reasoning_tokens", "0"),
|
|
123
|
+
_col(scols, "estimated_cost_usd"),
|
|
124
|
+
_col(scols, "actual_cost_usd"),
|
|
125
|
+
_col(scols, "billing_provider"),
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
where = "WHERE COALESCE(started_at, 0) >= ?" if days else ""
|
|
129
|
+
rows = con.execute(
|
|
130
|
+
f"SELECT {fields} FROM sessions {where}", (since,) if days else ()
|
|
131
|
+
).fetchall()
|
|
132
|
+
|
|
133
|
+
for r in rows:
|
|
134
|
+
actual = r["actual_cost_usd"]
|
|
135
|
+
est = r["estimated_cost_usd"]
|
|
136
|
+
cost, basis = (
|
|
137
|
+
(actual, "actual")
|
|
138
|
+
if actual is not None
|
|
139
|
+
else (est, "estimated")
|
|
140
|
+
if est is not None
|
|
141
|
+
else (None, "unknown")
|
|
142
|
+
)
|
|
143
|
+
snap.sessions.append(
|
|
144
|
+
SessionRec(
|
|
145
|
+
id=str(r["id"]),
|
|
146
|
+
source=normalize_source(r["source"]),
|
|
147
|
+
model=r["model"],
|
|
148
|
+
started_at=r["started_at"],
|
|
149
|
+
ended_at=r["ended_at"],
|
|
150
|
+
parent_id=r["parent_session_id"],
|
|
151
|
+
title=r["title"],
|
|
152
|
+
api_calls=int(r["api_call_count"] or 0),
|
|
153
|
+
input_tokens=int(r["input_tokens"] or 0),
|
|
154
|
+
output_tokens=int(r["output_tokens"] or 0),
|
|
155
|
+
cache_read_tokens=int(r["cache_read_tokens"] or 0),
|
|
156
|
+
cache_write_tokens=int(r["cache_write_tokens"] or 0),
|
|
157
|
+
reasoning_tokens=int(r["reasoning_tokens"] or 0),
|
|
158
|
+
cost_usd=float(cost) if cost is not None else None,
|
|
159
|
+
cost_basis=basis,
|
|
160
|
+
message_count=int(r["message_count"] or 0),
|
|
161
|
+
provider=r["billing_provider"],
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
mcols = _columns(con, "messages")
|
|
166
|
+
if {"tool_name", "timestamp"} <= mcols:
|
|
167
|
+
tok = "COALESCE(token_count, 0)" if "token_count" in mcols else "0"
|
|
168
|
+
mwhere = "AND timestamp >= ?" if days else ""
|
|
169
|
+
for r in con.execute(
|
|
170
|
+
f"""SELECT tool_name, COUNT(*) AS calls, SUM({tok}) AS toks
|
|
171
|
+
FROM messages
|
|
172
|
+
WHERE tool_name IS NOT NULL AND tool_name != '' {mwhere}
|
|
173
|
+
GROUP BY tool_name ORDER BY toks DESC""",
|
|
174
|
+
(since,) if days else (),
|
|
175
|
+
):
|
|
176
|
+
snap.tools.append(
|
|
177
|
+
ToolStat(name=r["tool_name"], calls=int(r["calls"]), result_tokens=int(r["toks"] or 0))
|
|
178
|
+
)
|
|
179
|
+
finally:
|
|
180
|
+
con.close()
|
|
181
|
+
|
|
182
|
+
comp = _sample_dumps(dumps_dir or os.path.join(os.path.dirname(path), "sessions"))
|
|
183
|
+
if comp:
|
|
184
|
+
snap.composition = comp
|
|
185
|
+
return snap
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _sample_dumps(dumps_dir: str, limit: int = 20) -> Optional[DumpComposition]:
|
|
189
|
+
"""Estimate input composition from request dumps (newest `limit` files).
|
|
190
|
+
|
|
191
|
+
Char-proportional split of the request body: system prompt vs tool
|
|
192
|
+
definitions vs message history. Proportions, not exact tokens — labeled
|
|
193
|
+
as sampled estimate in the report.
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
files = sorted(glob.glob(os.path.join(dumps_dir, "request_dump_*.json")))[-limit:]
|
|
197
|
+
except OSError:
|
|
198
|
+
return None
|
|
199
|
+
if not files:
|
|
200
|
+
return None
|
|
201
|
+
sys_c = tools_c = hist_c = 0
|
|
202
|
+
samples = 0
|
|
203
|
+
for f in files:
|
|
204
|
+
try:
|
|
205
|
+
with open(f, "r", encoding="utf-8", errors="replace") as fh:
|
|
206
|
+
payload = json.load(fh)
|
|
207
|
+
except (OSError, json.JSONDecodeError):
|
|
208
|
+
continue
|
|
209
|
+
body = payload.get("body") or payload.get("request") or payload
|
|
210
|
+
if not isinstance(body, dict):
|
|
211
|
+
continue
|
|
212
|
+
msgs = body.get("messages") or []
|
|
213
|
+
tools = body.get("tools") or []
|
|
214
|
+
s = t = h = 0
|
|
215
|
+
if isinstance(body.get("system"), str):
|
|
216
|
+
s += len(body["system"])
|
|
217
|
+
for m in msgs if isinstance(msgs, list) else []:
|
|
218
|
+
chunk = len(json.dumps(m, ensure_ascii=False, default=str))
|
|
219
|
+
if isinstance(m, dict) and m.get("role") == "system":
|
|
220
|
+
s += chunk
|
|
221
|
+
else:
|
|
222
|
+
h += chunk
|
|
223
|
+
t = len(json.dumps(tools, ensure_ascii=False, default=str)) if tools else 0
|
|
224
|
+
total = s + t + h
|
|
225
|
+
if total <= 0:
|
|
226
|
+
continue
|
|
227
|
+
sys_c += s
|
|
228
|
+
tools_c += t
|
|
229
|
+
hist_c += h
|
|
230
|
+
samples += 1
|
|
231
|
+
total = sys_c + tools_c + hist_c
|
|
232
|
+
if samples == 0 or total == 0:
|
|
233
|
+
return None
|
|
234
|
+
return DumpComposition(
|
|
235
|
+
samples=samples,
|
|
236
|
+
system_share=sys_c / total,
|
|
237
|
+
tools_share=tools_c / total,
|
|
238
|
+
history_share=hist_c / total,
|
|
239
|
+
)
|