explorbot 0.1.9 → 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.
Files changed (113) hide show
  1. package/bin/explorbot-cli.ts +70 -8
  2. package/boat/api-tester/src/ai/curler-tools.ts +3 -3
  3. package/boat/api-tester/src/ai/curler.ts +1 -1
  4. package/boat/api-tester/src/apibot.ts +2 -2
  5. package/boat/api-tester/src/config.ts +1 -1
  6. package/dist/bin/explorbot-cli.js +70 -7
  7. package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
  8. package/dist/boat/api-tester/src/apibot.js +2 -2
  9. package/dist/package.json +1 -1
  10. package/dist/src/ai/bosun.js +5 -1
  11. package/dist/src/ai/experience-compactor.js +235 -50
  12. package/dist/src/ai/historian.js +13 -6
  13. package/dist/src/ai/navigator.js +62 -62
  14. package/dist/src/ai/pilot.js +22 -0
  15. package/dist/src/ai/planner/subpages.js +1 -30
  16. package/dist/src/ai/planner.js +4 -4
  17. package/dist/src/ai/provider.js +1 -1
  18. package/dist/src/ai/rerunner.js +3 -3
  19. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  20. package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
  21. package/dist/src/ai/researcher/locators.js +1 -1
  22. package/dist/src/ai/researcher/sections.js +8 -1
  23. package/dist/src/ai/researcher.js +4 -11
  24. package/dist/src/ai/tools.js +5 -3
  25. package/dist/src/api/request-store.js +20 -0
  26. package/dist/src/api/xhr-capture.js +19 -3
  27. package/dist/src/command-handler.js +1 -1
  28. package/dist/src/commands/add-rule-command.js +1 -1
  29. package/dist/src/commands/base-command.js +20 -0
  30. package/dist/src/commands/clean-command.js +1 -1
  31. package/dist/src/commands/compact-command.js +138 -0
  32. package/dist/src/commands/context-command.js +7 -1
  33. package/dist/src/commands/drill-command.js +4 -1
  34. package/dist/src/commands/experience-command.js +104 -0
  35. package/dist/src/commands/explore-command.js +28 -5
  36. package/dist/src/commands/freesail-command.js +2 -0
  37. package/dist/src/commands/index.js +7 -3
  38. package/dist/src/commands/init-command.js +2 -2
  39. package/dist/src/commands/learn-command.js +1 -1
  40. package/dist/src/commands/navigate-command.js +4 -1
  41. package/dist/src/commands/plan-clear-command.js +4 -1
  42. package/dist/src/commands/plan-command.js +11 -4
  43. package/dist/src/commands/plan-edit-command.js +1 -1
  44. package/dist/src/commands/plan-load-command.js +4 -1
  45. package/dist/src/commands/plan-reload-command.js +4 -1
  46. package/dist/src/commands/plan-save-command.js +1 -1
  47. package/dist/src/commands/research-command.js +5 -2
  48. package/dist/src/commands/start-command.js +5 -1
  49. package/dist/src/commands/test-command.js +7 -1
  50. package/dist/src/experience-tracker.js +191 -56
  51. package/dist/src/explorbot.js +26 -14
  52. package/dist/src/explorer.js +3 -3
  53. package/dist/src/reporter.js +17 -2
  54. package/dist/src/stats.js +2 -0
  55. package/dist/src/suite.js +1 -1
  56. package/dist/src/utils/error-page.js +10 -0
  57. package/dist/src/utils/logger.js +1 -1
  58. package/dist/src/utils/rules-loader.js +1 -1
  59. package/dist/src/utils/test-files.js +1 -1
  60. package/dist/src/utils/url-matcher.js +50 -0
  61. package/package.json +1 -1
  62. package/src/ai/bosun.ts +5 -1
  63. package/src/ai/experience-compactor.ts +270 -63
  64. package/src/ai/historian.ts +12 -7
  65. package/src/ai/navigator.ts +68 -66
  66. package/src/ai/pilot.ts +22 -0
  67. package/src/ai/planner/subpages.ts +1 -24
  68. package/src/ai/planner.ts +5 -5
  69. package/src/ai/provider.ts +1 -1
  70. package/src/ai/rerunner.ts +3 -3
  71. package/src/ai/researcher/deep-analysis.ts +1 -1
  72. package/src/ai/researcher/fingerprint-worker.ts +1 -1
  73. package/src/ai/researcher/locators.ts +2 -2
  74. package/src/ai/researcher/sections.ts +7 -1
  75. package/src/ai/researcher.ts +4 -11
  76. package/src/ai/task-agent.ts +1 -1
  77. package/src/ai/tools.ts +6 -4
  78. package/src/api/request-store.ts +22 -0
  79. package/src/api/xhr-capture.ts +21 -3
  80. package/src/command-handler.ts +1 -1
  81. package/src/commands/add-rule-command.ts +2 -2
  82. package/src/commands/base-command.ts +26 -1
  83. package/src/commands/clean-command.ts +2 -2
  84. package/src/commands/compact-command.ts +156 -0
  85. package/src/commands/context-command.ts +8 -2
  86. package/src/commands/drill-command.ts +5 -2
  87. package/src/commands/experience-command.ts +125 -0
  88. package/src/commands/explore-command.ts +30 -7
  89. package/src/commands/freesail-command.ts +2 -0
  90. package/src/commands/index.ts +7 -3
  91. package/src/commands/init-command.ts +2 -2
  92. package/src/commands/learn-command.ts +2 -2
  93. package/src/commands/navigate-command.ts +5 -2
  94. package/src/commands/plan-clear-command.ts +5 -2
  95. package/src/commands/plan-command.ts +12 -5
  96. package/src/commands/plan-edit-command.ts +2 -2
  97. package/src/commands/plan-load-command.ts +5 -2
  98. package/src/commands/plan-reload-command.ts +5 -2
  99. package/src/commands/plan-save-command.ts +2 -2
  100. package/src/commands/research-command.ts +6 -3
  101. package/src/commands/start-command.ts +6 -2
  102. package/src/commands/test-command.ts +8 -2
  103. package/src/experience-tracker.ts +220 -71
  104. package/src/explorbot.ts +28 -15
  105. package/src/explorer.ts +3 -3
  106. package/src/reporter.ts +17 -3
  107. package/src/stats.ts +4 -0
  108. package/src/suite.ts +1 -1
  109. package/src/utils/error-page.ts +10 -0
  110. package/src/utils/logger.ts +1 -1
  111. package/src/utils/rules-loader.ts +1 -1
  112. package/src/utils/test-files.ts +1 -1
  113. package/src/utils/url-matcher.ts +43 -0
@@ -1,6 +1,7 @@
1
1
  import dedent from 'dedent';
2
2
  import { ActionResult } from '../action-result.js';
3
- import { ExperienceTracker } from '../experience-tracker.js';
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.currentAction = action;
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.currentAction = action;
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
- const relevantExperience = this.experienceTracker.getRelevantExperience(actionResult).map((experience) => experience.content);
157
-
158
- if (relevantExperience.length > 0) {
159
- const experienceContent = relevantExperience.join('\n\n---\n\n');
160
- experience = await this.experienceCompactor.compactExperience(experienceContent);
161
- tag('substep').log(`Found ${relevantExperience.length} experience ${pluralize(relevantExperience.length, 'file')} for: ${actionResult.url}`);
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.length, 'characters');
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 this.currentAction.attempt(codeBlock, message);
250
+ resolved = await action.attempt(codeBlock, message);
261
251
 
262
- if (this.currentUrl) {
263
- await this.currentAction.getActor().wait(2);
264
- const freshState = await this.currentAction.capturePageState();
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(this.currentUrl)) {
256
+ if (normalizeUrl(freshState.url || '') === normalizeUrl(expectedUrl)) {
267
257
  resolved = true;
268
258
  } else if (resolved) {
269
- tag('warning').log(`URL verification failed: expected ${this.currentUrl}, got ${freshState.url}`);
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
- await this.experienceTracker.saveSuccessfulResolution(actionResult, message, codeBlock);
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 && this.currentUrl) {
294
- await this.currentAction.getActor().wait(1);
295
- if (this.isOnExpectedPage(this.currentUrl, this.currentAction.stateManager)) {
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: ${this.currentAction.stateManager.getCurrentState()?.url}\n` + `Target: ${this.currentUrl}\nEnter CodeceptJS commands (or press Enter to skip):`);
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 this.currentAction.attempt(userInput, message);
310
- if (resolved && this.currentUrl) {
311
- await this.currentAction.getActor().wait(1);
312
- if (!this.isOnExpectedPage(this.currentUrl, this.currentAction.stateManager)) {
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
- const relevantExperience = this.experienceTracker.getRelevantExperience(actionResult).map((exp) => exp.content);
469
-
470
- if (relevantExperience.length > 0) {
471
- const experienceContent = relevantExperience.join('\n\n---\n\n');
472
- experience = await this.experienceCompactor.compactExperience(experienceContent);
473
- tag('substep').log(`Found ${relevantExperience.length} experience ${pluralize(relevantExperience.length, 'file')} for: ${actionResult.url}`);
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.length, 'characters');
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 { Suite } from '../suite.ts';
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');
@@ -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');
@@ -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 { locatorRule, actionRule, sectionContextRule } from './rules.ts';
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, readdirSync, readFileSync, statSync } from 'node:fs';
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
- const text = await this._researchSingleSection(name, description, ariaSnapshot, focusCss);
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;
@@ -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 { executionController } from '../execution-controller.ts';
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
- return dedent`
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
 
@@ -1,6 +1,6 @@
1
1
  import dedent from 'dedent';
2
2
  import type { ActionResult } from '../action-result.js';
3
- import { renderExperienceToc, type ExperienceTracker } from '../experience-tracker.js';
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 all keyboard interactions: I.fillField, I.type, I.pressKey
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 ## Successful Flow: ...". Pass the fileTag and sectionIndex.
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({
@@ -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
 
@@ -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);