fix(agent-browser): restore bounded presentation lifecycle
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user