UNPKG

jevrun

Version:

Run natural-language browser tasks on a Playwright page using Jev decisions.

71 lines • 4.38 kB
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