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.
Files changed (50) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/action-result.js +2 -0
  3. package/dist/src/action.js +88 -7
  4. package/dist/src/ai/captain/file-tools.js +100 -0
  5. package/dist/src/ai/captain/idle-mode.js +70 -6
  6. package/dist/src/ai/captain/web-mode.js +36 -6
  7. package/dist/src/ai/captain.js +87 -19
  8. package/dist/src/ai/historian/screencast.js +11 -2
  9. package/dist/src/ai/navigator.js +5 -2
  10. package/dist/src/ai/pilot.js +33 -5
  11. package/dist/src/ai/researcher/cache.js +8 -0
  12. package/dist/src/ai/researcher/coordinates.js +2 -3
  13. package/dist/src/ai/researcher/deep-analysis.js +149 -65
  14. package/dist/src/ai/researcher/locators.js +1 -2
  15. package/dist/src/ai/researcher.js +17 -18
  16. package/dist/src/ai/task-agent.js +1 -0
  17. package/dist/src/ai/tester.js +91 -39
  18. package/dist/src/ai/tools.js +17 -7
  19. package/dist/src/commands/explore-command.js +6 -1
  20. package/dist/src/components/LogPane.js +4 -3
  21. package/dist/src/explorer.js +270 -35
  22. package/dist/src/utils/browser-errors.js +23 -0
  23. package/dist/src/utils/error-page.js +17 -2
  24. package/dist/src/utils/logger.js +2 -2
  25. package/package.json +1 -1
  26. package/src/action-result.ts +2 -0
  27. package/src/action.ts +83 -7
  28. package/src/ai/captain/file-tools.ts +126 -0
  29. package/src/ai/captain/idle-mode.ts +72 -6
  30. package/src/ai/captain/mixin.ts +1 -1
  31. package/src/ai/captain/web-mode.ts +40 -5
  32. package/src/ai/captain.ts +94 -20
  33. package/src/ai/historian/screencast.ts +11 -2
  34. package/src/ai/navigator.ts +6 -2
  35. package/src/ai/pilot.ts +34 -5
  36. package/src/ai/researcher/cache.ts +7 -0
  37. package/src/ai/researcher/coordinates.ts +2 -3
  38. package/src/ai/researcher/deep-analysis.ts +169 -72
  39. package/src/ai/researcher/locators.ts +1 -2
  40. package/src/ai/researcher.ts +17 -18
  41. package/src/ai/task-agent.ts +1 -1
  42. package/src/ai/tester.ts +101 -41
  43. package/src/ai/tools.ts +17 -7
  44. package/src/commands/explore-command.ts +6 -1
  45. package/src/components/LogPane.tsx +4 -3
  46. package/src/explorer.ts +295 -38
  47. package/src/state-manager.ts +2 -0
  48. package/src/utils/browser-errors.ts +25 -0
  49. package/src/utils/error-page.ts +16 -3
  50. 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
- let expandables = await this._discoverExpandables(result.text);
30
- if (expandables.length === 0) {
31
- tag('info').log('No expandable elements identified by AI');
32
- return;
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
- const maxClicks = (this.explorer.getConfig().ai?.agents?.researcher as any)?.maxExpandableClicks ?? DEFAULT_MAX_EXPANDABLE_CLICKS;
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 expandables with valid locators');
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
- const dedupedSections = this._deduplicateExpandedSections(expandedSections);
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 async _discoverExpandables(researchText: string): Promise<ExpandableElement[]> {
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
- const hoverAction = this.explorer.createAction();
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.createAction().capturePageState();
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
- let clickCode: string | null = null;
306
- const action = this.explorer.createAction();
307
- for (const cmd of el.commands) {
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
- const sectionMarkdown = await this._analyzeExpandedAction(clickCode, el.description, diff, this._summarizeExpanded(expandedSections));
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.createAction().capturePageState();
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 page = this.explorer.playwrightHelper.page;
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!);
@@ -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.createAction().capturePageState({ includeScreenshot: screenshot && this.provider.hasVision() });
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.createAction().caputrePageWithScreenshot();
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.createAction().capturePageState({ includeScreenshot: screenshot });
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.createAction().capturePageState({ includeScreenshot: screenshot });
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.createAction().capturePageState({ includeScreenshot: screenshot ?? false });
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.createAction().capturePageState({ includeScreenshot });
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.createAction().capturePageState({ includeScreenshot });
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
- const action = this.explorer.createAction();
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 action.execute('I.clickXY(0, 0)');
772
+ await this.explorer.executeAction('I.clickXY(0, 0)');
774
773
  if (diffAriaSnapshots(beforeAria, this.stateManager.getCurrentState()?.ariaSnapshot || null)) return;
775
774
 
776
- await action.execute(`I.pressKey('Escape')`);
775
+ await this.explorer.executeAction(`I.pressKey('Escape')`);
777
776
  }
778
777
  }
@@ -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 abstract readonly ACTION_TOOLS: string[];
27
+ protected readonly ACTION_TOOLS: string[] = [];
28
28
 
29
29
  private _historian: Historian | null = null;
30
30
  private _quartermaster: Quartermaster | null = null;