Agent-centric, local-first observability. Wrap your agent; get a trace tree of every step, tool call, and loop with token + cost usage.
import agenticmeter as am
sink = am.configure() # scrub redaction on, auto-patches OpenAI/Anthropic/LangChain
@am.meter # wrap the run
def agent(task):
with am.span("plan", am.SpanType.LLM):
...
return do_work(task)
agent("plan my launch week")
# inspect the trace tree
root, children = sink.tree(sink.spans[0].trace_id)context.py— contextvars spine (current run/span) +bind_workerfor threadstracer.py— start/end spans, build tree, fail-open, route to sinkspan.py— OTel-shaped Span + SpanTyperedact.py— scrub secrets + truncate (on by default)cost.py— per-model token→$ price map (override witham.cost.set_prices)decorators.py—@meter,with meter.span(...),@meter.toolinstrument/openai.py— sync + async + streaming, enrich-not-duplicateinstrument/anthropic.py— sync + asyncinstrument/langchain.py— callback handler, run_id tree, global registersinks/base.py— Sink interface + MemorySink (+ tree builder)
LLM call usage is captured by SDK patching. When a framework (LangChain) already
opened an LLM span, the SDK patch enriches that span with exact token usage
instead of emitting a duplicate — driven by context.in_framework_llm.
- SQLite sink + background async flush queue (
flush.py) agenticmeter uilocal viewer- loop / cycle detection (
analysis/loops.py) - cloud exporter
python3 smoke_test.pyanalysis/insights.py — six pure detectors over the span tree, each returning
Insights with evidence (span ids) + an action. Assert-only-what-you-can-prove:
| code | tier | fires when |
|---|---|---|
| silent_tool_failures | fact | a tool errored but the run returned ok |
| cost_concentration | fact | one step >= 60% of run cost (>= 3 priced steps) |
| repeated_llm_calls | fact | identical prompt billed >= 2x |
| context_growth | fact | peak prompt >= 3x first (and >= 2k extra tokens) |
| repeated_tool_calls | pattern | same tool + same args >= 3x (the provable "loop") |
| time_concentration | fact | >= 70% of step time in retrieval/tool/custom |
from agenticmeter.analysis.insights import analyze, format_insights
print(format_insights(analyze(spans))) # spans = one trace's span listRun the demo / tests:
python3 behavior_demo.py # messy run -> all six warnings
python3 behavior_test.py # messy fires all six, clean fires noneLevel 0 — every @meter run prints its behavior summary when it closes (warnings
only by default, so clean runs stay silent):
import agenticmeter as am
am.configure(sink="sqlite", summary="auto") # "auto" | True | False
@am.meter
def agent(task): ...
agent("...") # -> prints ▸ run header + any ⚠ warnings to stderrLevel 1 — runs persist to ~/.agenticmeter/traces.db; replay them from the terminal:
agenticmeter runs # list recent runs (or: python -m agenticmeter runs)
agenticmeter show # latest run: trace tree + warnings
agenticmeter show <id> # a specific run (prefix ok)Try it: python3 level01_demo.py, then python3 -m agenticmeter --db /tmp/agenticmeter_demo.db show.
pip install -e . # registers the `agenticmeter` commandThis also avoids the import-shadowing trap: run from anywhere, edits picked up live.
agenticmeter ui # opens http://127.0.0.1:4319 in your browser
agenticmeter ui --port 8080 --no-openSingle page: live-polling run list (left), selected run's behavior findings + trace tree (right). Click any ⚠ finding and it highlights + scrolls to the offending steps in the trace — diagnosis you can verify in one click. Stdlib only, no deps.