explorbot 0.2.0 → 0.2.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.
Files changed (145) hide show
  1. package/README.md +21 -5
  2. package/bin/explorbot-cli.ts +29 -7
  3. package/boat/api-tester/src/config.ts +45 -3
  4. package/boat/doc-collector/src/ai/documentarian.ts +8 -2
  5. package/boat/doc-collector/src/ai/tools.ts +29 -16
  6. package/boat/doc-collector/src/cli.ts +2 -6
  7. package/boat/doc-collector/src/config.ts +2 -0
  8. package/boat/doc-collector/src/docbot.ts +42 -14
  9. package/boat/doc-collector/src/interaction-screenshots.ts +160 -0
  10. package/boat/doc-collector/src/screenshots.ts +24 -16
  11. package/dist/bin/explorbot-cli.js +26 -6
  12. package/dist/boat/api-tester/src/config.js +43 -4
  13. package/dist/boat/doc-collector/src/ai/documentarian.js +7 -2
  14. package/dist/boat/doc-collector/src/ai/tools.js +21 -15
  15. package/dist/boat/doc-collector/src/cli.js +2 -5
  16. package/dist/boat/doc-collector/src/config.js +1 -0
  17. package/dist/boat/doc-collector/src/docbot.js +39 -13
  18. package/dist/boat/doc-collector/src/interaction-screenshots.js +156 -0
  19. package/dist/boat/doc-collector/src/screenshots.js +25 -16
  20. package/dist/models.json +30 -0
  21. package/dist/package.json +8 -2
  22. package/dist/src/action.d.ts +11 -3
  23. package/dist/src/action.js +22 -5
  24. package/dist/src/ai/agent.d.ts +17 -0
  25. package/dist/src/ai/captain/idle-mode.js +1 -1
  26. package/dist/src/ai/captain/test-mode.js +1 -1
  27. package/dist/src/ai/captain/web-mode.js +18 -22
  28. package/dist/src/ai/captain.js +13 -26
  29. package/dist/src/ai/driller.d.ts +2 -13
  30. package/dist/src/ai/driller.js +23 -38
  31. package/dist/src/ai/historian/codeceptjs.js +1 -1
  32. package/dist/src/ai/historian/playwright.d.ts +3 -2
  33. package/dist/src/ai/historian/playwright.js +1 -1
  34. package/dist/src/ai/historian/screencast.d.ts +2 -1
  35. package/dist/src/ai/historian/screencast.js +2 -2
  36. package/dist/src/ai/historian.d.ts +5 -4
  37. package/dist/src/ai/navigator.d.ts +6 -2
  38. package/dist/src/ai/navigator.js +26 -22
  39. package/dist/src/ai/pilot.d.ts +13 -6
  40. package/dist/src/ai/pilot.js +49 -44
  41. package/dist/src/ai/planner.d.ts +3 -2
  42. package/dist/src/ai/planner.js +19 -17
  43. package/dist/src/ai/provider.js +0 -1
  44. package/dist/src/ai/quartermaster.d.ts +3 -2
  45. package/dist/src/ai/quartermaster.js +4 -4
  46. package/dist/src/ai/rerunner.d.ts +2 -11
  47. package/dist/src/ai/rerunner.js +9 -22
  48. package/dist/src/ai/researcher/coordinates.js +4 -3
  49. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
  50. package/dist/src/ai/researcher/deep-analysis.js +6 -6
  51. package/dist/src/ai/researcher/locators.d.ts +9 -2
  52. package/dist/src/ai/researcher/locators.js +65 -8
  53. package/dist/src/ai/researcher/sections.d.ts +2 -0
  54. package/dist/src/ai/researcher/sections.js +2 -2
  55. package/dist/src/ai/researcher.js +29 -54
  56. package/dist/src/ai/task-agent.d.ts +18 -5
  57. package/dist/src/ai/task-agent.js +35 -6
  58. package/dist/src/ai/tester.d.ts +6 -12
  59. package/dist/src/ai/tester.js +37 -51
  60. package/dist/src/ai/tools.d.ts +11 -10
  61. package/dist/src/ai/tools.js +24 -36
  62. package/dist/src/command-handler.js +1 -1
  63. package/dist/src/commands/context-aria-command.js +1 -1
  64. package/dist/src/commands/context-command.js +7 -5
  65. package/dist/src/commands/context-data-command.js +2 -2
  66. package/dist/src/commands/context-experience-command.js +2 -2
  67. package/dist/src/commands/context-html-command.js +2 -2
  68. package/dist/src/commands/context-knowledge-command.js +1 -1
  69. package/dist/src/commands/drill-command.js +1 -1
  70. package/dist/src/commands/explore-command.js +4 -3
  71. package/dist/src/commands/freesail-command.js +2 -2
  72. package/dist/src/commands/learn-command.js +4 -4
  73. package/dist/src/commands/path-command.js +1 -1
  74. package/dist/src/commands/research-command.js +1 -1
  75. package/dist/src/commands/test-command.js +1 -1
  76. package/dist/src/components/App.js +1 -1
  77. package/dist/src/config.d.ts +17 -0
  78. package/dist/src/config.js +144 -10
  79. package/dist/src/experience-tracker.js +1 -1
  80. package/dist/src/explorbot.d.ts +17 -8
  81. package/dist/src/explorbot.js +74 -36
  82. package/dist/src/explorer.d.ts +67 -91
  83. package/dist/src/explorer.js +327 -567
  84. package/dist/src/state-manager.d.ts +6 -1
  85. package/dist/src/state-manager.js +1 -0
  86. package/dist/src/utils/hooks-runner.js +2 -4
  87. package/dist/src/utils/test-files.js +1 -1
  88. package/dist/src/utils/web-annotate.d.ts +5 -0
  89. package/dist/src/utils/web-annotate.js +58 -0
  90. package/dist/src/utils/web-eidx.d.ts +2 -0
  91. package/dist/src/utils/web-eidx.js +20 -0
  92. package/dist/src/utils/web-element.d.ts +1 -0
  93. package/dist/src/utils/web-element.js +29 -0
  94. package/models.json +30 -0
  95. package/package.json +8 -2
  96. package/src/action.ts +25 -5
  97. package/src/ai/agent.ts +20 -0
  98. package/src/ai/captain/idle-mode.ts +1 -1
  99. package/src/ai/captain/test-mode.ts +1 -1
  100. package/src/ai/captain/web-mode.ts +18 -23
  101. package/src/ai/captain.ts +12 -25
  102. package/src/ai/driller.ts +50 -67
  103. package/src/ai/historian/codeceptjs.ts +1 -1
  104. package/src/ai/historian/playwright.ts +4 -3
  105. package/src/ai/historian/screencast.ts +4 -3
  106. package/src/ai/historian.ts +3 -2
  107. package/src/ai/navigator.ts +30 -25
  108. package/src/ai/pilot.ts +55 -47
  109. package/src/ai/planner.ts +22 -20
  110. package/src/ai/provider.ts +0 -1
  111. package/src/ai/quartermaster.ts +5 -4
  112. package/src/ai/rerunner.ts +10 -29
  113. package/src/ai/researcher/coordinates.ts +4 -3
  114. package/src/ai/researcher/deep-analysis.ts +8 -6
  115. package/src/ai/researcher/locators.ts +66 -9
  116. package/src/ai/researcher/sections.ts +4 -2
  117. package/src/ai/researcher.ts +32 -61
  118. package/src/ai/task-agent.ts +45 -11
  119. package/src/ai/tester.ts +40 -57
  120. package/src/ai/tools.ts +32 -53
  121. package/src/command-handler.ts +1 -1
  122. package/src/commands/context-aria-command.ts +1 -1
  123. package/src/commands/context-command.ts +7 -5
  124. package/src/commands/context-data-command.ts +2 -2
  125. package/src/commands/context-experience-command.ts +2 -2
  126. package/src/commands/context-html-command.ts +2 -2
  127. package/src/commands/context-knowledge-command.ts +1 -1
  128. package/src/commands/drill-command.ts +1 -1
  129. package/src/commands/explore-command.ts +4 -3
  130. package/src/commands/freesail-command.ts +2 -2
  131. package/src/commands/learn-command.ts +4 -4
  132. package/src/commands/path-command.ts +1 -1
  133. package/src/commands/research-command.ts +1 -1
  134. package/src/commands/test-command.ts +1 -1
  135. package/src/components/App.tsx +1 -1
  136. package/src/config.ts +173 -11
  137. package/src/experience-tracker.ts +1 -1
  138. package/src/explorbot.ts +78 -39
  139. package/src/explorer.ts +388 -626
  140. package/src/state-manager.ts +7 -1
  141. package/src/utils/hooks-runner.ts +2 -4
  142. package/src/utils/test-files.ts +1 -1
  143. package/src/utils/web-annotate.ts +64 -0
  144. package/src/utils/web-eidx.ts +21 -0
  145. package/src/utils/web-element.ts +30 -0
@@ -6,6 +6,7 @@ import { parseAriaLocator } from '../../utils/aria.ts';
6
6
  import { tag } from '../../utils/logger.js';
7
7
  import { mdq } from '../../utils/markdown-query.ts';
8
8
  import { WebElement } from '../../utils/web-element.ts';
9
+ import { isDynamicId } from '../../utils/xpath.ts';
9
10
  import type { Conversation } from '../conversation.ts';
10
11
  import type { Provider } from '../provider.js';
11
12
  import { locatorRule as generalLocatorRuleText } from '../rules.js';
@@ -38,7 +39,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
38
39
  declare provider: Provider;
39
40
  declare actionResult: ActionResult | undefined;
40
41
 
41
- async testLocators(locators: Locator[]): Promise<void> {
42
+ async testLocators(locators: Locator[], opts: { scope?: boolean } = {}): Promise<void> {
42
43
  let broken = 0;
43
44
  for (const loc of locators) {
44
45
  if (executionController.isInterrupted()) break;
@@ -51,20 +52,21 @@ export function WithLocators<T extends Constructor>(Base: T) {
51
52
  continue;
52
53
  }
53
54
  try {
54
- const count = await this.explorer.playwrightLocatorCount((page) => {
55
+ const count = await this.explorer.withPage((page) => {
55
56
  const base = loc.container ? page.locator(loc.container) : page;
56
57
  if (loc.type === 'aria') {
57
58
  const parsed = parseAriaLocator(loc.locator);
58
- if (!parsed) return page.locator('__invalid__');
59
- return base.getByRole(parsed.role as any, { name: parsed.text });
59
+ if (!parsed) return page.locator('__invalid__').count();
60
+ return base.getByRole(parsed.role as any, { name: parsed.text }).count();
60
61
  }
61
62
  const converted = loc.locator.replace(/:contains\(/g, ':has-text(');
62
63
  if (converted !== loc.locator) {
63
64
  loc.locator = converted;
64
65
  }
65
- return base.locator(loc.locator);
66
+ return base.locator(loc.locator).count();
66
67
  });
67
68
  loc.valid = count === 1;
69
+ if (opts.scope && count > 1) loc.valid = true;
68
70
  loc.pwLocator = buildPwLocatorString(loc);
69
71
  if (!loc.valid) {
70
72
  loc.error = count === 0 ? '0 elements' : `${count} elements`;
@@ -194,7 +196,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
194
196
  }
195
197
 
196
198
  if (needsXpath.length > 0) {
197
- const webElements = await this.explorer.runWithBrowserRecovery('backfillBrokenLocators', () => WebElement.fromEidxList(this.explorer.playwrightHelper.page, needsXpath));
199
+ const webElements = await this.explorer.withPage((page) => WebElement.fromEidxList(page, needsXpath));
198
200
  const changedSections = new Set<(typeof sections)[0]>();
199
201
  for (const w of webElements) {
200
202
  const entry = needsXpathEls.get(w.eidx!);
@@ -209,6 +211,60 @@ export function WithLocators<T extends Constructor>(Base: T) {
209
211
  await this.validateContainers(result);
210
212
  }
211
213
 
214
+ async resolveContainers(result: ResearchResult): Promise<Locator[]> {
215
+ const containerLocs = result.containerLocators;
216
+ await this.testLocators(containerLocs, { scope: true });
217
+ for (const loc of containerLocs.filter((l) => l.valid === false)) {
218
+ if (await this.recoverContainerFromChildren(result, loc.locator)) loc.valid = true;
219
+ }
220
+ return containerLocs.filter((l) => l.valid === false);
221
+ }
222
+
223
+ private async recoverContainerFromChildren(result: ResearchResult, css: string): Promise<boolean> {
224
+ const sections = parseResearchSections(result.text).filter((s) => s.containerCss === css);
225
+ let recovered = 0;
226
+
227
+ for (const section of sections) {
228
+ const eidxList = section.elements.map((el) => el.eidx).filter(Boolean) as string[];
229
+ if (eidxList.length < 2) continue;
230
+
231
+ const ancestor = await this.explorer.withPage((page) => WebElement.commonAncestor(page, eidxList));
232
+ if (!ancestor) continue;
233
+
234
+ const candidates: string[] = [];
235
+ const id = ancestor.attrs.id;
236
+ if (id && !isDynamicId(id)) candidates.push(`#${id}`);
237
+ for (const cls of ancestor.filteredClasses) {
238
+ if (!/^[\w-]+$/.test(cls)) continue;
239
+ candidates.push(`${ancestor.tag}.${cls}`);
240
+ }
241
+
242
+ let unique: string | null = null;
243
+ let multiple: string | null = null;
244
+ for (const candidate of candidates) {
245
+ let count = 0;
246
+ try {
247
+ count = await this.explorer.playwrightLocatorCount((page) => page.locator(candidate));
248
+ } catch {}
249
+ if (count === 1) {
250
+ unique = candidate;
251
+ break;
252
+ }
253
+ if (count > 1 && !multiple) multiple = candidate;
254
+ }
255
+
256
+ const newCss = unique || multiple;
257
+ if (!newCss) continue;
258
+
259
+ debugLog(`Recovered container: '${css}' → '${newCss}' in "${section.name}"`);
260
+ this.updateSectionContainer(result, section, newCss);
261
+ recovered++;
262
+ }
263
+
264
+ if (recovered > 0 && recovered < sections.length) debugLog(`Container '${css}' recovered in ${recovered}/${sections.length} sections, still broken for the rest`);
265
+ return recovered > 0 && recovered === sections.length;
266
+ }
267
+
212
268
  private async validateContainers(result: ResearchResult): Promise<void> {
213
269
  const sections = parseResearchSections(result.text);
214
270
 
@@ -217,7 +273,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
217
273
 
218
274
  let count = 0;
219
275
  try {
220
- count = await this.explorer.playwrightLocatorCount((page) => page.locator(section.containerCss!));
276
+ count = await this.explorer.withPage((page) => page.locator(section.containerCss!).count());
221
277
  } catch {}
222
278
 
223
279
  if (count >= 1) continue;
@@ -226,7 +282,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
226
282
  if (simplified) {
227
283
  let simplifiedCount = 0;
228
284
  try {
229
- simplifiedCount = await this.explorer.playwrightLocatorCount((page) => page.locator(simplified));
285
+ simplifiedCount = await this.explorer.withPage((page) => page.locator(simplified).count());
230
286
  } catch {}
231
287
 
232
288
  if (simplifiedCount >= 1) {
@@ -275,7 +331,8 @@ export interface Locator {
275
331
  }
276
332
 
277
333
  export interface LocatorMethods {
278
- testLocators(locators: Locator[]): Promise<void>;
334
+ testLocators(locators: Locator[], opts?: { scope?: boolean }): Promise<void>;
279
335
  fixBrokenSections(result: ResearchResult, conversation: Conversation): Promise<void>;
280
336
  backfillBrokenLocators(result: ResearchResult): Promise<void>;
337
+ resolveContainers(result: ResearchResult): Promise<Locator[]>;
281
338
  }
@@ -1,5 +1,6 @@
1
1
  import dedent from 'dedent';
2
2
  import type { ActionResult } from '../../action-result.js';
3
+ import type { ExplorbotConfig } from '../../config.ts';
3
4
  import { executionController } from '../../execution-controller.ts';
4
5
  import type Explorer from '../../explorer.ts';
5
6
  import type { StateManager } from '../../state-manager.js';
@@ -20,6 +21,7 @@ export function WithSections<T extends Constructor>(Base: T) {
20
21
  declare explorer: Explorer;
21
22
  declare provider: Provider;
22
23
  declare stateManager: StateManager;
24
+ declare config: ExplorbotConfig;
23
25
  declare actionResult: ActionResult | undefined;
24
26
 
25
27
  async researchBySections(): Promise<string> {
@@ -64,11 +66,11 @@ export function WithSections<T extends Constructor>(Base: T) {
64
66
  }
65
67
 
66
68
  private async _detectFocusCss(): Promise<string | null> {
67
- const focusSections = (this.explorer.getConfig().ai?.agents?.researcher as any)?.focusSections as string[] | undefined;
69
+ const focusSections = (this.config.ai?.agents?.researcher as any)?.focusSections as string[] | undefined;
68
70
  if (!focusSections?.length) return null;
69
71
 
70
72
  for (const css of focusSections) {
71
- const count = await this.explorer.playwrightLocatorCount((page: any) => page.locator(css)).catch(() => 0);
73
+ const count = await this.explorer.withPage((page) => page.locator(css).count()).catch(() => 0);
72
74
  if (count > 0) return css;
73
75
  }
74
76
  return null;
@@ -1,23 +1,22 @@
1
1
  import dedent from 'dedent';
2
2
  import { ActionResult } from '../action-result.js';
3
3
  import { setActivity } from '../activity.ts';
4
- import { ConfigParser, outputPath } from '../config.ts';
4
+ import { ConfigParser, type ExplorbotConfig, outputPath } from '../config.ts';
5
5
  import { executionController } from '../execution-controller.ts';
6
6
  import type { ExperienceTracker } from '../experience-tracker.ts';
7
7
  import type Explorer from '../explorer.ts';
8
- import type { KnowledgeTracker } from '../knowledge-tracker.ts';
9
8
  import { Observability } from '../observability.ts';
10
9
  import type { StateManager } from '../state-manager.js';
11
10
  import { WebPageState } from '../state-manager.js';
12
11
  import { Stats } from '../stats.ts';
13
12
  import { diffAriaSnapshots } from '../utils/aria.ts';
14
13
  import { ErrorPageError, detectPageCondition } from '../utils/error-page.ts';
15
- import { HooksRunner } from '../utils/hooks-runner.ts';
16
14
  import { isBodyEmpty } from '../utils/html.ts';
17
15
  import { createDebug, tag } from '../utils/logger.js';
18
16
  import { mdq } from '../utils/markdown-query.ts';
19
17
  import { RulesLoader } from '../utils/rules-loader.ts';
20
- import type { Agent } from './agent.js';
18
+ import { annotatePageElements } from '../utils/web-annotate.ts';
19
+ import type { Agent, AgentDeps } from './agent.js';
21
20
  import type { Navigator } from './navigator.ts';
22
21
  import { ContextLengthError, type Provider } from './provider.js';
23
22
  import { findSimilarResearch, getCachedResearch, saveResearch } from './researcher/cache.ts';
@@ -55,20 +54,16 @@ export class Researcher extends ResearcherBase implements Agent {
55
54
  declare explorer: Explorer;
56
55
  declare provider: Provider;
57
56
  declare stateManager: StateManager;
57
+ declare config: ExplorbotConfig;
58
58
  private experienceTracker!: ExperienceTracker;
59
59
  private hasScreenshotToAnalyze = false;
60
60
  declare actionResult: ActionResult | undefined;
61
- private hooksRunner!: HooksRunner;
62
61
 
63
- constructor(explorer: Explorer, provider: Provider) {
64
- super();
65
- this.explorer = explorer;
66
- this.provider = provider;
67
- this.stateManager = explorer.getStateManager();
68
- this.experienceTracker = this.stateManager.getExperienceTracker();
69
- this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
62
+ constructor(deps: AgentDeps) {
63
+ super(deps);
64
+ this.experienceTracker = deps.stateManager.getExperienceTracker();
70
65
 
71
- const ai = explorer.getConfig().ai;
66
+ const ai = deps.config.ai;
72
67
  if (ai) {
73
68
  ai.agents ??= {};
74
69
  ai.agents.researcher ??= {};
@@ -80,18 +75,6 @@ export class Researcher extends ResearcherBase implements Agent {
80
75
  throw new Error('not implemented');
81
76
  }
82
77
 
83
- protected getExperienceTracker(): ExperienceTracker {
84
- return this.experienceTracker;
85
- }
86
-
87
- protected getKnowledgeTracker(): KnowledgeTracker {
88
- return this.explorer.getKnowledgeTracker();
89
- }
90
-
91
- protected getProvider(): Provider {
92
- return this.provider;
93
- }
94
-
95
78
  static getCachedResearch(state: WebPageState): string {
96
79
  return getCachedResearch(state.hash || '');
97
80
  }
@@ -110,7 +93,7 @@ export class Researcher extends ResearcherBase implements Agent {
110
93
 
111
94
  async research(state: WebPageState, opts: { screenshot?: boolean; force?: boolean; deep?: boolean; data?: boolean; fix?: boolean; _retriesLeft?: number } = {}): Promise<string> {
112
95
  const { screenshot = false, force = false, deep = false, data = false, fix = true } = opts;
113
- const maxRetries = (this.explorer.getConfig().ai?.agents?.researcher as any)?.retries ?? 2;
96
+ const maxRetries = (this.config.ai?.agents?.researcher as any)?.retries ?? 2;
114
97
  let retriesLeft = opts._retriesLeft ?? maxRetries;
115
98
  this.actionResult = ActionResult.fromState(state);
116
99
  const stateHash = state.hash || this.actionResult.getStateHash();
@@ -134,9 +117,9 @@ export class Researcher extends ResearcherBase implements Agent {
134
117
  await this.ensureNavigated(displayUrl, screenshot && this.provider.hasVision());
135
118
  await this.hooksRunner.runBeforeHook('researcher', state.url);
136
119
 
137
- const annotatedElements = await this.explorer.annotateElements();
120
+ const { elements: annotatedElements } = await this.explorer.withPage(annotatePageElements);
138
121
  debugLog(`Annotated ${annotatedElements.length} interactive elements with eidx`);
139
- this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot && this.provider.hasVision() });
122
+ this.actionResult = await this.explorer.capture({ screenshot: screenshot && this.provider.hasVision() });
140
123
 
141
124
  const condition = detectPageCondition(this.actionResult!);
142
125
  if (condition === 'error') {
@@ -208,11 +191,10 @@ export class Researcher extends ResearcherBase implements Agent {
208
191
  }
209
192
 
210
193
  if (!interrupted()) {
211
- const containerLocs = result.containerLocators;
212
- await this.testLocators(containerLocs);
213
- const brokenContainers = containerLocs.filter((l) => l.valid === false);
214
- if (containerLocs.length > 0 && brokenContainers.length === containerLocs.length && retriesLeft > 0) {
215
- tag('warning').log(`All ${containerLocs.length} containers broken, retrying research (${maxRetries - retriesLeft + 1}/${maxRetries})...`);
194
+ const brokenContainers = await this.resolveContainers(result);
195
+ const containerCount = result.containers.length;
196
+ if (containerCount > 0 && brokenContainers.length === containerCount && retriesLeft > 0) {
197
+ tag('warning').log(`All ${containerCount} containers broken, retrying research (${maxRetries - retriesLeft + 1}/${maxRetries})...`);
216
198
  await new Promise((r) => setTimeout(r, 2000));
217
199
  return this.research(state, { ...opts, force: true, _retriesLeft: retriesLeft - 1 } as any);
218
200
  }
@@ -226,14 +208,6 @@ export class Researcher extends ResearcherBase implements Agent {
226
208
 
227
209
  const toTest = result.locators.filter((l) => l.valid === null);
228
210
  await this.testLocators(toTest);
229
-
230
- const brokenCount = result.locators.filter((l) => l.valid === false).length;
231
- const brokenRatio = result.locators.length > 0 ? brokenCount / result.locators.length : 0;
232
- if (brokenRatio > 0.8 && retriesLeft > 0) {
233
- tag('warning').log(`${Math.round(brokenRatio * 100)}% locators broken, waiting 3s and retrying research (${maxRetries - retriesLeft + 1}/${maxRetries})...`);
234
- await new Promise((r) => setTimeout(r, 3000));
235
- return this.research(state, { ...opts, force: true, _retriesLeft: retriesLeft - 1 } as any);
236
- }
237
211
  }
238
212
 
239
213
  // Stage 3: Fix broken sections via AI conversation continuation
@@ -245,7 +219,7 @@ export class Researcher extends ResearcherBase implements Agent {
245
219
  // Must run BEFORE visuallyAnnotateContainers — annotation overlays inject z-index 99998+ which would pollute the scoring.
246
220
  if (!interrupted() && this.hasScreenshotToAnalyze) {
247
221
  const sections = parseResearchSections(result.text);
248
- const focused = await this.explorer.runWithBrowserRecovery('detectFocusedSection', () => detectFocusedSection(this.explorer.playwrightHelper.page, sections));
222
+ const focused = await this.explorer.withPage((page) => detectFocusedSection(page, sections));
249
223
  if (focused) markSectionAsFocused(result, focused);
250
224
  }
251
225
 
@@ -254,11 +228,11 @@ export class Researcher extends ResearcherBase implements Agent {
254
228
  const validContainers = extractValidContainers(result.text);
255
229
  result.parseLocators();
256
230
  const freshContainerLocs = result.containerLocators;
257
- await this.testLocators(freshContainerLocs);
231
+ await this.testLocators(freshContainerLocs, { scope: true });
258
232
  const freshBroken = freshContainerLocs.filter((l) => l.valid === false).map((l) => l.locator);
259
233
  const containers = validContainers.filter((c) => !freshBroken.includes(c.css));
260
234
  await this.visuallyAnnotateElements({ containers });
261
- this.actionResult = await this.explorer.capturePageWithScreenshot();
235
+ this.actionResult = await this.explorer.capture({ screenshot: true });
262
236
  const visualResult = await this.analyzeScreenshotForVisualProps();
263
237
  if (visualResult.elements.size > 0) {
264
238
  await this.mergeVisualData(result, visualResult.elements);
@@ -334,8 +308,7 @@ export class Researcher extends ResearcherBase implements Agent {
334
308
  private async ensureNavigated(url: string, screenshot?: boolean): Promise<void> {
335
309
  if (!this.actionResult) {
336
310
  debugLog('No action result, navigating to URL');
337
- await this.explorer.visit(url);
338
- this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot });
311
+ this.actionResult = await this.explorer.visit(url, { screenshot });
339
312
  return;
340
313
  }
341
314
 
@@ -345,7 +318,7 @@ export class Researcher extends ResearcherBase implements Agent {
345
318
 
346
319
  if (!isEmpty && isOnCurrentState) {
347
320
  if ((!this.actionResult.screenshot && screenshot) || !this.actionResult.ariaSnapshot) {
348
- this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot });
321
+ this.actionResult = await this.explorer.capture({ screenshot });
349
322
  }
350
323
  return;
351
324
  }
@@ -353,8 +326,7 @@ export class Researcher extends ResearcherBase implements Agent {
353
326
  if (isEmpty && isOnCurrentState) {
354
327
  debugLog('HTML body empty on current URL, waiting for content');
355
328
  tag('step').log('Page body is empty, waiting for content...');
356
- await this.explorer.visit(url);
357
- this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot ?? false });
329
+ this.actionResult = await this.explorer.visit(url, { screenshot: screenshot ?? false });
358
330
  await this.waitUntilSettled(screenshot ?? false);
359
331
  return;
360
332
  }
@@ -362,22 +334,21 @@ export class Researcher extends ResearcherBase implements Agent {
362
334
  debugLog('Not on current state, navigating to URL');
363
335
  tag('step').log('Navigating to URL...');
364
336
 
365
- await this.explorer.visit(url);
366
- this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot ?? false });
337
+ this.actionResult = await this.explorer.visit(url, { screenshot: screenshot ?? false });
367
338
  }
368
339
 
369
340
  private async waitUntilSettled(screenshot: boolean): Promise<boolean> {
370
- const errorPageTimeout = (this.explorer.getConfig().ai?.agents?.researcher as any)?.errorPageTimeout ?? 10;
341
+ const errorPageTimeout = (this.config.ai?.agents?.researcher as any)?.errorPageTimeout ?? 10;
371
342
  if (errorPageTimeout <= 0) return false;
372
343
 
373
344
  const includeScreenshot = screenshot && this.provider.hasVision();
374
345
 
375
346
  try {
376
- await this.explorer.runWithBrowserRecovery('waitUntilSettled', () => this.explorer.playwrightHelper.page?.waitForLoadState('networkidle', { timeout: errorPageTimeout * 1000 }));
347
+ await this.explorer.withPage((page) => page.waitForLoadState('networkidle', { timeout: errorPageTimeout * 1000 }));
377
348
  } catch {}
378
349
 
379
- await this.explorer.annotateElements();
380
- this.actionResult = await this.explorer.capturePageState({ includeScreenshot });
350
+ await this.explorer.withPage(annotatePageElements);
351
+ this.actionResult = await this.explorer.capture({ screenshot: includeScreenshot });
381
352
 
382
353
  let condition = detectPageCondition(this.actionResult!);
383
354
  if (condition === 'error') {
@@ -387,8 +358,8 @@ export class Researcher extends ResearcherBase implements Agent {
387
358
 
388
359
  for (let i = 0; i < 3; i++) {
389
360
  await new Promise((r) => setTimeout(r, 1000));
390
- await this.explorer.annotateElements();
391
- this.actionResult = await this.explorer.capturePageState({ includeScreenshot });
361
+ await this.explorer.withPage(annotatePageElements);
362
+ this.actionResult = await this.explorer.capture({ screenshot: includeScreenshot });
392
363
  condition = detectPageCondition(this.actionResult!);
393
364
  if (condition === 'error') {
394
365
  throw new ErrorPageError(this.actionResult!.url, this.actionResult!.title, this.actionResult!.httpStatus);
@@ -400,7 +371,7 @@ export class Researcher extends ResearcherBase implements Agent {
400
371
  }
401
372
 
402
373
  private getConfiguredSections(): Record<string, string> {
403
- const configSections = (this.explorer.getConfig().ai?.agents?.researcher as any)?.sections as string[] | undefined;
374
+ const configSections = (this.config.ai?.agents?.researcher as any)?.sections as string[] | undefined;
404
375
  if (!configSections?.length) return POSSIBLE_SECTIONS;
405
376
  const filtered: Record<string, string> = {};
406
377
  for (const key of configSections) {
@@ -467,7 +438,7 @@ export class Researcher extends ResearcherBase implements Agent {
467
438
  if (!this.actionResult) throw new Error('actionResult is not set');
468
439
 
469
440
  const html = await this.actionResult.combinedHtml();
470
- const knowledge = this.explorer.getKnowledgeTracker().renderRelevantKnowledge(this.actionResult);
441
+ const knowledge = this.knowledgeTracker.renderRelevantKnowledge(this.actionResult);
471
442
 
472
443
  const ariaSnapshot = this.actionResult.getCompactARIA();
473
444
 
@@ -728,9 +699,9 @@ export class Researcher extends ResearcherBase implements Agent {
728
699
  async cancelInUi() {
729
700
  const beforeAria = this.stateManager.getCurrentState()?.ariaSnapshot || null;
730
701
 
731
- await this.explorer.executeAction('I.clickXY(0, 0)');
702
+ await this.explorer.action().execute('I.clickXY(0, 0)');
732
703
  if (diffAriaSnapshots(beforeAria, this.stateManager.getCurrentState()?.ariaSnapshot || null).text) return;
733
704
 
734
- await this.explorer.executeAction(`I.pressKey('Escape')`);
705
+ await this.explorer.action().execute(`I.pressKey('Escape')`);
735
706
  }
736
707
  }
@@ -1,6 +1,11 @@
1
1
  import type { ActionResult } from '../action-result.js';
2
+ import type { ExplorbotConfig } from '../config.ts';
2
3
  import type { ExperienceTracker } from '../experience-tracker.js';
4
+ import type Explorer from '../explorer.ts';
3
5
  import type { KnowledgeTracker } from '../knowledge-tracker.js';
6
+ import type { StateManager } from '../state-manager.ts';
7
+ import { HooksRunner } from '../utils/hooks-runner.ts';
8
+ import type { AgentDeps, ToolDeps } from './agent.ts';
4
9
  import { Historian } from './historian.js';
5
10
  import type { Navigator } from './navigator.js';
6
11
  import type { Provider } from './provider.js';
@@ -17,6 +22,12 @@ function createNullProxy<T extends object>(): T {
17
22
  }
18
23
 
19
24
  export abstract class TaskAgent {
25
+ explorer!: Explorer;
26
+ provider!: Provider;
27
+ config!: ExplorbotConfig;
28
+ stateManager!: StateManager;
29
+ knowledgeTracker!: KnowledgeTracker;
30
+ protected hooksRunner!: HooksRunner;
20
31
  protected consecutiveFailures = 0;
21
32
  protected consecutiveEmptyResults = 0;
22
33
  protected recentToolCalls: any[] = [];
@@ -25,10 +36,41 @@ export abstract class TaskAgent {
25
36
  private _historian: Historian | null = null;
26
37
  private _quartermaster: Quartermaster | null = null;
27
38
 
39
+ constructor(deps?: AgentDeps) {
40
+ if (!deps) return;
41
+ this.explorer = deps.explorer;
42
+ this.provider = deps.ai;
43
+ this.config = deps.config;
44
+ this.stateManager = deps.stateManager;
45
+ this.knowledgeTracker = deps.knowledgeTracker;
46
+ this.hooksRunner = new HooksRunner(deps.explorer, deps.config);
47
+ }
48
+
49
+ setHistorian(historian: Historian): void {
50
+ this._historian = historian;
51
+ }
52
+
53
+ setQuartermaster(quartermaster: Quartermaster): void {
54
+ this._quartermaster = quartermaster;
55
+ }
56
+
28
57
  protected abstract getNavigator(): Navigator;
29
- protected abstract getExperienceTracker(): ExperienceTracker;
30
- protected abstract getKnowledgeTracker(): KnowledgeTracker;
31
- protected abstract getProvider(): Provider;
58
+
59
+ protected get toolDeps(): ToolDeps {
60
+ return { explorer: this.explorer, stateManager: this.stateManager, ai: this.provider };
61
+ }
62
+
63
+ protected getExperienceTracker(): ExperienceTracker {
64
+ return this.stateManager.getExperienceTracker();
65
+ }
66
+
67
+ protected getKnowledgeTracker(): KnowledgeTracker {
68
+ return this.knowledgeTracker;
69
+ }
70
+
71
+ protected getProvider(): Provider {
72
+ return this.provider;
73
+ }
32
74
 
33
75
  protected getKnowledge(actionResult: ActionResult): string {
34
76
  return this.getKnowledgeTracker().renderRelevantKnowledge(actionResult);
@@ -38,19 +80,11 @@ export abstract class TaskAgent {
38
80
  return this.getExperienceTracker().renderExperienceTocFor(actionResult);
39
81
  }
40
82
 
41
- setHistorian(historian: Historian): void {
42
- this._historian = historian;
43
- }
44
-
45
83
  protected getHistorian(): Historian {
46
84
  if (this._historian) return this._historian;
47
85
  return createNullProxy<Historian>();
48
86
  }
49
87
 
50
- setQuartermaster(quartermaster: Quartermaster): void {
51
- this._quartermaster = quartermaster;
52
- }
53
-
54
88
  protected getQuartermaster(): Quartermaster {
55
89
  if (this._quartermaster) return this._quartermaster;
56
90
  return createNullProxy<Quartermaster>();