@agentium/browser 3.1.0 → 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 +131 -0
- package/dist/browser-agent.d.ts.map +1 -0
- package/dist/browser-provider.d.ts +242 -0
- package/dist/browser-provider.d.ts.map +1 -0
- package/dist/credential-vault.d.ts +37 -0
- package/dist/credential-vault.d.ts.map +1 -0
- package/dist/index.cjs +512 -31
- package/dist/index.d.ts +7 -801
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +502 -31
- package/dist/loop-detector.d.ts +83 -0
- package/dist/loop-detector.d.ts.map +1 -0
- package/dist/prompts.d.ts +21 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/stealth.d.ts +27 -0
- package/dist/stealth.d.ts.map +1 -0
- package/dist/{index.d.cts → types.d.ts} +64 -387
- package/dist/types.d.ts.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
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"}
|
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
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
if (
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
|
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"
|
|
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
|
};
|