explorbot 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/action-result.js +2 -0
  3. package/dist/src/action.js +88 -7
  4. package/dist/src/ai/captain/file-tools.js +100 -0
  5. package/dist/src/ai/captain/idle-mode.js +70 -6
  6. package/dist/src/ai/captain/web-mode.js +36 -6
  7. package/dist/src/ai/captain.js +87 -19
  8. package/dist/src/ai/historian/screencast.js +11 -2
  9. package/dist/src/ai/navigator.js +5 -2
  10. package/dist/src/ai/pilot.js +33 -5
  11. package/dist/src/ai/researcher/cache.js +8 -0
  12. package/dist/src/ai/researcher/coordinates.js +2 -3
  13. package/dist/src/ai/researcher/deep-analysis.js +149 -65
  14. package/dist/src/ai/researcher/locators.js +1 -2
  15. package/dist/src/ai/researcher.js +17 -18
  16. package/dist/src/ai/task-agent.js +1 -0
  17. package/dist/src/ai/tester.js +91 -39
  18. package/dist/src/ai/tools.js +17 -7
  19. package/dist/src/commands/explore-command.js +6 -1
  20. package/dist/src/components/LogPane.js +4 -3
  21. package/dist/src/explorer.js +270 -35
  22. package/dist/src/utils/browser-errors.js +23 -0
  23. package/dist/src/utils/error-page.js +17 -2
  24. package/dist/src/utils/logger.js +2 -2
  25. package/package.json +1 -1
  26. package/src/action-result.ts +2 -0
  27. package/src/action.ts +83 -7
  28. package/src/ai/captain/file-tools.ts +126 -0
  29. package/src/ai/captain/idle-mode.ts +72 -6
  30. package/src/ai/captain/mixin.ts +1 -1
  31. package/src/ai/captain/web-mode.ts +40 -5
  32. package/src/ai/captain.ts +94 -20
  33. package/src/ai/historian/screencast.ts +11 -2
  34. package/src/ai/navigator.ts +6 -2
  35. package/src/ai/pilot.ts +34 -5
  36. package/src/ai/researcher/cache.ts +7 -0
  37. package/src/ai/researcher/coordinates.ts +2 -3
  38. package/src/ai/researcher/deep-analysis.ts +169 -72
  39. package/src/ai/researcher/locators.ts +1 -2
  40. package/src/ai/researcher.ts +17 -18
  41. package/src/ai/task-agent.ts +1 -1
  42. package/src/ai/tester.ts +101 -41
  43. package/src/ai/tools.ts +17 -7
  44. package/src/commands/explore-command.ts +6 -1
  45. package/src/components/LogPane.tsx +4 -3
  46. package/src/explorer.ts +295 -38
  47. package/src/state-manager.ts +2 -0
  48. package/src/utils/browser-errors.ts +25 -0
  49. package/src/utils/error-page.ts +16 -3
  50. package/src/utils/logger.ts +3 -3
package/src/ai/tester.ts CHANGED
@@ -4,7 +4,7 @@ import { tool } from 'ai';
4
4
  import dedent from 'dedent';
5
5
  import { z } from 'zod';
6
6
  import { ActionResult } from '../action-result.ts';
7
- import { setActivity } from '../activity.ts';
7
+ import { clearActivity, setActivity } from '../activity.ts';
8
8
  import { ConfigParser } from '../config.ts';
9
9
  import type { ExperienceTracker } from '../experience-tracker.ts';
10
10
  import type Explorer from '../explorer.ts';
@@ -13,7 +13,7 @@ import type { StateTransition, WebPageState } from '../state-manager.ts';
13
13
  import { Stats } from '../stats.ts';
14
14
  import { type Note, type Test, TestResult, type TestResultType } from '../test-plan.ts';
15
15
  import { detectFocusArea, extractFocusedElement } from '../utils/aria.ts';
16
- import { ErrorPageError } from '../utils/error-page.ts';
16
+ import { ErrorPageError, isErrorPage } from '../utils/error-page.ts';
17
17
  import { HooksRunner } from '../utils/hooks-runner.ts';
18
18
  import { codeToMarkdown } from '../utils/html.ts';
19
19
  import { createDebug, tag } from '../utils/logger.ts';
@@ -136,18 +136,13 @@ export class Tester extends TaskAgent implements Agent {
136
136
  task.addNote(`Network error: ${r.method} ${r.path} → ${r.status}`, TestResult.FAILED);
137
137
  });
138
138
 
139
- const page = this.explorer.playwrightHelper?.page;
140
- const onPageError = (err: Error) => {
141
- task.addNote(`Console error: ${err.message}`, TestResult.FAILED);
142
- };
143
- const onConsoleMessage = (msg: any) => {
144
- if (msg.type() !== 'error') return;
145
- task.addNote(`Console error: ${msg.text()}`, TestResult.FAILED);
146
- };
147
- page?.on('pageerror', onPageError);
148
- page?.on('console', onConsoleMessage);
149
-
150
139
  const initialState = ActionResult.fromState(state);
140
+ if (isErrorPage(initialState)) {
141
+ task.start();
142
+ await this.explorer.startTest(task);
143
+ offFailedRequest?.();
144
+ return await this.abortStartedTestOnErrorPage(task, initialState);
145
+ }
151
146
 
152
147
  const conversation = this.provider.startConversation(this.getSystemMessage(), 'tester');
153
148
  conversation.markLastMessageCacheable();
@@ -176,20 +171,18 @@ export class Tester extends TaskAgent implements Agent {
176
171
  expected: task.expected,
177
172
  },
178
173
  },
179
- async () => this.runTestSession(task, initialState, conversation, { offFailedRequest, page, onPageError, onConsoleMessage })
174
+ async () => this.runTestSession(task, initialState, conversation, { offFailedRequest })
180
175
  );
181
176
  }
182
177
 
183
- private async runTestSession(task: Test, initialState: ActionResult, conversation: Conversation, handlers: { offFailedRequest?: () => void; page: any; onPageError: (err: Error) => void; onConsoleMessage: (msg: any) => void }): Promise<{ success: boolean }> {
184
- const { offFailedRequest, page, onPageError, onConsoleMessage } = handlers;
178
+ private async runTestSession(task: Test, initialState: ActionResult, conversation: Conversation, handlers: TestSessionHandlers): Promise<{ success: boolean }> {
179
+ const { offFailedRequest } = handlers;
185
180
 
186
181
  if (this.pilot) {
187
182
  try {
188
183
  const plan = await this.pilot.planTest(task, initialState);
189
184
  if (task.hasFinished) {
190
185
  offFailedRequest?.();
191
- page?.off('pageerror', onPageError);
192
- page?.off('console', onConsoleMessage);
193
186
  return { success: task.isSuccessful };
194
187
  }
195
188
  if (plan) {
@@ -201,22 +194,39 @@ export class Tester extends TaskAgent implements Agent {
201
194
  task.addNote(`Planning failed: ${message}`, TestResult.FAILED);
202
195
  task.finish(TestResult.FAILED);
203
196
  offFailedRequest?.();
204
- page?.off('pageerror', onPageError);
205
- page?.off('console', onConsoleMessage);
206
197
  return { success: false };
207
198
  }
208
199
  }
209
200
 
210
201
  debugLog('Starting test execution with tools');
211
202
 
212
- task.start();
213
- await this.explorer.startTest(task);
203
+ if (!(await this.explorer.startTest(task))) {
204
+ offFailedRequest?.();
205
+ await this.cleanupStartedTest(task);
206
+ return { success: task.isSuccessful };
207
+ }
214
208
 
215
209
  debugLog(`Navigating to ${task.startUrl}`);
216
- await this.explorer.visit(task.startUrl!);
210
+ try {
211
+ await this.explorer.visit(task.startUrl!);
212
+ } catch (error) {
213
+ const result = await this.handleLoopError(task, error);
214
+ if (result === 'stop') {
215
+ offFailedRequest?.();
216
+ await this.cleanupStartedTest(task);
217
+ return { success: task.isSuccessful };
218
+ }
219
+ }
217
220
 
218
221
  const startState = this.explorer.getStateManager().getCurrentState();
219
- if (startState) task.addUrlNote(startState);
222
+ if (startState) {
223
+ task.addUrlNote(startState);
224
+ const startActionResult = ActionResult.fromState(startState);
225
+ if (isErrorPage(startActionResult)) {
226
+ offFailedRequest?.();
227
+ return await this.abortStartedTestOnErrorPage(task, startActionResult);
228
+ }
229
+ }
220
230
  const currentUrl = startState?.url || task.startUrl || '';
221
231
  await this.hooksRunner.runBeforeHook('tester', currentUrl);
222
232
 
@@ -238,6 +248,12 @@ export class Tester extends TaskAgent implements Agent {
238
248
  await loop(
239
249
  async ({ stop, pause, iteration, userInput }) => {
240
250
  debugLog('iteration', iteration);
251
+ if (!(await this.explorer.ensurePageAvailable())) {
252
+ task.addNote('Browser page is unavailable');
253
+ task.finish(TestResult.FAILED);
254
+ stop();
255
+ return;
256
+ }
241
257
  const currentState = this.getCurrentState();
242
258
 
243
259
  const tools = {
@@ -385,22 +401,16 @@ export class Tester extends TaskAgent implements Agent {
385
401
  }
386
402
  : undefined,
387
403
  catch: async ({ error, stop }) => {
388
- tag('error').log(`Test execution error: ${error}`);
389
- const message = error instanceof Error ? error.message : String(error);
390
- if (!task.hasFinished) {
391
- task.addNote(`Execution error: ${message}`);
392
- }
393
- if (error instanceof Error && error.name === 'AbortError') {
394
- stop();
395
- return;
396
- }
397
- conversation.addUserText(`Previous AI call failed: ${message}. Take a different approach on the next step.`);
404
+ const result = await this.handleLoopError(task, error);
405
+ if (result === 'stop') stop();
398
406
  },
399
407
  }
400
408
  );
401
409
 
402
410
  if (task.hasFinished) break;
403
411
 
412
+ if (!(await this.explorer.ensurePageAvailable())) break;
413
+
404
414
  const finalState = this.getCurrentState();
405
415
  const wantsContinue = await this.pilot!.finalReview(task, finalState, conversation, this.navigator);
406
416
 
@@ -429,14 +439,8 @@ export class Tester extends TaskAgent implements Agent {
429
439
 
430
440
  offStateChange();
431
441
  offFailedRequest?.();
432
- page?.off('pageerror', onPageError);
433
- page?.off('console', onConsoleMessage);
434
442
  await this.finishTest(task);
435
- await this.explorer.stopTest(task, {
436
- startUrl: task.startUrl,
437
- style: task.style,
438
- sessionName: task.sessionName,
439
- });
443
+ await this.explorer.stopTest(task, this.buildStopTestMeta(task));
440
444
 
441
445
  return {
442
446
  success: task.isSuccessful,
@@ -689,6 +693,26 @@ export class Tester extends TaskAgent implements Agent {
689
693
  }
690
694
  }
691
695
 
696
+ private async abortStartedTestOnErrorPage(task: Test, actionResult: ActionResult): Promise<{ success: boolean }> {
697
+ const error = new ErrorPageError(actionResult.url || task.startUrl || '', actionResult.title, actionResult.httpStatus);
698
+ tag('warning').log(error.message);
699
+ task.addNote(error.message, TestResult.FAILED, actionResult.screenshotFile, actionResult.fullUrl || actionResult.url);
700
+ task.finish(TestResult.FAILED);
701
+ this.finishTest(task);
702
+ await this.explorer.stopTest(task, this.buildStopTestMeta(task));
703
+ clearActivity(true);
704
+ return { success: false };
705
+ }
706
+
707
+ private buildStopTestMeta(task: Test): Record<string, string> {
708
+ const meta: Record<string, string> = {
709
+ startUrl: task.startUrl,
710
+ };
711
+ if (task.style) meta.style = task.style;
712
+ if (task.sessionName) meta.sessionName = task.sessionName;
713
+ return meta;
714
+ }
715
+
692
716
  getSystemMessage(): string {
693
717
  return dedent`
694
718
  <role>
@@ -1093,4 +1117,40 @@ export class Tester extends TaskAgent implements Agent {
1093
1117
  }),
1094
1118
  };
1095
1119
  }
1120
+
1121
+ private async handleLoopError(task: Test, error: unknown): Promise<'continue' | 'stop'> {
1122
+ const message = error instanceof Error ? error.message : String(error);
1123
+ if (!task.hasFinished) task.addNote(`Execution error: ${message}`);
1124
+
1125
+ const result = await this.explorer.handleExecutionError(error);
1126
+ tag('info').log(`Browser supervisor: ${result.action} - ${result.message}`);
1127
+ task.addNote(result.message);
1128
+
1129
+ if (result.action === 'stop') {
1130
+ task.finish(TestResult.FAILED);
1131
+ return 'stop';
1132
+ }
1133
+
1134
+ if (result.recovered) {
1135
+ this.resetFailureCount();
1136
+ this.previousUrl = null;
1137
+ this.previousStateHash = null;
1138
+ }
1139
+
1140
+ this.currentConversation?.addUserText(result.message);
1141
+ return 'continue';
1142
+ }
1143
+
1144
+ private async cleanupStartedTest(task: Test): Promise<void> {
1145
+ await this.finishTest(task);
1146
+ await this.explorer.stopTest(task, {
1147
+ startUrl: task.startUrl,
1148
+ style: task.style,
1149
+ sessionName: task.sessionName,
1150
+ });
1151
+ }
1152
+ }
1153
+
1154
+ interface TestSessionHandlers {
1155
+ offFailedRequest?: () => void;
1096
1156
  }
package/src/ai/tools.ts CHANGED
@@ -6,6 +6,7 @@ import type { ExperienceTracker } from '../experience-tracker.ts';
6
6
  import type Explorer from '../explorer.ts';
7
7
  import { type Task, TestResult } from '../test-plan.js';
8
8
  import { extractFocusedElement } from '../utils/aria.ts';
9
+ import { isFatalBrowserError } from '../utils/browser-errors.ts';
9
10
  import { createDebug, tag } from '../utils/logger.js';
10
11
  import { pause } from '../utils/loop.js';
11
12
  import { WebElement } from '../utils/web-element.ts';
@@ -287,6 +288,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
287
288
  suggestion: 'Verify the key name is correct. For typing text, use form() tool instead.',
288
289
  });
289
290
  } catch (error) {
291
+ throwIfFatalBrowserError(error);
290
292
  activeNote.commit(TestResult.FAILED);
291
293
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
292
294
  return failedToolResult('pressKey', `PressKey tool failed: ${errorMessage}`);
@@ -405,6 +407,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
405
407
  action
406
408
  );
407
409
  } catch (error) {
410
+ throwIfFatalBrowserError(error);
408
411
  activeNote.commit(TestResult.FAILED);
409
412
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
410
413
  return failedToolResult('form', `Form tool failed: ${errorMessage}`);
@@ -444,8 +447,7 @@ export function createSpecialContextTools(explorer: Explorer, context: 'iframe')
444
447
 
445
448
  await explorer.switchToMainFrame();
446
449
 
447
- const action = explorer.createAction();
448
- const nextState = await action.capturePageState();
450
+ const nextState = await explorer.capturePageState();
449
451
  const toolResult = await nextState.toToolResult(previousState, 'I.switchTo()');
450
452
 
451
453
  return successToolResult('exitIframe', {
@@ -454,6 +456,7 @@ export function createSpecialContextTools(explorer: Explorer, context: 'iframe')
454
456
  code: 'I.switchTo()',
455
457
  });
456
458
  } catch (error) {
459
+ throwIfFatalBrowserError(error);
457
460
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
458
461
  return failedToolResult('exitIframe', `Failed to exit iframe: ${errorMessage}`);
459
462
  }
@@ -500,8 +503,7 @@ export function createAgentTools({
500
503
  }
501
504
 
502
505
  try {
503
- const action = explorer.createAction();
504
- const actionResult = await action.caputrePageWithScreenshot();
506
+ const actionResult = await explorer.capturePageWithScreenshot();
505
507
 
506
508
  if (!actionResult.screenshot) {
507
509
  return failedToolResult('see', 'Failed to capture screenshot for analysis');
@@ -519,6 +521,7 @@ export function createAgentTools({
519
521
  suggestion: 'Visual confirmation is valid evidence for test results. Use record() to note the visual findings.',
520
522
  });
521
523
  } catch (error) {
524
+ throwIfFatalBrowserError(error);
522
525
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
523
526
  visionDisabled = true;
524
527
  tag('warning').log('⚠️ Vision model is not available. Visual checks are disabled for this session.');
@@ -600,8 +603,7 @@ export function createAgentTools({
600
603
  });
601
604
  }
602
605
 
603
- const action = explorer.createAction();
604
- const actionResult = await action.capturePageState();
606
+ const actionResult = await explorer.capturePageState();
605
607
  const result = await navigator.verifyState(assertion, actionResult);
606
608
 
607
609
  if (result.verified) {
@@ -619,6 +621,7 @@ export function createAgentTools({
619
621
  suggestion: 'The assertion could not be verified. Check if the condition is actually present on the page or try a different assertion.',
620
622
  });
621
623
  } catch (error) {
624
+ throwIfFatalBrowserError(error);
622
625
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
623
626
  return failedToolResult('verify', `Verify tool failed: ${errorMessage}`, {
624
627
  error: errorMessage,
@@ -674,6 +677,7 @@ export function createAgentTools({
674
677
  `,
675
678
  });
676
679
  } catch (error) {
680
+ throwIfFatalBrowserError(error);
677
681
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
678
682
  return failedToolResult('research', `Research tool failed: ${errorMessage}`, {
679
683
  error: errorMessage,
@@ -718,6 +722,7 @@ export function createAgentTools({
718
722
  suggestion: 'The action could not be completed. Try a different instruction or use more specific element descriptions.',
719
723
  });
720
724
  } catch (error) {
725
+ throwIfFatalBrowserError(error);
721
726
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
722
727
  return failedToolResult('interact', `Interact tool failed: ${errorMessage}`, {
723
728
  error: errorMessage,
@@ -756,7 +761,7 @@ export function createAgentTools({
756
761
 
757
762
  const previousState = ActionResult.fromState(currentState);
758
763
  const action = explorer.createAction();
759
- const actionResult = await action.caputrePageWithScreenshot();
764
+ const actionResult = await explorer.capturePageWithScreenshot();
760
765
 
761
766
  if (!actionResult.screenshot) {
762
767
  return failedToolResult('visualClick', 'Failed to capture screenshot for visual analysis');
@@ -797,6 +802,7 @@ export function createAgentTools({
797
802
  analysis: locationResult,
798
803
  });
799
804
  } catch (error) {
805
+ throwIfFatalBrowserError(error);
800
806
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
801
807
  visionDisabled = true;
802
808
  tag('warning').log('⚠️ Vision model is not available. Visual clicks are disabled for this session.');
@@ -1018,6 +1024,10 @@ function cap(text: string | undefined | null, max: number): string {
1018
1024
  return `${text.slice(0, max)}\n[...truncated; ${text.length - max} chars omitted...]`;
1019
1025
  }
1020
1026
 
1027
+ function throwIfFatalBrowserError(error: unknown): void {
1028
+ if (isFatalBrowserError(error)) throw error;
1029
+ }
1030
+
1021
1031
  function transformContainsCommand(command: string): string {
1022
1032
  if (!command.includes(':contains(')) return command;
1023
1033
 
@@ -5,7 +5,7 @@ import { normalizeUrl } from '../state-manager.js';
5
5
  import { Stats } from '../stats.js';
6
6
  import { type Plan, type Test, TestResult } from '../test-plan.js';
7
7
  import { getCliName } from '../utils/cli-name.ts';
8
- import { ErrorPageError } from '../utils/error-page.ts';
8
+ import { ErrorPageError, getStateErrorPageError } from '../utils/error-page.ts';
9
9
  import { tag } from '../utils/logger.js';
10
10
  import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
11
11
  import { safeFilename } from '../utils/strings.ts';
@@ -56,6 +56,11 @@ export class ExploreCommand extends BaseCommand {
56
56
  Stats.mode ??= 'explore';
57
57
  Stats.focus ??= feature;
58
58
  const mainUrl = this.getCurrentPageUrl();
59
+ const error = getStateErrorPageError(this.explorBot.getExplorer().getStateManager().getCurrentState());
60
+ if (error) {
61
+ tag('warning').log(error.message);
62
+ return;
63
+ }
59
64
 
60
65
  if (cfg.enabled) {
61
66
  await this.runReuseMode(mainUrl, feature, cfg);
@@ -121,6 +121,7 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
121
121
  case 'step':
122
122
  return { color: 'cyan' as const, dimColor: true };
123
123
  case 'multiline':
124
+ case 'details':
124
125
  return { color: 'gray' as const, dimColor: true };
125
126
  case 'html':
126
127
  return { color: 'gray' as const };
@@ -143,16 +144,16 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
143
144
  }
144
145
  const styles = getLogStyles(log.type);
145
146
 
146
- if (log.type === 'multiline') {
147
+ if (log.type === 'multiline' || log.type === 'details') {
147
148
  const cleaned = stripAnsi(dedent(log.content));
148
149
  const parsed = parseMarkdownToTerminal(cleaned);
149
150
  const lines = parsed.split('\n');
150
151
  const maxLines = log.maxLines || 16;
151
- const truncated = lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
152
+ const content = log.type === 'details' ? parsed : lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
152
153
  return (
153
154
  <Box key={index} borderStyle="classic" borderLeft={false} borderRight={false} marginY={1} padding={1} borderColor="dim" overflow="hidden">
154
155
  <Text color="gray" dimColor>
155
- {truncated}
156
+ {content}
156
157
  </Text>
157
158
  </Box>
158
159
  );