fableforge-agent-telemetry 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 FableForge Contributors
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.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: fableforge-agent-telemetry
3
+ Version: 0.1.0
4
+ Summary: Datadog for AI agents — real-time observability, token tracking, cost estimation, error rates
5
+ Requires-Python: >=3.10
6
+ License-File: LICENSE
7
+ Requires-Dist: fastapi>=0.110.0
8
+ Requires-Dist: uvicorn[standard]>=0.27.0
9
+ Requires-Dist: clickhouse-driver>=0.2.6
10
+ Requires-Dist: sqlalchemy>=2.0.0
11
+ Requires-Dist: pydantic>=2.5.0
12
+ Requires-Dist: plotly>=5.18.0
13
+ Requires-Dist: click>=8.1.0
14
+ Requires-Dist: tiktoken>=0.6.0
15
+ Requires-Dist: rich>=13.7.0
16
+ Requires-Dist: jinja2>=3.1.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
19
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
20
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
21
+ Dynamic: license-file
@@ -0,0 +1,269 @@
1
+ # AgentTelemetry
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/) [![Tests](https://img.shields.io/badge/tests-0-yellow.svg)](tests/)
4
+
5
+
6
+ **Datadog for AI agents** — real-time observability, token tracking, cost estimation, and error rates for autonomous agent sessions.
7
+
8
+ ## Features
9
+
10
+ - **Multi-format trace ingestion** — Parse traces from Glint-Research, armand0e, and v-Fable formats with auto-detection
11
+ - **Token tracking** — Count tokens with tiktoken, estimate costs with real per-model pricing
12
+ - **Cost estimation** — Detailed breakdowns for Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku, GPT-4, GPT-4o, GPT-4o-mini, Qwen3-Coder
13
+ - **Error tracking** — Automated error classification (bash, edit, timeout, rate limit, auth, context overflow, etc.) and recovery rate calculation
14
+ - **Interactive dashboard** — FastAPI + Plotly charts for session timelines, heatmaps, cost pies, and error breakdowns
15
+ - **Dual storage** — ClickHouse for production, SQLite for local/dev with automatic fallback
16
+ - **CLI** — Analyze traces, start dashboards, and generate reports from the command line
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install -e .
22
+
23
+ # With ClickHouse support (production):
24
+ pip install -e ".[clickhouse]"
25
+
26
+ # Development:
27
+ pip install -e ".[dev]"
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ### Analyze a Trace File
33
+
34
+ ```bash
35
+ # Auto-detect format and analyze
36
+ agenttelemetry analyze trace.jsonl
37
+
38
+ # Specify format explicitly
39
+ agenttelemetry analyze trace.jsonl --format glint
40
+
41
+ # Analyze without storing
42
+ agenttelemetry analyze trace.jsonl --no-store
43
+ ```
44
+
45
+ ### Cost Report
46
+
47
+ ```bash
48
+ # Detailed cost breakdown
49
+ agenttelemetry cost trace.jsonl
50
+
51
+ # Output:
52
+ Model: gpt-4
53
+ Input tokens: 1,234,567 ($0.037037)
54
+ Output tokens: 98,765 ($0.005926)
55
+ Cache read: 500,000 ($0.000000)
56
+ Cache creation: 0 ($0.000000)
57
+ ─────────────────────────────────────
58
+ Total: $0.042963
59
+ ```
60
+
61
+ ### Error Report
62
+
63
+ ```bash
64
+ # Show errors with classification and recovery rates
65
+ agenttelemetry errors trace.jsonl
66
+ ```
67
+
68
+ ### Token Counting
69
+
70
+ ```bash
71
+ # Count tokens in a string
72
+ agenttelemetry tokens "Hello, world!" --model gpt-4
73
+ ```
74
+
75
+ ### Dashboard
76
+
77
+ ```bash
78
+ # Start the interactive dashboard
79
+ agenttelemetry dashboard
80
+
81
+ # Custom host/port
82
+ agenttelemetry dashboard --host 0.0.0.0 --port 9000
83
+ ```
84
+
85
+ Open `http://127.0.0.1:8088/dashboard` to see session metrics, timelines, heatmaps, and cost reports.
86
+
87
+ ## Analyzing Fable5 Traces
88
+
89
+ Fable5 (v-Fable) traces can be analyzed directly:
90
+
91
+ ```bash
92
+ # Ingest a Fable5 session trace
93
+ agenttelemetry analyze ~/.fable/sessions/2025-01-15-abc123.jsonl
94
+
95
+ # View cost breakdown
96
+ agenttelemetry cost ~/.fable/sessions/2025-01-15-abc123.jsonl
97
+
98
+ # Check errors and recovery
99
+ agenttelemetry errors ~/.fable/sessions/2025-01-15-abc123.jsonl
100
+
101
+ # Start dashboard with ingested data
102
+ agenttelemetry dashboard
103
+ # Then open http://127.0.0.1:8088/dashboard
104
+ ```
105
+
106
+ ### v-Fable Trace Format
107
+
108
+ The v-Fable format uses these fields per JSONL line:
109
+
110
+ ```json
111
+ {
112
+ "kind": "tool_use",
113
+ "timestamp": "2025-01-15T10:30:00Z",
114
+ "session_id": "abc123",
115
+ "span_id": "span-001",
116
+ "parent_span_id": null,
117
+ "tool_name": "Bash",
118
+ "tokens": {"prompt": 1500, "completion": 800, "cache_read": 200, "cache_write": 50},
119
+ "duration_ms": 2345.6,
120
+ "cost_usd": 0.0234,
121
+ "model": "claude-3.5-sonnet",
122
+ "status": "success",
123
+ "error_message": null
124
+ }
125
+ ```
126
+
127
+ ### Glint-Research Format
128
+
129
+ ```json
130
+ {
131
+ "type": "tool_call",
132
+ "timestamp": "2025-01-15T10:30:00Z",
133
+ "session_id": "glint-session-1",
134
+ "span_id": "span-001",
135
+ "tool": "Edit",
136
+ "usage": {
137
+ "input_tokens": 2000,
138
+ "output_tokens": 500,
139
+ "cache_read_input_tokens": 300,
140
+ "cache_creation_input_tokens": 100
141
+ },
142
+ "duration_ms": 1500.0,
143
+ "model": "gpt-4o",
144
+ "error": null
145
+ }
146
+ ```
147
+
148
+ ### armand0e Format
149
+
150
+ Uses paired `invocation`/`response` events:
151
+
152
+ ```json
153
+ {"event": "invocation", "id": "span-001", "session": "arm-session", "tool": {"name": "Write", "input": {"path": "/tmp/file.py"}}, "model": "claude-3.5-sonnet"}
154
+ {"event": "response", "id": "span-001", "tokens": {"in": 1500, "out": 800, "cached": 200}, "latency_ms": 1800.0, "model": "claude-3.5-sonnet"}
155
+ ```
156
+
157
+ ## Pricing Reference
158
+
159
+ | Model | Input ($/1M tok) | Output ($/1M tok) | Cache Read ($/1M) | Cache Write ($/1M) |
160
+ |---|---|---|---|---|
161
+ | Claude 3.5 Sonnet | $3.00 | $15.00 | $0.30 | $3.75 |
162
+ | Claude 3 Opus | $15.00 | $75.00 | $1.50 | $18.75 |
163
+ | Claude 3 Haiku | $0.25 | $1.25 | $0.03 | $0.30 |
164
+ | GPT-4 | $30.00 | $60.00 | — | — |
165
+ | GPT-4o | $2.50 | $10.00 | $1.25 | — |
166
+ | GPT-4o-mini | $0.15 | $0.60 | $0.075 | — |
167
+ | Qwen3-Coder | $0.50 | $2.00 | $0.10 | $0.50 |
168
+
169
+ ## Architecture
170
+
171
+ ```
172
+ agent_telemetry/
173
+ ├── __init__.py # Package exports
174
+ ├── models.py # Pydantic models (Span, SessionMetrics, ToolMetrics, CostReport)
175
+ ├── collector.py # Trace ingestion (parse_glint_trace, parse_armand0e_trace, parse_vfable_trace)
176
+ ├── token_tracker.py # Token counting + cost estimation with real pricing
177
+ ├── error_tracker.py # Error detection, classification, recovery rate
178
+ ├── storage.py # ClickHouse + SQLite dual storage
179
+ ├── dashboard.py # FastAPI dashboard with Plotly charts
180
+ └── cli.py # Click CLI (analyze, dashboard, cost, errors, tokens)
181
+ ```
182
+
183
+ ## API Endpoints
184
+
185
+ | Endpoint | Description |
186
+ |---|---|
187
+ | `GET /dashboard` | Main dashboard with session list |
188
+ | `GET /sessions/{id}` | Session detail with metrics table |
189
+ | `GET /sessions/{id}/timeline` | Tool call timeline (Plotly bar chart) |
190
+ | `GET /sessions/{id}/heatmap` | Tool usage heatmap (Plotly) |
191
+ | `GET /cost/report` | Cost breakdown across all sessions |
192
+ | `GET /cost/report?session_id=X` | Cost breakdown for a specific session |
193
+ | `GET /errors/report` | Error report across all sessions |
194
+ | `GET /errors/report?session_id=X` | Error report for a specific session |
195
+
196
+ ## Python API
197
+
198
+ ```python
199
+ from agent_telemetry.collector import ingest_trace, calculate_metrics
200
+ from agent_telemetry.token_tracker import estimate_cost, count_tokens
201
+ from agent_telemetry.error_tracker import generate_error_report
202
+ from agent_telemetry.storage import TelemetryStorage
203
+
204
+ # Analyze a trace
205
+ spans = ingest_trace("trace.jsonl", fmt="vfable")
206
+ metrics = calculate_metrics(spans)
207
+
208
+ # Estimate cost
209
+ cost = estimate_cost(10000, 5000, model="claude-3.5-sonnet", cache_read=3000)
210
+ print(f"Total: ${cost.total_cost:.6f}")
211
+
212
+ # Store in database
213
+ storage = TelemetryStorage()
214
+ storage.store_spans(spans)
215
+ storage.store_session_metrics(metrics["session"])
216
+
217
+ # Generate error report
218
+ report = generate_error_report("session-123", spans=spans)
219
+ print(f"Errors: {report.total_errors}, Recovery rate: {report.recovery_rate:.0%}")
220
+
221
+ # Query spans
222
+ session_spans = storage.query_spans(session_id="session-123")
223
+ tool_spans = storage.query_spans(tool_name="Bash")
224
+ ```
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ # Install dev dependencies
230
+ pip install -e ".[dev]"
231
+
232
+ # Run tests
233
+ pytest tests/
234
+
235
+ # Run specific test file
236
+ pytest tests/test_token_tracker.py -v
237
+ ```
238
+
239
+ ## License
240
+
241
+ MIT
242
+
243
+ ## Ecosystem
244
+
245
+ Part of the [FableForge](../) ecosystem — 21 open-source projects built from 210K real agent traces:
246
+
247
+ | Project | Description |
248
+ | --- | --- |
249
+ | **[Anvil](../anvil)** | Self-verified coding agent |
250
+ | **[VerifyLoop](../verifyloop)** | Plan→Execute→Verify→Recover framework |
251
+ | **[ErrorRecovery](../error-recovery)** | Self-healing middleware (3,725 error patterns) |
252
+ | **[FableForge-14B](../fableforge-14b)** | The fine-tuned 14B model (4-stage training) |
253
+ | **[ShellWhisperer](../shell-whisperer)** | 1.5B edge agent (phone/RPi, 50ms) |
254
+ | **[ReasonCritic](../reason-critic)** | Verification model (130 benchmark tasks) |
255
+ | **[TraceCompiler](../trace-compiler)** | Compile traces → LoRA skills |
256
+ | **[AgentRuntime](../agent-runtime)** | Persistent agent daemon (systemd for AI) |
257
+ | **[AgentSwarm](../agent-swarm)** | Multi-agent from real trace transitions |
258
+ | **[AgentTelemetry](../agent-telemetry)** | Datadog for agents (token tracking, costs) |
259
+ | **[BenchAgent](../bench-agent)** | HumanEval for tool-use (107 tasks) |
260
+ | **[AgentDev](../agent-dev)** | VSCode extension with verification |
261
+ | **[TraceViz](../trace-viz)** | Trace replay visualizer (Next.js) |
262
+ | **[AgentSkills](../agent-skills)** | npm for agent behaviors |
263
+ | **[AgentCurriculum](../agent-curriculum)** | 5-stage progressive training |
264
+ | **[AgentFuzzer](../agent-fuzzer)** | Adversarial testing for agents |
265
+ | **[AgentConstitution](../agent-constitution)** | Safety guardrails from traces |
266
+ | **[CostOptimizer](../cost-optimizer)** | Token cost reduction (50-80%) |
267
+ | **[AgentProfiler](../agent-profiler)** | Behavioral fingerprinting |
268
+ | **[TrajectoryDistiller](../trajectory-distiller)** | Trace→training data pipeline |
269
+ | **[Fable5-Dataset](../fable5-dataset)** | HuggingFace dataset release |
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fableforge-agent-telemetry"
7
+ version = "0.1.0"
8
+ description = "Datadog for AI agents — real-time observability, token tracking, cost estimation, error rates"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "fastapi>=0.110.0",
12
+ "uvicorn[standard]>=0.27.0",
13
+ "clickhouse-driver>=0.2.6",
14
+ "sqlalchemy>=2.0.0",
15
+ "pydantic>=2.5.0",
16
+ "plotly>=5.18.0",
17
+ "click>=8.1.0",
18
+ "tiktoken>=0.6.0",
19
+ "rich>=13.7.0",
20
+ "jinja2>=3.1.0",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ dev = [
25
+ "pytest>=8.0.0",
26
+ "pytest-asyncio>=0.23.0",
27
+ "httpx>=0.27.0",
28
+ ]
29
+
30
+ [project.scripts]
31
+ agenttelemetry = "agent_telemetry.cli:cli"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """AgentTelemetry — Datadog for AI agents."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from agent_telemetry.models import Span, SessionMetrics, ToolMetrics, CostReport
6
+
7
+ __all__ = ["Span", "SessionMetrics", "ToolMetrics", "CostReport", "__version__"]
@@ -0,0 +1,212 @@
1
+ """CLI for AgentTelemetry — analyze traces, view costs, start dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import click
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from agent_telemetry.collector import (
14
+ auto_detect_format,
15
+ calculate_metrics,
16
+ ingest_trace,
17
+ )
18
+ from agent_telemetry.error_tracker import classify_error, generate_error_report
19
+ from agent_telemetry.models import Span
20
+ from agent_telemetry.storage import TelemetryStorage
21
+ from agent_telemetry.token_tracker import estimate_cost, format_cost_table
22
+
23
+ console = Console()
24
+
25
+
26
+ @click.group()
27
+ @click.version_option(version="0.1.0")
28
+ def cli() -> None:
29
+ """AgentTelemetry — Datadog for AI agents."""
30
+
31
+
32
+ @cli.command()
33
+ @click.argument("trace_file", type=click.Path(exists=True))
34
+ @click.option("--format", "fmt", type=click.Choice(["glint", "armand0e", "vfable", "auto"]), default="auto", help="Trace format")
35
+ @click.option("--store/--no-store", default=True, help="Store results in database")
36
+ def analyze(trace_file: str, fmt: str, store: bool) -> None:
37
+ """Analyze a trace file and display metrics."""
38
+ if fmt == "auto":
39
+ fmt = auto_detect_format(trace_file)
40
+ console.print(f"[dim]Detected format: {fmt}[/dim]")
41
+
42
+ spans = ingest_trace(trace_file, fmt=fmt)
43
+ if not spans:
44
+ console.print("[red]No spans found in trace file.[/red]")
45
+ sys.exit(1)
46
+
47
+ console.print(f"[green]Loaded {len(spans)} spans[/green]")
48
+
49
+ metrics_result = calculate_metrics(spans)
50
+ session = metrics_result["session"]
51
+ tools = metrics_result["tools"]
52
+
53
+ console.print(f"\n[bold]Session:[/bold] {session.session_id}")
54
+ console.print(f"[bold]Model:[/bold] {session.model}")
55
+ console.print(f"[bold]Duration:[/bold] {session.duration_seconds:.1f}s")
56
+
57
+ metrics_table = Table(title="Session Metrics", show_header=True)
58
+ metrics_table.add_column("Metric", style="cyan")
59
+ metrics_table.add_column("Value", justify="right")
60
+
61
+ metrics_table.add_row("Total Tokens", f"{session.total_tokens:,}")
62
+ metrics_table.add_row("Total Cost", f"${session.total_cost:.6f}")
63
+ metrics_table.add_row("Tool Calls", str(session.tool_calls))
64
+ metrics_table.add_row("Errors", str(session.error_count))
65
+ metrics_table.add_row("Avg Duration", f"{session.avg_tool_duration_ms:.0f}ms")
66
+ metrics_table.add_row("P50 Duration", f"{session.p50_duration_ms:.0f}ms")
67
+ metrics_table.add_row("P95 Duration", f"{session.p95_duration_ms:.0f}ms")
68
+ metrics_table.add_row("P99 Duration", f"{session.p99_duration_ms:.0f}ms")
69
+ metrics_table.add_row("Cache Hit Rate", f"{session.cache_hit_rate:.1%}")
70
+ console.print(metrics_table)
71
+
72
+ tool_table = Table(title="Tool Metrics", show_header=True)
73
+ tool_table.add_column("Tool", style="cyan")
74
+ tool_table.add_column("Calls", justify="right")
75
+ tool_table.add_column("Avg ms", justify="right")
76
+ tool_table.add_column("P95 ms", justify="right")
77
+ tool_table.add_column("Error Rate", justify="right")
78
+ tool_table.add_column("Cost", justify="right", style="green")
79
+
80
+ for name, tm in sorted(tools.items()):
81
+ tool_table.add_row(
82
+ name,
83
+ str(tm.call_count),
84
+ f"{tm.avg_duration_ms:.0f}",
85
+ f"{tm.p95_duration_ms:.0f}",
86
+ f"{tm.error_rate:.1%}",
87
+ f"${tm.total_cost_usd:.6f}",
88
+ )
89
+ console.print(tool_table)
90
+
91
+ if store:
92
+ storage = TelemetryStorage()
93
+ storage.store_spans(spans)
94
+ storage.store_session_metrics(session)
95
+ console.print(f"\n[dim]Stored {len(spans)} spans in database[/dim]")
96
+
97
+
98
+ @cli.command()
99
+ @click.option("--host", default="127.0.0.1", help="Host to bind to")
100
+ @click.option("--port", default=8088, type=int, help="Port to bind to")
101
+ def dashboard(host: str, port: int) -> None:
102
+ """Start the interactive dashboard server."""
103
+ import uvicorn
104
+ from agent_telemetry.dashboard import app
105
+
106
+ console.print(f"[green]Starting AgentTelemetry dashboard on http://{host}:{port}[/green]")
107
+ console.print("[dim]Press Ctrl+C to stop[/dim]")
108
+ uvicorn.run(app, host=host, port=port)
109
+
110
+
111
+ @cli.command()
112
+ @click.argument("trace_file", type=click.Path(exists=True))
113
+ @click.option("--format", "fmt", type=click.Choice(["glint", "armand0e", "vfable", "auto"]), default="auto", help="Trace format")
114
+ def cost(trace_file: str, fmt: str) -> None:
115
+ """Show cost breakdown for a trace file."""
116
+ if fmt == "auto":
117
+ fmt = auto_detect_format(trace_file)
118
+ console.print(f"[dim]Detected format: {fmt}[/dim]")
119
+
120
+ spans = ingest_trace(trace_file, fmt=fmt)
121
+ if not spans:
122
+ console.print("[red]No spans found in trace file.[/red]")
123
+ sys.exit(1)
124
+
125
+ models: dict[str, list[Span]] = {}
126
+ for s in spans:
127
+ models.setdefault(s.model, []).append(s)
128
+
129
+ breakdowns = []
130
+ for model, model_spans in sorted(models.items()):
131
+ bd = estimate_cost(
132
+ sum(s.input_tokens for s in model_spans),
133
+ sum(s.output_tokens for s in model_spans),
134
+ model,
135
+ sum(s.cache_read for s in model_spans),
136
+ sum(s.cache_creation for s in model_spans),
137
+ )
138
+ breakdowns.append(bd)
139
+
140
+ console.print(format_cost_table(breakdowns))
141
+
142
+ total = sum(b.total_cost for b in breakdowns)
143
+ console.print(f"\n[bold green]Grand Total: ${total:.6f}[/bold green]")
144
+
145
+
146
+ @cli.command()
147
+ @click.argument("trace_file", type=click.Path(exists=True))
148
+ @click.option("--format", "fmt", type=click.Choice(["glint", "armand0e", "vfable", "auto"]), default="auto", help="Trace format")
149
+ def errors(trace_file: str, fmt: str) -> None:
150
+ """Show error report for a trace file."""
151
+ if fmt == "auto":
152
+ fmt = auto_detect_format(trace_file)
153
+ console.print(f"[dim]Detected format: {fmt}[/dim]")
154
+
155
+ spans = ingest_trace(trace_file, fmt=fmt)
156
+ if not spans:
157
+ console.print("[red]No spans found in trace file.[/red]")
158
+ sys.exit(1)
159
+
160
+ session_id = spans[0].session_id
161
+ report = generate_error_report(session_id, spans=spans)
162
+
163
+ console.print(f"\n[bold]Error Report: {session_id}[/bold]")
164
+ console.print(f"Total Errors: {report.total_errors}")
165
+ console.print(f"Recovered: {report.recovered_errors}")
166
+ console.print(f"Recovery Rate: {report.recovery_rate:.0%}")
167
+
168
+ if report.errors_by_type:
169
+ type_table = Table(title="Errors by Type", show_header=True)
170
+ type_table.add_column("Error Type", style="red")
171
+ type_table.add_column("Count", justify="right")
172
+
173
+ for etype, count in sorted(report.errors_by_type.items(), key=lambda x: -x[1]):
174
+ type_table.add_row(etype, str(count))
175
+ console.print(type_table)
176
+
177
+ if report.errors:
178
+ error_table = Table(title="Error Details", show_header=True)
179
+ error_table.add_column("Span ID", style="dim")
180
+ error_table.add_column("Type", style="red")
181
+ error_table.add_column("Tool", style="cyan")
182
+ error_table.add_column("Message", max_width=60)
183
+ error_table.add_column("Recovered")
184
+
185
+ for e in report.errors[:50]:
186
+ recovered = "[green]✓[/green]" if e.recovered else "[red]✗[/red]"
187
+ error_table.add_row(
188
+ e.span_id[:12] + "...",
189
+ e.error_type,
190
+ e.tool_name,
191
+ e.error_message[:60],
192
+ recovered,
193
+ )
194
+ console.print(error_table)
195
+
196
+
197
+ @cli.command()
198
+ @click.argument("text")
199
+ @click.option("--model", default="gpt-4", help="Model name for token counting")
200
+ def tokens(text: str, model: str) -> None:
201
+ """Count tokens in a text string."""
202
+ from agent_telemetry.token_tracker import count_tokens
203
+
204
+ n = count_tokens(text, model)
205
+ console.print(f"[bold]{n:,}[/bold] tokens ({model})")
206
+
207
+ bd = estimate_cost(n, 0, model)
208
+ console.print(f"Input cost (no output): ${bd.input_cost:.6f}")
209
+
210
+
211
+ if __name__ == "__main__":
212
+ cli()