explorbot 0.1.27 → 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 (48) 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/coordinates.js +2 -3
  12. package/dist/src/ai/researcher/deep-analysis.js +3 -4
  13. package/dist/src/ai/researcher/locators.js +1 -2
  14. package/dist/src/ai/researcher.js +17 -18
  15. package/dist/src/ai/task-agent.js +1 -0
  16. package/dist/src/ai/tester.js +91 -39
  17. package/dist/src/ai/tools.js +17 -7
  18. package/dist/src/commands/explore-command.js +6 -1
  19. package/dist/src/components/LogPane.js +4 -3
  20. package/dist/src/explorer.js +270 -35
  21. package/dist/src/utils/browser-errors.js +23 -0
  22. package/dist/src/utils/error-page.js +17 -2
  23. package/dist/src/utils/logger.js +2 -2
  24. package/package.json +1 -1
  25. package/src/action-result.ts +2 -0
  26. package/src/action.ts +83 -7
  27. package/src/ai/captain/file-tools.ts +126 -0
  28. package/src/ai/captain/idle-mode.ts +72 -6
  29. package/src/ai/captain/mixin.ts +1 -1
  30. package/src/ai/captain/web-mode.ts +40 -5
  31. package/src/ai/captain.ts +94 -20
  32. package/src/ai/historian/screencast.ts +11 -2
  33. package/src/ai/navigator.ts +6 -2
  34. package/src/ai/pilot.ts +34 -5
  35. package/src/ai/researcher/coordinates.ts +2 -3
  36. package/src/ai/researcher/deep-analysis.ts +3 -4
  37. package/src/ai/researcher/locators.ts +1 -2
  38. package/src/ai/researcher.ts +17 -18
  39. package/src/ai/task-agent.ts +1 -1
  40. package/src/ai/tester.ts +101 -41
  41. package/src/ai/tools.ts +17 -7
  42. package/src/commands/explore-command.ts +6 -1
  43. package/src/components/LogPane.tsx +4 -3
  44. package/src/explorer.ts +295 -38
  45. package/src/state-manager.ts +2 -0
  46. package/src/utils/browser-errors.ts +25 -0
  47. package/src/utils/error-page.ts +16 -3
  48. package/src/utils/logger.ts +3 -3
@@ -4,13 +4,13 @@ import { tool } from 'ai';
4
4
  import dedent from 'dedent';
5
5
  import { z } from 'zod';
6
6
  import { ActionResult } from "../action-result.js";
7
- import { setActivity } from "../activity.js";
7
+ import { clearActivity, setActivity } from "../activity.js";
8
8
  import { ConfigParser } from "../config.js";
9
9
  import { Observability } from "../observability.js";
10
10
  import { Stats } from "../stats.js";
11
11
  import { TestResult } from "../test-plan.js";
12
12
  import { detectFocusArea, extractFocusedElement } from "../utils/aria.js";
13
- import { ErrorPageError } from "../utils/error-page.js";
13
+ import { ErrorPageError, isErrorPage } from "../utils/error-page.js";
14
14
  import { HooksRunner } from "../utils/hooks-runner.js";
15
15
  import { createDebug, tag } from "../utils/logger.js";
16
16
  import { loop } from "../utils/loop.js";
@@ -107,18 +107,13 @@ export class Tester extends TaskAgent {
107
107
  const offFailedRequest = requestStore?.onFailedRequest((r) => {
108
108
  task.addNote(`Network error: ${r.method} ${r.path} → ${r.status}`, TestResult.FAILED);
109
109
  });
110
- const page = this.explorer.playwrightHelper?.page;
111
- const onPageError = (err) => {
112
- task.addNote(`Console error: ${err.message}`, TestResult.FAILED);
113
- };
114
- const onConsoleMessage = (msg) => {
115
- if (msg.type() !== 'error')
116
- return;
117
- task.addNote(`Console error: ${msg.text()}`, TestResult.FAILED);
118
- };
119
- page?.on('pageerror', onPageError);
120
- page?.on('console', onConsoleMessage);
121
110
  const initialState = ActionResult.fromState(state);
111
+ if (isErrorPage(initialState)) {
112
+ task.start();
113
+ await this.explorer.startTest(task);
114
+ offFailedRequest?.();
115
+ return await this.abortStartedTestOnErrorPage(task, initialState);
116
+ }
122
117
  const conversation = this.provider.startConversation(this.getSystemMessage(), 'tester');
123
118
  conversation.markLastMessageCacheable();
124
119
  this.currentConversation = conversation;
@@ -140,17 +135,15 @@ export class Tester extends TaskAgent {
140
135
  startUrl: task.startUrl,
141
136
  expected: task.expected,
142
137
  },
143
- }, async () => this.runTestSession(task, initialState, conversation, { offFailedRequest, page, onPageError, onConsoleMessage }));
138
+ }, async () => this.runTestSession(task, initialState, conversation, { offFailedRequest }));
144
139
  }
145
140
  async runTestSession(task, initialState, conversation, handlers) {
146
- const { offFailedRequest, page, onPageError, onConsoleMessage } = handlers;
141
+ const { offFailedRequest } = handlers;
147
142
  if (this.pilot) {
148
143
  try {
149
144
  const plan = await this.pilot.planTest(task, initialState);
150
145
  if (task.hasFinished) {
151
146
  offFailedRequest?.();
152
- page?.off('pageerror', onPageError);
153
- page?.off('console', onConsoleMessage);
154
147
  return { success: task.isSuccessful };
155
148
  }
156
149
  if (plan) {
@@ -163,19 +156,36 @@ export class Tester extends TaskAgent {
163
156
  task.addNote(`Planning failed: ${message}`, TestResult.FAILED);
164
157
  task.finish(TestResult.FAILED);
165
158
  offFailedRequest?.();
166
- page?.off('pageerror', onPageError);
167
- page?.off('console', onConsoleMessage);
168
159
  return { success: false };
169
160
  }
170
161
  }
171
162
  debugLog('Starting test execution with tools');
172
- task.start();
173
- await this.explorer.startTest(task);
163
+ if (!(await this.explorer.startTest(task))) {
164
+ offFailedRequest?.();
165
+ await this.cleanupStartedTest(task);
166
+ return { success: task.isSuccessful };
167
+ }
174
168
  debugLog(`Navigating to ${task.startUrl}`);
175
- await this.explorer.visit(task.startUrl);
169
+ try {
170
+ await this.explorer.visit(task.startUrl);
171
+ }
172
+ catch (error) {
173
+ const result = await this.handleLoopError(task, error);
174
+ if (result === 'stop') {
175
+ offFailedRequest?.();
176
+ await this.cleanupStartedTest(task);
177
+ return { success: task.isSuccessful };
178
+ }
179
+ }
176
180
  const startState = this.explorer.getStateManager().getCurrentState();
177
- if (startState)
181
+ if (startState) {
178
182
  task.addUrlNote(startState);
183
+ const startActionResult = ActionResult.fromState(startState);
184
+ if (isErrorPage(startActionResult)) {
185
+ offFailedRequest?.();
186
+ return await this.abortStartedTestOnErrorPage(task, startActionResult);
187
+ }
188
+ }
179
189
  const currentUrl = startState?.url || task.startUrl || '';
180
190
  await this.hooksRunner.runBeforeHook('tester', currentUrl);
181
191
  const offStateChange = this.explorer.getStateManager().onStateChange((event) => {
@@ -195,6 +205,12 @@ export class Tester extends TaskAgent {
195
205
  shouldContinue = false;
196
206
  await loop(async ({ stop, pause, iteration, userInput }) => {
197
207
  debugLog('iteration', iteration);
208
+ if (!(await this.explorer.ensurePageAvailable())) {
209
+ task.addNote('Browser page is unavailable');
210
+ task.finish(TestResult.FAILED);
211
+ stop();
212
+ return;
213
+ }
198
214
  const currentState = this.getCurrentState();
199
215
  const tools = {
200
216
  ...codeceptjsTools,
@@ -327,20 +343,15 @@ export class Tester extends TaskAgent {
327
343
  }
328
344
  : undefined,
329
345
  catch: async ({ error, stop }) => {
330
- tag('error').log(`Test execution error: ${error}`);
331
- const message = error instanceof Error ? error.message : String(error);
332
- if (!task.hasFinished) {
333
- task.addNote(`Execution error: ${message}`);
334
- }
335
- if (error instanceof Error && error.name === 'AbortError') {
346
+ const result = await this.handleLoopError(task, error);
347
+ if (result === 'stop')
336
348
  stop();
337
- return;
338
- }
339
- conversation.addUserText(`Previous AI call failed: ${message}. Take a different approach on the next step.`);
340
349
  },
341
350
  });
342
351
  if (task.hasFinished)
343
352
  break;
353
+ if (!(await this.explorer.ensurePageAvailable()))
354
+ break;
344
355
  const finalState = this.getCurrentState();
345
356
  const wantsContinue = await this.pilot.finalReview(task, finalState, conversation, this.navigator);
346
357
  if (!wantsContinue || task.hasFinished)
@@ -366,14 +377,8 @@ export class Tester extends TaskAgent {
366
377
  await this.getQuartermaster().analyzeSession(task, initialState, conversation);
367
378
  offStateChange();
368
379
  offFailedRequest?.();
369
- page?.off('pageerror', onPageError);
370
- page?.off('console', onConsoleMessage);
371
380
  await this.finishTest(task);
372
- await this.explorer.stopTest(task, {
373
- startUrl: task.startUrl,
374
- style: task.style,
375
- sessionName: task.sessionName,
376
- });
381
+ await this.explorer.stopTest(task, this.buildStopTestMeta(task));
377
382
  return {
378
383
  success: task.isSuccessful,
379
384
  ...task,
@@ -607,6 +612,26 @@ export class Tester extends TaskAgent {
607
612
  tag('warning').log(`Test with no result: ${task.scenario}`);
608
613
  }
609
614
  }
615
+ async abortStartedTestOnErrorPage(task, actionResult) {
616
+ const error = new ErrorPageError(actionResult.url || task.startUrl || '', actionResult.title, actionResult.httpStatus);
617
+ tag('warning').log(error.message);
618
+ task.addNote(error.message, TestResult.FAILED, actionResult.screenshotFile, actionResult.fullUrl || actionResult.url);
619
+ task.finish(TestResult.FAILED);
620
+ this.finishTest(task);
621
+ await this.explorer.stopTest(task, this.buildStopTestMeta(task));
622
+ clearActivity(true);
623
+ return { success: false };
624
+ }
625
+ buildStopTestMeta(task) {
626
+ const meta = {
627
+ startUrl: task.startUrl,
628
+ };
629
+ if (task.style)
630
+ meta.style = task.style;
631
+ if (task.sessionName)
632
+ meta.sessionName = task.sessionName;
633
+ return meta;
634
+ }
610
635
  getSystemMessage() {
611
636
  return dedent `
612
637
  <role>
@@ -991,4 +1016,31 @@ export class Tester extends TaskAgent {
991
1016
  }),
992
1017
  };
993
1018
  }
1019
+ async handleLoopError(task, error) {
1020
+ const message = error instanceof Error ? error.message : String(error);
1021
+ if (!task.hasFinished)
1022
+ task.addNote(`Execution error: ${message}`);
1023
+ const result = await this.explorer.handleExecutionError(error);
1024
+ tag('info').log(`Browser supervisor: ${result.action} - ${result.message}`);
1025
+ task.addNote(result.message);
1026
+ if (result.action === 'stop') {
1027
+ task.finish(TestResult.FAILED);
1028
+ return 'stop';
1029
+ }
1030
+ if (result.recovered) {
1031
+ this.resetFailureCount();
1032
+ this.previousUrl = null;
1033
+ this.previousStateHash = null;
1034
+ }
1035
+ this.currentConversation?.addUserText(result.message);
1036
+ return 'continue';
1037
+ }
1038
+ async cleanupStartedTest(task) {
1039
+ await this.finishTest(task);
1040
+ await this.explorer.stopTest(task, {
1041
+ startUrl: task.startUrl,
1042
+ style: task.style,
1043
+ sessionName: task.sessionName,
1044
+ });
1045
+ }
994
1046
  }
@@ -4,6 +4,7 @@ import { z } from 'zod';
4
4
  import { ActionResult } from "../action-result.js";
5
5
  import { TestResult } from '../test-plan.js';
6
6
  import { extractFocusedElement } from "../utils/aria.js";
7
+ import { isFatalBrowserError } from "../utils/browser-errors.js";
7
8
  import { createDebug, tag } from '../utils/logger.js';
8
9
  import { pause } from '../utils/loop.js';
9
10
  import { WebElement } from "../utils/web-element.js";
@@ -243,6 +244,7 @@ export function createCodeceptJSTools(explorer, task) {
243
244
  });
244
245
  }
245
246
  catch (error) {
247
+ throwIfFatalBrowserError(error);
246
248
  activeNote.commit(TestResult.FAILED);
247
249
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
248
250
  return failedToolResult('pressKey', `PressKey tool failed: ${errorMessage}`);
@@ -343,6 +345,7 @@ export function createCodeceptJSTools(explorer, task) {
343
345
  }, action);
344
346
  }
345
347
  catch (error) {
348
+ throwIfFatalBrowserError(error);
346
349
  activeNote.commit(TestResult.FAILED);
347
350
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
348
351
  return failedToolResult('form', `Form tool failed: ${errorMessage}`);
@@ -376,8 +379,7 @@ export function createSpecialContextTools(explorer, context) {
376
379
  });
377
380
  }
378
381
  await explorer.switchToMainFrame();
379
- const action = explorer.createAction();
380
- const nextState = await action.capturePageState();
382
+ const nextState = await explorer.capturePageState();
381
383
  const toolResult = await nextState.toToolResult(previousState, 'I.switchTo()');
382
384
  return successToolResult('exitIframe', {
383
385
  ...toolResult,
@@ -386,6 +388,7 @@ export function createSpecialContextTools(explorer, context) {
386
388
  });
387
389
  }
388
390
  catch (error) {
391
+ throwIfFatalBrowserError(error);
389
392
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
390
393
  return failedToolResult('exitIframe', `Failed to exit iframe: ${errorMessage}`);
391
394
  }
@@ -415,8 +418,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
415
418
  return failedToolResult('see', 'Vision tools are disabled for this session. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.');
416
419
  }
417
420
  try {
418
- const action = explorer.createAction();
419
- const actionResult = await action.caputrePageWithScreenshot();
421
+ const actionResult = await explorer.capturePageWithScreenshot();
420
422
  if (!actionResult.screenshot) {
421
423
  return failedToolResult('see', 'Failed to capture screenshot for analysis');
422
424
  }
@@ -431,6 +433,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
431
433
  });
432
434
  }
433
435
  catch (error) {
436
+ throwIfFatalBrowserError(error);
434
437
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
435
438
  visionDisabled = true;
436
439
  tag('warning').log('⚠️ Vision model is not available. Visual checks are disabled for this session.');
@@ -506,8 +509,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
506
509
  suggestion: verifications[assertion] ? 'This verification already passed. Call finish() to complete the test.' : 'This verification already failed. Perform actions to change the page state, then try again.',
507
510
  });
508
511
  }
509
- const action = explorer.createAction();
510
- const actionResult = await action.capturePageState();
512
+ const actionResult = await explorer.capturePageState();
511
513
  const result = await navigator.verifyState(assertion, actionResult);
512
514
  if (result.verified) {
513
515
  return successToolResult('verify', {
@@ -520,6 +522,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
520
522
  });
521
523
  }
522
524
  catch (error) {
525
+ throwIfFatalBrowserError(error);
523
526
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
524
527
  return failedToolResult('verify', `Verify tool failed: ${errorMessage}`, {
525
528
  error: errorMessage,
@@ -572,6 +575,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
572
575
  });
573
576
  }
574
577
  catch (error) {
578
+ throwIfFatalBrowserError(error);
575
579
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
576
580
  return failedToolResult('research', `Research tool failed: ${errorMessage}`, {
577
581
  error: errorMessage,
@@ -611,6 +615,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
611
615
  });
612
616
  }
613
617
  catch (error) {
618
+ throwIfFatalBrowserError(error);
614
619
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
615
620
  return failedToolResult('interact', `Interact tool failed: ${errorMessage}`, {
616
621
  error: errorMessage,
@@ -645,7 +650,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
645
650
  }
646
651
  const previousState = ActionResult.fromState(currentState);
647
652
  const action = explorer.createAction();
648
- const actionResult = await action.caputrePageWithScreenshot();
653
+ const actionResult = await explorer.capturePageWithScreenshot();
649
654
  if (!actionResult.screenshot) {
650
655
  return failedToolResult('visualClick', 'Failed to capture screenshot for visual analysis');
651
656
  }
@@ -678,6 +683,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
678
683
  });
679
684
  }
680
685
  catch (error) {
686
+ throwIfFatalBrowserError(error);
681
687
  const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
682
688
  visionDisabled = true;
683
689
  tag('warning').log('⚠️ Vision model is not available. Visual clicks are disabled for this session.');
@@ -874,6 +880,10 @@ function cap(text, max) {
874
880
  return text;
875
881
  return `${text.slice(0, max)}\n[...truncated; ${text.length - max} chars omitted...]`;
876
882
  }
883
+ function throwIfFatalBrowserError(error) {
884
+ if (isFatalBrowserError(error))
885
+ throw error;
886
+ }
877
887
  function transformContainsCommand(command) {
878
888
  if (!command.includes(':contains('))
879
889
  return command;
@@ -5,7 +5,7 @@ import { normalizeUrl } from '../state-manager.js';
5
5
  import { Stats } from '../stats.js';
6
6
  import { TestResult } from '../test-plan.js';
7
7
  import { getCliName } from "../utils/cli-name.js";
8
- import { ErrorPageError } from "../utils/error-page.js";
8
+ import { ErrorPageError, getStateErrorPageError } from "../utils/error-page.js";
9
9
  import { tag } from '../utils/logger.js';
10
10
  import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
11
11
  import { safeFilename } from "../utils/strings.js";
@@ -53,6 +53,11 @@ export class ExploreCommand extends BaseCommand {
53
53
  Stats.mode ??= 'explore';
54
54
  Stats.focus ??= feature;
55
55
  const mainUrl = this.getCurrentPageUrl();
56
+ const error = getStateErrorPageError(this.explorBot.getExplorer().getStateManager().getCurrentState());
57
+ if (error) {
58
+ tag('warning').log(error.message);
59
+ return;
60
+ }
56
61
  if (cfg.enabled) {
57
62
  await this.runReuseMode(mainUrl, feature, cfg);
58
63
  }
@@ -94,6 +94,7 @@ const LogPane = React.memo(({ verboseMode }) => {
94
94
  case 'step':
95
95
  return { color: 'cyan', dimColor: true };
96
96
  case 'multiline':
97
+ case 'details':
97
98
  return { color: 'gray', dimColor: true };
98
99
  case 'html':
99
100
  return { color: 'gray' };
@@ -113,14 +114,14 @@ const LogPane = React.memo(({ verboseMode }) => {
113
114
  return null;
114
115
  }
115
116
  const styles = getLogStyles(log.type);
116
- if (log.type === 'multiline') {
117
+ if (log.type === 'multiline' || log.type === 'details') {
117
118
  const cleaned = stripAnsi(dedent(log.content));
118
119
  const parsed = parseMarkdownToTerminal(cleaned);
119
120
  const lines = parsed.split('\n');
120
121
  const maxLines = log.maxLines || 16;
121
- const truncated = lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
122
+ const content = log.type === 'details' ? parsed : lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
122
123
  return (React.createElement(Box, { key: index, borderStyle: "classic", borderLeft: false, borderRight: false, marginY: 1, padding: 1, borderColor: "dim", overflow: "hidden" },
123
- React.createElement(Text, { color: "gray", dimColor: true }, truncated)));
124
+ React.createElement(Text, { color: "gray", dimColor: true }, content)));
124
125
  }
125
126
  if (log.type === 'html') {
126
127
  // Convert HTML to markdown, then render as multiline