evOo-local/packages/adapters/src/computer-browser.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

219 lines
6.8 KiB
TypeScript

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<PageBrowserResult>;
export type LivePageBrowserDriver = (
computer: ComputerRef,
request: PageBrowserCommand,
context: AdapterContext,
) => Promise<LivePayload>;
/**
* 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<BrowserNavigateResult> {
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<BrowserSnapshotResult> {
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,
};
}
async act(
computer: ComputerRef,
request: BrowserActRequest,
context: AdapterContext,
): Promise<BrowserActResult> {
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,
};
}
private canUseInProcess(computer: ComputerRef): boolean {
return computer.kind === "fake";
}
private async runLive(
computer: ComputerRef,
request: PageBrowserCommand,
context: AdapterContext,
): Promise<LivePayload> {
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");
}