pi-agent-browser-native 0.3.0 → 0.6.5

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 (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -1,5 +1,7 @@
1
- import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
2
- import { isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
1
+ import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
+ import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
3
+ import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
4
+ import { isCloseAllCommand, isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand } from "./command-taxonomy.js";
3
5
  import { isRecord } from "./parsing.js";
4
6
  import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
5
7
  import { enrichSnapshotRefEntries, getSnapshotRefEntries } from "./results/snapshot-refs.js";
@@ -93,6 +95,11 @@ export function extractSessionTabTargetFromBatchResults(data) {
93
95
  }
94
96
  const [name, subcommand] = extractBatchResultCommand(item);
95
97
  const result = item.result;
98
+ if (isCloseCommand(name)) {
99
+ currentTarget = undefined;
100
+ pendingTitle = undefined;
101
+ continue;
102
+ }
96
103
  if (name === "get" && subcommand === "title") {
97
104
  pendingTitle = extractStringResultField(result, "title");
98
105
  continue;
@@ -197,6 +204,20 @@ export function buildNoActivePageRefSnapshotInvalidation() {
197
204
  summary: "The latest snapshot for this session reported No active page. Old page-scoped refs are invalid until snapshot -i succeeds.",
198
205
  };
199
206
  }
207
+ export function buildPageTransitionRefSnapshotInvalidation(summary) {
208
+ return {
209
+ reason: "page-transition",
210
+ summary: summary ?? "A recording command (record start, or record restart with a URL) replaced or navigated the active page and invalidated the prior snapshot. Run snapshot -i before using page-scoped refs.",
211
+ };
212
+ }
213
+ export function getCommandRefSnapshotInvalidation(commandTokens) {
214
+ if (isRecordPageTransitionCommand(commandTokens))
215
+ return buildPageTransitionRefSnapshotInvalidation();
216
+ if (isWebMcpPageMutationCommand(commandTokens)) {
217
+ return buildPageTransitionRefSnapshotInvalidation("A WebMCP invoke, result, or cancel command can mutate, rerender, or navigate the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.");
218
+ }
219
+ return undefined;
220
+ }
200
221
  export function isNoActivePageSnapshotFailure(command, text) {
201
222
  return command === "snapshot" && /\bno active page\b/i.test(text ?? "");
202
223
  }
@@ -207,7 +228,17 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
207
228
  for (const item of data) {
208
229
  if (!isRecord(item))
209
230
  continue;
210
- const [name] = extractBatchResultCommand(item);
231
+ const commandTokens = extractBatchResultCommand(item);
232
+ const [name] = commandTokens;
233
+ if (item.success !== false && isCloseCommand(name)) {
234
+ latestState = undefined;
235
+ continue;
236
+ }
237
+ const transitionInvalidation = getCommandRefSnapshotInvalidation(commandTokens);
238
+ if (transitionInvalidation) {
239
+ latestState = { invalidation: transitionInvalidation };
240
+ continue;
241
+ }
211
242
  if (name !== "snapshot")
212
243
  continue;
213
244
  if (item.success === false) {
@@ -225,9 +256,10 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
225
256
  }
226
257
  function getRestoredRefSnapshotInvalidation(details, command) {
227
258
  const invalidation = isRecord(details.refSnapshotInvalidation) ? details.refSnapshotInvalidation : undefined;
228
- if (invalidation && invalidation.reason === "no-active-page") {
259
+ if (invalidation?.reason === "no-active-page")
229
260
  return buildNoActivePageRefSnapshotInvalidation();
230
- }
261
+ if (invalidation?.reason === "page-transition")
262
+ return buildPageTransitionRefSnapshotInvalidation(typeof invalidation.summary === "string" ? invalidation.summary : undefined);
231
263
  const errorText = typeof details.error === "string"
232
264
  ? details.error
233
265
  : typeof details.summary === "string"
@@ -316,14 +348,27 @@ export class SessionPageState {
316
348
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
317
349
  const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
318
350
  const sessionKey = getSessionPageStateKey(sessionName, namespace);
351
+ const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
352
+ const commandTokens = extractUpstreamCommandTokens(args);
353
+ const command = typeof details.command === "string" ? details.command : commandTokens[0];
354
+ const subcommand = typeof details.subcommand === "string" ? details.subcommand : commandTokens[1];
355
+ const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
356
+ const closeAllApplied = details.closeAllApplied === true
357
+ || (message.isError !== true && isCloseAllCommand(commandTokens))
358
+ || batchHasSuccessfulCloseAll(details.batchSteps);
359
+ if (closeAllApplied) {
360
+ restoredOrder += 1;
361
+ state.clearNamespace(namespace);
362
+ if (isCloseCommand(command) || batchCloseLifecycle?.endsClosed === true)
363
+ continue;
364
+ }
319
365
  if (!sessionKey)
320
366
  continue;
321
- const command = typeof details.command === "string" ? details.command : undefined;
322
- const subcommand = typeof details.subcommand === "string" ? details.subcommand : undefined;
323
- if (isCloseCommand(command) && message.isError !== true) {
367
+ if (!closeAllApplied && ((isCloseCommand(command) && message.isError !== true) || batchCloseLifecycle)) {
324
368
  restoredOrder += 1;
325
369
  state.clearSession(sessionKey);
326
- continue;
370
+ if (isCloseCommand(command) || batchCloseLifecycle?.endsClosed === true)
371
+ continue;
327
372
  }
328
373
  const tabTarget = getRestoredSessionTabTarget(details, command, subcommand);
329
374
  const tabTargetUnknown = details.sessionTabTargetUnknown === true;
@@ -333,8 +378,11 @@ export class SessionPageState {
333
378
  continue;
334
379
  restoredOrder += 1;
335
380
  if (tabTargetUnknown) {
336
- state.refSnapshotInvalidations.delete(sessionKey);
337
381
  state.refSnapshots.delete(sessionKey);
382
+ if (refSnapshotInvalidation)
383
+ state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
384
+ else
385
+ state.refSnapshotInvalidations.delete(sessionKey);
338
386
  state.tabTargets.delete(sessionKey);
339
387
  state.tabTargetUnknownOrders.set(sessionKey, restoredOrder);
340
388
  continue;
@@ -431,6 +479,19 @@ export class SessionPageState {
431
479
  this.tabTargetUnknownOrders.delete(sessionName);
432
480
  this.tabTargets.delete(sessionName);
433
481
  }
482
+ clearNamespace(namespace) {
483
+ const sessionKeys = new Set([
484
+ ...this.refSnapshotInvalidations.keys(),
485
+ ...this.refSnapshots.keys(),
486
+ ...this.tabPinningReasons.keys(),
487
+ ...this.tabTargetUnknownOrders.keys(),
488
+ ...this.tabTargets.keys(),
489
+ ]);
490
+ for (const sessionKey of sessionKeys) {
491
+ if (isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, namespace))
492
+ this.clearSession(sessionKey);
493
+ }
494
+ }
434
495
  markPinning(sessionName, reason) {
435
496
  this.tabPinningReasons.set(sessionName, reason);
436
497
  }
@@ -8,7 +8,6 @@ import { processStartIdentitiesMatch, readProcessStartIdentity } from "./process
8
8
  const TEMP_ROOT_PREFIX = "pi-agent-browser-";
9
9
  const TEMP_ROOT_MARKER_FILE_NAME = ".pi-agent-browser-owner.json";
10
10
  const TEMP_ROOT_MARKER_KIND = "pi-agent-browser-temp-root";
11
- const TEMP_ROOT_LEGACY_MARKER_VERSION = 1;
12
11
  const TEMP_ROOT_MARKER_VERSION = 2;
13
12
  const STALE_TEMP_ROOT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
14
13
  const TEMP_ROOT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_TEMP_ROOT_MAX_BYTES";
@@ -39,7 +38,7 @@ function isProtectedTempChildName(value) {
39
38
  function isTempRootOwnershipRecord(value) {
40
39
  if (!isRecord(value))
41
40
  return false;
42
- if (value.kind !== TEMP_ROOT_MARKER_KIND || ![TEMP_ROOT_LEGACY_MARKER_VERSION, TEMP_ROOT_MARKER_VERSION].includes(value.version))
41
+ if (value.kind !== TEMP_ROOT_MARKER_KIND || value.version !== TEMP_ROOT_MARKER_VERSION)
43
42
  return false;
44
43
  if (!isPositiveFiniteNumber(value.createdAtMs))
45
44
  return false;
@@ -0,0 +1,14 @@
1
+ import { MINIMUM_AGENT_BROWSER_VERSION, MINIMUM_AGENT_BROWSER_VERSION_LABEL, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL, isSupportedAgentBrowserVersion, } from "../../../scripts/agent-browser-target.mjs";
2
+ export { MINIMUM_AGENT_BROWSER_VERSION, MINIMUM_AGENT_BROWSER_VERSION_LABEL, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL, isSupportedAgentBrowserVersion, };
3
+ export function parseAgentBrowserVersionOutput(stdout) {
4
+ const match = stdout.trim().match(/^agent-browser\s+(\S+)$/);
5
+ return match?.[1];
6
+ }
7
+ export function getAgentBrowserVersionValidationError(stdout) {
8
+ const observed = parseAgentBrowserVersionOutput(stdout);
9
+ if (observed && isSupportedAgentBrowserVersion(observed))
10
+ return undefined;
11
+ return observed
12
+ ? `Installed agent-browser ${observed} is unsupported; stable versions must be at least ${MINIMUM_AGENT_BROWSER_VERSION_LABEL}. Install ${TARGET_AGENT_BROWSER_VERSION_LABEL} (recommended), run pi-agent-browser-doctor, then reload Pi.`
13
+ : `agent-browser --version returned an unrecognized value; expected ${SUPPORTED_AGENT_BROWSER_VERSION_LABEL}. Run pi-agent-browser-doctor and install a supported upstream version.`;
14
+ }
@@ -1,17 +1,24 @@
1
1
  import { JsonSchema } from "./json-schema.js";
2
2
  import { WEB_SEARCH_PROMPT_GUIDELINE } from "./playbook.js";
3
3
  import { StringEnum as localStringEnum } from "./string-enum-schema.js";
4
- import { DEFAULT_WEB_SEARCH_PROVIDER, WEB_SEARCH_PROVIDERS, resolvePreferredWebSearchCredential, } from "./config.js";
4
+ import { DEFAULT_WEB_SEARCH_PROVIDER, EXA_SEARCH_TYPES, WEB_SEARCH_PROVIDERS, resolvePreferredWebSearchCredential, } from "./config.js";
5
5
  export const AGENT_BROWSER_WEB_SEARCH_TOOL_NAME = "agent_browser_web_search";
6
6
  export const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
7
7
  export const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
8
8
  export const DEFAULT_SEARCH_RESULT_COUNT = 5;
9
9
  export const MAX_SEARCH_RESULT_COUNT = 10;
10
10
  export const SEARCH_REQUEST_TIMEOUT_MS = 15_000;
11
- export const EXA_DEEP_SEARCH_REQUEST_TIMEOUT_MS = 45_000;
11
+ export const EXA_DEEP_LITE_SEARCH_REQUEST_TIMEOUT_MS = 45_000;
12
+ export const EXA_DEEP_SEARCH_REQUEST_TIMEOUT_MS = 60_000;
13
+ export const EXA_DEEP_REASONING_SEARCH_REQUEST_TIMEOUT_MS = 90_000;
14
+ export const EXA_DYNAMIC_HIGHLIGHTS_BETA = "dynamic-highlights-2026-08-28";
12
15
  export const WEB_SEARCH_MIN_REQUEST_INTERVAL_MS = 1_100;
13
- export const EXA_SEARCH_TYPES = ["auto", "fast", "instant", "deep-lite", "deep", "deep-reasoning"];
16
+ export const EXA_SEARCH_SYSTEM_PROMPT = "Prefer primary, official sources. Respect any requested version or date. Avoid duplicate or equivalent results.";
17
+ export { EXA_SEARCH_TYPES };
14
18
  export const WEB_SEARCH_PROVIDER_PARAM_VALUES = ["auto", ...WEB_SEARCH_PROVIDERS];
19
+ export const EXA_SEARCH_CATEGORIES = ["company", "people", "publication", "news", "personal site", "financial report"];
20
+ const MAX_EXA_DOMAIN_FILTERS = 20;
21
+ const MAX_EXA_ADDITIONAL_QUERIES = 10;
15
22
  export function createAgentBrowserWebSearchParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
16
23
  return Type.Object({
17
24
  query: Type.String({
@@ -22,7 +29,28 @@ export function createAgentBrowserWebSearchParamsSchema(Type = JsonSchema, Strin
22
29
  description: `Optional provider override. auto uses configured keys and preferredProvider; when both Exa and Brave are available, the default preferred provider is ${DEFAULT_WEB_SEARCH_PROVIDER}.`,
23
30
  })),
24
31
  searchType: Type.Optional(StringEnum(EXA_SEARCH_TYPES, {
25
- description: "Optional Exa search type. Defaults to auto; ignored by Brave. Use deep/deep-reasoning only for harder research because they are slower.",
32
+ description: "Exa mode; omitted uses webSearch.defaultSearchType, then auto. instant (~250ms) is only for trivial lookups; fast (~450ms) favors latency; auto (~1s) is balanced. Pass searchType: deep-lite (~4s) for research before implementation unless config already defaults it; do not assume auto is deep enough. deep (4–15s) handles hard multi-source research; deep-reasoning (12–40s) is only for the hardest work. Brave ignores this field.",
33
+ })),
34
+ includeDomains: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
35
+ minItems: 1,
36
+ maxItems: MAX_EXA_DOMAIN_FILTERS,
37
+ description: `Exa only. Limit results to 1–${MAX_EXA_DOMAIN_FILTERS} hostnames, path prefixes (exa.ai/docs), or wildcard subdomains (*.substack.com).`,
38
+ })),
39
+ excludeDomains: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
40
+ minItems: 1,
41
+ maxItems: MAX_EXA_DOMAIN_FILTERS,
42
+ description: `Exa only. Exclude 1–${MAX_EXA_DOMAIN_FILTERS} hostnames, path prefixes, or wildcard subdomains. Not compatible with category company or people.`,
43
+ })),
44
+ category: Type.Optional(StringEnum(EXA_SEARCH_CATEGORIES, {
45
+ description: "Exa-only result category. company and people cannot be combined with freshness or excludeDomains.",
46
+ })),
47
+ additionalQueries: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
48
+ minItems: 1,
49
+ maxItems: MAX_EXA_ADDITIONAL_QUERIES,
50
+ description: `Exa only. Add 1–${MAX_EXA_ADDITIONAL_QUERIES} query variations when the effective searchType is deep-lite, deep, or deep-reasoning.`,
51
+ })),
52
+ highlightsDynamic: Type.Optional(Type.Boolean({
53
+ description: "Exa-only research preview. Allocate one highlight budget across all results; the wrapper sends the required Exa-Beta header. Regular per-page highlights remain the default.",
26
54
  })),
27
55
  count: Type.Optional(Type.Integer({
28
56
  minimum: 1,
@@ -198,12 +226,14 @@ export function normalizeBraveSearchResult(result) {
198
226
  const url = normalizeSearchUrl(result.url);
199
227
  if (!title || !url)
200
228
  return undefined;
229
+ const pageDate = cleanSearchText(result.page_age, 80);
201
230
  return {
202
231
  title,
203
232
  url,
204
233
  description: cleanSearchText(result.description, 320),
205
234
  source: cleanSearchText(result.profile?.name, 120) ?? cleanSearchText(result.meta_url?.hostname, 120),
206
235
  age: cleanSearchText(result.age, 80),
236
+ ...(pageDate ? { pageDate } : {}),
207
237
  language: cleanSearchText(result.language, 40),
208
238
  };
209
239
  }
@@ -213,13 +243,14 @@ export function normalizeExaSearchResult(result) {
213
243
  if (!title || !url)
214
244
  return undefined;
215
245
  const highlights = normalizeHighlightList(result.highlights);
246
+ const pageDate = cleanSearchText(result.publishedDate, 80);
216
247
  return {
217
248
  title,
218
249
  url,
219
250
  description: cleanSearchText(result.summary, 320) ?? highlights?.[0] ?? cleanSearchText(result.text, 320),
220
251
  highlights,
221
252
  source: cleanSearchText(result.author, 120) ?? cleanSearchText(getHostname(url), 120),
222
- age: cleanSearchText(result.publishedDate, 80),
253
+ ...(pageDate ? { pageDate } : {}),
223
254
  };
224
255
  }
225
256
  function getProviderLabel(provider) {
@@ -236,6 +267,8 @@ export function formatSearchResults(provider, query, results) {
236
267
  lines.push(` URL: ${result.url}`);
237
268
  if (result.source)
238
269
  lines.push(` Source: ${result.source}`);
270
+ if (result.pageDate)
271
+ lines.push(` ${provider === "exa" ? "Published" : "Page date"}: ${result.pageDate}`);
239
272
  if (result.age)
240
273
  lines.push(` Age: ${result.age}`);
241
274
  if (result.description)
@@ -277,14 +310,30 @@ function getStartPublishedDate(freshness, now) {
277
310
  return new Date(now().getTime() - days * 24 * 60 * 60 * 1000).toISOString();
278
311
  }
279
312
  export function buildExaSearchRequestBody(params, now = () => new Date()) {
313
+ const searchType = params.searchType ?? "auto";
314
+ if (params.additionalQueries?.length && !searchType.startsWith("deep")) {
315
+ throw new Error(`additionalQueries requires deep-lite, deep, or deep-reasoning; received ${searchType}.`);
316
+ }
317
+ if ((params.category === "company" || params.category === "people") && (params.freshness || params.excludeDomains?.length)) {
318
+ throw new Error(`category ${params.category} cannot be combined with freshness or excludeDomains.`);
319
+ }
280
320
  const body = {
281
321
  query: params.query,
282
- type: params.searchType ?? "auto",
322
+ type: searchType,
283
323
  numResults: Math.min(params.count + params.offset, 100),
284
- contents: { highlights: true },
324
+ contents: { highlights: params.highlightsDynamic ? { dynamic: true } : true },
325
+ systemPrompt: EXA_SEARCH_SYSTEM_PROMPT,
285
326
  };
327
+ if (params.category)
328
+ body.category = params.category;
286
329
  if (params.country)
287
330
  body.userLocation = params.country.toUpperCase();
331
+ if (params.includeDomains?.length)
332
+ body.includeDomains = params.includeDomains;
333
+ if (params.excludeDomains?.length)
334
+ body.excludeDomains = params.excludeDomains;
335
+ if (params.additionalQueries?.length)
336
+ body.additionalQueries = params.additionalQueries;
288
337
  if (params.safesearch && params.safesearch !== "off")
289
338
  body.moderation = true;
290
339
  const startPublishedDate = getStartPublishedDate(params.freshness, now);
@@ -396,7 +445,20 @@ export async function fetchBraveSearchJson(url, apiKey, signal) {
396
445
  });
397
446
  }
398
447
  function getExaRequestTimeoutMs(searchType) {
399
- return searchType?.startsWith("deep") ? EXA_DEEP_SEARCH_REQUEST_TIMEOUT_MS : SEARCH_REQUEST_TIMEOUT_MS;
448
+ if (searchType === "deep-lite")
449
+ return EXA_DEEP_LITE_SEARCH_REQUEST_TIMEOUT_MS;
450
+ if (searchType === "deep")
451
+ return EXA_DEEP_SEARCH_REQUEST_TIMEOUT_MS;
452
+ if (searchType === "deep-reasoning")
453
+ return EXA_DEEP_REASONING_SEARCH_REQUEST_TIMEOUT_MS;
454
+ return SEARCH_REQUEST_TIMEOUT_MS;
455
+ }
456
+ function usesDynamicHighlights(body) {
457
+ const contents = body.contents;
458
+ if (!contents || typeof contents !== "object" || Array.isArray(contents))
459
+ return false;
460
+ const highlights = contents.highlights;
461
+ return Boolean(highlights && typeof highlights === "object" && !Array.isArray(highlights) && highlights.dynamic === true);
400
462
  }
401
463
  export async function fetchExaSearchJson(body, apiKey, signal, timeoutMs = SEARCH_REQUEST_TIMEOUT_MS) {
402
464
  return fetchSearchJson({
@@ -408,6 +470,7 @@ export async function fetchExaSearchJson(body, apiKey, signal, timeoutMs = SEARC
408
470
  Accept: "application/json",
409
471
  "Content-Type": "application/json",
410
472
  "x-api-key": apiKey,
473
+ ...(usesDynamicHighlights(body) ? { "Exa-Beta": EXA_DYNAMIC_HIGHLIGHTS_BETA } : {}),
411
474
  },
412
475
  method: "POST",
413
476
  },
@@ -449,15 +512,7 @@ const EXA_WEB_SEARCH_ADAPTER = {
449
512
  buildRequest(params) {
450
513
  const searchType = params.searchType ?? "auto";
451
514
  return {
452
- body: buildExaSearchRequestBody({
453
- query: params.query,
454
- count: params.count,
455
- offset: params.offset,
456
- country: params.country,
457
- safesearch: params.safesearch,
458
- freshness: params.freshness,
459
- searchType,
460
- }),
515
+ body: buildExaSearchRequestBody(params),
461
516
  timeoutMs: getExaRequestTimeoutMs(searchType),
462
517
  };
463
518
  },
@@ -469,7 +524,7 @@ const EXA_WEB_SEARCH_ADAPTER = {
469
524
  return {
470
525
  extraDetails: {
471
526
  requestId: cleanSearchText(response.requestId, 120),
472
- searchType: cleanSearchText(response.searchType, 80) ?? searchType,
527
+ searchType,
473
528
  },
474
529
  results: (response.results ?? [])
475
530
  .map(normalizeExaSearchResult)
@@ -486,6 +541,15 @@ export const WEB_SEARCH_PROVIDER_ADAPTERS = {
486
541
  export function getWebSearchProviderAdapter(provider) {
487
542
  return WEB_SEARCH_PROVIDER_ADAPTERS[provider];
488
543
  }
544
+ export function dedupeSearchResults(results) {
545
+ const seen = new Set();
546
+ return results.filter((result) => {
547
+ if (seen.has(result.url))
548
+ return false;
549
+ seen.add(result.url);
550
+ return true;
551
+ });
552
+ }
489
553
  function buildMissingCredentialError(provider) {
490
554
  if (provider === "brave")
491
555
  return "agent_browser_web_search provider brave was requested but no BRAVE_API_KEY/config credential resolved.";
@@ -498,13 +562,13 @@ export function createAgentBrowserWebSearchTool(configState, options = {}) {
498
562
  return {
499
563
  name: AGENT_BROWSER_WEB_SEARCH_TOOL_NAME,
500
564
  label: "Agent Browser Web Search",
501
- description: `Search the web with Exa or Brave when configured. Returns up to ${MAX_SEARCH_RESULT_COUNT} concise web results.`,
565
+ 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.`,
502
566
  promptSnippet: "Search the live web with Exa or Brave for current or external information.",
503
567
  promptGuidelines: [
504
568
  WEB_SEARCH_PROMPT_GUIDELINE,
505
569
  "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.",
506
- "Prefer agent_browser_web_search over opening or typing into public search engine result pages with agent_browser when a quick result list is enough; browser-automated search forms are often anti-bot/CAPTCHA-gated, and this tool is the fallback for discovery rather than a CAPTCHA bypass.",
507
- "Do not issue parallel or repeated agent_browser_web_search calls; use one high-signal query, inspect the results, then only run a focused follow-up if needed. If the provider returns HTTP 429, stop searching and tell the user the API plan/rate limit needs time or a plan change.",
570
+ "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.",
571
+ "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.",
508
572
  "After using agent_browser_web_search, cite result URLs in the final answer when web evidence informed the answer.",
509
573
  ],
510
574
  parameters: AgentBrowserWebSearchParams,
@@ -520,6 +584,18 @@ export function createAgentBrowserWebSearchTool(configState, options = {}) {
520
584
  const resolved = await resolvePreferredWebSearchCredential(runtimeConfigState, { provider: requestedProvider, signal });
521
585
  if (!resolved)
522
586
  throw new Error(buildMissingCredentialError(requestedProvider));
587
+ if (resolved.provider === "brave") {
588
+ const exaOnlyFields = [
589
+ params.includeDomains ? "includeDomains" : undefined,
590
+ params.excludeDomains ? "excludeDomains" : undefined,
591
+ params.category ? "category" : undefined,
592
+ params.additionalQueries ? "additionalQueries" : undefined,
593
+ params.highlightsDynamic ? "highlightsDynamic" : undefined,
594
+ ].filter((field) => Boolean(field));
595
+ if (exaOnlyFields.length > 0) {
596
+ throw new Error(`${exaOnlyFields.join(", ")} ${exaOnlyFields.length === 1 ? "requires" : "require"} provider exa; resolved provider was brave.`);
597
+ }
598
+ }
523
599
  const query = params.query.trim();
524
600
  if (!query)
525
601
  throw new Error("query must not be blank");
@@ -527,18 +603,25 @@ export function createAgentBrowserWebSearchTool(configState, options = {}) {
527
603
  const offset = Math.max(params.offset ?? 0, 0);
528
604
  const adapter = getWebSearchProviderAdapter(resolved.provider);
529
605
  const executionParams = {
606
+ additionalQueries: params.additionalQueries,
607
+ category: params.category,
530
608
  country: params.country,
531
609
  count,
610
+ excludeDomains: params.excludeDomains,
532
611
  freshness: params.freshness,
612
+ highlightsDynamic: params.highlightsDynamic,
613
+ includeDomains: params.includeDomains,
533
614
  offset,
534
615
  query,
535
616
  safesearch: params.safesearch,
536
617
  searchLang: params.searchLang,
537
- searchType: params.searchType ?? "auto",
618
+ searchType: params.searchType ?? runtimeConfigState.config.webSearch?.defaultSearchType ?? "auto",
538
619
  };
539
620
  const request = adapter.buildRequest(executionParams);
540
621
  const data = await requestGate.run(signal, () => adapter.fetchJson(request, resolved.credential.value, signal));
541
622
  const normalized = adapter.normalizeResponse(data, executionParams);
623
+ const results = dedupeSearchResults(normalized.results);
624
+ const duplicatesRemoved = normalized.results.length - results.length;
542
625
  const details = {
543
626
  provider: adapter.provider,
544
627
  query,
@@ -547,10 +630,11 @@ export function createAgentBrowserWebSearchTool(configState, options = {}) {
547
630
  offset,
548
631
  ...normalized.extraDetails,
549
632
  fetchedAt: new Date().toISOString(),
550
- results: normalized.results,
633
+ results,
634
+ duplicatesRemoved: duplicatesRemoved || undefined,
551
635
  };
552
636
  return {
553
- content: [{ type: "text", text: formatSearchResults(adapter.provider, normalized.returnedQuery, normalized.results) }],
637
+ content: [{ type: "text", text: `${formatSearchResults(adapter.provider, normalized.returnedQuery, results)}${duplicatesRemoved ? `\n\nDuplicate URLs removed: ${duplicatesRemoved}.` : ""}` }],
554
638
  details,
555
639
  };
556
640
  },
@@ -0,0 +1,169 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createContext, runInContext, Script } from "node:vm";
3
+ function parseLimit(value, label) {
4
+ const parsed = Number(value);
5
+ if (!Number.isSafeInteger(parsed) || parsed <= 0)
6
+ throw new Error(`Invalid ${label}.`);
7
+ return parsed;
8
+ }
9
+ const maxMessageBytes = parseLimit(process.argv[2], "script IPC message limit");
10
+ const maxCumulativeBytes = parseLimit(process.argv[3], "script IPC cumulative limit");
11
+ let cumulativeBytes = 0;
12
+ let inputBuffer = Buffer.alloc(0);
13
+ let started = false;
14
+ const sandbox = Object.create(null);
15
+ const context = createContext(sandbox, {
16
+ codeGeneration: { strings: false, wasm: false },
17
+ name: "agent-browser-script",
18
+ });
19
+ const bridgeKey = `__piab_send_${randomBytes(16).toString("hex")}`;
20
+ const stateName = `__piab_state_${randomBytes(16).toString("hex")}`;
21
+ const hostSend = (json) => {
22
+ if (typeof json !== "string")
23
+ return false;
24
+ const bytes = Buffer.byteLength(json, "utf8") + 1;
25
+ if (bytes > maxMessageBytes || cumulativeBytes + bytes > maxCumulativeBytes)
26
+ return false;
27
+ cumulativeBytes += bytes;
28
+ try {
29
+ process.stdout.write(`${json}\n`);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ };
36
+ Object.setPrototypeOf(hostSend, null);
37
+ Object.freeze(hostSend);
38
+ sandbox[bridgeKey] = hostSend;
39
+ runInContext("const " + stateName + " = (() => {\n" +
40
+ " 'use strict';\n" +
41
+ " const send = globalThis[" + JSON.stringify(bridgeKey) + "];\n" +
42
+ " delete globalThis[" + JSON.stringify(bridgeKey) + "];\n" +
43
+ " for (const name of ['console','process','require','Buffer','fetch','WebSocket','setTimeout','setInterval','setImmediate','queueMicrotask','clearTimeout','clearInterval','clearImmediate']) Object.defineProperty(globalThis, name, { value: undefined, writable: false, configurable: false });\n" +
44
+ " const NativePromise = Promise;\n" +
45
+ " const promiseThen = Promise.prototype.then;\n" +
46
+ " const reflectApply = Reflect.apply;\n" +
47
+ " const pending = new Map();\n" +
48
+ " let nextId = 0;\n" +
49
+ " const encode = (value) => { const json = JSON.stringify(value); if (typeof json !== 'string') throw new TypeError('Value must be JSON-serializable.'); return json; };\n" +
50
+ " const sendValue = (value) => { const json = encode(value); if (json.length + 1 > " + maxMessageBytes + " || send(json) !== true) throw new RangeError('Script IPC limit exceeded.'); };\n" +
51
+ " const browser = function browser(params) {\n" +
52
+ " return new NativePromise((resolve, reject) => {\n" +
53
+ " const id = ++nextId;\n" +
54
+ " pending.set(id, { resolve, reject });\n" +
55
+ " try { sendValue({ type: 'call', id, params }); } catch (error) { pending.delete(id); reject(error); }\n" +
56
+ " });\n" +
57
+ " };\n" +
58
+ " const emit = function emit(value) { sendValue({ type: 'emit', value }); };\n" +
59
+ " Object.setPrototypeOf(browser, null);\n" +
60
+ " Object.setPrototypeOf(emit, null);\n" +
61
+ " Object.freeze(browser);\n" +
62
+ " Object.freeze(emit);\n" +
63
+ " Object.defineProperties(globalThis, { browser: { value: browser, writable: false, configurable: false }, emit: { value: emit, writable: false, configurable: false } });\n" +
64
+ " const complete = (ok, value) => {\n" +
65
+ " if (ok) {\n" +
66
+ " try { sendValue(value === undefined ? { type: 'complete', hasValue: false } : { type: 'complete', hasValue: true, value }); }\n" +
67
+ " catch { sendValue({ type: 'complete', error: { name: 'RangeError', message: 'Final script value is not serializable or exceeds the IPC limit.' } }); }\n" +
68
+ " return;\n" +
69
+ " }\n" +
70
+ " let name = 'Error'; let message = 'Script execution failed.';\n" +
71
+ " try { if (value && typeof value.name === 'string') name = value.name.slice(0, 80); } catch {}\n" +
72
+ " try { if (value && typeof value.message === 'string') message = value.message.replace(/[\\r\\n]+/g, ' ').slice(0, 400); } catch {}\n" +
73
+ " sendValue({ type: 'complete', error: { name, message } });\n" +
74
+ " };\n" +
75
+ " return Object.freeze({\n" +
76
+ " deliver(json) {\n" +
77
+ " const message = JSON.parse(json);\n" +
78
+ " const target = pending.get(message.id);\n" +
79
+ " if (!target) return;\n" +
80
+ " pending.delete(message.id);\n" +
81
+ " target.resolve(message.envelope);\n" +
82
+ " },\n" +
83
+ " run(thunk) {\n" +
84
+ " let promise;\n" +
85
+ " try { promise = reflectApply(thunk, undefined, []); } catch (error) { complete(false, error); return; }\n" +
86
+ " reflectApply(promiseThen, promise, [value => complete(true, value), error => complete(false, error)]);\n" +
87
+ " }\n" +
88
+ " });\n" +
89
+ "})();", context, { timeout: 1_000 });
90
+ const deliver = runInContext(`${stateName}.deliver`, context, { timeout: 1_000 });
91
+ function fail(name, message) {
92
+ hostSend(JSON.stringify({ type: "complete", error: { name, message } }));
93
+ }
94
+ function describeError(error, fallback) {
95
+ if (!error || typeof error !== "object")
96
+ return { message: fallback, name: "Error" };
97
+ const candidate = error;
98
+ return {
99
+ message: typeof candidate.message === "string" ? candidate.message.replace(/[\r\n]+/g, " ").slice(0, 400) : fallback,
100
+ name: typeof candidate.name === "string" ? candidate.name.slice(0, 80) : "Error",
101
+ };
102
+ }
103
+ function handleLine(line) {
104
+ const bytes = Buffer.byteLength(line, "utf8") + 1;
105
+ if (bytes > maxMessageBytes || cumulativeBytes + bytes > maxCumulativeBytes) {
106
+ fail("RangeError", "Script IPC limit exceeded.");
107
+ return;
108
+ }
109
+ cumulativeBytes += bytes;
110
+ let message;
111
+ try {
112
+ message = JSON.parse(line);
113
+ }
114
+ catch {
115
+ fail("Error", "Invalid parent IPC message.");
116
+ return;
117
+ }
118
+ if (!started) {
119
+ if (!message || typeof message !== "object" || message.type !== "start" || typeof message.code !== "string") {
120
+ fail("Error", "Invalid script start message.");
121
+ return;
122
+ }
123
+ started = true;
124
+ try {
125
+ const source = `'use strict';\n${stateName}.run(async function () {\n'use strict';\n${message.code}\n});`;
126
+ const script = new Script(source, {
127
+ filename: "agent-browser-script.js",
128
+ importModuleDynamically() {
129
+ process.exit(70);
130
+ },
131
+ });
132
+ script.runInContext(context, { timeout: undefined });
133
+ }
134
+ catch (error) {
135
+ const described = describeError(error, "Script compilation failed.");
136
+ fail(described.name, described.message);
137
+ }
138
+ return;
139
+ }
140
+ if (!message || typeof message !== "object" || message.type !== "response") {
141
+ fail("Error", "Invalid parent IPC response.");
142
+ return;
143
+ }
144
+ try {
145
+ deliver(line);
146
+ }
147
+ catch {
148
+ fail("Error", "Invalid browser response envelope.");
149
+ }
150
+ }
151
+ process.stdin.on("data", (rawChunk) => {
152
+ const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
153
+ inputBuffer = Buffer.concat([inputBuffer, chunk]);
154
+ if (inputBuffer.length > maxMessageBytes) {
155
+ fail("RangeError", "Script IPC message limit exceeded.");
156
+ process.stdin.pause();
157
+ return;
158
+ }
159
+ for (;;) {
160
+ const newline = inputBuffer.indexOf(10);
161
+ if (newline < 0)
162
+ break;
163
+ const line = inputBuffer.subarray(0, newline).toString("utf8");
164
+ inputBuffer = inputBuffer.subarray(newline + 1);
165
+ handleLine(line);
166
+ }
167
+ });
168
+ process.stdin.on("error", () => undefined);
169
+ hostSend(JSON.stringify({ type: "ready" }));
@@ -0,0 +1,21 @@
1
+ export const TARGET_AGENT_BROWSER_SOURCE = "scripts/agent-browser-target.mjs";
2
+ export const TARGET_AGENT_BROWSER_VERSION = "0.36.0";
3
+ export const TARGET_AGENT_BROWSER_VERSION_LABEL = `agent-browser ${TARGET_AGENT_BROWSER_VERSION}`;
4
+ export const MINIMUM_AGENT_BROWSER_VERSION = "0.35.0";
5
+ export const MINIMUM_AGENT_BROWSER_VERSION_LABEL = `agent-browser ${MINIMUM_AGENT_BROWSER_VERSION}`;
6
+ export const SUPPORTED_AGENT_BROWSER_VERSION_LABEL = `${MINIMUM_AGENT_BROWSER_VERSION_LABEL} or newer`;
7
+ function stableVersionParts(version) {
8
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
9
+ return match?.slice(1).map(Number);
10
+ }
11
+ export function isSupportedAgentBrowserVersion(version) {
12
+ const actual = stableVersionParts(version);
13
+ const minimum = stableVersionParts(MINIMUM_AGENT_BROWSER_VERSION);
14
+ if (!actual || !minimum)
15
+ return false;
16
+ for (let index = 0; index < minimum.length; index += 1) {
17
+ if (actual[index] !== minimum[index])
18
+ return actual[index] > minimum[index];
19
+ }
20
+ return true;
21
+ }