Skip to main content

redundo

PyPI CI License: MIT

Point it at your AI agent's OTLP traces (or Pi's and OpenCode's own session stores). 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.

Pi

Nothing to capture. Pi emits no OTLP, but it already keeps every conversation as a session file with real per-call cost, and redundo adapt reads those directly. Point it at one session file, or one project's session directory (named after the working directory):

redundo adapt ~/.pi/agent/sessions/--Users-me-src-myproject--/ --summary | redundo analyze --format html > report.html

Forked and cloned sessions are linked back to the session they came from, without counting the copied history twice. See docs/pi.md.

OpenCode

Nothing to capture either. OpenCode stores every session locally, with real per-step cost, and redundo adapt reads that directly: one exported session, or the whole database (opened read-only).

opencode export ses_XXXXXXXX > session.json
redundo adapt session.json --summary | redundo analyze --format html > report.html

# or every stored session:
redundo adapt "$(opencode db path)" --summary | redundo analyze --format html > report.html

Subagent sessions are linked to the session that spawned them. OpenCode 1.x only for now. See docs/opencode.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. Seven live, narrated demo apps (Claude Agent SDK, OpenClaw, Hermes, OpenAI Agents SDK, Google ADK, Pi, OpenCode): 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
Pi coding agent (its own session files, no OTLP needed) docs/pi.md
OpenCode 1.x (its own session database or exports, no OTLP needed) docs/opencode.md

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.

Metadata

Release files for redundo 0.6.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.6.0
File Size Uploaded Provenance Actions
redundo-0.6.0.tar.gz 438.3 kB With provenance Details for redundo-0.6.0.tar.gz

Built distribution (wheel)

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

Total release size: 624.6 kB

Release files / redundo-0.6.0.tar.gz

Download URL redundo-0.6.0.tar.gz
Size 438.3 kB
Tags Source
SHA-256 checksum
How to use checksums
dc950474f5f846c651a2b73de23589f06ff4237ae9b73a5eacfebfdde1804f1f
BLAKE2b-256 checksum
How to use checksums
e0ff4988e1bbb7f173029b85a7c656d4cf7eda55435e3f55d36bd5cf2c660b83
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 2, 2026.

Transparency log

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

Download URL redundo-0.6.0-py3-none-any.whl
Size 186.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2a3f1f378f371d40fa72da5c69ad716fabecf0ae5482ae8829ed636ccfa557a7
BLAKE2b-256 checksum
How to use checksums
d595892ef704d0226e5bb44caeff23f4919926680ff2f5108ec3b56ee7dfeb2c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 2, 2026.

Transparency log

Security for redundo 0.6.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for redundo-0.6.0.tar.gz

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 Nov 2, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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