A verification & control layer for AI agents that operate browsers
Sentience 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 sentienceapi
npx playwright install chromium- 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 { SentienceBrowser, AgentRuntime } from 'sentienceapi';
import { JsonlTraceSink, Tracer } from 'sentienceapi';
import { exists, urlContains } from 'sentienceapi';
import type { Page } from 'playwright';
async function main(): Promise<void> {
const tracer = new Tracer('demo', new JsonlTraceSink('trace.jsonl'));
const browser = new SentienceBrowser();
await browser.start();
const page = browser.getPage();
if (!page) throw new Error('no page');
await page.goto('https://example.com');
// AgentRuntime needs a snapshot provider; SentienceBrowser.snapshot() does not depend on Page,
// so we wrap it to fit the runtime interface.
const runtime = new AgentRuntime(
{ snapshot: async (_page: Page, options?: Record<string, any>) => browser.snapshot(options) },
page,
tracer
);
runtime.beginStep('Verify homepage');
await runtime.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');
await browser.close();
}
void main();If you already have an agent loop (LangGraph, custom planner/executor), keep it and attach Sentience as a verifier + trace layer.
Key idea: your agent still executes actions — Sentience snapshots and verifies outcomes.
import type { Page } from 'playwright';
import { SentienceDebugger, Tracer, JsonlTraceSink, exists, urlContains } from 'sentienceapi';
async function runExistingAgent(page: Page): Promise<void> {
const tracer = new Tracer('run-123', new JsonlTraceSink('trace.jsonl'));
const dbg = SentienceDebugger.attach(page, tracer);
await dbg.step('agent_step: navigate + verify', async () => {
// 1) Let your framework do whatever it does
await yourAgent.step();
// 2) Snapshot what the agent produced
await dbg.snapshot({ limit: 60 });
// 3) Verify outcomes (with bounded retries)
await dbg
.check(urlContains('example.com'), 'on_domain', true)
.eventually({ timeoutMs: 10_000 });
await dbg.check(exists('role=heading'), 'has_heading').eventually({ timeoutMs: 10_000 });
});
}If you want Sentience to drive the loop end-to-end, you can use the SDK primitives directly: take a snapshot, select elements, act, then verify.
import { SentienceBrowser, snapshot, find, typeText, click, waitFor } from 'sentienceapi';
async function loginExample(): Promise<void> {
const browser = new SentienceBrowser();
await browser.start();
const page = browser.getPage();
if (!page) throw new Error('no page');
await page.goto('https://example.com/login');
const snap = await snapshot(browser);
const email = find(snap, "role=textbox text~'email'");
const password = find(snap, "role=textbox text~'password'");
const submit = find(snap, "role=button text~'sign in'");
if (!email || !password || !submit) throw new Error('login form not found');
await typeText(browser, email.id, 'user@example.com');
await typeText(browser, password.id, 'password123');
await click(browser, submit.id);
const ok = await waitFor(browser, "role=heading text~'Dashboard'", 10_000);
if (!ok.found) throw new Error('login failed');
await browser.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(...)
- 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 'sentienceapi';
const registry = new ToolRegistry();
registerDefaultTools(registry);
const toolsForLLM = registry.llmTools();Chrome permission prompts are outside the DOM and can be invisible to snapshots. Prefer setting a policy before navigation.
import { SentienceBrowser } from 'sentienceapi';
import type { PermissionPolicy } from 'sentienceapi';
const policy: 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.
const browser = new SentienceBrowser(
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
false,
policy
);
await browser.start();If your backend supports it, you can also use ToolRegistry permission tools (grant_permissions, clear_permissions, set_geolocation) mid-run.
import { downloadCompleted } from 'sentienceapi';
runtime.assert(downloadCompleted('report.csv'), 'download_ok', true);- Manual driver CLI:
npx sentience driver --url https://example.com- Verification + artifacts + debugging with time-travel traces (Sentience 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
- Sentience SDK Documentation: https://www.sentienceapi.com/docs