pi-agent-browser-native 0.8.2 → 0.8.3

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 (32) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +16 -7
  3. package/dist/extensions/agent-browser/index.js +430 -157
  4. package/dist/extensions/agent-browser/lib/browser-journal.js +418 -0
  5. package/dist/extensions/agent-browser/lib/browser-session-conversion.js +381 -0
  6. package/dist/extensions/agent-browser/lib/browser-transcript.js +119 -19
  7. package/dist/extensions/agent-browser/lib/input-modes/job.js +49 -2
  8. package/dist/extensions/agent-browser/lib/input-modes/params.js +32 -35
  9. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +25 -5
  10. package/dist/extensions/agent-browser/lib/managed-session-restore.js +20 -5
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +97 -78
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +23 -2
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +52 -17
  14. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +1 -1
  15. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +13 -8
  16. package/dist/extensions/agent-browser/lib/playbook.js +9 -2
  17. package/dist/extensions/agent-browser/lib/prompt-policy.js +0 -14
  18. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +2 -1
  19. package/dist/extensions/agent-browser/lib/session-page-state.js +95 -157
  20. package/dist/extensions/agent-browser/lib/temp.js +14 -6
  21. package/dist/extensions/agent-browser/lib/tool-surface.js +12 -23
  22. package/dist/extensions/agent-browser/lib/web-search.js +0 -8
  23. package/docs/ARCHITECTURE.md +13 -11
  24. package/docs/COMMAND_REFERENCE.md +9 -7
  25. package/docs/RELEASE.md +3 -3
  26. package/docs/REQUIREMENTS.md +1 -1
  27. package/docs/SESSION_STORAGE.md +60 -0
  28. package/docs/SUPPORT_MATRIX.md +8 -5
  29. package/docs/TOOL_CONTRACT.md +17 -14
  30. package/package.json +9 -5
  31. package/scripts/build.mjs +4 -3
  32. package/scripts/convert-browser-session.mjs +22 -0
@@ -1,18 +1,22 @@
1
1
  import { stat } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES, createAgentBrowserScriptCloseArgs, isAgentBrowserScriptSessionName, } from "../input-modes/script.js";
3
4
  import { isRecord } from "../parsing.js";
5
+ import { appendBrowserTransition, getBrowserRecord } from "../browser-transcript.js";
4
6
  import { redactSensitiveText } from "../runtime.js";
5
7
  import { attachInlineImage } from "../results/presentation/artifacts.js";
6
8
  import { projectAgentBrowserObservation } from "../results/presentation/content.js";
7
9
  import { redactPresentationData } from "../results/presentation/diagnostics.js";
8
- const SCRIPT_SESSION_ENTRY_TYPE = "agent-browser-script-session";
9
- // Retire outstanding pre-0.7 isolated-session leases when an old transcript resumes.
10
- export function getScriptSessionLeasesFromBranch(branch) {
10
+ // The offline converter retains outstanding isolated-session cleanup facts in canonical events.
11
+ export function getScriptSessionLeasesFromBranch(branch, ownerSessionId) {
11
12
  const leases = new Map();
12
13
  for (const entry of branch) {
13
- if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== SCRIPT_SESSION_ENTRY_TYPE || !isRecord(entry.data))
14
+ const lease = getBrowserRecord(entry)?.event.state.scriptLease;
15
+ if (!isRecord(lease))
14
16
  continue;
15
- const { cleanup, closeCommandArgs, launchAttempted, sessionName } = entry.data;
17
+ if (ownerSessionId !== undefined && lease.ownerSessionId !== ownerSessionId)
18
+ continue;
19
+ const { cleanup, closeCommandArgs, launchAttempted, sessionName } = lease;
16
20
  if (!isAgentBrowserScriptSessionName(sessionName))
17
21
  continue;
18
22
  const expected = createAgentBrowserScriptCloseArgs(sessionName);
@@ -20,12 +24,13 @@ export function getScriptSessionLeasesFromBranch(branch) {
20
24
  || !Array.isArray(closeCommandArgs) || closeCommandArgs.length !== expected.length
21
25
  || !closeCommandArgs.every((token, index) => token === expected[index]))
22
26
  continue;
23
- leases.set(sessionName, { cleanup, closeCommandArgs: expected, launchAttempted: true, sessionName });
27
+ leases.set(sessionName, { cleanup, closeCommandArgs: expected, launchAttempted: true, sessionName, ownerSessionId: typeof lease.ownerSessionId === "string" ? lease.ownerSessionId : undefined });
24
28
  }
25
29
  return leases;
26
30
  }
27
- export function appendScriptSessionLease(pi, sessionName, cleanup) {
28
- pi.appendEntry(SCRIPT_SESSION_ENTRY_TYPE, { cleanup, closeCommandArgs: createAgentBrowserScriptCloseArgs(sessionName), launchAttempted: true, sessionName });
31
+ export function appendScriptSessionLease(pi, sessionName, cleanup, ownerSessionId) {
32
+ appendBrowserTransition(pi, { event: { version: 1, phase: "state", operationId: randomUUID(), toolCallId: "cleanup", commandIndex: 0, isError: cleanup !== "closed",
33
+ state: { scriptLease: { cleanup, closeCommandArgs: createAgentBrowserScriptCloseArgs(sessionName), launchAttempted: true, sessionName, ownerSessionId } } } });
29
34
  }
30
35
  export function createBrowserCodeOutput() {
31
36
  const images = new Map();
@@ -12,7 +12,7 @@ export const QUICK_START_GUIDELINES = [
12
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
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
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.",
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. The browser instructions cover these capabilities; activation reveals their selected tool declarations.",
16
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
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
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.",
@@ -38,6 +38,13 @@ export const ADVANCED_TOOL_PROMPT_GUIDELINES = {
38
38
  ],
39
39
  };
40
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.";
41
+ export const WEB_SEARCH_TOOL_PROMPT_GUIDELINES = [
42
+ WEB_SEARCH_PROMPT_GUIDELINE,
43
+ "agent_browser_web_search chooses Exa or Brave from configured keys; when both are available, Exa is preferred by default unless webSearch.preferredProvider says otherwise. Use provider only when the user/config calls for a specific provider.",
44
+ "Use Exa deep only when deep-lite may miss angles, and deep-reasoning only for exhaustive or still-thin research. Do not run parallel agent_browser_web_search calls; make one high-signal query, inspect its results, then at most one follow-up.",
45
+ "If agent_browser_web_search returns HTTP 429, stop searching and tell the user the API plan/rate limit needs time or a plan change.",
46
+ "After using agent_browser_web_search, cite result URLs in the final answer when web evidence informed the answer.",
47
+ ];
41
48
  export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
42
49
  QUICK_START_GUIDELINES[0],
43
50
  QUICK_START_GUIDELINES[3],
@@ -96,7 +103,7 @@ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
96
103
  "If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.",
97
104
  "If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.",
98
105
  ];
99
- /** Tier A: always-on tool promptGuidelines (keep small; Tier B lives in SHARED_BROWSER_PLAYBOOK_GUIDELINES and docs). */
106
+ /** Concise orientation included alongside the full operating playbook. */
100
107
  export const RUNTIME_PROMPT_GUIDELINES = [
101
108
  "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
109
  "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.",
@@ -1,10 +1,3 @@
1
- const BROWSER_PROMPT_PATTERNS = [
2
- /\b(?:agent[_ -]?browser|browser automation|eval\s+--stdin|screenshot|snapshot|tab\s+list)\b/i,
3
- /\b(?:react\s+(?:tree|inspect|renders|suspense)|web\s+vitals|core\s+web\s+vitals|pushstate)\b/i,
4
- /\b(?:live\s+docs?|online\s+research|research\s+(?:online|the\s+web)|search\s+(?:online|the\s+web)|web\s+research)\b/i,
5
- /\bbrowser\b.*\b(?:automation|click|fill|navigate|open|page|screenshot|site|snapshot|tab|url|visit|web(?:site| page)?)\b/i,
6
- /\b(?:browse|click|fill|login|navigate|open|visit)\b.*\b(?:https?:\/\/\S+|page|site|tab|url|web(?:site| page)?)\b/i,
7
- ];
8
1
  const LEGACY_BASH_ALLOW_PATTERNS = [
9
2
  /\b(?:bash-oriented workflow|bash workflow)\b/i,
10
3
  /\b(?:use|via|through|with)\s+bash\b/i,
@@ -220,13 +213,6 @@ function getMessageText(content) {
220
213
  .filter((text) => text.length > 0)
221
214
  .join("\n");
222
215
  }
223
- export function shouldAppendBrowserSystemPrompt(prompt) {
224
- const normalizedPrompt = prompt.trim();
225
- if (normalizedPrompt.length === 0) {
226
- return false;
227
- }
228
- return BROWSER_PROMPT_PATTERNS.some((pattern) => pattern.test(normalizedPrompt));
229
- }
230
216
  export function getLatestUserPrompt(branch) {
231
217
  for (let index = branch.length - 1; index >= 0; index -= 1) {
232
218
  const entry = branch[index];
@@ -101,7 +101,8 @@ export async function renderAgentBrowserObservation(options) {
101
101
  let spill;
102
102
  let spillError;
103
103
  try {
104
- spill = await writeLargeOutputSpillFile({ data: { ...observation, ...(!options.json ? { text: prose } : {}) }, persistentArtifactStore: options.persistentArtifactStore, text });
104
+ const write = () => writeLargeOutputSpillFile({ data: { ...observation, ...(!options.json ? { text: prose } : {}) }, persistentArtifactStore: options.persistentArtifactStore, text });
105
+ spill = await (options.withArtifactWrite ? options.withArtifactWrite(write) : write());
105
106
  artifactManifest = applyArtifactManifest({ content: [], summary: "" }, artifactManifest, buildSpillArtifactEntries({
106
107
  commandInfo: { command: typeof options.details.command === "string" ? options.details.command : undefined },
107
108
  evictedArtifacts: spill.evictedArtifacts, path: spill.path, storageScope: spill.storageScope,
@@ -1,8 +1,8 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
3
  import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
3
- import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
4
- import { getBrowserResultMessage } from "./browser-transcript.js";
5
- import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
4
+ import { getBrowserRecord, snapshotFromDefinition } from "./browser-transcript.js";
5
+ import { isCloseCommand, isOpenNavigationCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
6
6
  import { isRecord } from "./parsing.js";
7
7
  import { findReadConfirmation as findPendingReadConfirmation, parseReadConfirmation } from "./read-confirmation.js";
8
8
  import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
@@ -148,43 +148,6 @@ export function deriveSessionTabTarget(options) {
148
148
  return observedTarget ?? options.previousTarget;
149
149
  return undefined;
150
150
  }
151
- function batchContainsOnlyReadOnlyDiagnosticTargets(data) {
152
- if (!Array.isArray(data) || data.length === 0) {
153
- return false;
154
- }
155
- return data.every((item) => {
156
- if (!isRecord(item))
157
- return false;
158
- const [command, subcommand] = extractBatchResultCommand(item);
159
- return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand);
160
- });
161
- }
162
- function getRestoredSessionTabTarget(details, command, subcommand) {
163
- if (isReadOnlyDiagnosticSessionTargetCommand(command, subcommand)) {
164
- return undefined;
165
- }
166
- const storedTarget = isRecord(details.sessionTabTarget)
167
- ? normalizeSessionTabTarget({
168
- targetId: typeof details.sessionTabTarget.targetId === "string" ? details.sessionTabTarget.targetId : undefined,
169
- title: typeof details.sessionTabTarget.title === "string" ? details.sessionTabTarget.title : undefined,
170
- url: typeof details.sessionTabTarget.url === "string" ? details.sessionTabTarget.url : undefined,
171
- })
172
- : undefined;
173
- if (command !== "batch") {
174
- return storedTarget;
175
- }
176
- if (isRecord(details.compiledNetworkSourceLookup) || batchContainsOnlyReadOnlyDiagnosticTargets(details.data)) {
177
- return undefined;
178
- }
179
- // The stored target includes post-batch live verification and native tab identity.
180
- if (storedTarget || details.sessionTabTargetUnknown === true)
181
- return storedTarget;
182
- const batchTarget = extractSessionTabTargetFromBatchResults(details.data);
183
- if (batchTarget) {
184
- return batchTarget;
185
- }
186
- return storedTarget;
187
- }
188
151
  function extractRefSnapshotRefs(data) {
189
152
  if (!isRecord(data) || !isRecord(data.refs))
190
153
  return undefined;
@@ -278,62 +241,6 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
278
241
  }
279
242
  return latestState;
280
243
  }
281
- function getRestoredRefSnapshotInvalidation(details, command) {
282
- const invalidation = isRecord(details.refSnapshotInvalidation) ? details.refSnapshotInvalidation : undefined;
283
- if (invalidation?.reason === "no-active-page")
284
- return buildNoActivePageRefSnapshotInvalidation();
285
- if (invalidation?.reason === "page-transition")
286
- return buildPageTransitionRefSnapshotInvalidation(typeof invalidation.summary === "string" ? invalidation.summary : undefined);
287
- const errorText = typeof details.error === "string"
288
- ? details.error
289
- : typeof details.summary === "string"
290
- ? details.summary
291
- : undefined;
292
- return isNoActivePageSnapshotFailure(command, errorText) ? buildNoActivePageRefSnapshotInvalidation() : undefined;
293
- }
294
- function getRestoredRefSnapshot(details) {
295
- const refSnapshot = isRecord(details.refSnapshot) ? details.refSnapshot : undefined;
296
- if (!refSnapshot || !Array.isArray(refSnapshot.refIds))
297
- return undefined;
298
- const refIds = refSnapshot.refIds.filter((refId) => typeof refId === "string" && /^e\d+$/.test(refId));
299
- const refRecord = isRecord(refSnapshot.refs) ? refSnapshot.refs : undefined;
300
- const refEntries = refRecord
301
- ? Object.fromEntries(refIds.flatMap((refId) => {
302
- const entry = refRecord[refId];
303
- if (!isRecord(entry) || typeof entry.name !== "string" || typeof entry.role !== "string")
304
- return [];
305
- const isContentEditable = typeof entry.isContentEditable === "boolean" ? entry.isContentEditable : undefined;
306
- const isEditable = typeof entry.isEditable === "boolean" ? entry.isEditable : undefined;
307
- return [[refId, { ...(isContentEditable !== undefined ? { isContentEditable } : {}), ...(isEditable !== undefined ? { isEditable } : {}), name: entry.name, role: entry.role }]];
308
- }))
309
- : undefined;
310
- return {
311
- refIds,
312
- ...(refEntries && Object.keys(refEntries).length > 0 ? { refs: refEntries } : {}),
313
- target: isRecord(refSnapshot.target)
314
- ? normalizeSessionTabTarget({
315
- title: typeof refSnapshot.target.title === "string" ? refSnapshot.target.title : undefined,
316
- url: typeof refSnapshot.target.url === "string" ? refSnapshot.target.url : undefined,
317
- })
318
- : undefined,
319
- };
320
- }
321
- function getLatestTabTargetOrder(targets, unknownTargets) {
322
- let latestOrder = 0;
323
- for (const target of targets.values())
324
- latestOrder = Math.max(latestOrder, target.order);
325
- for (const order of unknownTargets.values())
326
- latestOrder = Math.max(latestOrder, order);
327
- return latestOrder;
328
- }
329
- function getLatestRefStateOrder(snapshots, invalidations) {
330
- let latestOrder = 0;
331
- for (const snapshot of snapshots.values())
332
- latestOrder = Math.max(latestOrder, snapshot.order);
333
- for (const invalidation of invalidations.values())
334
- latestOrder = Math.max(latestOrder, invalidation.order);
335
- return latestOrder;
336
- }
337
244
  function shouldApplyTabTargetUpdate(current, unknownOrder, updateOrder) {
338
245
  return updateOrder >= Math.max(current?.order ?? 0, unknownOrder ?? 0);
339
246
  }
@@ -342,7 +249,7 @@ function shouldApplyRefStateUpdate(options) {
342
249
  return options.updateOrder >= currentOrder;
343
250
  }
344
251
  function stripRefSnapshotOrder(snapshot) {
345
- return snapshot ? { refIds: snapshot.refIds, ...(snapshot.refs ? { refs: snapshot.refs } : {}), target: snapshot.target } : undefined;
252
+ return snapshot ? { ...(snapshot.snapshotId ? { snapshotId: snapshot.snapshotId } : {}), ...(snapshot.generation ? { generation: snapshot.generation } : {}), refIds: snapshot.refIds, ...(snapshot.refs ? { refs: snapshot.refs } : {}), target: snapshot.target } : undefined;
346
253
  }
347
254
  function stripRefSnapshotInvalidationOrder(invalidation) {
348
255
  return invalidation ? { reason: invalidation.reason, summary: invalidation.summary } : undefined;
@@ -358,78 +265,96 @@ export class SessionPageState {
358
265
  tabTargetUnknownOrders = new Map();
359
266
  tabTargets = new Map();
360
267
  updateOrder = 0;
268
+ nativeGenerations = new Map();
269
+ pending = new Map();
361
270
  static fromBranch(branch) {
362
271
  const state = new SessionPageState();
363
- let restoredOrder = 0;
364
272
  for (const entry of branch) {
365
- const message = getBrowserResultMessage(entry);
366
- if (!message)
367
- continue;
368
- const details = isRecord(message.details) ? message.details : undefined;
369
- if (!details)
273
+ const record = getBrowserRecord(entry);
274
+ if (record)
275
+ state.applyBrowserRecord(record);
276
+ }
277
+ for (const key of state.tabTargets.keys())
278
+ state.tabPinningReasons.set(key, "restore");
279
+ return state;
280
+ }
281
+ /** A command operates on its admitted state while the committed view is unavailable. */
282
+ fork() {
283
+ const state = new SessionPageState();
284
+ state.readConfirmations = new Map(this.readConfirmations);
285
+ state.refSnapshotInvalidations = new Map(this.refSnapshotInvalidations);
286
+ state.refSnapshots = new Map(this.refSnapshots);
287
+ state.tabPinningReasons = new Map(this.tabPinningReasons);
288
+ state.tabTargetUnknownOrders = new Map(this.tabTargetUnknownOrders);
289
+ state.tabTargets = new Map([...this.tabTargets].map(([key, value]) => [key, { ...value }]));
290
+ state.pending = new Map(this.pending);
291
+ state.nativeGenerations = new Map(this.nativeGenerations);
292
+ state.updateOrder = this.updateOrder;
293
+ return state;
294
+ }
295
+ views() {
296
+ return new Map([...new Set([...this.tabTargets.keys(), ...this.tabTargetUnknownOrders.keys(), ...this.refSnapshots.keys(), ...this.refSnapshotInvalidations.keys()])].map(key => [key, this.get(key)]));
297
+ }
298
+ /** The same reducer commits live observations and replays selected journal envelopes. */
299
+ applyBrowserRecord({ event, snapshot }) {
300
+ const update = this.beginUpdate();
301
+ const confirmation = parseReadConfirmation(event.state.readConfirmation);
302
+ for (const page of event.pages ?? []) {
303
+ const key = page.key;
304
+ if (event.phase === "begin") {
305
+ this.pending.set(key, { operationId: event.operationId, snapshot: this.refSnapshots.get(key) ?? this.pending.get(key)?.snapshot });
306
+ this.markTabTargetUnknown({ sessionName: key, update });
307
+ this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: buildPageTransitionRefSnapshotInvalidation("This browser operation has no persisted finish. Inspect the current URL and take a fresh snapshot before using refs; changes may already have happened.") });
370
308
  continue;
371
- const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
372
- const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
373
- const sessionKey = getSessionPageStateKey(sessionName, namespace);
374
- const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
375
- const commandTokens = extractUpstreamCommandTokens(args);
376
- const command = typeof details.command === "string" ? details.command : commandTokens[0];
377
- const subcommand = typeof details.subcommand === "string" ? details.subcommand : commandTokens[1];
378
- const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
379
- const closeAllApplied = details.closeAllApplied === true
380
- || (message.isError !== true && isCloseAllCommand(commandTokens))
381
- || batchHasSuccessfulCloseAll(details.batchSteps);
382
- const lifecycleReset = (isCloseCommand(command) && message.isError !== true) || batchCloseLifecycle !== undefined;
383
- if (closeAllApplied) {
384
- restoredOrder += 1;
385
- state.clearNamespace(namespace);
386
309
  }
387
- else if (sessionKey && lifecycleReset) {
388
- restoredOrder += 1;
389
- state.clearSession(sessionKey);
390
- }
391
- const readConfirmation = parseReadConfirmation(details.readConfirmation);
392
- if (readConfirmation)
393
- state.applyReadConfirmation(readConfirmation, ++restoredOrder);
394
- if (!sessionKey || ((closeAllApplied || lifecycleReset) && (isCloseCommand(command) || batchCloseLifecycle?.endsClosed === true)))
310
+ if (page.clear) {
311
+ this.clearSession(key);
395
312
  continue;
396
- const tabTarget = getRestoredSessionTabTarget(details, command, subcommand);
397
- const tabTargetUnknown = details.sessionTabTargetUnknown === true;
398
- const reopenPending = typeof details.sessionTabReopenPending === "boolean" ? details.sessionTabReopenPending : undefined;
399
- const refSnapshotInvalidation = getRestoredRefSnapshotInvalidation(details, command);
400
- const refSnapshot = refSnapshotInvalidation ? undefined : getRestoredRefSnapshot(details);
401
- if (!tabTarget && !tabTargetUnknown && !refSnapshotInvalidation && !refSnapshot && reopenPending === undefined)
313
+ }
314
+ const pending = this.pending.get(key);
315
+ if (event.phase === "finish" && pending && pending.operationId !== event.operationId)
402
316
  continue;
403
- restoredOrder += 1;
404
- if (tabTargetUnknown) {
405
- state.refSnapshots.delete(sessionKey);
406
- if (refSnapshotInvalidation)
407
- state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
408
- else
409
- state.refSnapshotInvalidations.delete(sessionKey);
410
- state.tabTargets.delete(sessionKey);
411
- state.tabTargetUnknownOrders.set(sessionKey, restoredOrder);
317
+ if (page.unknown || page.refs.kind === "unknown") {
318
+ this.markTabTargetUnknown({ sessionName: key, update });
319
+ this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: page.refs.kind === "unknown" && page.refs.invalidation
320
+ ? page.refs.invalidation : buildPageTransitionRefSnapshotInvalidation("The browser target or operation outcome is unknown. Verify the current URL and take a fresh snapshot before using refs.") });
412
321
  continue;
413
322
  }
414
- if (tabTarget) {
415
- state.tabTargetUnknownOrders.delete(sessionKey);
416
- state.tabTargets.set(sessionKey, { order: restoredOrder, reopenPending: state.tabTargets.get(sessionKey)?.reopenPending, target: tabTarget });
323
+ if (page.target)
324
+ this.applyTabTarget({ sessionName: key, target: page.target, update });
325
+ else {
326
+ this.tabTargets.delete(key);
327
+ this.tabTargetUnknownOrders.delete(key);
328
+ }
329
+ if (page.reopenPending !== undefined)
330
+ this.setTabReopenPending({ pending: page.reopenPending, sessionName: key, update });
331
+ if (page.pinningReason)
332
+ this.markPinning(key, page.pinningReason);
333
+ else
334
+ this.tabPinningReasons.delete(key);
335
+ if (page.refs.kind === "replace") {
336
+ const definition = snapshot?.id === page.refs.snapshotId && snapshot.refs ? snapshotFromDefinition(snapshot)
337
+ : { snapshotId: page.refs.snapshotId, refIds: [] };
338
+ this.applyRefSnapshot({ sessionName: key, snapshot: definition, fallbackTarget: page.target, update });
417
339
  }
418
- const currentTarget = state.tabTargets.get(sessionKey);
419
- if (currentTarget && reopenPending !== undefined)
420
- currentTarget.reopenPending = reopenPending;
421
- if (refSnapshotInvalidation) {
422
- state.refSnapshots.delete(sessionKey);
423
- state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
340
+ else if (page.refs.kind === "reuse") {
341
+ const candidate = pending?.snapshot ?? this.refSnapshots.get(key);
342
+ if (candidate?.snapshotId === page.refs.snapshotId)
343
+ this.applyRefSnapshot({ sessionName: key, snapshot: candidate, fallbackTarget: page.target, update });
344
+ else
345
+ this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: buildPageTransitionRefSnapshotInvalidation("The ancestral snapshot definition is unavailable. Take a new complete snapshot before using refs.") });
424
346
  }
425
- else if (refSnapshot) {
426
- state.refSnapshotInvalidations.delete(sessionKey);
427
- state.refSnapshots.set(sessionKey, { ...refSnapshot, order: restoredOrder });
347
+ else {
348
+ this.refSnapshots.delete(key);
349
+ if (page.refs.invalidation)
350
+ this.applyRefSnapshotInvalidation({ sessionName: key, invalidation: page.refs.invalidation, update });
351
+ else
352
+ this.refSnapshotInvalidations.delete(key);
428
353
  }
354
+ this.pending.delete(key);
429
355
  }
430
- state.updateOrder = Math.max(restoredOrder, getLatestTabTargetOrder(state.tabTargets, state.tabTargetUnknownOrders), getLatestRefStateOrder(state.refSnapshots, state.refSnapshotInvalidations));
431
- state.tabPinningReasons = new Map([...state.tabTargets.keys()].map((sessionName) => [sessionName, "restore"]));
432
- return state;
356
+ if (confirmation)
357
+ this.applyReadConfirmation(confirmation, update);
433
358
  }
434
359
  beginUpdate() {
435
360
  this.updateOrder += 1;
@@ -437,6 +362,8 @@ export class SessionPageState {
437
362
  }
438
363
  reset() {
439
364
  this.readConfirmations.clear();
365
+ this.pending.clear();
366
+ this.nativeGenerations.clear();
440
367
  this.refSnapshotInvalidations = new Map();
441
368
  this.refSnapshots = new Map();
442
369
  this.tabPinningReasons = new Map();
@@ -490,11 +417,20 @@ export class SessionPageState {
490
417
  })) {
491
418
  return { ...this.get(options.sessionName), applied: false, stale: true };
492
419
  }
493
- const snapshot = { ...options.snapshot, target: options.snapshot.target ?? options.fallbackTarget };
420
+ const snapshot = { ...options.snapshot, generation: options.snapshot.generation ?? this.nativeGenerations.get(options.sessionName), snapshotId: options.snapshot.snapshotId ?? randomUUID(), target: options.snapshot.target ?? options.fallbackTarget };
494
421
  this.refSnapshotInvalidations.delete(options.sessionName);
495
422
  this.refSnapshots.set(options.sessionName, { ...snapshot, order: options.update });
496
423
  return { ...this.get(options.sessionName), applied: true };
497
424
  }
425
+ bindSnapshotGeneration(sessionName, generation) {
426
+ if (generation)
427
+ this.nativeGenerations.set(sessionName, generation);
428
+ else
429
+ this.nativeGenerations.delete(sessionName);
430
+ const snapshot = this.refSnapshots.get(sessionName);
431
+ if (snapshot)
432
+ this.refSnapshots.set(sessionName, { ...snapshot, generation });
433
+ }
498
434
  applyRefSnapshotInvalidation(options) {
499
435
  if (!shouldApplyRefStateUpdate({
500
436
  currentInvalidation: this.refSnapshotInvalidations.get(options.sessionName),
@@ -519,6 +455,8 @@ export class SessionPageState {
519
455
  return { ...this.get(options.sessionName), applied: true };
520
456
  }
521
457
  clearSession(sessionName) {
458
+ this.pending.delete(sessionName);
459
+ this.nativeGenerations.delete(sessionName);
522
460
  this.readConfirmations.delete(sessionName);
523
461
  this.refSnapshotInvalidations.delete(sessionName);
524
462
  this.refSnapshots.delete(sessionName);
@@ -1,4 +1,4 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { randomBytes, randomInt } from "node:crypto";
2
2
  import { existsSync, readdirSync, rmSync } from "node:fs";
3
3
  import { chmod, mkdir, mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
@@ -10,6 +10,8 @@ const TEMP_ROOT_MARKER_FILE_NAME = ".pi-agent-browser-owner.json";
10
10
  const TEMP_ROOT_MARKER_KIND = "pi-agent-browser-temp-root";
11
11
  const TEMP_ROOT_MARKER_VERSION = 2;
12
12
  const STALE_TEMP_ROOT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
13
+ const STALE_TEMP_ROOT_BATCH_SIZE = 8;
14
+ let lastTempGcName;
13
15
  const TEMP_ROOT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_TEMP_ROOT_MAX_BYTES";
14
16
  const DEFAULT_TEMP_ROOT_MAX_BYTES = 32 * 1_024 * 1_024;
15
17
  const SESSION_ARTIFACT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MAX_BYTES";
@@ -237,10 +239,17 @@ async function pruneStaleTempRoots(currentTempRoot) {
237
239
  const entries = await readdir(tmpdir(), { withFileTypes: true }).catch(() => []);
238
240
  const cutoffTime = Date.now() - STALE_TEMP_ROOT_MAX_AGE_MS;
239
241
  const currentUid = getCurrentProcessUid();
240
- await Promise.all(entries
241
- .filter((entry) => entry.isDirectory() && entry.name.startsWith(TEMP_ROOT_PREFIX))
242
- .map(async (entry) => {
243
- const path = join(tmpdir(), entry.name);
242
+ // ponytail: list/sort all names; stream directories if enumeration becomes the bottleneck.
243
+ const candidates = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith(TEMP_ROOT_PREFIX))
244
+ .map((entry) => entry.name).sort();
245
+ if (candidates.length === 0)
246
+ return;
247
+ const start = lastTempGcName === undefined ? randomInt(candidates.length)
248
+ : Math.max(0, candidates.findIndex((name) => name > lastTempGcName));
249
+ const batch = Array.from({ length: Math.min(STALE_TEMP_ROOT_BATCH_SIZE, candidates.length) }, (_, offset) => candidates[(start + offset) % candidates.length]);
250
+ lastTempGcName = batch.at(-1);
251
+ await Promise.all(batch.map(async (name) => {
252
+ const path = join(tmpdir(), name);
244
253
  if (path === currentTempRoot)
245
254
  return;
246
255
  const ownershipMarker = await readTempRootOwnershipMarker(path);
@@ -400,7 +409,6 @@ async function prunePersistentSessionArtifactsToBudget(sessionArtifactDir, addit
400
409
  async function getSessionTempRoot() {
401
410
  if (!sessionTempRootPromise) {
402
411
  sessionTempRootPromise = (async () => {
403
- await pruneStaleTempRoots(undefined);
404
412
  const tempRoot = await mkdtemp(join(tmpdir(), TEMP_ROOT_PREFIX));
405
413
  await chmod(tempRoot, 0o700).catch(() => undefined);
406
414
  await writeSecureTempRootOwnershipMarker(tempRoot);
@@ -1,7 +1,6 @@
1
1
  import { Text } from "@earendil-works/pi-tui";
2
2
  import { formatAgentBrowserRenderCall } from "./pi-tool-rendering.js";
3
3
  import { AGENT_BROWSER_PARAMS, AGENT_BROWSER_CODE_PARAMS, AGENT_BROWSER_ACTION_PARAMS, AGENT_BROWSER_QA_PARAMS, AGENT_BROWSER_ELECTRON_PARAMS, AGENT_BROWSER_SOURCE_PARAMS, AGENT_BROWSER_NETWORK_SOURCE_PARAMS, AGENT_BROWSER_TOOLS_PARAMS, } from "./input-modes/params.js";
4
- import { ADVANCED_TOOL_PROMPT_GUIDELINES, buildToolPromptGuidelines } from "./playbook.js";
5
4
  export const AGENT_BROWSER_TOOL_INVENTORY = {
6
5
  action: { name: "agent_browser_action", description: "Click, check, fill, or select a unique accessible target using stable locators or current refs." },
7
6
  qa: { name: "agent_browser_qa", description: "Check a URL or attached page with visible assertions and a diagnostic pass/fail verdict." },
@@ -9,23 +8,18 @@ export const AGENT_BROWSER_TOOL_INVENTORY = {
9
8
  source: { name: "agent_browser_source", description: "Find experimental DOM/React/local-source candidates with a bounded workspace scan." },
10
9
  network: { name: "agent_browser_network_source", description: "Correlate failed requests with initiator and local-source candidates." },
11
10
  };
12
- export const AGENT_BROWSER_DISCOVERY_GROUP = {
11
+ export const AGENT_BROWSER_INSTRUCTION_GROUP = {
13
12
  name: "browser",
14
13
  description: "Search the web, browse and interact with pages, verify browser UI, and inspect Electron apps.",
15
- sections: ["agent_browser"],
16
14
  };
17
- const entryDiscovery = { discovery: { group: AGENT_BROWSER_DISCOVERY_GROUP, role: "entry" } };
18
- const advancedDiscovery = { discovery: { group: AGENT_BROWSER_DISCOVERY_GROUP, role: "advanced" } };
19
15
  const advancedNames = new Set(Object.values(AGENT_BROWSER_TOOL_INVENTORY).map(({ name }) => name));
20
16
  /** Register once; advanced calls adapt only their input shape and reuse the ordinary executor. */
21
17
  export function registerAgentBrowserToolSurface(pi, options) {
22
18
  pi.registerTool({
23
- ...entryDiscovery,
24
19
  name: "agent_browser",
25
20
  label: "Agent Browser",
26
21
  description: "Browse and interact through native agent-browser commands. One command in args; fixed sequences use batch --bail and JSON-array stdin. Use agent_browser_code for loops/branches and agent_browser_tools for advanced capabilities.",
27
22
  promptSnippet: "Browse pages, inspect current refs, interact, and run native command batches.",
28
- promptGuidelines: options.promptGuidelines ?? buildToolPromptGuidelines({ includeWebSearch: false }),
29
23
  parameters: AGENT_BROWSER_PARAMS,
30
24
  renderCall: options.renderCall,
31
25
  renderResult: options.renderResult,
@@ -33,7 +27,6 @@ export function registerAgentBrowserToolSurface(pi, options) {
33
27
  executionMode: options.executionMode,
34
28
  });
35
29
  pi.registerTool({
36
- ...entryDiscovery,
37
30
  name: "agent_browser_code",
38
31
  label: "Agent Browser Code",
39
32
  description: "Run fresh JavaScript against a persistent browser. await browser({args,stdin?,timeoutMs?}) returns success/data/error/nextActions and imageObservations; emit(selected JSON) and emitImage(image handle) explicitly choose output. No host APIs or imports. Use native batch for fixed sequences.",
@@ -49,10 +42,8 @@ export function registerAgentBrowserToolSurface(pi, options) {
49
42
  renderResult: options.renderResult,
50
43
  });
51
44
  pi.registerTool({
52
- ...advancedDiscovery,
53
45
  ...AGENT_BROWSER_TOOL_INVENTORY.action,
54
46
  label: "Browser Action",
55
- promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.action],
56
47
  parameters: AGENT_BROWSER_ACTION_PARAMS,
57
48
  renderResult: options.renderResult,
58
49
  executionMode: options.executionMode,
@@ -61,10 +52,8 @@ export function registerAgentBrowserToolSurface(pi, options) {
61
52
  },
62
53
  });
63
54
  pi.registerTool({
64
- ...advancedDiscovery,
65
55
  ...AGENT_BROWSER_TOOL_INVENTORY.qa,
66
56
  label: "Browser QA",
67
- promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.qa],
68
57
  parameters: AGENT_BROWSER_QA_PARAMS,
69
58
  renderResult: options.renderResult,
70
59
  executionMode: options.executionMode,
@@ -73,10 +62,8 @@ export function registerAgentBrowserToolSurface(pi, options) {
73
62
  },
74
63
  });
75
64
  pi.registerTool({
76
- ...advancedDiscovery,
77
65
  ...AGENT_BROWSER_TOOL_INVENTORY.electron,
78
66
  label: "Browser Electron",
79
- promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.electron],
80
67
  parameters: AGENT_BROWSER_ELECTRON_PARAMS,
81
68
  renderResult: options.renderResult,
82
69
  executionMode: options.executionMode,
@@ -85,10 +72,8 @@ export function registerAgentBrowserToolSurface(pi, options) {
85
72
  },
86
73
  });
87
74
  pi.registerTool({
88
- ...advancedDiscovery,
89
75
  ...AGENT_BROWSER_TOOL_INVENTORY.source,
90
76
  label: "Browser Source",
91
- promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.source],
92
77
  parameters: AGENT_BROWSER_SOURCE_PARAMS,
93
78
  renderResult: options.renderResult,
94
79
  executionMode: options.executionMode,
@@ -97,10 +82,8 @@ export function registerAgentBrowserToolSurface(pi, options) {
97
82
  },
98
83
  });
99
84
  pi.registerTool({
100
- ...advancedDiscovery,
101
85
  ...AGENT_BROWSER_TOOL_INVENTORY.network,
102
86
  label: "Browser Network Source",
103
- promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.network],
104
87
  parameters: AGENT_BROWSER_NETWORK_SOURCE_PARAMS,
105
88
  renderResult: options.renderResult,
106
89
  executionMode: options.executionMode,
@@ -109,7 +92,6 @@ export function registerAgentBrowserToolSurface(pi, options) {
109
92
  },
110
93
  });
111
94
  pi.registerTool({
112
- ...entryDiscovery,
113
95
  name: "agent_browser_tools",
114
96
  label: "Browser Tools",
115
97
  description: "List or enable advanced browser tools: action → agent_browser_action; qa → agent_browser_qa; electron → agent_browser_electron; source → agent_browser_source; network → agent_browser_network_source. Omit enable for inventory. Activation only adds tools; it preserves other active tools.",
@@ -142,10 +124,17 @@ export function registerAgentBrowserToolSurface(pi, options) {
142
124
  const available = new Set(pi.getAllTools().map(({ name }) => name));
143
125
  if (!["agent_browser", "agent_browser_code", "agent_browser_tools", ...advancedNames].every(name => available.has(name)))
144
126
  return;
145
- // Keep Pi's replay helper behind the awaited restoration boundary, not factory registration.
146
- const { getCurrentSystemMessage } = await import("@earendil-works/pi-ai");
147
- const current = getCurrentSystemMessage(ctx.sessionManager.buildSessionProjection().messages);
148
- const restored = new Set(current?.toolsAdded?.map(({ name }) => name));
127
+ // Native dynamic imports bypass Pi's host-package aliases in consumer installs.
128
+ // Only tool names are needed; replay their native deltas without a host import.
129
+ const restored = new Set();
130
+ for (const message of ctx.sessionManager.buildSessionProjection().messages) {
131
+ if (message.role !== "system")
132
+ continue;
133
+ for (const removed of message.toolsRemoved ?? [])
134
+ restored.delete(removed.name);
135
+ for (const added of message.toolsAdded ?? [])
136
+ restored.add(added.name);
137
+ }
149
138
  const active = pi.getActiveTools().filter((name) => !advancedNames.has(name) || restored.has(name));
150
139
  pi.setActiveTools([...new Set([...active, ...[...restored].filter((name) => advancedNames.has(name) && available.has(name))])]);
151
140
  });
@@ -1,5 +1,4 @@
1
1
  import { JsonSchema } from "./json-schema.js";
2
- import { WEB_SEARCH_PROMPT_GUIDELINE } from "./playbook.js";
3
2
  import { StringEnum as localStringEnum } from "./string-enum-schema.js";
4
3
  import { DEFAULT_WEB_SEARCH_PROVIDER, EXA_SEARCH_TYPES, WEB_SEARCH_PROVIDERS, resolvePreferredWebSearchCredential, } from "./config.js";
5
4
  export const AGENT_BROWSER_WEB_SEARCH_TOOL_NAME = "agent_browser_web_search";
@@ -566,13 +565,6 @@ export function createAgentBrowserWebSearchTool(configState, options = {}) {
566
565
  label: "Agent Browser Web Search",
567
566
  description: `Search the live web with Exa or Brave for current or external information. For Exa research tasks, use searchType deep-lite or deeper. Returns up to ${MAX_SEARCH_RESULT_COUNT} concise web results.`,
568
567
  promptSnippet: "Search the live web with Exa or Brave for current or external information.",
569
- promptGuidelines: [
570
- WEB_SEARCH_PROMPT_GUIDELINE,
571
- "agent_browser_web_search chooses Exa or Brave from configured keys; when both are available, Exa is preferred by default unless webSearch.preferredProvider says otherwise. Use provider only when the user/config calls for a specific provider.",
572
- "Use Exa deep only when deep-lite may miss angles, and deep-reasoning only for exhaustive or still-thin research. Do not run parallel agent_browser_web_search calls; make one high-signal query, inspect its results, then at most one follow-up.",
573
- "If agent_browser_web_search returns HTTP 429, stop searching and tell the user the API plan/rate limit needs time or a plan change.",
574
- "After using agent_browser_web_search, cite result URLs in the final answer when web evidence informed the answer.",
575
- ],
576
568
  parameters: AgentBrowserWebSearchParams,
577
569
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
578
570
  const runtimeConfigState = ctx ? options.loadConfigState?.(ctx) ?? configState : configState;