Skip to content

Repository files navigation

Predicate Python SDK

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

What Predicate is

  • A verification-first runtime (AgentRuntime) for browser agents
  • Treats the browser as an adapter (Playwright / CDP / browser-use); AgentRuntime is 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>)

What Predicate is not

  • Not a browser driver
  • Not a Playwright replacement
  • Not a vision-first agent framework

Install

pip install predicate-runtime
playwright install chromium

Legacy install compatibility remains available through the shim package:

pip install predicate-sdk

If you’re developing from source (this repo), install the local checkout instead:

pip install -e .
playwright install chromium

Conceptual example (why this exists)

In 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

Quickstart: a verification-first loop

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())

PredicateDebugger: attach to your existing agent framework (sidecar mode)

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)

SDK-driven full loop (snapshots + actions)

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")

Pre-action authority hook (production pattern)

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: bool field (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
)

Capabilities (lifecycle guarantees)

Controlled perception

  • Semantic snapshots instead of raw DOM dumps
  • Pruning knobs via SnapshotOptions (limit/filter)
  • Snapshot diagnostics that help decide when “structure is insufficient”

Constrained action space

  • Action primitives operate on stable IDs / rects derived from snapshots
  • Optional helpers for ordinality (“click the 3rd result”)

Verified progress

  • Predicates like exists(...), url_matches(...), is_enabled(...), value_equals(...)
  • Fluent assertion DSL via expect(...)
  • Retrying verification via runtime.check(...).eventually(...)

Scroll verification (prevent no-op scroll drift)

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).")

Explained failure

  • 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

Framework interoperability

  • Bring your own LLM and orchestration (LangGraph, AutoGen, custom loops)
  • Register explicit LLM-callable tools with ToolRegistry

ToolRegistry (LLM-callable tools)

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()

Permissions (avoid Chrome permission bubbles)

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.

Downloads (verification predicate)

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)

Debugging (fast)

  • 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

Integrations (examples)

About

A verification-first runtime for AI web agents — with Jest-style assertions and token-efficient snapshots

Topics

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages