tessera
Keep LLM transcripts valid. Tool-call pairing survives compaction, interruption, retry and provider swap.
The problem
messages.11: `tool_use` ids were found without `tool_result` blocks immediately after
If you have built anything agentic, you have seen this. It happens when a stream is cut between the model requesting a tool and the result being recorded, when a retry appends instead of replacing, or — most often — when context compaction slices the message list and the cut lands between a tool call and its answer.
There are 1,331 open GitHub issues matching that error string. It is the single most common structural failure in production LLM systems, and the usual fix is a hand-rolled tail slice that works until it doesn't.
messages = messages[-20:] # looks fine. breaks intermittently, under load, in production.
That slice lands wherever the arithmetic puts it. When it happens to land between an assistant turn and the results answering its calls, the kept results reference calls that are no longer present, and the provider rejects the request. Because it depends on where the tool calls happen to fall, it survives testing and surfaces later.
The fix
pip install tessera-transcript # the import name is `tessera`
The distribution is
tessera-transcriptbecausetesserawas already taken on PyPI. You stillimport tessera.
from tessera import inspect, repair, compact
# Is this safe to send?
if not inspect(messages):
messages = repair(messages).transcript
# Shrink to a budget without ever splitting a tool-call pair
result = compact(messages, max_tokens=100_000)
if result.refused:
print(result.reason) # says why, and leaves the transcript untouched
Works on the dicts you already have. No conversion, no wrapper types, no provider SDK.
Design
Dicts in, dicts out
tessera does not define a Message class and ask you to migrate. It reads and writes the
provider's own wire format, and preserves fields it does not recognise.
This is the load-bearing decision. The moment you need transcript repair is the moment something has already gone wrong — a stream was cut, a retry fired, a request came back 400 — and at that moment you are holding raw provider JSON, often deserialised from a log. A library that requires conversion is unavailable exactly when it is needed.
It also means cache-control markers, thinking blocks, citations and compaction blocks survive a repair. An adapter that rebuilt messages from its own understanding would silently drop them, and you would find out when a cache breakpoint stopped working.
A dangling call is answered. An orphaned result is deleted.
These look like mirror images. They are not, and treating them alike is the subtle bug.
| Defect | Fix | Why |
|---|---|---|
| Orphaned result — a result whose call is gone | Delete it | It refers to something outside the conversation. Nothing is lost |
| Dangling call — a call with no result | Answer it | Deleting it would erase the fact that the model asked. Its next turn was conditioned on having made that request |
Delete a dangling call and you have rewritten history: the model may repeat the call, or reason about a tool it has no record of invoking. Answering it — with an explicit "this did not complete" — keeps the request and tells the truth about the outcome.
The synthesised result is deliberately non-committal about side effects:
This tool call did not complete: the conversation was interrupted before a result was recorded. No side effects should be assumed either way.
Because we genuinely do not know. A stream can be cut after the tool ran but before the result was appended. Telling the model "the tool failed" is a claim we cannot support — and a model that believes it can safely retry a non-idempotent write because we said so is a real hazard.
Every synthesised block is marked _tessera_synthesised, so a later pass, a human reading a log,
or an eval can tell invented content from real content.
Refuse rather than emit something invalid
compact() walks backwards from the desired cut until it finds a boundary that splits no
tool-call pair. Such a boundary always exists, because the start of the transcript is one.
If the budget cannot be met without splitting a pair — one enormous tool exchange, say — tessera
returns the transcript unchanged, with refused=True and a reason.
A caller holding a slightly-too-large valid transcript can decide what to do. A caller holding an invalid one gets a 400 and no idea why.
Ambiguity raises
Repairing an Anthropic transcript with the OpenAI adapter would not crash. It would find no tool calls, report the transcript clean, and hand back something still broken.
So when two adapters claim a transcript with equal confidence, autodetect raises rather than
guessing. A confident wrong answer is worse than an error.
API
| Function | Purpose |
|---|---|
inspect(transcript) → Report |
Find every defect. Never mutates. bool(report) is "safe to send" |
repair(transcript) → RepairResult |
Return a valid copy plus a full account of every change |
compact(transcript, max_tokens=...) → CompactionResult |
Shrink to a budget, or refuse |
find_safe_boundary(transcript, i) → int |
The nearest cut at or before i that splits no pair |
is_valid(transcript) → bool |
Shorthand for bool(inspect(...)) |
Defects are named, not boolean, because the right response differs per defect:
DANGLING_CALL · ORPHANED_RESULT · DUPLICATE_RESULT · RESULT_BEFORE_CALL ·
EMPTY_ASSISTANT_TURN. Each carries a severity — FATAL (the provider will reject this) or
WASTEFUL (it will succeed but carry noise) — so strict= has something principled to key on.
Use it as a CI check
The library answers "is this sendable?"; the CLI makes that a build gate. Most teams discover a broken transcript when a request 400s in production — a persisted log on disk is the cheapest place to catch it first.
tessera check logs/ # exit 1 if any transcript is invalid
tessera show broken.json # explain the defects, change nothing
tessera fix broken.json -o fixed.json
tessera fix broken.json | curl -d @- ... # diagnostics go to stderr, so this pipes
Reads a JSON array of messages or JSONL. Exit codes are the contract: 0 valid,
1 invalid, 2 could not run. Unparseable files are reported and skipped rather
than failing the run, because a real logs directory contains unrelated JSON.
Providers
| Provider | Adapter | Result shape |
|---|---|---|
| Anthropic Messages | anthropic |
tool_result blocks nested in a user message |
| OpenAI Chat Completions | openai |
standalone role: "tool" messages |
Autodetected from content. Add your own with register(name, adapter) — the contract is four
methods.
Two provider details tessera gets right and hand-rolled code usually does not:
- An Anthropic
usermessage is not necessarily a human turn. It may be pure plumbing carrying onlytool_resultblocks. Any rule keyed onrole == "user"meaning "a person typed this" is wrong. - OpenAI tool arguments are a JSON string, and tessera never reparses them. Re-serialising is not byte-identical — key order and whitespace shift — and that string is what the model committed to. Rewriting it silently modifies the model's output and invalidates content-hash caching.
Not in scope
- Summarising dropped content. tessera decides where it is safe to cut. What you do with the dropped middle is your policy, and it needs an LLM call — which this library deliberately does not make.
- Token counting precision. The built-in counter is a ~4-chars-per-token heuristic, biased
high on structure so budgets are more likely respected than blown. Pass
token_counter=for a real tokenizer. - Being a framework. No agent loop, no provider client, no retry policy. One job.
Development
uv venv && uv pip install -e ".[dev]"
uv run pytest # coverage floor is 90%, enforced
uv run mypy src/tessera # strict
uv run ruff check .
No runtime dependencies, so the test suite runs offline and a fork PR from a stranger goes green without a single secret configured.
Correctness is checked three ways: unit tests per defect and provider; property-based tests (Hypothesis) asserting that repair always yields a valid transcript, is idempotent, and never mutates its input, over generated transcripts; and regression fixtures built from the real crash-loop transcripts reported in public issues.
Licence
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tessera_transcript-0.1.0.tar.gz.
File metadata
- Download URL: tessera_transcript-0.1.0.tar.gz
- Upload date:
- Size: 51.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4082039bad2ebc6ccb61ec7f566f37b56f41697677f458f1f7d38990a348c7d
|
|
| MD5 |
7ad5eff4d9dffdcdf81ecfdb2cab6f40
|
|
| BLAKE2b-256 |
6036d9501115d7c3d61807666138f9d5733ef2c64e6a71b2b350a613a0796c8d
|
Provenance
The following attestation bundles were made for tessera_transcript-0.1.0.tar.gz:
Publisher:
release.yml on Raghu23-dev/tessera
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tessera_transcript-0.1.0.tar.gz -
Subject digest:
f4082039bad2ebc6ccb61ec7f566f37b56f41697677f458f1f7d38990a348c7d - Sigstore transparency entry: 2492399856
- Sigstore integration time:
-
Permalink:
Raghu23-dev/tessera@ada643ade9b86ecd34e1eb479560e934fd91a8ac -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Raghu23-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ada643ade9b86ecd34e1eb479560e934fd91a8ac -
Trigger Event:
push
-
Statement type:
File details
Details for the file tessera_transcript-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tessera_transcript-0.1.0-py3-none-any.whl
- Upload date:
- Size: 34.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
472189c867fd14bc083cc3a340cd189a6a307c14b9d02cb6e76b2010f2334cdd
|
|
| MD5 |
c1cc035bcdd813f6a57e8d5ce8caab5f
|
|
| BLAKE2b-256 |
8e12ac3f8fc1a4223c0450c96fba8886f8f5eb9856d94410064414783af500f2
|
Provenance
The following attestation bundles were made for tessera_transcript-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Raghu23-dev/tessera
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tessera_transcript-0.1.0-py3-none-any.whl -
Subject digest:
472189c867fd14bc083cc3a340cd189a6a307c14b9d02cb6e76b2010f2334cdd - Sigstore transparency entry: 2492399926
- Sigstore integration time:
-
Permalink:
Raghu23-dev/tessera@ada643ade9b86ecd34e1eb479560e934fd91a8ac -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Raghu23-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ada643ade9b86ecd34e1eb479560e934fd91a8ac -
Trigger Event:
push
-
Statement type: