Skip to main content

ResultSeal logo

ResultSeal

HTTP 200 is not an observation. Empty is not not-found. A tool call is not an effect.

CI PyPI Python versions MIT license Live playground

An agent asks a CRM tool for customer 42. The search returns [], and the agent tells the user the customer does not exist. Nothing failed: the HTTP status was 200, the JSON was valid, the schema check passed. The empty list simply got promoted into a fact it never supported.

ResultSeal sits between your tools and your model. You declare, per tool, what a result has to contain before it may back a claim. ResultSeal checks each raw result against that contract and either seals it or blocks it with a stable reason code. Empty, partial, stale, mismatched, errored and unverified results are blocked by default.

  • Framework-neutral. Plain Python and a CLI. There are drop-in examples for LangChain, LangGraph, LlamaIndex, CrewAI, Pydantic-AI, smolagents and AutoGen.
  • Deterministic. The same input and reference clock always produce the same decision and the same SHA-256 fingerprint. No model calls and no network.
  • Small and strict. One runtime dependency (PyYAML). Inputs are bounded, YAML tags and anchors are refused, and contracts are data only.

What it catches

The tool returns A naive agent concludes ResultSeal
[] or {} from a search "That record does not exist." blocked: EMPTY_WITHOUT_NOT_FOUND_SENTINEL
{"status": "NOT_FOUND"}, and the contract names NOT_FOUND as its sentinel "That record does not exist." This time it is right. sealed: not_found
HTTP 404 with {"error": "not_found"} "Confirmed absent." blocked: TRANSPORT_FAILED
HTTP 204 with an empty body after a DELETE "Deleted." blocked: EMPTY_WITHOUT_NOT_FOUND_SENTINEL
MCP result with isError: true and some text Reads the error text as the answer blocked: PROTOCOL_CONFLICT
A cached record at revision-9 when the contract needs revision-12 Acts on stale state blocked: STALE_OBSERVATION
{"customer_id": "42"} when name is required Fills the gap blocked: MISSING_REQUIRED_FIELD
A record for customer:7 when you asked for customer:42 Answers about the wrong customer blocked: TARGET_MISMATCH
"Email sent" with no message id "Done." blocked: UNVERIFIED_EFFECT

Several of these cases are presets in the playground.

Install

pip install resultseal

Requires Python 3.11 or newer.

Quickstart

Guard a tool in Python

A contract says what a sealed result must contain and where it must come from:

import resultseal

contract = resultseal.load_contract({
    "schema_version": "1",
    "claim_type": "found",
    "required_fields": ["customer_id", "name"],
    "freshness": {"mode": "not_required"},
    "source_ref": "crm://customers",
    "target_ref": "customer:42",
})

Wrap the raw tool output in an adapter input and call seal. It returns the payload when the result is sealed and raises BlockedObservation when it is not, so a blocked result cannot reach the model through a forgotten if:

def observe(rows):
    return {
        "kind": "json",
        "source_ref": "crm://customers",
        "target_ref": "customer:42",
        "tool_name": "search_customer",
        "body": rows,
    }

resultseal.seal(observe({"customer_id": "42", "name": "Ada"}), contract)
# -> {'customer_id': '42', 'name': 'Ada'}

try:
    resultseal.seal(observe([]), contract)
except resultseal.BlockedObservation as blocked:
    print(blocked)
    # observation blocked as empty: EMPTY_WITHOUT_NOT_FOUND_SENTINEL

smolagents and AutoGen hand a tool's exception back to the model as an error result, as does LangGraph's ToolNode(handle_tool_errors=True). The model then reads "observation blocked as empty" where it would otherwise have read [].

If you want to branch on the result instead of catching an exception, use check. record() returns the decision record, the same JSON that the CLI prints:

result = resultseal.check(observe({"customer_id": "42"}), contract)

result.sealed        # -> False
result.truth_state   # -> TruthState.PARTIAL
result.reason_codes  # -> ('MISSING_REQUIRED_FIELD',)
result.record()["deterministic_fingerprint"]  # 'sha256:' + 64 hex digits

Absence can be sealed too, but only when the source says so explicitly. A not_found contract names the sentinel value that counts as a real "no such record":

not_found = resultseal.load_contract({
    "schema_version": "1",
    "claim_type": "not_found",
    "required_fields": [],
    "freshness": {"mode": "not_required"},
    "source_ref": "crm://customers",
    "target_ref": "customer:42",
    "not_found_sentinel": "NOT_FOUND",
})

resultseal.check(observe({"status": "NOT_FOUND"}), not_found).sealed  # -> True
resultseal.check(observe([]), not_found).sealed                       # -> False

Check from the command line

The CLI evaluates JSON or YAML files and uses exit codes, so it fits into CI:

# A complete MCP result seals (exit 0)
resultseal check examples/mcp_result.json --contract examples/customer_contract.json

# An HTTP 200 with an empty body blocks (exit 1)
resultseal check examples/http_empty.json --contract examples/customer_contract.json --format md

# Replay every shipped fixture against its recorded expectation
resultseal replay fixtures/*.yaml

# Validate a contract or fixture without evaluating anything
resultseal validate examples/customer_contract.json
MATCH: bare-json-payload -> empty/blocked
MATCH: complete-fresh-result -> sealed/sealed
MATCH: dynamodb-empty-query -> partial/blocked
...
MATCH: unsafe-input -> rejected (UNSAFE_INPUT)
Exit code check replay validate
0 sealed every expectation matched valid
1 blocked at least one mismatch —
2 invalid input or contract invalid fixture invalid
3 unsafe input (limits, YAML tags or anchors) unsafe fixture document unsafe

check and replay accept --format json|md, --now <ISO-8601> to pin the reference clock, and --redact FIELD (repeatable) to mask record fields before they are fingerprinted.

Framework integrations

Each example is a single self-contained file. The guard runs without the framework installed, so the test suite covers every example. Each example was also run end to end against current releases of its framework.

Framework Example Hook point
LangChain langchain_tool_guard.py @tool
LangGraph langgraph_tool_guard.py ToolNode inside a StateGraph
LlamaIndex llamaindex_tool_guard.py FunctionTool
CrewAI crewai_tool_guard.py crewai.tools.tool
Pydantic-AI pydantic_ai_guard.py @agent.tool_plain, run offline with TestModel
smolagents smolagents_tool_guard.py @tool
AutoGen autogen_tool_guard.py AssistantAgent tools, run offline with ReplayChatCompletionClient

All of them follow the same pattern:

from pathlib import Path

import resultseal

CONTRACT = resultseal.load_contract(Path("examples/customer_contract.json"))

def search_customer(query: str) -> object:
    rows = []  # your real lookup goes here
    return resultseal.seal(
        {"kind": "json", "source_ref": "mcp://crm-server",
         "target_ref": "customer:42", "body": rows},
        CONTRACT,
    )

How it decides

Adapter inputs

An input is a JSON object that describes one tool result. Every input needs a source_ref and a target_ref. These are the identity that the contract must match. Inputs may also carry tool_name, observed_at, source_version, evidence_refs, observation_id, tool_call_id and scalar metadata. If you leave out kind, it is inferred from the fields that are present.

kind Payload fields Structural facts it establishes
json body (structured value, or a JSON string) Only body is the payload. Other top-level fields are metadata.
http_json status_code, body 2xx/3xx means transported. Any other status is TRANSPORT_FAILED, whatever the body says.
mcp structuredContent, text, isError, outputSchema isError: true is a PROTOCOL_CONFLICT. structuredContent takes precedence over text.
stdio exit_code, stdout, output_format: json A non-zero exit is TRANSPORT_FAILED. A zero exit proves only that the process completed.
yaml value (YAML text) Parsed under the same safe loader as files.
claim_only claim A claim with no dispatch record. It is always NO_DISPATCH.

Contracts

Field Meaning
schema_version Always "1".
claim_type found, not_found, read_complete, effect_observed or task_complete.
required_fields Top-level payload keys that must be present.
freshness {"mode": "not_required"}, {"mode": "source_version"} (needs min_source_version), or {"mode": "max_age_seconds", "max_age_seconds": N}.
source_ref, target_ref The identity the observation must carry.
not_found_sentinel The only value that can seal a not_found claim.
effect_evidence_required For effect and task claims, require non-empty evidence_refs.

Evaluation order

The rules run in a fixed order and the first one that fails decides the result. The engine is a pure function: it does no I/O and reads no clock or environment.

# Condition Truth state Reason code
1 No dispatch record unknown NO_DISPATCH
2 Transport failed unknown TRANSPORT_FAILED
3 Payload did not parse parse_error PARSE_FAILED
4 Protocol contradicts itself unknown PROTOCOL_CONFLICT
5 Payload is empty and the sentinel does not match empty EMPTY_WITHOUT_NOT_FOUND_SENTINEL
6 Source or target differs from the contract source_mismatch SOURCE_MISMATCH, TARGET_MISMATCH
7 Older than the freshness requirement stale STALE_OBSERVATION
8 Required field missing partial MISSING_REQUIRED_FIELD
9 Effect claimed without evidence unverified_effect UNVERIFIED_EFFECT
10 Every condition holds sealed (not_found for absence) SEALED_WITH_REQUIRED_EVIDENCE

Malformed or unsafe input is never evaluated. It raises a ResultSealError whose code is one of SCHEMA_INVALID, PARSE_FAILED, CONTRACT_INVALID, INVALID_ARGUMENT, UNSAFE_INPUT or LIMIT_EXCEEDED. The full precedence rules are in PROMOTION_RULES.md.

Fixtures from real backends

fixtures/ holds 19 self-contained cases. Each one includes the input, the contract and the expected outcome. Every CI run replays all of them. They cover the core semantics and the response shapes that real backends produce when nothing matched: SQL empty result sets, Redis nil, Elasticsearch zero hits, DynamoDB Items: [], GraphQL data: null with errors, and structured HTTP 404 bodies. The fixture catalog lists each case and why it seals or blocks.

Guarantees and limits

  • Determinism. Decisions depend only on the input, the contract and the reference clock. Reports never contain wall-clock values. Pin --now (or now=) if you need max_age_seconds verdicts that replay identically.
  • Bounded input. By default: 128 KiB per file, nesting depth 32, 4,096 nodes, 16,384-character strings. These are checked before and right after parsing. YAML anchors, aliases, and any tag beyond the plain scalar, sequence, mapping, and timestamp types are refused.
  • Offline. The package never opens a socket or starts a subprocess. A test scans the source to enforce this.
  • Scope. ResultSeal protects the local promotion decision. It does not prove that a remote system is honest, judge the quality of a model's prose, or replace authorization. See THREAT_MODEL.md and NON_GOALS.md.

ResultSeal is not an agent framework, proxy, dashboard, policy engine, retry layer or LLM judge. It is one function you call at the tool boundary.

Documentation

Contributing

New fixtures from real backends, framework examples and adapter work are all welcome. CONTRIBUTING.md covers setup and the project's fail-closed principles.

git clone https://github.com/sx4im/resultseal.git
cd resultseal
python -m venv .venv && source .venv/bin/activate
make install   # editable install with dev tools
make all       # tests, ruff, mypy --strict, build
Contributors

License

MIT

Metadata

Release files for resultseal 0.2.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 resultseal 0.2.0
File Size Uploaded
resultseal-0.2.0.tar.gz 473.1 kB Details

Built distribution (wheel)

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

Total release size: 509.9 kB

Release files / resultseal-0.2.0.tar.gz

Download URL resultseal-0.2.0.tar.gz
Size 473.1 kB
Tags Source
SHA-256 checksum
How to use checksums
edad5131204cec35f82a79133fe7c3ca8efa57bc30fc8efe013d7b768050b26f
BLAKE2b-256 checksum
How to use checksums
1c33685d3c6dc32d3e36214b49ed2a8dcee786420c9c44d426253c8172e9fec5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Oct 15, 2026.

Transparency log

Release files / resultseal-0.2.0-py3-none-any.whl

Download URL resultseal-0.2.0-py3-none-any.whl
Size 36.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7653224645269fde66091f07c88296c5bc605c603b9a990be2e2a55fbefdb4ee
BLAKE2b-256 checksum
How to use checksums
e621c5fda28b6d5d4946c6769b9098ff862a953f455aaf7d1b0a8f2c83ee4e2c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

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