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.
@@ -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 implicit session convenience, and return pi-friendly content/details.
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
- resolveImplicitSessionActiveState,
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
- useActiveSession: Type.Optional(
37
- Type.Boolean({
38
- description: "When true and no explicit --session is present, inject the implicit session for this pi session.",
39
- default: true,
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 implicit session handle the common path unless you explicitly need upstream flags like --session, --profile, or --cdp.",
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 /(^|[\s;&|])(npx\s+)?agent-browser(\s|$)/.test(command);
102
+ return DIRECT_AGENT_BROWSER_BASH_PATTERN.test(command);
85
103
  }
86
104
 
87
105
  function isHarmlessAgentBrowserInspectionCommand(command: string): boolean {
88
- return /(command\s+-v|which)\s+agent-browser\b/.test(command) || /(^|\s)agent-browser\s+--(help|version)\b/.test(command);
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
- function buildInspectionDeflectionMessage(): string {
96
- return [
97
- "Do not inspect agent_browser help for a normal browser task.",
98
- "Use the workflow directly:",
99
- "1. open the target URL",
100
- "2. snapshot -i",
101
- "3. interact using refs and re-snapshot after navigation or major DOM changes",
102
- "For authenticated or user-specific content like feeds, inboxes, dashboards, or accounts, start with an authenticated strategy such as --profile Default on the first browser call and let the implicit session carry continuity. Use --auto-connect only if profile-based reuse is unavailable.",
103
- ].join("\n");
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 implicitSessionActive = false;
139
- let implicitSessionName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
140
- let implicitSessionCwd = process.cwd();
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
- implicitSessionActive = false;
144
- implicitSessionName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
145
- implicitSessionCwd = ctx.cwd;
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
- implicitSessionActive = false;
150
- const controller = new AbortController();
151
- const timer = setTimeout(() => controller.abort(), implicitSessionCloseTimeoutMs);
152
- try {
153
- await runAgentBrowserProcess({
154
- args: ["--session", implicitSessionName, "close"],
155
- cwd: implicitSessionCwd,
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
- implicitSessionActive,
218
- implicitSessionName,
219
- useActiveSession: params.useActiveSession ?? true,
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.usedImplicitSession
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
- implicitSessionActive = resolveImplicitSessionActiveState({
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
- priorActive: implicitSessionActive,
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: parsed.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) {