@tangle-network/browser-agent-driver 0.8.0 → 0.8.7
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/brain/index.d.ts +24 -7
- package/dist/brain/index.d.ts.map +1 -1
- package/dist/brain/index.js +227 -93
- package/dist/brain/index.js.map +1 -1
- package/dist/captcha.d.ts +55 -0
- package/dist/captcha.d.ts.map +1 -0
- package/dist/captcha.js +403 -0
- package/dist/captcha.js.map +1 -0
- package/dist/cli-design-audit.d.ts.map +1 -1
- package/dist/cli-design-audit.js +81 -59
- package/dist/cli-design-audit.js.map +1 -1
- package/dist/cli-ui.d.ts +49 -0
- package/dist/cli-ui.d.ts.map +1 -0
- package/dist/cli-ui.js +383 -0
- package/dist/cli-ui.js.map +1 -0
- package/dist/cli.js +54 -186
- package/dist/cli.js.map +1 -1
- package/dist/drivers/cdp-snapshot.d.ts.map +1 -1
- package/dist/drivers/cdp-snapshot.js +2 -1
- package/dist/drivers/cdp-snapshot.js.map +1 -1
- package/dist/drivers/playwright.d.ts +6 -0
- package/dist/drivers/playwright.d.ts.map +1 -1
- package/dist/drivers/playwright.js +34 -9
- package/dist/drivers/playwright.js.map +1 -1
- package/dist/drivers/snapshot.d.ts +1 -0
- package/dist/drivers/snapshot.d.ts.map +1 -1
- package/dist/drivers/snapshot.js +12 -5
- package/dist/drivers/snapshot.js.map +1 -1
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +16 -35
- package/dist/recovery.js.map +1 -1
- package/dist/runner/runner.d.ts +1 -0
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/runner.js +133 -98
- package/dist/runner/runner.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
package/dist/brain/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Uses Vercel AI SDK for multi-provider support (OpenAI, Anthropic, Google, Codex CLI, Claude Code).
|
|
6
6
|
*/
|
|
7
|
-
import type { ModelMessage } from 'ai';
|
|
7
|
+
import type { ModelMessage, LanguageModel } from 'ai';
|
|
8
8
|
import type { Action, PageState, AgentConfig, DesignFinding, GoalVerification } from '../types.js';
|
|
9
9
|
export interface BrainDecision {
|
|
10
10
|
action: Action;
|
|
@@ -64,6 +64,11 @@ export declare class Brain {
|
|
|
64
64
|
private resolveModelName;
|
|
65
65
|
private shouldSendTemperature;
|
|
66
66
|
private generationOptions;
|
|
67
|
+
/** Get a LLM model instance, optionally with provider/model override (e.g. for CAPTCHA fallback) */
|
|
68
|
+
getLanguageModel(selection?: {
|
|
69
|
+
provider?: 'openai' | 'anthropic' | 'google';
|
|
70
|
+
model?: string;
|
|
71
|
+
}): Promise<LanguageModel>;
|
|
67
72
|
/** Lazily create the LLM model instance based on provider config */
|
|
68
73
|
private getModel;
|
|
69
74
|
private generate;
|
|
@@ -78,6 +83,12 @@ export declare class Brain {
|
|
|
78
83
|
* not for decide(). The flag is kept for future experiments with better routing signals.
|
|
79
84
|
*/
|
|
80
85
|
private shouldUseNavigationModel;
|
|
86
|
+
/**
|
|
87
|
+
* Build the system prompt dynamically, injecting conditional rule groups
|
|
88
|
+
* based on goal text, page snapshot content, and turn number.
|
|
89
|
+
* Saves ~800 tokens per turn on simple navigation tasks.
|
|
90
|
+
*/
|
|
91
|
+
private buildSystemPrompt;
|
|
81
92
|
/** Reset conversation history (call between scenarios) */
|
|
82
93
|
reset(): void;
|
|
83
94
|
/** Get current conversation history */
|
|
@@ -91,15 +102,21 @@ export declare class Brain {
|
|
|
91
102
|
private buildUserContent;
|
|
92
103
|
/**
|
|
93
104
|
* Compact conversation history: strip ELEMENTS blocks and screenshots
|
|
94
|
-
* from
|
|
105
|
+
* from older observations, keeping the last 2 user messages intact.
|
|
95
106
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* keep full text) is the empirically best balance.
|
|
107
|
+
* For older turns, replaces the full ELEMENTS block with a one-line
|
|
108
|
+
* summary showing element count and the selectors the agent actually
|
|
109
|
+
* used, extracted from the paired assistant response.
|
|
100
110
|
*/
|
|
101
111
|
private compactHistory;
|
|
102
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Extract @ref selectors from an assistant JSON response.
|
|
114
|
+
*/
|
|
115
|
+
private extractSelectorsFromResponse;
|
|
116
|
+
/**
|
|
117
|
+
* Replace the ELEMENTS block with a one-line action-only summary.
|
|
118
|
+
*/
|
|
119
|
+
private summarizeElements;
|
|
103
120
|
decide(goal: string, state: PageState, extraContext?: string, turnInfo?: {
|
|
104
121
|
current: number;
|
|
105
122
|
max: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/brain/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/brain/index.ts"],"names":[],"mappings":"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"}
|
package/dist/brain/index.js
CHANGED
|
@@ -9,7 +9,8 @@ import { AriaSnapshotHelper } from '../drivers/snapshot.js';
|
|
|
9
9
|
import { resolveProviderApiKey, resolveProviderModelName } from '../provider-defaults.js';
|
|
10
10
|
import { buildFirstPartyBoundaryNote } from '../domain-policy.js';
|
|
11
11
|
import { generateWithSandboxBackend } from '../providers/sandbox-backend.js';
|
|
12
|
-
|
|
12
|
+
/** Core system prompt: preamble, actions, format, and rules 1-14 (always sent) */
|
|
13
|
+
const CORE_RULES = `You are a senior staff engineer operating a browser via Playwright automation.
|
|
13
14
|
|
|
14
15
|
You can SEE the page (via screenshot) and READ the page structure (via accessibility tree with @ref IDs).
|
|
15
16
|
Use BOTH inputs together — the screenshot shows layout/design/visual state, the a11y tree shows interactive elements with refs.
|
|
@@ -59,17 +60,25 @@ RULES:
|
|
|
59
60
|
11. After the app builds and a preview is visible, use "verifyPreview" to check for errors before completing
|
|
60
61
|
12. BLOCKER-FIRST POLICY: if a modal, limit, quota, permission, or error dialog blocks progress, resolve THAT first before continuing the main goal
|
|
61
62
|
13. For quota/limit blockers, use an unblock ladder: open manage path -> clean up old test resources if needed -> retry the original action
|
|
62
|
-
14. If the same action triggers the same blocker twice, switch strategy immediately (different button/path), do not repeat blind retries
|
|
63
|
+
14. If the same action triggers the same blocker twice, switch strategy immediately (different button/path), do not repeat blind retries`;
|
|
64
|
+
/** Search-related rules (15-17): injected when page has search elements or /search URL */
|
|
65
|
+
const SEARCH_RULES = `
|
|
63
66
|
15. SEARCH FORMS: Always interact with the form (type in search box, then click Search or press Enter). Do NOT navigate to a URL with search query parameters — many sites require form submission to trigger filtering. If a search yields no results, try the page's own search box rather than the site-wide search
|
|
64
67
|
16. CONTENT DISCOVERY: If the ELEMENTS list doesn't show the link/content you need (e.g., the page has many links but the a11y tree is truncated), use runScript to find it: document.querySelectorAll('a[href]') filtered by keyword. Navigate to the discovered URL directly instead of clicking blindly through menus
|
|
65
|
-
17. EXTERNAL SEARCH REDIRECTS: If a site's search form redirects to an external search engine (e.g., search.usa.gov for .gov sites), the results still link back to the original site. Click a relevant search result link — it will take you to the target domain. Do NOT abandon search results to navigate the target site manually
|
|
68
|
+
17. EXTERNAL SEARCH REDIRECTS: If a site's search form redirects to an external search engine (e.g., search.usa.gov for .gov sites), the results still link back to the original site. Click a relevant search result link — it will take you to the target domain. Do NOT abandon search results to navigate the target site manually`;
|
|
69
|
+
/** Data extraction rules (18, 21-23): injected when goal involves extracting data */
|
|
70
|
+
const DATA_EXTRACTION_RULES = `
|
|
66
71
|
18. DATA EXTRACTION: When the goal asks for specific data (prices, ratings, counts, names) from a list or search results page, use runScript to extract all needed data at once: e.g., document.querySelectorAll('.product-card').forEach(...). Do NOT click into each individual item when the data is visible on the list page. Extract first, then complete with the extracted data
|
|
67
|
-
19. FORM FIELD TARGETING: Before typing, verify you are targeting the correct input field using its @ref from the ELEMENTS list. If multiple inputs are visible (e.g., search box + price filter), ensure you select the right one by checking its label or placeholder text in the a11y tree. Never assume focus — always specify the exact @ref
|
|
68
|
-
20. SECTION NAVIGATION: When you need to find a specific section (e.g., rugby, sports, travel) and the nav links aren't in the truncated a11y tree, use runScript to discover navigation: JSON.stringify(Array.from(document.querySelectorAll('nav a, header a, [role="navigation"] a, .nav a')).slice(0, 30).map(a => ({text: a.textContent.trim(), href: a.href}))). Then navigate directly to the matching section URL
|
|
69
72
|
21. EFFICIENT COMPLETION: When you have enough data to answer the goal, complete immediately. Do not navigate to additional pages for "confirmation" if the data was already extracted via runScript or is visible in the current a11y tree. Include all extracted data in the completion result
|
|
70
73
|
22. EXTRACT BEFORE NAVIGATING: On search results, directory listings, or any page showing multiple items, ALWAYS extract ALL needed data via runScript BEFORE clicking into individual items. This includes names, phone numbers, addresses, ratings, prices — anything visible on list cards. Use: document.querySelectorAll('.result-card, .listing, [class*="card"]') to grab everything at once. Many sites use anti-bot protection on detail pages but leave listing pages accessible. If you can answer the goal from list-level data, do so without navigating deeper. NEVER click into 3+ individual items when the data is on the list page
|
|
71
|
-
23. FILTER vs SEARCH: When a goal asks to filter results (e.g., "under $50", "4+ stars"), look for filter controls (sliders, dropdowns, checkboxes in a sidebar or toolbar) rather than typing filter values into the search box. Search boxes are for keyword queries, not numeric filters. After applying a filter: (1) wait 2-3 seconds for results to update, (2) verify the filter took effect by checking the updated results, (3) extract the filtered data via runScript. Do NOT keep searching for more filter controls after one is applied — extract and complete
|
|
72
|
-
|
|
74
|
+
23. FILTER vs SEARCH: When a goal asks to filter results (e.g., "under $50", "4+ stars"), look for filter controls (sliders, dropdowns, checkboxes in a sidebar or toolbar) rather than typing filter values into the search box. Search boxes are for keyword queries, not numeric filters. After applying a filter: (1) wait 2-3 seconds for results to update, (2) verify the filter took effect by checking the updated results, (3) extract the filtered data via runScript. Do NOT keep searching for more filter controls after one is applied — extract and complete`;
|
|
75
|
+
/** Heavy page rules (19-20, 24): injected when snapshot is large or turn count is high */
|
|
76
|
+
const HEAVY_PAGE_RULES = `
|
|
77
|
+
19. FORM FIELD TARGETING: Before typing, verify you are targeting the correct input field using its @ref from the ELEMENTS list. If multiple inputs are visible (e.g., search box + price filter), ensure you select the right one by checking its label or placeholder text in the a11y tree. Never assume focus — always specify the exact @ref
|
|
78
|
+
20. SECTION NAVIGATION: When you need to find a specific section (e.g., rugby, sports, travel) and the nav links aren't in the truncated a11y tree, use runScript to discover navigation: JSON.stringify(Array.from(document.querySelectorAll('nav a, header a, [role="navigation"] a, .nav a')).slice(0, 30).map(a => ({text: a.textContent.trim(), href: a.href}))). Then navigate directly to the matching section URL
|
|
79
|
+
24. HEAVY PAGE RECOVERY: If a page takes very long to load or seems stuck, do NOT wait — use runScript to check document.readyState and extract whatever content is already in the DOM. Partial data is better than a timeout. If the page is completely blank, try navigating to a simpler version (mobile site, search page) instead of waiting`;
|
|
80
|
+
/** Reasoning framework and examples (always appended after rules) */
|
|
81
|
+
const REASONING_SUFFIX = `
|
|
73
82
|
|
|
74
83
|
REASONING FRAMEWORK — before choosing an action:
|
|
75
84
|
1. What is the current state vs. the goal state? What is missing?
|
|
@@ -83,6 +92,12 @@ EXAMPLE 1 — Multi-step form fill (use actual refs from ELEMENTS, not these pla
|
|
|
83
92
|
|
|
84
93
|
EXAMPLE 2 — Recovery after failure:
|
|
85
94
|
{"plan":["Click the send button","Wait for response"],"currentStep":0,"action":{"action":"scroll","direction":"down","amount":300},"reasoning":"My last click failed because the element was not visible in the viewport. I can see from the screenshot that the send button is below the fold. Scrolling down to bring it into view before retrying.","expectedEffect":"The send button should become visible in the viewport"}`;
|
|
95
|
+
/** Full static prompt (all rules) — used as default when config.systemPrompt is not set */
|
|
96
|
+
const SYSTEM_PROMPT = CORE_RULES + SEARCH_RULES + DATA_EXTRACTION_RULES + HEAVY_PAGE_RULES + REASONING_SUFFIX;
|
|
97
|
+
/** Pattern for detecting data-extraction keywords in goal text */
|
|
98
|
+
const DATA_EXTRACTION_PATTERN = /\b(extract|list|find|data|price|pric|names?|rating|cost|count)\b/i;
|
|
99
|
+
/** Pattern for detecting search-related roles in snapshot text */
|
|
100
|
+
const SEARCH_SNAPSHOT_PATTERN = /^\s*-\s+(?:searchbox|combobox)\s/m;
|
|
86
101
|
const FIRST_TURN_COMPACT_PROMPT = `You are a browser agent choosing the fastest safe next action.
|
|
87
102
|
|
|
88
103
|
Return ONLY valid JSON with:
|
|
@@ -102,35 +117,9 @@ Rules:
|
|
|
102
117
|
4. If a blocker is visible, resolve it first.
|
|
103
118
|
5. Do not over-explore on the first turn.
|
|
104
119
|
6. Respond with JSON only.`;
|
|
105
|
-
const LINK_SCOUT_PROMPT = `
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
You will receive:
|
|
110
|
-
- the user goal
|
|
111
|
-
- the current URL/title
|
|
112
|
-
- the current page structure
|
|
113
|
-
- a small ranked candidate list of visible links
|
|
114
|
-
|
|
115
|
-
Choose the single best candidate that most directly advances the goal.
|
|
116
|
-
Prefer:
|
|
117
|
-
- first-party links already visible on the current page
|
|
118
|
-
- links whose text matches the requested entity/content type
|
|
119
|
-
- links that avoid unnecessary search detours
|
|
120
|
-
|
|
121
|
-
Respond with ONLY a JSON object:
|
|
122
|
-
{
|
|
123
|
-
"selector": "@ref",
|
|
124
|
-
"reasoning": "brief reason",
|
|
125
|
-
"confidence": 0.82
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
Rules:
|
|
129
|
-
1. selector must exactly match one candidate ref
|
|
130
|
-
2. choose only one candidate
|
|
131
|
-
3. do not invent refs
|
|
132
|
-
4. confidence must be 0 to 1
|
|
133
|
-
5. if none are viable, choose the best available candidate anyway`;
|
|
120
|
+
const LINK_SCOUT_PROMPT = `Pick the best link from CANDIDATES to advance the GOAL. Respond with ONLY JSON:
|
|
121
|
+
{"selector":"@ref","reasoning":"brief reason","confidence":0.82}
|
|
122
|
+
Rules: use exact candidate ref, pick one, confidence 0-1, prefer first-party and text-matching links.`;
|
|
134
123
|
const DESIGN_AUDIT_PROMPT = `You are a senior product designer and UX engineer auditing a web application.
|
|
135
124
|
|
|
136
125
|
Analyze the screenshot and accessibility tree for design quality, UX issues, and visual bugs.
|
|
@@ -253,6 +242,10 @@ export class Brain {
|
|
|
253
242
|
: { maxOutputTokens }),
|
|
254
243
|
};
|
|
255
244
|
}
|
|
245
|
+
/** Get a LLM model instance, optionally with provider/model override (e.g. for CAPTCHA fallback) */
|
|
246
|
+
async getLanguageModel(selection) {
|
|
247
|
+
return this.getModel(selection);
|
|
248
|
+
}
|
|
256
249
|
/** Lazily create the LLM model instance based on provider config */
|
|
257
250
|
async getModel(selection) {
|
|
258
251
|
const providerName = selection?.provider || this.provider;
|
|
@@ -384,6 +377,32 @@ export class Brain {
|
|
|
384
377
|
// Verification still routes to nav model (separate code path).
|
|
385
378
|
return false;
|
|
386
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Build the system prompt dynamically, injecting conditional rule groups
|
|
382
|
+
* based on goal text, page snapshot content, and turn number.
|
|
383
|
+
* Saves ~800 tokens per turn on simple navigation tasks.
|
|
384
|
+
*/
|
|
385
|
+
buildSystemPrompt(goal, state, turn) {
|
|
386
|
+
// If a custom systemPrompt was set via config, use it verbatim
|
|
387
|
+
if (this.systemPrompt !== SYSTEM_PROMPT)
|
|
388
|
+
return this.systemPrompt;
|
|
389
|
+
let prompt = CORE_RULES;
|
|
390
|
+
// Search rules: page has searchbox/combobox roles or URL contains /search
|
|
391
|
+
const snapshotSample = state.snapshot.length > 4000 ? state.snapshot.slice(0, 4000) : state.snapshot;
|
|
392
|
+
if (SEARCH_SNAPSHOT_PATTERN.test(snapshotSample) || /\/search\b/i.test(state.url)) {
|
|
393
|
+
prompt += SEARCH_RULES;
|
|
394
|
+
}
|
|
395
|
+
// Data extraction rules: goal mentions extraction-related keywords
|
|
396
|
+
if (DATA_EXTRACTION_PATTERN.test(goal)) {
|
|
397
|
+
prompt += DATA_EXTRACTION_RULES;
|
|
398
|
+
}
|
|
399
|
+
// Heavy page rules: large snapshot or late in the run
|
|
400
|
+
if (state.snapshot.length > 10_000 || turn > 10) {
|
|
401
|
+
prompt += HEAVY_PAGE_RULES;
|
|
402
|
+
}
|
|
403
|
+
prompt += REASONING_SUFFIX;
|
|
404
|
+
return prompt;
|
|
405
|
+
}
|
|
387
406
|
/** Reset conversation history (call between scenarios) */
|
|
388
407
|
reset() {
|
|
389
408
|
this.history = [];
|
|
@@ -417,62 +436,135 @@ export class Brain {
|
|
|
417
436
|
}
|
|
418
437
|
/**
|
|
419
438
|
* Compact conversation history: strip ELEMENTS blocks and screenshots
|
|
420
|
-
* from
|
|
439
|
+
* from older observations, keeping the last 2 user messages intact.
|
|
421
440
|
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* keep full text) is the empirically best balance.
|
|
441
|
+
* For older turns, replaces the full ELEMENTS block with a one-line
|
|
442
|
+
* summary showing element count and the selectors the agent actually
|
|
443
|
+
* used, extracted from the paired assistant response.
|
|
426
444
|
*/
|
|
427
445
|
compactHistory() {
|
|
428
446
|
if (this.history.length === 0)
|
|
429
447
|
return [];
|
|
448
|
+
// Find indices of the last 2 user messages to keep intact
|
|
449
|
+
const userIndices = [];
|
|
450
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
451
|
+
if (this.history[i].role === 'user') {
|
|
452
|
+
userIndices.push(i);
|
|
453
|
+
if (userIndices.length === 2)
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const keepIntactFrom = userIndices.length > 0
|
|
458
|
+
? userIndices[userIndices.length - 1]
|
|
459
|
+
: this.history.length;
|
|
430
460
|
return this.history.map((msg, idx) => {
|
|
431
461
|
if (msg.role !== 'user')
|
|
432
462
|
return msg;
|
|
433
|
-
// Keep the last user
|
|
434
|
-
if (idx >=
|
|
463
|
+
// Keep the last 2 user messages intact (full snapshot)
|
|
464
|
+
if (idx >= keepIntactFrom)
|
|
435
465
|
return msg;
|
|
466
|
+
// For older user messages, extract selectors from paired assistant response
|
|
467
|
+
const assistantMsg = idx + 1 < this.history.length ? this.history[idx + 1] : undefined;
|
|
468
|
+
const selectors = assistantMsg?.role === 'assistant'
|
|
469
|
+
? this.extractSelectorsFromResponse(typeof assistantMsg.content === 'string' ? assistantMsg.content : '')
|
|
470
|
+
: [];
|
|
436
471
|
// Handle multimodal content (array of parts)
|
|
437
472
|
if (Array.isArray(msg.content)) {
|
|
438
473
|
const compacted = msg.content
|
|
439
|
-
// Keep only text parts (strip screenshots from old messages)
|
|
440
474
|
.filter((part) => part.type === 'text')
|
|
441
475
|
.map((part) => ({
|
|
442
476
|
...part,
|
|
443
|
-
text: this.
|
|
477
|
+
text: this.summarizeElements(part.text, selectors),
|
|
444
478
|
}));
|
|
445
479
|
return { ...msg, content: compacted };
|
|
446
480
|
}
|
|
447
481
|
// Handle string content
|
|
448
482
|
if (typeof msg.content === 'string') {
|
|
449
|
-
return { ...msg, content: this.
|
|
483
|
+
return { ...msg, content: this.summarizeElements(msg.content, selectors) };
|
|
450
484
|
}
|
|
451
485
|
return msg;
|
|
452
486
|
});
|
|
453
487
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
488
|
+
/**
|
|
489
|
+
* Extract @ref selectors from an assistant JSON response.
|
|
490
|
+
*/
|
|
491
|
+
extractSelectorsFromResponse(raw) {
|
|
492
|
+
const selectors = [];
|
|
493
|
+
try {
|
|
494
|
+
let text = raw.trim();
|
|
495
|
+
if (text.startsWith('```')) {
|
|
496
|
+
text = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
|
|
497
|
+
}
|
|
498
|
+
const parsed = JSON.parse(text);
|
|
499
|
+
if (parsed.action?.selector)
|
|
500
|
+
selectors.push(parsed.action.selector);
|
|
501
|
+
if (Array.isArray(parsed.nextActions)) {
|
|
502
|
+
for (const na of parsed.nextActions) {
|
|
503
|
+
if (na?.selector)
|
|
504
|
+
selectors.push(na.selector);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// Best effort
|
|
510
|
+
}
|
|
511
|
+
return selectors;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Replace the ELEMENTS block with a one-line action-only summary.
|
|
515
|
+
*/
|
|
516
|
+
summarizeElements(text, selectors) {
|
|
517
|
+
return text.replace(/ELEMENTS[^:\n]*:\n[\s\S]*?(?=\n\n|What action should you take\?|$)/, (match) => {
|
|
518
|
+
const snapshotStart = match.indexOf('\n');
|
|
458
519
|
if (snapshotStart === -1)
|
|
459
520
|
return 'ELEMENTS:\n[previous snapshot]';
|
|
460
|
-
const snapshotText =
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return
|
|
521
|
+
const snapshotText = match.slice(snapshotStart + 1);
|
|
522
|
+
const elementCount = (snapshotText.match(/\[ref=\w+\]/g) || []).length;
|
|
523
|
+
const selectorList = selectors.length > 0
|
|
524
|
+
? selectors.join(', ')
|
|
525
|
+
: 'none';
|
|
526
|
+
return `ELEMENTS:\n[Page snapshot: ${elementCount} elements | agent used: ${selectorList}]`;
|
|
466
527
|
});
|
|
467
528
|
}
|
|
468
529
|
async decide(goal, state, extraContext, turnInfo, options) {
|
|
469
530
|
const useCompactFirstTurn = this.compactFirstTurn && turnInfo?.current === 1;
|
|
470
531
|
const samePageAsPrevious = this.lastDecisionUrl === state.url;
|
|
532
|
+
const isFirstTurn = !turnInfo || turnInfo.current <= 1;
|
|
533
|
+
// Diff-only mode: on same-page turns with small diffs, send only changed
|
|
534
|
+
// elements instead of the full snapshot. Saves 40-80% of input tokens on
|
|
535
|
+
// form-fill / interaction-heavy pages where the page structure is stable.
|
|
536
|
+
const rawDiff = state.snapshotDiffRaw;
|
|
537
|
+
const diffChanges = rawDiff ? rawDiff.added.length + rawDiff.removed.length + rawDiff.changed.length : 0;
|
|
538
|
+
const diffTotal = rawDiff ? diffChanges + rawDiff.unchangedCount : 0;
|
|
539
|
+
const useDiffOnly = samePageAsPrevious
|
|
540
|
+
&& !isFirstTurn
|
|
541
|
+
&& rawDiff !== undefined
|
|
542
|
+
&& diffChanges > 0
|
|
543
|
+
&& diffTotal > 0
|
|
544
|
+
&& diffChanges / diffTotal < 0.3;
|
|
471
545
|
// Tighter snapshot budget on same-page turns — agent already saw the full page
|
|
472
546
|
const snapshotBudget = samePageAsPrevious ? 8_000 : 16_000;
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
547
|
+
let visibleSnapshot;
|
|
548
|
+
let elementsHeader;
|
|
549
|
+
if (useDiffOnly) {
|
|
550
|
+
// Build compact diff-only view: changed/added elements with refs
|
|
551
|
+
const lines = [];
|
|
552
|
+
if (rawDiff.added.length)
|
|
553
|
+
lines.push('ADDED:', ...rawDiff.added);
|
|
554
|
+
if (rawDiff.changed.length)
|
|
555
|
+
lines.push('CHANGED:', ...rawDiff.changed);
|
|
556
|
+
if (rawDiff.removed.length)
|
|
557
|
+
lines.push('REMOVED:', ...rawDiff.removed);
|
|
558
|
+
lines.push(`(${rawDiff.unchangedCount} elements unchanged — refs from previous turn still valid)`);
|
|
559
|
+
visibleSnapshot = lines.join('\n');
|
|
560
|
+
elementsHeader = 'ELEMENTS (diff-only, previous refs still valid)';
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
visibleSnapshot = useCompactFirstTurn
|
|
564
|
+
? compactFirstTurnSnapshot(state.snapshot)
|
|
565
|
+
: budgetSnapshot(state.snapshot, snapshotBudget);
|
|
566
|
+
elementsHeader = 'ELEMENTS';
|
|
567
|
+
}
|
|
476
568
|
this.lastDecisionUrl = state.url;
|
|
477
569
|
// Build user message with stable prefix (GOAL) for prompt caching,
|
|
478
570
|
// then dynamic per-turn content (turn budget, page state, elements).
|
|
@@ -482,7 +574,7 @@ CURRENT PAGE:
|
|
|
482
574
|
URL: ${state.url}
|
|
483
575
|
Title: ${state.title}
|
|
484
576
|
|
|
485
|
-
|
|
577
|
+
${elementsHeader}:
|
|
486
578
|
${visibleSnapshot}`;
|
|
487
579
|
if (turnInfo) {
|
|
488
580
|
const remaining = turnInfo.max - turnInfo.current;
|
|
@@ -498,8 +590,8 @@ ${visibleSnapshot}`;
|
|
|
498
590
|
textContent += ` — HALF BUDGET USED. If you have extracted useful data, try completing now. Do not navigate away from pages with relevant content without attempting completion first`;
|
|
499
591
|
}
|
|
500
592
|
}
|
|
501
|
-
// Append snapshot diff when
|
|
502
|
-
if (state.snapshotDiff && state.snapshotDiff.length < state.snapshot.length * 0.3) {
|
|
593
|
+
// Append snapshot diff only when NOT using diff-only mode (avoid redundant info)
|
|
594
|
+
if (!useDiffOnly && state.snapshotDiff && state.snapshotDiff.length < state.snapshot.length * 0.3) {
|
|
503
595
|
textContent += `\n\nSNAPSHOT CHANGES (since last turn):\n${state.snapshotDiff}`;
|
|
504
596
|
}
|
|
505
597
|
if (extraContext) {
|
|
@@ -523,7 +615,10 @@ ${visibleSnapshot}`;
|
|
|
523
615
|
...this.compactHistory(),
|
|
524
616
|
{ role: 'user', content: userContent },
|
|
525
617
|
];
|
|
526
|
-
const
|
|
618
|
+
const dynamicSystemPrompt = useCompactFirstTurn
|
|
619
|
+
? FIRST_TURN_COMPACT_PROMPT
|
|
620
|
+
: this.buildSystemPrompt(goal, state, turnInfo?.current ?? 1);
|
|
621
|
+
const result = await this.generate(dynamicSystemPrompt, messages, { provider: effectiveProvider, model: effectiveModel }, useCompactFirstTurn ? 500 : 600);
|
|
527
622
|
const raw = result.text;
|
|
528
623
|
const tokensUsed = result.tokensUsed;
|
|
529
624
|
if (!raw) {
|
|
@@ -601,18 +696,14 @@ Please evaluate the quality of this page/application.`;
|
|
|
601
696
|
}
|
|
602
697
|
async recommendLinkCandidate(goal, state, candidates, extraContext) {
|
|
603
698
|
const topCandidates = candidates.slice(0, 5);
|
|
699
|
+
// Scout only needs candidates + context, not the full snapshot (saves 2-8k tokens)
|
|
604
700
|
const lines = [
|
|
605
701
|
`GOAL: ${goal}`,
|
|
606
702
|
'',
|
|
607
|
-
|
|
608
|
-
`URL: ${state.url}`,
|
|
609
|
-
`Title: ${state.title}`,
|
|
610
|
-
'',
|
|
611
|
-
'ELEMENTS:',
|
|
612
|
-
state.snapshot,
|
|
703
|
+
`PAGE: ${state.url} — ${state.title}`,
|
|
613
704
|
'',
|
|
614
705
|
'CANDIDATES:',
|
|
615
|
-
...topCandidates.map((candidate, index) => `${index + 1}. ${candidate.ref} — ${candidate.text} (
|
|
706
|
+
...topCandidates.map((candidate, index) => `${index + 1}. ${candidate.ref} — ${candidate.text} (score ${candidate.score})`),
|
|
616
707
|
];
|
|
617
708
|
if (extraContext) {
|
|
618
709
|
lines.push('', extraContext);
|
|
@@ -686,30 +777,11 @@ Was the goal actually achieved? Analyze the current page state carefully.`;
|
|
|
686
777
|
const verifyModel = this.adaptiveModelRouting && this.navModelName
|
|
687
778
|
? this.navModelName
|
|
688
779
|
: undefined;
|
|
689
|
-
const result = await this.generate(`
|
|
690
|
-
|
|
691
|
-
Analyze the page state (screenshot + accessibility tree) and determine if the stated goal was accomplished.
|
|
780
|
+
const result = await this.generate(`Verify whether the browser agent achieved its goal. Respond with ONLY JSON:
|
|
781
|
+
{"achieved":true,"confidence":0.9,"evidence":["observation"],"missing":[]}
|
|
692
782
|
|
|
693
|
-
Check
|
|
694
|
-
|
|
695
|
-
2. Are there error messages, incomplete forms, or missing elements?
|
|
696
|
-
3. Does the URL match what you'd expect after goal completion?
|
|
697
|
-
4. Is the claimed result consistent with what's visible on the page?
|
|
698
|
-
5. CRITICAL — SUPPLEMENTAL TOOL EVIDENCE: If the claimed result includes "SUPPLEMENTAL TOOL EVIDENCE" or "SCRIPT RESULT" sections, this data was extracted programmatically from the actual page DOM via JavaScript. This evidence is VERIFIED and TRUSTWORTHY — treat it as equivalent to data visible on the current page. It can fully satisfy data requirements (titles, dates, prices, ratings, counts, URLs) even if the current page no longer shows that data. Do NOT reject a completion simply because the extracted data isn't visible in the current accessibility tree.
|
|
699
|
-
6. MULTI-PAGE TASKS: For goals requiring data from multiple pages (e.g., "find X and extract Y"), the agent may have navigated through several pages collecting data via runScript. If the claimed result contains specific data points that match the SUPPLEMENTAL TOOL EVIDENCE, accept the completion even if the current page is a different page from where the data was extracted.
|
|
700
|
-
|
|
701
|
-
Respond with ONLY a JSON object:
|
|
702
|
-
{
|
|
703
|
-
"achieved": true,
|
|
704
|
-
"confidence": 0.9,
|
|
705
|
-
"evidence": ["The dashboard shows the new item", "URL changed to /success"],
|
|
706
|
-
"missing": []
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
- achieved: true if the goal is clearly met, false if not or uncertain
|
|
710
|
-
- confidence: 0.0 to 1.0 — how sure are you?
|
|
711
|
-
- evidence: specific observations supporting your judgment
|
|
712
|
-
- missing: what's still needed (empty array if achieved)`, [{ role: 'user', content: userContent }], verifyProvider && verifyModel ? { provider: verifyProvider, model: verifyModel } : undefined, 600);
|
|
783
|
+
Check: page state matches goal, no errors, URL is expected, claimed result matches visible data.
|
|
784
|
+
SUPPLEMENTAL TOOL EVIDENCE / SCRIPT RESULT in claimed results = verified DOM data, trustworthy even if page navigated away. Multi-page data collection is valid.`, [{ role: 'user', content: userContent }], verifyProvider && verifyModel ? { provider: verifyProvider, model: verifyModel } : undefined, 600);
|
|
713
785
|
const raw = result.text;
|
|
714
786
|
if (this.debug) {
|
|
715
787
|
console.log('[Brain] Goal verification:', raw.slice(0, 300));
|
|
@@ -906,12 +978,74 @@ Only include facts that are genuinely useful. Quality over quantity. Max 10 fact
|
|
|
906
978
|
}
|
|
907
979
|
}
|
|
908
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* Collapse consecutive runs of similar elements (same indent + role, names
|
|
983
|
+
* differing only by a trailing number/short suffix) into a single representative
|
|
984
|
+
* line with a count. Reduces token cost on pages with long pagination, nav
|
|
985
|
+
* lists, or repeated product cards.
|
|
986
|
+
*
|
|
987
|
+
* Skips dialog/alertdialog (agent must see each one) and groups < 3 items.
|
|
988
|
+
*/
|
|
989
|
+
function deduplicateSnapshot(snapshot) {
|
|
990
|
+
const lines = snapshot.split('\n');
|
|
991
|
+
const out = [];
|
|
992
|
+
// Extract (indent, role) from a snapshot line. Returns null for non-element lines.
|
|
993
|
+
const parseLine = (line) => {
|
|
994
|
+
const m = line.match(/^(\s*-\s+)(\w+)\s+"([^"]*)"\s*\[ref=(\w+)\]/);
|
|
995
|
+
if (!m)
|
|
996
|
+
return null;
|
|
997
|
+
return { indent: m[1], role: m[2], name: m[3], ref: m[4], full: line };
|
|
998
|
+
};
|
|
999
|
+
// Strip trailing numbers/ordinals to get a "name stem" for grouping.
|
|
1000
|
+
// "Page 1" and "Page 20" → "Page ", "Item #3" and "Item #42" → "Item #"
|
|
1001
|
+
const nameStem = (name) => name.replace(/\d+/g, '#');
|
|
1002
|
+
let i = 0;
|
|
1003
|
+
while (i < lines.length) {
|
|
1004
|
+
const parsed = parseLine(lines[i]);
|
|
1005
|
+
// Non-element line or dialog/alertdialog — emit as-is
|
|
1006
|
+
if (!parsed || /\b(?:dialog|alertdialog)\b/i.test(parsed.role)) {
|
|
1007
|
+
out.push(lines[i]);
|
|
1008
|
+
i++;
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
// Collect a consecutive run of same (indent, role) with similar name stems
|
|
1012
|
+
const group = [parsed];
|
|
1013
|
+
const stem = nameStem(parsed.name);
|
|
1014
|
+
let j = i + 1;
|
|
1015
|
+
while (j < lines.length) {
|
|
1016
|
+
const next = parseLine(lines[j]);
|
|
1017
|
+
if (!next ||
|
|
1018
|
+
next.indent !== parsed.indent ||
|
|
1019
|
+
next.role !== parsed.role ||
|
|
1020
|
+
nameStem(next.name) !== stem)
|
|
1021
|
+
break;
|
|
1022
|
+
group.push(next);
|
|
1023
|
+
j++;
|
|
1024
|
+
}
|
|
1025
|
+
if (group.length < 3) {
|
|
1026
|
+
// Not enough to dedup — emit originals
|
|
1027
|
+
for (const g of group)
|
|
1028
|
+
out.push(g.full);
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
// Emit first element with a summary of the rest
|
|
1032
|
+
const last = group[group.length - 1];
|
|
1033
|
+
out.push(`${parsed.full} (+${group.length - 1} similar: "${group[1].name}"\u2026"${last.name}")`);
|
|
1034
|
+
}
|
|
1035
|
+
i = j;
|
|
1036
|
+
}
|
|
1037
|
+
return out.join('\n');
|
|
1038
|
+
}
|
|
909
1039
|
/**
|
|
910
1040
|
* Cap snapshot size for non-first turns to control token cost on large pages.
|
|
911
1041
|
* Keeps the full snapshot when it fits within budget; otherwise truncates
|
|
912
1042
|
* non-interactive decorative lines first, then hard-caps with a notice.
|
|
913
1043
|
*/
|
|
914
1044
|
function budgetSnapshot(snapshot, maxChars = 16_000) {
|
|
1045
|
+
// Skip dedup on small snapshots — not enough repetition to justify the O(n) scan
|
|
1046
|
+
if (snapshot.length > 6_000) {
|
|
1047
|
+
snapshot = deduplicateSnapshot(snapshot);
|
|
1048
|
+
}
|
|
915
1049
|
if (snapshot.length <= maxChars)
|
|
916
1050
|
return snapshot;
|
|
917
1051
|
// First pass: drop non-interactive lines (images, paragraphs, decorative text)
|