tomo-ai 0.8.11 → 0.8.12
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.
- package/CHANGELOG.md +11 -0
- package/contrib/observability/README.md +90 -0
- package/contrib/observability/alloy/config.alloy +53 -0
- package/contrib/observability/docker-compose.yml +70 -0
- package/contrib/observability/grafana/dashboards/tomo.json +328 -0
- package/contrib/observability/grafana/provisioning/dashboards/dashboards.yml +8 -0
- package/contrib/observability/grafana/provisioning/datasources/datasources.yml +16 -0
- package/contrib/observability/prometheus.yml +9 -0
- package/dist/cli/start.js +26 -0
- package/dist/cli/start.js.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -1
- package/dist/metrics/activity-log.d.ts +46 -0
- package/dist/metrics/activity-log.d.ts.map +1 -0
- package/dist/metrics/activity-log.js +103 -0
- package/dist/metrics/activity-log.js.map +1 -0
- package/dist/metrics/exporter.d.ts +58 -0
- package/dist/metrics/exporter.d.ts.map +1 -0
- package/dist/metrics/exporter.js +255 -0
- package/dist/metrics/exporter.js.map +1 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.12 (2026-07-12)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **Prometheus metrics + activity log with Grafana stack** (#238). Two new `WatchBus` subscribers, gated by `config.metrics` (default off): a `prom-client` exporter on `127.0.0.1:9464` (turns, cost, per-session context, tool calls, cron, heartbeats, compactions) and an NDJSON activity log at `~/.tomo/logs/activity.ndjson` for Loki tailing (transcript text local-only, redactable via `metrics.includeMessageText=false`). `contrib/observability/` ships a docker-compose stack (Prometheus, Loki, Alloy, Grafana) with a provisioned Tomo dashboard.
|
|
8
|
+
|
|
9
|
+
### Dependencies
|
|
10
|
+
|
|
11
|
+
- Bump `@anthropic-ai/claude-agent-sdk` 0.3.201 → 0.3.207 (#237).
|
|
12
|
+
- Bump the dev-dependencies group (#236). The TypeScript ^6 → ^7 bump in that group was reverted — no `typescript-eslint` release yet supports TypeScript 7 (peer range caps at `<6.1.0`), so it broke `npm ci`.
|
|
13
|
+
|
|
3
14
|
## 0.8.11 (2026-07-09)
|
|
4
15
|
|
|
5
16
|
### Features
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Tomo observability stack
|
|
2
|
+
|
|
3
|
+
A self-contained Grafana + Prometheus + Loki + Alloy stack for watching Tomo
|
|
4
|
+
from a dashboard. Data never leaves the machine: Prometheus, Loki, and Alloy
|
|
5
|
+
bind to `127.0.0.1` only. Grafana is reachable from the local network (login
|
|
6
|
+
required) so you can open the dashboard from another computer.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
Tomo daemon ──/metrics (127.0.0.1:9464)──▶ Prometheus ──┐
|
|
10
|
+
│ ├──▶ Grafana (127.0.0.1:3000)
|
|
11
|
+
└──~/.tomo/logs/activity.ndjson──▶ Alloy ──▶ Loki ─┘
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
1. Enable metrics in `~/.tomo/config.json`:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"metrics": { "enabled": true }
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then restart the daemon (`tomo restart`) and verify:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
curl -s localhost:9464/metrics | grep tomo_
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
2. Copy the stack somewhere you own, then start it (requires Docker Desktop).
|
|
31
|
+
Copying matters: run it in place and an `npm upgrade` will overwrite any
|
|
32
|
+
compose/dashboard customizations.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# from a git clone:
|
|
36
|
+
cp -r contrib/observability ~/tomo-observability
|
|
37
|
+
# or from a global npm install:
|
|
38
|
+
cp -r "$(npm root -g)/tomo-ai/contrib/observability" ~/tomo-observability
|
|
39
|
+
|
|
40
|
+
cd ~/tomo-observability
|
|
41
|
+
docker compose up -d
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
3. Open <http://localhost:3000> — or from another computer on your network,
|
|
45
|
+
`http://<hostname>.local:3000` (e.g. `http://mac-mini.local:3000`). Sign in
|
|
46
|
+
with `admin` / `admin`; Grafana forces a password change on first login.
|
|
47
|
+
The **Tomo** dashboard is pre-provisioned.
|
|
48
|
+
|
|
49
|
+
To restrict Grafana to this machine only, change its port mapping back to
|
|
50
|
+
`"127.0.0.1:3000:3000"` in `docker-compose.yml`. For access from *outside*
|
|
51
|
+
your home network, don't port-forward — use Tailscale or similar.
|
|
52
|
+
|
|
53
|
+
## Config reference
|
|
54
|
+
|
|
55
|
+
`~/.tomo/config.json` → `metrics` (env overrides in parens):
|
|
56
|
+
|
|
57
|
+
| Key | Default | Meaning |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `enabled` (`TOMO_METRICS`) | `false` | Serve `/metrics` and write the activity log |
|
|
60
|
+
| `port` (`TOMO_METRICS_PORT`) | `9464` | Prometheus exporter port, loopback-only |
|
|
61
|
+
| `activityLog` | `true` | Write `~/.tomo/logs/activity.ndjson` for Loki |
|
|
62
|
+
| `includeMessageText` | `true` | Include transcript text in the activity log. Disable if you ever ship the log off this machine. |
|
|
63
|
+
|
|
64
|
+
## What's collected
|
|
65
|
+
|
|
66
|
+
**Prometheus** (`tomo_*`, plus standard `process_*`/`nodejs_*`): turns by
|
|
67
|
+
source/outcome, turn duration histogram, cumulative cost (USD), per-session
|
|
68
|
+
context usage, tool calls, cron runs + upcoming cron schedule, compactions,
|
|
69
|
+
heartbeats (count, last, next), warn/error issues.
|
|
70
|
+
|
|
71
|
+
**Loki** (`{job="tomo-activity"}`): every watch-bus event as structured JSON —
|
|
72
|
+
the same feed `tomo watch` renders. Labels: `type`, `session`, `level`.
|
|
73
|
+
Example queries:
|
|
74
|
+
|
|
75
|
+
```logql
|
|
76
|
+
{job="tomo-activity", type="tool.start"} # every tool call
|
|
77
|
+
{job="tomo-activity", type="transcript", session="dm:merlin"}
|
|
78
|
+
{job="tomo-activity"} | json | costUsd > 0.10 # expensive turns
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Notes
|
|
82
|
+
|
|
83
|
+
- Prometheus keeps 90 days of metrics; Loki uses its default retention.
|
|
84
|
+
Volumes (`prometheus-data`, `loki-data`, `grafana-data`) survive
|
|
85
|
+
`docker compose down`; add `-v` to wipe them.
|
|
86
|
+
- Counters reset when the daemon restarts — dashboard queries use
|
|
87
|
+
`increase()`/`rate()`, which handle resets.
|
|
88
|
+
- If `TOMO_HOME` is not `~/.tomo`, fix the Alloy volume mount in
|
|
89
|
+
`docker-compose.yml`.
|
|
90
|
+
- History starts when you enable the stack; nothing is backfilled.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Tail Tomo's NDJSON activity feed into Loki. The glob also matches rotated
|
|
2
|
+
// files (activity.ndjson.1, …) so lines written just before a rotation are
|
|
3
|
+
// still picked up if the shipper lags.
|
|
4
|
+
|
|
5
|
+
local.file_match "tomo_activity" {
|
|
6
|
+
path_targets = [{ __path__ = "/var/log/tomo/activity.ndjson*" }]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
loki.source.file "tomo_activity" {
|
|
10
|
+
targets = local.file_match.tomo_activity.targets
|
|
11
|
+
forward_to = [loki.process.tomo.receiver]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
loki.process "tomo" {
|
|
15
|
+
stage.json {
|
|
16
|
+
expressions = {
|
|
17
|
+
type = "type",
|
|
18
|
+
session = "sessionKey",
|
|
19
|
+
level = "level",
|
|
20
|
+
ts = "ts",
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Low-cardinality labels only: event type, session key, issue level.
|
|
25
|
+
// Everything else stays in the JSON body and is queryable via `| json`.
|
|
26
|
+
stage.labels {
|
|
27
|
+
values = {
|
|
28
|
+
type = "",
|
|
29
|
+
session = "",
|
|
30
|
+
level = "",
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
stage.static_labels {
|
|
35
|
+
values = {
|
|
36
|
+
job = "tomo-activity",
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Use the event's own millisecond timestamp, not ingestion time.
|
|
41
|
+
stage.timestamp {
|
|
42
|
+
source = "ts"
|
|
43
|
+
format = "UnixMs"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
forward_to = [loki.write.local.receiver]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
loki.write "local" {
|
|
50
|
+
endpoint {
|
|
51
|
+
url = "http://loki:3100/loki/api/v1/push"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Local observability stack for Tomo: Prometheus (metrics), Loki (activity
|
|
2
|
+
# feed), Alloy (log shipper), Grafana (dashboard). See README.md.
|
|
3
|
+
#
|
|
4
|
+
# Grafana listens on the LAN (with login required); everything else binds to
|
|
5
|
+
# 127.0.0.1 — Prometheus/Loki have no auth of their own, and Grafana reaches
|
|
6
|
+
# them over the internal Docker network, so they never need outside exposure.
|
|
7
|
+
services:
|
|
8
|
+
prometheus:
|
|
9
|
+
image: prom/prometheus:v3.4.1
|
|
10
|
+
ports:
|
|
11
|
+
- "127.0.0.1:9090:9090"
|
|
12
|
+
volumes:
|
|
13
|
+
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
14
|
+
- prometheus-data:/prometheus
|
|
15
|
+
command:
|
|
16
|
+
- --config.file=/etc/prometheus/prometheus.yml
|
|
17
|
+
- --storage.tsdb.retention.time=90d
|
|
18
|
+
extra_hosts:
|
|
19
|
+
# Docker Desktop for Mac provides host.docker.internal natively; this
|
|
20
|
+
# line makes the same name work if the stack ever runs on Linux.
|
|
21
|
+
- "host.docker.internal:host-gateway"
|
|
22
|
+
restart: unless-stopped
|
|
23
|
+
|
|
24
|
+
loki:
|
|
25
|
+
image: grafana/loki:3.5.1
|
|
26
|
+
# No host port: Grafana and Alloy reach Loki over the internal Docker
|
|
27
|
+
# network, and host 3100 is BlueBubbles' default webhook port — publishing
|
|
28
|
+
# it would collide with a default iMessage setup. To poke Loki directly:
|
|
29
|
+
# docker compose exec loki wget -qO- localhost:3100/ready
|
|
30
|
+
volumes:
|
|
31
|
+
- loki-data:/loki
|
|
32
|
+
restart: unless-stopped
|
|
33
|
+
|
|
34
|
+
alloy:
|
|
35
|
+
image: grafana/alloy:v1.9.1
|
|
36
|
+
volumes:
|
|
37
|
+
- ./alloy/config.alloy:/etc/alloy/config.alloy:ro
|
|
38
|
+
- alloy-data:/var/lib/alloy/data
|
|
39
|
+
# Tomo's activity log (metrics.activityLog). Adjust if TOMO_HOME moved.
|
|
40
|
+
- ~/.tomo/logs:/var/log/tomo:ro
|
|
41
|
+
command:
|
|
42
|
+
- run
|
|
43
|
+
- /etc/alloy/config.alloy
|
|
44
|
+
- --storage.path=/var/lib/alloy/data
|
|
45
|
+
depends_on:
|
|
46
|
+
- loki
|
|
47
|
+
restart: unless-stopped
|
|
48
|
+
|
|
49
|
+
grafana:
|
|
50
|
+
image: grafana/grafana:12.0.2
|
|
51
|
+
ports:
|
|
52
|
+
# LAN-reachable so other machines can open the dashboard. Because the
|
|
53
|
+
# activity feed can contain message text, login stays REQUIRED — first
|
|
54
|
+
# sign-in is admin/admin and Grafana forces a password change. To lock
|
|
55
|
+
# it back down to this machine only, prefix with 127.0.0.1: again.
|
|
56
|
+
- "3000:3000"
|
|
57
|
+
volumes:
|
|
58
|
+
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
|
59
|
+
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
|
60
|
+
- grafana-data:/var/lib/grafana
|
|
61
|
+
depends_on:
|
|
62
|
+
- prometheus
|
|
63
|
+
- loki
|
|
64
|
+
restart: unless-stopped
|
|
65
|
+
|
|
66
|
+
volumes:
|
|
67
|
+
prometheus-data:
|
|
68
|
+
loki-data:
|
|
69
|
+
alloy-data:
|
|
70
|
+
grafana-data:
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
{
|
|
2
|
+
"uid": "tomo-overview",
|
|
3
|
+
"title": "Tomo",
|
|
4
|
+
"tags": ["tomo"],
|
|
5
|
+
"schemaVersion": 39,
|
|
6
|
+
"editable": true,
|
|
7
|
+
"time": { "from": "now-24h", "to": "now" },
|
|
8
|
+
"refresh": "30s",
|
|
9
|
+
"panels": [
|
|
10
|
+
{
|
|
11
|
+
"id": 1,
|
|
12
|
+
"type": "stat",
|
|
13
|
+
"title": "Daemon",
|
|
14
|
+
"gridPos": { "h": 4, "w": 3, "x": 0, "y": 0 },
|
|
15
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
16
|
+
"targets": [{ "refId": "A", "expr": "up{job=\"tomo\"}" }],
|
|
17
|
+
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "background" },
|
|
18
|
+
"fieldConfig": {
|
|
19
|
+
"defaults": {
|
|
20
|
+
"mappings": [
|
|
21
|
+
{
|
|
22
|
+
"type": "value",
|
|
23
|
+
"options": {
|
|
24
|
+
"0": { "text": "DOWN", "color": "red" },
|
|
25
|
+
"1": { "text": "UP", "color": "green" }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"overrides": []
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": 2,
|
|
35
|
+
"type": "stat",
|
|
36
|
+
"title": "Cost (24h)",
|
|
37
|
+
"gridPos": { "h": 4, "w": 3, "x": 3, "y": 0 },
|
|
38
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
39
|
+
"targets": [{ "refId": "A", "expr": "sum(increase(tomo_cost_usd_total[24h]))" }],
|
|
40
|
+
"options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
|
|
41
|
+
"fieldConfig": { "defaults": { "unit": "currencyUSD", "decimals": 2 }, "overrides": [] }
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": 3,
|
|
45
|
+
"type": "stat",
|
|
46
|
+
"title": "Turns (24h)",
|
|
47
|
+
"gridPos": { "h": 4, "w": 3, "x": 6, "y": 0 },
|
|
48
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
49
|
+
"targets": [{ "refId": "A", "expr": "sum(increase(tomo_turns_total[24h]))" }],
|
|
50
|
+
"options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
|
|
51
|
+
"fieldConfig": { "defaults": { "unit": "short", "decimals": 0 }, "overrides": [] }
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": 4,
|
|
55
|
+
"type": "stat",
|
|
56
|
+
"title": "Errors (24h)",
|
|
57
|
+
"gridPos": { "h": 4, "w": 3, "x": 9, "y": 0 },
|
|
58
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
59
|
+
"targets": [
|
|
60
|
+
{ "refId": "A", "expr": "sum(increase(tomo_issues_total{level=\"error\"}[24h])) or vector(0)" }
|
|
61
|
+
],
|
|
62
|
+
"options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
|
|
63
|
+
"fieldConfig": {
|
|
64
|
+
"defaults": {
|
|
65
|
+
"unit": "short",
|
|
66
|
+
"decimals": 0,
|
|
67
|
+
"thresholds": {
|
|
68
|
+
"mode": "absolute",
|
|
69
|
+
"steps": [
|
|
70
|
+
{ "color": "green", "value": null },
|
|
71
|
+
{ "color": "red", "value": 1 }
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"overrides": []
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": 5,
|
|
80
|
+
"type": "stat",
|
|
81
|
+
"title": "In flight",
|
|
82
|
+
"gridPos": { "h": 4, "w": 3, "x": 12, "y": 0 },
|
|
83
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
84
|
+
"targets": [{ "refId": "A", "expr": "tomo_turns_active" }],
|
|
85
|
+
"options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
|
|
86
|
+
"fieldConfig": { "defaults": { "unit": "short", "decimals": 0 }, "overrides": [] }
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": 6,
|
|
90
|
+
"type": "stat",
|
|
91
|
+
"title": "Memory (RSS)",
|
|
92
|
+
"gridPos": { "h": 4, "w": 3, "x": 15, "y": 0 },
|
|
93
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
94
|
+
"targets": [{ "refId": "A", "expr": "process_resident_memory_bytes{job=\"tomo\"}" }],
|
|
95
|
+
"options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
|
|
96
|
+
"fieldConfig": { "defaults": { "unit": "bytes" }, "overrides": [] }
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": 7,
|
|
100
|
+
"type": "stat",
|
|
101
|
+
"title": "Next heartbeat",
|
|
102
|
+
"gridPos": { "h": 4, "w": 3, "x": 18, "y": 0 },
|
|
103
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
104
|
+
"targets": [{ "refId": "A", "expr": "tomo_heartbeat_next_timestamp_seconds * 1000" }],
|
|
105
|
+
"options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
|
|
106
|
+
"fieldConfig": {
|
|
107
|
+
"defaults": {
|
|
108
|
+
"unit": "dateTimeFromNow",
|
|
109
|
+
"noValue": "n/a",
|
|
110
|
+
"mappings": [
|
|
111
|
+
{
|
|
112
|
+
"type": "value",
|
|
113
|
+
"options": { "0": { "text": "continuity off", "color": "text" } }
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"overrides": []
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": 8,
|
|
122
|
+
"type": "stat",
|
|
123
|
+
"title": "Next cron job",
|
|
124
|
+
"gridPos": { "h": 4, "w": 3, "x": 21, "y": 0 },
|
|
125
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
126
|
+
"targets": [{ "refId": "A", "expr": "min(tomo_cron_next_run_timestamp_seconds) * 1000" }],
|
|
127
|
+
"options": { "reduceOptions": { "calcs": ["lastNotNull"] } },
|
|
128
|
+
"fieldConfig": { "defaults": { "unit": "dateTimeFromNow", "noValue": "none" }, "overrides": [] }
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": 21,
|
|
132
|
+
"type": "table",
|
|
133
|
+
"title": "Upcoming cron jobs",
|
|
134
|
+
"gridPos": { "h": 9, "w": 8, "x": 0, "y": 4 },
|
|
135
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
136
|
+
"targets": [
|
|
137
|
+
{
|
|
138
|
+
"refId": "A",
|
|
139
|
+
"expr": "tomo_cron_next_run_timestamp_seconds * 1000",
|
|
140
|
+
"format": "table",
|
|
141
|
+
"instant": true
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"transformations": [
|
|
145
|
+
{
|
|
146
|
+
"id": "organize",
|
|
147
|
+
"options": {
|
|
148
|
+
"excludeByName": { "Time": true, "__name__": true, "instance": true, "job": true },
|
|
149
|
+
"renameByName": { "cron_job": "Job", "Value": "Next run" }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"options": { "sortBy": [{ "displayName": "Next run", "desc": false }] },
|
|
154
|
+
"fieldConfig": {
|
|
155
|
+
"defaults": { "unit": "dateTimeFromNow" },
|
|
156
|
+
"overrides": [
|
|
157
|
+
{
|
|
158
|
+
"matcher": { "id": "byName", "options": "Job" },
|
|
159
|
+
"properties": [{ "id": "unit", "value": "string" }]
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": 22,
|
|
166
|
+
"type": "logs",
|
|
167
|
+
"title": "Tool calls",
|
|
168
|
+
"gridPos": { "h": 9, "w": 16, "x": 8, "y": 4 },
|
|
169
|
+
"datasource": { "type": "loki", "uid": "loki" },
|
|
170
|
+
"targets": [
|
|
171
|
+
{
|
|
172
|
+
"refId": "A",
|
|
173
|
+
"expr": "{job=\"tomo-activity\", type=\"tool.start\"} | json | line_format \"{{.tool}}{{if .agent}} [{{.agent}}]{{end}}{{if .detail}} — {{.detail}}{{end}}\""
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"refId": "B",
|
|
177
|
+
"expr": "{job=\"tomo-activity\", type=\"tool.end\"} | json | ok=\"false\" | line_format \"{{.tool}} FAILED{{if .durationMs}} after {{.durationMs}}ms{{end}}\""
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
"options": {
|
|
181
|
+
"showTime": true,
|
|
182
|
+
"sortOrder": "Descending",
|
|
183
|
+
"wrapLogMessage": true,
|
|
184
|
+
"enableLogDetails": true,
|
|
185
|
+
"dedupStrategy": "none"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": 10,
|
|
190
|
+
"type": "timeseries",
|
|
191
|
+
"title": "Cost per hour",
|
|
192
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 13 },
|
|
193
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
194
|
+
"targets": [
|
|
195
|
+
{ "refId": "A", "expr": "sum(increase(tomo_cost_usd_total[1h]))", "legendFormat": "USD/h" }
|
|
196
|
+
],
|
|
197
|
+
"fieldConfig": { "defaults": { "unit": "currencyUSD" }, "overrides": [] }
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"id": 11,
|
|
201
|
+
"type": "timeseries",
|
|
202
|
+
"title": "Turns per hour by source",
|
|
203
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 13 },
|
|
204
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
205
|
+
"targets": [
|
|
206
|
+
{
|
|
207
|
+
"refId": "A",
|
|
208
|
+
"expr": "sum by (source) (increase(tomo_turns_total[1h]))",
|
|
209
|
+
"legendFormat": "{{source}}"
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
"fieldConfig": {
|
|
213
|
+
"defaults": { "unit": "short", "custom": { "stacking": { "mode": "normal" } } },
|
|
214
|
+
"overrides": []
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"id": 12,
|
|
219
|
+
"type": "timeseries",
|
|
220
|
+
"title": "Turn duration (p50 / p95)",
|
|
221
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 21 },
|
|
222
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
223
|
+
"targets": [
|
|
224
|
+
{
|
|
225
|
+
"refId": "A",
|
|
226
|
+
"expr": "histogram_quantile(0.5, sum by (le) (rate(tomo_turn_duration_seconds_bucket[$__rate_interval])))",
|
|
227
|
+
"legendFormat": "p50"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"refId": "B",
|
|
231
|
+
"expr": "histogram_quantile(0.95, sum by (le) (rate(tomo_turn_duration_seconds_bucket[$__rate_interval])))",
|
|
232
|
+
"legendFormat": "p95"
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
"fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] }
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"id": 13,
|
|
239
|
+
"type": "timeseries",
|
|
240
|
+
"title": "Context usage by session",
|
|
241
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 21 },
|
|
242
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
243
|
+
"targets": [
|
|
244
|
+
{
|
|
245
|
+
"refId": "A",
|
|
246
|
+
"expr": "tomo_context_used_tokens / on(session) tomo_context_max_tokens",
|
|
247
|
+
"legendFormat": "{{session}}"
|
|
248
|
+
}
|
|
249
|
+
],
|
|
250
|
+
"fieldConfig": {
|
|
251
|
+
"defaults": { "unit": "percentunit", "min": 0, "max": 1 },
|
|
252
|
+
"overrides": []
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"id": 14,
|
|
257
|
+
"type": "timeseries",
|
|
258
|
+
"title": "Tool calls per hour",
|
|
259
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 29 },
|
|
260
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
261
|
+
"targets": [
|
|
262
|
+
{
|
|
263
|
+
"refId": "A",
|
|
264
|
+
"expr": "sum by (tool) (increase(tomo_tool_calls_total[1h]))",
|
|
265
|
+
"legendFormat": "{{tool}}"
|
|
266
|
+
}
|
|
267
|
+
],
|
|
268
|
+
"fieldConfig": {
|
|
269
|
+
"defaults": { "unit": "short", "custom": { "stacking": { "mode": "normal" } } },
|
|
270
|
+
"overrides": []
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"id": 15,
|
|
275
|
+
"type": "timeseries",
|
|
276
|
+
"title": "Messages per hour by channel",
|
|
277
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 29 },
|
|
278
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
279
|
+
"targets": [
|
|
280
|
+
{
|
|
281
|
+
"refId": "A",
|
|
282
|
+
"expr": "sum by (channel, role) (increase(tomo_messages_total[1h]))",
|
|
283
|
+
"legendFormat": "{{channel}} / {{role}}"
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
"fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] }
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"id": 20,
|
|
290
|
+
"type": "logs",
|
|
291
|
+
"title": "Activity feed",
|
|
292
|
+
"gridPos": { "h": 12, "w": 24, "x": 0, "y": 37 },
|
|
293
|
+
"datasource": { "type": "loki", "uid": "loki" },
|
|
294
|
+
"targets": [{ "refId": "A", "expr": "{job=\"tomo-activity\"}" }],
|
|
295
|
+
"options": {
|
|
296
|
+
"showTime": true,
|
|
297
|
+
"sortOrder": "Descending",
|
|
298
|
+
"wrapLogMessage": true,
|
|
299
|
+
"enableLogDetails": true
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
"annotations": {
|
|
304
|
+
"list": [
|
|
305
|
+
{
|
|
306
|
+
"name": "Compactions",
|
|
307
|
+
"datasource": { "type": "loki", "uid": "loki" },
|
|
308
|
+
"enable": true,
|
|
309
|
+
"expr": "{job=\"tomo-activity\", type=\"compact\"}",
|
|
310
|
+
"iconColor": "orange"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"name": "Heartbeats",
|
|
314
|
+
"datasource": { "type": "loki", "uid": "loki" },
|
|
315
|
+
"enable": true,
|
|
316
|
+
"expr": "{job=\"tomo-activity\", type=\"heartbeat\"}",
|
|
317
|
+
"iconColor": "blue"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"name": "Issues",
|
|
321
|
+
"datasource": { "type": "loki", "uid": "loki" },
|
|
322
|
+
"enable": false,
|
|
323
|
+
"expr": "{job=\"tomo-activity\", type=\"issue\"}",
|
|
324
|
+
"iconColor": "red"
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
apiVersion: 1
|
|
2
|
+
|
|
3
|
+
datasources:
|
|
4
|
+
- name: Prometheus
|
|
5
|
+
uid: prometheus
|
|
6
|
+
type: prometheus
|
|
7
|
+
access: proxy
|
|
8
|
+
url: http://prometheus:9090
|
|
9
|
+
isDefault: true
|
|
10
|
+
editable: false
|
|
11
|
+
- name: Loki
|
|
12
|
+
uid: loki
|
|
13
|
+
type: loki
|
|
14
|
+
access: proxy
|
|
15
|
+
url: http://loki:3100
|
|
16
|
+
editable: false
|
package/dist/cli/start.js
CHANGED
|
@@ -175,9 +175,35 @@ async function startForeground() {
|
|
|
175
175
|
}),
|
|
176
176
|
sendChat: (text) => agent.handleWatchChat(text),
|
|
177
177
|
});
|
|
178
|
+
// Metrics exporter + activity log: two more watch-bus subscribers, for
|
|
179
|
+
// Prometheus scrapes and Loki tailing. Off unless config.metrics.enabled.
|
|
180
|
+
const { MetricsExporter } = await import("../metrics/exporter.js");
|
|
181
|
+
const { ActivityLog } = await import("../metrics/activity-log.js");
|
|
182
|
+
let metricsExporter = null;
|
|
183
|
+
let activityLog = null;
|
|
184
|
+
if (config.metrics.enabled) {
|
|
185
|
+
metricsExporter = new MetricsExporter({
|
|
186
|
+
version: getCurrentVersion(),
|
|
187
|
+
model: config.model,
|
|
188
|
+
collectors: {
|
|
189
|
+
cronJobs: () => cronStore.list(),
|
|
190
|
+
nextHeartbeatAt: () => continuity.nextFireAt(),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
await metricsExporter.start(config.metrics.port);
|
|
194
|
+
if (config.metrics.activityLog) {
|
|
195
|
+
activityLog = new ActivityLog({
|
|
196
|
+
path: join(config.logsDir, "activity.ndjson"),
|
|
197
|
+
includeMessageText: config.metrics.includeMessageText,
|
|
198
|
+
});
|
|
199
|
+
activityLog.start();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
178
202
|
// Write PID so `tomo stop` can find us
|
|
179
203
|
writeFileSync(PID_FILE, String(process.pid));
|
|
180
204
|
const shutdown = async () => {
|
|
205
|
+
activityLog?.stop();
|
|
206
|
+
metricsExporter?.stop();
|
|
181
207
|
watchServer.stop();
|
|
182
208
|
versionChecker.stop();
|
|
183
209
|
rollupRunner.stop();
|