import type { AgentBrowserModule } from '../../../agent-browser'; import type { AgentBrowserCdpResult, AgentBrowserSnapshot } from '../../../../shared/agent-browser'; import type { CodingAttachmentStore } from '../../../coding-projects/attachment-store'; import type { AgentBrowserDetailsV1 } from '../../contracts'; export interface AgentBrowserToolContext { projectId: string; projectPath: string; } export interface AgentBrowserToolResult { content: Array<{ type: 'text'; text: string }>; details: AgentBrowserDetailsV1; } function record(value: unknown): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error('Agent browser input is invalid'); } return value as Record; } function string(value: unknown, required = false): string | undefined { const result = typeof value === 'string' ? value.trim() : ''; if (required && !result) throw new Error('Agent browser input is incomplete'); return result || undefined; } function integer(value: unknown): number | undefined { if (value === undefined) return undefined; if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error('Agent browser number is invalid'); return value as number; } function publicSnapshot(snapshot: AgentBrowserSnapshot): Omit { const { projectPath: _projectPath, ...safe } = snapshot; return safe; } function result( action: AgentBrowserDetailsV1['action'], value: unknown, attachment?: { attachmentId: string; mime: string }, ): AgentBrowserToolResult { return { content: [{ type: 'text', text: JSON.stringify(value) }], details: { schema: 'agent-browser.v1', action, ...(attachment ?? {}), }, }; } async function readPayloadJson( browser: AgentBrowserModule, projectPath: string, payload: Extract, ): Promise { let offset = 0; let data = ''; while (offset < payload.byteLength) { const chunk = await browser.readPayload({ projectPath, handle: payload.handle, offset }); if (chunk.encoding !== 'utf8') throw new Error('Browser screenshot payload encoding is invalid'); data += chunk.data; offset = chunk.nextOffset; if (chunk.done) break; } return JSON.parse(data); } export class PiAgentBrowserTool { constructor( private readonly browser: AgentBrowserModule, private readonly attachments: CodingAttachmentStore, ) {} async execute( context: AgentBrowserToolContext, input: unknown, ): Promise { const body = record(input); const action = string(body.action, true) as AgentBrowserDetailsV1['action']; if (action === 'status') { return result(action, publicSnapshot(await this.browser.getSnapshot(context.projectPath))); } if (action === 'open') { const snapshot = await this.browser.open({ projectId: context.projectId, projectPath: context.projectPath, url: string(body.url, true) as string, visible: false, ...(body.injectProjectData === true ? { injectProjectData: true } : {}), }); return result(action, publicSnapshot(snapshot)); } if (action === 'close') { return result(action, publicSnapshot(await this.browser.close(context.projectPath))); } if (action === 'reset_profile') { return result(action, publicSnapshot(await this.browser.resetProfile(context.projectPath))); } if (action === 'navigate') { const navigation = string(body.navigation, true); if (!navigation || !['url', 'back', 'forward', 'reload'].includes(navigation)) { throw new Error('Agent browser navigation is invalid'); } const snapshot = await this.browser.navigate({ projectPath: context.projectPath, action: navigation as 'url' | 'back' | 'forward' | 'reload', ...(body.url === undefined ? {} : { url: string(body.url) }), }); return result(action, publicSnapshot(snapshot)); } if (action === 'read_events') { const methods = body.methods === undefined ? undefined : Array.isArray(body.methods) && body.methods.every((item) => typeof item === 'string') ? body.methods.map((item) => item.trim()).filter(Boolean) : (() => { throw new Error('Agent browser event methods are invalid'); })(); return result(action, await this.browser.readEvents({ projectPath: context.projectPath, after: integer(body.after), methods, limit: integer(body.limit), waitMs: integer(body.waitMs), })); } if (action === 'read_payload') { return result(action, await this.browser.readPayload({ projectPath: context.projectPath, handle: string(body.handle, true) as string, offset: integer(body.offset), maxBytes: integer(body.maxBytes), })); } if (action !== 'send_cdp') throw new Error('Agent browser action is invalid'); const method = string(body.method, true) as string; const params = body.params === undefined ? undefined : record(body.params); const cdp = await this.browser.sendCdp({ projectPath: context.projectPath, method, params, sessionRef: string(body.sessionRef), timeoutMs: integer(body.timeoutMs), }); if (method !== 'Page.captureScreenshot') return result(action, cdp); const projected = cdp.kind === 'inline' ? cdp.value : await readPayloadJson(this.browser, context.projectPath, cdp); const data = record(projected).data; if (typeof data !== 'string' || !data) throw new Error('Browser screenshot result is invalid'); const format = string(params?.format) ?? 'png'; const mime = format === 'jpeg' ? 'image/jpeg' : format === 'webp' ? 'image/webp' : 'image/png'; const attachment = await this.attachments.put(Buffer.from(data, 'base64'), mime); return result(action, { attachmentId: attachment.attachmentId, mime }, { attachmentId: attachment.attachmentId, mime, }); } }