Skip to main content

toolpact

PyPI Version Tests Codecov License: MIT

Catch breaking signature changes before they reach a deployed agent. package-lock.json for LLM tool schemas.

Why toolpact?

Tool schemas are generated from Python function signatures at runtime. When you rename a parameter, the schema silently changes. LLMs in deployed sessions were prompted with the old schema — they keep sending tool calls with the old argument names until they fail. No linter catches this. No type checker catches this. It is invisible.

toolpact treats the schema as a versioned contract. First run — generates the schema, hashes it, writes toolpact.lock. Every subsequent run — regenerates and compares. If the schema changed, it raises PactChanged and blocks startup until you explicitly accept the change.


How it works

Check at decoration time, not call time

The @pact decorator runs when Python loads the module — at import, before any request is served. A schema change crashes the import, not a live call. The wrapped function is returned unchanged, so there is zero per-call overhead.

Hash of canonical JSON Schema.

The schema is serialized with sorted keys and no whitespace before hashing. Identical schemas always produce the same hash regardless of dict insertion order. Description fields are stripped before hashing — changing a docstring never triggers a check, only structural changes do (parameter names, types, required status).

Lockfile in version control

toolpact.lock is committed to git. A signature change produces a diff in the lockfile that is visible in PR review. Reviewers see exactly what the LLM contract changed before it merges.

Breaking vs non-breaking

Adding an optional parameter is non-breaking — LLMs using the old schema still work, they just won't use the new argument. Renaming a required parameter is breaking — the LLM sends the old name and the function fails. toolpact classifies every change so you can decide how to respond.


Installation

pip install toolpact

Quick Start

Step 1: Decorate your tools

from toolpact import pact

@pact
def search(query: str, max_results: int = 10) -> list[str]:
    """Search documents by query."""
    ...

@pact
def get_weather(city: str, units: str = "celsius") -> dict:
    """Get current weather for a city."""
    ...

First import: both functions are registered in toolpact.lock. Commit this file.

Step 2: Change a signature

# renamed: query -> search_query
@pact
def search(search_query: str, max_results: int = 10) -> list[str]:
    """Search documents by query."""
    ...

Next import raises immediately:

[toolpact] Schema changed for 'search'

  [BREAKING]  parameter removed: query
  [BREAKING]  parameter added: search_query

  2 breaking change(s).

To accept: toolpact accept search
Accept all: toolpact accept --all

Step 3: Review and accept

toolpact diff            # show all pending changes
toolpact accept search   # accept and update lockfile

Re-import — no error.


CI Integration

Add to your pipeline to block merges that silently break tool contracts:

# .github/workflows/ci.yml
- name: Check tool schema contracts
  run: toolpact check

toolpact check exits 0 if all schemas match, exits 1 if any have pending changes:

toolpact: checking 3 function(s) against toolpact.lock

  v  get_weather          unchanged
  v  summarize            unchanged
  x  search               SCHEMA CHANGED (BREAKING)

1 function(s) have schema changes. Run 'toolpact diff' to review.

Change Classification

Not every signature change breaks a deployed agent. toolpact classifies each:

Change Breaking? Reason
Required parameter renamed Yes LLM sends old name, function expects new name
Required parameter removed Yes LLM sends it, function no longer accepts it
Required parameter type changed Yes LLM sends old type, function expects new type
Optional parameter removed Yes LLM might send it if prompted with old schema
Parameter made required (was optional) Yes LLM with old schema won't send it
Enum narrowed (values removed) Yes LLM might send a removed value
Optional parameter added No LLM won't send it — default covers it
Parameter made optional (was required) No LLM still sends it, function still works
Enum widened (values added) No Old values remain valid
Description changed No Excluded from hash — docstring edits never trigger a check

Use breaking_only=True to warn instead of raise on non-breaking changes:

@pact(breaking_only=True)
def search(query: str, max_results: int = 10, offset: int = 0) -> list[str]:
    ...

CLI Reference

toolpact check                  # exit 0 if all match, exit 1 if any changed
toolpact diff                   # show diff for all pending changes
toolpact diff <fn_name>         # show diff for one function
toolpact accept <fn_name>       # accept change, update lockfile
toolpact accept --all           # accept all pending changes
toolpact list                   # list all registered functions with timestamps
toolpact show <fn_name>         # print current stored schema as JSON

Lockfile Format

toolpact.lock is plain JSON — commit it, never ignore it:

{
  "_toolpact": "1",
  "functions": {
    "search": {
      "accepted_at": "2026-07-12T10:00:00Z",
      "breaking": false,
      "hash": "sha256:a3f9b2c1d4e5f678",
      "schema": {
        "name": "search",
        "description": "Search documents by query.",
        "parameters": {
          "type": "object",
          "properties": {
            "query": {"type": "string"},
            "max_results": {"type": "integer", "default": 10}
          },
          "required": ["query"]
        }
      }
    }
  }
}

A signature change produces a visible diff in this file — reviewers see exactly what the LLM contract changed before the PR merges.


Options

@pact(
    breaking_only=False,       # warn on non-breaking, raise only on breaking
    lockfile="toolpact.lock",  # custom lockfile path
    mode="eager",              # "eager": check at import | "lazy": check on first call
)
def my_tool(...): ...

mode="lazy" is useful in notebooks and scripts where import-time failures are inconvenient. mode="eager" (default) is safer for production servers — fail fast before any request is served.

Metadata

Release files for toolpact 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for toolpact 0.1.0
File Size Uploaded Provenance
toolpact-0.1.0.tar.gz 9.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for toolpact 0.1.0
File Interpreter ABI Platform
toolpact-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 20.4 kB

Release files / toolpact-0.1.0.tar.gz

Download URL toolpact-0.1.0.tar.gz
Size 9.0 kB
Tags Source
SHA-256 checksum
How to use checksums
815f675bbad8689caae1ec47417a64494db1792fc6206b2dd7d47fa106b0e16b
BLAKE2b-256 checksum
How to use checksums
a6683f254c84e8a8e1f223e43757a3ef4f921cc9c17b1dd83b9f1ba9af32717d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 17, 2026.

Transparency log

Release files / toolpact-0.1.0-py3-none-any.whl

Download URL toolpact-0.1.0-py3-none-any.whl
Size 11.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0580a2542bf684bdf3a0a21c6410353dce2d6cc1bf40f0289bbcdf0c629f703e
BLAKE2b-256 checksum
How to use checksums
c291331defeb0fb6a18523e40a79bb98358d9cfa6a53384e55f645d338e938b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 17, 2026.

Transparency log

Security for toolpact 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for toolpact-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 17, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page