Observability layer release. Pure data API — visualization moved to the React frontend in frontend/. Backend ships no HTML.
New endpoint
GET /stats — typed JSON snapshot of in-memory metrics
The React dashboard fetches this directly. Schema is pinned by Pydantic (StatsResponse):
{
"since": "2026-06-07T14:59:56+00:00",
"uptime_seconds": 253.1,
"total_requests": 9,
"intents": {
"<intent_name>": {
"count": <int>,
"share": <float>,
"confidence": {
"min": <float>, "max": <float>, "mean": <float>,
"p50": <float>, "p95": <float>, "count": <int>
} | null
}
},
"fallbacks": {
"total": <int>,
"rate": <float>,
"by_attempted_intent": { "<intent>": <int>, ... }
},
"latency_ms": {
"<path_taken>": { "p50": <float>, "p95": <float>, "mean": <float>, "count": <int> }
},
"errors": { "429": <int>, "500": <int>, "503": <int> },
"ephemeral": true,
"note": "..."
}Live first taste (9 requests right after v0.2.4 deploy):
intents count conf p50 conf p95
chitchat 3 0.48 0.54
simple_qa 2 0.86 0.87
complex_task 2 0.94 0.94
document_qa 2 0.88 0.88
latency_ms p50 p95 count
simple_qa:direct_llm 803 ms 812 ms 2
document_qa:rag_stub 40 ms 43 ms 2
complex_task:orchestrator 16,969 ms 24,685 ms 2
low_confidence_fallback 31 ms 31 ms 1
chitchat:direct_llm 81,462 ms — (averaged with cold-start)
fallbacks total 1 rate 11.1% by_attempted={"chitchat": 1}
errors {}
The chitchat row exactly reproduces what router/results/generalization_test.txt captured — confidence saturates between 0.39 and 0.55. The per-class threshold of 0.45 lets the legitimate cases through (mean 0.47) while still catching the truly ambiguous "Tell me about the requirements" (0.385) and attributing it to chitchat in the fallback breakdown.
What's instrumented
| Signal | Where |
|---|---|
| Volume + share per intent | dispatcher records intent per request |
| Confidence distribution per intent | bounded deque per intent, p50/p95/mean/min/max |
Latency per path_taken |
time.perf_counter around the whole dispatch |
| Fallback total + rate + by attempted intent | the calibration signal for per-class thresholds |
| HTTP error counters | 429 / 500 / 503 via the exception handlers |
| Structured JSON log line per request | stdlib JSONFormatter; secrets/inputs filtered |
Honest about ephemerality
Metrics live in process memory. They reset on every Cloud Run cold start or revision change. Two layers of disclosure:
ephemeral: true(stable programmatic flag the React frontend can key off)note(prose explanation for humans / log readers)
A TODO in metrics_collector.py marks where a to_prometheus() exporter would go for persistence. The collector's internal shape (per-intent / per-path / per-status indexed counters) already matches the natural Prometheus label dimensions; adding the exporter later is mechanical.
Removed
GET /dashboardand its inline HTML (~200 lines, Chart.js via CDN) — first cut shipped this for visualization, second cut moves it to the React app. Backend goes back to data-only.HTMLResponseimport.
A regression test test_dashboard_endpoint_is_removed asserts the /dashboard route stays 404, so a future re-introduction of inline HTML breaks loudly.
CORS
The existing allowlist (CORS_ALLOW_ORIGINS env var on Cloud Run) already covers the Vercel frontend origins + http://localhost:5173. The middleware applies globally, so /stats is automatically reachable from the React dashboard. Verified with a real preflight from the Vercel origin.
Tests
124 tests (was 98 in v0.2.3):
- 18 new
test_metrics_collector.pycases — counters, percentiles, fallback attribution by attempted intent, bounded memory via deque maxlen, error-counter wiring, full reset, thread safety (20 threads × 100 records ⇒ exactly 2000). - 7 new
/statscases — empty snapshot, real /route reflected, fallback attribution end-to-end, 503/500 counters via handlers, ephemeral bool flag, CORS preflight for the frontend origin, dashboard-404 regression guard.
All pass without GPU or tokens; the conftest.py autouse fixture resets the singleton between tests to prevent cross-test leakage.
Cloud Run runtime config (unchanged)
The deploy uses the same env vars as v0.2.3 plus the new /stats endpoint:
CONFIDENCE_THRESHOLDS (defaults; per-class)
MAX_CONCURRENT_EXECUTORS=5
ENABLE_API_DOCS=false
MAX_BODY_BYTES=10000
CORS_ALLOW_ORIGINS (Vercel + localhost)
Live: https://agent-router-909428365094.us-central1.run.app/stats
What's next
- React dashboard view in
frontend/consuming/stats(next feature). - Optional
to_prometheus()export when there's a Prometheus instance to scrape — collector shape is already aligned.