import type { AdapterContext, BrowserActRequest, BrowserActResult, BrowserNavigateRequest, BrowserNavigateResult, BrowserProvider, BrowserSnapshotNode, BrowserSnapshotRequest, BrowserSnapshotResult, ComputerRef, PageBrowserCommand, PageBrowserResult, SandboxProvider, } from "@rakazo/adapter-kit"; import { FakeBrowserProvider, type FakeBrowserProviderOptions } from "./fake-browser.js"; const DETACHED_MESSAGE = "Page browser is not attached to this computer's Chrome. Use computer_act on the desktop browser instead."; type LivePayload = Partial; export type LivePageBrowserDriver = ( computer: ComputerRef, request: PageBrowserCommand, context: AdapterContext, ) => Promise; /** * Production page-browser adapter for the bot computer. * * Fake computers (and the fake sandbox) use the in-process DOM session. * Real computers drive the live Chrome already on the graphical display via * CDP (`rakazo-page-browser`). If that path cannot operate, return * `fallback: "computer_act"` — never report success against a detached DOM. */ export class ComputerBrowserProvider implements BrowserProvider { private readonly fake: FakeBrowserProvider; private readonly sandbox?: SandboxProvider; private readonly liveDriver?: LivePageBrowserDriver; constructor( options: FakeBrowserProviderOptions & { sandbox?: SandboxProvider; /** Test seam for the live CDP helper. */ liveDriver?: LivePageBrowserDriver; } = {}, ) { const { sandbox, liveDriver, ...fakeOptions } = options; this.sandbox = sandbox; this.liveDriver = liveDriver; this.fake = new FakeBrowserProvider(fakeOptions); } describe() { return { id: "computer", contractVersion: "1", adapterVersion: "0.1.0", capabilities: { page: Boolean( this.liveDriver || this.sandbox?.pageBrowser || this.sandbox?.describe().id === "fake", ), refs: true, keyless: true, }, }; } async navigate( computer: ComputerRef, request: BrowserNavigateRequest, context: AdapterContext, ): Promise { if (this.canUseInProcess(computer)) { return this.fake.navigate(computer, request, context); } const live = await this.runLive(computer, { command: "navigate", url: request.url }, context); if (live?.ok !== true || live.fallback === "computer_act") { return { url: request.url, title: "", fallback: "computer_act", error: live?.error || DETACHED_MESSAGE, }; } return { url: typeof live.url === "string" ? live.url : request.url, title: typeof live.title === "string" ? live.title : "", }; } async snapshot( computer: ComputerRef, request: BrowserSnapshotRequest, context: AdapterContext, ): Promise { if (this.canUseInProcess(computer)) { return this.fake.snapshot(computer, request, context); } const live = await this.runLive(computer, { command: "snapshot" }, context); if (live?.ok !== true || live.fallback === "computer_act") { return { url: "", title: "", tree: "", elements: [], fallback: "computer_act", error: live?.error || DETACHED_MESSAGE, }; } const elements = Array.isArray(live.elements) ? live.elements : []; return { url: typeof live.url === "string" ? live.url : "", title: typeof live.title === "string" ? live.title : "", tree: typeof live.tree === "string" ? live.tree : formatTree(elements), elements, ...(live.screen ? { screen: live.screen } : {}), }; } async act( computer: ComputerRef, request: BrowserActRequest, context: AdapterContext, ): Promise { if (this.canUseInProcess(computer)) { return this.fake.act(computer, request, context); } const live = await this.runLive( computer, { command: "act", actions: request.actions }, context, ); if (live?.ok !== true || live.fallback === "computer_act") { return { ok: false, completed: typeof live?.completed === "number" ? live.completed : 0, uncertain: live?.uncertain ?? true, url: typeof live?.url === "string" ? live.url : "", title: typeof live?.title === "string" ? live.title : "", fallback: "computer_act", error: live?.error || DETACHED_MESSAGE, }; } const elements = Array.isArray(live.elements) ? live.elements : undefined; return { ok: true, completed: typeof live.completed === "number" ? live.completed : request.actions.length, url: typeof live.url === "string" ? live.url : "", title: typeof live.title === "string" ? live.title : "", tree: typeof live.tree === "string" ? live.tree : elements ? formatTree(elements) : undefined, elements, ...(live.screen ? { screen: live.screen } : {}), }; } private canUseInProcess(computer: ComputerRef): boolean { return computer.kind === "fake"; } private async runLive( computer: ComputerRef, request: PageBrowserCommand, context: AdapterContext, ): Promise { context.signal.throwIfAborted(); try { const result = this.liveDriver ? await this.liveDriver(computer, request, context) : this.sandbox?.pageBrowser ? await this.sandbox.pageBrowser(computer, request, context) : { ok: false, error: DETACHED_MESSAGE, fallback: "computer_act" as const, uncertain: false, }; if (result?.ok === true) { if (typeof result.url !== "string" || typeof result.title !== "string") { throw new Error("Incomplete page browser response"); } if ( request.command === "snapshot" && (typeof result.tree !== "string" || !Array.isArray(result.elements)) ) { throw new Error("Incomplete page snapshot"); } if ( request.command === "act" && (!Number.isInteger(result.completed) || result.completed !== request.actions.length || result.uncertain) ) { throw new Error("Unconfirmed page actions"); } } return result; } catch (error) { context.signal.throwIfAborted(); return { uncertain: request.command === "act", ok: false, error: error instanceof Error ? error.message : String(error), fallback: "computer_act", }; } } } function formatTree(elements: BrowserSnapshotNode[]): string { if (elements.length === 0) return "(no interactive elements)"; return elements .map((el) => { const value = el.value !== undefined && el.value !== "" ? ` value=${JSON.stringify(el.value)}` : ""; return `- ${el.role} "${el.name}" [${el.ref}]${value}`; }) .join("\n"); }