OpenAIDR
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. One agent kind reads today (
claude-code). 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 one kind: Claude Code. Codex and Cursor are the intended next kinds and are not read yet — a session from either is absent from the output entirely, not collected and marked. 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.1.0
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: sessions, turns, tool
calls, and span identity, read by an OpenAIDR-owned reader that calls
adr-sensor's Claude Code parser one file at a time and normalises everything
upstream's shared event schema cannot express. The model has six outcome
statuses; this reader emits five of them (unknown, rejected, pending,
ok, error) from Claude Code's own transcripts and, for MCP calls, its
per-server connection logs — interrupted is modelled but not emitted, since
neither source records it.
Not yet built, in the order they matter:
- Two further agent kinds, Codex and Cursor. What each would cost is measured in Agent kind coverage rather than assumed — including why Cursor cannot be added dependency-only without breaking span identity.
- Incremental collection. Every run today is one cold pass: the whole window re-walked and re-parsed from scratch, held in memory. That is affordable once and not on every change. Because sessions are append-only, a steady-state path can instead re-read only the files whose modification time moved, replacing each session with its longer self — and span identity is derived the way it is precisely so that every span already emitted survives that re-read rather than shifting under a consumer.
Why this 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.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| openaidr-0.2.0.tar.gz | 198.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| openaidr-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 266.8 kB
Release files / openaidr-0.2.0.tar.gz
| Download URL | openaidr-0.2.0.tar.gz |
|---|---|
| Size | 198.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6e3e1e3cc9c2a631afd58e6f6aa36fd5621bbb20d3b422360e05546fc14ee429
|
|
BLAKE2b-256 checksum How to use checksums |
3bc3d0bfff101c26788c36b3147dba7a81f4bf1b2fd80c5305fed9e194daf6eb
|
| 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 17, 2026.
Transparency logRelease files / openaidr-0.2.0-py3-none-any.whl
| Download URL | openaidr-0.2.0-py3-none-any.whl |
|---|---|
| Size | 68.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b3582cd37612da3f383d5ee59603534b4faaf14fa9dc1b39017d16c967df27a4
|
|
BLAKE2b-256 checksum How to use checksums |
7e62bbbe18e1d9ec3ffea2887c6aef43e03a6c66e99888e8933866cf4b3ec848
|
| 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 17, 2026.
Transparency log