Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions examples/runtime_agent_minimal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
Example: RuntimeAgent (AgentRuntime-backed) minimal demo.

This demonstrates the verification-first loop:
snapshot -> propose action (structured executor) -> execute -> verify (AgentRuntime predicates)

Usage:
python examples/runtime_agent_minimal.py
"""

import asyncio

from sentience import AsyncSentienceBrowser
from sentience.agent_runtime import AgentRuntime
from sentience.llm_provider import LLMProvider, LLMResponse
from sentience.runtime_agent import RuntimeAgent, RuntimeStep, StepVerification
from sentience.tracing import JsonlTraceSink, Tracer
from sentience.verification import AssertContext, AssertOutcome, exists, url_contains


class FixedActionProvider(LLMProvider):
"""A tiny in-process provider for examples/tests."""

def __init__(self, action: str):
super().__init__(model="fixed-action")
self._action = action

def generate(self, system_prompt: str, user_prompt: str, **kwargs) -> LLMResponse:
_ = system_prompt, user_prompt, kwargs
return LLMResponse(content=self._action, model_name=self.model_name)

def supports_json_mode(self) -> bool:
return False

@property
def model_name(self) -> str:
return "fixed-action"


async def main() -> None:
# Local trace (viewable in Studio if uploaded later).
run_id = "runtime-agent-minimal"
tracer = Tracer(run_id=run_id, sink=JsonlTraceSink(f"traces/{run_id}.jsonl"))

async with AsyncSentienceBrowser(headless=False) as browser:
page = await browser.new_page()
await page.goto("https://example.com")
await page.wait_for_load_state("networkidle")

runtime = await AgentRuntime.from_sentience_browser(browser=browser, page=page, tracer=tracer)

# Structured executor (for demo, we just return FINISH()).
executor = FixedActionProvider("FINISH()")

agent = RuntimeAgent(
runtime=runtime,
executor=executor,
# vision_executor=... (optional)
# vision_verifier=... (optional, for AgentRuntime assertion vision fallback)
)

# One step: no action needed; we just verify structure + URL.
def has_example_heading(ctx: AssertContext) -> AssertOutcome:
# Demonstrates custom predicates (you can also use exists/url_contains helpers).
snap = ctx.snapshot
ok = bool(snap and any((el.role == "heading" and (el.text or "").startswith("Example")) for el in snap.elements))
return AssertOutcome(passed=ok, reason="" if ok else "missing heading", details={})

step = RuntimeStep(
goal="Confirm Example Domain page is loaded",
verifications=[
StepVerification(predicate=url_contains("example.com"), label="url_contains_example", required=True),
StepVerification(predicate=exists("role=heading"), label="has_heading", required=True),
StepVerification(predicate=has_example_heading, label="heading_text_matches", required=False),
],
max_snapshot_attempts=2,
snapshot_limit_base=60,
)

ok = await agent.run_step(task_goal="Open example.com and verify", step=step)
print(f"step ok: {ok}")

tracer.close()
print(f"trace written to traces/{run_id}.jsonl")


if __name__ == "__main__":
asyncio.run(main())

4 changes: 4 additions & 0 deletions sentience/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
from .query import find, query
from .read import read
from .recorder import Recorder, Trace, TraceStep, record
from .runtime_agent import RuntimeAgent, RuntimeStep, StepVerification
from .screenshot import screenshot
from .sentience_methods import AgentAction, SentienceMethod
from .snapshot import snapshot
Expand Down Expand Up @@ -210,6 +211,9 @@
"MLXVLMProvider",
"SentienceAgent",
"SentienceAgentAsync",
"RuntimeAgent",
"RuntimeStep",
"StepVerification",
"SentienceVisualAgent",
"SentienceVisualAgentAsync",
"ConversationalAgent",
Expand Down
6 changes: 3 additions & 3 deletions sentience/extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ async function handleSnapshotProcessing(rawData, options = {}) {
const startTime = performance.now();
try {
if (!Array.isArray(rawData)) throw new Error("rawData must be an array");
if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(),
if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(),
!wasmReady) throw new Error("WASM module not initialized");
let analyzedElements, prunedRawData;
try {
const wasmPromise = new Promise((resolve, reject) => {
try {
let result;
result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData),
result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData),
resolve(result);
} catch (e) {
reject(e);
Expand Down Expand Up @@ -101,4 +101,4 @@ initWASM().catch(err => {}), chrome.runtime.onMessage.addListener((request, send
event.preventDefault();
}), self.addEventListener("unhandledrejection", event => {
event.preventDefault();
});
});
18 changes: 9 additions & 9 deletions sentience/extension/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
if (!elements || !Array.isArray(elements)) return;
removeOverlay();
const host = document.createElement("div");
host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ",
host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ",
document.body.appendChild(host);
const shadow = host.attachShadow({
mode: "closed"
Expand All @@ -94,15 +94,15 @@
let color;
color = isTarget ? "#FF0000" : isPrimary ? "#0066FF" : "#00FF00";
const importanceRatio = maxImportance > 0 ? importance / maxImportance : .5, borderOpacity = isTarget ? 1 : isPrimary ? .9 : Math.max(.4, .5 + .5 * importanceRatio), fillOpacity = .2 * borderOpacity, borderWidth = isTarget ? 2 : isPrimary ? 1.5 : Math.max(.5, Math.round(2 * importanceRatio)), hexOpacity = Math.round(255 * fillOpacity).toString(16).padStart(2, "0"), box = document.createElement("div");
if (box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px solid ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${borderOpacity};\n pointer-events: none;\n `,
if (box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px solid ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${borderOpacity};\n pointer-events: none;\n `,
importance > 0 || isPrimary) {
const badge = document.createElement("span");
badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `,
badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `,
box.appendChild(badge);
}
if (isTarget) {
const targetIndicator = document.createElement("span");
targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ",
targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ",
box.appendChild(targetIndicator);
}
shadow.appendChild(box);
Expand All @@ -122,7 +122,7 @@
if (!grids || !Array.isArray(grids)) return;
removeOverlay();
const host = document.createElement("div");
host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ",
host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ",
document.body.appendChild(host);
const shadow = host.attachShadow({
mode: "closed"
Expand All @@ -138,10 +138,10 @@
let labelText = grid.label ? `Grid ${grid.grid_id}: ${grid.label}` : `Grid ${grid.grid_id}`;
grid.is_dominant && (labelText = `⭐ ${labelText} (dominant)`);
const badge = document.createElement("span");
if (badge.textContent = labelText, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `,
if (badge.textContent = labelText, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `,
box.appendChild(badge), isTarget) {
const targetIndicator = document.createElement("span");
targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ",
targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ",
box.appendChild(targetIndicator);
}
shadow.appendChild(box);
Expand All @@ -155,7 +155,7 @@
let overlayTimeout = null;
function removeOverlay() {
const existing = document.getElementById(OVERLAY_HOST_ID);
existing && existing.remove(), overlayTimeout && (clearTimeout(overlayTimeout),
existing && existing.remove(), overlayTimeout && (clearTimeout(overlayTimeout),
overlayTimeout = null);
}
}();
}();
Loading
Loading