explorbot 0.1.8 → 0.1.10
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/bin/explorbot-cli.ts +70 -8
- package/boat/api-tester/src/ai/curler-tools.ts +3 -3
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/apibot.ts +2 -2
- package/boat/api-tester/src/config.ts +1 -1
- package/dist/bin/explorbot-cli.js +70 -7
- package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
- package/dist/boat/api-tester/src/apibot.js +2 -2
- package/dist/package.json +1 -1
- package/dist/src/ai/bosun.js +5 -1
- package/dist/src/ai/experience-compactor.js +235 -50
- package/dist/src/ai/historian.js +13 -6
- package/dist/src/ai/navigator.js +62 -62
- package/dist/src/ai/pilot.js +22 -0
- package/dist/src/ai/planner/subpages.js +1 -30
- package/dist/src/ai/planner.js +4 -4
- package/dist/src/ai/provider.js +1 -1
- package/dist/src/ai/rerunner.js +3 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/sections.js +8 -1
- package/dist/src/ai/researcher.js +4 -11
- package/dist/src/ai/tools.js +5 -3
- package/dist/src/api/request-store.js +20 -0
- package/dist/src/api/xhr-capture.js +19 -3
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/add-rule-command.js +1 -1
- package/dist/src/commands/base-command.js +20 -0
- package/dist/src/commands/clean-command.js +1 -1
- package/dist/src/commands/compact-command.js +138 -0
- package/dist/src/commands/context-command.js +7 -1
- package/dist/src/commands/drill-command.js +4 -1
- package/dist/src/commands/experience-command.js +104 -0
- package/dist/src/commands/explore-command.js +33 -7
- package/dist/src/commands/freesail-command.js +2 -0
- package/dist/src/commands/index.js +7 -3
- package/dist/src/commands/init-command.js +2 -2
- package/dist/src/commands/learn-command.js +1 -1
- package/dist/src/commands/navigate-command.js +4 -1
- package/dist/src/commands/plan-clear-command.js +4 -1
- package/dist/src/commands/plan-command.js +11 -4
- package/dist/src/commands/plan-edit-command.js +1 -1
- package/dist/src/commands/plan-load-command.js +4 -1
- package/dist/src/commands/plan-reload-command.js +4 -1
- package/dist/src/commands/plan-save-command.js +1 -1
- package/dist/src/commands/research-command.js +5 -2
- package/dist/src/commands/start-command.js +5 -1
- package/dist/src/commands/test-command.js +7 -1
- package/dist/src/experience-tracker.js +191 -56
- package/dist/src/explorbot.js +26 -14
- package/dist/src/explorer.js +3 -3
- package/dist/src/reporter.js +17 -2
- package/dist/src/stats.js +2 -0
- package/dist/src/suite.js +1 -1
- package/dist/src/utils/error-page.js +10 -0
- package/dist/src/utils/logger.js +1 -1
- package/dist/src/utils/rules-loader.js +1 -1
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/url-matcher.js +50 -0
- package/package.json +1 -1
- package/src/ai/bosun.ts +5 -1
- package/src/ai/experience-compactor.ts +270 -63
- package/src/ai/historian.ts +12 -7
- package/src/ai/navigator.ts +68 -66
- package/src/ai/pilot.ts +22 -0
- package/src/ai/planner/subpages.ts +1 -24
- package/src/ai/planner.ts +5 -5
- package/src/ai/provider.ts +1 -1
- package/src/ai/rerunner.ts +3 -3
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +1 -1
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/sections.ts +7 -1
- package/src/ai/researcher.ts +4 -11
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tools.ts +6 -4
- package/src/api/request-store.ts +22 -0
- package/src/api/xhr-capture.ts +21 -3
- package/src/command-handler.ts +1 -1
- package/src/commands/add-rule-command.ts +2 -2
- package/src/commands/base-command.ts +26 -1
- package/src/commands/clean-command.ts +2 -2
- package/src/commands/compact-command.ts +156 -0
- package/src/commands/context-command.ts +8 -2
- package/src/commands/drill-command.ts +5 -2
- package/src/commands/experience-command.ts +125 -0
- package/src/commands/explore-command.ts +35 -9
- package/src/commands/freesail-command.ts +2 -0
- package/src/commands/index.ts +7 -3
- package/src/commands/init-command.ts +2 -2
- package/src/commands/learn-command.ts +2 -2
- package/src/commands/navigate-command.ts +5 -2
- package/src/commands/plan-clear-command.ts +5 -2
- package/src/commands/plan-command.ts +12 -5
- package/src/commands/plan-edit-command.ts +2 -2
- package/src/commands/plan-load-command.ts +5 -2
- package/src/commands/plan-reload-command.ts +5 -2
- package/src/commands/plan-save-command.ts +2 -2
- package/src/commands/research-command.ts +6 -3
- package/src/commands/start-command.ts +6 -2
- package/src/commands/test-command.ts +8 -2
- package/src/experience-tracker.ts +220 -71
- package/src/explorbot.ts +28 -15
- package/src/explorer.ts +3 -3
- package/src/reporter.ts +17 -3
- package/src/stats.ts +4 -0
- package/src/suite.ts +1 -1
- package/src/utils/error-page.ts +10 -0
- package/src/utils/logger.ts +1 -1
- package/src/utils/rules-loader.ts +1 -1
- package/src/utils/test-files.ts +1 -1
- package/src/utils/url-matcher.ts +43 -0
package/src/ai/navigator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../action-result.js';
|
|
3
|
-
import
|
|
3
|
+
import type Action from '../action.ts';
|
|
4
|
+
import { ExperienceTracker, renderExperienceToc } from '../experience-tracker.js';
|
|
4
5
|
import Explorer from '../explorer.ts';
|
|
5
6
|
import { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
6
7
|
import { type WebPageState, normalizeUrl } from '../state-manager.js';
|
|
@@ -8,14 +9,15 @@ import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
|
8
9
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
9
10
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
10
11
|
import { loop, pause } from '../utils/loop.js';
|
|
12
|
+
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
11
13
|
import type { Agent } from './agent.js';
|
|
12
14
|
import type { Conversation } from './conversation.js';
|
|
13
15
|
import { ExperienceCompactor } from './experience-compactor.js';
|
|
14
16
|
import type { Provider } from './provider.js';
|
|
15
17
|
import { Researcher } from './researcher.ts';
|
|
16
18
|
import { actionRule, locatorRule } from './rules.js';
|
|
17
|
-
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
18
19
|
import { isInteractive } from './task-agent.js';
|
|
20
|
+
import { createAgentTools } from './tools.ts';
|
|
19
21
|
|
|
20
22
|
const debugLog = createDebug('explorbot:navigator');
|
|
21
23
|
|
|
@@ -25,8 +27,6 @@ class Navigator implements Agent {
|
|
|
25
27
|
private experienceCompactor: ExperienceCompactor;
|
|
26
28
|
private knowledgeTracker: KnowledgeTracker;
|
|
27
29
|
private experienceTracker: ExperienceTracker;
|
|
28
|
-
private currentAction: any = null;
|
|
29
|
-
private currentUrl: string | null = null;
|
|
30
30
|
private hooksRunner: HooksRunner;
|
|
31
31
|
|
|
32
32
|
private MAX_ATTEMPTS = Number.parseInt(process.env.MAX_ATTEMPTS || '5');
|
|
@@ -102,9 +102,7 @@ class Navigator implements Agent {
|
|
|
102
102
|
const actionResult = action.actionResult || ActionResult.fromState(action.stateManager.getCurrentState()!);
|
|
103
103
|
const originalMessage = `Navigate to: ${url}. Current page: ${actualPath}`;
|
|
104
104
|
|
|
105
|
-
this.
|
|
106
|
-
this.currentUrl = url;
|
|
107
|
-
const resolved = await this.resolveState(originalMessage, actionResult);
|
|
105
|
+
const resolved = await this.resolveState(originalMessage, actionResult, { action, expectedUrl: url });
|
|
108
106
|
if (!resolved) {
|
|
109
107
|
throw new Error(`Navigation to ${url} failed: redirected to ${actualPath} and could not resolve`);
|
|
110
108
|
}
|
|
@@ -116,9 +114,7 @@ class Navigator implements Agent {
|
|
|
116
114
|
But I got error: ${action.lastError?.message || 'Navigation failed'}.
|
|
117
115
|
`.trim();
|
|
118
116
|
|
|
119
|
-
this.
|
|
120
|
-
this.currentUrl = url;
|
|
121
|
-
const resolved = await this.resolveState(originalMessage, actionResult);
|
|
117
|
+
const resolved = await this.resolveState(originalMessage, actionResult, { action, expectedUrl: url });
|
|
122
118
|
if (!resolved) {
|
|
123
119
|
throw new Error(`Navigation to ${url} failed: ${action.lastError?.message}`);
|
|
124
120
|
}
|
|
@@ -136,10 +132,13 @@ class Navigator implements Agent {
|
|
|
136
132
|
}
|
|
137
133
|
}
|
|
138
134
|
|
|
139
|
-
async resolveState(message: string, actionResult: ActionResult): Promise<boolean> {
|
|
135
|
+
async resolveState(message: string, actionResult: ActionResult, opts?: { action?: Action; expectedUrl?: string }): Promise<boolean> {
|
|
140
136
|
tag('info').log('AI Navigator resolving state at', actionResult.url);
|
|
141
137
|
debugLog('Resolution message:', message);
|
|
142
138
|
|
|
139
|
+
const action = opts?.action ?? this.explorer.createAction();
|
|
140
|
+
const expectedUrl = opts?.expectedUrl;
|
|
141
|
+
|
|
143
142
|
let knowledge = '';
|
|
144
143
|
let experience = '';
|
|
145
144
|
|
|
@@ -153,26 +152,13 @@ class Navigator implements Agent {
|
|
|
153
152
|
</hint>`;
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
experience = dedent`
|
|
164
|
-
<experience>
|
|
165
|
-
Here is the experience of interacting with the page.
|
|
166
|
-
Learn from it AND DO NOT REPEAT THE SAME MISTAKES.
|
|
167
|
-
If there was found successful solution to an issue, propose it as a first solution.
|
|
168
|
-
If there are no successful solutions, analyze failed intentions and actions to avoid them.
|
|
169
|
-
Do not try again same failed solutions
|
|
170
|
-
|
|
171
|
-
Focus on successful solutions and avoid actions and locators that caused errors in past.
|
|
172
|
-
|
|
173
|
-
${experienceContent}
|
|
174
|
-
|
|
175
|
-
</experience>`;
|
|
155
|
+
if (!actionResult.isInsideIframe) {
|
|
156
|
+
const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
|
|
157
|
+
if (toc.length > 0) {
|
|
158
|
+
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
159
|
+
tag('substep').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
|
|
160
|
+
experience = renderExperienceToc(toc);
|
|
161
|
+
}
|
|
176
162
|
}
|
|
177
163
|
|
|
178
164
|
const prompt = dedent`
|
|
@@ -210,6 +196,8 @@ class Navigator implements Agent {
|
|
|
210
196
|
const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
|
|
211
197
|
conversation.addUserText(prompt);
|
|
212
198
|
|
|
199
|
+
const tools = this.buildExperienceTools();
|
|
200
|
+
|
|
213
201
|
let codeBlocks: string[] = [];
|
|
214
202
|
let htmlContextAdded = false;
|
|
215
203
|
let codeBlockIndex = 0;
|
|
@@ -219,11 +207,11 @@ class Navigator implements Agent {
|
|
|
219
207
|
await loop(
|
|
220
208
|
async ({ stop }) => {
|
|
221
209
|
if (codeBlocks.length === 0) {
|
|
222
|
-
const result = await this.provider.invokeConversation(conversation);
|
|
210
|
+
const result = await this.provider.invokeConversation(conversation, tools);
|
|
223
211
|
if (!result) return;
|
|
224
212
|
const aiResponse = result?.response?.text;
|
|
225
213
|
debugLog('AI:', aiResponse?.split('\n')[0]);
|
|
226
|
-
debugLog('Received AI response:', aiResponse
|
|
214
|
+
debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
|
|
227
215
|
codeBlocks = extractCodeBlocks(aiResponse ?? '');
|
|
228
216
|
codeBlockIndex = 0;
|
|
229
217
|
}
|
|
@@ -256,24 +244,26 @@ class Navigator implements Agent {
|
|
|
256
244
|
codeBlockIndex++;
|
|
257
245
|
totalAttempts++;
|
|
258
246
|
|
|
247
|
+
await this.explorer.switchToMainFrame();
|
|
248
|
+
|
|
259
249
|
debugLog(`Attempting resolution: ${codeBlock}`);
|
|
260
|
-
resolved = await
|
|
250
|
+
resolved = await action.attempt(codeBlock, message);
|
|
261
251
|
|
|
262
|
-
if (
|
|
263
|
-
await
|
|
264
|
-
const freshState = await
|
|
252
|
+
if (expectedUrl) {
|
|
253
|
+
await (action.getActor() as any).wait(2);
|
|
254
|
+
const freshState = await action.capturePageState();
|
|
265
255
|
|
|
266
|
-
if (normalizeUrl(freshState.url || '') === normalizeUrl(
|
|
256
|
+
if (normalizeUrl(freshState.url || '') === normalizeUrl(expectedUrl)) {
|
|
267
257
|
resolved = true;
|
|
268
258
|
} else if (resolved) {
|
|
269
|
-
tag('warning').log(`URL verification failed: expected ${
|
|
259
|
+
tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${freshState.url}`);
|
|
270
260
|
resolved = false;
|
|
271
261
|
}
|
|
272
262
|
}
|
|
273
263
|
|
|
274
264
|
if (resolved) {
|
|
275
265
|
tag('success').log('Navigation resolved successfully');
|
|
276
|
-
|
|
266
|
+
this.experienceTracker.writeAction(actionResult, { title: message, code: codeBlock });
|
|
277
267
|
stop();
|
|
278
268
|
return;
|
|
279
269
|
}
|
|
@@ -290,9 +280,9 @@ class Navigator implements Agent {
|
|
|
290
280
|
}
|
|
291
281
|
);
|
|
292
282
|
|
|
293
|
-
if (!resolved &&
|
|
294
|
-
await
|
|
295
|
-
if (this.isOnExpectedPage(
|
|
283
|
+
if (!resolved && expectedUrl) {
|
|
284
|
+
await (action.getActor() as any).wait(1);
|
|
285
|
+
if (this.isOnExpectedPage(expectedUrl, action.stateManager)) {
|
|
296
286
|
resolved = true;
|
|
297
287
|
tag('success').log('Navigation resolved after delayed redirect');
|
|
298
288
|
}
|
|
@@ -303,13 +293,13 @@ class Navigator implements Agent {
|
|
|
303
293
|
}
|
|
304
294
|
|
|
305
295
|
if (!resolved && isInteractive()) {
|
|
306
|
-
const userInput = await pause(`Navigator failed to resolve. Current: ${
|
|
296
|
+
const userInput = await pause(`Navigator failed to resolve. Current: ${action.stateManager.getCurrentState()?.url}\n` + `Target: ${expectedUrl ?? '(none)'}\nEnter CodeceptJS commands (or press Enter to skip):`);
|
|
307
297
|
|
|
308
298
|
if (userInput?.trim()) {
|
|
309
|
-
resolved = await
|
|
310
|
-
if (resolved &&
|
|
311
|
-
await
|
|
312
|
-
if (!this.isOnExpectedPage(
|
|
299
|
+
resolved = await action.attempt(userInput, message);
|
|
300
|
+
if (resolved && expectedUrl) {
|
|
301
|
+
await (action.getActor() as any).wait(1);
|
|
302
|
+
if (!this.isOnExpectedPage(expectedUrl, action.stateManager)) {
|
|
313
303
|
resolved = false;
|
|
314
304
|
}
|
|
315
305
|
}
|
|
@@ -319,6 +309,23 @@ class Navigator implements Agent {
|
|
|
319
309
|
return resolved;
|
|
320
310
|
}
|
|
321
311
|
|
|
312
|
+
private buildExperienceTools(): { learn_experience: unknown } | undefined {
|
|
313
|
+
const stateManager = this.explorer.getStateManager();
|
|
314
|
+
const getState = () => {
|
|
315
|
+
const s = stateManager.getCurrentState();
|
|
316
|
+
return s ? ActionResult.fromState(s) : null;
|
|
317
|
+
};
|
|
318
|
+
const { learn_experience } = createAgentTools({
|
|
319
|
+
explorer: this.explorer,
|
|
320
|
+
researcher: null as unknown as Researcher,
|
|
321
|
+
navigator: this,
|
|
322
|
+
experienceTracker: this.experienceTracker,
|
|
323
|
+
getState,
|
|
324
|
+
});
|
|
325
|
+
if (!learn_experience) return undefined;
|
|
326
|
+
return { learn_experience };
|
|
327
|
+
}
|
|
328
|
+
|
|
322
329
|
async freeSail(opts?: { strategy?: 'deep' | 'shallow'; scope?: string; visitedUrls?: Set<string> }, actionResult?: ActionResult): Promise<{ target: string; reason: string } | null> {
|
|
323
330
|
const stateManager = this.explorer.getStateManager();
|
|
324
331
|
const state = stateManager.getCurrentState();
|
|
@@ -465,22 +472,13 @@ class Navigator implements Agent {
|
|
|
465
472
|
</hint>`;
|
|
466
473
|
}
|
|
467
474
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
experience = dedent`
|
|
476
|
-
<experience>
|
|
477
|
-
Here is the experience of interacting with the page.
|
|
478
|
-
Learn from it AND DO NOT REPEAT THE SAME MISTAKES.
|
|
479
|
-
If there was found successful solution to an issue, propose it as a first solution.
|
|
480
|
-
|
|
481
|
-
${experienceContent}
|
|
482
|
-
|
|
483
|
-
</experience>`;
|
|
475
|
+
if (!actionResult.isInsideIframe) {
|
|
476
|
+
const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
|
|
477
|
+
if (toc.length > 0) {
|
|
478
|
+
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
479
|
+
tag('substep').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
|
|
480
|
+
experience = renderExperienceToc(toc);
|
|
481
|
+
}
|
|
484
482
|
}
|
|
485
483
|
|
|
486
484
|
const prompt = dedent`
|
|
@@ -522,6 +520,8 @@ class Navigator implements Agent {
|
|
|
522
520
|
const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
|
|
523
521
|
conversation.addUserText(prompt);
|
|
524
522
|
|
|
523
|
+
const tools = this.buildExperienceTools();
|
|
524
|
+
|
|
525
525
|
let codeBlocks: string[] = [];
|
|
526
526
|
const successfulCodes: string[] = [];
|
|
527
527
|
|
|
@@ -530,10 +530,10 @@ class Navigator implements Agent {
|
|
|
530
530
|
await loop(
|
|
531
531
|
async ({ stop, iteration }) => {
|
|
532
532
|
if (codeBlocks.length === 0) {
|
|
533
|
-
const result = await this.provider.invokeConversation(conversation);
|
|
533
|
+
const result = await this.provider.invokeConversation(conversation, tools);
|
|
534
534
|
if (!result) return;
|
|
535
535
|
const aiResponse = result?.response?.text;
|
|
536
|
-
debugLog('Received AI response:', aiResponse
|
|
536
|
+
debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
|
|
537
537
|
tag('step').log('Verifying assertion...');
|
|
538
538
|
codeBlocks = extractCodeBlocks(aiResponse ?? '');
|
|
539
539
|
}
|
|
@@ -548,6 +548,8 @@ class Navigator implements Agent {
|
|
|
548
548
|
return;
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
await this.explorer.switchToMainFrame();
|
|
552
|
+
|
|
551
553
|
const verified = await action.attempt(codeBlock, message, false);
|
|
552
554
|
|
|
553
555
|
if (verified) {
|
package/src/ai/pilot.ts
CHANGED
|
@@ -487,6 +487,28 @@ export class Pilot implements Agent {
|
|
|
487
487
|
lines.push(`verifications: ${verifyLines.join(', ')}`);
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
const consoleErrors = (state.browserLogs ?? []).filter((l: any) => (l.type || l.level) === 'error');
|
|
491
|
+
if (consoleErrors.length > 0) {
|
|
492
|
+
const sample = consoleErrors
|
|
493
|
+
.slice(0, 3)
|
|
494
|
+
.map((e: any) => e.text || e.message || String(e))
|
|
495
|
+
.join(' | ');
|
|
496
|
+
lines.push(`console errors: ${consoleErrors.length} (${sample})`);
|
|
497
|
+
} else {
|
|
498
|
+
lines.push('console errors: none');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const failedRequests = this.explorer.getRequestStore()?.getFailedRequests() ?? [];
|
|
502
|
+
if (failedRequests.length > 0) {
|
|
503
|
+
const sample = failedRequests
|
|
504
|
+
.slice(-5)
|
|
505
|
+
.map((r) => `${r.method} ${r.path} → ${r.status}`)
|
|
506
|
+
.join(', ');
|
|
507
|
+
lines.push(`network errors: ${sample}`);
|
|
508
|
+
} else {
|
|
509
|
+
lines.push('network errors: none');
|
|
510
|
+
}
|
|
511
|
+
|
|
490
512
|
const interactiveNodes = collectInteractiveNodes(state.ariaSnapshot);
|
|
491
513
|
const disabledButtons = interactiveNodes.filter((n) => n.role === 'button' && n.disabled === true && n.name).map((n) => n.name);
|
|
492
514
|
lines.push(`disabled buttons: ${disabledButtons.length > 0 ? disabledButtons.join(', ') : 'none'}`);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { ConfigParser } from '../../config.ts';
|
|
4
3
|
import { normalizeUrl } from '../../state-manager.ts';
|
|
5
4
|
import type { StateManager } from '../../state-manager.ts';
|
|
6
5
|
import type { Plan } from '../../test-plan.ts';
|
|
7
6
|
import { tag } from '../../utils/logger.ts';
|
|
7
|
+
import { isDynamicSegment } from '../../utils/url-matcher.ts';
|
|
8
8
|
import type { Provider } from '../provider.ts';
|
|
9
9
|
import type { Constructor } from '../researcher/mixin.ts';
|
|
10
10
|
|
|
@@ -38,29 +38,6 @@ function buildKey(url: string, feature?: string): string {
|
|
|
38
38
|
return normalized;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export function isDynamicSegment(segment: string): boolean {
|
|
42
|
-
try {
|
|
43
|
-
const configRegex = ConfigParser.getInstance().getConfig().dynamicPageRegex;
|
|
44
|
-
if (configRegex) return new RegExp(configRegex, 'i').test(segment);
|
|
45
|
-
} catch {
|
|
46
|
-
/* config not loaded yet */
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// numeric: /users/123
|
|
50
|
-
if (/^\d+$/.test(segment)) return true;
|
|
51
|
-
// UUID: /items/550e8400-e29b-41d4-a716-446655440000
|
|
52
|
-
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment)) return true;
|
|
53
|
-
// ULID: /items/01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
54
|
-
if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment)) return true;
|
|
55
|
-
// hex ID (4+ chars): /suite/70dae98a
|
|
56
|
-
if (/^[a-f0-9]{4,}$/i.test(segment)) return true;
|
|
57
|
-
// hex-prefixed slug (8+ hex before dash): /suite/95ef0c94-mobile
|
|
58
|
-
if (/^[a-f0-9]{8,}-/i.test(segment)) return true;
|
|
59
|
-
// short mixed alphanumeric (digits + letters, ≤8 chars, no dash): /item/x7f2
|
|
60
|
-
if (segment.length <= 8 && !segment.includes('-') && /\d/.test(segment) && /[a-z]/i.test(segment)) return true;
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
41
|
export function isTemplateMatch(urlA: string, urlB: string): boolean {
|
|
65
42
|
const partsA = normalizeUrl(urlA).split('/');
|
|
66
43
|
const partsB = normalizeUrl(urlB).split('/');
|
package/src/ai/planner.ts
CHANGED
|
@@ -8,22 +8,22 @@ import type Explorer from '../explorer.ts';
|
|
|
8
8
|
import { Observability } from '../observability.ts';
|
|
9
9
|
import type { StateManager } from '../state-manager.js';
|
|
10
10
|
import { Stats } from '../stats.ts';
|
|
11
|
+
import { Suite } from '../suite.ts';
|
|
11
12
|
import { Plan, Test } from '../test-plan.ts';
|
|
12
|
-
import { planToCompactAiContext } from '../utils/test-plan-markdown.ts';
|
|
13
13
|
import { createDebug, tag } from '../utils/logger.js';
|
|
14
14
|
import { jsonToTable } from '../utils/markdown-parser.ts';
|
|
15
15
|
import { mdq } from '../utils/markdown-query.js';
|
|
16
|
+
import { planToCompactAiContext } from '../utils/test-plan-markdown.ts';
|
|
16
17
|
import type { Agent } from './agent.js';
|
|
17
18
|
import { Conversation } from './conversation.ts';
|
|
18
19
|
import type { Fisherman } from './fisherman.ts';
|
|
19
|
-
import { getActiveStyle, getStyles } from './planner/styles.ts';
|
|
20
20
|
import { WithSessionDedup } from './planner/session-dedup.ts';
|
|
21
|
+
import { getActiveStyle, getStyles } from './planner/styles.ts';
|
|
21
22
|
import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from './planner/subpages.ts';
|
|
22
|
-
import { findSimilarStateHash } from './researcher/cache.ts';
|
|
23
23
|
import type { Provider } from './provider.js';
|
|
24
|
-
import { hasFocusedSection } from './researcher/focus.ts';
|
|
25
24
|
import { POSSIBLE_SECTIONS, Researcher } from './researcher.ts';
|
|
26
|
-
import {
|
|
25
|
+
import { findSimilarStateHash } from './researcher/cache.ts';
|
|
26
|
+
import { hasFocusedSection } from './researcher/focus.ts';
|
|
27
27
|
import { fileUploadRule, protectionRule } from './rules.ts';
|
|
28
28
|
|
|
29
29
|
const debugLog = createDebug('explorbot:planner');
|
package/src/ai/provider.ts
CHANGED
|
@@ -4,12 +4,12 @@ import { generateObject, generateText } from 'ai';
|
|
|
4
4
|
import type { ModelMessage } from 'ai';
|
|
5
5
|
import { clearActivity, setActivity } from '../activity.ts';
|
|
6
6
|
import type { AIConfig } from '../config.js';
|
|
7
|
-
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
8
7
|
import { executionController } from '../execution-controller.ts';
|
|
9
8
|
import { Observability } from '../observability.ts';
|
|
10
9
|
import { Stats } from '../stats.ts';
|
|
11
10
|
import { createDebug, tag } from '../utils/logger.js';
|
|
12
11
|
import { type RetryOptions, withRetry } from '../utils/retry.js';
|
|
12
|
+
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
13
13
|
import { Conversation } from './conversation.js';
|
|
14
14
|
|
|
15
15
|
const debugLog = createDebug('explorbot:provider');
|
package/src/ai/rerunner.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { highlight } from 'cli-highlight';
|
|
|
7
7
|
import * as codeceptjs from 'codeceptjs';
|
|
8
8
|
import heal from 'codeceptjs/lib/heal';
|
|
9
9
|
import aiTracePlugin from 'codeceptjs/lib/plugin/aiTrace';
|
|
10
|
-
import figureSet from 'figures';
|
|
11
10
|
import dedent from 'dedent';
|
|
11
|
+
import figureSet from 'figures';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { ActionResult } from '../action-result.ts';
|
|
14
14
|
import { setActivity } from '../activity.ts';
|
|
@@ -19,14 +19,14 @@ import { Stats } from '../stats.ts';
|
|
|
19
19
|
import { Task, Test, TestResult } from '../test-plan.ts';
|
|
20
20
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
21
21
|
import { loop } from '../utils/loop.ts';
|
|
22
|
+
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
22
23
|
import { loadTestSuites, printTestList } from '../utils/test-files.ts';
|
|
23
24
|
import type { Agent } from './agent.ts';
|
|
24
25
|
import { toolExecutionLabel } from './conversation.ts';
|
|
25
26
|
import type { Navigator } from './navigator.ts';
|
|
26
27
|
import { Provider } from './provider.ts';
|
|
27
|
-
import {
|
|
28
|
+
import { actionRule, locatorRule, sectionContextRule } from './rules.ts';
|
|
28
29
|
import { TaskAgent } from './task-agent.ts';
|
|
29
|
-
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
30
30
|
import { createCodeceptJSTools } from './tools.ts';
|
|
31
31
|
|
|
32
32
|
const debugLog = createDebug('explorbot:rerunner');
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult, type Diff } from '../../action-result.js';
|
|
3
|
+
import { executionController } from '../../execution-controller.ts';
|
|
3
4
|
import type Explorer from '../../explorer.ts';
|
|
4
5
|
import type { StateManager } from '../../state-manager.js';
|
|
5
6
|
import { WebPageState } from '../../state-manager.js';
|
|
6
7
|
import { detectFocusArea, diffAriaSnapshots } from '../../utils/aria.ts';
|
|
7
|
-
import { executionController } from '../../execution-controller.ts';
|
|
8
8
|
import { tag } from '../../utils/logger.js';
|
|
9
9
|
import { mdq } from '../../utils/markdown-query.ts';
|
|
10
10
|
import type { Provider } from '../provider.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parentPort } from 'node:worker_threads';
|
|
4
4
|
import { computeHtmlFingerprint } from '../../utils/html-diff.ts';
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import type { ActionResult } from '../../action-result.js';
|
|
3
|
-
import type Explorer from '../../explorer.ts';
|
|
4
3
|
import { executionController } from '../../execution-controller.ts';
|
|
4
|
+
import type Explorer from '../../explorer.ts';
|
|
5
5
|
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 { FOCUSED_MARKER } from './focus.ts';
|
|
10
9
|
import type { Conversation } from '../conversation.ts';
|
|
11
10
|
import type { Provider } from '../provider.js';
|
|
12
11
|
import { locatorRule as generalLocatorRuleText } from '../rules.js';
|
|
12
|
+
import { FOCUSED_MARKER } from './focus.ts';
|
|
13
13
|
import { type Constructor, debugLog } from './mixin.ts';
|
|
14
14
|
import { parseResearchSections } from './parser.ts';
|
|
15
15
|
import type { ResearchResult } from './research-result.ts';
|
|
@@ -37,7 +37,13 @@ export function WithSections<T extends Constructor>(Base: T) {
|
|
|
37
37
|
const parts: string[] = [];
|
|
38
38
|
for (const [name, description] of targets) {
|
|
39
39
|
if (executionController.isInterrupted()) break;
|
|
40
|
-
|
|
40
|
+
let text = '';
|
|
41
|
+
try {
|
|
42
|
+
text = await this._researchSingleSection(name, description, ariaSnapshot, focusCss);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
tag('warning').log(`Section "${name}" research failed, skipping: ${err instanceof Error ? err.message : err}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
41
47
|
if (!text) continue;
|
|
42
48
|
const trimmed = text.trim();
|
|
43
49
|
if (trimmed === 'NOT_PRESENT' || trimmed.startsWith('NOT_PRESENT')) continue;
|
package/src/ai/researcher.ts
CHANGED
|
@@ -3,6 +3,7 @@ import dedent from 'dedent';
|
|
|
3
3
|
import { ActionResult } from '../action-result.js';
|
|
4
4
|
import { setActivity } from '../activity.ts';
|
|
5
5
|
import { ConfigParser, outputPath } from '../config.ts';
|
|
6
|
+
import { executionController } from '../execution-controller.ts';
|
|
6
7
|
import type { ExperienceTracker } from '../experience-tracker.ts';
|
|
7
8
|
import type Explorer from '../explorer.ts';
|
|
8
9
|
import type { KnowledgeTracker } from '../knowledge-tracker.ts';
|
|
@@ -11,13 +12,13 @@ import type { StateManager } from '../state-manager.js';
|
|
|
11
12
|
import { WebPageState } from '../state-manager.js';
|
|
12
13
|
import { Stats } from '../stats.ts';
|
|
13
14
|
import { diffAriaSnapshots } from '../utils/aria.ts';
|
|
14
|
-
import { isErrorPage } from '../utils/error-page.ts';
|
|
15
|
+
import { ErrorPageError, isErrorPage } from '../utils/error-page.ts';
|
|
15
16
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
16
17
|
import { isBodyEmpty } from '../utils/html.ts';
|
|
17
18
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
18
19
|
import { mdq } from '../utils/markdown-query.ts';
|
|
19
20
|
import { withRetry } from '../utils/retry.ts';
|
|
20
|
-
import {
|
|
21
|
+
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
21
22
|
import type { Agent } from './agent.js';
|
|
22
23
|
import type { Navigator } from './navigator.ts';
|
|
23
24
|
import { ContextLengthError, type Provider } from './provider.js';
|
|
@@ -30,7 +31,6 @@ import { extractValidContainers, formatResearchSummary, parseResearchSections }
|
|
|
30
31
|
import { ResearchResult } from './researcher/research-result.ts';
|
|
31
32
|
import { type SectionMethods, WithSections } from './researcher/sections.ts';
|
|
32
33
|
import { locatorRule as generalLocatorRuleText } from './rules.js';
|
|
33
|
-
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
34
34
|
import { TaskAgent } from './task-agent.ts';
|
|
35
35
|
|
|
36
36
|
export type { Locator } from './researcher/locators.ts';
|
|
@@ -136,14 +136,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
136
136
|
const recovered = await this.waitForPageLoad(screenshot);
|
|
137
137
|
if (!recovered) {
|
|
138
138
|
tag('warning').log(`Detected error page at ${state.url}`);
|
|
139
|
-
|
|
140
|
-
## Error Page Detected
|
|
141
|
-
|
|
142
|
-
URL: ${state.url}
|
|
143
|
-
Title: ${this.actionResult!.title || 'N/A'}
|
|
144
|
-
|
|
145
|
-
Research skipped. Navigate to a valid page to continue.
|
|
146
|
-
`;
|
|
139
|
+
throw new ErrorPageError(state.url, this.actionResult!.title);
|
|
147
140
|
}
|
|
148
141
|
}
|
|
149
142
|
|
package/src/ai/task-agent.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import type { ActionResult } from '../action-result.js';
|
|
3
|
-
import {
|
|
3
|
+
import { type ExperienceTracker, renderExperienceToc } from '../experience-tracker.js';
|
|
4
4
|
import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
5
5
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
6
6
|
|
package/src/ai/tools.ts
CHANGED
|
@@ -10,9 +10,9 @@ import { createDebug, tag } from '../utils/logger.js';
|
|
|
10
10
|
import { pause } from '../utils/loop.js';
|
|
11
11
|
import { WebElement } from '../utils/web-element.ts';
|
|
12
12
|
import { Navigator } from './navigator.ts';
|
|
13
|
+
import type { AIProvider } from './provider.ts';
|
|
13
14
|
import { Researcher } from './researcher.ts';
|
|
14
15
|
import { sectionContextRule } from './rules.ts';
|
|
15
|
-
import type { AIProvider } from './provider.ts';
|
|
16
16
|
import { isInteractive } from './task-agent.ts';
|
|
17
17
|
|
|
18
18
|
const debugLog = createDebug('explorbot:tools');
|
|
@@ -289,14 +289,16 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
|
|
|
289
289
|
form: tool({
|
|
290
290
|
description: dedent`
|
|
291
291
|
Execute raw CodeceptJS code block with multiple commands.
|
|
292
|
-
USE THIS TOOL for
|
|
292
|
+
USE THIS TOOL for typing text into fields: I.fillField, I.type
|
|
293
293
|
|
|
294
294
|
Follow <actions> from system prompt for available commands.
|
|
295
295
|
Follow <locator_priority> from system prompt for locator selection.
|
|
296
296
|
|
|
297
|
+
I.type(text) types the literal characters of its argument into the focused element.
|
|
298
|
+
To press key combination or special keys (Ctrl, Meta, Esc) use I.pressKey instead.
|
|
299
|
+
|
|
297
300
|
Use cases:
|
|
298
301
|
- Typing into input fields (I.fillField, I.type)
|
|
299
|
-
- Pressing keyboard keys (I.pressKey)
|
|
300
302
|
- Working with iframes (switch context with I.switchTo)
|
|
301
303
|
- Performing multiple form actions in a single batch
|
|
302
304
|
- Complex interactions requiring sequential commands
|
|
@@ -957,7 +959,7 @@ export function createAgentTools({
|
|
|
957
959
|
tools.learn_experience = tool({
|
|
958
960
|
description: dedent`
|
|
959
961
|
Read the full body of a specific experience section listed in <experience>.
|
|
960
|
-
The TOC shows entries like "A.1 ##
|
|
962
|
+
The TOC shows entries like "A.1 ## FLOW: ..." or "A.2 ## ACTION: ...". Pass the fileTag and sectionIndex.
|
|
961
963
|
Only call when a TOC entry looks directly relevant to the current step.
|
|
962
964
|
`,
|
|
963
965
|
inputSchema: z.object({
|
package/src/api/request-store.ts
CHANGED
|
@@ -7,6 +7,8 @@ const AUTH_HEADERS = ['authorization', 'cookie', 'x-api-key', 'x-csrf-token'];
|
|
|
7
7
|
export class RequestStore {
|
|
8
8
|
private capturedRequests: RequestResult[] = [];
|
|
9
9
|
private madeRequests: RequestResult[] = [];
|
|
10
|
+
private failedRequests: RequestResult[] = [];
|
|
11
|
+
private onFailedListeners: Array<(r: RequestResult) => void> = [];
|
|
10
12
|
private outputDir: string;
|
|
11
13
|
|
|
12
14
|
constructor(outputDir: string) {
|
|
@@ -18,6 +20,25 @@ export class RequestStore {
|
|
|
18
20
|
result.save(this.outputDir);
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
addFailedRequest(result: RequestResult): void {
|
|
24
|
+
this.failedRequests.push(result);
|
|
25
|
+
for (const cb of this.onFailedListeners) {
|
|
26
|
+
cb(result);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getFailedRequests(): RequestResult[] {
|
|
31
|
+
return this.failedRequests;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onFailedRequest(cb: (r: RequestResult) => void): () => void {
|
|
35
|
+
this.onFailedListeners.push(cb);
|
|
36
|
+
return () => {
|
|
37
|
+
const idx = this.onFailedListeners.indexOf(cb);
|
|
38
|
+
if (idx !== -1) this.onFailedListeners.splice(idx, 1);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
addMadeRequest(result: RequestResult): void {
|
|
22
43
|
this.madeRequests.push(result);
|
|
23
44
|
result.save(this.outputDir);
|
|
@@ -122,6 +143,7 @@ export class RequestStore {
|
|
|
122
143
|
clear(): void {
|
|
123
144
|
this.capturedRequests = [];
|
|
124
145
|
this.madeRequests = [];
|
|
146
|
+
this.failedRequests = [];
|
|
125
147
|
}
|
|
126
148
|
}
|
|
127
149
|
|
package/src/api/xhr-capture.ts
CHANGED
|
@@ -38,15 +38,33 @@ export class XhrCapture {
|
|
|
38
38
|
if (resourceType !== 'xhr' && resourceType !== 'fetch') return;
|
|
39
39
|
|
|
40
40
|
const method = request.method();
|
|
41
|
-
if (!WRITE_METHODS.has(method)) return;
|
|
42
|
-
|
|
43
41
|
const url = request.url();
|
|
44
42
|
if (!url.startsWith(this.baseOrigin)) return;
|
|
45
43
|
|
|
44
|
+
const status = response.status();
|
|
45
|
+
|
|
46
|
+
if (status >= 400) {
|
|
47
|
+
const failedUrl = new URL(url);
|
|
48
|
+
const failure = new RequestResult({
|
|
49
|
+
id: generateRequestId(method, failedUrl.pathname, 'fail_'),
|
|
50
|
+
method,
|
|
51
|
+
path: failedUrl.pathname,
|
|
52
|
+
fullUrl: failedUrl.pathname + failedUrl.search,
|
|
53
|
+
requestHeaders: {},
|
|
54
|
+
status,
|
|
55
|
+
statusText: response.statusText(),
|
|
56
|
+
responseHeaders: {},
|
|
57
|
+
timing: 0,
|
|
58
|
+
timestamp: new Date(),
|
|
59
|
+
});
|
|
60
|
+
this.store.addFailedRequest(failure);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!WRITE_METHODS.has(method)) return;
|
|
64
|
+
|
|
46
65
|
const contentType = response.headers()['content-type'] || '';
|
|
47
66
|
if (!JSON_CONTENT_TYPES.test(contentType)) return;
|
|
48
67
|
|
|
49
|
-
const status = response.status();
|
|
50
68
|
if (status === 304) return;
|
|
51
69
|
|
|
52
70
|
const parsedUrl = new URL(url);
|