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.
- package/README.md +21 -5
- package/bin/explorbot-cli.ts +29 -7
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +8 -2
- package/boat/doc-collector/src/ai/tools.ts +29 -16
- package/boat/doc-collector/src/cli.ts +2 -6
- package/boat/doc-collector/src/config.ts +2 -0
- package/boat/doc-collector/src/docbot.ts +42 -14
- package/boat/doc-collector/src/interaction-screenshots.ts +160 -0
- package/boat/doc-collector/src/screenshots.ts +24 -16
- package/dist/bin/explorbot-cli.js +26 -6
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +7 -2
- package/dist/boat/doc-collector/src/ai/tools.js +21 -15
- package/dist/boat/doc-collector/src/cli.js +2 -5
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +39 -13
- package/dist/boat/doc-collector/src/interaction-screenshots.js +156 -0
- package/dist/boat/doc-collector/src/screenshots.js +25 -16
- package/dist/models.json +30 -0
- package/dist/package.json +8 -2
- package/dist/src/action.d.ts +11 -3
- package/dist/src/action.js +22 -5
- package/dist/src/ai/agent.d.ts +17 -0
- package/dist/src/ai/captain/idle-mode.js +1 -1
- package/dist/src/ai/captain/test-mode.js +1 -1
- package/dist/src/ai/captain/web-mode.js +18 -22
- package/dist/src/ai/captain.js +13 -26
- package/dist/src/ai/driller.d.ts +2 -13
- package/dist/src/ai/driller.js +23 -38
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.d.ts +3 -2
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -2
- package/dist/src/ai/historian.d.ts +5 -4
- package/dist/src/ai/navigator.d.ts +6 -2
- package/dist/src/ai/navigator.js +26 -22
- package/dist/src/ai/pilot.d.ts +13 -6
- package/dist/src/ai/pilot.js +49 -44
- package/dist/src/ai/planner.d.ts +3 -2
- package/dist/src/ai/planner.js +19 -17
- package/dist/src/ai/provider.js +0 -1
- package/dist/src/ai/quartermaster.d.ts +3 -2
- package/dist/src/ai/quartermaster.js +4 -4
- package/dist/src/ai/rerunner.d.ts +2 -11
- package/dist/src/ai/rerunner.js +9 -22
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
- package/dist/src/ai/researcher/deep-analysis.js +6 -6
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- package/dist/src/ai/researcher/sections.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +2 -2
- package/dist/src/ai/researcher.js +29 -54
- package/dist/src/ai/task-agent.d.ts +18 -5
- package/dist/src/ai/task-agent.js +35 -6
- package/dist/src/ai/tester.d.ts +6 -12
- package/dist/src/ai/tester.js +37 -51
- package/dist/src/ai/tools.d.ts +11 -10
- package/dist/src/ai/tools.js +24 -36
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +7 -5
- package/dist/src/commands/context-data-command.js +2 -2
- package/dist/src/commands/context-experience-command.js +2 -2
- package/dist/src/commands/context-html-command.js +2 -2
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.js +4 -3
- package/dist/src/commands/freesail-command.js +2 -2
- package/dist/src/commands/learn-command.js +4 -4
- package/dist/src/commands/path-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/test-command.js +1 -1
- package/dist/src/components/App.js +1 -1
- package/dist/src/config.d.ts +17 -0
- package/dist/src/config.js +144 -10
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.d.ts +17 -8
- package/dist/src/explorbot.js +74 -36
- package/dist/src/explorer.d.ts +67 -91
- package/dist/src/explorer.js +327 -567
- package/dist/src/state-manager.d.ts +6 -1
- package/dist/src/state-manager.js +1 -0
- package/dist/src/utils/hooks-runner.js +2 -4
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/web-annotate.d.ts +5 -0
- package/dist/src/utils/web-annotate.js +58 -0
- package/dist/src/utils/web-eidx.d.ts +2 -0
- package/dist/src/utils/web-eidx.js +20 -0
- package/dist/src/utils/web-element.d.ts +1 -0
- package/dist/src/utils/web-element.js +29 -0
- package/models.json +30 -0
- package/package.json +8 -2
- package/src/action.ts +25 -5
- package/src/ai/agent.ts +20 -0
- package/src/ai/captain/idle-mode.ts +1 -1
- package/src/ai/captain/test-mode.ts +1 -1
- package/src/ai/captain/web-mode.ts +18 -23
- package/src/ai/captain.ts +12 -25
- package/src/ai/driller.ts +50 -67
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +4 -3
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian.ts +3 -2
- package/src/ai/navigator.ts +30 -25
- package/src/ai/pilot.ts +55 -47
- package/src/ai/planner.ts +22 -20
- package/src/ai/provider.ts +0 -1
- package/src/ai/quartermaster.ts +5 -4
- package/src/ai/rerunner.ts +10 -29
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +8 -6
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/sections.ts +4 -2
- package/src/ai/researcher.ts +32 -61
- package/src/ai/task-agent.ts +45 -11
- package/src/ai/tester.ts +40 -57
- package/src/ai/tools.ts +32 -53
- package/src/command-handler.ts +1 -1
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +7 -5
- package/src/commands/context-data-command.ts +2 -2
- package/src/commands/context-experience-command.ts +2 -2
- package/src/commands/context-html-command.ts +2 -2
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/learn-command.ts +4 -4
- package/src/commands/path-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/test-command.ts +1 -1
- package/src/components/App.tsx +1 -1
- package/src/config.ts +173 -11
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +78 -39
- package/src/explorer.ts +388 -626
- package/src/state-manager.ts +7 -1
- package/src/utils/hooks-runner.ts +2 -4
- package/src/utils/test-files.ts +1 -1
- package/src/utils/web-annotate.ts +64 -0
- package/src/utils/web-eidx.ts +21 -0
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|
package/src/ai/researcher.ts
CHANGED
|
@@ -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
|
|
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(
|
|
64
|
-
super();
|
|
65
|
-
this.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
347
|
+
await this.explorer.withPage((page) => page.waitForLoadState('networkidle', { timeout: errorPageTimeout * 1000 }));
|
|
377
348
|
} catch {}
|
|
378
349
|
|
|
379
|
-
await this.explorer.
|
|
380
|
-
this.actionResult = await this.explorer.
|
|
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.
|
|
391
|
-
this.actionResult = await this.explorer.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
705
|
+
await this.explorer.action().execute(`I.pressKey('Escape')`);
|
|
735
706
|
}
|
|
736
707
|
}
|
package/src/ai/task-agent.ts
CHANGED
|
@@ -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
|
-
|
|
30
|
-
protected
|
|
31
|
-
|
|
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>();
|