@agentium/browser 3.1.1 → 3.1.2

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.
package/dist/index.cjs CHANGED
@@ -32,10 +32,154 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  BrowserAgent: () => BrowserAgent,
34
34
  BrowserProvider: () => BrowserProvider,
35
- CredentialVault: () => CredentialVault
35
+ CredentialVault: () => CredentialVault,
36
+ buildActionSpace: () => buildActionSpace,
37
+ guessSearchQuery: () => guessSearchQuery,
38
+ isBlockedPageUrl: () => isBlockedPageUrl,
39
+ isBotChallengeText: () => isBotChallengeText,
40
+ isResultTitle: () => isResultTitle,
41
+ isSearchResultsUrl: () => isSearchResultsUrl,
42
+ labelToAction: () => labelToAction,
43
+ looksLikeResultList: () => looksLikeResultList,
44
+ searchUrl: () => searchUrl,
45
+ titlesFromElements: () => titlesFromElements
36
46
  });
37
47
  module.exports = __toCommonJS(index_exports);
38
48
 
49
+ // src/action-space.ts
50
+ var DEFAULT_MAX_ACTION_CHOICES = 40;
51
+ function searchUrl(query, engine = "duckduckgo") {
52
+ const q = encodeURIComponent(query.trim());
53
+ switch (engine) {
54
+ case "google":
55
+ return `https://www.google.com/search?q=${q}`;
56
+ case "bing":
57
+ return `https://www.bing.com/search?q=${q}`;
58
+ default:
59
+ return `https://html.duckduckgo.com/html/?q=${q}`;
60
+ }
61
+ }
62
+ function isSearchResultsUrl(url) {
63
+ try {
64
+ const u = new URL(url);
65
+ const host = u.hostname.replace(/^www\./, "");
66
+ if (host === "duckduckgo.com" || host.endsWith(".duckduckgo.com")) {
67
+ return u.searchParams.has("q") || u.pathname.includes("/html");
68
+ }
69
+ if (host === "google.com" || host.endsWith(".google.com")) return u.pathname.startsWith("/search");
70
+ if (host === "bing.com" || host.endsWith(".bing.com")) return u.pathname.startsWith("/search");
71
+ return false;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+ function isBlockedPageUrl(url) {
77
+ return /418\.html|\/sorry\/|captcha|challenge|cf-browser-verification/i.test(url);
78
+ }
79
+ function isBotChallengeText(text) {
80
+ return /unfortunately, bots use|select all squares containing a duck|unusual traffic from your computer|are you a robot|verify you are human|detected unusual traffic/i.test(
81
+ text
82
+ );
83
+ }
84
+ var CHROME_LABEL = /^(email us|feedback|privacy|terms|settings|facebook|twitter|reddit|about|help|sign in|log in|login|all regions|any time|more results|next|previous|images|videos|news|maps|shopping|lite|duckduckgo|menu|home|cookies|advertising|github|wikipedia|instagram|youtube)$/i;
85
+ function isResultTitle(label) {
86
+ const t = label.replace(/\s+/g, " ").trim();
87
+ if (t.length < 16) return false;
88
+ if (CHROME_LABEL.test(t)) return false;
89
+ if (/^(https?:|javascript:|mailto:)/i.test(t)) return false;
90
+ return true;
91
+ }
92
+ function looksLikeResultList(text) {
93
+ const lines = text.split("\n").map((l) => l.replace(/^\d+\.\s*/, "").trim()).filter(Boolean);
94
+ return lines.length >= 2 && lines.filter(isResultTitle).length >= 2;
95
+ }
96
+ function titlesFromElements(elements, max = 5) {
97
+ const seen = /* @__PURE__ */ new Set();
98
+ const labels = [];
99
+ for (const e of elements) {
100
+ if (e.tag !== "a" && e.role !== "link" && e.role !== "heading") continue;
101
+ const label = e.label?.replace(/\s+/g, " ").trim() ?? "";
102
+ if (!isResultTitle(label) || seen.has(label.toLowerCase())) continue;
103
+ seen.add(label.toLowerCase());
104
+ labels.push(label);
105
+ }
106
+ labels.sort((a, b) => b.length - a.length);
107
+ return labels.slice(0, max).map((t, i) => `${i + 1}. ${t}`).join("\n");
108
+ }
109
+ var SERP_TITLE_SELECTORS = ["a.result__a", "a.result-link", "#links .result__a", "h2 a", "h3 a", "a > h3"];
110
+ function guessSearchQuery(task) {
111
+ const quoted = task.match(/["']([^"']{1,200})["']/);
112
+ if (quoted?.[1]) return quoted[1].trim();
113
+ return task.replace(/^(please\s+)?(search for|google|bing|find|look up)\s+/i, "").slice(0, 200).trim();
114
+ }
115
+ function buildActionSpace(elements, tabs = [], opts) {
116
+ const max = opts?.max ?? DEFAULT_MAX_ACTION_CHOICES;
117
+ const criteria = {
118
+ back: "Go to the previous page",
119
+ screenshot: "Take a fresh screenshot next step",
120
+ fail: "Give up \u2014 the task cannot be completed",
121
+ new_tab: "Open a blank new tab"
122
+ };
123
+ if (opts?.allowWait !== false) {
124
+ criteria.wait = "Wait for the page to settle. Use at most once.";
125
+ }
126
+ if (opts?.allowDone !== false) {
127
+ criteria.done = "The task is finished \u2014 return the result titles you already found";
128
+ }
129
+ if (opts?.allowSearch !== false) {
130
+ criteria.search = "Open a NEW web search. Do not pick this if results are already on screen.";
131
+ }
132
+ if ((opts?.pagesBelow ?? 1) > 0) {
133
+ criteria.scroll_down = "Scroll down one viewport";
134
+ }
135
+ if ((opts?.pagesAbove ?? 1) > 0) {
136
+ criteria.scroll_up = "Scroll up one viewport";
137
+ }
138
+ const slice = elements.slice(0, max);
139
+ for (const e of slice) {
140
+ const label = e.label?.trim() || e.role || e.tag;
141
+ criteria[`click_${e.index}`] = `Click [${e.index}] ${e.role}: ${label}`;
142
+ if (e.isInput) {
143
+ criteria[`type_${e.index}`] = `Type into [${e.index}] ${label}`;
144
+ }
145
+ }
146
+ for (const tab of tabs) {
147
+ if (!tab.active) {
148
+ criteria[`switch_tab_${tab.id}`] = `Switch to tab ${tab.id} (${tab.url || "blank"})`;
149
+ }
150
+ if (tabs.length > 1) {
151
+ criteria[`close_tab_${tab.id}`] = `Close tab ${tab.id}`;
152
+ }
153
+ }
154
+ return {
155
+ criteria,
156
+ toAction(label) {
157
+ return labelToAction(label);
158
+ }
159
+ };
160
+ }
161
+ function labelToAction(label, extras) {
162
+ if (label === "back") return { action: "back" };
163
+ if (label === "scroll_down") return { action: "scroll", direction: "down" };
164
+ if (label === "scroll_up") return { action: "scroll", direction: "up" };
165
+ if (label === "wait") return { action: "wait", ms: 1500 };
166
+ if (label === "screenshot") return { action: "screenshot" };
167
+ if (label === "done") return { action: "done", result: extras?.doneResult ?? "Done" };
168
+ if (label === "fail") return { action: "fail", reason: "Jev chose fail" };
169
+ if (label === "search") return { action: "search", query: extras?.searchQuery ?? "" };
170
+ if (label === "new_tab") return { action: "new_tab" };
171
+ if (label === "none") return void 0;
172
+ const click = /^click_(\d+)$/.exec(label);
173
+ if (click) return { action: "click", index: Number(click[1]) };
174
+ const type = /^type_(\d+)$/.exec(label);
175
+ if (type) return { action: "type", index: Number(type[1]), text: extras?.typeText ?? "" };
176
+ const sw = /^switch_tab_(.+)$/.exec(label);
177
+ if (sw) return { action: "switch_tab", tabId: sw[1] };
178
+ const cl = /^close_tab_(.+)$/.exec(label);
179
+ if (cl) return { action: "close_tab", tabId: cl[1] };
180
+ return void 0;
181
+ }
182
+
39
183
  // src/browser-agent.ts
40
184
  var import_core = require("@agentium/core");
41
185
  var import_zod = require("zod");
@@ -944,6 +1088,73 @@ var BrowserProvider = class {
944
1088
  }
945
1089
  }
946
1090
  }
1091
+ async visibleText(maxChars = 2e3) {
1092
+ this.ensurePage();
1093
+ return this.page.evaluate(
1094
+ (n) => (globalThis.document?.body?.innerText ?? "").slice(0, n),
1095
+ maxChars
1096
+ );
1097
+ }
1098
+ /**
1099
+ * Grep visible page text. No LLM. Used by `search_page`.
1100
+ */
1101
+ async searchPage(opts) {
1102
+ this.ensurePage();
1103
+ const pattern = opts.pattern;
1104
+ const useRegex = !!opts.regex;
1105
+ const caseSensitive = !!opts.caseSensitive;
1106
+ const maxResults = Math.min(Math.max(opts.maxResults ?? 20, 1), 50);
1107
+ const contextChars = opts.contextChars ?? 100;
1108
+ return this.page.evaluate(
1109
+ ({ pattern: pattern2, useRegex: useRegex2, caseSensitive: caseSensitive2, maxResults: maxResults2, contextChars: contextChars2 }) => {
1110
+ const doc = globalThis.document;
1111
+ const text = doc?.body?.innerText ?? "";
1112
+ const src = String(pattern2);
1113
+ const flags = caseSensitive2 ? "g" : "gi";
1114
+ let re;
1115
+ try {
1116
+ re = useRegex2 ? new RegExp(src, flags) : new RegExp(src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
1117
+ } catch {
1118
+ return [];
1119
+ }
1120
+ const matches = [];
1121
+ let m = re.exec(text);
1122
+ while (m && matches.length < Number(maxResults2)) {
1123
+ const start = Math.max(0, m.index - Number(contextChars2));
1124
+ const end = Math.min(text.length, m.index + m[0].length + Number(contextChars2));
1125
+ matches.push({ match: m[0], context: text.slice(start, end), index: m.index });
1126
+ if (!re.global) break;
1127
+ m = re.exec(text);
1128
+ }
1129
+ return matches;
1130
+ },
1131
+ { pattern, useRegex, caseSensitive, maxResults, contextChars }
1132
+ );
1133
+ }
1134
+ /**
1135
+ * querySelectorAll over the page. No LLM. Used by `find_elements`.
1136
+ */
1137
+ async findElements(selector, opts) {
1138
+ this.ensurePage();
1139
+ const maxResults = Math.min(Math.max(opts?.maxResults ?? 50, 1), 80);
1140
+ return this.page.evaluate(
1141
+ ({ selector: selector2, maxResults: maxResults2 }) => {
1142
+ const doc = globalThis.document;
1143
+ let nodes;
1144
+ try {
1145
+ nodes = Array.from(doc.querySelectorAll(selector2)).slice(0, maxResults2);
1146
+ } catch {
1147
+ return [];
1148
+ }
1149
+ return nodes.map((el) => ({
1150
+ tag: String(el.tagName ?? "").toLowerCase(),
1151
+ text: String(el.textContent ?? "").trim().slice(0, 200),
1152
+ href: el.href ? String(el.href) : void 0
1153
+ }));
1154
+ },
1155
+ { selector, maxResults }
1156
+ );
1157
+ }
947
1158
  listTabs() {
948
1159
  const tabs = [];
949
1160
  for (const [id, pg] of this.pages) {
@@ -1161,7 +1372,19 @@ function normalizeAction(action) {
1161
1372
  case "scroll":
1162
1373
  return `scroll:${action.direction}:${action.amount ?? "default"}${action.index != null ? `:${action.index}` : ""}`;
1163
1374
  case "navigate":
1164
- return `navigate:${action.url}`;
1375
+ return `navigate:${action.url}${action.newTab ? ":new" : ""}`;
1376
+ case "search":
1377
+ return `search:${truncate(action.query, 40)}:${action.engine ?? "default"}`;
1378
+ case "new_tab":
1379
+ return `new_tab:${action.url ?? ""}`;
1380
+ case "switch_tab":
1381
+ return `switch_tab:${action.tabId}`;
1382
+ case "close_tab":
1383
+ return `close_tab:${action.tabId}`;
1384
+ case "search_page":
1385
+ return `search_page:${truncate(action.pattern, 40)}`;
1386
+ case "find_elements":
1387
+ return `find_elements:${truncate(action.selector, 40)}`;
1165
1388
  case "back":
1166
1389
  return "back";
1167
1390
  case "wait":
@@ -1268,8 +1491,18 @@ function buildSystemPrompt(viewport, extraInstructions, credentialKeys, options)
1268
1491
  `Set a file on an \`<input type="file">\` by index. \`path\` must be a path the runtime can read; the agent does NOT have a filesystem of its own.`,
1269
1492
  `\`{ "action": "upload_file", "index": <n>, "path": "/abs/path/to/file.pdf" }\``,
1270
1493
  ``,
1271
- `### navigate / back`,
1272
- `\`{ "action": "navigate", "url": "<full URL>" }\` \xB7 \`{ "action": "back" }\``,
1494
+ `### navigate / back / search`,
1495
+ `\`{ "action": "navigate", "url": "<full URL>" }\` \xB7 \`{ "action": "navigate", "url": "<full URL>", "newTab": true }\` \xB7 \`{ "action": "back" }\``,
1496
+ `\`{ "action": "search", "query": "<query>", "engine": "duckduckgo"|"google"|"bing" }\` opens a search-engine results page (default engine: duckduckgo).`,
1497
+ ``,
1498
+ `### tabs`,
1499
+ `Tab ids come from the **Open tabs** list in the observation.`,
1500
+ `\`{ "action": "new_tab", "url": "<optional URL>" }\` \xB7 \`{ "action": "switch_tab", "tabId": "tab-2" }\` \xB7 \`{ "action": "close_tab", "tabId": "tab-1" }\``,
1501
+ ``,
1502
+ `### search_page / find_elements`,
1503
+ `Zero-LLM inspect. Use these instead of \`extract\` when you just need to grep visible text or run a CSS selector.`,
1504
+ `\`{ "action": "search_page", "pattern": "Total", "regex": false, "caseSensitive": false }\``,
1505
+ `\`{ "action": "find_elements", "selector": "a.storylink" }\``,
1273
1506
  ``,
1274
1507
  `### wait / screenshot`,
1275
1508
  `\`{ "action": "wait", "ms": <ms \u2264 10000> }\` \xB7 \`{ "action": "screenshot" }\` (request a fresh image on the next step \u2014 only useful when vision mode is "auto").`,
@@ -1359,7 +1592,7 @@ function responseFormatLines(maxActions, useThinking) {
1359
1592
  `Only the "action" field is executed; the others help you self-correct over multiple steps.`
1360
1593
  ];
1361
1594
  }
1362
- function buildUserMessage(task, pageUrl, pageTitle, stepIndex, actionHistory, domSnapshot, lastExtract, scroll, nudge, stepBudget) {
1595
+ function buildUserMessage(task, pageUrl, pageTitle, stepIndex, actionHistory, domSnapshot, lastExtract, scroll, nudge, stepBudget, tabs) {
1363
1596
  const lines = [];
1364
1597
  lines.push(`**Task:** ${task}`);
1365
1598
  lines.push(`**Current URL:** ${pageUrl}`);
@@ -1377,6 +1610,13 @@ function buildUserMessage(task, pageUrl, pageTitle, stepIndex, actionHistory, do
1377
1610
  if (scroll.pagesAbove === 0 && scroll.pagesBelow === 0) parts.push("fits in viewport");
1378
1611
  lines.push(`**Page stats:** ${parts.join(" \xB7 ")}`);
1379
1612
  }
1613
+ if (tabs && tabs.length > 0) {
1614
+ lines.push(`**Open tabs:**`);
1615
+ for (const tab of tabs) {
1616
+ const mark = tab.active ? " (active)" : "";
1617
+ lines.push(`- ${tab.id}${mark}: ${tab.url || "blank"}`);
1618
+ }
1619
+ }
1380
1620
  if (domSnapshot !== void 0) {
1381
1621
  if (domSnapshot.trim().length === 0) {
1382
1622
  lines.push(``);
@@ -1421,7 +1661,19 @@ function summarizeAction(action) {
1421
1661
  if (typeof action.index === "number") return `Scrolled [${action.index}] into view`;
1422
1662
  return `Scrolled ${action.direction}${action.amount ? ` ${action.amount}px` : ""}`;
1423
1663
  case "navigate":
1424
- return `Navigated to ${action.url}`;
1664
+ return action.newTab ? `Opened ${action.url} in a new tab` : `Navigated to ${action.url}`;
1665
+ case "search":
1666
+ return `Searched ${action.engine ?? "web"} for "${action.query}"`;
1667
+ case "new_tab":
1668
+ return action.url ? `Opened new tab: ${action.url}` : `Opened new tab`;
1669
+ case "switch_tab":
1670
+ return `Switched to ${action.tabId}`;
1671
+ case "close_tab":
1672
+ return `Closed ${action.tabId}`;
1673
+ case "search_page":
1674
+ return `Searched page for "${action.pattern}"`;
1675
+ case "find_elements":
1676
+ return `Found elements: ${action.selector}`;
1425
1677
  case "back":
1426
1678
  return `Went back to previous page`;
1427
1679
  case "wait":
@@ -1487,6 +1739,11 @@ var BrowserAgent = class {
1487
1739
  stealth;
1488
1740
  humanize;
1489
1741
  tools;
1742
+ planner;
1743
+ jevModel;
1744
+ maxActionChoices;
1745
+ searchEngine;
1746
+ jevProvider = null;
1490
1747
  costTracker;
1491
1748
  memoryManager = null;
1492
1749
  logger;
@@ -1526,6 +1783,10 @@ var BrowserAgent = class {
1526
1783
  this.stealth = config.stealth;
1527
1784
  this.humanize = config.humanize;
1528
1785
  this.tools = config.tools ?? [];
1786
+ this.planner = config.planner ?? "vision";
1787
+ this.jevModel = config.jevModel ?? "jev-latest";
1788
+ this.maxActionChoices = config.maxActionChoices ?? DEFAULT_MAX_ACTION_CHOICES;
1789
+ this.searchEngine = config.searchEngine ?? "duckduckgo";
1529
1790
  this.costTracker = config.costTracker ?? null;
1530
1791
  this.eventBus = config.eventBus ?? new import_core.EventBus();
1531
1792
  this.logger = new import_core.Logger({
@@ -1611,11 +1872,14 @@ ${memoryContext}` : memoryContext;
1611
1872
  const pageInfo = await browser.getPageInfo();
1612
1873
  let domSnapshot;
1613
1874
  let scrollCtx;
1875
+ let elements = [];
1614
1876
  if (this.useDOM) {
1615
1877
  const dom = await browser.extractDOM();
1616
1878
  domSnapshot = dom.text;
1617
1879
  scrollCtx = dom.scroll;
1880
+ elements = dom.elements;
1618
1881
  }
1882
+ const tabs = browser.listTabs();
1619
1883
  const pageAdvice = loop.recordPage({
1620
1884
  url: pageInfo.url,
1621
1885
  interactiveCount: scrollCtx?.totalInteractive ?? 0,
@@ -1649,34 +1913,65 @@ ${memoryContext}` : memoryContext;
1649
1913
  lastExtractResult,
1650
1914
  scrollCtx,
1651
1915
  nudge,
1652
- { current: step, max: maxSteps }
1916
+ { current: step, max: maxSteps },
1917
+ tabs
1653
1918
  );
1654
- const messages = this.buildMessages(systemPrompt, historyTurns, userText, wantVision ? screenshot : null);
1655
- this.logger.debug("Calling model", { step, url: pageInfo.url, vision: wantVision });
1656
- const { response, modelUsed } = await this.callModelWithFallback(messages, opts?.apiKey);
1657
- if (!response) {
1658
- consecutiveFailures++;
1659
- actionHistory.push(`(model call failed \u2014 retrying, ${consecutiveFailures}/${this.maxFailures})`);
1660
- if (consecutiveFailures > this.maxFailures) {
1661
- return await this.forceDone(browser, steps, startTime, opts, extractedContent, actionHistory, "model");
1662
- }
1663
- continue;
1664
- }
1665
- if (this.costTracker && response.usage) {
1666
- this.costTracker.track({
1667
- runId: sessionId,
1668
- agentName: this.name,
1669
- modelId: modelUsed.modelId,
1670
- usage: response.usage,
1671
- sessionId,
1672
- userId
1919
+ this.logger.debug("Calling planner", { step, url: pageInfo.url, planner: this.planner, vision: wantVision });
1920
+ let envelope = null;
1921
+ let modelUsed = this.model;
1922
+ if (this.planner === "jev") {
1923
+ const planned = await this.planWithJev({
1924
+ task,
1925
+ url: pageInfo.url,
1926
+ title: pageInfo.title,
1927
+ elements,
1928
+ tabs,
1929
+ actionHistory,
1930
+ lastExtract: lastExtractResult,
1931
+ pagesBelow: scrollCtx?.pagesBelow,
1932
+ pagesAbove: scrollCtx?.pagesAbove,
1933
+ apiKey: opts?.apiKey
1673
1934
  });
1935
+ envelope = planned.envelope;
1936
+ modelUsed = planned.modelUsed;
1937
+ if (this.costTracker && planned.usage) {
1938
+ this.costTracker.track({
1939
+ runId: sessionId,
1940
+ agentName: this.name,
1941
+ modelId: modelUsed.modelId,
1942
+ usage: planned.usage,
1943
+ sessionId,
1944
+ userId
1945
+ });
1946
+ }
1947
+ } else {
1948
+ const messages = this.buildMessages(systemPrompt, historyTurns, userText, wantVision ? screenshot : null);
1949
+ const { response, modelUsed: used } = await this.callModelWithFallback(messages, opts?.apiKey);
1950
+ modelUsed = used;
1951
+ if (!response) {
1952
+ consecutiveFailures++;
1953
+ actionHistory.push(`(model call failed \u2014 retrying, ${consecutiveFailures}/${this.maxFailures})`);
1954
+ if (consecutiveFailures > this.maxFailures) {
1955
+ return await this.forceDone(browser, steps, startTime, opts, extractedContent, actionHistory, "model");
1956
+ }
1957
+ continue;
1958
+ }
1959
+ if (this.costTracker && response.usage) {
1960
+ this.costTracker.track({
1961
+ runId: sessionId,
1962
+ agentName: this.name,
1963
+ modelId: modelUsed.modelId,
1964
+ usage: response.usage,
1965
+ sessionId,
1966
+ userId
1967
+ });
1968
+ }
1969
+ const raw = typeof response.message.content === "string" ? response.message.content : "";
1970
+ envelope = this.parseEnvelope(raw);
1674
1971
  }
1675
- const raw = typeof response.message.content === "string" ? response.message.content : "";
1676
- const envelope = this.parseEnvelope(raw);
1677
1972
  if (!envelope) {
1678
1973
  consecutiveFailures++;
1679
- this.logger.warn("Failed to parse model response", { raw: raw.slice(0, 200), consecutiveFailures });
1974
+ this.logger.warn("Failed to parse planner response", { planner: this.planner, consecutiveFailures });
1680
1975
  if (consecutiveFailures > this.maxFailures) {
1681
1976
  return await this.forceDone(browser, steps, startTime, opts, extractedContent, actionHistory, "parse");
1682
1977
  }
@@ -1731,7 +2026,9 @@ ${memoryContext}` : memoryContext;
1731
2026
  didNavigate = true;
1732
2027
  await this.navigationHealthCheck(browser, pageInfo.url);
1733
2028
  }
1734
- if (action.action === "extract" && exec?.output) lastExtractResult = exec.output;
2029
+ if (exec?.output && (action.action === "extract" || action.action === "search" || action.action === "find_elements" || action.action === "search_page")) {
2030
+ lastExtractResult = exec.output;
2031
+ }
1735
2032
  if (action.action === "screenshot") lastActionWasScreenshot = true;
1736
2033
  } catch (e) {
1737
2034
  stepOk = false;
@@ -1811,6 +2108,7 @@ ${memoryContext}` : memoryContext;
1811
2108
  }
1812
2109
  // ── Private helpers ──────────────────────────────────────────────────
1813
2110
  shouldCaptureVision(step, lastActionWasScreenshot) {
2111
+ if (this.planner === "jev" && this.useVision !== true) return false;
1814
2112
  if (this.useVision === false) return false;
1815
2113
  if (this.useVision === true) return true;
1816
2114
  if (step === 0) return true;
@@ -1910,6 +2208,119 @@ ${memoryContext}` : memoryContext;
1910
2208
  if (msg.includes("401") || msg.includes("402") || msg.includes("auth")) return true;
1911
2209
  return false;
1912
2210
  }
2211
+ getJev() {
2212
+ if (!this.jevProvider) this.jevProvider = (0, import_core.jev)(this.jevModel);
2213
+ return this.jevProvider;
2214
+ }
2215
+ /**
2216
+ * Ask Jev to pick one label from this frame's action space, then map it
2217
+ * to a BrowserAction. Type/search strings come from a text model or the task.
2218
+ */
2219
+ async planWithJev(args) {
2220
+ const alreadySearched = args.actionHistory.some((h) => /^Searched /.test(h));
2221
+ const alreadyWaited = args.actionHistory.some((h) => /^Waited /.test(h));
2222
+ const foundTitles = args.lastExtract ?? titlesFromElements(args.elements);
2223
+ const space = buildActionSpace(args.elements, args.tabs, {
2224
+ max: this.maxActionChoices,
2225
+ pagesBelow: args.pagesBelow,
2226
+ pagesAbove: args.pagesAbove,
2227
+ allowSearch: !alreadySearched && !isSearchResultsUrl(args.url) && !isBlockedPageUrl(args.url),
2228
+ allowDone: looksLikeResultList(foundTitles),
2229
+ allowWait: !alreadyWaited
2230
+ });
2231
+ const provider = this.getJev();
2232
+ try {
2233
+ const response = await provider.generate(
2234
+ [
2235
+ {
2236
+ role: "user",
2237
+ content: JSON.stringify({
2238
+ task: args.task,
2239
+ url: args.url,
2240
+ title: args.title,
2241
+ lastActions: args.actionHistory.slice(-8),
2242
+ lastExtract: args.lastExtract,
2243
+ visibleTitles: titlesFromElements(args.elements, 8),
2244
+ tabs: args.tabs,
2245
+ hint: looksLikeResultList(foundTitles) ? "Result titles are in lastExtract. Pick done." : alreadySearched ? "A search already ran. Wait or click a result title. Do not search again." : void 0
2246
+ })
2247
+ }
2248
+ ],
2249
+ {
2250
+ questions: {
2251
+ action: (0, import_core.choice)(
2252
+ "Which browser action next? Do not repeat lastActions. If search results are already visible, click a result or done.",
2253
+ space.criteria
2254
+ )
2255
+ },
2256
+ apiKey: args.apiKey
2257
+ }
2258
+ );
2259
+ const raw = typeof response.message.content === "string" ? response.message.content : "";
2260
+ let pick;
2261
+ try {
2262
+ const answers = JSON.parse(raw);
2263
+ pick = typeof answers.action === "string" ? answers.action : answers.action?.choice;
2264
+ } catch {
2265
+ pick = void 0;
2266
+ }
2267
+ if (!pick) return { envelope: null, modelUsed: provider, usage: response.usage };
2268
+ const extras = {
2269
+ searchQuery: pick === "search" ? guessSearchQuery(args.task) : void 0,
2270
+ typeText: pick.startsWith("type_") ? await this.inferTypeText(args.task, pick) : void 0,
2271
+ doneResult: pick === "done" ? looksLikeResultList(args.lastExtract ?? "") ? args.lastExtract : titlesFromElements(args.elements) || args.lastExtract || "Done" : void 0
2272
+ };
2273
+ const action = labelToAction(pick, extras);
2274
+ if (!action) return { envelope: null, modelUsed: provider, usage: response.usage };
2275
+ return { envelope: { action, nextGoal: pick }, modelUsed: provider, usage: response.usage };
2276
+ } catch (e) {
2277
+ this.logger.warn("Jev planner failed", { error: e?.message });
2278
+ return { envelope: null, modelUsed: provider };
2279
+ }
2280
+ }
2281
+ async pageLooksBlocked(browser) {
2282
+ const info = await browser.getPageInfo();
2283
+ if (isBlockedPageUrl(info.url)) return true;
2284
+ try {
2285
+ return isBotChallengeText(await browser.visibleText());
2286
+ } catch {
2287
+ return false;
2288
+ }
2289
+ }
2290
+ async scrapeSerpTitles(browser) {
2291
+ for (const selector of SERP_TITLE_SELECTORS) {
2292
+ const found = await browser.findElements(selector, { maxResults: 8 });
2293
+ const labels = [];
2294
+ const seen = /* @__PURE__ */ new Set();
2295
+ for (const f of found) {
2296
+ const text = f.text.replace(/\s+/g, " ").trim();
2297
+ if (!isResultTitle(text) || seen.has(text.toLowerCase())) continue;
2298
+ seen.add(text.toLowerCase());
2299
+ labels.push(text);
2300
+ }
2301
+ if (labels.length >= 2) {
2302
+ return labels.slice(0, 5).map((t, i) => `${i + 1}. ${t}`).join("\n");
2303
+ }
2304
+ }
2305
+ return "";
2306
+ }
2307
+ async inferTypeText(task, pick) {
2308
+ const model = this.pageExtractionLLM ?? (this.model.providerId === "jev" ? null : this.model);
2309
+ if (!model) return guessSearchQuery(task);
2310
+ try {
2311
+ const response = await model.generate([
2312
+ {
2313
+ role: "user",
2314
+ content: `Task: ${task}
2315
+ The next browser action is ${pick}. Reply with ONLY the text to type into that field. No quotes.`
2316
+ }
2317
+ ]);
2318
+ const text = typeof response.message.content === "string" ? response.message.content.trim() : "";
2319
+ return text.replace(/^["']|["']$/g, "").slice(0, 500);
2320
+ } catch {
2321
+ return guessSearchQuery(task);
2322
+ }
2323
+ }
1913
2324
  /**
1914
2325
  * Parse the model's raw response into an `AgentOutput`. Tolerant to
1915
2326
  * three shapes:
@@ -2102,9 +2513,69 @@ Last actions: ${tail || "(none)"}${extracts}`;
2102
2513
  }
2103
2514
  case "navigate": {
2104
2515
  this.assertDomainAllowed(action.url);
2516
+ if (action.newTab) {
2517
+ const tabId = await browser.newTab(action.url);
2518
+ await browser.switchTab(tabId);
2519
+ return { didNavigate: true, output: `Opened ${action.url} in ${tabId}` };
2520
+ }
2105
2521
  await browser.navigate(action.url);
2106
2522
  return { didNavigate: true };
2107
2523
  }
2524
+ case "search": {
2525
+ const engine = action.engine ?? this.searchEngine;
2526
+ const url = searchUrl(action.query, engine);
2527
+ this.assertDomainAllowed(url);
2528
+ await browser.navigate(url);
2529
+ let used = engine;
2530
+ if (await this.pageLooksBlocked(browser)) {
2531
+ if (engine === "duckduckgo") {
2532
+ const fallback = searchUrl(action.query, "bing");
2533
+ this.assertDomainAllowed(fallback);
2534
+ await browser.navigate(fallback);
2535
+ used = "bing";
2536
+ } else {
2537
+ return {
2538
+ didNavigate: true,
2539
+ output: `Search blocked on ${engine}. Pick fail or navigate elsewhere.`
2540
+ };
2541
+ }
2542
+ }
2543
+ await browser.waitForStable(400);
2544
+ const titles = await this.scrapeSerpTitles(browser);
2545
+ return {
2546
+ didNavigate: true,
2547
+ output: titles || `Searched ${used} for "${action.query}"`
2548
+ };
2549
+ }
2550
+ case "new_tab": {
2551
+ if (action.url) this.assertDomainAllowed(action.url);
2552
+ const tabId = await browser.newTab(action.url);
2553
+ await browser.switchTab(tabId);
2554
+ return { didNavigate: !!action.url, output: `Opened ${tabId}` };
2555
+ }
2556
+ case "switch_tab":
2557
+ await browser.switchTab(action.tabId);
2558
+ return { didNavigate: true, output: `Switched to ${action.tabId}` };
2559
+ case "close_tab":
2560
+ await browser.closeTab(action.tabId);
2561
+ return { output: `Closed ${action.tabId}` };
2562
+ case "search_page": {
2563
+ const hits = await browser.searchPage({
2564
+ pattern: action.pattern,
2565
+ regex: action.regex,
2566
+ caseSensitive: action.caseSensitive,
2567
+ maxResults: action.maxResults
2568
+ });
2569
+ const lines = hits.map((h, i) => `${i + 1}. "${h.match}" \u2014 \u2026${h.context}\u2026`);
2570
+ return { output: hits.length ? `search_page:
2571
+ ${lines.join("\n")}` : "search_page: no matches" };
2572
+ }
2573
+ case "find_elements": {
2574
+ const found = await browser.findElements(action.selector, { maxResults: action.maxResults });
2575
+ const lines = found.map((el, i) => `${i + 1}. <${el.tag}> ${el.text}${el.href ? ` ${el.href}` : ""}`);
2576
+ return { output: found.length ? `find_elements:
2577
+ ${lines.join("\n")}` : "find_elements: none" };
2578
+ }
2108
2579
  case "back":
2109
2580
  await browser.back();
2110
2581
  return { didNavigate: true };
@@ -2307,5 +2778,15 @@ var CredentialVault = class {
2307
2778
  0 && (module.exports = {
2308
2779
  BrowserAgent,
2309
2780
  BrowserProvider,
2310
- CredentialVault
2781
+ CredentialVault,
2782
+ buildActionSpace,
2783
+ guessSearchQuery,
2784
+ isBlockedPageUrl,
2785
+ isBotChallengeText,
2786
+ isResultTitle,
2787
+ isSearchResultsUrl,
2788
+ labelToAction,
2789
+ looksLikeResultList,
2790
+ searchUrl,
2791
+ titlesFromElements
2311
2792
  });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ export type { ActionSpace, TabInfo } from "./action-space.js";
2
+ export { buildActionSpace, guessSearchQuery, isBlockedPageUrl, isBotChallengeText, isResultTitle, isSearchResultsUrl, labelToAction, looksLikeResultList, searchUrl, titlesFromElements, } from "./action-space.js";
1
3
  export { BrowserAgent } from "./browser-agent.js";
2
4
  export { BrowserProvider } from "./browser-provider.js";
3
5
  export { CredentialVault } from "./credential-vault.js";
4
- export type { AgentOutput, BrowserAction, BrowserAgentConfig, BrowserRunOpts, BrowserRunOutput, BrowserStep, DomElement, DomScrollContext, DomSnapshot, HumanizeConfig, PageInfo, StealthConfig, } from "./types.js";
6
+ export type { AgentOutput, BrowserAction, BrowserAgentConfig, BrowserPlanner, BrowserRunOpts, BrowserRunOutput, BrowserStep, DomElement, DomScrollContext, DomSnapshot, HumanizeConfig, PageInfo, SearchEngine, StealthConfig, } from "./types.js";
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,aAAa,GACd,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,SAAS,EACT,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,aAAa,GACd,MAAM,YAAY,CAAC"}