@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/README.md +11 -1
- package/dist/action-space.d.ts +42 -0
- package/dist/action-space.d.ts.map +1 -0
- package/dist/browser-agent.d.ts +14 -0
- package/dist/browser-agent.d.ts.map +1 -1
- package/dist/browser-provider.d.ts +25 -0
- package/dist/browser-provider.d.ts.map +1 -1
- package/dist/index.cjs +512 -31
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +502 -31
- package/dist/loop-detector.d.ts.map +1 -1
- package/dist/prompts.d.ts +2 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/types.d.ts +48 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
if (
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
|
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"
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|