fix(agent-browser): restore bounded presentation lifecycle

This commit is contained in:
2026-09-06 12:50:18 +08:00
parent 5c61110f46
commit 9fed0cc7c4
20 changed files with 1714 additions and 49 deletions

View File

@@ -4,10 +4,17 @@ import type { CodingAttachmentStore } from '../../../coding-projects/attachment-
import type { AgentBrowserDetailsV1 } from '../../contracts';
export interface AgentBrowserToolContext {
conversationId: string;
runId: string;
projectId: string;
projectPath: string;
}
export type AgentBrowserPresentationRequester = (snapshot: AgentBrowserSnapshot) => void;
export type AgentBrowserStatePublisher = (snapshot: AgentBrowserSnapshot) => void;
const PRESENTATION_TIMEOUT_MS = 5_000;
export interface AgentBrowserToolResult {
content: Array<{ type: 'text'; text: string }>;
details: AgentBrowserDetailsV1;
@@ -70,11 +77,27 @@ async function readPayloadJson(
}
export class PiAgentBrowserTool {
private readonly diagnosticRuns = new Map<string, { projectPath: string; owner: string }>();
constructor(
private readonly browser: AgentBrowserModule,
private readonly attachments: CodingAttachmentStore,
private readonly requestPresentation?: AgentBrowserPresentationRequester,
private readonly publishState?: AgentBrowserStatePublisher,
) {}
async releaseRun(conversationId: string, runId: string): Promise<void> {
const key = `${conversationId}:${runId}`;
const diagnostic = this.diagnosticRuns.get(key);
if (!diagnostic) return;
this.diagnosticRuns.delete(key);
await this.browser.setDiagnostics({
projectPath: diagnostic.projectPath,
enabled: false,
owner: diagnostic.owner,
}).catch(() => undefined);
}
async execute(
context: AgentBrowserToolContext,
input: unknown,
@@ -85,20 +108,63 @@ export class PiAgentBrowserTool {
return result(action, publicSnapshot(await this.browser.getSnapshot(context.projectPath)));
}
if (action === 'open') {
const snapshot = await this.browser.open({
const owner = `agent:${context.conversationId}:${context.runId}`;
let snapshot = await this.browser.open({
projectId: context.projectId,
projectPath: context.projectPath,
url: string(body.url, true) as string,
visible: false,
diagnosticsOwner: owner,
...(body.injectProjectData === true ? { injectProjectData: true } : {}),
});
const runKey = `${context.conversationId}:${context.runId}`;
this.diagnosticRuns.set(runKey, { projectPath: context.projectPath, owner });
if (this.requestPresentation) {
try {
this.requestPresentation(snapshot);
snapshot = await this.browser.waitForPresentation({
projectPath: context.projectPath,
generation: snapshot.generation,
timeoutMs: PRESENTATION_TIMEOUT_MS,
});
} catch (error) {
this.diagnosticRuns.delete(runKey);
const closed = await this.browser.close(context.projectPath).catch(() => null);
if (closed) {
this.publishState?.({
...closed,
projectId: context.projectId,
projectPath: context.projectPath,
});
}
throw error;
}
}
return result(action, publicSnapshot(snapshot));
}
if (action === 'close') {
return result(action, publicSnapshot(await this.browser.close(context.projectPath)));
for (const [key, diagnostic] of this.diagnosticRuns) {
if (diagnostic.projectPath === context.projectPath) this.diagnosticRuns.delete(key);
}
const snapshot = await this.browser.close(context.projectPath);
this.publishState?.({
...snapshot,
projectId: context.projectId,
projectPath: context.projectPath,
});
return result(action, publicSnapshot(snapshot));
}
if (action === 'reset_profile') {
return result(action, publicSnapshot(await this.browser.resetProfile(context.projectPath)));
for (const [key, diagnostic] of this.diagnosticRuns) {
if (diagnostic.projectPath === context.projectPath) this.diagnosticRuns.delete(key);
}
const snapshot = await this.browser.resetProfile(context.projectPath);
this.publishState?.({
...snapshot,
projectId: context.projectId,
projectPath: context.projectPath,
});
return result(action, publicSnapshot(snapshot));
}
if (action === 'navigate') {
const navigation = string(body.navigation, true);
@@ -110,6 +176,7 @@ export class PiAgentBrowserTool {
action: navigation as 'url' | 'back' | 'forward' | 'reload',
...(body.url === undefined ? {} : { url: string(body.url) }),
});
this.publishState?.(snapshot);
return result(action, publicSnapshot(snapshot));
}
if (action === 'read_events') {