pi-agent-browser-native 0.6.17 → 0.7.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +55 -167
  3. package/dist/extensions/agent-browser/index.js +317 -223
  4. package/dist/extensions/agent-browser/lib/browser-transcript.js +33 -0
  5. package/dist/extensions/agent-browser/lib/execution-cwd.js +4 -2
  6. package/dist/extensions/agent-browser/lib/input-modes/job.js +2 -213
  7. package/dist/extensions/agent-browser/lib/input-modes/params.js +146 -142
  8. package/dist/extensions/agent-browser/lib/input-modes/script.js +64 -92
  9. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +170 -24
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +13 -1
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +26 -13
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +21 -1
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +4 -2
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -3
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +35 -20
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +3 -1
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +79 -0
  18. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -4
  19. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +19 -52
  20. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +3 -3
  21. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -3
  22. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +91 -278
  23. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +9 -9
  24. package/dist/extensions/agent-browser/lib/playbook.js +46 -25
  25. package/dist/extensions/agent-browser/lib/process-identity.js +22 -6
  26. package/dist/extensions/agent-browser/lib/process.js +16 -13
  27. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +2 -2
  28. package/dist/extensions/agent-browser/lib/results/next-actions.js +1 -2
  29. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +25 -4
  30. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +6 -2
  31. package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -0
  32. package/dist/extensions/agent-browser/lib/results/presentation/content.js +39 -0
  33. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +3 -3
  34. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +41 -1
  35. package/dist/extensions/agent-browser/lib/results/presentation.js +16 -11
  36. package/dist/extensions/agent-browser/lib/runtime.js +18 -6
  37. package/dist/extensions/agent-browser/lib/session-page-state.js +3 -4
  38. package/dist/extensions/agent-browser/lib/tool-surface.js +131 -0
  39. package/dist/extensions/agent-browser/script-worker.js +4 -1
  40. package/docs/ARCHITECTURE.md +46 -62
  41. package/docs/COMMAND_REFERENCE.md +77 -136
  42. package/docs/ELECTRON.md +51 -49
  43. package/docs/RELEASE.md +31 -15
  44. package/docs/REQUIREMENTS.md +18 -14
  45. package/docs/SUPPORT_MATRIX.md +33 -15
  46. package/docs/TOOL_CONTRACT.md +216 -405
  47. package/package.json +1 -1
  48. package/scripts/doctor.mjs +2 -2
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { dirname, join } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
@@ -5,14 +6,16 @@ import { Text } from "@earendil-works/pi-tui";
5
6
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./lib/batch-lifecycle.js";
6
7
  import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
7
8
  import { SessionPageState } from "./lib/session-page-state.js";
8
- import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactSensitiveText, redactSensitiveValue, isPlainTextInspectionArgs, } from "./lib/runtime.js";
9
+ import { appendBrowserTransition, getBrowserResultMessage } from "./lib/browser-transcript.js";
10
+ import { buildExecutionPlan, canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactInvocationArgs, redactSensitiveText, redactSensitiveValue, isPlainTextInspectionArgs, } from "./lib/runtime.js";
9
11
  import { deleteIdentityKeysInNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./lib/argv-grammar.js";
10
12
  import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
11
13
  import { needsManagedSession } from "./lib/command-policy.js";
12
14
  import { ManagedSessionRestoreState, resolveOwnedManagedSessionContext, withOwnedManagedSessionContext } from "./lib/managed-session-restore.js";
13
15
  import { isRecord } from "./lib/parsing.js";
14
- import { runAgentBrowserProcess } from "./lib/process.js";
15
- import { getAgentBrowserProcessEnvironment, withIsolatedAgentBrowserEnvironment } from "./lib/process-environment.js";
16
+ import { getAgentBrowserProcessTimeoutMs, runAgentBrowserProcess } from "./lib/process.js";
17
+ import { getCommandAwareProcessTimeoutMs } from "./lib/orchestration/browser-run/prepare/wait-timeouts.js";
18
+ import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment, withIsolatedAgentBrowserEnvironment } from "./lib/process-environment.js";
16
19
  import { withNativeSessionDefaults } from "./lib/orchestration/native-session-defaults.js";
17
20
  import { getBrowserCwdError, resolveExecutionCwd, restoreManagedSessionCwd } from "./lib/execution-cwd.js";
18
21
  import { resolveOperationPaths } from "./lib/orchestration/operation-paths.js";
@@ -22,15 +25,16 @@ import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt
22
25
  import { isCloseAllCommand, isCloseCommand } from "./lib/command-taxonomy.js";
23
26
  import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
24
27
  import { cleanupSecureTempArtifacts } from "./lib/temp.js";
25
- import { AGENT_BROWSER_PARAMS } from "./lib/input-modes/params.js";
26
- import { AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS, AGENT_BROWSER_SCRIPT_NAMESPACE, createAgentBrowserScriptSessionName, isAgentBrowserScriptSessionName, runAgentBrowserScript, } from "./lib/input-modes/script.js";
28
+ import { AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS, AGENT_BROWSER_SCRIPT_NAMESPACE, bindBrowserCodeCall, isAgentBrowserScriptSessionName, runAgentBrowserScript, } from "./lib/input-modes/script.js";
27
29
  import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination, getRecordContactSheetDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
28
- import { findElectronLaunchRecordForSession, getActiveElectronRecords, getSessionContextKey } from "./lib/orchestration/browser-run/session-state.js";
30
+ import { findElectronLaunchRecordForSession, getActiveElectronRecords, getPersistentSessionArtifactStore, getSessionContextKey } from "./lib/orchestration/browser-run/session-state.js";
29
31
  import { parseBatchCommandArgument, parseUserBatchStdin } from "./lib/orchestration/batch-stdin.js";
30
32
  import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
31
33
  import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
32
34
  import { applyAgentBrowserOutputPath, canWriteAgentBrowserOutput, normalizeRequestedOutputPath } from "./lib/orchestration/output-file.js";
33
- import { appendScriptSessionLease, buildScriptBrowserEnvelope, buildScriptToolResult, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
35
+ import { appendScriptSessionLease, createBrowserCodeOutput, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
36
+ import { resolveBrowserExecutionIdentity, withBrowserExecutionLock, withBrowserExecutionLocks } from "./lib/managed-session-policy-lock.js";
37
+ import { registerAgentBrowserToolSurface } from "./lib/tool-surface.js";
34
38
  import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingCommand, isSessionArtifactManifest, mergeSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
35
39
  import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, applySessionToNextActions, buildNextToolAction } from "./lib/results/next-actions.js";
36
40
  import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
@@ -39,6 +43,12 @@ import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
39
43
  import { scopeReadConfirmationArgs } from "./lib/read-confirmation.js";
40
44
  import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
41
45
  import { AgentBrowserResultComponent, buildAgentBrowserToolResultPatch, formatAgentBrowserRenderCall, formatAgentBrowserRenderResult, } from "./lib/pi-tool-rendering.js";
46
+ function browserExecutionFailure(error, signal, fallbackCategory = "upstream-error") {
47
+ const timeout = error instanceof Error && error.name === "TimeoutError";
48
+ const summary = redactSensitiveText(error instanceof Error ? error.message : String(error));
49
+ const failureCategory = timeout ? "timeout" : signal?.aborted ? "aborted" : fallbackCategory;
50
+ return { content: [{ type: "text", text: summary }], details: { error: summary, summary, resultCategory: "failure", failureCategory }, isError: true };
51
+ }
42
52
  function isBashToolCallEvent(event) {
43
53
  if (!isRecord(event) || event.toolName !== "bash" || !isRecord(event.input))
44
54
  return false;
@@ -167,10 +177,8 @@ function getResultFileArtifacts(result) {
167
177
  function restoreArtifactManifestFromBranch(branch) {
168
178
  let restoredManifest;
169
179
  for (const entry of branch) {
170
- if (!isRecord(entry) || entry.type !== "message")
171
- continue;
172
- const message = isRecord(entry.message) ? entry.message : undefined;
173
- if (!message || message.toolName !== "agent_browser")
180
+ const message = getBrowserResultMessage(entry);
181
+ if (!message)
174
182
  continue;
175
183
  const details = isRecord(message.details) ? message.details : undefined;
176
184
  if (isSessionArtifactManifest(details?.artifactManifest) && (!restoredManifest || details.artifactManifest.updatedAtMs >= restoredManifest.updatedAtMs)) {
@@ -189,10 +197,8 @@ function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionN
189
197
  let restored;
190
198
  const targetKey = getSessionContextKey(sessionName, namespace);
191
199
  for (const entry of branch) {
192
- if (!isRecord(entry) || entry.type !== "message")
193
- continue;
194
- const message = isRecord(entry.message) ? entry.message : undefined;
195
- if (!message || message.toolName !== "agent_browser")
200
+ const message = getBrowserResultMessage(entry);
201
+ if (!message)
196
202
  continue;
197
203
  const details = isRecord(message.details) ? message.details : undefined;
198
204
  if (!details)
@@ -222,10 +228,8 @@ function restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, sessionNa
222
228
  let restored = false;
223
229
  const targetKey = getSessionContextKey(sessionName, namespace);
224
230
  for (const entry of branch) {
225
- if (!isRecord(entry) || entry.type !== "message")
226
- continue;
227
- const message = isRecord(entry.message) ? entry.message : undefined;
228
- if (!message || message.toolName !== "agent_browser")
231
+ const message = getBrowserResultMessage(entry);
232
+ if (!message)
229
233
  continue;
230
234
  const details = isRecord(message.details) ? message.details : undefined;
231
235
  if (!details)
@@ -246,10 +250,8 @@ function restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, sessionNa
246
250
  let restored;
247
251
  const targetKey = getSessionContextKey(sessionName, namespace);
248
252
  for (const entry of branch) {
249
- if (!isRecord(entry) || entry.type !== "message")
250
- continue;
251
- const message = isRecord(entry.message) ? entry.message : undefined;
252
- if (!message || message.toolName !== "agent_browser")
253
+ const message = getBrowserResultMessage(entry);
254
+ if (!message)
253
255
  continue;
254
256
  const details = isRecord(message.details) ? message.details : undefined;
255
257
  if (!details)
@@ -293,10 +295,8 @@ function isAttachedBrowserInvocation(args, env = getAgentBrowserProcessEnvironme
293
295
  function restoreAttachedSessionKeysFromBranch(branch) {
294
296
  const attachedSessionKeys = new Set();
295
297
  for (const entry of branch) {
296
- if (!isRecord(entry) || entry.type !== "message")
297
- continue;
298
- const message = isRecord(entry.message) ? entry.message : undefined;
299
- if (!message || message.toolName !== "agent_browser")
298
+ const message = getBrowserResultMessage(entry);
299
+ if (!message)
300
300
  continue;
301
301
  const details = isRecord(message.details) ? message.details : undefined;
302
302
  if (!details)
@@ -539,10 +539,8 @@ function collectBranchManagedResourceEvents(branch) {
539
539
  };
540
540
  let eventRank = 0;
541
541
  for (const entry of branch) {
542
- if (!isRecord(entry) || entry.type !== "message")
543
- continue;
544
- const message = isRecord(entry.message) ? entry.message : undefined;
545
- if (!message || message.toolName !== "agent_browser")
542
+ const message = getBrowserResultMessage(entry);
543
+ if (!message)
546
544
  continue;
547
545
  const details = isRecord(message.details) ? message.details : undefined;
548
546
  if (!details)
@@ -661,36 +659,56 @@ function shouldSerializeBrowserCommand(options) {
661
659
  // Serializes managed-session read/modify/write work so overlapping tool calls cannot promote stale state or close an in-use session.
662
660
  class AsyncExecutionQueue {
663
661
  tail = Promise.resolve();
664
- run(work) {
662
+ active = new AsyncLocalStorage();
663
+ isCurrent() { return this.active.getStore()?.active === true; }
664
+ run(work, signal, barrier = Promise.resolve()) {
665
+ if (this.isCurrent()) {
666
+ signal?.throwIfAborted();
667
+ return work();
668
+ }
665
669
  const previous = this.tail;
666
670
  let release;
667
671
  this.tail = new Promise((resolve) => {
668
672
  release = resolve;
669
673
  });
670
- return (async () => {
671
- await previous;
674
+ let rejectWaiting;
675
+ const cancelled = new Promise((_resolve, reject) => { rejectWaiting = reject; });
676
+ const abortWaiting = () => rejectWaiting(signal?.reason);
677
+ signal?.addEventListener("abort", abortWaiting, { once: true });
678
+ if (signal?.aborted)
679
+ abortWaiting();
680
+ const execution = (async () => {
681
+ await Promise.all([previous, barrier]);
682
+ signal?.removeEventListener("abort", abortWaiting);
672
683
  try {
673
- return await work();
684
+ signal?.throwIfAborted();
685
+ const scope = { active: true };
686
+ try {
687
+ return await this.active.run(scope, work);
688
+ }
689
+ finally {
690
+ scope.active = false;
691
+ }
674
692
  }
675
693
  finally {
676
694
  release();
677
695
  }
678
696
  })();
697
+ return signal ? Promise.race([execution, cancelled]) : execution;
679
698
  }
680
699
  }
681
700
  export class KeyedAsyncExecutionQueue {
682
701
  barriers = new Map();
683
702
  entries = new Map();
684
- async run(key, namespace, work) {
703
+ async run(key, namespace, work, signal) {
685
704
  const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
705
+ if (entry.queue.isCurrent())
706
+ return work();
686
707
  const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
687
708
  entry.users += 1;
688
709
  this.entries.set(key, entry);
689
710
  try {
690
- return await entry.queue.run(async () => {
691
- await barrier;
692
- return await work();
693
- });
711
+ return await entry.queue.run(work, signal, barrier);
694
712
  }
695
713
  finally {
696
714
  entry.users -= 1;
@@ -1228,17 +1246,18 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1228
1246
  cwd: ctx.cwd,
1229
1247
  includeProjectConfig: shouldIncludeProjectConfig(ctx),
1230
1248
  }));
1231
- await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
1249
+ await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1232
1250
  await recoverScriptSessionLeasesWithinQueue(ctx);
1233
1251
  flushRecordingReservations();
1234
1252
  notifyRecordingPersistence(ctx);
1235
1253
  }));
1236
1254
  });
1237
1255
  pi.on("session_tree", async (_event, ctx) => {
1256
+ branchRestoreGeneration += 1;
1238
1257
  for (const controller of activeScriptControllers)
1239
1258
  controller.abort();
1240
1259
  await Promise.allSettled([...activeScriptExecutions]);
1241
- await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
1260
+ await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1242
1261
  restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
1243
1262
  await recoverScriptSessionLeasesWithinQueue(ctx);
1244
1263
  flushRecordingReservations();
@@ -1246,13 +1265,13 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1246
1265
  }));
1247
1266
  });
1248
1267
  // Fork-only checkpoint event; official Pi validation types do not declare it.
1249
- // Pi owns awaited tools/events (including their execution queues). Only script
1250
- // children and retained browser/recording resources need extension-side checks.
1268
+ // Pi owns awaited tools/events (including their execution queues). Detached code
1269
+ // children and retained browser/recording resources still need extension-side checks.
1251
1270
  const checkpointPi = pi;
1252
1271
  checkpointPi.on("session_checkpoint", async (event, ctx) => {
1253
1272
  const blocked = (reason) => ({ sleepReady: false, reason });
1254
1273
  if (activeScriptControllers.size || activeScriptExecutions.size)
1255
- return blocked("Browser script execution or cleanup is pending");
1274
+ return blocked("Browser code execution is pending");
1256
1275
  flushRecordingReservations();
1257
1276
  if (recordingReservationsDirty)
1258
1277
  return blocked("Browser recording journal persistence is dirty");
@@ -1278,9 +1297,10 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1278
1297
  if (managedSessionActive)
1279
1298
  trackOwnedManagedSession(sessions, managedSessionName, managedSessionCwd, { namespace: managedSessionNamespace });
1280
1299
  for (const entry of entries) {
1281
- if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.toolName !== "agent_browser")
1300
+ const message = getBrowserResultMessage(entry);
1301
+ if (!message)
1282
1302
  continue;
1283
- const details = isRecord(entry.message.details) ? entry.message.details : undefined;
1303
+ const details = isRecord(message.details) ? message.details : undefined;
1284
1304
  // Helpers can launch before a main-process failure. Conversely, native
1285
1305
  // launch flags on sessionless reads can open the unnamed default browser.
1286
1306
  const sessionName = typeof details?.sessionName === "string" ? details.sessionName
@@ -1324,7 +1344,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1324
1344
  branchRestoreGeneration += 1;
1325
1345
  branchStateGeneration += 1;
1326
1346
  let preservedElectronProfileDirs = [];
1327
- await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
1347
+ await managedSessionExecutionQueue.run(() => artifactExecutionQueue.run(async () => {
1328
1348
  const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
1329
1349
  const quitting = event?.reason === "quit";
1330
1350
  preservedElectronProfileDirs = quitting
@@ -1398,9 +1418,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1398
1418
  const runtimeConfigPrompt = browserGuidance.length > 0
1399
1419
  ? `\n\nProject agent_browser config guidance:\n${browserGuidance.map((line) => `- ${line}`).join("\n")}`
1400
1420
  : "";
1401
- return {
1402
- systemPrompt: `${event.systemPrompt}\n\n${PROJECT_RULE_PROMPT}${runtimeConfigPrompt}`,
1403
- };
1421
+ event.systemPromptOptions.sections.agent_browser = `${PROJECT_RULE_PROMPT}${runtimeConfigPrompt}`;
1404
1422
  });
1405
1423
  pi.on("tool_call", async (event, ctx) => {
1406
1424
  const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
@@ -1417,12 +1435,6 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1417
1435
  });
1418
1436
  pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
1419
1437
  const agentBrowserTool = {
1420
- name: "agent_browser",
1421
- label: "Agent Browser",
1422
- description: "Browse and interact with websites using agent-browser. Use this for reading live pages, opening known URLs, taking snapshots or screenshots, clicking links, filling forms, extracting page content, and authenticated/profile-based browser work. Input choice: `script` for one-shot JavaScript orchestration; default `args` for open → snapshot -i → click/fill @refs; `semanticAction` for stable role/text/label targets; `job` or `qa` for multi-step checks; `electron` only for desktop apps; experimental `sourceLookup` / `networkSourceLookup` for candidates only.",
1423
- promptSnippet: "Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
1424
- promptGuidelines: toolPromptGuidelines,
1425
- parameters: AGENT_BROWSER_PARAMS,
1426
1438
  renderCall(args, theme, context) {
1427
1439
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1428
1440
  text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
@@ -1435,7 +1447,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1435
1447
  component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
1436
1448
  return component;
1437
1449
  },
1438
- async execute(toolCallId, params, signal, onUpdate, ctx, nativeToolCallId = toolCallId, capturedCwd) {
1450
+ async execute(toolCallId, params, signal, onUpdate, ctx, nativeToolCallId = toolCallId, capturedCwd, modelVisible = true) {
1439
1451
  let operationCwd;
1440
1452
  try {
1441
1453
  operationCwd = capturedCwd ?? resolveExecutionCwd(pi, ctx);
@@ -1454,11 +1466,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1454
1466
  }
1455
1467
  const explicitConfig = scanUpstreamGlobalFlagOccurrences(admittedInput.toolArgs, "--config").length > 0 || getAgentBrowserProcessEnvironment().AGENT_BROWSER_CONFIG !== undefined;
1456
1468
  // Managed selection must follow earlier queued fresh launches, while file paths keep their admission snapshot.
1457
- const managedAtAdmission = admittedInput.kind !== "script" && admittedInput.kind !== "electron"
1469
+ const managedAtAdmission = admittedInput.kind !== "electron"
1458
1470
  && !extractExplicitSessionName(admittedInput.toolArgs) && !explicitConfig;
1459
- const artifactsAtAdmission = managedAtAdmission && commandTouchesArtifactLifecycle(admittedInput.toolArgs, admittedInput.toolStdin, outputPath);
1460
- const withManagedQueue = (run) => managedAtAdmission ? run() : managedSessionExecutionQueue.run(run);
1461
- const withArtifactQueue = (run) => artifactsAtAdmission ? run() : artifactExecutionQueue.run(run);
1462
1471
  const executeAdmitted = async () => {
1463
1472
  let resolvedInput = admittedInput;
1464
1473
  const requestedSession = extractExplicitSessionName(resolvedInput.toolArgs);
@@ -1473,8 +1482,8 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1473
1482
  const bound = resolveOperationPaths(resolvedInput.toolArgs, resolvedInput.toolStdin, operationCwd);
1474
1483
  resolvedInput = { ...resolvedInput, toolArgs: bound.args, toolStdin: bound.stdin };
1475
1484
  }
1476
- if (resolvedInput.kind !== "script")
1477
- await beforeExecute?.(nativeToolCallId, { ...ctx, signal });
1485
+ const executionTimeoutMs = (resolvedInput.kind === "electron" && "timeoutMs" in resolvedInput.compiledElectron ? resolvedInput.compiledElectron.timeoutMs : undefined) ?? params.timeoutMs ?? getCommandAwareProcessTimeoutMs(extractUpstreamCommandTokens(resolvedInput.toolArgs), resolvedInput.toolStdin) ?? getAgentBrowserProcessTimeoutMs();
1486
+ await beforeExecute?.(nativeToolCallId, { ...ctx, signal });
1478
1487
  const runtimeBrowserConfig = loadAgentBrowserConfigSync({ cwd: ctx.cwd, includeProjectConfig: shouldIncludeProjectConfig(ctx) });
1479
1488
  const rootProfile = runtimeBrowserConfig.trustedBrowserDefaultProfile;
1480
1489
  const pendingReadConfirmation = sessionPageState.findReadConfirmation(resolvedInput.toolArgs, resolveAgentBrowserNamespace(resolvedInput.toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE));
@@ -1490,7 +1499,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1490
1499
  const applyUnserializedOutputPath = async (result, preserveTextContent = false) => {
1491
1500
  if (!outputPath || !canWriteAgentBrowserOutput(result))
1492
1501
  return warnRecordingPersistence(result);
1493
- return withArtifactQueue(async () => {
1502
+ return artifactExecutionQueue.run(async () => {
1494
1503
  flushRecordingReservations();
1495
1504
  const reservationError = getArtifactPreflightValidationError({
1496
1505
  activeRecordingReservations: activeRecordingReservations.values(),
@@ -1507,118 +1516,15 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1507
1516
  const versionCheckCommand = extractUpstreamCommandTokens(resolvedInput.toolArgs)[0];
1508
1517
  const electronHostOnlyAction = resolvedInput.kind === "electron" && ["cleanup", "list", "status"].includes(resolvedInput.compiledElectron.action);
1509
1518
  const browserBackedVersionCheck = readConfirmation?.capabilities?.readRequiresConfirmation !== true && needsManagedSession(parseArgvDescriptor(resolvedInput.toolArgs), resolvedInput.toolStdin);
1510
- if (resolvedInput.kind !== "script" && !electronHostOnlyAction && browserBackedVersionCheck && !isPlainTextInspectionArgs(resolvedInput.toolArgs) && !isCloseCommand(versionCheckCommand) && signal?.aborted !== true) {
1519
+ if (!electronHostOnlyAction && browserBackedVersionCheck && !isPlainTextInspectionArgs(resolvedInput.toolArgs) && !isCloseCommand(versionCheckCommand) && signal?.aborted !== true) {
1511
1520
  const versionFailure = await validateUpstreamVersion(browserCwd, signal);
1512
1521
  if (versionFailure)
1513
1522
  return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, result: versionFailure });
1514
1523
  }
1515
- if (resolvedInput.kind === "script") {
1516
- if (!ctx.sessionManager.getSessionFile()) {
1517
- return buildValidationFailureResult({
1518
- attemptedKind: "script",
1519
- kind: "invalid",
1520
- redactedArgs: [],
1521
- status: "invalid",
1522
- toolArgs: [],
1523
- validationError: "script requires a persisted Pi session so its isolated browser-session cleanup lease survives restart; relaunch Pi without --no-session.",
1524
- });
1525
- }
1526
- const sessionName = createAgentBrowserScriptSessionName();
1527
- const innerResults = [];
1528
- const scriptTimeoutMs = params.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS;
1529
- const deadline = Date.now() + scriptTimeoutMs;
1530
- let leased = false;
1531
- let cleanupError;
1532
- let run = {
1533
- callCount: 0,
1534
- emitCount: 0,
1535
- error: "Script sandbox execution failed.",
1536
- failureCategory: "upstream-error",
1537
- ok: false,
1538
- rejectedCallCount: 0,
1539
- steps: [],
1540
- };
1541
- const scriptController = new AbortController();
1542
- const abortScript = () => scriptController.abort();
1543
- signal?.addEventListener("abort", abortScript, { once: true });
1544
- if (signal?.aborted)
1545
- scriptController.abort();
1546
- activeScriptControllers.add(scriptController);
1547
- let finishScriptExecution;
1548
- const scriptExecution = new Promise((resolve) => {
1549
- finishScriptExecution = resolve;
1550
- });
1551
- activeScriptExecutions.add(scriptExecution);
1552
- try {
1553
- // Keep preflight inside shutdown tracking so quit cannot race into starting the sandbox afterward.
1554
- const versionFailure = await withIsolatedAgentBrowserEnvironment(() => validateUpstreamVersion(browserCwd, scriptController.signal));
1555
- if (versionFailure)
1556
- return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, result: versionFailure });
1557
- const pendingRun = runAgentBrowserScript({
1558
- beforeFirstCall() {
1559
- appendScriptSessionLease(pi, sessionName, "active");
1560
- trackOwnedManagedSession(ownedManagedSessions, sessionName, ctx.cwd, { namespace: AGENT_BROWSER_SCRIPT_NAMESPACE });
1561
- managedSessionRestoreState.disable(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
1562
- // This fresh, unselectable lease owns any daemon its first read starts.
1563
- managedSessionRestoreState.recordDaemonRestoreKey(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE, null);
1564
- leased = true;
1565
- },
1566
- code: resolvedInput.compiledScript.code,
1567
- dispatch: async (innerParams, innerSignal) => {
1568
- const remainingMs = Math.max(1, deadline - Date.now());
1569
- const innerTimeoutMs = Math.min(innerParams.timeoutMs ?? remainingMs, remainingMs);
1570
- const innerResult = await withIsolatedAgentBrowserEnvironment(() => agentBrowserTool.execute(`${toolCallId}:script:${innerResults.length + 1}`, {
1571
- args: ["--namespace", AGENT_BROWSER_SCRIPT_NAMESPACE, "--session", sessionName, ...innerParams.args],
1572
- stdin: innerParams.stdin,
1573
- timeoutMs: innerTimeoutMs,
1574
- }, innerSignal, undefined, ctx, nativeToolCallId, operationCwd));
1575
- innerResults.push(innerResult);
1576
- return await buildScriptBrowserEnvelope(innerResult, innerParams.args, sessionName);
1577
- },
1578
- signal: scriptController.signal,
1579
- timeoutMs: scriptTimeoutMs,
1580
- });
1581
- run = await pendingRun;
1582
- }
1583
- catch { }
1584
- finally {
1585
- activeScriptControllers.delete(scriptController);
1586
- signal?.removeEventListener("abort", abortScript);
1587
- if (leased) {
1588
- try {
1589
- cleanupError = await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => {
1590
- flushRecordingReservations();
1591
- return closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd);
1592
- }));
1593
- }
1594
- catch {
1595
- cleanupError = "The isolated script session cleanup operation failed.";
1596
- try {
1597
- appendScriptSessionLease(pi, sessionName, "failed");
1598
- }
1599
- catch { }
1600
- }
1601
- }
1602
- activeScriptExecutions.delete(scriptExecution);
1603
- finishScriptExecution();
1604
- }
1605
- let scriptResult = buildScriptToolResult({ cleanupError, innerResults, run, sessionName: leased ? sessionName : undefined });
1606
- if (artifactManifest) {
1607
- scriptResult = {
1608
- ...scriptResult,
1609
- details: {
1610
- ...(isRecord(scriptResult.details) ? scriptResult.details : {}),
1611
- artifactManifest: redactSensitiveValue(artifactManifest),
1612
- artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
1613
- },
1614
- };
1615
- }
1616
- return applyUnserializedOutputPath(scriptResult);
1617
- }
1618
1524
  const { toolArgs } = resolvedInput;
1619
1525
  const compiledElectron = resolvedInput.kind === "electron" ? resolvedInput.compiledElectron : undefined;
1620
1526
  const redactedCompiledElectron = resolvedInput.kind === "electron" ? resolvedInput.redactedCompiledElectron : undefined;
1621
- const runElectronHostInput = async () => {
1527
+ const runElectronHostInput = async (executionSignal = signal) => {
1622
1528
  const electronHostLaunchRecords = getElectronHostLaunchRecordsForInput({
1623
1529
  branchRecords: electronLaunchRecords,
1624
1530
  compiledElectron,
@@ -1638,7 +1544,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1638
1544
  ownedManagedSessions,
1639
1545
  redactedCompiledElectron,
1640
1546
  sessionPageState,
1641
- signal,
1547
+ signal: executionSignal,
1642
1548
  });
1643
1549
  if (electronHostResult && compiledElectron?.action === "cleanup") {
1644
1550
  branchStateGeneration += 1;
@@ -1686,27 +1592,43 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1686
1592
  };
1687
1593
  }
1688
1594
  }
1595
+ if (electronHostResult && executionSignal?.aborted) {
1596
+ const failure = browserExecutionFailure(executionSignal.reason, executionSignal);
1597
+ return { ...electronHostResult, ...failure, details: { ...(isRecord(electronHostResult.details) ? electronHostResult.details : {}), ...(isRecord(failure.details) ? failure.details : {}) } };
1598
+ }
1689
1599
  return electronHostResult;
1690
1600
  };
1691
- const runSerializedElectronHostInput = () => shouldSerializeElectronHostInput(compiledElectron)
1692
- ? managedSessionExecutionQueue.run(runElectronHostInput)
1693
- : runElectronHostInput();
1694
- const electronHostResult = compiledElectron?.action === "cleanup"
1695
- ? await artifactExecutionQueue.run(async () => {
1696
- flushRecordingReservations();
1697
- const reservationError = outputPath ? getArtifactPreflightValidationError({
1698
- activeRecordingReservations: activeRecordingReservations.values(),
1699
- args: [],
1700
- cwd: operationCwd,
1701
- outputPath,
1702
- }) : undefined;
1703
- if (reservationError) {
1704
- return warnRecordingPersistence(buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError }));
1705
- }
1706
- const result = await runSerializedElectronHostInput();
1707
- return result ? applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, result: warnRecordingPersistence(result) }) : result;
1708
- })
1709
- : await runSerializedElectronHostInput();
1601
+ const runCoordinatedElectronHostInput = async () => {
1602
+ const execute = (executionSignal = signal) => compiledElectron?.action === "cleanup"
1603
+ ? artifactExecutionQueue.run(async () => {
1604
+ flushRecordingReservations();
1605
+ const error = outputPath ? getArtifactPreflightValidationError({ activeRecordingReservations: activeRecordingReservations.values(), args: [], cwd: operationCwd, outputPath }) : undefined;
1606
+ if (error)
1607
+ return warnRecordingPersistence(buildValidationFailureResult({ ...resolvedInput, attemptedKind: resolvedInput.kind, kind: "invalid", status: "invalid", validationError: error }));
1608
+ const result = await runElectronHostInput(executionSignal);
1609
+ return result ? applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, result: warnRecordingPersistence(result) }) : result;
1610
+ }) : runElectronHostInput(executionSignal);
1611
+ if (!shouldSerializeElectronHostInput(compiledElectron))
1612
+ return execute();
1613
+ const records = getElectronHostLaunchRecordsForInput({ branchRecords: electronLaunchRecords, compiledElectron, ownedRecords: ownedElectronLaunchRecords });
1614
+ const launchId = compiledElectron && "launchId" in compiledElectron ? compiledElectron.launchId : undefined;
1615
+ const selected = launchId ? [records.get(launchId)].filter((record) => record !== undefined) : getActiveElectronRecords(records);
1616
+ const sessions = selected.flatMap(record => record.sessionName ? [{ sessionName: record.sessionName, namespace: record.namespace }] : []);
1617
+ if (compiledElectron?.action === "probe" && !launchId && managedSessionActive)
1618
+ sessions.push({ sessionName: managedSessionName, namespace: managedSessionNamespace });
1619
+ if (sessions.length === 0)
1620
+ return execute();
1621
+ try {
1622
+ const identities = await Promise.all(sessions.map(session => resolveBrowserExecutionIdentity({ ...session, ownedManagedSession: true })));
1623
+ return await withBrowserExecutionLocks({ identities, signal, deadline: Date.now() + executionTimeoutMs, waitOnly: true }, execute);
1624
+ }
1625
+ catch (error) {
1626
+ return browserExecutionFailure(error, signal);
1627
+ }
1628
+ };
1629
+ const electronHostResult = await (shouldSerializeElectronHostInput(compiledElectron)
1630
+ ? managedSessionExecutionQueue.run(runCoordinatedElectronHostInput, signal)
1631
+ : runCoordinatedElectronHostInput());
1710
1632
  if (electronHostResult) {
1711
1633
  return compiledElectron?.action === "cleanup" ? electronHostResult : applyUnserializedOutputPath(electronHostResult);
1712
1634
  }
@@ -1724,7 +1646,7 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1724
1646
  const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
1725
1647
  ? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
1726
1648
  : undefined;
1727
- const runBrowserCommand = async (daemonInactive) => {
1649
+ const runBrowserCommand = async (daemonInactive, executionSignal = signal) => {
1728
1650
  // Load execution-only preparation/output code inside the existing session queue,
1729
1651
  // before capturing state. Registration, rendering, and restore stay synchronous/eager.
1730
1652
  const { runAgentBrowserTool } = await import("./lib/orchestration/browser-run/index.js");
@@ -1786,18 +1708,20 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1786
1708
  implicitSessionCloseTimeoutMs,
1787
1709
  implicitSessionIdleTimeoutMs,
1788
1710
  input: resolvedInput,
1711
+ modelVisible,
1789
1712
  onUpdate,
1790
1713
  params,
1791
1714
  establishAttachedBrowserSession: attachedSessionRequested && !attachedSessionKnown,
1792
1715
  preserveAttachedBrowserSession: attachedSessionRequested || attachedSessionKnown,
1793
1716
  promptPolicy,
1794
1717
  sessionPageStateUpdate,
1795
- signal,
1718
+ signal: executionSignal,
1796
1719
  state: browserRunState,
1797
1720
  });
1798
1721
  const branchRestoreStillCurrent = branchRestoreGenerationAtStart === branchRestoreGeneration;
1799
1722
  const resultDetails = isRecord(result.details) ? result.details : undefined;
1800
- if (resultDetails?.sessionName === browserRunState.managedSessionName && resultDetails.namespace === browserRunState.managedSessionNamespace && browserRunState.managedSessionActive) {
1723
+ if (typeof resultDetails?.sessionName === "string" && browserRunState.managedSessionActive
1724
+ && getAgentBrowserSessionIdentityKey(resultDetails.sessionName, typeof resultDetails.namespace === "string" ? resultDetails.namespace : undefined) === getAgentBrowserSessionIdentityKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace)) {
1801
1725
  resultDetails.managedSessionCwd = browserRunState.managedSessionCwd;
1802
1726
  }
1803
1727
  const resultSessionName = typeof resultDetails?.sessionName === "string"
@@ -1889,10 +1813,47 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1889
1813
  return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result: warnRecordingPersistence(result) });
1890
1814
  };
1891
1815
  const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
1892
- const runWithLaunchDefaults = () => withLaunchDefaults ? withLaunchDefaults(runBrowserCommand) : runBrowserCommand();
1816
+ const runWithLaunchDefaults = async () => {
1817
+ // Electron's browser identity is known before its host process supplies the CDP port.
1818
+ const plan = buildExecutionPlan(compiledElectron?.action === "launch" ? ["connect"] : toolArgs, {
1819
+ freshSessionName: createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1),
1820
+ managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionName, managedSessionNamespace,
1821
+ sessionMode: compiledElectron?.action === "launch" ? "fresh" : params.sessionMode ?? "auto",
1822
+ stdin: resolvedInput.toolStdin,
1823
+ });
1824
+ const run = (executionSignal = signal) => {
1825
+ const execute = () => withLaunchDefaults
1826
+ ? withLaunchDefaults(inactive => runBrowserCommand(inactive, executionSignal), executionSignal)
1827
+ : runBrowserCommand(undefined, executionSignal);
1828
+ if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
1829
+ return execute();
1830
+ return artifactExecutionQueue.run(async () => {
1831
+ const error = getArtifactPreflightValidationError({ activeRecordingReservations: activeRecordingReservations.values(), args: toolArgs, cwd: operationCwd, outputPath, stdin: resolvedInput.toolStdin });
1832
+ if (!error)
1833
+ return execute();
1834
+ flushRecordingReservations();
1835
+ return warnRecordingPersistence(buildValidationFailureResult({ ...resolvedInput, attemptedKind: resolvedInput.kind, kind: "invalid", status: "invalid", validationError: error }));
1836
+ });
1837
+ };
1838
+ if (plan.validationError || (!plan.sessionName && plan.commandInfo.command !== "read" && !closesAllSessions))
1839
+ return run();
1840
+ try {
1841
+ const owned = plan.managedSessionName !== undefined || ownedManagedSessions.has(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace));
1842
+ const identities = [await resolveBrowserExecutionIdentity({ namespace: plan.namespace, ownedManagedSession: owned, sessionName: closesAllSessions ? undefined : plan.sessionName ?? "default" })];
1843
+ if (managedSessionActive && (params.sessionMode === "fresh" || compiledElectron?.action === "launch") && !explicitSessionName) {
1844
+ identities.push(await resolveBrowserExecutionIdentity({ namespace: managedSessionNamespace, ownedManagedSession: true, sessionName: managedSessionName }));
1845
+ }
1846
+ const activePageUrl = sessionPageState.get(getAgentBrowserSessionIdentityKey(plan.sessionName ?? "default", plan.namespace)).tabTarget?.url;
1847
+ const deadline = Date.now() + (params.timeoutMs ?? getCommandAwareProcessTimeoutMs(extractUpstreamCommandTokens(toolArgs), resolvedInput.toolStdin, activePageUrl) ?? getAgentBrowserProcessTimeoutMs());
1848
+ return await withBrowserExecutionLocks({ identities, signal, deadline, waitOnly: true }, run);
1849
+ }
1850
+ catch (error) {
1851
+ return browserExecutionFailure(error, signal);
1852
+ }
1853
+ };
1893
1854
  const runWithinSessionQueue = () => {
1894
1855
  if (closesAllSessions)
1895
- return withManagedQueue(() => {
1856
+ return managedSessionExecutionQueue.run(() => {
1896
1857
  const plan = buildExecutionPlan(toolArgs, {
1897
1858
  freshSessionName: createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1),
1898
1859
  managedSessionActive,
@@ -1905,34 +1866,12 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1905
1866
  return callerOwnedSessionExecutionQueues.runExclusive(plan.namespace, runWithLaunchDefaults);
1906
1867
  });
1907
1868
  if (serializeBrowserCommand)
1908
- return withManagedQueue(runWithLaunchDefaults);
1869
+ return managedSessionExecutionQueue.run(runWithLaunchDefaults, signal);
1909
1870
  return callerOwnedSessionQueueKey
1910
- ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace, runWithLaunchDefaults)
1871
+ ? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace, runWithLaunchDefaults, signal)
1911
1872
  : runWithLaunchDefaults();
1912
1873
  };
1913
- if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
1914
- return runWithinSessionQueue();
1915
- return withArtifactQueue(async () => {
1916
- const artifactValidationError = getArtifactPreflightValidationError({
1917
- activeRecordingReservations: activeRecordingReservations.values(),
1918
- args: toolArgs,
1919
- cwd: operationCwd,
1920
- outputPath,
1921
- stdin: resolvedInput.toolStdin,
1922
- });
1923
- if (!artifactValidationError)
1924
- return runWithinSessionQueue();
1925
- flushRecordingReservations();
1926
- return warnRecordingPersistence(buildValidationFailureResult({
1927
- attemptedKind: resolvedInput.kind,
1928
- kind: "invalid",
1929
- redactedArgs: resolvedInput.redactedArgs,
1930
- status: "invalid",
1931
- toolArgs: resolvedInput.toolArgs,
1932
- toolStdin: resolvedInput.toolStdin,
1933
- validationError: artifactValidationError,
1934
- }));
1935
- });
1874
+ return runWithinSessionQueue();
1936
1875
  }, managedSessionActive || freshSessionOrdinal > 0 || params.sessionMode === "fresh" || pendingReadConfirmation ? undefined : {
1937
1876
  id: rootSessionId,
1938
1877
  profile: rootProfile?.policy === "always" && !/[\\/~]/.test(rootProfile.name) ? rootProfile.name : undefined,
@@ -1941,10 +1880,165 @@ export default function agentBrowserExtension(pi, { beforeExecute } = {}) {
1941
1880
  };
1942
1881
  if (!managedAtAdmission)
1943
1882
  return executeAdmitted();
1944
- const executeManaged = () => managedSessionExecutionQueue.run(executeAdmitted);
1945
- return artifactsAtAdmission ? artifactExecutionQueue.run(executeManaged) : executeManaged();
1883
+ return managedSessionExecutionQueue.run(executeAdmitted, signal);
1946
1884
  },
1947
1885
  };
1948
- pi.registerTool(beforeExecute ? { ...agentBrowserTool, executionMode: "sequential" } : agentBrowserTool);
1886
+ const executeCode = async (toolCallId, params, signal, _onUpdate, ctx) => {
1887
+ if (!ctx.sessionManager.getSessionFile())
1888
+ return browserExecutionFailure(new Error("agent_browser_code requires a persisted Pi session for ordered browser-state recovery; relaunch without --no-session."), signal, "validation-error");
1889
+ let operationCwd;
1890
+ try {
1891
+ operationCwd = resolveExecutionCwd(pi, ctx);
1892
+ }
1893
+ catch (error) {
1894
+ return browserExecutionFailure(error, signal, "validation-error");
1895
+ }
1896
+ const controller = new AbortController();
1897
+ const abort = () => controller.abort(signal?.reason);
1898
+ signal?.addEventListener("abort", abort, { once: true });
1899
+ if (signal?.aborted)
1900
+ abort();
1901
+ activeScriptControllers.add(controller);
1902
+ let finish;
1903
+ const execution = new Promise(resolve => { finish = resolve; });
1904
+ activeScriptExecutions.add(execution);
1905
+ const deadline = Date.now() + (params.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS);
1906
+ const deadlineTimer = setTimeout(() => controller.abort(new DOMException("Browser code deadline exceeded.", "TimeoutError")), Math.max(0, deadline - Date.now()));
1907
+ const generation = branchRestoreGeneration;
1908
+ const output = createBrowserCodeOutput();
1909
+ const nativeEnvironment = getAgentBrowserProcessEnvironment();
1910
+ try {
1911
+ const args = [
1912
+ ...(params.namespace !== undefined ? ["--namespace", params.namespace] : []),
1913
+ ...(params.session !== undefined ? ["--session", params.session] : []),
1914
+ "get", "url",
1915
+ ];
1916
+ const input = resolveAgentBrowserInput({ params: { args }, getBatchPreflightValidationError: () => undefined });
1917
+ if (input.status === "invalid")
1918
+ return buildValidationFailureResult(input);
1919
+ const config = loadAgentBrowserConfigSync({ cwd: ctx.cwd, includeProjectConfig: shouldIncludeProjectConfig(ctx) });
1920
+ const profile = config.trustedBrowserDefaultProfile;
1921
+ const rootId = process.env.PI_SUBAGENT_CHILD === "1" && process.env.PI_SUBAGENT_ROOT_SESSION_ID
1922
+ ? process.env.PI_SUBAGENT_ROOT_SESSION_ID : ctx.sessionManager.getSessionId();
1923
+ const runAdmitted = async () => {
1924
+ const owner = params.session ? ownedManagedSessions.get(getAgentBrowserSessionIdentityKey(params.session, params.namespace ?? nativeEnvironment.AGENT_BROWSER_NAMESPACE)) : undefined;
1925
+ const browserCwd = nativeEnvironment.AGENT_BROWSER_CONFIG !== undefined ? operationCwd
1926
+ : params.session ? owner?.cwd ?? ctx.cwd : managedSessionActive ? managedSessionCwd : ctx.cwd;
1927
+ const cwdError = getBrowserCwdError(browserCwd);
1928
+ if (cwdError)
1929
+ return browserExecutionFailure(new Error(cwdError), controller.signal, "validation-error");
1930
+ return withNativeSessionDefaults(input, browserCwd, controller.signal, async (input) => {
1931
+ const plan = buildExecutionPlan(input.toolArgs, { freshSessionName: managedSessionName, managedSessionActive, managedSessionName, managedSessionNamespace, sessionMode: "auto" });
1932
+ const sessionName = plan.sessionName ?? "default";
1933
+ const namespace = plan.namespace || undefined;
1934
+ const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
1935
+ const owned = plan.managedSessionName !== undefined || ownedManagedSessions.has(key);
1936
+ const run = async () => {
1937
+ const identity = await resolveBrowserExecutionIdentity({ sessionName, namespace, ownedManagedSession: owned });
1938
+ return withBrowserExecutionLock({ identity, deadline, signal: controller.signal }, async (executionSignal) => {
1939
+ let journalFailed = false;
1940
+ const run = await runAgentBrowserScript({
1941
+ code: params.code, signal: executionSignal, timeoutMs: Math.max(1, deadline - Date.now()),
1942
+ emitImage: output.emitImage,
1943
+ dispatch: async (inner, innerSignal) => {
1944
+ if (journalFailed)
1945
+ throw new Error("Browser-state persistence failed. Inspect the current page before continuing in another tool call.");
1946
+ const { args } = bindBrowserCodeCall(inner, { sessionName, namespace });
1947
+ if (plan.usedImplicitSession && !managedSessionActive && managedSessionName === sessionName && extractExplicitSessionName(inner.args) === undefined)
1948
+ args.splice(args.indexOf("--session"), 2);
1949
+ const pending = { args: redactInvocationArgs(args), sessionName, namespace, resultCategory: "failure", sessionTabTargetUnknown: true };
1950
+ try {
1951
+ appendBrowserTransition(pi, toolCallId, pending, true);
1952
+ }
1953
+ catch {
1954
+ journalFailed = true;
1955
+ throw new Error("Could not persist browser-state intent; the browser command was not run.");
1956
+ }
1957
+ // Resolve launch defaults from the real command, never the routing-only get-url probe.
1958
+ const result = await withAgentBrowserProcessEnvironment({ AGENT_BROWSER_SESSION: nativeEnvironment.AGENT_BROWSER_SESSION }, () => agentBrowserTool.execute(toolCallId, { args, stdin: inner.stdin, timeoutMs: Math.min(inner.timeoutMs ?? deadline - Date.now(), Math.max(1, deadline - Date.now())) }, innerSignal, undefined, ctx, toolCallId, operationCwd, false));
1959
+ const page = sessionPageState.get(key);
1960
+ const details = { ...(isRecord(result.details) ? result.details : {}), sessionName, namespace, sessionTabTarget: page.tabTarget, sessionTabTargetUnknown: page.tabTargetUnknown, sessionTabReopenPending: page.tabReopenPending, refSnapshot: page.refSnapshot, refSnapshotInvalidation: page.refSnapshotInvalidation };
1961
+ if (generation === branchRestoreGeneration) {
1962
+ try {
1963
+ appendBrowserTransition(pi, toolCallId, details, result.isError === true);
1964
+ }
1965
+ catch {
1966
+ journalFailed = true;
1967
+ throw new Error("The browser command finished, but its state could not be persisted. Inspect before retrying; changes may already have happened.");
1968
+ }
1969
+ }
1970
+ return output.observe(result);
1971
+ },
1972
+ });
1973
+ const result = await output.finish(run, sessionName, namespace);
1974
+ if (artifactManifest && isRecord(result.details))
1975
+ result.details.artifactManifest = redactSensitiveValue(artifactManifest);
1976
+ if (!params.outputPath)
1977
+ return result;
1978
+ return artifactExecutionQueue.run(async () => {
1979
+ const error = getArtifactPreflightValidationError({ args: [], cwd: operationCwd, outputPath: params.outputPath, activeRecordingReservations: activeRecordingReservations.values() });
1980
+ if (error) {
1981
+ const failure = browserExecutionFailure(new Error(error), undefined, "validation-error");
1982
+ return { ...result, isError: true, content: [...failure.content, ...result.content], details: { ...(isRecord(result.details) ? result.details : {}), ...(isRecord(failure.details) ? failure.details : {}) } };
1983
+ }
1984
+ return applyAgentBrowserOutputPath({ cwd: operationCwd, outputPath: params.outputPath, preserveTextContent: true, result });
1985
+ });
1986
+ });
1987
+ };
1988
+ return shouldSerializeBrowserCommand({ namespace, explicitSessionName: sessionName, managedSessionName, ownedElectronLaunchRecords, ownedManagedSessions })
1989
+ ? managedSessionExecutionQueue.run(run, controller.signal)
1990
+ : callerOwnedSessionExecutionQueues.run(key, namespace, run, controller.signal);
1991
+ }, managedSessionActive || freshSessionOrdinal > 0 ? undefined : { id: rootId, profile: profile?.policy === "always" && !/[\\/~]/.test(profile.name) ? profile.name : undefined, executablePath: config.trustedBrowserExecutablePath });
1992
+ };
1993
+ return await (!params.session && nativeEnvironment.AGENT_BROWSER_CONFIG === undefined
1994
+ ? managedSessionExecutionQueue.run(runAdmitted, controller.signal) : runAdmitted());
1995
+ }
1996
+ catch (error) {
1997
+ return browserExecutionFailure(error, controller.signal);
1998
+ }
1999
+ finally {
2000
+ clearTimeout(deadlineTimer);
2001
+ activeScriptControllers.delete(controller);
2002
+ signal?.removeEventListener("abort", abort);
2003
+ activeScriptExecutions.delete(execution);
2004
+ finish();
2005
+ }
2006
+ };
2007
+ const finalizeObservation = async (result, params, ctx) => {
2008
+ if ("args" in params && params.args && isPlainTextInspectionArgs(params.args))
2009
+ return result;
2010
+ const { renderAgentBrowserObservation } = await import("./lib/results/presentation/large-output.js");
2011
+ const details = isRecord(result.details) ? result.details : {};
2012
+ if (result.isError && details.error === undefined)
2013
+ details.error = details.validationError ?? details.summary ?? result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
2014
+ const priorManifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
2015
+ const rendered = await renderAgentBrowserObservation({ content: result.content, details, json: "code" in params || params.args?.includes("--json") === true, succeeded: result.isError !== true, persistentArtifactStore: getPersistentSessionArtifactStore(ctx) });
2016
+ artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, priorManifest, rendered.artifactManifest);
2017
+ return { ...result, content: rendered.content, details: { ...details, ...(artifactManifest ? { artifactManifest: redactSensitiveValue(artifactManifest) } : {}) } };
2018
+ };
2019
+ registerAgentBrowserToolSurface(pi, {
2020
+ async execute(id, params, signal, onUpdate, ctx) {
2021
+ let result;
2022
+ try {
2023
+ result = await agentBrowserTool.execute(id, params, signal, onUpdate, ctx);
2024
+ }
2025
+ catch (error) {
2026
+ result = browserExecutionFailure(error, signal);
2027
+ }
2028
+ return finalizeObservation(result, params, ctx);
2029
+ },
2030
+ async executeCode(id, params, signal, onUpdate, ctx) {
2031
+ const result = await executeCode(id, params, signal, onUpdate, ctx);
2032
+ return finalizeObservation(result, params, ctx);
2033
+ },
2034
+ executionMode: beforeExecute ? "sequential" : undefined,
2035
+ renderCodeCall(args, theme, context) {
2036
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
2037
+ text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
2038
+ return text;
2039
+ },
2040
+ promptGuidelines: toolPromptGuidelines,
2041
+ renderCall: agentBrowserTool.renderCall, renderResult: agentBrowserTool.renderResult,
2042
+ });
1949
2043
  registerWebSearchToolIfAvailable(agentBrowserConfig);
1950
2044
  }