explorbot 0.1.26 → 0.1.28
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/package.json +1 -1
- package/dist/src/action-result.js +2 -0
- package/dist/src/action.js +88 -7
- package/dist/src/ai/captain/file-tools.js +100 -0
- package/dist/src/ai/captain/idle-mode.js +70 -6
- package/dist/src/ai/captain/web-mode.js +36 -6
- package/dist/src/ai/captain.js +87 -19
- package/dist/src/ai/historian/screencast.js +11 -2
- package/dist/src/ai/navigator.js +5 -2
- package/dist/src/ai/pilot.js +33 -5
- package/dist/src/ai/researcher/cache.js +8 -0
- package/dist/src/ai/researcher/coordinates.js +2 -3
- package/dist/src/ai/researcher/deep-analysis.js +149 -65
- package/dist/src/ai/researcher/locators.js +1 -2
- package/dist/src/ai/researcher.js +17 -18
- package/dist/src/ai/task-agent.js +1 -0
- package/dist/src/ai/tester.js +91 -39
- package/dist/src/ai/tools.js +17 -7
- package/dist/src/commands/explore-command.js +6 -1
- package/dist/src/components/LogPane.js +4 -3
- package/dist/src/explorer.js +270 -35
- package/dist/src/utils/browser-errors.js +23 -0
- package/dist/src/utils/error-page.js +17 -2
- package/dist/src/utils/logger.js +2 -2
- package/package.json +1 -1
- package/src/action-result.ts +2 -0
- package/src/action.ts +83 -7
- package/src/ai/captain/file-tools.ts +126 -0
- package/src/ai/captain/idle-mode.ts +72 -6
- package/src/ai/captain/mixin.ts +1 -1
- package/src/ai/captain/web-mode.ts +40 -5
- package/src/ai/captain.ts +94 -20
- package/src/ai/historian/screencast.ts +11 -2
- package/src/ai/navigator.ts +6 -2
- package/src/ai/pilot.ts +34 -5
- package/src/ai/researcher/cache.ts +7 -0
- package/src/ai/researcher/coordinates.ts +2 -3
- package/src/ai/researcher/deep-analysis.ts +169 -72
- package/src/ai/researcher/locators.ts +1 -2
- package/src/ai/researcher.ts +17 -18
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tester.ts +101 -41
- package/src/ai/tools.ts +17 -7
- package/src/commands/explore-command.ts +6 -1
- package/src/components/LogPane.tsx +4 -3
- package/src/explorer.ts +295 -38
- package/src/state-manager.ts +2 -0
- package/src/utils/browser-errors.ts +25 -0
- package/src/utils/error-page.ts +16 -3
- package/src/utils/logger.ts +3 -3
|
@@ -5,10 +5,11 @@ import type Explorer from '../../explorer.ts';
|
|
|
5
5
|
import type { StateManager } from '../../state-manager.js';
|
|
6
6
|
import { WebPageState } from '../../state-manager.js';
|
|
7
7
|
import { detectFocusArea, diffAriaSnapshots } from '../../utils/aria.ts';
|
|
8
|
+
import { extractCodeBlocks } from '../../utils/code-extractor.ts';
|
|
8
9
|
import { tag } from '../../utils/logger.js';
|
|
9
10
|
import { mdq } from '../../utils/markdown-query.ts';
|
|
10
11
|
import type { Provider } from '../provider.js';
|
|
11
|
-
import { getCachedResearch, saveResearch } from './cache.ts';
|
|
12
|
+
import { getCachedResearch, getPreviousResearch, saveResearch } from './cache.ts';
|
|
12
13
|
import { type Constructor, debugLog } from './mixin.ts';
|
|
13
14
|
import { type ResearchElement, parseResearchSections } from './parser.ts';
|
|
14
15
|
import type { ResearchResult } from './research-result.ts';
|
|
@@ -26,14 +27,30 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
26
27
|
tag('info').log('Starting deep analysis of expandable elements');
|
|
27
28
|
await (this as any).navigateTo(state.fullUrl || state.url);
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const maxClicks = (this.explorer.getConfig().ai?.agents?.researcher as any)?.maxExpandableClicks ?? DEFAULT_MAX_EXPANDABLE_CLICKS;
|
|
31
|
+
|
|
32
|
+
const expandedSections: string[] = [];
|
|
33
|
+
const navigationLinks: Array<{ code: string; url: string }> = [];
|
|
34
|
+
let verifiedCodes: string[] = [];
|
|
35
|
+
let missing: PreviousSection[] = [];
|
|
36
|
+
|
|
37
|
+
const previousSections = this._loadPreviousExtendedSections(state.hash || '');
|
|
38
|
+
if (previousSections.length > 0) {
|
|
39
|
+
tag('substep').log(`Replaying ${previousSections.length} previously discovered sections`);
|
|
40
|
+
const replay = await this._replayPreviousSections(state, previousSections, maxClicks);
|
|
41
|
+
expandedSections.push(...replay.verified);
|
|
42
|
+
verifiedCodes = replay.verifiedCodes;
|
|
43
|
+
missing = replay.missing;
|
|
44
|
+
tag('info').log(`Reused ${replay.verified.length}/${previousSections.length} previous sections, ${missing.length} to re-discover`);
|
|
45
|
+
|
|
46
|
+
if (missing.length === 0 && replay.verified.length >= maxClicks) {
|
|
47
|
+
tag('info').log('Page appears unchanged, reusing previous sections and skipping discovery');
|
|
48
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
33
51
|
}
|
|
34
|
-
tag('substep').log(`Identified ${expandables.length} expandable elements`);
|
|
35
52
|
|
|
36
|
-
|
|
53
|
+
let expandables = await this._discoverExpandables(result.text, missing, verifiedCodes);
|
|
37
54
|
if (expandables.length > maxClicks) {
|
|
38
55
|
expandables = await this._selectExpandables(expandables, state.fullUrl || state.url, maxClicks);
|
|
39
56
|
tag('substep').log(`Selected ${expandables.length} expandables to click (max: ${maxClicks})`);
|
|
@@ -44,10 +61,12 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
44
61
|
commands: this._buildClickCommands(el),
|
|
45
62
|
description: el.name,
|
|
46
63
|
}))
|
|
47
|
-
.filter((el) => el.commands.length > 0)
|
|
64
|
+
.filter((el) => el.commands.length > 0)
|
|
65
|
+
.filter((el) => !el.commands.some((cmd) => verifiedCodes.includes(cmd)));
|
|
48
66
|
|
|
49
67
|
if (elements.length === 0) {
|
|
50
|
-
tag('info').log('No
|
|
68
|
+
tag('info').log('No new expandable elements to click');
|
|
69
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
51
70
|
return;
|
|
52
71
|
}
|
|
53
72
|
|
|
@@ -57,25 +76,11 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
57
76
|
for (const el of elements) debugLog(`Expandable: ${el.description} → ${el.commands[0]}`);
|
|
58
77
|
tag('substep').log(`Clicking ${elements.length} expandable elements`);
|
|
59
78
|
|
|
60
|
-
const expandedSections: string[] = [];
|
|
61
|
-
const navigationLinks: Array<{ code: string; url: string }> = [];
|
|
62
|
-
|
|
63
79
|
await this._clickExpandableElements(elements, state, expandedSections, navigationLinks);
|
|
64
80
|
|
|
65
81
|
tag('info').log(`Deep analysis complete. Sections: ${expandedSections.length}, navigation links: ${navigationLinks.length}`);
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
if (dedupedSections.length !== expandedSections.length) {
|
|
69
|
-
tag('substep').log(`Deduplicated ${expandedSections.length} → ${dedupedSections.length} extended sections`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (dedupedSections.length > 0) {
|
|
73
|
-
result.text += `\n\n# Extended Research\n\n${dedupedSections.join('\n\n---\n\n')}`;
|
|
74
|
-
}
|
|
75
|
-
if (navigationLinks.length > 0) {
|
|
76
|
-
const links = navigationLinks.map((l) => `- \`${l.code}\` opens ${l.url}`).join('\n');
|
|
77
|
-
result.text += `\n\n## Navigation Links\n\n${links}`;
|
|
78
|
-
}
|
|
83
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
async researchOverlay(current: ActionResult, previous: ActionResult, pageStateHash: string): Promise<string | null> {
|
|
@@ -127,7 +132,70 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
127
132
|
return sectionMarkdown;
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
private
|
|
135
|
+
private _loadPreviousExtendedSections(hash: string): PreviousSection[] {
|
|
136
|
+
if (!hash) return [];
|
|
137
|
+
const previous = getPreviousResearch(hash);
|
|
138
|
+
if (!previous) return [];
|
|
139
|
+
|
|
140
|
+
const sections: PreviousSection[] = [];
|
|
141
|
+
for (const section of parseResearchSections(previous)) {
|
|
142
|
+
if (!section.isExtended) continue;
|
|
143
|
+
const code = extractCodeBlocks(section.rawMarkdown)[0];
|
|
144
|
+
if (!code) continue;
|
|
145
|
+
sections.push({ name: section.name, code });
|
|
146
|
+
}
|
|
147
|
+
return sections;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async _replayPreviousSections(state: WebPageState, prevSections: PreviousSection[], maxClicks: number): Promise<{ verified: string[]; verifiedCodes: string[]; missing: PreviousSection[] }> {
|
|
151
|
+
const originalAria = state.ariaSnapshot || '';
|
|
152
|
+
const verified: string[] = [];
|
|
153
|
+
const verifiedCodes: string[] = [];
|
|
154
|
+
const missing: PreviousSection[] = [];
|
|
155
|
+
|
|
156
|
+
for (const section of prevSections.slice(0, maxClicks)) {
|
|
157
|
+
if (executionController.isInterrupted()) break;
|
|
158
|
+
|
|
159
|
+
let outcome: ExpansionOutcome;
|
|
160
|
+
try {
|
|
161
|
+
outcome = await this._executeAndAnalyze([section.code], section.name, state, originalAria, this._summarizeExpanded(verified));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
tag('warning').log(`Replay failed for "${section.name}": ${err instanceof Error ? err.message : err}`);
|
|
164
|
+
await this._restorePageState(state.url, originalAria).catch(() => {});
|
|
165
|
+
missing.push(section);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (outcome.status === 'revealed') {
|
|
170
|
+
verified.push(outcome.sectionMarkdown);
|
|
171
|
+
verifiedCodes.push(section.code);
|
|
172
|
+
debugLog(`Replayed and verified section: ${section.name}`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
debugLog(`Could not replay previous section: ${section.name}`);
|
|
177
|
+
missing.push(section);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { verified, verifiedCodes, missing };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private _appendExtendedResearch(result: ResearchResult, expandedSections: string[], navigationLinks: Array<{ code: string; url: string }>): void {
|
|
184
|
+
const dedupedSections = this._deduplicateExpandedSections(expandedSections);
|
|
185
|
+
if (dedupedSections.length !== expandedSections.length) {
|
|
186
|
+
tag('substep').log(`Deduplicated ${expandedSections.length} → ${dedupedSections.length} extended sections`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (dedupedSections.length > 0) {
|
|
190
|
+
result.text += `\n\n# Extended Research\n\n${dedupedSections.join('\n\n---\n\n')}`;
|
|
191
|
+
}
|
|
192
|
+
if (navigationLinks.length > 0) {
|
|
193
|
+
const links = navigationLinks.map((l) => `- \`${l.code}\` opens ${l.url}`).join('\n');
|
|
194
|
+
result.text += `\n\n## Navigation Links\n\n${links}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async _discoverExpandables(researchText: string, missing: PreviousSection[] = [], verifiedCodes: string[] = []): Promise<ExpandableElement[]> {
|
|
131
199
|
const allElements = new Map<string, ExpandableElement>();
|
|
132
200
|
for (const section of parseResearchSections(researchText)) {
|
|
133
201
|
for (const el of section.elements) {
|
|
@@ -138,6 +206,16 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
138
206
|
|
|
139
207
|
const eidxList = [...allElements.keys()].join(', ');
|
|
140
208
|
|
|
209
|
+
let missingHint = '';
|
|
210
|
+
if (missing.length > 0) {
|
|
211
|
+
const list = missing.map((s) => `- "${s.name}" (previously revealed via ${s.code})`).join('\n');
|
|
212
|
+
missingHint = dedent`
|
|
213
|
+
|
|
214
|
+
These sections were present on a previous visit but their trigger could not be replayed now — the element may have moved or been renamed. Prioritize finding the element that now reveals each:
|
|
215
|
+
${list}
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
|
|
141
219
|
const textPrompt = dedent`
|
|
142
220
|
From this UI research, identify elements that could reveal hidden UI when clicked
|
|
143
221
|
(dropdown menus, popups, expandable panels, accordion sections, overflow menus, tab switches).
|
|
@@ -145,6 +223,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
145
223
|
Available eidx refs: ${eidxList}
|
|
146
224
|
|
|
147
225
|
${researchText}
|
|
226
|
+
${missingHint}
|
|
148
227
|
|
|
149
228
|
Rules:
|
|
150
229
|
- Only pick elements that HIDE content until clicked (menus, dropdowns, accordions, tabs)
|
|
@@ -168,6 +247,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
168
247
|
|
|
169
248
|
Look for: overflow/ellipsis menus, chevron dropdowns, hamburger menus,
|
|
170
249
|
gear/settings buttons, accordion toggles, tab switches, filter buttons.
|
|
250
|
+
${missingHint}
|
|
171
251
|
|
|
172
252
|
Rules:
|
|
173
253
|
- For repeated icons (same icon on every list row), pick only the FIRST one
|
|
@@ -279,11 +359,10 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
279
359
|
const isCoordinateClick = el.commands[0].startsWith('I.clickXY(');
|
|
280
360
|
if (!isCoordinateClick) {
|
|
281
361
|
const hoverCmd = el.commands[0].replace('I.click(', 'I.moveCursorTo(');
|
|
282
|
-
|
|
283
|
-
await hoverAction.attempt(hoverCmd, undefined, false);
|
|
362
|
+
await this.explorer.attemptAction(hoverCmd, undefined, false);
|
|
284
363
|
await new Promise((r) => setTimeout(r, 500));
|
|
285
364
|
|
|
286
|
-
await this.explorer.
|
|
365
|
+
await this.explorer.capturePageState();
|
|
287
366
|
const hoverAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
288
367
|
const hoverDiff = await hoverAR.diff(previousState);
|
|
289
368
|
await hoverDiff.calculate();
|
|
@@ -302,53 +381,15 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
302
381
|
}
|
|
303
382
|
}
|
|
304
383
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (await action.attempt(cmd, undefined, false)) {
|
|
309
|
-
clickCode = cmd;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (!clickCode) {
|
|
314
|
-
debugLog(`Click failed: ${el.description.slice(0, 80)}`);
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
319
|
-
|
|
320
|
-
let diff: Diff;
|
|
321
|
-
try {
|
|
322
|
-
await this.explorer.createAction().capturePageState();
|
|
323
|
-
const currAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
324
|
-
diff = await currAR.diff(previousState);
|
|
325
|
-
await diff.calculate();
|
|
326
|
-
} catch (err) {
|
|
327
|
-
tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
|
|
328
|
-
await this._restorePageState(state.url, originalAria);
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (diff.urlHasChanged()) {
|
|
333
|
-
debugLog(`Click navigated to ${this.stateManager.getCurrentState()?.url}`);
|
|
334
|
-
navigationLinks.push({ code: clickCode, url: this.stateManager.getCurrentState()?.url || '' });
|
|
335
|
-
await (this as any).navigateTo(state.url);
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const clickHtmlSize = diff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
340
|
-
if (!diff.ariaChanged && clickHtmlSize <= 150) {
|
|
341
|
-
debugLog(`No changes from: ${el.description.slice(0, 80)}`);
|
|
384
|
+
const outcome = await this._executeAndAnalyze(el.commands, el.description, state, originalAria, this._summarizeExpanded(expandedSections));
|
|
385
|
+
if (outcome.status === 'navigated') {
|
|
386
|
+
navigationLinks.push({ code: outcome.code, url: outcome.url });
|
|
342
387
|
continue;
|
|
343
388
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (sectionMarkdown) {
|
|
347
|
-
expandedSections.push(sectionMarkdown);
|
|
389
|
+
if (outcome.status === 'revealed') {
|
|
390
|
+
expandedSections.push(outcome.sectionMarkdown);
|
|
348
391
|
debugLog(`Captured section from: ${el.description.slice(0, 80)}`);
|
|
349
392
|
}
|
|
350
|
-
|
|
351
|
-
await this._restorePageState(state.url, originalAria);
|
|
352
393
|
} catch (err) {
|
|
353
394
|
tag('warning').log(`Expandable click failed for "${el.description.slice(0, 80)}": ${err instanceof Error ? err.message : err}`);
|
|
354
395
|
try {
|
|
@@ -358,10 +399,59 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
358
399
|
}
|
|
359
400
|
}
|
|
360
401
|
|
|
402
|
+
private async _executeAndAnalyze(commands: string[], description: string, state: WebPageState, originalAria: string, alreadyExpanded: string[]): Promise<ExpansionOutcome> {
|
|
403
|
+
const previousState = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
404
|
+
|
|
405
|
+
let clickCode: string | null = null;
|
|
406
|
+
const action = this.explorer.createAction();
|
|
407
|
+
for (const cmd of commands) {
|
|
408
|
+
if (await action.attempt(cmd, undefined, false)) {
|
|
409
|
+
clickCode = cmd;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (!clickCode) {
|
|
414
|
+
debugLog(`Click failed: ${description.slice(0, 80)}`);
|
|
415
|
+
return { status: 'failed' };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
419
|
+
|
|
420
|
+
let diff: Diff;
|
|
421
|
+
try {
|
|
422
|
+
await this.explorer.createAction().capturePageState();
|
|
423
|
+
const currAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
424
|
+
diff = await currAR.diff(previousState);
|
|
425
|
+
await diff.calculate();
|
|
426
|
+
} catch (err) {
|
|
427
|
+
tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
|
|
428
|
+
await this._restorePageState(state.url, originalAria);
|
|
429
|
+
return { status: 'failed' };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (diff.urlHasChanged()) {
|
|
433
|
+
const url = this.stateManager.getCurrentState()?.url || '';
|
|
434
|
+
debugLog(`Click navigated to ${url}`);
|
|
435
|
+
await (this as any).navigateTo(state.url);
|
|
436
|
+
return { status: 'navigated', code: clickCode, url };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const clickHtmlSize = diff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
440
|
+
if (!diff.ariaChanged && clickHtmlSize <= 150) {
|
|
441
|
+
debugLog(`No changes from: ${description.slice(0, 80)}`);
|
|
442
|
+
return { status: 'none', code: clickCode };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const sectionMarkdown = await this._analyzeExpandedAction(clickCode, description, diff, alreadyExpanded);
|
|
446
|
+
await this._restorePageState(state.url, originalAria);
|
|
447
|
+
if (!sectionMarkdown) return { status: 'none', code: clickCode };
|
|
448
|
+
return { status: 'revealed', code: clickCode, sectionMarkdown };
|
|
449
|
+
}
|
|
450
|
+
|
|
361
451
|
private async _restorePageState(url: string, originalAria: string): Promise<void> {
|
|
362
452
|
try {
|
|
363
453
|
await (this as any).cancelInUi();
|
|
364
|
-
await this.explorer.
|
|
454
|
+
await this.explorer.capturePageState();
|
|
365
455
|
const currentAria = this.stateManager.getCurrentState()?.ariaSnapshot || '';
|
|
366
456
|
if (!diffAriaSnapshots(originalAria, currentAria)) return;
|
|
367
457
|
} catch (err) {
|
|
@@ -484,6 +574,13 @@ interface ExpandableElement extends ResearchElement {
|
|
|
484
574
|
container: string | null;
|
|
485
575
|
}
|
|
486
576
|
|
|
577
|
+
interface PreviousSection {
|
|
578
|
+
name: string;
|
|
579
|
+
code: string;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
type ExpansionOutcome = { status: 'revealed'; code: string; sectionMarkdown: string } | { status: 'navigated'; code: string; url: string } | { status: 'none'; code: string } | { status: 'failed' };
|
|
583
|
+
|
|
487
584
|
export interface DeepAnalysisMethods {
|
|
488
585
|
performDeepAnalysis(state: WebPageState, result: ResearchResult): Promise<void>;
|
|
489
586
|
researchOverlay(current: ActionResult, previous: ActionResult, pageStateHash: string): Promise<string | null>;
|
|
@@ -194,8 +194,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
if (needsXpath.length > 0) {
|
|
197
|
-
const
|
|
198
|
-
const webElements = await WebElement.fromEidxList(page, needsXpath);
|
|
197
|
+
const webElements = await this.explorer.runWithBrowserRecovery('backfillBrokenLocators', () => WebElement.fromEidxList(this.explorer.playwrightHelper.page, needsXpath));
|
|
199
198
|
const changedSections = new Set<(typeof sections)[0]>();
|
|
200
199
|
for (const w of webElements) {
|
|
201
200
|
const entry = needsXpathEls.get(w.eidx!);
|
package/src/ai/researcher.ts
CHANGED
|
@@ -130,12 +130,12 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
130
130
|
|
|
131
131
|
const annotatedElements = await this.explorer.annotateElements();
|
|
132
132
|
debugLog(`Annotated ${annotatedElements.length} interactive elements with eidx`);
|
|
133
|
-
this.actionResult = await this.explorer.
|
|
133
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot && this.provider.hasVision() });
|
|
134
134
|
|
|
135
135
|
const condition = detectPageCondition(this.actionResult!);
|
|
136
136
|
if (condition === 'error') {
|
|
137
137
|
tag('warning').log(`Detected error page at ${state.url}`);
|
|
138
|
-
throw new ErrorPageError(state.url, this.actionResult!.title);
|
|
138
|
+
throw new ErrorPageError(state.url, this.actionResult!.title, this.actionResult!.httpStatus);
|
|
139
139
|
}
|
|
140
140
|
if (condition === 'loading') {
|
|
141
141
|
const settled = await this.waitUntilSettled(screenshot);
|
|
@@ -239,7 +239,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
239
239
|
// Must run BEFORE visuallyAnnotateContainers — annotation overlays inject z-index 99998+ which would pollute the scoring.
|
|
240
240
|
if (!interrupted() && this.hasScreenshotToAnalyze) {
|
|
241
241
|
const sections = parseResearchSections(result.text);
|
|
242
|
-
const focused = await detectFocusedSection(this.explorer.playwrightHelper.page, sections);
|
|
242
|
+
const focused = await this.explorer.runWithBrowserRecovery('detectFocusedSection', () => detectFocusedSection(this.explorer.playwrightHelper.page, sections));
|
|
243
243
|
if (focused) markSectionAsFocused(result, focused);
|
|
244
244
|
}
|
|
245
245
|
|
|
@@ -252,7 +252,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
252
252
|
const freshBroken = freshContainerLocs.filter((l) => l.valid === false).map((l) => l.locator);
|
|
253
253
|
const containers = validContainers.filter((c) => !freshBroken.includes(c.css));
|
|
254
254
|
await this.visuallyAnnotateElements({ containers });
|
|
255
|
-
this.actionResult = await this.explorer.
|
|
255
|
+
this.actionResult = await this.explorer.capturePageWithScreenshot();
|
|
256
256
|
const visualResult = await this.analyzeScreenshotForVisualProps();
|
|
257
257
|
if (visualResult.elements.size > 0) {
|
|
258
258
|
await this.mergeVisualData(result, visualResult.elements);
|
|
@@ -331,7 +331,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
331
331
|
if (!this.actionResult) {
|
|
332
332
|
debugLog('No action result, navigating to URL');
|
|
333
333
|
await this.explorer.visit(url);
|
|
334
|
-
this.actionResult = await this.explorer.
|
|
334
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot });
|
|
335
335
|
return;
|
|
336
336
|
}
|
|
337
337
|
|
|
@@ -341,7 +341,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
341
341
|
|
|
342
342
|
if (!isEmpty && isOnCurrentState) {
|
|
343
343
|
if ((!this.actionResult.screenshot && screenshot) || !this.actionResult.ariaSnapshot) {
|
|
344
|
-
this.actionResult = await this.explorer.
|
|
344
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot });
|
|
345
345
|
}
|
|
346
346
|
return;
|
|
347
347
|
}
|
|
@@ -349,6 +349,8 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
349
349
|
if (isEmpty && isOnCurrentState) {
|
|
350
350
|
debugLog('HTML body empty on current URL, waiting for content');
|
|
351
351
|
tag('step').log('Page body is empty, waiting for content...');
|
|
352
|
+
await this.explorer.visit(url);
|
|
353
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot ?? false });
|
|
352
354
|
await this.waitUntilSettled(screenshot ?? false);
|
|
353
355
|
return;
|
|
354
356
|
}
|
|
@@ -357,36 +359,35 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
357
359
|
tag('step').log('Navigating to URL...');
|
|
358
360
|
|
|
359
361
|
await this.explorer.visit(url);
|
|
360
|
-
this.actionResult = await this.explorer.
|
|
362
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot ?? false });
|
|
361
363
|
}
|
|
362
364
|
|
|
363
365
|
private async waitUntilSettled(screenshot: boolean): Promise<boolean> {
|
|
364
366
|
const errorPageTimeout = (this.explorer.getConfig().ai?.agents?.researcher as any)?.errorPageTimeout ?? 10;
|
|
365
367
|
if (errorPageTimeout <= 0) return false;
|
|
366
368
|
|
|
367
|
-
const page = this.explorer.playwrightHelper.page;
|
|
368
369
|
const includeScreenshot = screenshot && this.provider.hasVision();
|
|
369
370
|
|
|
370
371
|
try {
|
|
371
|
-
await page?.waitForLoadState('networkidle', { timeout: errorPageTimeout * 1000 });
|
|
372
|
+
await this.explorer.runWithBrowserRecovery('waitUntilSettled', () => this.explorer.playwrightHelper.page?.waitForLoadState('networkidle', { timeout: errorPageTimeout * 1000 }));
|
|
372
373
|
} catch {}
|
|
373
374
|
|
|
374
375
|
await this.explorer.annotateElements();
|
|
375
|
-
this.actionResult = await this.explorer.
|
|
376
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot });
|
|
376
377
|
|
|
377
378
|
let condition = detectPageCondition(this.actionResult!);
|
|
378
379
|
if (condition === 'error') {
|
|
379
|
-
throw new ErrorPageError(this.actionResult!.url, this.actionResult!.title);
|
|
380
|
+
throw new ErrorPageError(this.actionResult!.url, this.actionResult!.title, this.actionResult!.httpStatus);
|
|
380
381
|
}
|
|
381
382
|
if (condition === 'ok') return true;
|
|
382
383
|
|
|
383
384
|
for (let i = 0; i < 3; i++) {
|
|
384
385
|
await new Promise((r) => setTimeout(r, 1000));
|
|
385
386
|
await this.explorer.annotateElements();
|
|
386
|
-
this.actionResult = await this.explorer.
|
|
387
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot });
|
|
387
388
|
condition = detectPageCondition(this.actionResult!);
|
|
388
389
|
if (condition === 'error') {
|
|
389
|
-
throw new ErrorPageError(this.actionResult!.url, this.actionResult!.title);
|
|
390
|
+
throw new ErrorPageError(this.actionResult!.url, this.actionResult!.title, this.actionResult!.httpStatus);
|
|
390
391
|
}
|
|
391
392
|
if (condition === 'ok') return true;
|
|
392
393
|
}
|
|
@@ -762,17 +763,15 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
762
763
|
}
|
|
763
764
|
|
|
764
765
|
async navigateTo(url: string): Promise<void> {
|
|
765
|
-
|
|
766
|
-
await action.execute(`I.amOnPage("${url}")`);
|
|
766
|
+
await this.explorer.visit(url);
|
|
767
767
|
}
|
|
768
768
|
|
|
769
769
|
async cancelInUi() {
|
|
770
770
|
const beforeAria = this.stateManager.getCurrentState()?.ariaSnapshot || null;
|
|
771
|
-
const action = this.explorer.createAction();
|
|
772
771
|
|
|
773
|
-
await
|
|
772
|
+
await this.explorer.executeAction('I.clickXY(0, 0)');
|
|
774
773
|
if (diffAriaSnapshots(beforeAria, this.stateManager.getCurrentState()?.ariaSnapshot || null)) return;
|
|
775
774
|
|
|
776
|
-
await
|
|
775
|
+
await this.explorer.executeAction(`I.pressKey('Escape')`);
|
|
777
776
|
}
|
|
778
777
|
}
|
package/src/ai/task-agent.ts
CHANGED
|
@@ -24,7 +24,7 @@ export abstract class TaskAgent {
|
|
|
24
24
|
protected consecutiveFailures = 0;
|
|
25
25
|
protected consecutiveEmptyResults = 0;
|
|
26
26
|
protected recentToolCalls: any[] = [];
|
|
27
|
-
protected
|
|
27
|
+
protected readonly ACTION_TOOLS: string[] = [];
|
|
28
28
|
|
|
29
29
|
private _historian: Historian | null = null;
|
|
30
30
|
private _quartermaster: Quartermaster | null = null;
|