Skip to main content

OpenAIDR

PyPI Python Licence CI

Session collection for AI coding agents.

OpenAIDR reads the session state AI coding agents already write to disk and normalises it into one session model with stable span identity.

Status: beta, on PyPI. Four agent kinds read today (claude-code, devin, codex, cursor). Start with Install and run, then What OpenAIDR reads, and what it emits if you want to know what leaves your machine before you run it. (Nothing does.)

Today it reads four kinds: Claude Code, Devin, Codex and Cursor (devin, which covers Devin CLI, Devin Desktop's local agent and editors driving it over ACP: one agent, one session store; codex, which covers Codex CLI, codex exec, and the IDE extension and Codex app through codex app-server, from docs/specs/codex.md; cursor, which covers the Cursor CLI in the terminal, in print mode and over ACP, and the desktop app's agent panel, from docs/specs/cursor.md). The parsing dependency supports more formats than this package instantiates, which is a distinction worth holding onto; what each additional kind would cost is measured in Agent kind coverage.

It answers what an agent did, never what that means. No scoring, no identity resolution, no findings, no upload path. Interpreting a session is a consumer's job; OpenAIDR takes no position on what a consumer concludes.

What OpenAIDR reads, and what it emits

OpenAIDR reads files the agent already wrote — Claude Code's JSONL transcripts and its per-server MCP connection logs — opened read-only. No process hooks, no proxy, no injected agent, no listener, and nothing is written back.

There is no upload path at all. OpenAIDR reads, normalises and returns a model. It opens no network connection and ships nothing anywhere. What a consumer does with the model is the consumer's decision and the consumer's boundary to state.

Transcripts contain a person's own words, the arguments they passed to tools, and the paths they work in. The session model separates that from the structural account of what happened, and the separation is enforced in code, not by convention — LOCAL_ONLY names every field that serves correlation and local rendering only.

Neither CLI surface prints the conversation:

Text (--detail) --format json
Session id, agent kind, start, turn count shown emitted
Tool name, MCP server, status, result size, duration, truncation shown emitted
Span identity — emitted
Model, source, working directory — emitted
MCP transport, endpoint, advertised name/version, failure category aggregate counts emitted per connection
Turn text, initial prompt, context items never never
Tool arguments, tool results never never
Machine, user never never
MCP failure detail (the server's own words) never never
Tool error text shown withheld

--format json is the shape most likely to be redirected to a file, piped into another program, or pasted somewhere, so it is the narrower of the two: it carries the structural account and drops even the error text the local text surface shows. There are tests asserting that a prompt and an sk--shaped token present in a transcript reach neither surface.

One honest caveat: the boundary described above is the boundary of the CLI. The in-memory Session model does carry the LOCAL_ONLY fields, because a consumer embedding OpenAIDR as a library may legitimately need them on the machine that produced them. If you embed it, Session.public_descriptor() is the deliberately narrow shape intended to travel outward, and drawing your own boundary is your call to state.

What OpenAIDR adds on top of adr-sensor

Session parsing itself comes from Uber's ADR project (adr-sensor, Apache-2.0) — it owns the on-disk formats, and it is OpenAIDR's only runtime dependency. It is confined behind an adapter: upstream types convert at the reader boundary and never appear elsewhere. Any one agent kind can instead be backed by an OpenAIDR-owned reader on the same contract — per kind, not all-or-nothing.

What the adapter adds is not a thin wrapper. Upstream's unified event model was measured against this one rather than assumed (the full table):

adr-sensor supplies OpenAIDR adds
Discovery A whole-tree walk keyed on the sessionId field A per-file walk. Every record in a subagent transcript carries the parent's sessionId, so upstream's own walk collapses a parent and its subagents into one identity. OpenAIDR keeps the file path, which is the only thing distinguishing them.
Span identity No call identifier below the session An identity for one tool call that survives re-reads of a growing session, derived only from what cannot change as it grows
Outcomes success or unknown A six-status model — ok, error, rejected, pending, interrupted, unknown — with five emitted from Claude Code's own records
Timing No timestamp below the session Per-call wall clock, from the transcript's record of both ends of a call
MCP Nothing Transport, the server's advertised identity, connection outcome and per-call outcome, read from the per-server connection logs — the only place any of it is written down
Tool names The raw name A split into (server, tool) per kind, never one global pattern: applied to the wrong kind a general pattern does not fail to find a server, it finds the wrong one
Agent kinds An on-disk self-name A mapping to an agent kind, where an unmapped kind is reported as unmapped rather than guessed
Failures A per-file error printed to stdout and swallowed That narration captured and turned into a reported ReaderFailure, so an unreadable file is a stated gap rather than a silent absence
Privacy boundary None LOCAL_ONLY, above

The pin is exact (adr-sensor==1.0.0, not >=): the adapter reads upstream internals as well as its public entry point, and a test asserts the exact error string upstream prints, so a version bump is a deliberate re-check rather than silent drift.

Install and run

uv tool install openaidr

That installs an openaidr command:

$ openaidr --version
openaidr 0.10.2

openaidr sessions prints what each agent on this machine actually did — sessions, turns, tool calls, and how each one ended. Per-call timings come from the transcript's own record of both ends of a call, not from the parsing dependency's event schema, which carries no timestamp below the session.

For Claude Code it also reads the per-server MCP connection logs, which is the only place the transport, the server's advertised identity, and the outcome of an MCP call are written down. That enrichment is best-effort — the path is an undocumented cache — and every way it can fall short is reported rather than passed off as an absence.

$ uv run openaidr sessions --since 2d --detail
claude-code:96a1d0d4-d106-4cd1-9031-1e174593f8d0  [claude-code]  2026-08-29T05:11:38+00:00  86 turns
                   142c  Read
                    79c  Bash
    rejected        41c  github/create_issue

--format json emits the same account machine-readably, --agent-kind selects one kind, and --since bounds the window. Nothing is resolved to a component and nothing is judged: what a consumer concludes is the consumer's concern.

Design

docs/specs/session-collection.md — the session model, span identity, per-agent-kind normalisation, and the collection interface. Decisions in docs/adrs/, and a doc index in docs/.

Status

Beta. Session collection works for claude-code, devin, codex and cursor: sessions, turns, tool calls and span identity. Claude Code is read through adr-sensor, one file at a time. Devin is read from its SQLite store and run logs, and Codex from its rollouts and log database; both of those readers are OpenAIDR-owned. The model has six outcome statuses. Claude Code emits five of them (unknown, rejected, pending, ok, error) from its transcripts and, for MCP calls, its per-server connection logs. interrupted is not emitted for Claude Code or Devin: neither Claude Code source records a stop, and Devin's result reasons do not include one. Codex emits it, from a completion item's own status and from output Codex marks aborted.

Cursor is read from the CLI's chat stores, with per-response tokens, the entrypoint and MCP connection failures from its debug log (docs/specs/cursor.md). Its statuses come from Cursor's typed outcome, interrupted included. The desktop app's conversations are read from its state.vscdb with the same decoder, with usage from its structured log where its agent loop runs on the machine.

For long-lived consumers, IncrementalCollector takes an agent kind and one changed path. Claude Code and Codex cold-read that file once, then project only newly appended, newline-terminated records, and return what that file holds now. Each keeps those projections in an LRU bounded to 128 files and a 16 MiB source-byte budget, except that the transcript just read is never evicted to make room — so one active transcript larger than the byte budget on its own still stays cached rather than losing its incremental read (ADR-0029, which supersedes ADR-0011). Devin keeps every session in one store, so a change under its data root rebuilds them and returns the sessions whose output differs (ADR-0018). IncrementalCollector.locations() names the directories that hold each kind's session files, sub-agents included, so a consumer can watch every kind without knowing its layout (ADR-0023). The cursors stay in memory. A process restart reads cold again.

Why the Claude Code reader walks the tree and calls adr-sensor's parser per file, rather than going through upstream's own whole-tree walk, is recorded in the spec.

Contributing

Reading an agent's session log and normalising it is plumbing, and every new agent format is coverage a contributor can add without touching anything that interprets the result. That is the contribution this project is shaped around, and CONTRIBUTING.md walks the reader contract end to end — the four wiring points, then the traps that cost us an ADR each.

Security

For a vulnerability in OpenAIDR's own code, see SECURITY.md — report privately via a GitHub security advisory rather than a public issue.

Licence

Apache-2.0. See LICENSE.

OpenAIDR depends on adr-sensor from Uber's ADR project, Copyright Uber Technologies, Inc., also Apache-2.0, consumed unmodified from PyPI with no source vendored here. Attribution is in NOTICE, which ships inside both the wheel and the sdist.

Metadata

Release files for openaidr 0.10.2

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

Source distribution (sdist)

Source distribution for openaidr 0.10.2
File Size Uploaded Provenance Actions
openaidr-0.10.2.tar.gz 461.9 kB With provenance Details for openaidr-0.10.2.tar.gz

Built distribution (wheel)

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

Total release size: 620.6 kB

Release files / openaidr-0.10.2.tar.gz

Download URL openaidr-0.10.2.tar.gz
Size 461.9 kB
Tags Source
SHA-256 checksum
How to use checksums
4e481ca0846d7eeec2e7f473408846b8516d0232d4fb3fefb66490b1071c0a2d
BLAKE2b-256 checksum
How to use checksums
b0a57975991731e2e62d40a58558d2286d7731e98feca4d6b06100da20d3a058
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 1, 2026.

Transparency log

Release files / openaidr-0.10.2-py3-none-any.whl

Download URL openaidr-0.10.2-py3-none-any.whl
Size 158.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
79b96830c2102df0ae0b1dac84ee4cf18802d271b4a6f3721b5479fc378c6290
BLAKE2b-256 checksum
How to use checksums
ab42632632d88258da0f4efb682fd9c74a3310f5ab17a12904dddd1d1914f14b
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 1, 2026.

Transparency log

Security for openaidr 0.10.2

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 openaidr-0.10.2.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 1, 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