Skip to main content

redundo

PyPI CI License: MIT

Point it at your AI agent's OTLP traces. Get a report on what's actually wasted: repeated work, not a guess.

  • Runs on your machine. No account, no SaaS. Point it at OTLP traces you already have, or capture them with the bundled collector.
  • Every number is checkable. Each bucket links back to a real, hand-verifiable case.

Quickstart

Already have a directory of OTLP JSON traces (your framework wrote them directly, or you captured them some other way)? Point redundo at it:

pip install redundo
redundo adapt ./otlp_traces --summary | redundo analyze --format html > report.html

Each source has a genuinely different OTLP shape, and redundo adapt tells them apart from the data itself. See detect.py's module docstring for the exact rules, or force one with --source.

Each half also runs on its own:

redundo adapt ./otlp_traces -o trace.jsonl       # just convert
redundo analyze trace.jsonl --format json         # just analyze a file
redundo analyze trace.jsonl                       # reads stdin if the path is omitted or "-"

Capturing OTLP traces

Don't have traces yet? Most frameworks only export telemetry over the network, so you need somewhere local to catch it first:

pip install "redundo[collector]"
redundo collect --out-dir ./otlp_traces &
# enable your framework's OTLP export (see below), run it, then adapt/analyze as above

Setup for your framework, most need the collector above running first; OpenClaw's own plugin below is the one exception:

OpenClaw

Recommended: the openclaw-localtrace plugin. OpenClaw's own built-in exporter (@openclaw/diagnostics-otel) strips every session and write signal this analysis relies on most; the plugin exists specifically to keep them, writing straight to a local directory instead of a network endpoint. No redundo collect step for this source.

openclaw plugins install clawhub:@cogentwizards/openclaw-localtrace
openclaw plugins enable openclaw-localtrace
openclaw config set plugins.entries.openclaw-localtrace.config.enabled true
openclaw config set plugins.entries.openclaw-localtrace.config.captureIdentifiers true   # opt-in; off by default
openclaw config set plugins.entries.openclaw-localtrace.config.captureContent true       # opt-in; off by default
openclaw config set plugins.entries.openclaw-localtrace.hooks.allowConversationAccess true
openclaw gateway restart

# drive real turns through the Gateway, then:
redundo adapt "$(openclaw config get plugins.entries.openclaw-localtrace.config.outputDir)" \
  --summary | redundo analyze --format html > report.html

captureIdentifiers and hooks.allowConversationAccess unlock a real conversation-scoped task_id and a real per-call write signal. Read the plugin's own README before turning them on. Without the plugin at all, docs/openclaw.md covers the built-in exporter instead, at the cost of that missing signal.

Hermes

Needs the community hermes-otel plugin installed first:

hermes plugins install briancaffey/hermes-otel/hermes_otel
# import into the SAME venv that runs `hermes`. Check `hermes --version`'s
# "Install directory" for the real path if this doesn't match yours:
/path/to/hermes-agent/venv/bin/pip install -r ~/.hermes/plugins/hermes_otel/requirements.txt
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318/v1/traces"   # note the /v1/traces suffix

# restart Hermes, drive real turns through it, then:
redundo adapt ./otlp_traces --source openinference --summary | redundo analyze --format html > report.html

Full content and a real conversation-scoped task_id both work out of the box, no permission opt-in needed. See docs/openinference.md.

OpenAI Agents SDK

Instrument it with the community openinference-instrumentation-openai-agents package, pointed at a redundo collect receiver:

pip install openinference-instrumentation-openai-agents opentelemetry-exporter-otlp-proto-http
from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")))
OpenAIAgentsInstrumentor().instrument(tracer_provider=provider)

# run your agent(s), then:
redundo adapt ./otlp_traces --source openinference --summary | redundo analyze --format html > report.html

In-trace handoffs work out of the box. Cross-trace correlation (the SDK's own group_id) is dropped by this translator today, an upstream gap, not something this adapter can fix. See docs/openinference.md's per-source table.

Google ADK

Instrument it with the community openinference-instrumentation-google-adk package, pointed at a redundo collect receiver:

pip install openinference-instrumentation-google-adk opentelemetry-exporter-otlp-proto-http
from openinference.instrumentation.google_adk import GoogleADKInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")))
GoogleADKInstrumentor().instrument(tracer_provider=provider)

# run your agent(s), then:
redundo adapt ./otlp_traces --source openinference --summary | redundo analyze --format html > report.html

ADK's own session id maps onto the standard session.id attribute, so a real conversation-scoped task_id works out of the box. Subagent delegation via AgentTool reuses the parent's own session id, so delegated work lands in the same task automatically, no extra link needed. See docs/openinference.md's per-source table.

Claude Code (CLI)
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
export OTEL_TRACES_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=none   # not consumed by this adapter
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_TRACES_EXPORT_INTERVAL=1000   # the default 5s can lose a short -p session to an early exit
export OTEL_LOGS_EXPORT_INTERVAL=1000
export OTEL_LOG_USER_PROMPTS=1      # first-of-turn llm_call content
export OTEL_LOG_TOOL_DETAILS=1      # built-in tool arguments + MCP tool_input
export OTEL_LOG_TOOL_CONTENT=1      # tool.output content (needs tracing on)

# run your claude session(s), then:
redundo adapt ./otlp_traces --source claude-code --summary | redundo analyze --format html > report.html

MCP tool call arguments only ever appear on the logs signal; tool output content only ever appears in a span event gated by OTEL_LOG_TOOL_CONTENT=1. Full detail in docs/claude-code.md.

Claude Agent SDK

Same env vars as the CLI above. The SDK launches the claude binary as a subprocess, which inherits its parent's environment, so set these in whatever process calls query(), shell export before running your script, or os.environ/process.env before the SDK import, rather than anywhere inside the SDK's own options.

python your_agent_script.py   # anything that calls claude_agent_sdk.query()
redundo adapt ./otlp_traces --source claude-code --summary | redundo analyze --format html > report.html

Same --source claude-code as the CLI; the SDK is detected as the same source. One thing genuinely differs: the SDK always launches the CLI in streaming mode, which never emits the span the CLI normally uses to attach a turn's prompt text. This adapter recovers that content automatically via a time-window correlation against the logs signal. See "Agent SDK content recovery" in docs/claude-code.md.

See it work

redundo analyze examples/demo_trace.jsonl
Coverage: 16/25 events priced (64%). $0.1060 of tracked spend is what this analysis actually covers.
  Cost basis: $0.1060 (100%) reported directly by the source.

Candidate redundant-repeat pairs: 8
Verdicts reached: 6/8 (75%) (exact repeats judged; 2 unclassified for missing signal)

Fix these first:
  1. $0.0200 for task=A step=1 (llm_call/gpt-5.6): result identical; no intervening write; task terminated in failure

Exact repeats:
3 confirmed_waste: repeated call, unchanged result, no intervening write, task failed
  Your agent repeated itself, learned nothing new, and still failed: these are the places it was stuck, not working.
  at 1,000 calls/day: ~$52.50/mo projected
3 likely_legitimate: result changed, or a write intervened
2 unclassified: a required signal was missing from the trace, no verdict, on purpose

Similar or related:
0 near_duplicate / 0 cross_task_redundancy / 0 recurring_pattern

task=A is a short label, not a rename: the report prints a legend mapping it back to the real task id once, right after the coverage block, so a reader doesn't scroll past the same UUID a dozen times. Every count above traces back to a real, checkable case in examples/demo_trace.jsonl. Full report: examples/demo_report.html. Five live, narrated demo apps (Claude Agent SDK, OpenClaw, Hermes, OpenAI Agents SDK, Google ADK): examples/demo-apps/.

How it works

redundo is two programs joined by a plain schema, installed as one package.

adapt turns a framework's OTLP telemetry into one common event schema (docs/schema.md). analyze classifies repeated calls into one of six buckets:

Bucket Fires when
confirmed_waste identical call, identical result, no write in between, task failed
likely_legitimate the result changed, or a write intervened
unclassified a required signal was missing from the trace
near_duplicate similar, not identical, surfaced for review, not scored
cross_task_redundancy same call, different task, a confirmed link between them
recurring_pattern same call, different task, no confirmed link

Full reasoning for each: docs/schema.md, or classify.py's own module docstring.

Not a tracing platform

Langfuse, Phoenix, and other observability platforms show you traces: spans, timings, a UI to browse them. That's real, valuable, and a different job. redundo doesn't show you traces, it adjudicates them. For every repeated call it finds, it issues one of the six verdicts above under a stated evidence rule, never a vague severity score. When the trace doesn't carry enough signal to decide, it says so directly: unclassified is a verdict too, not a fallback dressed up as an answer. Nobody else ships that abstention. The two aren't competitors: point one of those platforms' own OTLP export at redundo instead of choosing between them. One shows you what happened; the other tells you which of it was wasted.

Modular by design

redundo is three small programs that plug together, not one monolith:

  • collect is a local OTLP receiver. It writes whatever telemetry it's sent to disk, nothing more.
  • adapt turns one framework's raw telemetry into the common event schema.
  • analyze classifies repeated calls from that schema into the six buckets above.

Each stage only needs the one before it to speak the schema in between, so any stage can be swapped for your own. Bring your own event source by writing an adapt plugin, or add a new classification by writing an analyze plugin, no fork or PR against this repo required. See docs/plugins.md.

Supported sources

Source Docs
Hermes, and anything OpenInference-instrumented docs/openinference.md
Claude Code (CLI, IDE extensions, Agent SDK) docs/claude-code.md
Claude Cowork docs/cowork.md
OpenClaw docs/openclaw.md, or the openclaw-localtrace plugin (docs) for a real write signal

Not listed? Pipe your own NDJSON matching the schema straight into analyze. It doesn't know or care where its input came from. Or write an adapter plugin, no PR against this repo required: docs/plugins.md.

Pricing data

When a source doesn't report cost_usd directly, redundo estimates it from a bundled, per-model pricing snapshot. Refresh it any time:

redundo update-pricing

Full detail: docs/pricing.md.

Docs

Development

uv sync
uv run pytest

Contributions welcome. See CONTRIBUTING.md.

License

MIT. See LICENSE.

Release files for redundo 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for redundo 0.5.0
File Size Uploaded
redundo-0.5.0.tar.gz 394.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for redundo 0.5.0
File Interpreter ABI Platform
redundo-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 559.9 kB

Release files / redundo-0.5.0.tar.gz

Download URL redundo-0.5.0.tar.gz
Size 394.5 kB
Tags Source
SHA-256 checksum
How to use checksums
cbbfdc4248bcc3757b30a5d2f1069490a2d176a9e66a7b3efdd072ac81d1dd8c
BLAKE2b-256 checksum
How to use checksums
66897a676010016bdab243ed09cf8fc8ea9ac7a889290df40ea695fe53582544
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / redundo-0.5.0-py3-none-any.whl

Download URL redundo-0.5.0-py3-none-any.whl
Size 165.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
960684148a6e6162ed79bf3ca95f56bd87bffb7dc0a8d12c6fb9c68061aaf7ad
BLAKE2b-256 checksum
How to use checksums
405a5c90c567ff7d78189b2817c3fcf32759628089cd3bbf1a7312015a008234
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page