Metadata-Version: 2.4
Name: tool-call-repair
Version: 0.1.0
Summary: Keep tool_use and tool_result paired when you trim an agent's history. Zero dependencies.
Author: Umar Aslam
License-Expression: MIT
Project-URL: Homepage, https://github.com/Umaraslam66/tool-call-repair
Project-URL: Issues, https://github.com/Umaraslam66/tool-call-repair/issues
Keywords: llm,agents,anthropic,openai,tool-use,context-window,conversation-history
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Dynamic: license-file

<div align="center">

# toolrepair

**Your agent trims its history, and the *next* turn 400s.**

```
messages.1: unexpected tool_use_id found in tool_result blocks
```

**One function fixes it. Zero dependencies.**

</div>

---

Anthropic and OpenAI both require a tool call and its result to stay adjacent:
every `tool_use` must be answered in the **immediately following** message. Any
history manipulation can cut between them — sliding-window trimming,
summarization, compaction, resuming a session, dropping a failed tool.

Then the API rejects the request. And it does it *late*: the turn that trimmed
succeeds, and the turn after that fails, so the traceback points nowhere near
the code that caused it. From the LangChain issue thread:

> "hard to catch because it only manifests on the second turn after a turn that used tools"

A GitHub search for that error string returns **over 1,300 matching issues**,
spread thin across `vercel/ai`, `litellm`, `github/copilot-cli`, `laravel/ai`,
and dozens of smaller agents — a handful each. It isn't one broken framework;
it's everyone independently hitting the same wall, with repair logic living
private inside each project. LangChain's issue has been open since Feb 2025 with
**7 unmerged fix PRs from 6 contributors**.

## Install

```bash
pip install git+https://github.com/Umaraslam66/tool-call-repair.git
```

Or just copy `toolrepair.py` into your project. It's one file with no
dependencies and works on plain dicts, so it doesn't care which SDK you use.

The import name is `toolrepair` either way.

## Use

```python
from toolrepair import validate, repair, safe_trim

validate(messages)   # -> [Violation(...)]  — what's broken, and exactly where
repair(messages)     # -> a corrected copy that the API will accept
safe_trim(messages, keep=20)   # -> a trim that cannot orphan anything
```

Format is detected automatically; pass `format="anthropic"` or `"openai"` to be
explicit.

### Stop the bug at the source

Replace the naive tail slice — the single most common cause:

```python
- messages = messages[-20:]                    # cuts mid-pair, breaks next turn
+ messages = safe_trim(messages, keep=20)      # backs up to a legal boundary
```

`keep` is a floor, not a ceiling: the cut moves *earlier* to stay legal, so you
may retain a couple more messages than you asked for. It never returns more than
you gave it.

### Repair what you already have

```python
from toolrepair import repair

try:
    response = client.messages.create(model="claude-opus-5", messages=messages, ...)
except anthropic.BadRequestError:
    response = client.messages.create(model="claude-opus-5",
                                      messages=repair(messages), ...)
```

Two strategies:

| Strategy | What it does | Use when |
|---|---|---|
| `drop` (default) | Removes the unpaired call | Cleaning up history you're about to discard anyway |
| `synthesize` | Answers the call with an `is_error` placeholder | **Mid-conversation** — keeps the assistant's reasoning in context, so following turns still read coherently |

Orphaned *results* are always dropped under both. There's no call to attach them
to, and inventing one would fabricate a request the model never made.

### See what's wrong

```python
for v in validate(messages):
    print(v)
# orphaned_tool_use at messages[4].content[1] (toolu_01A): tool_use has no
# tool_result in the immediately following user message.
```

| Code | Meaning |
|---|---|
| `orphaned_tool_use` | A call with no result in the next message |
| `orphaned_tool_result` | A result answering no call in the previous message |
| `duplicate_tool_result` | Two results for the same call |
| `tool_result_not_first` | Results must lead the content list (Anthropic) |
| `malformed_tool_use` | A call with no `id`, so nothing can reference it |

## Correctness

The guarantee is a round trip: **for any input, `repair` produces a history that
`validate` accepts.** That's asserted directly rather than by checking for a
particular output shape, because the shape is an implementation choice and the
invariant is not.

- **86 unit tests** covering both API formats, parallel calls, partial answers,
  misordered content, string-vs-list content, and the LangChain scenario above.
- **Property tests over randomly generated histories**, including deliberately
  corrupted ones — naive head cuts, deleted middles, shuffled orders — asserting
  five invariants:

  1. `repair(x)` always validates clean
  2. `repair` is idempotent
  3. `repair` leaves an already-valid history byte-identical
  4. `safe_trim(x, k)` is legal at every `k`
  5. neither function mutates its input

**2,886 tests pass.** A separate 40,000-history fuzz run reports zero failures.

```bash
pytest tests/ -q
```

## What this does not do

- It won't re-run your tools. A synthesized result is explicitly an error
  placeholder, never a fabricated success.
- It won't count tokens. `safe_trim` trims by message count; if you need a token
  budget, trim to your budget however you like and then call `repair`.
- It won't fix a history that was never valid in a different way — malformed
  blocks, bad roles, missing fields outside the tool-pairing rules.

## License

MIT
