pi-agent-browser-native 0.6.17 → 0.7.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +55 -167
  3. package/dist/extensions/agent-browser/index.js +312 -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 +138 -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 +35 -21
  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/docs/platform-smoke.md +2 -2
  48. package/package.json +1 -1
  49. package/scripts/doctor.mjs +2 -2
@@ -1,6 +1,7 @@
1
1
  import { getKeybindings, Text, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { BROWSER_RESULT_TOOLS } from "./browser-transcript.js";
2
3
  import { compileAgentBrowserElectron } from "./input-modes/electron.js";
3
- import { compileAgentBrowserJob, compileAgentBrowserQaPreset } from "./input-modes/job.js";
4
+ import { compileAgentBrowserQaPreset } from "./input-modes/job.js";
4
5
  import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup } from "./input-modes/lookups.js";
5
6
  import { compileAgentBrowserSemanticAction } from "./input-modes/semantic-action.js";
6
7
  import { isRecord } from "./parsing.js";
@@ -95,14 +96,12 @@ function formatVisualTruncationNotice(remainingLines, totalLines, theme, width)
95
96
  return truncateToWidth(notice, Math.max(0, width));
96
97
  }
97
98
  function getStructuredModeInvocation(input) {
98
- if (typeof input.script === "string")
99
- return { mode: "script", rawArgs: [], scriptSource: input.script };
99
+ if (typeof input.code === "string")
100
+ return { mode: "code", rawArgs: [], scriptSource: input.code };
100
101
  if (Array.isArray(input.args))
101
102
  return { rawArgs: input.args.filter((value) => typeof value === "string") };
102
103
  if (input.semanticAction !== undefined)
103
104
  return { mode: "semanticAction", rawArgs: compileAgentBrowserSemanticAction(input.semanticAction).compiled?.args ?? [] };
104
- if (input.job !== undefined)
105
- return { mode: "job", rawArgs: compileAgentBrowserJob(input.job).compiled?.args ?? [] };
106
105
  if (input.qa !== undefined)
107
106
  return { mode: "qa", rawArgs: compileAgentBrowserQaPreset(input.qa).compiled?.args ?? [] };
108
107
  if (input.sourceLookup !== undefined)
@@ -137,9 +136,10 @@ export function formatAgentBrowserRenderCall(args, theme, expanded = false) {
137
136
  const invocationPreview = scriptSource === undefined
138
137
  ? formatInvocationPreview(rawArgs)
139
138
  : formatScriptSourceForDisplay(scriptSource, expanded);
140
- let text = theme.fg("toolTitle", theme.bold("agent_browser"));
139
+ let text = theme.fg("toolTitle", theme.bold(mode === "code" ? "agent_browser_code" : "agent_browser"));
141
140
  if (mode) {
142
- text += ` ${theme.fg("accent", mode)}`;
141
+ if (mode !== "code")
142
+ text += ` ${theme.fg("accent", mode)}`;
143
143
  if (scriptSource !== undefined && expanded) {
144
144
  text += `\n${theme.fg("dim", "Source:")}\n${theme.fg("accent", invocationPreview)}`;
145
145
  }
@@ -220,9 +220,9 @@ function appendModelVisibleFailureCategoryNotice(content, notice) {
220
220
  : item);
221
221
  }
222
222
  export function buildAgentBrowserToolResultPatch(event) {
223
- if (event.toolName !== "agent_browser")
223
+ if (!BROWSER_RESULT_TOOLS.has(event.toolName))
224
224
  return undefined;
225
- const preservesParseableJson = agentBrowserToolResultRequestedJson(event) && agentBrowserToolResultHasParseableJsonContent(event.content);
225
+ const preservesParseableJson = (event.toolName === "agent_browser_code" || agentBrowserToolResultRequestedJson(event)) && agentBrowserToolResultHasParseableJsonContent(event.content);
226
226
  const notice = preservesParseableJson ? undefined : formatModelVisibleFailureCategoryNotice(event.details);
227
227
  const content = notice ? appendModelVisibleFailureCategoryNotice(event.content, notice) : undefined;
228
228
  const shouldMarkError = isRecord(event.details) && event.details.resultCategory === "failure" && event.isError !== true;
@@ -7,51 +7,70 @@ export function buildInstalledDocsGuideline(paths) {
7
7
  return `For detailed agent_browser docs, read targeted sections: ${paths.readmePath} (setup), ${paths.commandReferencePath} (commands), ${paths.toolContractPath} (result/details). Do not load the full command reference unless needed.`;
8
8
  }
9
9
  export const QUICK_START_GUIDELINES = [
10
- `Quick start mental model: use exactly one of script (one-shot JavaScript orchestration), args (exact agent-browser CLI args after the binary), semanticAction (a thin shorthand compiled to find argv for locator actions, direct selector/ref click/check/fill, or select argv for native dropdowns), job (a constrained short-workflow schema compiled to batch --bail by default; set failFast:false only when later diagnostics remain safe if an earlier navigation fails), qa (a lightweight fail-fast QA preset built on batch --bail with bounded visible expected-text checks, including qa.attached for current sessions), electron (desktop Electron list/launch/status/cleanup/probe), or the experimental sourceLookup / networkSourceLookup helpers (candidates only; each compiled to batch); stdin is only for batch, eval --stdin, auth save --password-stdin, and wrapper-generated batch stdin from job, qa, sourceLookup, or networkSourceLookup, and is rejected with electron; sessionMode=fresh switches the extension-managed pi-scoped session to a fresh upstream launch when you need new launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}) to apply. Use outputPath for durable eval/get/snapshot captures. Omit --json unless you need JSON text; the wrapper otherwise injects it for structured details.`,
11
- "Use script only when loops, conditional page branches, or multi-page aggregation would otherwise require several top-level calls: call await browser({ args, stdin?, timeoutMs? }), check each returned { ok, data, error, details, failureCategory, nextActions, resultCategory, summary, text } envelope, and call emit(value) for the one final JSON value. Script runs in a one-shot isolated session without profiles, attachments, imports, host filesystem/network/process access, caller session controls, or inherited agent-browser launch/proxy settings; inner browser calls and their response envelopes are serialized and bounded, compatible suggested next actions can be passed back to browser(), and the wrapper always closes that session. One top-level approval can authorize up to 25 inner calls, so inspect the full source before approval. Use args/job/qa for ordinary linear work.",
12
- "There is no first-class reusable named browser recipe runtime: script is ad hoc source for one call only, with no saved names, registry, or versioned workflow state. Keep recurring flows in documentation examples, job, qa, or raw batch (closed RQ-0068; see docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet).",
13
- "Common first calls (first-call recipe): { args: [\"open\", \"<url>\"] } → { args: [\"snapshot\", \"-i\"] } → { args: [\"click\", \"@eN\"] } or { args: [\"fill\", \"@eN\", \"<text>\"] } using @refs and visible labels from that snapshot, then { args: [\"snapshot\", \"-i\"] } after navigation or DOM changes. On https://example.com/ the main link label is Learn more (use exact snapshot text, not guessed link copy).",
14
- "Locator-first clicks/fills and native select changes without hand-building argv: { semanticAction: { action: \"click\", locator: \"text\", value: \"Close\" } }, { semanticAction: { action: \"fill\", locator: \"label\", value: \"Email\", text: \"user@example.com\" } }, direct current targets such as { semanticAction: { action: \"fill\", selector: \"@e1\", text: \"prompt\" } }, or { semanticAction: { action: \"select\", selector: \"#flavor\", value: \"chocolate\" } }; add semanticAction.session when targeting a named upstream browser session; details.compiledSemanticAction shows the semantic target, while details.effectiveArgs may show a resolved current @ref for active-session role/name click/check/fill actions to avoid hidden duplicate matches; semanticAction does not expose uncheck while upstream find ... uncheck is not runtime-supported, so use raw uncheck with a stable selector or current ref; selector-not-found failures may append bounded click try-*-candidate next actions or, for fill misses with current editable refs, details.richInputRecovery with focus/click actions that do not copy fill text; stale-ref failures can return retry-semantic-action-after-stale-ref for compiled find actions when retry safety is provable.",
15
- `Common advanced calls: { args: ["batch", "--bail"], stdin: "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }, { job: { steps: [{ action: "open", url: "https://example.com" }, { action: "assertText", text: "Example Domain" }, { action: "screenshot", path: ".dogfood/example.png" }] } }, { qa: { url: "https://example.com", expectedText: "Example Domain", screenshotPath: ".dogfood/qa-example.png" } } (example.com smoke only; elsewhere match exact visible text from snapshot -i), { electron: { action: "list", query: "code" } }, { electron: { action: "launch", appName: "Visual Studio Code", handoff: "snapshot" } }, { electron: { action: "probe" } }, { qa: { attached: true, expectedText: "Explorer" } }, { args: ["eval", "--stdin"], stdin: "document.title", outputPath: "logs/page-title.json" }, { args: ["auth", "save", "name", "--password-stdin"], stdin: "<password from user-approved secret source>" }, { args: ["--profile", "Default", "open", "https://example.com/account"], sessionMode: "fresh" }, and { args: ["open", "--enable", "react-devtools", "https://example.com"], sessionMode: "fresh" }. For app pages with a native dropdown, job steps can include { action: "select", selector: "#flavor", value: "chocolate" } before the dependent assertion; for locator-friendly pages, job click/fill steps can use semantic locator fields such as { action: "fill", locator: "role", role: "searchbox", name: "Search", text: "agent browser" }; for human-paced input, job type steps can use { action: "type", selector: "#prompt", text: "hello", delayMs: 20, press: "Enter" }; delayed typing is capped at 200 characters per step, and generated per-character rows are compacted in visible batch prose while full rows remain in details.batchSteps.`,
16
- "Constrained job navigation is explicit only: click (and select/submit flows that may navigate) does not prove the next page loaded; add an assertUrl that does not already match the starting page and/or assertText for new page state after navigation-prone steps before screenshot or later interactions. assertText takes only text, not selector or locator fields. Clicks can stale subsequent @refs: split the job and re-snapshot before using those refs. Keep jobs short around navigation, click, and rerender boundaries on dynamic React/product apps; avoid a whole checkout in one job. If a long job times out and details.timeoutPartialProgress shows a mutating incomplete step, inspect current page state and continue with a shorter job or single action instead of blindly retrying the mutating step. Example: { job: { steps: [{ action: \"open\", url: \"https://shop.example/checkout\" }, { action: \"fill\", selector: \"#email\", text: \"user@example.com\" }, { action: \"click\", selector: \"#continue\" }, { action: \"assertUrl\", url: \"**/shipping\" }, { action: \"assertText\", text: \"Shipping address\" }, { action: \"screenshot\", path: \".dogfood/shipping.png\" }] } }. Top-level click may add pageChangeSummary hints, but job never auto-inserts post-click asserts.",
17
- "High-value command reference: click <selector> --new-tab opens link-like targets in a new tab; select <selector> <value...> changes native dropdown values; wrapper-handled scroll <dir> [px|percent] and scroll to end/top target document scrolling before upstream fallback, while scroll <selector> <dir> [px|percent] targets nested scrollers; download <selector> <path> saves a file triggered by a click; read [url] returns agent-readable text (explicit URLs and all-read batches skip browser helpers and managed replacement, including timeout recovery; omit the URL for verified active-tab DOM); get title/url need no selector; get text/html/value/count <selector> and get attr <selector> <name> read elements/page state (use body for whole-page text/html); screenshot [selector] [path] captures a page or element image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree, react inspect <fiberId>, react renders start/stop, and react suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation; tap <selector> and swipe <direction> [distance] support iOS/provider touch flows.",
18
- "For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata; record start rows are pending/openRecording until record stop finalizes the target. Recording receipts separate captured-frame rate/wall duration from nominal FPS and held/encoded frames; unknown native metrics stay unknown, and sparse or final-only frames cannot prove smoothness. A stop timeout gets one bounded session-info receipt query; follow its exact status/stop actions. Distinct outputPath exports retain failed attempts too. Current upstream records the active page unless a URL is supplied. To support older natives, the wrapper conservatively blocks prior @e… refs after every dispatched start attempt and URL-bearing restart, even on failure; this does not prove the page changed. Take a fresh snapshot before continuing. A restart with only --fps keeps the page and refs; --fps <n> selects 1–60 fps (default 30). The wrapper creates parent directories for direct artifact paths; native download owns the click and download, including loopback links, generated Blob exports, and redirects. Browser close does not delete explicit saved files; if close reports details.artifactCleanup, use host file tools to remove paths listed in explicitArtifactPaths (when non-empty) after inspection. If close fails with details.promptGuard.reason=requested-artifacts-missing-before-close, save the exact required artifact path before closing. A bare inbound image/video path is not a requested output artifact and does not block close. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step; if annotation labels crowd a dense page, use a scoped or non-annotated screenshot plus snapshot refs instead.",
19
- "Use session info for one read-only preflight distinguishing daemon PID/activity from native browser liveness, Chrome PID, exact profile, tabs and native ownership; Pi cleanup ownership is separate and absent fields stay unknown. For policy-required URL reads, use the exact confirm/deny actions and session they return. Only native explicit-read provenance plus capabilities.readRequiresConfirmation enables browserless confirmation; legacy/DOM confirmations retain normal page checks.",
20
- "When failure output shows Next actions, prefer those exact native agent_browser follow-up payloads over guessed commands. The same actions are available in details.nextActions to callers that expose structured details; short stdin is shown inline, while long stdin stays details-only.",
10
+ "Choose agent_browser for one native command, native batch --bail with JSON-array stdin for a known fixed sequence, or agent_browser_code for loops, branches, and aggregation. Return to the model when a fresh observation needs judgment. Neither tool is a named recipe registry.",
11
+ "agent_browser takes args (argv after the binary), optional stdin, outputPath, timeoutMs, and sessionMode. Start with { args: [\"open\", \"<url>\"] }, then { args: [\"snapshot\", \"-i\"] }; act on current @refs and re-snapshot after navigation or DOM changes. Native commands, providers, plugins, profiles, auth, file access, and caller paths remain available through args.",
12
+ "Use { args: [\"batch\", \"--bail\"], stdin: \"[[\\\"fill\\\",\\\"@e1\\\",\\\"text\\\"],[\\\"click\\\",\\\"@e2\\\"]]\" } for same-snapshot form fields followed by submit. Split if a fill rerenders later fields. Add native condition waits after navigation-prone actions; a click alone does not prove the destination loaded. Avoid blind mutation retries after timeout.",
13
+ "agent_browser_code takes { code, session?, namespace?, timeoutMs?, outputPath? }. JavaScript globals are fresh for every call; the selected browser persists and is shared with agent_browser. Use await browser({ args, stdin?, timeoutMs? }), inspect result.success, and emit selected JSON instead of whole envelopes. Use emitImage(result.imageObservations[0]) only when image inspection is needed; image handles belong to the current code call. No imports or host filesystem/network/process APIs are exposed.",
14
+ "Example agent_browser_code: { code: 'const result = await browser({args:[\"get\",\"title\"]}); if (!result.success) { emit(result); } else { emit({title:result.data}); }' }. Native batch still avoids extra CLI processes for fixed segments inside code. Explicit session/namespace controls select the browser for this cell without changing the default browser.",
15
+ "Call agent_browser_tools with no fields for inventory, or { enable: [\"action\", \"qa\", \"electron\", \"source\", \"network\"] } for advanced native tools. It adds agent_browser_action, agent_browser_qa, agent_browser_electron, agent_browser_source, and agent_browser_network_source without disabling other tools. Their specialized instructions appear when activated.",
16
+ "Use read <url> for readable web text; get title/url, get text/html/value/count <selector>, or get attr <selector> <name> for targeted state. For eval --stdin, put JavaScript in stdin and return the desired value. outputPath saves result data and must differ from screenshot/download/recording paths. Use --json only when JSON text is needed.",
17
+ "For artifacts, verify artifactVerification and artifacts before reporting success. A pending recording is not verified output; install ffmpeg before starting and pair start with stop. Native download owns the click and saves Blob/redirect downloads. Close keeps explicitly saved files. Save promptGuard-required paths before close. Screenshot pixels are not browser CSS coordinates: use imageObservations geometry (dimensions, viewport, crop/origin, scroll and DPR) before any visual-coordinate action; full-page and element captures are not current-viewport coordinates, and Pi resizing only maps sent pixels to original image pixels.",
18
+ "Follow model-visible nextActions when present, including identity and stdin, instead of guessing commands. Treat native pending confirmations and unknown mutation outcomes as inspection boundaries. For dense snapshots, inspect Omitted high-value controls before reading a full spill.",
21
19
  ];
20
+ export const ADVANCED_TOOL_PROMPT_GUIDELINES = {
21
+ action: [
22
+ "agent_browser_action uses flat action/locator/value/role/name/text/selector fields. Use current @refs or stable visible labels. Select uses selector plus value/values, or role=combobox/listbox plus values, or locator=label with value=label text and values=options; it preserves unique accessible-target selection. Use native uncheck through agent_browser.",
23
+ "For agent_browser_action fill misses, follow nextActions/richInputRecovery to a current editable ref, then use agent_browser keyboard type for framework-controlled editors. keyboard inserttext may change only DOM state; verify application state. Do not submit unless the requested flow calls for it.",
24
+ ],
25
+ qa: [
26
+ "agent_browser_qa returns a QA verdict from fail-fast native batch, visible-text/selector waits and actionable diagnostics. URL mode defaults console/errors/network checks on; attached:true preserves the current page and defaults those checks off to avoid attributing old errors. Set checks explicitly when needed. attached:true cannot use url or sessionMode:fresh.",
27
+ "agent_browser_qa defaults loadState to domcontentloaded; use networkidle only without long-lived requests. Use exact observed expectedText, not guessed copy. Verify screenshot artifacts; benign icon failures are warnings rather than failed checks. A timeout or partial mutation requires inspection before retry.",
28
+ ],
29
+ electron: [
30
+ "agent_browser_electron uses flat action fields: list before launch; default handoff=snapshot and targetType=page. Launch owns an isolated temporary profile, not the app's existing signed-in profile. Use probe/status for readiness and always cleanup the returned launchId. Native browser close alone does not perform owned app/profile cleanup.",
31
+ "For a user-requested existing signed-in app, host-launch its normal profile with remote debugging when appropriate, then use agent_browser connect, get url, tab list/selection and a fresh snapshot. agent_browser_electron timeoutMs belongs to the host action; list has no timeout option. Caller-owned app shutdown remains with its owner.",
32
+ ],
33
+ source: [
34
+ "agent_browser_source provides experimental DOM/React/workspace candidates, not proven DOM-to-file mappings. For React, launch with --enable react-devtools before navigation. componentName scans bounded local source files even when separate filesystem tools are absent; includeDomHints:false skips the HTML probe.",
35
+ ],
36
+ network: [
37
+ "agent_browser_network_source correlates failed request IDs/filters/URLs with initiator evidence and bounded local URL literals. Results are experimental candidates, not definitive blame; preserve explicit session/namespace for the request being diagnosed.",
38
+ ],
39
+ };
22
40
  export const WEB_SEARCH_PROMPT_GUIDELINE = "Prefer agent_browser_web_search for current or external web facts and URL discovery over public search-engine forms that can hit anti-bot/CAPTCHA-gated pages. For research before implementation, pass searchType: deep-lite unless webSearch.defaultSearchType already does; omit it for everyday lookups so config/auto wins. Provider rank is not proof of authority: when correctness or version matters, prefer the vendor or project's primary current docs, inspect page-date and version clues, and constrain one follow-up after discovering the official domain (Exa includeDomains; Brave site: in query). Do not count URL aliases as independent sources. Use agent_browser after you have a target URL that needs interaction, screenshots, or DOM inspection.";
23
41
  export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
24
- "Use top-level script only for one-shot loops, conditional page branches, or multi-page aggregation that would otherwise require several calls: await browser({ args, stdin?, timeoutMs? }), branch on its ok field, and emit one bounded JSON value. Script gets an isolated non-profile browser session that is always closed, cannot use caller session/namespace/lifecycle/attachment controls, inherited agent-browser launch/proxy settings, user/project config or explicit --config, or host APIs, and is not a reusable named recipe. One top-level approval can authorize up to 25 inner calls, so inspect the full source before approval. Use args/job/qa for ordinary linear flows.",
42
+ QUICK_START_GUIDELINES[0],
43
+ QUICK_START_GUIDELINES[3],
25
44
  "Standard workflow: open the page, snapshot -i, interact using current @refs from that snapshot, and re-snapshot after navigation, scrolling, rerendering, or other major DOM changes because refs are page-scoped; the wrapper fails mutation-prone stale/recycled refs before upstream can silently target a different current-page element. On dense pages, use wrapper-side snapshot -i --search <text> or snapshot -i --filter role=<role> to render matching refs while preserving the full ref map in details.refSnapshot, add snapshot --viewport when scroll position or above/below-fold context matters, and add snapshot --diff when a quick before/after ref-map delta would prevent reading a full spill file.",
26
45
  "For ordinary forms from one snapshot, batch multiple fill @refs before the submit/click step to avoid serial tool calls; if a fill may autosubmit, navigate, or rerender later fields, split the flow and refresh refs first.",
27
46
  "Do not use browser automation to drive public search-engine forms such as Google for discovery; headless jobs that type a query and press Enter can be redirected to anti-bot or CAPTCHA pages. Prefer agent_browser_web_search for live discovery, then agent_browser on a target URL. Do not attempt CAPTCHA bypass.",
28
47
  "Snapshot choice: prefer snapshot -i for routine clicks/fills (interactive @refs, main-content-first). Use snapshot --compact when you need a denser same-page tree without full spill; use full snapshot (no -i) only when you need the complete accessibility tree. Re-snapshot after navigation or major DOM changes. When snapshot -i compacts because the tree is oversized, scan visible output for Omitted high-value controls and optional details.data.highValueControlRefIds before opening the spill file: those list bounded searchboxes, textboxes, comboboxes, buttons, named action links, tabs, checkboxes, radios, options, and menuitems that did not fit the key/other ref previews.",
29
48
  "When a visible text or accessible-name target should survive ref churn, prefer find locators such as role, text, label, placeholder, alt, title, or testid with the intended action instead of guessing a CSS selector.",
30
- "For desktop or host-controlled rich inputs, if semanticAction fill misses, refresh refs and prefer a current editable @ref from details.richInputRecovery or the latest snapshot; focus or click that ref, then use keyboard type for framework-controlled editors that require real key events. keyboard inserttext is paste-like and can change a DOM value without updating application state, so use it only when later application-state evidence proves the edit was accepted. Do not auto-submit with Enter or a submit button unless the user flow explicitly calls for it.",
49
+ "For desktop or host-controlled rich inputs, if agent_browser_action fill misses, refresh refs and prefer a current editable @ref from details.richInputRecovery or the latest snapshot; focus or click that ref, then use keyboard type for framework-controlled editors that require real key events. keyboard inserttext is paste-like and can change a DOM value without updating application state, so use it only when later application-state evidence proves the edit was accepted. Do not auto-submit with Enter or a submit button unless the user flow explicitly calls for it.",
31
50
  "Do not assume Playwright selector dialects such as text=Close or button:has-text('Close') are supported wrapper syntax unless current upstream agent-browser behavior has been verified.",
32
51
  "For authenticated or user-specific content explicitly requested by the user, such as feeds, inboxes, account pages, or private dashboards, use a real profile only when the user/config asks for it or profiles have been inspected; do not assume --profile Default exists on every machine. Do not use a real profile for public pages just because they are dashboards. Treat visible page content from real profiles as model-visible transcript data. On macOS, copied Chrome profiles may omit encrypted cookies, so profile selection alone is not proof of authentication; verify the target page and use a user-approved headed login once when needed. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser. If profile/user-data-dir resolution fails, stop retrying opens, run profiles and/or doctor through agent_browser, then report what the user needs to configure.",
33
52
  "Ordinary bare calls share one native browser within your root Pi parent/subagent group; unrelated roots browse independently. Coordinate multi-call navigation inside your group with existing subagent/intercom tools. Explicit native session defaults still win. Pi quit does not close group browsers; never close an unrelated browser.",
34
53
  `When using launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), 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.`,
35
- "Caller-owned explicit sessions are serialized per effective canonical namespace/session inside this extension while live URL checks, semantic-action snapshots, and the requested command run. For raw batches whose later content step depends on navigation, use exact batch --bail or split the calls; unsafe continue-after-navigation-failure shapes are rejected before the batch runs.",
54
+ "Cooperating Pi processes serialize each complete browser operation and code cell by native socket context, namespace, and session, including target/ref helpers and state updates. Different identities remain concurrent; namespace-wide close waits for matching operations. All participating Pi instances must load this release; humans, third-party clients, and older extensions are outside that coordination. It is not a transaction or rollback. For raw batches whose later content depends on navigation, use exact batch --bail or split the calls.",
36
55
  "After a successful `connect`, `--cdp`, or enabled `--auto-connect` call, verify with get url and keep using the resulting session without repeating the attach flag. The wrapper remembers that attachment across active-branch reload/resume and live-checks the URL before later page reads/interactions because an attached browser can drift externally; caller config, file access, launch arguments, and environment pass through unchanged. A successful close clears the marker. When several named sessions share one Chrome, pass --pin-tab once (AGENT_BROWSER_PIN_TAB) so a closed bound tab fails as tab_gone instead of acting on a neighbor; recover with tab new or tab list. --no-pin-tab turns the sticky pin off. tab list includes each tab's CDP targetId, accepted as a tab ref.",
37
56
  `If you already used the implicit session and now need launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), retry with top-level sessionMode set to fresh or pass an explicit --session for the new launch; never pass --session-mode inside args. After a successful unnamed fresh launch, later auto calls follow that new session.`,
38
57
  "For WebGPU pages, use args [\"--webgpu\", \"open\", \"<url>\"] on a fresh local browser launch; use doctor --webgpu (or --headed on Linux/Windows capture paths) to prove rendering before trusting a non-black screenshot. WebGPU cannot be combined with --cdp, --auto-connect, or provider launches unless --webgpu false overrides an enabled config/environment default.",
39
58
  "For experimental WebMCP page tools, use webmcp list, then webmcp invoke <tool> with --params and optional --frame/--detach/--timeout; use webmcp result or cancel for detached calls. Locally managed Chrome enables WebMCP by default; a positive navigation hint means the page has tools to list. --no-webmcp is launch-scoped and requires a fresh session; invoke/result/cancel can mutate or navigate, so refresh snapshot refs afterward.",
40
59
  "For --allowed-domains, use a fresh local Chrome context. Upstream rejects CDP/auto-connect, profiles, restore/state replay, direct-page providers, iOS/Safari, and startup/profile Chrome args because they cannot guarantee containment; Chromium also disables RTCPeerConnection while the allowlist is active.",
41
- "For React introspection, launch the page with --enable react-devtools before first navigation, then use react tree, react inspect <fiberId>, sourceLookup candidates for local UI source hints, react renders start/stop, or react suspense; sourceLookup is experimental and reports confidence/evidence instead of guaranteed DOM-to-file mappings. For failed fetches and APIs, networkSourceLookup (experimental) correlates failed network requests with initiator metadata and bounded workspace URL literals—candidates only, not definitive blame. Use vitals [url] for Core Web Vitals and hydration timing, and pushstate <url> for client-side SPA navigation.",
60
+ "For React introspection, launch the page with --enable react-devtools before first navigation, then use react tree, react inspect <fiberId>, agent_browser_source candidates for local UI source hints, react renders start/stop, or react suspense; agent_browser_source is experimental and reports confidence/evidence instead of guaranteed DOM-to-file mappings. For failed fetches and APIs, agent_browser_network_source (experimental) correlates failed network requests with initiator metadata and bounded workspace URL literals—candidates only, not definitive blame. Use vitals [url] for Core Web Vitals and hydration timing, and pushstate <url> for client-side SPA navigation.",
42
61
  "For first-navigation setup, use open without a URL plus network route --resource-type <csv>, cookies set --curl <file>, or --init-script/--enable before navigate/opening the target page.",
43
62
  "For stateful browser context work, use auth save --password-stdin with the tool stdin field for credentials, auth list/show/delete/remove for local auth-profile maintenance, auth login when you need the browser to fill a saved profile, and state save/load/list/show/rename/clear/clean for upstream saved-state lifecycle. State paths, restore identifiers, wrapper-prefixed sessions, and all upstream list rows remain available; credential values inside cookie/storage/auth payloads are still redacted from presentation.",
44
63
  "Upstream restore sessions periodically autosave cookies and localStorage while the browser stays open, including page-driven background changes; AGENT_BROWSER_AUTOSAVE_INTERVAL_MS controls the interval (30000 by default; 0 disables periodic saves but keeps save-on-close), while the never value for --restore-save disables automatic saves for that restore session. For wrapper-owned headed launches, the wrapper defaults the interval to 0 because upstream 0.33.2 collects multi-origin storage through visible temporary tabs, then records and reapplies the effective launch-time value to helpers and follow-ups so daemon configuration remains stable. Native close still saves, but direct window close can lose newer state because upstream exempts headed browsers from idle shutdown; set AGENT_BROWSER_AUTOSAVE_INTERVAL_MS before launch when periodic preservation matters, because changing it on a running wrapper-owned headed session requires close plus a fresh launch.",
45
64
  "For batch chains that touch cookies, storage, auth, or other secret-bearing commands, use details.batchSteps for per-step artifacts, categories, spill paths, and full structured errors; top-level details.data on batch is only a compact redacted step matrix (success, argv-redacted command, redacted result or scrubbed error text) built from the same presentation rules as standalone calls.",
46
- "For non-core families, pass current upstream commands through the native tool directly: network requests, network route <url>, network har start/stop [path], diff snapshot, diff screenshot --baseline <file>, diff url <u1> <u2>, trace start, trace stop [path], profiler start, profiler stop [path], record start <path>, record stop, console/errors [--clear], highlight <selector>, inspect, clipboard read, clipboard write <text>, clipboard copy/paste, stream enable/disable/status, dashboard start/stop, device list for iOS simulator inventory, and chat <message>. For compact network requests output, prefer details.nextActions for request detail, route-mock diagnostics, actionable failed-request networkSourceLookup, filtering, clearing the aggregate buffer before repro, or HAR capture follow-ups instead of guessing request-id syntax. Artifact-producing commands report details.artifacts and verification state; long-running starts such as stream, dashboard, trace/profiler, and record should be paired with the matching stop/disable command when the task is done; stream enable already-enabled outcomes are treated as idempotent success with status/disable follow-ups.",
47
- "For Electron desktop apps, prefer top-level electron for wrapper-owned discovery, isolated launch, status, compact probe, and cleanup: list first, treat likely-sensitive annotations as hints rather than enforcement, launch with the default snapshot handoff unless handoff: \"tabs\" is the safer diagnostic starting point, use electron.probe or snapshot -i/qa.attached for current-session state, and always cleanup the returned launchId when done. electron.launch uses an isolated temporary profile; it does not reuse the app's normal signed-in profile or attach to an already-running authenticated app. For signed-in local app state, host-launch the normal app with --remote-debugging-port when appropriate, then use raw args connect <port|url>; after connect, run get url to verify the active target before page-content reads, inspect tab list, select the stable tab id such as tab t2, verify it again with get url, then run a condition wait or snapshot -i before using refs. close commands (`close`, `quit`, or `exit`) only close the browser/CDP session; leave manually launched app shutdown, profile cleanup, and explicit artifacts to the host owner.",
65
+ "For non-core families, pass current upstream commands through the native tool directly: network requests, network route <url>, network har start/stop [path], diff snapshot, diff screenshot --baseline <file>, diff url <u1> <u2>, trace start, trace stop [path], profiler start, profiler stop [path], record start <path>, record stop, console/errors [--clear], highlight <selector>, inspect, clipboard read, clipboard write <text>, clipboard copy/paste, stream enable/disable/status, dashboard start/stop, device list for iOS simulator inventory, and chat <message>. For compact network requests output, prefer details.nextActions for request detail, route-mock diagnostics, actionable failed-request agent_browser_network_source, filtering, clearing the aggregate buffer before repro, or HAR capture follow-ups instead of guessing request-id syntax. Artifact-producing commands report details.artifacts and verification state; long-running starts such as stream, dashboard, trace/profiler, and record should be paired with the matching stop/disable command when the task is done; stream enable already-enabled outcomes are treated as idempotent success with status/disable follow-ups.",
66
+ "For Electron desktop apps, enable agent_browser_electron through agent_browser_tools for wrapper-owned discovery, isolated launch, status, compact probe, and cleanup: list first, treat likely-sensitive annotations as hints rather than enforcement, launch with the default snapshot handoff unless handoff: \"tabs\" is the safer diagnostic starting point, use agent_browser_electron probe or snapshot -i/agent_browser_qa attached:true for current-session state, and always cleanup the returned launchId when done. agent_browser_electron launch uses an isolated temporary profile; it does not reuse the app's normal signed-in profile or attach to an already-running authenticated app. For signed-in local app state, host-launch the normal app with --remote-debugging-port when appropriate, then use raw args connect <port|url>; after connect, run get url to verify the active target before page-content reads, inspect tab list, select the stable tab id such as tab t2, verify it again with get url, then run a condition wait or snapshot -i before using refs. close commands (`close`, `quit`, or `exit`) only close the browser/CDP session; leave manually launched app shutdown, profile cleanup, and explicit artifacts to the host owner.",
48
67
  "For provider or specialized app workflows, load version-matched upstream guidance with skills get agentcore|electron|slack|dogfood|vercel-sandbox|derive-client through the native tool; add --full when you need references/templates, and use skills get --all only for broad skill audits. Use derive-client when recording HAR traffic to generate a standalone API client; prefer network har start (text bodies by default) or network har start --content all|none before multi-step capture. For accessibility audits use a11y or a11y --tags wcag2a,wcag2aa (CDP browsers only). Hosted sandbox workflows should use upstream @agent-browser/sandbox helpers outside this wrapper. Provider launches such as -p ios, --provider browserbase/kernel/browseruse/browserless/agentcore, and iOS --device are upstream-owned setup paths; use sessionMode fresh when switching providers and expect external credentials or local Appium/Xcode setup to be required.",
49
68
  "For dialogs and frames, use dialog status/accept/dismiss and frame <selector|main> through native args; dialog commands and eval snippets that look like alert/confirm/prompt/dialog triggers are shorter-bounded than normal browser calls, and timed-out dialog-like interactions may add inspect-dialog-after-timeout, dismiss-dialog-after-timeout, or recover-fresh-session-after-dialog-timeout nextActions. When --confirm-actions produces a pending confirmation, use details.nextActions or exact confirm <id> / deny <id> calls instead of inventing ids.",
50
- "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 <tab-id-or-label> / snapshot -i to recover state before retrying different URLs or fallback strategies. For headed demos, put --headed on the first launch with sessionMode=fresh and verify with screenshot/tab/get-url evidence because tool success cannot prove the OS window is visible to the user. For desktop readiness, prefer real conditions first: wait --text, wait --url, wait --fn, wait --load <state>, wait --download, or qa.attached; for disappearance checks, use wait --fn predicates instead of stale upstream-help examples like wait <selector> --state hidden. Use electron.probe/status for wrapper-owned launch health or target mismatch. Fixed waits are a last resort: their duration is positional (wait <ms>, not wait --time <ms>). Use explicit --timeout or top-level timeoutMs for legitimately slow waits, and treat a successful payload like \"waited\":\"timeout\" as elapsed time only—verify completion with an observed condition, fresh snapshot, or screenshot.",
69
+ "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 <tab-id-or-label> / snapshot -i to recover state before retrying different URLs or fallback strategies. For headed demos, put --headed on the first launch with sessionMode=fresh and verify with screenshot/tab/get-url evidence because tool success cannot prove the OS window is visible to the user. For desktop readiness, prefer real conditions first: wait --text, wait --url, wait --fn, wait --load <state>, wait --download, or agent_browser_qa attached:true; for disappearance checks, use wait --fn predicates instead of stale upstream-help examples like wait <selector> --state hidden. Use agent_browser_electron probe/status for wrapper-owned launch health or target mismatch. Fixed waits are a last resort: their duration is positional (wait <ms>, not wait --time <ms>). Use explicit --timeout or top-level timeoutMs for legitimately slow waits, and treat a successful payload like \"waited\":\"timeout\" as elapsed time only—verify completion with an observed condition, fresh snapshot, or screenshot.",
51
70
  "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.",
52
71
  "For read-only browsing tasks, use read <url> for documentation or other unstructured text without requiring a Chrome page, or read with no URL for rendered active-tab DOM. Prefer the current snapshot, structured ref labels, getters, or scoped eval --stdin when you need interactive structure or targeted page state. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
53
72
  "For downloads, prefer download <selector> <path> when an element click should save a file; native download owns the click and download, including loopback links, generated Blob exports, and redirects. Do not rely on click alone when you need the downloaded file on disk.",
54
- "On dashboards with nested scroll containers, verify scroll with a screenshot or fresh snapshot -i; if the viewport did not move, details.data.scrolled may be false/noMovement true and you should prefer scrollintoview <@ref> or target the actual scrollable region with scroll <selector> <dir> [px|percent]. For native selects, use select <selector> <value...> (or semanticAction/job select) instead of clicking option refs; for custom comboboxes, a click/semanticAction may only focus the field, so re-snapshot and use keyboard type <text> for focused input, press ArrowDown or press Enter, or visible option refs. Raw type requires both <selector> and <text>.",
73
+ "On dashboards with nested scroll containers, verify scroll with a screenshot or fresh snapshot -i; if the viewport did not move, details.data.scrolled may be false/noMovement true and you should prefer scrollintoview <@ref> or target the actual scrollable region with scroll <selector> <dir> [px|percent]. For native selects, use select <selector> <value...> (or agent_browser_action select) instead of clicking option refs; for custom comboboxes, a click/agent_browser_action may only focus the field, so re-snapshot and use keyboard type <text> for focused input, press ArrowDown or press Enter, or visible option refs. Raw type requires both <selector> and <text>.",
55
74
  "When using eval --stdin, scope checks and actions to the target element or route whenever possible instead of relying on broad page-wide text heuristics.",
56
75
  "When using eval --stdin for extraction, pass the JavaScript through the native tool stdin field, not as an extra args token after --stdin, and return the value you want instead of relying on console.log as the primary result channel. Prefer plain expressions like ({ title: document.title }) or explicitly invoked functions like (() => ({ title: document.title }))(); use outputPath when the eval/get/snapshot data should be saved as a durable local file, but never reuse a screenshot, download, recording, or other browser artifact destination as outputPath. If a function-shaped snippet returns {}, details.evalStdinHint may warn that the function was serialized instead of called. Local file pages and caller-selected output paths are supported when upstream allows them. If get text on a broad CSS selector surfaces details.selectorTextVisibility or selectorTextVisibilityAll, prefer a visible @ref, a more specific selector, or the inspect-visible-text-candidates nextAction over hidden tab content.",
57
76
  "When details.pageChangeSummary is present, use changeType and summary as a compact signal for navigation, DOM mutation, confirmations, or artifacts; when nextActionIds is set, match those ids to entries in details.nextActions (or per-step nextActions inside batch) for concrete follow-up payloads instead of inferring from prose alone. Click-dispatch probes require native get attr readback of a temporary marker on the candidate target; unproven ref or XPath identity leaves the native click unverified, not failed. If details.clickDispatch reports a click-dispatch miss, refresh/inspect/retry the real click first; for static local fixtures only, an explicit eval --stdin programmatic .click() can exercise app handlers, but treat it as an untrusted scripted workaround and never use it to bypass stop-before-submit/order/purchase boundaries. If an upstream click failure says the target is covered by another element at the target's click point, use the inspect-overlay-state nextAction to refresh refs and inspect the blocker before deciding whether to retry; do not blindly repeat the blocked click. If a no-navigation click surfaces details.overlayBlockers, inspect the fresh snapshot evidence before using a close/dismiss candidate nextAction; ordinary page chrome without dialog/alertdialog evidence should not trigger this diagnostic.",
@@ -79,12 +98,14 @@ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
79
98
  ];
80
99
  /** Tier A: always-on tool promptGuidelines (keep small; Tier B lives in SHARED_BROWSER_PLAYBOOK_GUIDELINES and docs). */
81
100
  export const RUNTIME_PROMPT_GUIDELINES = [
82
- "agent_browser: one input mode: script, args, semanticAction, job, qa, sourceLookup/networkSourceLookup, or electron. stdin: batch/eval/auth/wrapper batch only; electron rejects it. Use --json for JSON text.",
83
- "For agent_browser, use open → snapshot -i → @refs; re-snapshot after changes. In authenticated unattended/auto-approved employee flows, ordinary requested non-destructive submissions may proceed. Honor explicit stops; require explicit authorization for purchases, production-control, destructive/irreversible, or account/security/privacy changes.",
84
- "agent_browser bare calls share a root Pi browser with descendants, not unrelated roots; coordinate within the group. Native explicit sessions win. Use sessionMode=fresh for an intentional separate launch. Use requested/configured profiles only; verify auth after restart. Script is disposable.",
85
- "agent_browser: exact user paths; verify artifactVerification/artifacts before success claims. Save promptGuard-required files before close; ffmpeg before recording; close keeps files; waited:timeout proves nothing.",
86
- "When agent_browser details.nextActions exists, use them. Check Omitted high-value controls in dense snapshots. Dashboards: verify scroll via screenshot/snapshot.",
87
- "agent_browser: read <url> for docs/text or active DOM; get title/url; get text/html/value/count <selector>; get attr <selector> <name>. Batch 3+ getters; heed visibility warnings.",
101
+ "Use agent_browser for one native command; batch --bail with JSON-array stdin for fixed sequences; agent_browser_code for loops/branches/aggregation. Return to the model at judgment boundaries. Use agent_browser_tools to discover/enable action, qa, electron, source, or network tools.",
102
+ "agent_browser_code runs fresh JavaScript against the persistent browser: await browser({args, stdin?, timeoutMs?}), check result.success, emit(selected JSON), or emitImage(result.imageObservations[0]). No host APIs/imports; globals do not persist. Explicit session/namespace selects this cell's browser without changing the default.",
103
+ "For agent_browser, use open → snapshot -i → current @refs; re-snapshot after changes. Batch same-snapshot fills before submit; split on rerenders/navigation. Native args, profiles, auth, providers, paths, config and environment pass through. Use requested/configured profiles and verify authentication after restart.",
104
+ "agent_browser bare calls share a root Pi browser with descendants, not unrelated roots; coordinate navigation within the group. Native explicit sessions win. sessionMode:fresh intentionally launches a separate managed browser. Never close an unrelated browser.",
105
+ "For agent_browser and agent_browser_code, follow visible nextActions. Inspect unknown mutation outcomes before retry. Use exact user paths; verify artifactVerification/artifacts before success claims. Save promptGuard-required files before close; close keeps files; waited:timeout proves nothing.",
106
+ "For agent_browser screenshot coordinates, use imageObservations dimensions/viewport/crop/origin/scroll/DPR; full-page and element images are not viewport CSS coordinates. Pi image resizing maps sent pixels to original image pixels only. Prefer refs; inspect screenshots only when the task needs vision.",
107
+ "In agent_browser workflows, ordinary requested non-destructive submissions may proceed in authenticated unattended/auto-approved flows. Honor explicit stops; require explicit authorization for purchases, production-control, destructive/irreversible, or account/security/privacy changes.",
108
+ "agent_browser: read <url> for text, get title/url, get text/html/value/count <selector>, get attr <selector> <name>; heed visibility warnings and Omitted high-value controls. Verify nested scrolling via snapshot/screenshot. Use --json only for JSON text; recording needs ffmpeg before start and a verified stop.",
88
109
  ];
89
110
  export function buildBrowserExecutablePathGuideline(executablePath) {
90
111
  if (!executablePath)
@@ -46,9 +46,13 @@ export function normalizeProcessStartIdentity(stdout) {
46
46
  return trimmed.replace(/\s+/g, " ");
47
47
  }
48
48
  let currentProcessStartIdentityPromise;
49
- async function executeProcessStartIdentityCommand(command) {
49
+ let currentProcessStartIdentity;
50
+ async function executeProcessStartIdentityCommand(command, budget = {}) {
51
+ if (budget.signal?.aborted || (budget.deadline !== undefined && Date.now() >= budget.deadline))
52
+ return undefined;
53
+ const timeout = Math.max(1, Math.min(PROCESS_START_IDENTITY_TIMEOUT_MS, (budget.deadline ?? Infinity) - Date.now()));
50
54
  return await new Promise((resolve) => {
51
- execFile(command.file, command.args, { timeout: PROCESS_START_IDENTITY_TIMEOUT_MS }, (error, stdout) => {
55
+ execFile(command.file, command.args, { timeout, signal: budget.signal }, (error, stdout) => {
52
56
  resolve(error ? undefined : normalizeProcessStartIdentity(stdout));
53
57
  });
54
58
  });
@@ -61,15 +65,27 @@ export async function resolveProcessStartIdentityFromCommands(commands, execute
61
65
  }
62
66
  return undefined;
63
67
  }
64
- async function readUncachedProcessStartIdentity(pid, platform) {
65
- return await resolveProcessStartIdentityFromCommands(buildProcessStartIdentityCommands(pid, platform));
68
+ async function readUncachedProcessStartIdentity(pid, platform, budget) {
69
+ return await resolveProcessStartIdentityFromCommands(buildProcessStartIdentityCommands(pid, platform), command => executeProcessStartIdentityCommand(command, budget));
66
70
  }
67
- export async function readProcessStartIdentity(pid, platform = process.platform) {
71
+ export async function readProcessStartIdentity(pid, platform = process.platform, budget) {
72
+ if (budget?.signal?.aborted || (budget?.deadline !== undefined && Date.now() >= budget.deadline))
73
+ return undefined;
68
74
  if (pid !== process.pid || platform !== process.platform)
69
- return await readUncachedProcessStartIdentity(pid, platform);
75
+ return await readUncachedProcessStartIdentity(pid, platform, budget);
76
+ if (currentProcessStartIdentity)
77
+ return currentProcessStartIdentity;
78
+ if (budget) {
79
+ const identity = await readUncachedProcessStartIdentity(pid, platform, budget);
80
+ if (identity)
81
+ currentProcessStartIdentity = identity;
82
+ return identity;
83
+ }
70
84
  currentProcessStartIdentityPromise ??= readUncachedProcessStartIdentity(pid, platform).then((identity) => {
71
85
  if (!identity)
72
86
  currentProcessStartIdentityPromise = undefined;
87
+ else
88
+ currentProcessStartIdentity = identity;
73
89
  return identity;
74
90
  });
75
91
  return await currentProcessStartIdentityPromise;
@@ -159,6 +159,11 @@ export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeo
159
159
  : DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX;
160
160
  return `${prefix}${!termuxAppRoot && typeof uid === "number" ? `-${uid}` : ""}`;
161
161
  }
162
+ export function resolveAgentBrowserSocketDir(options = {}) {
163
+ const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
164
+ return options.env?.[AGENT_BROWSER_SOCKET_DIR_ENV] ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV]
165
+ ?? (!options.ownedManagedSession ? parentEnv[AGENT_BROWSER_SOCKET_DIR_ENV] : undefined) ?? getAgentBrowserSocketDir();
166
+ }
162
167
  export function isTrustedAndroidAppDataRoot(path, metadata, uid, platform = processPlatform) {
163
168
  if (platform !== "android" || metadata.uid !== uid || metadata.isSymbolicLink() || !metadata.isDirectory() || (metadata.mode & 0o777) !== 0o700)
164
169
  return false;
@@ -345,9 +350,10 @@ export async function runAgentBrowserProcess(options) {
345
350
  const ownedManagedSession = options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args);
346
351
  const args = options.args;
347
352
  const timeoutMs = options.timeoutMs ?? getAgentBrowserProcessTimeoutMs();
348
- if (signal?.aborted) {
349
- return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
350
- }
353
+ const deadlineExpired = () => signal?.reason instanceof Error && signal.reason.name === "TimeoutError";
354
+ const cancelledResult = () => ({ aborted: !deadlineExpired(), agentBrowserStarted: false, exitCode: deadlineExpired() ? 124 : 1, stderr: "", stdout: "", timedOut: deadlineExpired(), timeoutMs: deadlineExpired() ? timeoutMs : undefined });
355
+ if (signal?.aborted)
356
+ return cancelledResult();
351
357
  const parentEnv = getAgentBrowserProcessEnvironment();
352
358
  const managedSessionRestoreOptions = {
353
359
  args,
@@ -382,15 +388,13 @@ export async function runAgentBrowserProcess(options) {
382
388
  };
383
389
  const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
384
390
  let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
385
- const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV]
386
- ?? (!ownedManagedSession ? parentEnv[AGENT_BROWSER_SOCKET_DIR_ENV] : undefined) ?? getAgentBrowserSocketDir();
391
+ const requestedSocketDir = resolveAgentBrowserSocketDir({ env: processOverrides, ownedManagedSession, parentEnv });
387
392
  if (requestedSocketDir !== undefined) {
388
393
  const socketDirError = requestedSocketDir.length > 0
389
394
  ? await getAgentBrowserSocketDirValidationError(requestedSocketDir)
390
395
  : "the configured path is empty";
391
- if (signal?.aborted) {
392
- return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
393
- }
396
+ if (signal?.aborted)
397
+ return cancelledResult();
394
398
  const socketPathError = socketDirError ? undefined : getAgentBrowserSocketPathValidationError({ args, env: effectiveEnv, socketDir: requestedSocketDir });
395
399
  if (socketDirError || socketPathError) {
396
400
  return {
@@ -407,9 +411,8 @@ export async function runAgentBrowserProcess(options) {
407
411
  }
408
412
  const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
409
413
  const stockLauncher = resolveWindowsStockLauncher(cwd, childEnv);
410
- if (signal?.aborted) {
411
- return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
412
- }
414
+ if (signal?.aborted)
415
+ return cancelledResult();
413
416
  return await new Promise((resolve) => {
414
417
  let aborted = false;
415
418
  let agentBrowserStarted = false;
@@ -597,10 +600,10 @@ export async function runAgentBrowserProcess(options) {
597
600
  timeoutTimer.unref?.();
598
601
  }
599
602
  if (signal) {
600
- abortListener = () => terminateChild("abort");
603
+ abortListener = () => terminateChild(deadlineExpired() ? "timeout" : "abort");
601
604
  signal.addEventListener("abort", abortListener, { once: true });
602
605
  if (signal.aborted)
603
- terminateChild("abort");
606
+ abortListener();
604
607
  }
605
608
  writeChildStdin();
606
609
  });
@@ -23,10 +23,10 @@ function buildArtifactVerificationAction(artifact) {
23
23
  function buildElectronToolAction(options) {
24
24
  return {
25
25
  id: options.id,
26
- params: { electron: { action: options.action, launchId: options.launchId } },
26
+ params: { action: options.action, launchId: options.launchId },
27
27
  reason: options.reason,
28
28
  ...(options.safety ? { safety: options.safety } : {}),
29
- tool: "agent_browser",
29
+ tool: "agent_browser_electron",
30
30
  };
31
31
  }
32
32
  function getDownloadRetryPath(args, fallback) {
@@ -15,8 +15,7 @@ export function applyNamespaceToNextActions(actions, namespace) {
15
15
  const args = action.params?.args;
16
16
  if (args)
17
17
  return { ...action, params: { ...action.params, args: withOptionalNamespaceArgs(namespace, args) } };
18
- const networkSourceLookup = action.params?.networkSourceLookup;
19
- return networkSourceLookup ? { ...action, params: { ...action.params, networkSourceLookup: { ...networkSourceLookup, namespace } } } : action;
18
+ return action.tool === "agent_browser_network_source" ? { ...action, params: { ...action.params, namespace } } : action;
20
19
  });
21
20
  }
22
21
  export function applySessionToNextActions(actions, sessionName) {
@@ -1,5 +1,6 @@
1
1
  import { open, readFile, stat } from "node:fs/promises";
2
2
  import { extname, resolve } from "node:path";
3
+ import { getImageDimensions } from "@earendil-works/pi-tui";
3
4
  import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
4
5
  import { getExplicitArtifactDestination } from "../../orchestration/browser-run/artifact-paths.js";
5
6
  import { isRecord, parsePositiveInteger } from "../../parsing.js";
@@ -258,6 +259,8 @@ async function buildPreviousRestartRecordingArtifact(options) {
258
259
  }
259
260
  }
260
261
  export async function extractFileArtifacts(options) {
262
+ if (options.commandInfo.command === "screenshot" && isRecord(options.data) && options.data.changed === false)
263
+ return [];
261
264
  const candidates = extractPathStrings(options.data);
262
265
  const recording = options.commandInfo.command === "record" ? getRecordingReceipt(options.data, options.commandInfo.subcommand === "stop" ? options.recordingOutcome : undefined) : undefined;
263
266
  const recordingPending = options.recordingPending ?? (recording?.success === null && isRecord(options.data) && isRecord(options.data.capture) && recording.capture.endedAt === null);
@@ -565,9 +568,30 @@ export function extractImagePath(commandInfo, cwd, data) {
565
568
  const path = typeof data === "string" ? data : isRecord(data) && typeof data.path === "string" ? data.path : undefined;
566
569
  return path?.trim() && !isNonFileArtifactPathCandidate(path) ? resolve(cwd, path) : undefined;
567
570
  }
568
- export async function attachInlineImage(presentation, imagePath) {
571
+ export async function attachInlineImage(presentation, imagePath, modelVisible = true) {
569
572
  try {
570
573
  const fileStats = await stat(imagePath);
574
+ const handle = await open(imagePath, "r");
575
+ let header;
576
+ try {
577
+ // Pi's dimension API accepts base64. Encode only a bounded header, never the whole code-mode image.
578
+ const bytes = Buffer.alloc(64 * 1024);
579
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
580
+ header = bytes.subarray(0, bytesRead);
581
+ }
582
+ finally {
583
+ await handle.close();
584
+ }
585
+ const mimeType = getImageMimeType(header);
586
+ if (!mimeType)
587
+ return presentation;
588
+ const dimensions = getImageDimensions(header.toString("base64"), mimeType);
589
+ presentation.imagePath = imagePath;
590
+ presentation.imageObservations = [{ path: imagePath, mimeType,
591
+ pixels: dimensions ? { width: dimensions.widthPx, height: dimensions.heightPx } : undefined,
592
+ capture: "unknown", geometry: { status: "unknown", reason: dimensions ? "Capture geometry was not observed." : "Image dimensions could not be read from the bounded header." } }];
593
+ if (!modelVisible)
594
+ return presentation;
571
595
  const inlineImageMaxBytes = getInlineImageMaxBytes();
572
596
  if (fileStats.size > inlineImageMaxBytes) {
573
597
  appendPresentationNotice(presentation, `Image attachment skipped: ${formatByteCount(fileStats.size)} exceeds the inline limit of ${formatByteCount(inlineImageMaxBytes)}.`);
@@ -575,9 +599,6 @@ export async function attachInlineImage(presentation, imagePath) {
575
599
  return presentation;
576
600
  }
577
601
  const file = await readFile(imagePath);
578
- const mimeType = getImageMimeType(file);
579
- if (!mimeType)
580
- return presentation;
581
602
  presentation.content.push({ type: "image", data: file.toString("base64"), mimeType });
582
603
  presentation.imagePath = imagePath;
583
604
  return presentation;
@@ -246,6 +246,7 @@ async function buildBatchStepPresentation(options) {
246
246
  ? buildNetworkRouteDiagnostics(item.result, networkRoutes)
247
247
  : undefined;
248
248
  const presentation = await buildNestedToolPresentation({
249
+ modelVisible: options.modelVisible,
249
250
  artifactManifest,
250
251
  artifactMaxUpdatedAtMs,
251
252
  artifactMinUpdatedAtMs,
@@ -307,6 +308,7 @@ async function buildBatchStepPresentation(options) {
307
308
  fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
308
309
  imagePath: imagePaths[0],
309
310
  imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
311
+ imageObservations: presentation.imageObservations,
310
312
  index,
311
313
  lifecycle,
312
314
  networkRouteDiagnostics: presentation.networkRouteDiagnostics,
@@ -383,6 +385,7 @@ export async function buildBatchPresentation(options) {
383
385
  let currentNetworkRoutes = networkRoutes;
384
386
  for (const [index, item] of data.entries()) {
385
387
  const step = await buildBatchStepPresentation({
388
+ modelVisible: options.modelVisible,
386
389
  artifactManifest: currentArtifactManifest,
387
390
  artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs,
388
391
  artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs,
@@ -424,7 +427,7 @@ export async function buildBatchPresentation(options) {
424
427
  const mutationEvidenceText = unverifiedMutationCount > 0
425
428
  ? `Mutation evidence: ${unverifiedMutationCount} action result${unverifiedMutationCount === 1 ? " proves" : "s prove"} dispatch only, not application state change. Use explicit later assertions or external receipts as postconditions; fixed waits are not postconditions.`
426
429
  : undefined;
427
- const stepText = formatBatchStepsText(steps);
430
+ const stepText = options.modelVisible === false ? "" : formatBatchStepsText(steps);
428
431
  const batchSummary = batchFailure === undefined
429
432
  ? summary
430
433
  : `Batch failed: ${batchFailure.successCount}/${batchFailure.totalCount} succeeded`;
@@ -472,13 +475,14 @@ export async function buildBatchPresentation(options) {
472
475
  artifacts: artifacts.length > 0 ? artifacts : undefined,
473
476
  batchFailure,
474
477
  batchSteps: steps.map((step) => step.details),
475
- content: [{ type: "text", text: contentText }, ...images],
478
+ content: options.modelVisible === false ? [] : [{ type: "text", text: contentText }, ...images],
476
479
  failureCategory: batchFailure?.failedStep.failureCategory,
477
480
  data: redactedBatchData,
478
481
  fullOutputPath: fullOutputPaths[0],
479
482
  fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
480
483
  imagePath: imagePaths[0],
481
484
  imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
485
+ imageObservations: steps.flatMap(step => step.presentation.imageObservations ?? []),
482
486
  nextActions,
483
487
  pageChangeSummary,
484
488
  resultCategory: batchFailure ? "failure" : "success",
@@ -36,6 +36,8 @@ export function extractAgentBrowserLifecycle(result) {
36
36
  return extractAgentBrowserLifecycle(result.result ?? result.data);
37
37
  }
38
38
  export function omitUpstreamLifecycle(data) {
39
+ if (Array.isArray(data))
40
+ return data;
39
41
  const { lifecycle: _lifecycle, ...rest } = data;
40
42
  return rest;
41
43
  }
@@ -1,3 +1,7 @@
1
+ import { isRecord } from "../../parsing.js";
2
+ import { redactSensitiveValue } from "../../runtime.js";
3
+ import { omitUpstreamLifecycle } from "./common.js";
4
+ import { redactTimeoutPartialProgress } from "../../orchestration/browser-run/diagnostics.js";
1
5
  export function isStringArray(value) {
2
6
  return Array.isArray(value) && value.every((item) => typeof item === "string");
3
7
  }
@@ -17,3 +21,38 @@ export function getPresentationPaths(options) {
17
21
  export function formatBatchStepCommand(command, index) {
18
22
  return command && command.length > 0 ? command.join(" ") : `step-${index + 1}`;
19
23
  }
24
+ // Shared by direct results and code observations; ownership/replay state stays in details.
25
+ export function projectAgentBrowserObservation(details, succeeded) {
26
+ const observation = { success: succeeded, resultCategory: succeeded ? "success" : "failure" };
27
+ for (const key of [
28
+ "data", "error", "summary", "failureCategory", "successCategory", "nextActions", "warnings",
29
+ "sessionName", "namespace", "codeRun", "failures",
30
+ "artifacts", "artifactVerification", "imageObservations", "fullOutputPath", "fullOutputPaths", "fullOutputUnavailable",
31
+ "recordingRecovery", "readConfirmation", "sessionRecoveryHint", "pageChangeSummary", "timeoutPartialProgress",
32
+ "qaPreset", "sourceLookup", "networkSourceLookup", "networkRouteDiagnostics", "webMcpCatalog",
33
+ "clickDispatch", "overlayBlockers", "fillVerification", "visibleRefFallback", "richInputRecovery",
34
+ "snapshotDiff", "snapshotFilter", "snapshotViewport", "scrollPage", "scrollContainer",
35
+ "comboboxFocus", "scrollNoop", "selectorTextVisibility", "selectorTextVisibilityAll", "evalStdinHint", "evalResultWarning",
36
+ "electronGetTextScopeWarning", "electronGetTextScopeWarnings", "recordingDependencyWarning",
37
+ ]) {
38
+ if (details[key] !== undefined)
39
+ observation[key] = details[key];
40
+ }
41
+ if (details.inspection === true && details.data === undefined && typeof details.stdout === "string")
42
+ observation.data = details.stdout;
43
+ if (isRecord(observation.data))
44
+ observation.data = omitUpstreamLifecycle(observation.data);
45
+ if (Array.isArray(details.batchSteps)) {
46
+ observation.batchSteps = details.batchSteps.filter(isRecord).map(step => {
47
+ const { data: _data, ...metadata } = projectAgentBrowserObservation(step, step.success !== false);
48
+ return { index: step.index, ...metadata };
49
+ });
50
+ }
51
+ if (observation.timeoutPartialProgress)
52
+ observation.timeoutPartialProgress = redactTimeoutPartialProgress(observation.timeoutPartialProgress);
53
+ const redacted = redactSensitiveValue(observation);
54
+ // Native stream endpoints are usable browser resource identifiers, not credentials.
55
+ if (isRecord(redacted.data) && isRecord(details.data) && typeof details.data.wsUrl === "string")
56
+ redacted.data.wsUrl = details.data.wsUrl;
57
+ return redacted;
58
+ }
@@ -558,10 +558,10 @@ export function buildNetworkRequestsNextActions(data, sessionName, routeDiagnost
558
558
  if (selected.kind === "actionable") {
559
559
  actions.push({
560
560
  id: "trace-actionable-network-source",
561
- params: { networkSourceLookup: { requestId: selected.requestId, ...(sessionName ? { session: sessionName } : {}) } },
561
+ params: { requestId: selected.requestId, ...(sessionName ? { session: sessionName } : {}) },
562
562
  reason: `Look for local source candidates related to ${descriptor}.`,
563
- safety: "Read-only experimental helper; it reports bounded candidates and may miss bundled or dynamic call sites.",
564
- tool: "agent_browser",
563
+ safety: "Read-only experimental helper; enable network with agent_browser_tools when this tool is inactive. It reports bounded candidates and may miss bundled or dynamic call sites.",
564
+ tool: "agent_browser_network_source",
565
565
  });
566
566
  }
567
567
  if (selected.filter) {