@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.js CHANGED
@@ -1,5 +1,139 @@
1
+ // src/action-space.ts
2
+ var DEFAULT_MAX_ACTION_CHOICES = 40;
3
+ function searchUrl(query, engine = "duckduckgo") {
4
+ const q = encodeURIComponent(query.trim());
5
+ switch (engine) {
6
+ case "google":
7
+ return `https://www.google.com/search?q=${q}`;
8
+ case "bing":
9
+ return `https://www.bing.com/search?q=${q}`;
10
+ default:
11
+ return `https://html.duckduckgo.com/html/?q=${q}`;
12
+ }
13
+ }
14
+ function isSearchResultsUrl(url) {
15
+ try {
16
+ const u = new URL(url);
17
+ const host = u.hostname.replace(/^www\./, "");
18
+ if (host === "duckduckgo.com" || host.endsWith(".duckduckgo.com")) {
19
+ return u.searchParams.has("q") || u.pathname.includes("/html");
20
+ }
21
+ if (host === "google.com" || host.endsWith(".google.com")) return u.pathname.startsWith("/search");
22
+ if (host === "bing.com" || host.endsWith(".bing.com")) return u.pathname.startsWith("/search");
23
+ return false;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ function isBlockedPageUrl(url) {
29
+ return /418\.html|\/sorry\/|captcha|challenge|cf-browser-verification/i.test(url);
30
+ }
31
+ function isBotChallengeText(text) {
32
+ 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(
33
+ text
34
+ );
35
+ }
36
+ 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;
37
+ function isResultTitle(label) {
38
+ const t = label.replace(/\s+/g, " ").trim();
39
+ if (t.length < 16) return false;
40
+ if (CHROME_LABEL.test(t)) return false;
41
+ if (/^(https?:|javascript:|mailto:)/i.test(t)) return false;
42
+ return true;
43
+ }
44
+ function looksLikeResultList(text) {
45
+ const lines = text.split("\n").map((l) => l.replace(/^\d+\.\s*/, "").trim()).filter(Boolean);
46
+ return lines.length >= 2 && lines.filter(isResultTitle).length >= 2;
47
+ }
48
+ function titlesFromElements(elements, max = 5) {
49
+ const seen = /* @__PURE__ */ new Set();
50
+ const labels = [];
51
+ for (const e of elements) {
52
+ if (e.tag !== "a" && e.role !== "link" && e.role !== "heading") continue;
53
+ const label = e.label?.replace(/\s+/g, " ").trim() ?? "";
54
+ if (!isResultTitle(label) || seen.has(label.toLowerCase())) continue;
55
+ seen.add(label.toLowerCase());
56
+ labels.push(label);
57
+ }
58
+ labels.sort((a, b) => b.length - a.length);
59
+ return labels.slice(0, max).map((t, i) => `${i + 1}. ${t}`).join("\n");
60
+ }
61
+ var SERP_TITLE_SELECTORS = ["a.result__a", "a.result-link", "#links .result__a", "h2 a", "h3 a", "a > h3"];
62
+ function guessSearchQuery(task) {
63
+ const quoted = task.match(/["']([^"']{1,200})["']/);
64
+ if (quoted?.[1]) return quoted[1].trim();
65
+ return task.replace(/^(please\s+)?(search for|google|bing|find|look up)\s+/i, "").slice(0, 200).trim();
66
+ }
67
+ function buildActionSpace(elements, tabs = [], opts) {
68
+ const max = opts?.max ?? DEFAULT_MAX_ACTION_CHOICES;
69
+ const criteria = {
70
+ back: "Go to the previous page",
71
+ screenshot: "Take a fresh screenshot next step",
72
+ fail: "Give up \u2014 the task cannot be completed",
73
+ new_tab: "Open a blank new tab"
74
+ };
75
+ if (opts?.allowWait !== false) {
76
+ criteria.wait = "Wait for the page to settle. Use at most once.";
77
+ }
78
+ if (opts?.allowDone !== false) {
79
+ criteria.done = "The task is finished \u2014 return the result titles you already found";
80
+ }
81
+ if (opts?.allowSearch !== false) {
82
+ criteria.search = "Open a NEW web search. Do not pick this if results are already on screen.";
83
+ }
84
+ if ((opts?.pagesBelow ?? 1) > 0) {
85
+ criteria.scroll_down = "Scroll down one viewport";
86
+ }
87
+ if ((opts?.pagesAbove ?? 1) > 0) {
88
+ criteria.scroll_up = "Scroll up one viewport";
89
+ }
90
+ const slice = elements.slice(0, max);
91
+ for (const e of slice) {
92
+ const label = e.label?.trim() || e.role || e.tag;
93
+ criteria[`click_${e.index}`] = `Click [${e.index}] ${e.role}: ${label}`;
94
+ if (e.isInput) {
95
+ criteria[`type_${e.index}`] = `Type into [${e.index}] ${label}`;
96
+ }
97
+ }
98
+ for (const tab of tabs) {
99
+ if (!tab.active) {
100
+ criteria[`switch_tab_${tab.id}`] = `Switch to tab ${tab.id} (${tab.url || "blank"})`;
101
+ }
102
+ if (tabs.length > 1) {
103
+ criteria[`close_tab_${tab.id}`] = `Close tab ${tab.id}`;
104
+ }
105
+ }
106
+ return {
107
+ criteria,
108
+ toAction(label) {
109
+ return labelToAction(label);
110
+ }
111
+ };
112
+ }
113
+ function labelToAction(label, extras) {
114
+ if (label === "back") return { action: "back" };
115
+ if (label === "scroll_down") return { action: "scroll", direction: "down" };
116
+ if (label === "scroll_up") return { action: "scroll", direction: "up" };
117
+ if (label === "wait") return { action: "wait", ms: 1500 };
118
+ if (label === "screenshot") return { action: "screenshot" };
119
+ if (label === "done") return { action: "done", result: extras?.doneResult ?? "Done" };
120
+ if (label === "fail") return { action: "fail", reason: "Jev chose fail" };
121
+ if (label === "search") return { action: "search", query: extras?.searchQuery ?? "" };
122
+ if (label === "new_tab") return { action: "new_tab" };
123
+ if (label === "none") return void 0;
124
+ const click = /^click_(\d+)$/.exec(label);
125
+ if (click) return { action: "click", index: Number(click[1]) };
126
+ const type = /^type_(\d+)$/.exec(label);
127
+ if (type) return { action: "type", index: Number(type[1]), text: extras?.typeText ?? "" };
128
+ const sw = /^switch_tab_(.+)$/.exec(label);
129
+ if (sw) return { action: "switch_tab", tabId: sw[1] };
130
+ const cl = /^close_tab_(.+)$/.exec(label);
131
+ if (cl) return { action: "close_tab", tabId: cl[1] };
132
+ return void 0;
133
+ }
134
+
1
135
  // src/browser-agent.ts
2
- import { EventBus, Logger, MemoryManager, RunContext } from "@agentium/core";
136
+ import { choice, EventBus, jev, Logger, MemoryManager, RunContext } from "@agentium/core";
3
137
  import { z } from "zod";
4
138
 
5
139
  // src/stealth.ts
@@ -906,6 +1040,73 @@ var BrowserProvider = class {
906
1040
  }
907
1041
  }
908
1042
  }
1043
+ async visibleText(maxChars = 2e3) {
1044
+ this.ensurePage();
1045
+ return this.page.evaluate(
1046
+ (n) => (globalThis.document?.body?.innerText ?? "").slice(0, n),
1047
+ maxChars
1048
+ );
1049
+ }
1050
+ /**
1051
+ * Grep visible page text. No LLM. Used by `search_page`.
1052
+ */
1053
+ async searchPage(opts) {
1054
+ this.ensurePage();
1055
+ const pattern = opts.pattern;
1056
+ const useRegex = !!opts.regex;
1057
+ const caseSensitive = !!opts.caseSensitive;
1058
+ const maxResults = Math.min(Math.max(opts.maxResults ?? 20, 1), 50);
1059
+ const contextChars = opts.contextChars ?? 100;
1060
+ return this.page.evaluate(
1061
+ ({ pattern: pattern2, useRegex: useRegex2, caseSensitive: caseSensitive2, maxResults: maxResults2, contextChars: contextChars2 }) => {
1062
+ const doc = globalThis.document;
1063
+ const text = doc?.body?.innerText ?? "";
1064
+ const src = String(pattern2);
1065
+ const flags = caseSensitive2 ? "g" : "gi";
1066
+ let re;
1067
+ try {
1068
+ re = useRegex2 ? new RegExp(src, flags) : new RegExp(src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
1069
+ } catch {
1070
+ return [];
1071
+ }
1072
+ const matches = [];
1073
+ let m = re.exec(text);
1074
+ while (m && matches.length < Number(maxResults2)) {
1075
+ const start = Math.max(0, m.index - Number(contextChars2));
1076
+ const end = Math.min(text.length, m.index + m[0].length + Number(contextChars2));
1077
+ matches.push({ match: m[0], context: text.slice(start, end), index: m.index });
1078
+ if (!re.global) break;
1079
+ m = re.exec(text);
1080
+ }
1081
+ return matches;
1082
+ },
1083
+ { pattern, useRegex, caseSensitive, maxResults, contextChars }
1084
+ );
1085
+ }
1086
+ /**
1087
+ * querySelectorAll over the page. No LLM. Used by `find_elements`.
1088
+ */
1089
+ async findElements(selector, opts) {
1090
+ this.ensurePage();
1091
+ const maxResults = Math.min(Math.max(opts?.maxResults ?? 50, 1), 80);
1092
+ return this.page.evaluate(
1093
+ ({ selector: selector2, maxResults: maxResults2 }) => {
1094
+ const doc = globalThis.document;
1095
+ let nodes;
1096
+ try {
1097
+ nodes = Array.from(doc.querySelectorAll(selector2)).slice(0, maxResults2);
1098
+ } catch {
1099
+ return [];
1100
+ }
1101
+ return nodes.map((el) => ({
1102
+ tag: String(el.tagName ?? "").toLowerCase(),
1103
+ text: String(el.textContent ?? "").trim().slice(0, 200),
1104
+ href: el.href ? String(el.href) : void 0
1105
+ }));
1106
+ },
1107
+ { selector, maxResults }
1108
+ );
1109
+ }
909
1110
  listTabs() {
910
1111
  const tabs = [];
911
1112
  for (const [id, pg] of this.pages) {
@@ -1123,7 +1324,19 @@ function normalizeAction(action) {
1123
1324
  case "scroll":
1124
1325
  return `scroll:${action.direction}:${action.amount ?? "default"}${action.index != null ? `:${action.index}` : ""}`;
1125
1326
  case "navigate":
1126
- return `navigate:${action.url}`;
1327
+ return `navigate:${action.url}${action.newTab ? ":new" : ""}`;
1328
+ case "search":
1329
+ return `search:${truncate(action.query, 40)}:${action.engine ?? "default"}`;
1330
+ case "new_tab":
1331
+ return `new_tab:${action.url ?? ""}`;
1332
+ case "switch_tab":
1333
+ return `switch_tab:${action.tabId}`;
1334
+ case "close_tab":
1335
+ return `close_tab:${action.tabId}`;
1336
+ case "search_page":
1337
+ return `search_page:${truncate(action.pattern, 40)}`;
1338
+ case "find_elements":
1339
+ return `find_elements:${truncate(action.selector, 40)}`;
1127
1340
  case "back":
1128
1341
  return "back";
1129
1342
  case "wait":
@@ -1230,8 +1443,18 @@ function buildSystemPrompt(viewport, extraInstructions, credentialKeys, options)
1230
1443
  `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.`,
1231
1444
  `\`{ "action": "upload_file", "index": <n>, "path": "/abs/path/to/file.pdf" }\``,
1232
1445
  ``,
1233
- `### navigate / back`,
1234
- `\`{ "action": "navigate", "url": "<full URL>" }\` \xB7 \`{ "action": "back" }\``,
1446
+ `### navigate / back / search`,
1447
+ `\`{ "action": "navigate", "url": "<full URL>" }\` \xB7 \`{ "action": "navigate", "url": "<full URL>", "newTab": true }\` \xB7 \`{ "action": "back" }\``,
1448
+ `\`{ "action": "search", "query": "<query>", "engine": "duckduckgo"|"google"|"bing" }\` opens a search-engine results page (default engine: duckduckgo).`,
1449
+ ``,
1450
+ `### tabs`,
1451
+ `Tab ids come from the **Open tabs** list in the observation.`,
1452
+ `\`{ "action": "new_tab", "url": "<optional URL>" }\` \xB7 \`{ "action": "switch_tab", "tabId": "tab-2" }\` \xB7 \`{ "action": "close_tab", "tabId": "tab-1" }\``,
1453
+ ``,
1454
+ `### search_page / find_elements`,
1455
+ `Zero-LLM inspect. Use these instead of \`extract\` when you just need to grep visible text or run a CSS selector.`,
1456
+ `\`{ "action": "search_page", "pattern": "Total", "regex": false, "caseSensitive": false }\``,
1457
+ `\`{ "action": "find_elements", "selector": "a.storylink" }\``,
1235
1458
  ``,
1236
1459
  `### wait / screenshot`,
1237
1460
  `\`{ "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").`,
@@ -1321,7 +1544,7 @@ function responseFormatLines(maxActions, useThinking) {
1321
1544
  `Only the "action" field is executed; the others help you self-correct over multiple steps.`
1322
1545
  ];
1323
1546
  }
1324
- function buildUserMessage(task, pageUrl, pageTitle, stepIndex, actionHistory, domSnapshot, lastExtract, scroll, nudge, stepBudget) {
1547
+ function buildUserMessage(task, pageUrl, pageTitle, stepIndex, actionHistory, domSnapshot, lastExtract, scroll, nudge, stepBudget, tabs) {
1325
1548
  const lines = [];
1326
1549
  lines.push(`**Task:** ${task}`);
1327
1550
  lines.push(`**Current URL:** ${pageUrl}`);
@@ -1339,6 +1562,13 @@ function buildUserMessage(task, pageUrl, pageTitle, stepIndex, actionHistory, do
1339
1562
  if (scroll.pagesAbove === 0 && scroll.pagesBelow === 0) parts.push("fits in viewport");
1340
1563
  lines.push(`**Page stats:** ${parts.join(" \xB7 ")}`);
1341
1564
  }
1565
+ if (tabs && tabs.length > 0) {
1566
+ lines.push(`**Open tabs:**`);
1567
+ for (const tab of tabs) {
1568
+ const mark = tab.active ? " (active)" : "";
1569
+ lines.push(`- ${tab.id}${mark}: ${tab.url || "blank"}`);
1570
+ }
1571
+ }
1342
1572
  if (domSnapshot !== void 0) {
1343
1573
  if (domSnapshot.trim().length === 0) {
1344
1574
  lines.push(``);
@@ -1383,7 +1613,19 @@ function summarizeAction(action) {
1383
1613
  if (typeof action.index === "number") return `Scrolled [${action.index}] into view`;
1384
1614
  return `Scrolled ${action.direction}${action.amount ? ` ${action.amount}px` : ""}`;
1385
1615
  case "navigate":
1386
- return `Navigated to ${action.url}`;
1616
+ return action.newTab ? `Opened ${action.url} in a new tab` : `Navigated to ${action.url}`;
1617
+ case "search":
1618
+ return `Searched ${action.engine ?? "web"} for "${action.query}"`;
1619
+ case "new_tab":
1620
+ return action.url ? `Opened new tab: ${action.url}` : `Opened new tab`;
1621
+ case "switch_tab":
1622
+ return `Switched to ${action.tabId}`;
1623
+ case "close_tab":
1624
+ return `Closed ${action.tabId}`;
1625
+ case "search_page":
1626
+ return `Searched page for "${action.pattern}"`;
1627
+ case "find_elements":
1628
+ return `Found elements: ${action.selector}`;
1387
1629
  case "back":
1388
1630
  return `Went back to previous page`;
1389
1631
  case "wait":
@@ -1449,6 +1691,11 @@ var BrowserAgent = class {
1449
1691
  stealth;
1450
1692
  humanize;
1451
1693
  tools;
1694
+ planner;
1695
+ jevModel;
1696
+ maxActionChoices;
1697
+ searchEngine;
1698
+ jevProvider = null;
1452
1699
  costTracker;
1453
1700
  memoryManager = null;
1454
1701
  logger;
@@ -1488,6 +1735,10 @@ var BrowserAgent = class {
1488
1735
  this.stealth = config.stealth;
1489
1736
  this.humanize = config.humanize;
1490
1737
  this.tools = config.tools ?? [];
1738
+ this.planner = config.planner ?? "vision";
1739
+ this.jevModel = config.jevModel ?? "jev-latest";
1740
+ this.maxActionChoices = config.maxActionChoices ?? DEFAULT_MAX_ACTION_CHOICES;
1741
+ this.searchEngine = config.searchEngine ?? "duckduckgo";
1491
1742
  this.costTracker = config.costTracker ?? null;
1492
1743
  this.eventBus = config.eventBus ?? new EventBus();
1493
1744
  this.logger = new Logger({
@@ -1573,11 +1824,14 @@ ${memoryContext}` : memoryContext;
1573
1824
  const pageInfo = await browser.getPageInfo();
1574
1825
  let domSnapshot;
1575
1826
  let scrollCtx;
1827
+ let elements = [];
1576
1828
  if (this.useDOM) {
1577
1829
  const dom = await browser.extractDOM();
1578
1830
  domSnapshot = dom.text;
1579
1831
  scrollCtx = dom.scroll;
1832
+ elements = dom.elements;
1580
1833
  }
1834
+ const tabs = browser.listTabs();
1581
1835
  const pageAdvice = loop.recordPage({
1582
1836
  url: pageInfo.url,
1583
1837
  interactiveCount: scrollCtx?.totalInteractive ?? 0,
@@ -1611,34 +1865,65 @@ ${memoryContext}` : memoryContext;
1611
1865
  lastExtractResult,
1612
1866
  scrollCtx,
1613
1867
  nudge,
1614
- { current: step, max: maxSteps }
1868
+ { current: step, max: maxSteps },
1869
+ tabs
1615
1870
  );
1616
- const messages = this.buildMessages(systemPrompt, historyTurns, userText, wantVision ? screenshot : null);
1617
- this.logger.debug("Calling model", { step, url: pageInfo.url, vision: wantVision });
1618
- const { response, modelUsed } = await this.callModelWithFallback(messages, opts?.apiKey);
1619
- if (!response) {
1620
- consecutiveFailures++;
1621
- actionHistory.push(`(model call failed \u2014 retrying, ${consecutiveFailures}/${this.maxFailures})`);
1622
- if (consecutiveFailures > this.maxFailures) {
1623
- return await this.forceDone(browser, steps, startTime, opts, extractedContent, actionHistory, "model");
1624
- }
1625
- continue;
1626
- }
1627
- if (this.costTracker && response.usage) {
1628
- this.costTracker.track({
1629
- runId: sessionId,
1630
- agentName: this.name,
1631
- modelId: modelUsed.modelId,
1632
- usage: response.usage,
1633
- sessionId,
1634
- userId
1871
+ this.logger.debug("Calling planner", { step, url: pageInfo.url, planner: this.planner, vision: wantVision });
1872
+ let envelope = null;
1873
+ let modelUsed = this.model;
1874
+ if (this.planner === "jev") {
1875
+ const planned = await this.planWithJev({
1876
+ task,
1877
+ url: pageInfo.url,
1878
+ title: pageInfo.title,
1879
+ elements,
1880
+ tabs,
1881
+ actionHistory,
1882
+ lastExtract: lastExtractResult,
1883
+ pagesBelow: scrollCtx?.pagesBelow,
1884
+ pagesAbove: scrollCtx?.pagesAbove,
1885
+ apiKey: opts?.apiKey
1635
1886
  });
1887
+ envelope = planned.envelope;
1888
+ modelUsed = planned.modelUsed;
1889
+ if (this.costTracker && planned.usage) {
1890
+ this.costTracker.track({
1891
+ runId: sessionId,
1892
+ agentName: this.name,
1893
+ modelId: modelUsed.modelId,
1894
+ usage: planned.usage,
1895
+ sessionId,
1896
+ userId
1897
+ });
1898
+ }
1899
+ } else {
1900
+ const messages = this.buildMessages(systemPrompt, historyTurns, userText, wantVision ? screenshot : null);
1901
+ const { response, modelUsed: used } = await this.callModelWithFallback(messages, opts?.apiKey);
1902
+ modelUsed = used;
1903
+ if (!response) {
1904
+ consecutiveFailures++;
1905
+ actionHistory.push(`(model call failed \u2014 retrying, ${consecutiveFailures}/${this.maxFailures})`);
1906
+ if (consecutiveFailures > this.maxFailures) {
1907
+ return await this.forceDone(browser, steps, startTime, opts, extractedContent, actionHistory, "model");
1908
+ }
1909
+ continue;
1910
+ }
1911
+ if (this.costTracker && response.usage) {
1912
+ this.costTracker.track({
1913
+ runId: sessionId,
1914
+ agentName: this.name,
1915
+ modelId: modelUsed.modelId,
1916
+ usage: response.usage,
1917
+ sessionId,
1918
+ userId
1919
+ });
1920
+ }
1921
+ const raw = typeof response.message.content === "string" ? response.message.content : "";
1922
+ envelope = this.parseEnvelope(raw);
1636
1923
  }
1637
- const raw = typeof response.message.content === "string" ? response.message.content : "";
1638
- const envelope = this.parseEnvelope(raw);
1639
1924
  if (!envelope) {
1640
1925
  consecutiveFailures++;
1641
- this.logger.warn("Failed to parse model response", { raw: raw.slice(0, 200), consecutiveFailures });
1926
+ this.logger.warn("Failed to parse planner response", { planner: this.planner, consecutiveFailures });
1642
1927
  if (consecutiveFailures > this.maxFailures) {
1643
1928
  return await this.forceDone(browser, steps, startTime, opts, extractedContent, actionHistory, "parse");
1644
1929
  }
@@ -1693,7 +1978,9 @@ ${memoryContext}` : memoryContext;
1693
1978
  didNavigate = true;
1694
1979
  await this.navigationHealthCheck(browser, pageInfo.url);
1695
1980
  }
1696
- if (action.action === "extract" && exec?.output) lastExtractResult = exec.output;
1981
+ if (exec?.output && (action.action === "extract" || action.action === "search" || action.action === "find_elements" || action.action === "search_page")) {
1982
+ lastExtractResult = exec.output;
1983
+ }
1697
1984
  if (action.action === "screenshot") lastActionWasScreenshot = true;
1698
1985
  } catch (e) {
1699
1986
  stepOk = false;
@@ -1773,6 +2060,7 @@ ${memoryContext}` : memoryContext;
1773
2060
  }
1774
2061
  // ── Private helpers ──────────────────────────────────────────────────
1775
2062
  shouldCaptureVision(step, lastActionWasScreenshot) {
2063
+ if (this.planner === "jev" && this.useVision !== true) return false;
1776
2064
  if (this.useVision === false) return false;
1777
2065
  if (this.useVision === true) return true;
1778
2066
  if (step === 0) return true;
@@ -1872,6 +2160,119 @@ ${memoryContext}` : memoryContext;
1872
2160
  if (msg.includes("401") || msg.includes("402") || msg.includes("auth")) return true;
1873
2161
  return false;
1874
2162
  }
2163
+ getJev() {
2164
+ if (!this.jevProvider) this.jevProvider = jev(this.jevModel);
2165
+ return this.jevProvider;
2166
+ }
2167
+ /**
2168
+ * Ask Jev to pick one label from this frame's action space, then map it
2169
+ * to a BrowserAction. Type/search strings come from a text model or the task.
2170
+ */
2171
+ async planWithJev(args) {
2172
+ const alreadySearched = args.actionHistory.some((h) => /^Searched /.test(h));
2173
+ const alreadyWaited = args.actionHistory.some((h) => /^Waited /.test(h));
2174
+ const foundTitles = args.lastExtract ?? titlesFromElements(args.elements);
2175
+ const space = buildActionSpace(args.elements, args.tabs, {
2176
+ max: this.maxActionChoices,
2177
+ pagesBelow: args.pagesBelow,
2178
+ pagesAbove: args.pagesAbove,
2179
+ allowSearch: !alreadySearched && !isSearchResultsUrl(args.url) && !isBlockedPageUrl(args.url),
2180
+ allowDone: looksLikeResultList(foundTitles),
2181
+ allowWait: !alreadyWaited
2182
+ });
2183
+ const provider = this.getJev();
2184
+ try {
2185
+ const response = await provider.generate(
2186
+ [
2187
+ {
2188
+ role: "user",
2189
+ content: JSON.stringify({
2190
+ task: args.task,
2191
+ url: args.url,
2192
+ title: args.title,
2193
+ lastActions: args.actionHistory.slice(-8),
2194
+ lastExtract: args.lastExtract,
2195
+ visibleTitles: titlesFromElements(args.elements, 8),
2196
+ tabs: args.tabs,
2197
+ 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
2198
+ })
2199
+ }
2200
+ ],
2201
+ {
2202
+ questions: {
2203
+ action: choice(
2204
+ "Which browser action next? Do not repeat lastActions. If search results are already visible, click a result or done.",
2205
+ space.criteria
2206
+ )
2207
+ },
2208
+ apiKey: args.apiKey
2209
+ }
2210
+ );
2211
+ const raw = typeof response.message.content === "string" ? response.message.content : "";
2212
+ let pick;
2213
+ try {
2214
+ const answers = JSON.parse(raw);
2215
+ pick = typeof answers.action === "string" ? answers.action : answers.action?.choice;
2216
+ } catch {
2217
+ pick = void 0;
2218
+ }
2219
+ if (!pick) return { envelope: null, modelUsed: provider, usage: response.usage };
2220
+ const extras = {
2221
+ searchQuery: pick === "search" ? guessSearchQuery(args.task) : void 0,
2222
+ typeText: pick.startsWith("type_") ? await this.inferTypeText(args.task, pick) : void 0,
2223
+ doneResult: pick === "done" ? looksLikeResultList(args.lastExtract ?? "") ? args.lastExtract : titlesFromElements(args.elements) || args.lastExtract || "Done" : void 0
2224
+ };
2225
+ const action = labelToAction(pick, extras);
2226
+ if (!action) return { envelope: null, modelUsed: provider, usage: response.usage };
2227
+ return { envelope: { action, nextGoal: pick }, modelUsed: provider, usage: response.usage };
2228
+ } catch (e) {
2229
+ this.logger.warn("Jev planner failed", { error: e?.message });
2230
+ return { envelope: null, modelUsed: provider };
2231
+ }
2232
+ }
2233
+ async pageLooksBlocked(browser) {
2234
+ const info = await browser.getPageInfo();
2235
+ if (isBlockedPageUrl(info.url)) return true;
2236
+ try {
2237
+ return isBotChallengeText(await browser.visibleText());
2238
+ } catch {
2239
+ return false;
2240
+ }
2241
+ }
2242
+ async scrapeSerpTitles(browser) {
2243
+ for (const selector of SERP_TITLE_SELECTORS) {
2244
+ const found = await browser.findElements(selector, { maxResults: 8 });
2245
+ const labels = [];
2246
+ const seen = /* @__PURE__ */ new Set();
2247
+ for (const f of found) {
2248
+ const text = f.text.replace(/\s+/g, " ").trim();
2249
+ if (!isResultTitle(text) || seen.has(text.toLowerCase())) continue;
2250
+ seen.add(text.toLowerCase());
2251
+ labels.push(text);
2252
+ }
2253
+ if (labels.length >= 2) {
2254
+ return labels.slice(0, 5).map((t, i) => `${i + 1}. ${t}`).join("\n");
2255
+ }
2256
+ }
2257
+ return "";
2258
+ }
2259
+ async inferTypeText(task, pick) {
2260
+ const model = this.pageExtractionLLM ?? (this.model.providerId === "jev" ? null : this.model);
2261
+ if (!model) return guessSearchQuery(task);
2262
+ try {
2263
+ const response = await model.generate([
2264
+ {
2265
+ role: "user",
2266
+ content: `Task: ${task}
2267
+ The next browser action is ${pick}. Reply with ONLY the text to type into that field. No quotes.`
2268
+ }
2269
+ ]);
2270
+ const text = typeof response.message.content === "string" ? response.message.content.trim() : "";
2271
+ return text.replace(/^["']|["']$/g, "").slice(0, 500);
2272
+ } catch {
2273
+ return guessSearchQuery(task);
2274
+ }
2275
+ }
1875
2276
  /**
1876
2277
  * Parse the model's raw response into an `AgentOutput`. Tolerant to
1877
2278
  * three shapes:
@@ -2064,9 +2465,69 @@ Last actions: ${tail || "(none)"}${extracts}`;
2064
2465
  }
2065
2466
  case "navigate": {
2066
2467
  this.assertDomainAllowed(action.url);
2468
+ if (action.newTab) {
2469
+ const tabId = await browser.newTab(action.url);
2470
+ await browser.switchTab(tabId);
2471
+ return { didNavigate: true, output: `Opened ${action.url} in ${tabId}` };
2472
+ }
2067
2473
  await browser.navigate(action.url);
2068
2474
  return { didNavigate: true };
2069
2475
  }
2476
+ case "search": {
2477
+ const engine = action.engine ?? this.searchEngine;
2478
+ const url = searchUrl(action.query, engine);
2479
+ this.assertDomainAllowed(url);
2480
+ await browser.navigate(url);
2481
+ let used = engine;
2482
+ if (await this.pageLooksBlocked(browser)) {
2483
+ if (engine === "duckduckgo") {
2484
+ const fallback = searchUrl(action.query, "bing");
2485
+ this.assertDomainAllowed(fallback);
2486
+ await browser.navigate(fallback);
2487
+ used = "bing";
2488
+ } else {
2489
+ return {
2490
+ didNavigate: true,
2491
+ output: `Search blocked on ${engine}. Pick fail or navigate elsewhere.`
2492
+ };
2493
+ }
2494
+ }
2495
+ await browser.waitForStable(400);
2496
+ const titles = await this.scrapeSerpTitles(browser);
2497
+ return {
2498
+ didNavigate: true,
2499
+ output: titles || `Searched ${used} for "${action.query}"`
2500
+ };
2501
+ }
2502
+ case "new_tab": {
2503
+ if (action.url) this.assertDomainAllowed(action.url);
2504
+ const tabId = await browser.newTab(action.url);
2505
+ await browser.switchTab(tabId);
2506
+ return { didNavigate: !!action.url, output: `Opened ${tabId}` };
2507
+ }
2508
+ case "switch_tab":
2509
+ await browser.switchTab(action.tabId);
2510
+ return { didNavigate: true, output: `Switched to ${action.tabId}` };
2511
+ case "close_tab":
2512
+ await browser.closeTab(action.tabId);
2513
+ return { output: `Closed ${action.tabId}` };
2514
+ case "search_page": {
2515
+ const hits = await browser.searchPage({
2516
+ pattern: action.pattern,
2517
+ regex: action.regex,
2518
+ caseSensitive: action.caseSensitive,
2519
+ maxResults: action.maxResults
2520
+ });
2521
+ const lines = hits.map((h, i) => `${i + 1}. "${h.match}" \u2014 \u2026${h.context}\u2026`);
2522
+ return { output: hits.length ? `search_page:
2523
+ ${lines.join("\n")}` : "search_page: no matches" };
2524
+ }
2525
+ case "find_elements": {
2526
+ const found = await browser.findElements(action.selector, { maxResults: action.maxResults });
2527
+ const lines = found.map((el, i) => `${i + 1}. <${el.tag}> ${el.text}${el.href ? ` ${el.href}` : ""}`);
2528
+ return { output: found.length ? `find_elements:
2529
+ ${lines.join("\n")}` : "find_elements: none" };
2530
+ }
2070
2531
  case "back":
2071
2532
  await browser.back();
2072
2533
  return { didNavigate: true };
@@ -2268,5 +2729,15 @@ var CredentialVault = class {
2268
2729
  export {
2269
2730
  BrowserAgent,
2270
2731
  BrowserProvider,
2271
- CredentialVault
2732
+ CredentialVault,
2733
+ buildActionSpace,
2734
+ guessSearchQuery,
2735
+ isBlockedPageUrl,
2736
+ isBotChallengeText,
2737
+ isResultTitle,
2738
+ isSearchResultsUrl,
2739
+ labelToAction,
2740
+ looksLikeResultList,
2741
+ searchUrl,
2742
+ titlesFromElements
2272
2743
  };
@@ -1 +1 @@
1
- {"version":3,"file":"loop-detector.d.ts","sourceRoot":"","sources":["../src/loop-detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,EAAE,MAAM,CAAC;IACzB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAoD;IACrF,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAoD;gBAEvE,IAAI,CAAC,EAAE;QACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,gBAAgB,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACxE,cAAc,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACvE;IAeD;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,UAAU;IA8B/C;;;;OAIG;IACH,UAAU,CAAC,EAAE,EAAE,eAAe,GAAG,UAAU;IAmC3C,0DAA0D;IAC1D,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU;CAIzD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CA8C7D;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAOzC"}
1
+ {"version":3,"file":"loop-detector.d.ts","sourceRoot":"","sources":["../src/loop-detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,EAAE,MAAM,CAAC;IACzB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAoD;IACrF,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAoD;gBAEvE,IAAI,CAAC,EAAE;QACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,gBAAgB,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACxE,cAAc,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACvE;IAeD;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,UAAU;IA8B/C;;;;OAIG;IACH,UAAU,CAAC,EAAE,EAAE,eAAe,GAAG,UAAU;IAmC3C,0DAA0D;IAC1D,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU;CAIzD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CA0D7D;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAOzC"}
package/dist/prompts.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ToolDef } from "@agentium/core";
2
+ import type { TabInfo } from "./action-space.js";
2
3
  import type { DomScrollContext } from "./types.js";
3
4
  export declare function buildSystemPrompt(viewport: {
4
5
  width: number;
@@ -15,6 +16,6 @@ export declare function buildSystemPrompt(viewport: {
15
16
  export declare function buildUserMessage(task: string, pageUrl: string, pageTitle: string, stepIndex: number, actionHistory: string[], domSnapshot?: string, lastExtract?: string, scroll?: DomScrollContext, nudge?: string, stepBudget?: {
16
17
  current: number;
17
18
  max: number;
18
- }): string;
19
+ }, tabs?: TabInfo[]): string;
19
20
  export declare function summarizeAction(action: Record<string, unknown>): string;
20
21
  //# sourceMappingURL=prompts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC3C,iBAAiB,CAAC,EAAE,MAAM,EAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,EACzB,OAAO,CAAC,EAAE;IACR,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,MAAM,CAiIR;AA6CD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EAAE,EACvB,WAAW,CAAC,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,gBAAgB,EACzB,KAAK,CAAC,EAAE,MAAM,EACd,UAAU,CAAC,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5C,MAAM,CA0DR;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CA4CvE"}
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC3C,iBAAiB,CAAC,EAAE,MAAM,EAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,EACzB,OAAO,CAAC,EAAE;IACR,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,MAAM,CA2IR;AA6CD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EAAE,EACvB,WAAW,CAAC,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,gBAAgB,EACzB,KAAK,CAAC,EAAE,MAAM,EACd,UAAU,CAAC,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAC7C,IAAI,CAAC,EAAE,OAAO,EAAE,GACf,MAAM,CAkER;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAwDvE"}