pi-agent-browser-native 0.1.6 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +99 -5
- package/docs/ARCHITECTURE.md +16 -8
- package/docs/TOOL_CONTRACT.md +27 -17
- package/extensions/agent-browser/index.ts +196 -59
- package/extensions/agent-browser/lib/results/envelope.ts +7 -0
- package/extensions/agent-browser/lib/results/presentation.ts +263 -22
- package/extensions/agent-browser/lib/results/shared.ts +25 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +32 -16
- package/extensions/agent-browser/lib/runtime.ts +158 -32
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purpose: Register the native agent_browser tool for pi so agents can invoke agent-browser without going through bash.
|
|
3
|
-
* Responsibilities: Define the tool schema, inject thin wrapper behavior around the upstream CLI, manage
|
|
3
|
+
* Responsibilities: Define the tool schema, inject thin wrapper behavior around the upstream CLI, manage extension-owned browser session convenience, and return pi-friendly content/details.
|
|
4
4
|
* Scope: Native tool registration and orchestration only; the wrapper intentionally stays close to the upstream agent-browser CLI.
|
|
5
5
|
* Usage: Loaded by pi through the package manifest in this package, or explicitly via `pi --no-extensions -e .` during local checkout development.
|
|
6
6
|
* Invariants/Assumptions: agent-browser is installed separately on PATH, the wrapper targets the current locally installed upstream version only, and no backward-compatibility shims are provided.
|
|
@@ -17,31 +17,40 @@ import {
|
|
|
17
17
|
buildExecutionPlan,
|
|
18
18
|
buildPromptPolicy,
|
|
19
19
|
createEphemeralSessionSeed,
|
|
20
|
+
createFreshSessionName,
|
|
20
21
|
createImplicitSessionName,
|
|
21
22
|
getImplicitSessionCloseTimeoutMs,
|
|
22
23
|
getImplicitSessionIdleTimeoutMs,
|
|
23
24
|
getLatestUserPrompt,
|
|
24
25
|
hasUsableBraveApiKey,
|
|
25
|
-
|
|
26
|
+
resolveManagedSessionState,
|
|
26
27
|
validateToolArgs,
|
|
27
28
|
} from "./lib/runtime.js";
|
|
28
29
|
import { cleanupSecureTempArtifacts } from "./lib/temp.js";
|
|
29
30
|
|
|
31
|
+
const DEFAULT_SESSION_MODE = "auto" as const;
|
|
32
|
+
|
|
30
33
|
const AGENT_BROWSER_PARAMS = Type.Object({
|
|
31
34
|
args: Type.Array(Type.String({ description: "Exact agent-browser CLI arguments, excluding the binary name." }), {
|
|
32
35
|
description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators.",
|
|
33
36
|
minItems: 1,
|
|
34
37
|
}),
|
|
35
38
|
stdin: Type.Optional(Type.String({ description: "Optional raw stdin content for commands like eval --stdin or batch." })),
|
|
36
|
-
|
|
37
|
-
Type.
|
|
38
|
-
description:
|
|
39
|
-
|
|
39
|
+
sessionMode: Type.Optional(
|
|
40
|
+
Type.Union([Type.Literal("auto"), Type.Literal("fresh")], {
|
|
41
|
+
description:
|
|
42
|
+
"Session handling mode. `auto` reuses the extension-managed pi-scoped session when possible. `fresh` switches that managed session to a fresh upstream launch so startup-scoped flags like --profile, --session-name, or --cdp apply and later auto calls follow the new browser.",
|
|
43
|
+
default: DEFAULT_SESSION_MODE,
|
|
40
44
|
}),
|
|
41
45
|
),
|
|
42
46
|
});
|
|
43
47
|
const PROJECT_RULE_PROMPT =
|
|
44
48
|
"Project rule: when browser automation is needed, prefer the native `agent_browser` tool. Do not run direct `agent-browser` bash commands unless the user explicitly asks for a bash-oriented workflow or browser-integration debugging.";
|
|
49
|
+
const QUICK_START_GUIDELINES = [
|
|
50
|
+
"Quick start mental model: args are the exact agent-browser CLI args after the binary; stdin is only for batch and eval --stdin; sessionMode=fresh switches the extension-managed session to a fresh upstream launch when you need new --profile, --session-name, or --cdp state.",
|
|
51
|
+
"Common first calls: { args: [\"open\", \"https://example.com\"] } then { args: [\"snapshot\", \"-i\"] }; after navigation, use { args: [\"click\", \"@e2\"] } then { args: [\"snapshot\", \"-i\"] }.",
|
|
52
|
+
"Common advanced calls: { args: [\"batch\"], stdin: \"[[\\\"open\\\",\\\"https://example.com\\\"],[\\\"snapshot\\\",\\\"-i\\\"]]\" }, { args: [\"eval\", \"--stdin\"], stdin: \"document.title\" }, and { args: [\"--profile\", \"Default\", \"open\", \"https://example.com/account\"], sessionMode: \"fresh\" }.",
|
|
53
|
+
] as const;
|
|
45
54
|
const BRAVE_SEARCH_PROMPT_GUIDELINE =
|
|
46
55
|
"When a non-empty BRAVE_API_KEY is available in the current environment, prefer the Brave Search API via bash/curl to discover specific destination URLs, then open the chosen URL with agent_browser instead of browsing a search engine results page just to find the target.";
|
|
47
56
|
const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
@@ -49,6 +58,7 @@ const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
|
49
58
|
"For authenticated or user-specific content like feeds, inboxes, dashboards, and accounts, prefer --profile Default on the first browser call and let the implicit session carry continuity. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser.",
|
|
50
59
|
"Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.",
|
|
51
60
|
"When using --profile, --session-name, or --cdp, put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.",
|
|
61
|
+
"If you already used the implicit session and now need startup-scoped flags like --profile, --session-name, or --cdp, retry with sessionMode set to fresh or pass an explicit --session for the new launch. After a successful unnamed fresh launch, later auto calls follow that new session.",
|
|
52
62
|
"If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <n> / snapshot -i to recover state before retrying different URLs or fallback strategies. Only use wait with an explicit argument like milliseconds, --load, --url, --fn, or --text.",
|
|
53
63
|
"For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.",
|
|
54
64
|
"For read-only browsing tasks, prefer extracting the answer from the current snapshot, structured ref labels, or eval --stdin on the current page before navigating away. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
|
|
@@ -62,7 +72,8 @@ const TOOL_PROMPT_GUIDELINES_SUFFIX = [
|
|
|
62
72
|
"Do not fall back to osascript, AppleScript, or generic browser-driving bash commands when this tool can do the job.",
|
|
63
73
|
"Pass exact agent-browser CLI arguments in args, excluding the binary name.",
|
|
64
74
|
"Use stdin for commands like eval --stdin and batch instead of shell heredocs.",
|
|
65
|
-
"Let the
|
|
75
|
+
"Let the extension-managed session handle the common path unless you explicitly need a fresh launch for upstream flags like --profile, --session-name, or --cdp.",
|
|
76
|
+
"Use sessionMode=fresh when switching from an existing implicit session to a new profile/debug launch without inventing a fixed explicit session name; later auto calls will follow that new session.",
|
|
66
77
|
] as const;
|
|
67
78
|
|
|
68
79
|
function buildMissingBinaryMessage(): string {
|
|
@@ -80,27 +91,100 @@ function buildInvocationPreview(effectiveArgs: string[]): string {
|
|
|
80
91
|
return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
|
|
81
92
|
}
|
|
82
93
|
|
|
94
|
+
const AGENT_BROWSER_BASH_PREFIX = String.raw`(?:env(?:\s+[A-Za-z_][A-Za-z0-9_]*=[^\s;&|]+)*\s+)?(?:(?:npx|bunx)(?:\s+-[^\s;&|]+|\s+--[^\s;&|]+(?:=[^\s;&|]+)?)*\s+|(?:pnpm|yarn)\s+dlx(?:\s+-[^\s;&|]+|\s+--[^\s;&|]+(?:=[^\s;&|]+)?)*\s+)?`;
|
|
95
|
+
const AGENT_BROWSER_BASH_EXECUTABLE = String.raw`(?:[.~]|\.\.?|\/)?(?:[^\s;&|]+\/)?agent-browser`;
|
|
96
|
+
const DIRECT_AGENT_BROWSER_BASH_PATTERN = new RegExp(
|
|
97
|
+
String.raw`(^|[\s;&|])${AGENT_BROWSER_BASH_PREFIX}${AGENT_BROWSER_BASH_EXECUTABLE}(?=\s|$)`,
|
|
98
|
+
);
|
|
99
|
+
const HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN = /(command\s+-v|which|type\s+-P)\s+agent-browser\b/;
|
|
100
|
+
|
|
83
101
|
function looksLikeDirectAgentBrowserBash(command: string): boolean {
|
|
84
|
-
return
|
|
102
|
+
return DIRECT_AGENT_BROWSER_BASH_PATTERN.test(command);
|
|
85
103
|
}
|
|
86
104
|
|
|
87
105
|
function isHarmlessAgentBrowserInspectionCommand(command: string): boolean {
|
|
88
|
-
return
|
|
106
|
+
return HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN.test(command);
|
|
89
107
|
}
|
|
90
108
|
|
|
91
109
|
function isPlainTextInspectionArgs(args: string[]): boolean {
|
|
92
110
|
return args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-V");
|
|
93
111
|
}
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
|
|
114
|
+
|
|
115
|
+
interface NavigationSummary {
|
|
116
|
+
title?: string;
|
|
117
|
+
url?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
121
|
+
return typeof value === "object" && value !== null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function shouldCaptureNavigationSummary(command: string | undefined, data: unknown): boolean {
|
|
125
|
+
return (
|
|
126
|
+
command !== undefined &&
|
|
127
|
+
NAVIGATION_SUMMARY_COMMANDS.has(command) &&
|
|
128
|
+
(!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string"))
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractStringResultField(data: unknown, fieldName: "title" | "url"): string | undefined {
|
|
133
|
+
if (typeof data === "string") {
|
|
134
|
+
const text = data.trim();
|
|
135
|
+
return text.length > 0 ? text : undefined;
|
|
136
|
+
}
|
|
137
|
+
if (!isRecord(data) || typeof data[fieldName] !== "string") {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const text = data[fieldName].trim();
|
|
141
|
+
return text.length > 0 ? text : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function collectNavigationSummary(options: {
|
|
145
|
+
cwd: string;
|
|
146
|
+
sessionName?: string;
|
|
147
|
+
signal?: AbortSignal;
|
|
148
|
+
}): Promise<NavigationSummary | undefined> {
|
|
149
|
+
const { cwd, sessionName, signal } = options;
|
|
150
|
+
if (!sessionName) return undefined;
|
|
151
|
+
|
|
152
|
+
const readField = async (fieldName: "title" | "url"): Promise<string | undefined> => {
|
|
153
|
+
const processResult = await runAgentBrowserProcess({
|
|
154
|
+
args: ["--json", "--session", sessionName, "get", fieldName],
|
|
155
|
+
cwd,
|
|
156
|
+
signal,
|
|
157
|
+
});
|
|
158
|
+
if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
const parsed = await parseAgentBrowserEnvelope({
|
|
162
|
+
stdout: processResult.stdout,
|
|
163
|
+
stdoutPath: processResult.stdoutSpillPath,
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
if (parsed.parseError || parsed.envelope?.success === false) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
return extractStringResultField(parsed.envelope?.data, fieldName);
|
|
170
|
+
} finally {
|
|
171
|
+
if (processResult.stdoutSpillPath) {
|
|
172
|
+
await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const title = await readField("title");
|
|
178
|
+
const url = await readField("url");
|
|
179
|
+
if (!title && !url) return undefined;
|
|
180
|
+
return { title, url };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: NavigationSummary): unknown {
|
|
184
|
+
if (isRecord(data)) {
|
|
185
|
+
return { ...data, navigationSummary };
|
|
186
|
+
}
|
|
187
|
+
return { navigationSummary, result: data };
|
|
104
188
|
}
|
|
105
189
|
|
|
106
190
|
function buildSharedBrowserPlaybookGuidelines(hasBraveApiKey: boolean): string[] {
|
|
@@ -115,6 +199,9 @@ function buildBrowserSystemPromptAppendix(hasBraveApiKey: boolean): string {
|
|
|
115
199
|
return [
|
|
116
200
|
PROJECT_RULE_PROMPT,
|
|
117
201
|
"",
|
|
202
|
+
"Quick start:",
|
|
203
|
+
...QUICK_START_GUIDELINES.map((guideline) => `- ${guideline}`),
|
|
204
|
+
"",
|
|
118
205
|
"Browser operating playbook:",
|
|
119
206
|
...buildSharedBrowserPlaybookGuidelines(hasBraveApiKey).map((guideline) => `- ${guideline}`),
|
|
120
207
|
].join("\n");
|
|
@@ -123,11 +210,28 @@ function buildBrowserSystemPromptAppendix(hasBraveApiKey: boolean): string {
|
|
|
123
210
|
function buildToolPromptGuidelines(hasBraveApiKey: boolean): string[] {
|
|
124
211
|
return [
|
|
125
212
|
...TOOL_PROMPT_GUIDELINES_PREFIX,
|
|
213
|
+
...QUICK_START_GUIDELINES,
|
|
126
214
|
...buildSharedBrowserPlaybookGuidelines(hasBraveApiKey),
|
|
127
215
|
...TOOL_PROMPT_GUIDELINES_SUFFIX,
|
|
128
216
|
];
|
|
129
217
|
}
|
|
130
218
|
|
|
219
|
+
async function closeManagedSession(options: { cwd: string; sessionName: string; timeoutMs: number }): Promise<void> {
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
222
|
+
try {
|
|
223
|
+
await runAgentBrowserProcess({
|
|
224
|
+
args: ["--session", options.sessionName, "close"],
|
|
225
|
+
cwd: options.cwd,
|
|
226
|
+
signal: controller.signal,
|
|
227
|
+
});
|
|
228
|
+
} catch {
|
|
229
|
+
// Best-effort cleanup only.
|
|
230
|
+
} finally {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
131
235
|
export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
132
236
|
const ephemeralSessionSeed = createEphemeralSessionSeed();
|
|
133
237
|
const hasBraveApiKey = hasUsableBraveApiKey();
|
|
@@ -135,32 +239,28 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
135
239
|
const toolPromptGuidelines = buildToolPromptGuidelines(hasBraveApiKey);
|
|
136
240
|
const implicitSessionIdleTimeoutMs = getImplicitSessionIdleTimeoutMs();
|
|
137
241
|
const implicitSessionCloseTimeoutMs = getImplicitSessionCloseTimeoutMs();
|
|
138
|
-
let
|
|
139
|
-
let
|
|
140
|
-
let
|
|
242
|
+
let managedSessionActive = false;
|
|
243
|
+
let managedSessionBaseName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
|
|
244
|
+
let managedSessionName = managedSessionBaseName;
|
|
245
|
+
let managedSessionCwd = process.cwd();
|
|
246
|
+
let freshSessionOrdinal = 0;
|
|
141
247
|
|
|
142
248
|
pi.on("session_start", async (_event, ctx) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
249
|
+
managedSessionActive = false;
|
|
250
|
+
managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
|
|
251
|
+
managedSessionName = managedSessionBaseName;
|
|
252
|
+
managedSessionCwd = ctx.cwd;
|
|
253
|
+
freshSessionOrdinal = 0;
|
|
146
254
|
});
|
|
147
255
|
|
|
148
256
|
pi.on("session_shutdown", async () => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
signal: controller.signal,
|
|
157
|
-
});
|
|
158
|
-
} catch {
|
|
159
|
-
// Best-effort cleanup only.
|
|
160
|
-
} finally {
|
|
161
|
-
clearTimeout(timer);
|
|
162
|
-
await cleanupSecureTempArtifacts();
|
|
163
|
-
}
|
|
257
|
+
managedSessionActive = false;
|
|
258
|
+
await closeManagedSession({
|
|
259
|
+
cwd: managedSessionCwd,
|
|
260
|
+
sessionName: managedSessionName,
|
|
261
|
+
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
262
|
+
});
|
|
263
|
+
await cleanupSecureTempArtifacts();
|
|
164
264
|
});
|
|
165
265
|
|
|
166
266
|
pi.on("before_agent_start", async (event) => {
|
|
@@ -194,16 +294,6 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
194
294
|
promptGuidelines: toolPromptGuidelines,
|
|
195
295
|
parameters: AGENT_BROWSER_PARAMS,
|
|
196
296
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
197
|
-
const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
|
|
198
|
-
if (!promptPolicy.allowAgentBrowserInspection && isPlainTextInspectionArgs(params.args)) {
|
|
199
|
-
const errorText = buildInspectionDeflectionMessage();
|
|
200
|
-
return {
|
|
201
|
-
content: [{ type: "text", text: errorText }],
|
|
202
|
-
details: { args: params.args, inspectionBlocked: true },
|
|
203
|
-
isError: true,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
297
|
const validationError = validateToolArgs(params.args);
|
|
208
298
|
if (validationError) {
|
|
209
299
|
return {
|
|
@@ -213,17 +303,26 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
213
303
|
};
|
|
214
304
|
}
|
|
215
305
|
|
|
306
|
+
const sessionMode = params.sessionMode ?? DEFAULT_SESSION_MODE;
|
|
307
|
+
const freshSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1);
|
|
216
308
|
const executionPlan = buildExecutionPlan(params.args, {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
309
|
+
freshSessionName,
|
|
310
|
+
managedSessionActive,
|
|
311
|
+
managedSessionName,
|
|
312
|
+
sessionMode,
|
|
220
313
|
});
|
|
314
|
+
if (executionPlan.managedSessionName === freshSessionName) {
|
|
315
|
+
freshSessionOrdinal += 1;
|
|
316
|
+
}
|
|
221
317
|
|
|
222
318
|
if (executionPlan.validationError) {
|
|
223
319
|
return {
|
|
224
320
|
content: [{ type: "text", text: executionPlan.validationError }],
|
|
225
321
|
details: {
|
|
226
322
|
args: params.args,
|
|
323
|
+
invalidValueFlag: executionPlan.invalidValueFlag,
|
|
324
|
+
sessionMode,
|
|
325
|
+
sessionRecoveryHint: executionPlan.recoveryHint,
|
|
227
326
|
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
228
327
|
validationError: executionPlan.validationError,
|
|
229
328
|
},
|
|
@@ -235,6 +334,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
235
334
|
content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(executionPlan.effectiveArgs)}` }],
|
|
236
335
|
details: {
|
|
237
336
|
effectiveArgs: executionPlan.effectiveArgs,
|
|
337
|
+
sessionMode,
|
|
238
338
|
sessionName: executionPlan.sessionName,
|
|
239
339
|
usedImplicitSession: executionPlan.usedImplicitSession,
|
|
240
340
|
},
|
|
@@ -243,9 +343,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
243
343
|
const processResult = await runAgentBrowserProcess({
|
|
244
344
|
args: executionPlan.effectiveArgs,
|
|
245
345
|
cwd: ctx.cwd,
|
|
246
|
-
env: executionPlan.
|
|
247
|
-
? { AGENT_BROWSER_IDLE_TIMEOUT_MS: implicitSessionIdleTimeoutMs }
|
|
248
|
-
: undefined,
|
|
346
|
+
env: executionPlan.managedSessionName ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: implicitSessionIdleTimeoutMs } : undefined,
|
|
249
347
|
signal,
|
|
250
348
|
stdin: params.stdin,
|
|
251
349
|
});
|
|
@@ -257,6 +355,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
257
355
|
details: {
|
|
258
356
|
args: params.args,
|
|
259
357
|
effectiveArgs: executionPlan.effectiveArgs,
|
|
358
|
+
sessionMode,
|
|
260
359
|
spawnError: processResult.spawnError.message,
|
|
261
360
|
},
|
|
262
361
|
isError: true,
|
|
@@ -268,18 +367,49 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
268
367
|
stdout: processResult.stdout,
|
|
269
368
|
stdoutPath: processResult.stdoutSpillPath,
|
|
270
369
|
});
|
|
370
|
+
let presentationEnvelope = parsed.envelope;
|
|
271
371
|
const processSucceeded = !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
|
|
272
372
|
const plainTextInspection = isPlainTextInspectionArgs(params.args) && processSucceeded && parsed.parseError !== undefined;
|
|
273
373
|
const envelopeSuccess = plainTextInspection ? true : parsed.envelope?.success !== false;
|
|
274
374
|
const parseSucceeded = plainTextInspection || parsed.parseError === undefined;
|
|
275
375
|
const succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
|
|
276
376
|
|
|
277
|
-
|
|
377
|
+
let navigationSummary: NavigationSummary | undefined;
|
|
378
|
+
if (succeeded && shouldCaptureNavigationSummary(executionPlan.commandInfo.command, parsed.envelope?.data)) {
|
|
379
|
+
navigationSummary = await collectNavigationSummary({
|
|
380
|
+
cwd: ctx.cwd,
|
|
381
|
+
sessionName: executionPlan.sessionName,
|
|
382
|
+
signal,
|
|
383
|
+
});
|
|
384
|
+
if (navigationSummary && presentationEnvelope) {
|
|
385
|
+
presentationEnvelope = {
|
|
386
|
+
...presentationEnvelope,
|
|
387
|
+
data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const priorManagedSessionCwd = managedSessionCwd;
|
|
393
|
+
const managedSessionState = resolveManagedSessionState({
|
|
278
394
|
command: executionPlan.commandInfo.command,
|
|
279
|
-
|
|
395
|
+
managedSessionName: executionPlan.managedSessionName,
|
|
396
|
+
priorActive: managedSessionActive,
|
|
397
|
+
priorSessionName: managedSessionName,
|
|
280
398
|
succeeded,
|
|
281
|
-
usedImplicitSession: executionPlan.usedImplicitSession,
|
|
282
399
|
});
|
|
400
|
+
const replacedManagedSessionName = managedSessionState.replacedSessionName;
|
|
401
|
+
managedSessionActive = managedSessionState.active;
|
|
402
|
+
managedSessionName = managedSessionState.sessionName;
|
|
403
|
+
if (executionPlan.managedSessionName && succeeded) {
|
|
404
|
+
managedSessionCwd = ctx.cwd;
|
|
405
|
+
}
|
|
406
|
+
if (replacedManagedSessionName) {
|
|
407
|
+
await closeManagedSession({
|
|
408
|
+
cwd: priorManagedSessionCwd,
|
|
409
|
+
sessionName: replacedManagedSessionName,
|
|
410
|
+
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
283
413
|
|
|
284
414
|
const errorText = getAgentBrowserErrorText({
|
|
285
415
|
aborted: processResult.aborted,
|
|
@@ -300,7 +430,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
300
430
|
: await buildToolPresentation({
|
|
301
431
|
commandInfo: executionPlan.commandInfo,
|
|
302
432
|
cwd: ctx.cwd,
|
|
303
|
-
envelope:
|
|
433
|
+
envelope: presentationEnvelope,
|
|
304
434
|
errorText,
|
|
305
435
|
});
|
|
306
436
|
|
|
@@ -308,16 +438,23 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
308
438
|
content: presentation.content,
|
|
309
439
|
details: {
|
|
310
440
|
args: params.args,
|
|
441
|
+
batchFailure: presentation.batchFailure,
|
|
442
|
+
batchSteps: presentation.batchSteps,
|
|
311
443
|
command: executionPlan.commandInfo.command,
|
|
312
444
|
subcommand: executionPlan.commandInfo.subcommand,
|
|
313
445
|
data: presentation.data,
|
|
314
446
|
error: parsed.envelope?.error,
|
|
447
|
+
navigationSummary,
|
|
315
448
|
effectiveArgs: executionPlan.effectiveArgs,
|
|
316
449
|
exitCode: processResult.exitCode,
|
|
317
450
|
fullOutputPath: presentation.fullOutputPath,
|
|
451
|
+
fullOutputPaths: presentation.fullOutputPaths,
|
|
318
452
|
imagePath: presentation.imagePath,
|
|
453
|
+
imagePaths: presentation.imagePaths,
|
|
319
454
|
parseError: parsed.parseError,
|
|
455
|
+
sessionMode,
|
|
320
456
|
sessionName: executionPlan.sessionName,
|
|
457
|
+
sessionRecoveryHint: executionPlan.recoveryHint,
|
|
321
458
|
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
322
459
|
stderr: processResult.stderr || undefined,
|
|
323
460
|
stdout: parseSucceeded ? undefined : processResult.stdout,
|
|
@@ -10,6 +10,10 @@ import { readFile } from "node:fs/promises";
|
|
|
10
10
|
|
|
11
11
|
import { type AgentBrowserBatchResult, type AgentBrowserEnvelope, isRecord, stringifyUnknown } from "./shared.js";
|
|
12
12
|
|
|
13
|
+
function hasStructuredBatchStepFailure(data: unknown): data is AgentBrowserBatchResult[] {
|
|
14
|
+
return Array.isArray(data) && data.some((item) => isRecord(item) && item.success === false);
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
async function readEnvelopeSource(options: { stdout: string; stdoutPath?: string }): Promise<string> {
|
|
14
18
|
if (!options.stdoutPath) {
|
|
15
19
|
return options.stdout;
|
|
@@ -93,6 +97,9 @@ export function getAgentBrowserErrorText(options: {
|
|
|
93
97
|
if (spawnError) return spawnError.message;
|
|
94
98
|
if (parseError) return parseError;
|
|
95
99
|
if (envelope?.success === false) {
|
|
100
|
+
if (hasStructuredBatchStepFailure(envelope.data) && envelope.error === undefined) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
96
103
|
return extractEnvelopeErrorText(envelope.error) ?? (stderr.trim() || `agent-browser reported failure${exitCode !== 0 ? ` (exit code ${exitCode})` : "."}`);
|
|
97
104
|
}
|
|
98
105
|
if (exitCode !== 0) {
|