Skip to main content

faultgate-zero

Agent failure checks that run inside your existing pytest tests. No tracing setup, no collector, no account, no network, no LLM judge.

Install it, run pytest, and every test that drives an agent is checked for the failures answer-only asserts miss: loops on empty results, repeated identical calls, tool errors the agent ignored, provider errors it hid behind a fallback message, answers cut at the token limit, and tool calls that break your policy (refund limits, identity checks, required steps, denied paths).

Status: 0.1.0 alpha. Trace-based checks for existing pytest tests; no dependency on the faultgate package. See the known limitations.

60-second quickstart

pip install faultgate-zero

To try the included example from source:

git clone https://github.com/Faultline-lab/faultgate-zero
cd faultgate-zero
python -m venv .venv
.venv/bin/pip install -e .            # Windows: .venv\Scripts\pip install -e .
.venv/bin/pip install openai
cd examples/refund_agent
../../.venv/bin/pytest                 # Windows: ..\..\.venv\Scripts\pytest

The example's seven tests only assert on the agent's answer, so all seven pass without faultgate-zero. With it, one passes and six fail, each naming the step and the reason:

FAILED test_refund_agent.py::test_answers_about_an_order
faultgate-zero: 1 check fired on run 'test_refund_agent.py::test_answers_about_an_order' (3 steps, 4 model calls)
  repeated_call  step 3: lookup_order called 3x with identical arguments {"order_id":"1042"} and identical results
hint: mark the test with @pytest.mark.faultgate(skip=[...]) if a check is expected to fire
...
faultgate-zero: 7 runs checked, 6 with failures

In your own project: install it in the environment that runs your tests and run pytest. The plugin loads through pytest's pytest11 entry point; there is nothing to import or configure.

How it works

While tests run, faultgate-zero wraps the model SDK methods (OpenAI, Anthropic, LiteLLM). Each test is one agent run. Every model call made during the test is recorded in memory, with its request, response or exception, and tool calls are read from the messages the agent sends back to the model. When the test ends, the run is checked. A check that fires fails the test.

  • The wrappers never change what the SDK returns or raises; you get the SDK's own objects back, streams included. A bug in faultgate-zero never breaks your agent: the run is marked "capture incomplete", a warning is printed and its checks are skipped.
  • Nothing leaves the process and nothing is written to disk unless you ask for a report.
  • Runtime dependency: pytest only.

The checks

Check On by default Fires when
search_loop yes The run ends with no answer after at least 5 tool calls, and at least half the results it saw were empty.
repeated_call yes The same tool is called 3 or more times with the same arguments and gets the same result each time.
unrecovered_tool_error yes A tool call fails (error flag, Error: / Exception: / traceback result, or an error field) and that tool never succeeds later in the run.
unrecovered_model_error yes A model call raises (outage, rate limit, bad request) and no later model call succeeds, while the agent carries on, e.g. with a fallback message.
context_overflow yes A model response stops at the token limit (length, max_tokens, ...) or the provider rejects the prompt as too long.
policy yes A tool call breaks the policy file (below). With no file, only the built-in denied text applies (../, file://, ~/.ssh, /etc/passwd, id_rsa).
abstention no The final answer says the information could not be found. Off by default, because many tests expect a correct "not found".

Each check has clean look-alikes it must not fire on (a retried tool that then succeeds, a poll whose result changes, a model call retried successfully, an answer that merely mentions tokens). They are pinned in tests/test_checks.py.

Errors your test expects are not failures. If a model or tool error leaves the agent, because it is caught by pytest.raises, wrapped and re-raised into one, or reaches your test function, it does not fire the "unrecovered" checks. Only errors the agent itself swallowed do.

Named runs and explicit checks

import faultgate_zero as fz

def test_refund_respects_policy():
    with fz.run("refund-double-charge", user_id="cust_42") as run:
        run_agent("Refund order 1042")
    run.assert_clean()                       # raises fz.AgentCheckFailed with step-level reasons
    assert run.steps[0].tool == "verify_identity"
  • user_id is what the policy's identity_arg rule compares against.
  • A run your test checks itself (run.check(), run.report, run.assert_clean()) is left alone by the plugin, so you can assert that a check fires: assert not run.check(checks="policy").passed.
  • run.check(checks=..., skip=[...], policy="path.json") returns a result with passed, fired and verdicts.

Exact tool results: @fz.tool (optional)

@fz.tool
def issue_refund(order_id: str, amount: float) -> dict: ...

@fz.tool(name="lookup_order")      # the name must match the tool name the model calls
def lookup(order_id: str) -> dict: ...

Without the decorator, tool results are read from the next request the agent sends to the model, so the very last result of a run (never sent back) is unknown. The decorator records the call where it happens, with its exact result or exception, and takes the model's call id so it is never counted twice.

Providers called over raw HTTP

fz.record_model_call(messages=request_messages, response=chat_completion_dict, model="in-house")

Messages and response in OpenAI chat format. Outside a run it does nothing.

Policy file

.faultline/policy.json in the project root is picked up automatically.

{
  "allow": ["verify_identity", "lookup_order", "issue_refund"],
  "deny_patterns": ["../", "file://", "~/.ssh", "DROP TABLE"],
  "max_arg_chars": 4000,
  "max_calls": 20,
  "require_complete": true,
  "tools": {
    "issue_refund": {
      "max_calls": 1,
      "limits": {"amount": 500},
      "allowed_values": {"currency": ["USD", "EUR"]},
      "required_args": ["order_id", "amount"],
      "identity_arg": "customer_id",
      "requires": ["verify_identity"]
    }
  }
}
Key Meaning
allow Only these tools may be called.
deny_patterns Text that must not appear anywhere in a call's arguments: literal and case-insensitive, matched over the whole argument. Replaces the built-in list. Not regular expressions, on purpose: a regex run on model-written arguments can be made to take exponential time.
max_arg_chars, max_calls Per run: longest arguments, number of tool calls.
require_complete The run must end with an answer and no escaped exception.
tools.<name>.max_calls Calls of this tool per run.
tools.<name>.limits Numeric argument maxima; dotted paths reach nested keys (refund.amount). "$900.00" counts as 900.
tools.<name>.allowed_values The argument must be one of these.
tools.<name>.required_args These arguments must be present.
tools.<name>.identity_arg This argument must equal the run's user_id.
tools.<name>.requires These tools must have succeeded earlier in the run.

Unknown keys, empty denied text and wrong types stop the test session with a message naming the file, the key and the problem.

Settings

First found wins: the test's marker, a command-line option, an environment variable, [tool.faultgate_zero] in pyproject.toml, the default.

Setting Default Option / environment Meaning
mode fail --fgz=fail|warn|off / FAULTGATE_ZERO Fail the test, warn only (listed at the end of the run), or turn capture off.
checks the default set --fgz-checks / FAULTGATE_ZERO_CHECKS Comma-separated check names.
policy .faultline/policy.json if present --fgz-policy / FAULTGATE_ZERO_POLICY Policy file.
redact builtin --fgz-redact / FAULTGATE_ZERO_REDACT Mask emails, card numbers, API keys, bearer tokens and JWTs in every reason and report: builtin or off.
json, markdown, export_otlp none --fgz-json, --fgz-markdown, --fgz-export-otlp Reports, below.
loop_min_steps, repeat_threshold 5, 3 pyproject only Check thresholds.
# the agent template's pyproject.toml
[project.optional-dependencies]
dev = ["pytest>=8", "faultgate-zero>=0.1"]

[tool.faultgate_zero]
mode = "fail"
policy = ".faultline/policy.json"

Per test:

@pytest.mark.faultgate(skip=["unrecovered_tool_error"])   # this test exercises a tool failure on purpose
@pytest.mark.faultgate(policy="tests/policy_strict.json")
@pytest.mark.faultgate(checks="policy,abstention")
@pytest.mark.faultgate(mode="warn")
@pytest.mark.faultgate(off=True)

Reports

Option Writes
--fgz-json PATH One JSON report: {"tool", "version", "runs": [{"test", "scenario", "steps", "model_calls", "tokens", "latency_ms", "capture_complete", "skipped", "checked_by_test", "verdicts": [{"check", "fired", "reason", "step"}]}], "summary": {"runs", "failed_runs", "skipped_runs", "fired"}}. Test ids, run names and reasons are masked. A run the test checked itself is listed with checked_by_test: true and does not count as a failure.
--fgz-markdown PATH A table of failed runs, for $GITHUB_STEP_SUMMARY.
--fgz-export-otlp PATH The captured runs as OTLP/JSON with OpenTelemetry GenAI attributes. Contains raw prompts, answers and tool payloads (not redacted). faultgate_zero.otlp.load_otlp(PATH) reads it back.

Paths on the command line are relative to where pytest runs; in pyproject.toml, to the project root. With pytest-xdist (-n), workers send their results to the controller and one merged report is written.

Supported SDKs and frameworks

Tested here, with offline fakes that run the SDKs' own request and response code:

Package Tested version What is captured
openai 2.54.0 chat.completions.create / parse / stream, responses.create / parse / stream; sync and async; streaming; with_raw_response.
anthropic 1.17 messages.create, messages.stream, and the beta equivalents; sync and async; streaming; server tool results.
litellm 1.106.2 completion, acompletion (a call LiteLLM makes through the OpenAI SDK underneath is counted once).
langchain-openai / langchain-anthropic 1.7 ChatOpenAI, ChatAnthropic with bound tools.
opentelemetry-instrumentation-openai-v2 2.4b0 Runs alongside it; both record.
pytest-xdist 3.8 -n with merged reports.

Python 3.11, 3.12 and 3.13. Other frameworks that call these SDKs underneath are likely to work but are not claimed until a test covers them. See docs/known-limitations.md.

Development

pip install -e ".[dev,compat]"
pytest                                 # the repo's own suite; its pyproject turns the plugin off for itself

License

MIT.

Metadata

Release files for faultgate-zero 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 faultgate-zero 0.1.0
File Size Uploaded Provenance
faultgate_zero-0.1.0.tar.gz 80.4 kB With provenance Details

Built distribution (wheel)

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

Total release size: 131.4 kB

Release files / faultgate_zero-0.1.0.tar.gz

Download URL faultgate_zero-0.1.0.tar.gz
Size 80.4 kB
Tags Source
SHA-256 checksum
How to use checksums
bf0817d5b021d0625932be15546e912c539d8ecbf2341aff0a14ef984804275a
BLAKE2b-256 checksum
How to use checksums
6986061ab1f2afada1e79e3187cad8657f5583a64c2abdc483db0737c463e559
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 22, 2026.

Transparency log

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

Download URL faultgate_zero-0.1.0-py3-none-any.whl
Size 51.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
007250df791f2e1051e5748aeee8002a61d510c6047e7413dc4dba6acb4a7028
BLAKE2b-256 checksum
How to use checksums
bf9b57d6bb50e50a1dce5d374e672097de9e9cce45682cd5938c9e70448b887d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 22, 2026.

Transparency log

Security for faultgate-zero 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 faultgate_zero-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 Oct 22, 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.

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