Metadata-Version: 2.5
Name: tessera-transcript
Version: 0.1.0
Summary: Keep LLM transcripts valid. Tool-call pairing survives compaction, interruption, retry and provider swap.
Project-URL: Homepage, https://github.com/Raghu23-dev/tessera
Project-URL: Repository, https://github.com/Raghu23-dev/tessera
Project-URL: Issues, https://github.com/Raghu23-dev/tessera/issues
Project-URL: Changelog, https://github.com/Raghu23-dev/tessera/blob/main/CHANGELOG.md
Author-email: Raghuram P <raghu2308.dev@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: agents,anthropic,compaction,context-window,llm,openai,tool-use
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: tokenizers
Requires-Dist: tiktoken>=0.7; extra == 'tokenizers'
Description-Content-Type: text/markdown

# tessera

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

[![CI](https://github.com/Raghu23-dev/tessera/actions/workflows/ci.yml/badge.svg)](https://github.com/Raghu23-dev/tessera/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/tessera-transcript)](https://pypi.org/project/tessera-transcript/)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Coverage 97%](https://img.shields.io/badge/coverage-97%25-brightgreen)](#development)
[![Types: strict](https://img.shields.io/badge/mypy-strict-blue)](#development)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Raghu23-dev/tessera/badge)](https://scorecard.dev/viewer/?uri=github.com/Raghu23-dev/tessera)

---

## 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.

```python
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

```bash
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`.

```python
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.

```bash
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

```bash
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
