Skip to main content

nabit — nab your agent's silent failures

PyPI version Python License: MIT

Your LLM agent said it created the customer. Your database says otherwise. You found out three days later from a support ticket.

nabit is a dead-simple verification layer for LLM agents. Your agent claims it's done; nabit checks the real system state, tells you whether that was true, and — if it wasn't — re-runs the action to fix it. One decorator. Zero dependencies. Sync or async.

from nabit import verify

@verify(lambda result, ctx: db.exists("customers", result["id"]),
        retries=2)                      # self-heal: re-run on failure
def create_customer(name):
    # agent / tool does the work and claims success
    return {"id": 42, "status": "created"}

If the agent returns {"status": "created"} but the row isn't in the database, nabit catches the lie instead of letting a green dashboard hide it — then retries the action up to retries times before giving up.

How it's different

Most tools in this space either detect problems without fixing them, only check the text the model produced (not whether the real action happened), or make you stand up a backend to do it. nabit:

  • checks real side effects, not output schema (vs Guardrails AI / Instructor)
  • closes the loop — self-heal retries, not just detection (vs Drift / trace viewers)
  • verifies inline at runtime, not in a postmortem (vs agent-coroner)
  • has zero dependencies and no backend — it's a decorator, not a platform (vs COGEXT)

Why this exists

Trace viewers (Langfuse, LangSmith, Helicone) answer "what did the agent do?" really well. They don't answer "was what it did actually correct?"

The expensive failures are semantic, not technical: no exception thrown, the tool call succeeded, and the output was still wrong. Those look identical to a success at the log level. nabit is the layer that checks the claim against reality.

Install

pip install nabit

Usage

Post-conditions check reality, not the agent's word

A post-condition is (result, context) -> bool. result is what the function returned; context is the bound call arguments (so you can compare inputs to outputs). Return truthy if reality matches the claim.

from nabit import verify

@verify(lambda result, ctx: ticket_store.is_closed(result["ticket_id"]))
def close_ticket(ticket_id):
    agent.act(f"close ticket {ticket_id}")
    return {"ticket_id": ticket_id, "status": "closed"}

Modes — tune how loud failures are

from nabit import verify, Mode

@verify(check, mode=Mode.WARN)    # log a warning, keep running  (default — safe for prod)
@verify(check, mode=Mode.RAISE)   # raise VerificationError      (great for tests / CI)
@verify(check, mode=Mode.LOG)     # info-level log only
@verify(check, mode=Mode.SILENT)  # record only; inspect later

Self-heal — re-run the action when verification fails

def feedback(attempt, last_result, ctx):
    # optional: nudge the agent before the next attempt
    log.warning("verification failed on attempt %d, retrying", attempt)

@verify(check, retries=3, backoff=0.5, on_retry=feedback)
def book_flight(req):
    ...

retries re-runs the whole action up to N times until the post-condition passes; backoff adds linear delay between attempts; on_retry lets you feed the discrepancy back to the agent. Detection and correction, in one decorator.

Composable checks (no hand-written lambdas)

from nabit import verify
from nabit.checks import (all_of, has_keys, field_equals, file_fresh, http_ok,
                         truthy, min_length, contains, matches, in_range)

@verify(all_of(has_keys("id", "status"), field_equals("status", "created")))
def create(...): ...

@verify(file_fresh(lambda r, c: r["path"], max_age_s=60))   # cron wrote a fresh file?
def nightly_report(): ...

@verify(http_ok(lambda r, c: r["url"]))                     # deployed URL is live?
def deploy(): ...

@verify(truthy())                                           # not [] / "" / None
def search(...): ...

# an agent's "reportable" finding must carry real proof, not a one-line claim
@verify(all_of(min_length(100, key="evidence"), matches(r"https?://", key="evidence")))
def finish_task(finding): ...

@verify(in_range(1, 10, key="score"))                       # LLM judge score in spec
def judge(...): ...

Full check list: all_of / any_of / not_, has_keys, equals, field_equals, truthy, non_empty, min_length, contains, matches, in_range, file_exists, file_fresh, http_ok, predicate.

Group verifications under a run id

from nabit import run, summary

with run("signup-flow") as rid:
    create_customer(...)
    charge_card(...)

print(summary(run_id=rid))
# {'total': 2, 'passed': 1, 'failed': 1, 'pass_rate': 0.5, 'failures': ['charge_card']}

Solves the "nothing shares a run id" problem — all the checks for one logical task carry the same id.

Tee results anywhere (pluggable sinks, still zero-dep)

import nabit
from nabit import jsonl_sink

nabit.add_sink(jsonl_sink("verifications.jsonl"))   # built-in

def otel_sink(r):                                   # or your own, 3 lines
    span.set_attribute("nabit.passed", r.passed)
nabit.add_sink(otel_sink)

LangGraph / LangChain

nabit works with any framework via the decorator, but LangGraph users get a first-class adapter (lazily imported — installing nabit never pulls in LangChain). Two options:

Passive callback — add it once, verify every tool result, no restructure:

from nabit.adapters.langgraph import NabitCallback
from nabit.checks import has_keys, truthy

cb = NabitCallback(checks={
    "create_customer": has_keys("id"),
    "search_hotels": truthy(),        # catches the empty-list silent failure
})
graph.invoke(state, config={"callbacks": [cb]})
print(cb.summary())

Node wrapper — wrap a node to get full self-heal (a node is just a function, so it can be re-run):

from nabit.adapters.langgraph import verify_node
from nabit.checks import predicate

builder.add_node("book", verify_node(
    book_node,
    predicate(lambda update, ctx: db.has_booking(update["booking_id"])),
    retries=2,
))

Async works the same way

@verify(pc, retries=2)                    # async funcs + async post-conditions
async def create_order(cart):
    ...

Inspect what happened

from nabit import get_results

for r in get_results():
    print(r.name, "PASS" if r.passed else "FAIL",
          f"{r.duration_ms:.0f}ms", f"attempts={r.attempts}", r.error)

Also catches "it threw but the side effect still happened"

By default (on_error=True) the post-condition runs even when the wrapped function raises — so a tool that errors after mutating state (or succeeds in reality despite throwing) still gets verified. The original exception is re-raised after recording.

Scope

nabit is the outcome verifier: it answers "did this specific action actually happen?" and corrects it when it didn't. It deliberately does not try to be an observability platform. For fleet-wide behavioral monitoring — real-time degradation detection across many runs (step-count blowups, token spikes, slow drift over hundreds of runs), trajectory snapshot diffing, and a dashboard — see The Production Agent Reliability Kit, built on published research (SENTINEL, self-supervised anomaly detection for LLM agents).

Rule of thumb: use nabit to verify one action's real effect inline; reach for the Kit when you need to watch patterns across runs over time.

License

MIT — see LICENSE. Use it anywhere, including commercially.


Built by Jake Garnier.

Metadata

Release files for nabit 0.3.1

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

Source distribution (sdist)

Source distribution for nabit 0.3.1
File Size Uploaded
nabit-0.3.1.tar.gz 21.0 kB Details

Built distribution (wheel)

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

Total release size: 39.4 kB

Release files / nabit-0.3.1.tar.gz

Download URL nabit-0.3.1.tar.gz
Size 21.0 kB
Tags Source
SHA-256 checksum
How to use checksums
45b4dd6b4e3b5c80f7a5bc222068d664a1d986546a296f25af45f43568add869
BLAKE2b-256 checksum
How to use checksums
f3cb66a4f9e3d53736d1c34d3633c37d8f46089986143ea27b09942dbda14f29
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release files / nabit-0.3.1-py3-none-any.whl

Download URL nabit-0.3.1-py3-none-any.whl
Size 18.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
81c9bdbf7a3f9f8be585f3e25cd521f7621e5f23eac022d20ba9f1eaa4fdbf60
BLAKE2b-256 checksum
How to use checksums
1aaaa4db98e6c4b612a3a3c16f3e8697e011f2d3b5974fb8bfd8a2a7a62bd45c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release history Release notifications | RSS feed

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

This release

0.3.1 This release

2 release files

0.3.0

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