Skip to main content

atif-make

Make ATIF v1.7 trajectories from agent logs — Claude Code, Codex, Copilot CLI, HAR captures, and the SLEIGHT-Bench, ATBench and METR MALT datasets.

Zero runtime dependencies. Python 3.12+.

Install

uv tool install atif-make          # puts `atif-make` on your PATH

Or as a library in a project:

uv add atif-make

For a browser view of what you convert, see the companion transcript-viewer, which depends on this package.

uv tool install builds an isolated environment, so nothing lands in your project or system Python. To follow local edits instead, use uv tool install --editable .; to remove it, uv tool uninstall atif-make.

Running from a checkout without installing works too: uv run atif-make ....

atif-make ~/.claude/projects/my-project/session.jsonl   # convert one log

Commands

atif-make <file>                 convert (shorthand for `atif-make convert`)
atif-make convert <file>         convert one log
atif-make convert <dir|archive>  convert every log inside
atif-make formats                list supported input formats
Flag Command Meaning
-o, --output convert output path (default <input>.trajectory.json)
-f, --format convert force the input format instead of detecting it
--json convert write one self-contained document to stdout
--bundle OUT.zip convert zip the trajectory with its images and subagents
--split-subagents convert write subagents as sibling files, not embedded
--indent N convert JSON indent (default 2)
-q, --quiet convert suppress progress output

Supported inputs

Most agents write two unrelated log shapes — what the CLI streams, and what it persists on disk — and they are not interchangeable. atif-make reads both.

Format Source
claude-code-transcript ~/.claude/projects/<project>/<session>.jsonl
claude-code-stream claude -p --output-format stream-json
codex-rollout ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
codex-exec codex exec --json
copilot-cli Copilot CLI session logs
sleight-bench SLEIGHT-Bench transcripts — one content block per line
atbench ATBench — labelled agent trajectories, many to a file
metr-malt METR MALT — agent runs recorded as a tree of nodes
har Anthropic Messages, OpenAI Chat Completions, OpenAI Responses
atif An ATIF trajectory that is already converted

sleight-bench reads the SLEIGHT-Bench benchmark, where each line holds a single Anthropic content block rather than a whole message, so one assistant turn spans several lines. Its transcripts open with a canary object asking that the data be kept out of training corpora; that line is skipped as a message and carried into the trajectory's extra, so the opt-out travels with the converted file instead of being dropped at the door.

atbench reads ATBench, a safety benchmark of tool-using agent trajectories where each one is labelled for whether the agent did something unsafe. Its two configs differ only in spelling — contents/id against content/conv_id — and both are read.

A row's parts carry three roles: a user request, an agent turn as a thought plus an action, and an environment reply holding what a tool returned. An action is either a tool call or the word Complete followed by the agent's final answer, so the ending is a message rather than a call to a tool named Complete. Thoughts and final answers frequently arrive JSON-encoded a second time, as a quoted string inside a string; those are unwrapped, but only when they really do decode, so a thought that merely opens with a quotation mark keeps it.

The labelling has no home in ATIF's own fields, so it travels in the trajectory's extra: label, risk_source, failure_mode, real_world_harm and reason. A tool whose description was tampered with keeps the original under _original_description in the agent's tool definitions — that is the whole mechanism of an indirect prompt injection, and it is preserved as it ships.

metr-malt reads METR's MALT transcripts, where a run is a tree rather than a list: each node carries one message and points at its parent, and a branch_id says which line of the run it belongs to. Messages are in OpenAI's older chat shape, with function_call rather than tool_calls, because these runs date from that scaffold.

Most records hold one conversation with short offshoots hanging off it — an alternative continuation sampled at some step, sometimes followed by a rater scoring what it was worth. Those offshoots are the reason the dataset exists: MALT publishes the same runs five times over, each with the chain of thought altered a different way, so what branches off a step is what is being compared against it. Each is kept on the step it branches from, in that step's extra, with the rater's scores where there are any — they arrive in a call's arguments with nothing in the message at all, so reading only the text would lose them.

A few records hold several whole conversations at once: an advisor briefing an agent, the agent working, and raters scoring it, each with its own system prompt and its own root, interleaved in one list of nodes. Splicing those into one would invent a run that never happened, so they are separated by the root each node climbs to, the longest becomes the trajectory, and the rest travel as subagent trajectories.

The dataset ships as Parquet. Reading it needs the parquet extra (uv tool install "atif-make[parquet]"); nothing else here does, so the package stays dependency-free for every other format. Splitting a shard is the only step that needs it — what comes out is ordinary JSON.

A file that holds many transcripts

ATBench is published as one JSON array of a thousand trajectories rather than a thousand files. That is a container, like a zip, so it is treated as one: the file is split into a transcript per file and each is converted separately, into an output directory.

atif-make convert test.json -o converted/     # 1000 transcripts -> 1000 documents

Names carry the dataset's own identifier (test-00007-unsafe_003923_aa62fc1a), so a converted document can be traced back to the row it came from. Asking to convert such a file as though it were a single transcript is refused rather than answered with the first of a thousand.

The parser was built against all 86 transcripts in the dataset, not the published spec alone, which is how the three places they disagree came to light: a tool result can be a list of content blocks rather than a string, cwd is documented as required but is sometimes absent, and a transcript can end on a tool call that never got a result. All 86 convert with no loss and pass the reference validator.

Directories and archives — .zip, .tar, .tar.gz, .tgz, .tar.bz2, .tbz2, .tar.xz, .txz — are read as containers: every log inside is found and converted. That closes the loop on --bundle — the zip atif-make hands you to send someone opens again in atif-make, images and all.

Archives are extracted to a temporary directory, once per run. Members naming absolute paths or climbing out with .. are refused rather than quietly sanitised, and an archive that expands past 8 GB or 20,000 entries is rejected outright.

Format is detected from content, never from the extension. atif exists so a trajectory someone sends you opens like anything else — it is loaded, not reparsed, and unknown fields from a newer ATIF minor version are dropped rather than rejected.

What it gets right

These are the things that are easy to get wrong, and that silently corrupt a trajectory when you do:

Split messages. Claude Code writes one API response as several JSONL lines that share a message.id — thinking, text, and each parallel tool_use arrive separately, with the same usage object repeated on every line. Treating those as separate turns inflates step counts and multiplies token totals. atif-make coalesces them and counts usage once.

Out-of-order tool results. Parallel calls come back interleaved, and a slow call can return several turns after it was issued. Pairing results to calls by position drops some and misattributes others. atif-make pairs by tool_use_id.

Byte-capped detection. A JSONL preamble (hook events, rate-limit notices) can push the identifying line kilobytes into a file. atif-make scans whole lines.

Subagent structure. Claude Code links a delegated agent through a .meta.json sidecar (toolUseId) and an agentId field on the result line — not through anything in the result text. atif-make links by call id, so refs actually resolve instead of leaving orphaned subagents.

Images. Codex embeds screenshots as base64 data URLs and Claude Code as base64 content blocks — 24 of the sessions on one test machine carried them, and a single Codex session held 65 images totalling 14 MB. Dropping them loses the thing the agent was actually looking at, and inlining them makes an unreadable document. atif-make writes them to images/ and references them by relative path, which is what the spec asks for.

Malformed timestamps. ATIF requires ISO 8601. A truncated or hand-edited log can carry something else, and passing it through would make the whole trajectory fail validation, so an unparseable timestamp is dropped rather than emitted.

HAR tool results. In a HAR capture a tool's output is not in the response that called it — it appears in the next request's message history. atif-make harvests results across entries and pairs them back by id, while emitting the shared conversation prefix only once.

Output — a trajectory is a directory, not a file

ATIF references images and split subagents by path relative to the trajectory file, so anything with attachments is inherently multi-file:

session.trajectory.json                 the document
session.trajectory.<agent-id>.json      subagents, with --split-subagents
images/<sha>.png                        images, referenced as "images/<sha>.png"

Images are de-duplicated by content hash, so the same screenshot pasted five times is stored once.

Three ways out, depending on where it's going:

atif-make session.jsonl -o out/t.json      # directory form: t.json + images/
atif-make session.jsonl --json             # one self-contained doc; images inlined as data: URIs
atif-make session.jsonl --bundle send.zip  # zip of the whole directory — for sending someone

--json is the exception that proves the rule: stdout has no directory to put siblings in, so images become data URIs to keep the document standalone.

Output carries timestamp, reasoning_content, per-step metrics, multimodal ContentPart message content, and subagents either embedded (subagent_trajectories) or split into sibling files with resolvable trajectory_path refs.

What this does not do

It converts, and stops there. No index, no library, no memory of what you have already looked at — those belong to whatever is doing the looking. In practice that is transcript-viewer, which depends on this package and keeps its own ~/.atif/index.json.

The line is worth stating because it moved: the index used to live here, which meant the converter knew the viewer's directory (~/.atif/opened) and its vocabulary for where a session came from, for the sake of a command conversion never needed. A converter should take one log — or an archive of them — and produce ATIF.

Discovery survives only as far as conversion requires it: convert.find walks a directory or an archive and reports what is convertible, with the format of each. Nothing is remembered between runs.

Tests

uv run pytest                      # unit tests, all synthetic fixtures
uv sync --extra spec               # pulls harbor (large)
uv run --extra spec pytest         # + validate against the reference ATIF models

The spec suite validates every fixture against harbor's own pydantic models — ground truth for whether the output is really ATIF, rather than what atif-make believes ATIF to be.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

atif_make-0.5.0.tar.gz (215.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

atif_make-0.5.0-py3-none-any.whl (49.8 kB view details)

Uploaded Python 3

File details

Details for the file atif_make-0.5.0.tar.gz.

File metadata

  • Download URL: atif_make-0.5.0.tar.gz
  • Upload date:
  • Size: 215.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for atif_make-0.5.0.tar.gz
Algorithm Hash digest
SHA256 d4c3122a63b553099f0843d5d1c666136b0bccb1c11e8ebd2b4f9c4a9200cb5e
MD5 58250d1b3944e0e2986b82fcfc8b4d0a
BLAKE2b-256 602c9e5d1b1ba278fde5135b8066487e029dd0620161b6a220f5cb2a118a0333

See more details on using hashes here.

File details

Details for the file atif_make-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: atif_make-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 49.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for atif_make-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b52fac5489f5cbc34f947f5d8b21b51a6a04276420d908a96f938e1bf7443791
MD5 3117aaae26c84be6be8edba7d3301edb
BLAKE2b-256 0047f9a09d10b3d53600c43fa1b22c1d01cb731e628b3572c2e40109de195cfb

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page