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

@@ -18,6 +18,7 @@ import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins'
import type { PiSkillEntry } from './resource-loader';
import type { EffectivePluginSnapshot } from '../../coding-plugins/effective-resolver';
import { DevicePackageError } from '../../coding-packages/device-package-manager';
import { AgentBrowserFault } from '../../agent-browser/fault';
const MAX_REQUEST_BYTES = 64 * 1024;
const PRODUCT_TOOL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9._:-]{0,63}$/u;
@@ -516,6 +517,17 @@ export class PiManagedExtensionHost {
record.waiters.delete(value.resourceId);
}
} catch (error) {
if (!response.writableEnded && error instanceof AgentBrowserFault) {
const message = error.message.slice(0, 512);
this.respond(response, 400, {
code: error.code,
error: `${error.code}: ${message}`,
retryable: error.retryable,
...(error.generation === undefined ? {} : { generation: error.generation }),
...(error.outcome === undefined ? {} : { outcome: error.outcome }),
});
return;
}
if (!response.writableEnded && error instanceof DevicePackageError) {
this.respond(response, 400, {
code: error.code,

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') {

View File

@@ -23,6 +23,10 @@ import type { EffectivePluginSnapshot } from '../../coding-plugins/effective-res
import type { KnownToolDetails, RuntimeContextDetailsV1 } from '../contracts';
import { BUNDLED_CODING_SKILL_IDS } from '../../../shared/coding-skills';
import { PiAgentBrowserTool } from './extensions/agent-browser';
import type {
AgentBrowserPresentationRequester,
AgentBrowserStatePublisher,
} from './extensions/agent-browser';
import { reportChangedFiles } from './extensions/changed-file';
import { projectTaskState } from './extensions/task-state';
import type { ModelToolRegistryPort } from './model-tools/model-tool-registry';
@@ -73,6 +77,8 @@ export interface PiProductToolsOptions {
capabilityRegistry?: CodingCapabilityRegistry;
modelToolRegistry?: ModelToolRegistryPort;
devicePackageTools?: DevicePackageTools;
requestAgentBrowserPresentation?: AgentBrowserPresentationRequester;
publishAgentBrowserState?: AgentBrowserStatePublisher;
}
export class PiProductTools {
@@ -82,7 +88,12 @@ export class PiProductTools {
constructor(private readonly options: PiProductToolsOptions) {
this.changeTracker = options.changeTracker ?? new ConversationChangeTracker();
this.browser = new PiAgentBrowserTool(options.browser, options.attachments);
this.browser = new PiAgentBrowserTool(
options.browser,
options.attachments,
options.requestAgentBrowserPresentation,
options.publishAgentBrowserState,
);
this.capabilityRegistry = options.capabilityRegistry;
}
@@ -94,8 +105,12 @@ export class PiProductTools {
return this.changeTracker.beginRun(input);
}
settleRun(conversationId: string, runId: string) {
return this.changeTracker.settleRun(conversationId, runId);
async settleRun(conversationId: string, runId: string) {
try {
return await this.changeTracker.settleRun(conversationId, runId);
} finally {
await this.browser.releaseRun(conversationId, runId);
}
}
getChanges(conversationId: string): ConversationChangesSnapshot | null {