import { Type } from "typebox"; import type { ActionName, Expectation, WaitSpec } from "../domain/types.ts"; import { AgentError } from "../domain/types.ts"; import type { ExtensionAPI, ExtensionContext } from "../pi-api.ts"; import { textResult } from "../pi-api.ts"; import { parseStartArgs, type ActionInput } from "../session.ts"; import type { SessionHandle } from "../host/session-handle.ts"; import { operatorCanConfirm } from "../host/pi-subagent/unattended.ts"; function stringify(value: unknown): string { return JSON.stringify(value, null, 2); } function runParam() { return Type.Optional(Type.String({ description: "Run id; defaults to the current run" })); } function tabParam() { return Type.Optional(Type.String({ description: "Tab id; defaults to the run's current tab" })); } function expectParam() { return Type.Optional( Type.Object({ urlIncludes: Type.Optional(Type.String()), titleIncludes: Type.Optional(Type.String()), textVisible: Type.Optional(Type.String()), refExists: Type.Optional(Type.String()), dialogOpen: Type.Optional(Type.Boolean()), noConsoleError: Type.Optional(Type.Boolean()), }), ); } export function registerBrowserTools(pi: ExtensionAPI, session: SessionHandle): void { const wrap = ( name: string, execute: (params: Record, ctx: ExtensionContext) => Promise, ) => { return async ( _id: string, params: Record, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext, ) => { try { const result = await execute(params, ctx); const verification = (result as { verification?: { status?: string } } | undefined)?.verification; const failed = verification?.status === "failed" || (result as { ok?: boolean } | undefined)?.ok === false; await session.recordTool(name, params, { ok: !failed, verification }, failed).catch(() => undefined); return textResult(stringify(result), { result }, failed); } catch (err) { const error = err instanceof AgentError ? err : new AgentError("tool_error", String(err)); await session.recordTool( name, params, { ok: false, code: error.code, message: error.message }, true, ).catch(() => undefined); return textResult(`${error.code}: ${error.message}`, { error }, true); } }; }; const act = async (params: Record, action: ActionName) => { const input: ActionInput = { runId: params.runId as string | undefined, tabId: params.tabId as string | undefined, action, url: params.url as string | undefined, ref: params.ref as string | undefined, text: params.text as string | undefined, value: params.value as string | undefined, dy: params.dy as number | undefined, wait: params.wait as WaitSpec | undefined, expect: params.expect as Expectation | undefined, }; return session.act(input); }; pi.registerTool({ name: "browser_inspect", label: "Inspect page", description: "Observe the current page: URL, title, ref-tagged controls, dialogs, errors, recent changes.", parameters: Type.Object({ runId: runParam(), tabId: tabParam() }), promptSnippet: "Inspect the focused browser tab as a compact semantic snapshot.", promptGuidelines: [ "Use browser_inspect before acting so refs match the current DOM.", "Never invent CSS selectors; only use refs from the latest inspect.", ], execute: wrap("browser_inspect", async (params) => session.inspect(params.runId as string | undefined, params.tabId as string | undefined), ), }); pi.registerTool({ name: "browser_navigate", label: "Navigate", description: "Navigate the owned tab to a URL and verify the result.", parameters: Type.Object({ url: Type.String({ description: "Absolute URL" }), runId: runParam(), tabId: tabParam(), expect: expectParam(), }), promptSnippet: "Navigate the owned browser tab.", promptGuidelines: ["Use browser_navigate for full URL changes, not in-page clicks."], execute: wrap("browser_navigate", (params) => act(params, "navigate")), }); pi.registerTool({ name: "browser_click", label: "Click", description: "Click a control by snapshot ref.", parameters: Type.Object({ ref: Type.String({ description: "Control ref from the latest inspect, e.g. e3" }), runId: runParam(), tabId: tabParam(), expect: expectParam(), }), promptSnippet: "Click a ref from the latest browser_inspect.", promptGuidelines: ["Use browser_click with a ref from the latest inspect of that tab."], execute: wrap("browser_click", (params) => act(params, "click")), }); pi.registerTool({ name: "browser_type", label: "Type", description: "Fill a text or password control by snapshot ref.", parameters: Type.Object({ ref: Type.String(), text: Type.String(), runId: runParam(), tabId: tabParam(), expect: expectParam(), }), promptSnippet: "Type into a ref from the latest browser_inspect.", promptGuidelines: ["Use browser_type for inputs. Password values are redacted in the evidence log."], execute: wrap("browser_type", (params) => act(params, "type")), }); pi.registerTool({ name: "browser_select", label: "Select", description: "Choose an option on a select control by snapshot ref.", parameters: Type.Object({ ref: Type.String(), value: Type.String(), runId: runParam(), tabId: tabParam(), expect: expectParam(), }), promptSnippet: "Select an option by ref.", promptGuidelines: ["Use browser_select for native