import type { AdapterContext, BrowserActKind, BrowserActStep, BrowserProvider, ComputerRef, } from "@rakazo/adapter-kit"; import type { ComputerPointerPayload } from "@rakazo/contracts"; import { type BrowserPointerTargets, withoutBrowserGeometry } from "./computer-pointer.js"; /** Optional hooks that let the visible bot cursor follow page-browser actions. */ export interface BrowserPointerHooks { targets: BrowserPointerTargets; announce: (payload: ComputerPointerPayload) => Promise; } const MAX_BROWSER_ACTIONS = 24; export async function browserNavigateFromTool( browser: BrowserProvider, computer: ComputerRef, context: AdapterContext, args: Record, pointer?: BrowserPointerHooks, ) { const url = String(args.url ?? "").trim(); try { const parsed = new URL(url); if (!/^https?:$/.test(parsed.protocol) || parsed.username || parsed.password) { return { error: "An HTTP(S) URL without embedded credentials is required", fallback: "computer_act" as const, }; } pointer?.targets.forget(); const result = await browser.navigate(computer, { url, signal: context.signal }, context); return formatBrowserResult(result); } catch (error) { context.signal.throwIfAborted(); return { error: error instanceof Error ? error.message : String(error), fallback: "computer_act" as const, }; } } export async function browserSnapshotFromTool( browser: BrowserProvider, computer: ComputerRef, context: AdapterContext, args: Record, pointer?: BrowserPointerHooks, ) { void args; try { const result = await browser.snapshot(computer, { signal: context.signal }, context); pointer?.targets.remember(result); return formatBrowserResult(withoutBrowserGeometry(result)); } catch (error) { context.signal.throwIfAborted(); return { error: error instanceof Error ? error.message : String(error), fallback: "computer_act" as const, }; } } export async function browserActFromTool( browser: BrowserProvider, computer: ComputerRef, context: AdapterContext, args: Record, pointer?: BrowserPointerHooks, ) { try { const actions = parseBrowserActions(args.actions); const announcement = pointer?.targets.payloadFor(actions); if (announcement) await pointer?.announce(announcement); const result = await browser.act(computer, { actions, signal: context.signal }, context); if (result.elements) pointer?.targets.remember(result); else pointer?.targets.forget(); return formatBrowserResult(withoutBrowserGeometry(result)); } catch (error) { context.signal.throwIfAborted(); return { ok: false, uncertain: true, error: error instanceof Error ? error.message : String(error), fallback: "computer_act" as const, }; } } export function parseBrowserActions(value: unknown): BrowserActStep[] { if (!Array.isArray(value) || value.length === 0) { throw new Error("browser_act requires at least one action"); } if (value.length > MAX_BROWSER_ACTIONS) { throw new Error(`browser_act accepts at most ${MAX_BROWSER_ACTIONS} actions`); } return value.map((raw, index) => { if (!raw || typeof raw !== "object") { throw new Error(`browser_act action ${index} must be an object`); } const action = raw as Record; const kind = String(action.kind ?? "") as BrowserActKind; if (kind !== "click" && kind !== "fill" && kind !== "type") { throw new Error(`browser_act action ${index} has unsupported kind`); } const ref = String(action.ref ?? "").trim(); if (!ref) throw new Error(`browser_act action ${index} requires ref`); if (kind === "fill" || kind === "type") { if (typeof action.text !== "string") { throw new Error(`browser_act ${kind} requires text`); } return { kind, ref, text: String(action.text) }; } return { kind, ref }; }); } function formatBrowserResult(result: T) { if (result.fallback === "computer_act") { return { ...result, note: "Page browser could not complete this step. Inspect the current state before continuing with computer_act if available, otherwise request_takeover. Do not replay completed or uncertain actions.", }; } return result; }