jevrun
Version:
Run natural-language browser tasks on a Playwright page using Jev decisions.
71 lines • 4.38 kB
JavaScript
import { literals, targets, actionsFor } from "./actions.js";
import { decide } from "./jev.js";
const running = new WeakSet();
/** Execute a prompt against a caller-owned page. Throws on uncertainty or failure. */
export async function run(page, prompt, options = {}) {
if (typeof prompt !== "string" || !prompt.trim())
throw new Error("Prompt must be a non-empty string.");
if (prompt.length > 10_000)
throw new Error("Prompt exceeds the 10,000-character limit.");
const apiKey = options.apiKey ?? process.env.TYPESAFE_API_KEY;
if (!apiKey?.trim())
throw new Error("Set TYPESAFE_API_KEY or pass options.apiKey.");
const maxSteps = options.maxSteps ?? 10;
const minConfidence = options.minConfidence ?? 0.7;
const timeoutMs = options.timeoutMs ?? 30_000;
const model = options.model ?? "jev-latest";
if (!Number.isInteger(maxSteps) || maxSteps < 1 || maxSteps > 100)
throw new Error("maxSteps must be an integer between 1 and 100.");
if (!Number.isFinite(minConfidence) || minConfidence < 0 || minConfidence > 1)
throw new Error("minConfidence must be between 0 and 1.");
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2_147_483_647)
throw new Error("timeoutMs must be a positive 32-bit integer.");
if (!model.trim())
throw new Error("model must be non-empty.");
if (page.isClosed())
throw new Error("Page is closed.");
if (running.has(page))
throw new Error("A run is already in progress on this page.");
running.add(page);
const steps = [];
try {
const values = literals(prompt);
// One final observation is allowed after the last action to verify completion.
for (let step = 0; step <= maxSteps; step++) {
const snapshot = await page.locator("body").ariaSnapshot({ mode: "ai", timeout: timeoutMs });
if (snapshot.length > 40_000)
throw new Error("Page snapshot exceeds the 40,000-character limit.");
const elements = targets(snapshot);
const state = { prompt, url: page.url(), page: snapshot, history: steps };
const config = { apiKey, model, timeoutMs, minConfidence };
const target = await decide(state, {
...elements,
done: "The entire user task is already satisfied, based on the current page and successful history.",
blocked: "Cannot proceed: required information or supported element is missing.",
}, config, "Choose the element to interact with next to fulfill the user's prompt. Choose done only if the entire task is satisfied; otherwise choose blocked when no element fits.");
if (target.choice === "done")
return { status: "completed", steps };
if (target.choice === "blocked")
throw new Error("Jev could not find an element to complete the prompt.");
if (step === maxSteps)
throw new Error(`Task did not finish within ${maxSteps} actions.`);
const locator = page.locator(`aria-ref=${target.choice}`);
const actions = await actionsFor(locator, values, timeoutMs);
const criteria = { blocked: "None of the available actions can fulfill the next step. Required input may be missing." };
actions.forEach((action, index) => { criteria[`action_${index}`] = action.description; });
const decision = await decide({ ...state, selectedElement: elements[target.choice] ?? target.choice }, criteria, config, "Choose the next action on the selected element that advances the user's prompt. Use blocked if none fits. Literal values must come from the user.");
if (decision.choice === "blocked")
throw new Error("No supported action fits. Supply literal input text in quotes or simplify the task.");
const action = actions[Number(decision.choice.slice(7))];
if (!action)
throw new Error("Jev selected an unavailable action.");
await action.execute();
steps.push({ action: action.description, confidence: Math.min(target.confidence, decision.confidence) });
}
throw new Error("Task did not finish.");
}
finally {
running.delete(page);
}
}
//# sourceMappingURL=index.js.map