Updated 2026-01-15 — Comprehensive guide to Sentience assertions: predicates, state-aware checks, retries, confidence gating, and optional vision fallback.
Think Jest for AI web agents. Take a browser snapshot (rendered DOM + layout), then evaluate predicates that return pass/fail with structured details.
In Sentience, an assertion is a predicate:
AssertContext (current snapshot, current url, and stepId)AssertOutcome (passed, reason, details)Predicates are pure functions — they read snapshot state and return AssertOutcome.
Runtime execution decides how and when to evaluate predicates (once vs retry, confidence gating, fallback, tracing).
pip install sentienceapifrom sentience import AsyncSentienceBrowser
from sentience.agent_runtime import AgentRuntime
from sentience.verification import exists
from sentience.tracing import Tracer, JsonlTraceSink
async with AsyncSentienceBrowser() as browser:
page = await browser.new_page()
await page.goto("https://example.com")
tracer = Tracer(run_id="demo-run", sink=JsonlTraceSink("trace.jsonl"))
runtime = await AgentRuntime.from_sentience_browser(browser=browser, page=page, tracer=tracer)
await runtime.snapshot()
runtime.assert_(exists("text~'Example Domain'"), label="example_domain_visible", required=True)assert is a Python keyword, so the Python API uses assert_ instead.
These predicates are for simple checks:
URL predicates:
url_matches, url_containsurlMatches, urlContainsElement predicates:
exists, not_exists, element_countexists, notExists, elementCountComposition:
all_of, any_of, customallOf, anyOf, customfrom sentience.verification import all_of, exists, url_contains
on_checkout = all_of(
url_contains("/checkout"),
exists("text~'Order summary'")
)
runtime.assert_(on_checkout, label="checkout_loaded", required=True)These predicates are deterministic checks against element state extracted from the live DOM:
is_enabled / isEnabled — enabled/disabledis_checked / isChecked — checked/uncheckedvalue_contains / valueContains — value equals/containsis_expanded / isExpanded — expanded/collapsedfrom sentience.verification import is_enabled, is_checked, value_contains
runtime.assert_(is_enabled("role=button text~'Continue'"), label="continue_enabled", required=True)
runtime.assert_(is_checked("role=checkbox name~'Accept terms'"), label="terms_checked", required=True)
runtime.assert_(value_contains("role=textbox name~'Email'", "@"), label="email_has_at_symbol").eventually()Sentience provides a fluent assertion handle:
.once() — evaluates a predicate once (same semantics as assert/assert_).eventually() — retries with fresh snapshots until success, timeout, or exhaustionok = await runtime.check(
exists("role=button text~'Continue'"),
label="continue_visible",
required=True,
).eventually(timeout_s=10, poll_s=0.25)If you pass minConfidence / min_confidence, .eventually() can treat low-confidence snapshots as failures and resnapshot.
ok = await runtime.check(
exists("text~'Payment method'"),
label="payment_ready",
required=True,
).eventually(
timeout_s=15,
poll_s=0.25,
min_confidence=0.7,
max_snapshot_attempts=3,
)If confidence stays below the threshold for maxSnapshotAttempts, the assertion ends with reason_code = "snapshot_exhausted" and includes snapshot.diagnostics for debugging.
After snapshot_exhausted, you can optionally provide a vision-capable LLMProvider and let Sentience ask a strict YES/NO verifier question using a screenshot.
ok = await runtime.check(
exists("text~'Order confirmed'"),
label="order_confirmed",
required=True,
).eventually(
min_confidence=0.8,
max_snapshot_attempts=2,
vision_provider=llm, # must supports_vision() and generate_with_image(...)
)The verifier question is derived from the assertion label. Vision is only used after structured snapshot attempts are exhausted — never by default.
Jest uses beforeAll/beforeEach/afterEach/afterAll. In Sentience, the idea is the same: create a browser + runtime once, snapshot as needed, then close.
# pytest-asyncio example
import pytest
from sentience import AsyncSentienceBrowser
from sentience.agent_runtime import AgentRuntime
from sentience.tracing import Tracer, JsonlTraceSink
from sentience.verification import exists
@pytest.fixture
async def runtime():
tracer = Tracer(run_id="pytest-run", sink=JsonlTraceSink("trace.jsonl"))
async with AsyncSentienceBrowser() as browser:
page = await browser.new_page()
rt = await AgentRuntime.from_sentience_browser(browser=browser, page=page, tracer=tracer)
yield rt, page
@pytest.mark.asyncio
async def test_homepage_renders(runtime):
rt, page = runtime
await page.goto("https://example.com")
await rt.snapshot()
rt.assert_(exists("text~'Example Domain'"), label="example_domain_visible", required=True)AgentRuntimeAgentRuntime is the recommended place to run assertions because it:
.check().eventually() for robust "wait until true" verificationWhen an assertion fails, Sentience includes structured details:
reason_code (e.g. no_match, state_mismatch, snapshot_low_confidence, snapshot_exhausted)nearest_matches (best-effort suggestions when a selector fails)Jest snapshots freeze output. Sentience snapshots model interactive state. They solve different problems, even though they share the word "snapshot".
Jest snapshots are essentially golden files:
Jest snapshot characteristics:
A Sentience snapshot is a runtime perception frame of a live browser:
Sentience snapshot characteristics:
| Aspect | Jest snapshot | Sentience snapshot |
|---|---|---|
| Purpose | Detect output changes | Enable reasoning & verification |
| Lifecycle | Stored & diffed | Ephemeral, per-step |
| Input | Render output / strings | Live browser state |
| Stability | Assumes determinism | Measures instability |
| Used for | Regression testing | Agent execution + QA verification |
| Handles SPAs | Poorly | Natively |
| Geometry / ordinality | No | Yes |
.eventually() for every assertion?No. A good rule of thumb:
assert/assert_ for stable, immediate checks (URL checks, "element exists", etc.).check(...).eventually() for anything that depends on async UI timing (SPAs, loading states, transitions, network-backed components).eventually() sometimes fail with snapshot_low_confidence / snapshot_exhausted?If you set minConfidence / min_confidence, the runtime treats low-confidence snapshots as unreliable and will resnapshot. After maxSnapshotAttempts / max_snapshot_attempts, it stops with snapshot_exhausted to avoid infinite retries and surfaces snapshot.diagnostics in details to help you debug instability.