A verification & control layer for AI agents that operate browsers
Predicate is built for AI agent developers who already use Playwright / CDP / browser-use / LangGraph and care about flakiness, cost, determinism, evals, and debugging.
Often described as Jest for Browser AI Agents - but applied to end-to-end agent runs (not unit tests).
The core loop is:
Agent → Snapshot → Action → Verification → Artifact
- A verification-first runtime (
AgentRuntime) for browser agents - Treats the browser as an adapter (Playwright / CDP / browser-use);
AgentRuntimeis the product - A controlled perception layer (semantic snapshots; pruning/limits; lowers token usage by filtering noise from what models see)
- A debugging layer (structured traces + failure artifacts)
- Enables local LLM small models (3B-7B) for browser automation (privacy, compliance, and cost control)
- Keeps vision models optional (use as a fallback when DOM/snapshot structure falls short, e.g.
<canvas>)
- Not a browser driver
- Not a Playwright replacement
- Not a vision-first agent framework
pip install predicate-runtime
playwright install chromiumLegacy install compatibility remains available through the shim package:
pip install predicate-sdkIf you’re developing from source (this repo), install the local checkout instead:
pip install -e .
playwright install chromiumIn Predicate, agents don’t “hope” an action worked.
- Every step is gated by verifiable UI assertions
- If progress can’t be proven, the run fails with evidence (trace + artifacts)
- This is how you make runs reproducible and debuggable, and how you run evals reliably
This is the smallest useful pattern: snapshot → assert → act → assert-done.
importasynciofrompredicateimportAgentRuntime, AsyncPredicateBrowserfrompredicate.tracingimportJsonlTraceSink, Tracerfrompredicate.verificationimportexists, url_containsasyncdefmain() ->None:
tracer=Tracer(run_id="demo", sink=JsonlTraceSink("trace.jsonl"))
asyncwithAsyncPredicateBrowser() asbrowser:
page=awaitbrowser.new_page()
awaitpage.goto("https://example.com")
runtime=awaitAgentRuntime.from_sentience_browser(
browser=browser,
page=page,
tracer=tracer,
)
runtime.begin_step("Verify homepage")
awaitruntime.snapshot()
runtime.assert_(url_contains("example.com"), label="on_domain", required=True)
runtime.assert_(exists("role=heading"), label="has_heading")
runtime.assert_done(exists("text~'Example'"), label="task_complete")
if__name__=="__main__":
asyncio.run(main())If you already have an agent loop (LangGraph, browser-use, custom planner/executor), you can keep it and attach Predicate as a verifier + trace layer.
Key idea: your agent still decides and executes actions — Predicate snapshots and verifies outcomes.
frompredicateimportPredicateDebugger, create_tracerfrompredicate.verificationimportexists, url_containsasyncdefrun_existing_agent(page) ->None:
# page: playwright.async_api.Page (owned by your agent/framework)tracer=create_tracer(run_id="run-123") # local JSONL by defaultdbg=PredicateDebugger.attach(page, tracer=tracer)
asyncwithdbg.step("agent_step: navigate + verify"):
# 1) Let your framework do whatever it doesawaityour_agent.step()
# 2) Snapshot what the agent producedawaitdbg.snapshot()
# 3) Verify outcomes (with bounded retries)awaitdbg.check(url_contains("example.com"), label="on_domain", required=True).eventually(timeout_s=10)
awaitdbg.check(exists("role=heading"), label="has_heading").eventually(timeout_s=10)If you want Predicate to drive the loop end-to-end, you can use the SDK primitives directly: take a snapshot, select elements, act, then verify.
frompredicateimportPredicateBrowser, snapshot, find, click, type_text, wait_fordeflogin_example() ->None:
withPredicateBrowser() asbrowser:
browser.page.goto("https://example.com/login")
snap=snapshot(browser)
email=find(snap, "role=textbox text~'email'")
password=find(snap, "role=textbox text~'password'")
submit=find(snap, "role=button text~'sign in'")
ifnot (emailandpasswordandsubmit):
raiseRuntimeError("login form not found")
type_text(browser, email.id, "user@example.com")
type_text(browser, password.id, "password123")
click(browser, submit.id)
# Verify successok=wait_for(browser, "role=heading text~'Dashboard'", timeout=10.0)
ifnotok.found:
raiseRuntimeError("login failed")If you want every action proposal to be authorized before execution, pass a
pre_action_authorizer into RuntimeAgent.
This hook receives a shared predicate-contractsActionRequest generated from
runtime state (snapshot + assertion evidence) and must return either:
True/False, or- an object with an
allowed: boolfield (for richer decision payloads).
frompredicate.agent_runtimeimportAgentRuntimefrompredicate.runtime_agentimportRuntimeAgent, RuntimeStep# Optional: your authority client can be local guard, sidecar client, or remote API client.defpre_action_authorizer(action_request):
# Example: call your authority service# resp = authority_client.authorize(action_request)# return respreturnTrueruntime=AgentRuntime(backend=backend, tracer=tracer)
agent=RuntimeAgent(
runtime=runtime,
executor=executor,
pre_action_authorizer=pre_action_authorizer,
authority_principal_id="agent:web-checkout",
authority_tenant_id="tenant-a",
authority_session_id="session-123",
authority_fail_closed=True, # deny/authorizer errors block action execution
)
ok=awaitagent.run_step(
task_goal="Complete checkout",
step=RuntimeStep(goal="Click submit order"),
)Fail-open option (not recommended for sensitive actions):
agent=RuntimeAgent(
runtime=runtime,
executor=executor,
pre_action_authorizer=pre_action_authorizer,
authority_fail_closed=False, # authorizer errors allow action to proceed
)- Semantic snapshots instead of raw DOM dumps
- Pruning knobs via
SnapshotOptions(limit/filter) - Snapshot diagnostics that help decide when “structure is insufficient”
- Action primitives operate on stable IDs / rects derived from snapshots
- Optional helpers for ordinality (“click the 3rd result”)
- Predicates like
exists(...),url_matches(...),is_enabled(...),value_equals(...) - Fluent assertion DSL via
expect(...) - Retrying verification via
runtime.check(...).eventually(...)
A common agent failure mode is “scrolling” without the UI actually advancing (overlays, nested scrollers, focus issues). Use AgentRuntime.scroll_by(...) to deterministically verify scroll had effect via before/after scrollTop.
runtime.begin_step("Scroll the page and verify it moved")
ok=awaitruntime.scroll_by(
600,
verify=True,
min_delta_px=50,
label="scroll_effective",
required=True,
timeout_s=5.0,
)
ifnotok:
raiseRuntimeError("Scroll had no effect (likely blocked by overlay or nested scroller).")- JSONL trace events (
Tracer+JsonlTraceSink) - Optional failure artifact bundles (snapshots, diagnostics, step timelines, frames/clip)
- Deterministic failure semantics: when required assertions can’t be proven, the run fails with artifacts you can replay
- Bring your own LLM and orchestration (LangGraph, AutoGen, custom loops)
- Register explicit LLM-callable tools with
ToolRegistry
Predicate can expose a typed tool surface for agents (with tool-call tracing).
frompredicate.toolsimportToolRegistry, register_default_toolsregistry=ToolRegistry()
register_default_tools(registry, runtime) # or pass a ToolContext# LLM-ready tool specstools_for_llm=registry.llm_tools()Chrome permission prompts are outside the DOM and can be invisible to snapshots. Prefer setting a policy before navigation.
frompredicateimportAsyncPredicateBrowser, PermissionPolicypolicy=PermissionPolicy(
default="clear",
auto_grant=["geolocation"],
geolocation={"latitude": 37.77, "longitude": -122.41, "accuracy": 50},
origin="https://example.com",
)
asyncwithAsyncPredicateBrowser(permission_policy=policy) asbrowser:
...If your backend supports it, you can also use ToolRegistry permission tools (grant_permissions, clear_permissions, set_geolocation) mid-run.
If a flow is expected to download a file, assert it explicitly:
frompredicate.verificationimportdownload_completedruntime.assert_(download_completed("report.csv"), label="download_ok", required=True)- Manual driver CLI (inspect clickables, click/type/press quickly):
predicate driver --url https://example.com- Verification + artifacts + debugging with time-travel traces (Predicate Studio demo):
ss_studio_small.mp4
If the video tag doesn’t render in your GitHub README view, use this link: sentience-studio-demo.mp4
- Predicate SDK Documentation: https://predicatelabs.dev/docs
- Browser-use:examples/browser-use
- LangChain:examples/lang-chain
- LangGraph:examples/langgraph
- Pydantic AI:examples/pydantic_ai