Skip to main content

tessera

Keep LLM transcripts valid. Tool-call pairing survives compaction, interruption, retry and provider swap.

CI PyPI Python 3.11+ License: MIT Coverage 97% Types: strict OpenSSF Scorecard


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-transcript because tessera was already taken on PyPI. You still import 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 user message is not necessarily a human turn. It may be pure plumbing carrying only tool_result blocks. Any rule keyed on role == "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

tessera_transcript-0.1.0.tar.gz (51.6 kB view details)

Uploaded Source

Built Distribution

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

tessera_transcript-0.1.0-py3-none-any.whl (34.2 kB view details)

Uploaded Python 3

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

Hashes for tessera_transcript-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f4082039bad2ebc6ccb61ec7f566f37b56f41697677f458f1f7d38990a348c7d
MD5 7ad5eff4d9dffdcdf81ecfdb2cab6f40
BLAKE2b-256 6036d9501115d7c3d61807666138f9d5733ef2c64e6a71b2b350a613a0796c8d

See more details on using hashes here.

Provenance

The following attestation bundles were made for tessera_transcript-0.1.0.tar.gz:

Publisher: release.yml on Raghu23-dev/tessera

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tessera_transcript-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for tessera_transcript-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 472189c867fd14bc083cc3a340cd189a6a307c14b9d02cb6e76b2010f2334cdd
MD5 c1cc035bcdd813f6a57e8d5ce8caab5f
BLAKE2b-256 8e12ac3f8fc1a4223c0450c96fba8886f8f5eb9856d94410064414783af500f2

See more details on using hashes here.

Provenance

The following attestation bundles were made for tessera_transcript-0.1.0-py3-none-any.whl:

Publisher: release.yml on Raghu23-dev/tessera

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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