A verification & control layer for AI agents that operate browsers
Predicate is built for AI agent developers who already use Playwright / CDP / 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);
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
npm install @predicatesystems/runtime
npx playwright install chromiumLegacy install compatibility remains available through the shim package:
npm install @predicatesystems/sdkUse the new Predicate* class names for all new code:
PredicateBrowserPredicateAgentPredicateVisualAgentPredicateDebuggerbackends.PredicateContext
- Steps are gated by verifiable UI assertions
- If progress can’t be proven, the run fails with evidence
- This is how you make runs reproducible and debuggable, and how you run evals reliably
import{PredicateBrowser,AgentRuntime}from'@predicatesystems/runtime';import{JsonlTraceSink,Tracer}from'@predicatesystems/runtime';import{exists,urlContains}from'@predicatesystems/runtime';importtype{Page}from'playwright';asyncfunctionmain(): Promise<void>{consttracer=newTracer('demo',newJsonlTraceSink('trace.jsonl'));constbrowser=newPredicateBrowser();awaitbrowser.start();constpage=browser.getPage();if(!page)thrownewError('no page');awaitpage.goto('https://example.com');// AgentRuntime needs a snapshot provider; PredicateBrowser.snapshot() does not depend on Page,// so we wrap it to fit the runtime interface.construntime=newAgentRuntime({snapshot: async(_page: Page,options?: Record<string,any>)=>browser.snapshot(options)},page,tracer);runtime.beginStep('Verify homepage');awaitruntime.snapshot({limit: 60});runtime.assert(urlContains('example.com'),'on_domain',true);runtime.assert(exists('role=heading'),'has_heading');runtime.assertDone(exists("text~'Example'"),'task_complete');awaitbrowser.close();}voidmain();If you already have an agent loop (LangGraph, custom planner/executor), keep it and attach Predicate as a verifier + trace layer.
Key idea: your agent still executes actions — Predicate snapshots and verifies outcomes.
importtype{Page}from'playwright';import{PredicateDebugger,Tracer,JsonlTraceSink,exists,urlContains,}from'@predicatesystems/runtime';asyncfunctionrunExistingAgent(page: Page): Promise<void>{consttracer=newTracer('run-123',newJsonlTraceSink('trace.jsonl'));constdbg=PredicateDebugger.attach(page,tracer);awaitdbg.step('agent_step: navigate + verify',async()=>{// 1) Let your framework do whatever it doesawaityourAgent.step();// 2) Snapshot what the agent producedawaitdbg.snapshot({limit: 60});// 3) Verify outcomes (with bounded retries)awaitdbg.check(urlContains('example.com'),'on_domain',true).eventually({timeoutMs: 10_000});awaitdbg.check(exists('role=heading'),'has_heading').eventually({timeoutMs: 10_000});});}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.
import{PredicateBrowser,snapshot,find,typeText,click,waitFor,}from'@predicatesystems/runtime';asyncfunctionloginExample(): Promise<void>{constbrowser=newPredicateBrowser();awaitbrowser.start();constpage=browser.getPage();if(!page)thrownewError('no page');awaitpage.goto('https://example.com/login');constsnap=awaitsnapshot(browser);constemail=find(snap,"role=textbox text~'email'");constpassword=find(snap,"role=textbox text~'password'");constsubmit=find(snap,"role=button text~'sign in'");if(!email||!password||!submit)thrownewError('login form not found');awaittypeText(browser,email.id,'user@example.com');awaittypeText(browser,password.id,'password123');awaitclick(browser,submit.id);constok=awaitwaitFor(browser,"role=heading text~'Dashboard'",10_000);if(!ok.found)thrownewError('login failed');awaitbrowser.close();}- 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(...),urlMatches(...),isEnabled(...),valueEquals(...) - 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.scrollBy(...) to deterministically verify scroll had effect via before/after scrollTop.
runtime.beginStep('Scroll the page and verify it moved');constok=awaitruntime.scrollBy(600,{verify: true,minDeltaPx: 50,label: 'scroll_effective',required: true,timeoutMs: 5_000,});if(!ok){thrownewError('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, custom loops)
- Register explicit LLM-callable tools with
ToolRegistry
import{ToolRegistry,registerDefaultTools}from'@predicatesystems/runtime';constregistry=newToolRegistry();registerDefaultTools(registry);consttoolsForLLM=registry.llmTools();Chrome permission prompts are outside the DOM and can be invisible to snapshots. Prefer setting a policy before navigation.
import{PredicateBrowser}from'@predicatesystems/runtime';importtype{PermissionPolicy}from'@predicatesystems/runtime';constpolicy: PermissionPolicy={default: 'clear',autoGrant: ['geolocation'],geolocation: {latitude: 37.77,longitude: -122.41,accuracy: 50},origin: 'https://example.com',};// `permissionPolicy` is the last constructor argument; pass `keepAlive` right before it.constbrowser=newPredicateBrowser(undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,false,policy);awaitbrowser.start();If your backend supports it, you can also use ToolRegistry permission tools (grant_permissions, clear_permissions, set_geolocation) mid-run.
import{downloadCompleted}from'@predicatesystems/runtime';runtime.assert(downloadCompleted('report.csv'),'download_ok',true);- Manual driver CLI:
npx 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