explorbot 0.1.32-beta.1 → 0.2.1

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 (222) hide show
  1. package/README.md +35 -19
  2. package/bin/explorbot-cli.ts +33 -10
  3. package/boat/api-tester/src/config.ts +45 -3
  4. package/boat/doc-collector/src/ai/documentarian.ts +26 -28
  5. package/boat/doc-collector/src/ai/tools.ts +74 -16
  6. package/boat/doc-collector/src/cli.ts +1 -6
  7. package/boat/doc-collector/src/docbot.ts +30 -9
  8. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  9. package/boat/doc-collector/src/screenshots.ts +34 -2
  10. package/boat/doc-collector/src/state-diagram.ts +281 -0
  11. package/dist/bin/explorbot-cli.js +29 -9
  12. package/dist/boat/api-tester/src/config.js +43 -4
  13. package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
  14. package/dist/boat/doc-collector/src/ai/tools.js +53 -16
  15. package/dist/boat/doc-collector/src/cli.js +1 -5
  16. package/dist/boat/doc-collector/src/docbot.js +27 -8
  17. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  18. package/dist/boat/doc-collector/src/screenshots.js +30 -1
  19. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  20. package/dist/models.json +30 -0
  21. package/dist/package.json +23 -3
  22. package/dist/src/action-result.d.ts +8 -10
  23. package/dist/src/action-result.js +72 -164
  24. package/dist/src/action.d.ts +13 -12
  25. package/dist/src/action.js +37 -113
  26. package/dist/src/ai/agent.d.ts +17 -0
  27. package/dist/src/ai/captain/idle-mode.js +1 -1
  28. package/dist/src/ai/captain/test-mode.js +1 -1
  29. package/dist/src/ai/captain/web-mode.js +18 -29
  30. package/dist/src/ai/captain.d.ts +1 -3
  31. package/dist/src/ai/captain.js +17 -45
  32. package/dist/src/ai/conversation.d.ts +2 -5
  33. package/dist/src/ai/conversation.js +9 -28
  34. package/dist/src/ai/driller.d.ts +2 -17
  35. package/dist/src/ai/driller.js +26 -50
  36. package/dist/src/ai/experience-compactor.d.ts +2 -2
  37. package/dist/src/ai/experience-compactor.js +16 -33
  38. package/dist/src/ai/fisherman-tools.js +1 -1
  39. package/dist/src/ai/fisherman.d.ts +0 -1
  40. package/dist/src/ai/fisherman.js +0 -3
  41. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  42. package/dist/src/ai/historian/codeceptjs.js +4 -3
  43. package/dist/src/ai/historian/playwright.d.ts +4 -1
  44. package/dist/src/ai/historian/playwright.js +4 -3
  45. package/dist/src/ai/historian/screencast.d.ts +2 -1
  46. package/dist/src/ai/historian/screencast.js +2 -2
  47. package/dist/src/ai/historian/utils.d.ts +0 -1
  48. package/dist/src/ai/historian/utils.js +0 -1
  49. package/dist/src/ai/historian.d.ts +10 -8
  50. package/dist/src/ai/historian.js +1 -5
  51. package/dist/src/ai/navigator.d.ts +8 -6
  52. package/dist/src/ai/navigator.js +34 -65
  53. package/dist/src/ai/pilot.d.ts +13 -8
  54. package/dist/src/ai/pilot.js +53 -59
  55. package/dist/src/ai/planner.d.ts +5 -4
  56. package/dist/src/ai/planner.js +66 -52
  57. package/dist/src/ai/provider.d.ts +4 -1
  58. package/dist/src/ai/provider.js +77 -137
  59. package/dist/src/ai/quartermaster.d.ts +22 -22
  60. package/dist/src/ai/quartermaster.js +8 -14
  61. package/dist/src/ai/rerunner.d.ts +3 -13
  62. package/dist/src/ai/rerunner.js +23 -44
  63. package/dist/src/ai/researcher/cache.js +11 -18
  64. package/dist/src/ai/researcher/coordinates.js +4 -3
  65. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
  66. package/dist/src/ai/researcher/deep-analysis.js +8 -11
  67. package/dist/src/ai/researcher/locators.d.ts +9 -2
  68. package/dist/src/ai/researcher/locators.js +65 -8
  69. package/dist/src/ai/researcher/parser.js +1 -1
  70. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  71. package/dist/src/ai/researcher/research-result.js +0 -19
  72. package/dist/src/ai/researcher/sections.d.ts +2 -0
  73. package/dist/src/ai/researcher/sections.js +7 -3
  74. package/dist/src/ai/researcher.js +40 -106
  75. package/dist/src/ai/rules.d.ts +1 -0
  76. package/dist/src/ai/rules.js +22 -15
  77. package/dist/src/ai/session-analyst.js +8 -5
  78. package/dist/src/ai/task-agent.d.ts +19 -6
  79. package/dist/src/ai/task-agent.js +37 -33
  80. package/dist/src/ai/tester.d.ts +6 -14
  81. package/dist/src/ai/tester.js +46 -111
  82. package/dist/src/ai/tools.d.ts +26 -14
  83. package/dist/src/ai/tools.js +138 -132
  84. package/dist/src/command-handler.js +1 -1
  85. package/dist/src/commands/compact-command.d.ts +1 -1
  86. package/dist/src/commands/compact-command.js +6 -6
  87. package/dist/src/commands/context-aria-command.js +1 -1
  88. package/dist/src/commands/context-command.js +8 -6
  89. package/dist/src/commands/context-data-command.js +2 -2
  90. package/dist/src/commands/context-experience-command.js +2 -2
  91. package/dist/src/commands/context-html-command.js +2 -2
  92. package/dist/src/commands/context-knowledge-command.js +2 -2
  93. package/dist/src/commands/drill-command.js +1 -1
  94. package/dist/src/commands/experience-command.js +1 -1
  95. package/dist/src/commands/explore-command.js +4 -3
  96. package/dist/src/commands/freesail-command.js +2 -2
  97. package/dist/src/commands/knows-command.js +1 -1
  98. package/dist/src/commands/learn-command.js +5 -3
  99. package/dist/src/commands/path-command.js +1 -1
  100. package/dist/src/commands/research-command.js +1 -1
  101. package/dist/src/commands/test-command.js +1 -1
  102. package/dist/src/components/AddKnowledge.d.ts +2 -0
  103. package/dist/src/components/AddKnowledge.js +3 -7
  104. package/dist/src/components/App.js +3 -3
  105. package/dist/src/config.d.ts +19 -0
  106. package/dist/src/config.js +156 -10
  107. package/dist/src/experience-tracker.d.ts +11 -6
  108. package/dist/src/experience-tracker.js +82 -155
  109. package/dist/src/explorbot.d.ts +21 -13
  110. package/dist/src/explorbot.js +84 -80
  111. package/dist/src/explorer.d.ts +66 -102
  112. package/dist/src/explorer.js +332 -623
  113. package/dist/src/knowledge-tracker.d.ts +2 -2
  114. package/dist/src/knowledge-tracker.js +38 -45
  115. package/dist/src/state-manager.d.ts +10 -30
  116. package/dist/src/state-manager.js +11 -129
  117. package/dist/src/utils/aria.d.ts +6 -1
  118. package/dist/src/utils/aria.js +4 -1
  119. package/dist/src/utils/cache.d.ts +15 -0
  120. package/dist/src/utils/cache.js +34 -0
  121. package/dist/src/utils/context-formatter.d.ts +6 -0
  122. package/dist/src/utils/context-formatter.js +15 -27
  123. package/dist/src/utils/hooks-runner.js +2 -4
  124. package/dist/src/utils/html.d.ts +5 -0
  125. package/dist/src/utils/html.js +71 -0
  126. package/dist/src/utils/markdown-files.d.ts +10 -0
  127. package/dist/src/utils/markdown-files.js +21 -0
  128. package/dist/src/utils/markdown-query.d.ts +6 -0
  129. package/dist/src/utils/markdown-query.js +14 -1
  130. package/dist/src/utils/page-readiness.d.ts +1 -0
  131. package/dist/src/utils/page-readiness.js +1 -1
  132. package/dist/src/utils/secrets.js +2 -2
  133. package/dist/src/utils/strings.d.ts +2 -0
  134. package/dist/src/utils/strings.js +10 -0
  135. package/dist/src/utils/test-files.js +1 -1
  136. package/dist/src/utils/web-annotate.d.ts +5 -0
  137. package/dist/src/utils/web-annotate.js +58 -0
  138. package/dist/src/utils/web-eidx.d.ts +2 -0
  139. package/dist/src/utils/web-eidx.js +20 -0
  140. package/dist/src/utils/web-element.d.ts +3 -1
  141. package/dist/src/utils/web-element.js +32 -0
  142. package/dist/src/utils/web-sandbox.d.ts +4 -0
  143. package/dist/src/utils/web-sandbox.js +37 -0
  144. package/models.json +30 -0
  145. package/package.json +23 -3
  146. package/src/action-result.ts +70 -173
  147. package/src/action.ts +38 -132
  148. package/src/ai/agent.ts +20 -0
  149. package/src/ai/captain/idle-mode.ts +1 -1
  150. package/src/ai/captain/test-mode.ts +1 -1
  151. package/src/ai/captain/web-mode.ts +18 -30
  152. package/src/ai/captain.ts +17 -42
  153. package/src/ai/conversation.ts +9 -32
  154. package/src/ai/driller.ts +53 -83
  155. package/src/ai/experience-compactor.ts +16 -35
  156. package/src/ai/fisherman-tools.ts +1 -1
  157. package/src/ai/fisherman.ts +0 -4
  158. package/src/ai/historian/codeceptjs.ts +5 -3
  159. package/src/ai/historian/playwright.ts +7 -4
  160. package/src/ai/historian/screencast.ts +4 -3
  161. package/src/ai/historian/utils.ts +0 -1
  162. package/src/ai/historian.ts +6 -8
  163. package/src/ai/navigator.ts +39 -70
  164. package/src/ai/pilot.ts +59 -61
  165. package/src/ai/planner.ts +76 -62
  166. package/src/ai/provider.ts +85 -146
  167. package/src/ai/quartermaster.ts +39 -45
  168. package/src/ai/rerunner.ts +27 -50
  169. package/src/ai/researcher/cache.ts +10 -16
  170. package/src/ai/researcher/coordinates.ts +4 -3
  171. package/src/ai/researcher/deep-analysis.ts +10 -11
  172. package/src/ai/researcher/locators.ts +66 -9
  173. package/src/ai/researcher/parser.ts +1 -1
  174. package/src/ai/researcher/research-result.ts +0 -18
  175. package/src/ai/researcher/sections.ts +9 -3
  176. package/src/ai/researcher.ts +42 -119
  177. package/src/ai/rules.ts +24 -13
  178. package/src/ai/session-analyst.ts +8 -5
  179. package/src/ai/task-agent.ts +45 -38
  180. package/src/ai/tester.ts +51 -129
  181. package/src/ai/tools.ts +145 -153
  182. package/src/command-handler.ts +1 -1
  183. package/src/commands/compact-command.ts +7 -8
  184. package/src/commands/context-aria-command.ts +1 -1
  185. package/src/commands/context-command.ts +8 -6
  186. package/src/commands/context-data-command.ts +2 -2
  187. package/src/commands/context-experience-command.ts +2 -2
  188. package/src/commands/context-html-command.ts +2 -2
  189. package/src/commands/context-knowledge-command.ts +2 -2
  190. package/src/commands/drill-command.ts +1 -1
  191. package/src/commands/experience-command.ts +1 -1
  192. package/src/commands/explore-command.ts +4 -3
  193. package/src/commands/freesail-command.ts +2 -2
  194. package/src/commands/knows-command.ts +1 -1
  195. package/src/commands/learn-command.ts +5 -3
  196. package/src/commands/path-command.ts +1 -1
  197. package/src/commands/research-command.ts +1 -1
  198. package/src/commands/test-command.ts +1 -1
  199. package/src/components/AddKnowledge.tsx +5 -7
  200. package/src/components/App.tsx +3 -3
  201. package/src/config.ts +185 -11
  202. package/src/experience-tracker.ts +89 -152
  203. package/src/explorbot.ts +88 -83
  204. package/src/explorer.ts +402 -693
  205. package/src/knowledge-tracker.ts +42 -49
  206. package/src/state-manager.ts +20 -155
  207. package/src/utils/aria.ts +11 -2
  208. package/src/utils/cache.ts +40 -0
  209. package/src/utils/context-formatter.ts +12 -15
  210. package/src/utils/hooks-runner.ts +2 -4
  211. package/src/utils/html.ts +79 -0
  212. package/src/utils/markdown-files.ts +30 -0
  213. package/src/utils/markdown-query.ts +15 -1
  214. package/src/utils/page-readiness.ts +1 -1
  215. package/src/utils/secrets.ts +2 -3
  216. package/src/utils/strings.ts +12 -0
  217. package/src/utils/test-files.ts +1 -1
  218. package/src/utils/web-annotate.ts +64 -0
  219. package/src/utils/web-eidx.ts +21 -0
  220. package/src/utils/web-element.ts +34 -0
  221. package/src/utils/web-sandbox.ts +43 -0
  222. package/dist/boat/api-tester/example/apibot.config.js +0 -30
@@ -1,20 +1,18 @@
1
1
  import fs from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { faker } from '@faker-js/faker';
4
3
  import { context, trace } from '@opentelemetry/api';
5
4
  import { container, recorder } from 'codeceptjs';
6
5
  import * as codeceptjs from 'codeceptjs';
7
- import { hopeThat, retryTo, tryTo, within } from 'codeceptjs/lib/effects';
8
- import step from 'codeceptjs/steps';
9
6
  import { ActionResult } from './action-result.js';
10
7
  import { clearActivity, setActivity } from "./activity.js";
11
8
  import { ConfigParser, outputPath } from './config.js';
12
9
  import { Observability } from "./observability.js";
13
- import { isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
14
- import { htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
10
+ import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
11
+ import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
15
12
  import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
16
- import { waitForPageReadiness } from "./utils/page-readiness.js";
13
+ import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
17
14
  import { safeFilename } from "./utils/strings.js";
15
+ import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
18
16
  const debugLog = createDebug('explorbot:action');
19
17
  const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
20
18
  class Action {
@@ -24,22 +22,20 @@ class Action {
24
22
  config;
25
23
  // action info
26
24
  action = null;
27
- expectation = null;
28
25
  lastError = null;
29
26
  playwrightHelper;
30
27
  playwrightGroupId = null;
31
28
  assertionSteps = [];
32
29
  recorder;
30
+ recovery;
33
31
  mainDocumentStatus = undefined;
34
- constructor(actor, stateManager, recorder) {
32
+ constructor(actor, stateManager, recorder, recovery) {
35
33
  this.actor = actor;
36
34
  this.stateManager = stateManager;
37
35
  this.config = ConfigParser.getInstance().getConfig();
38
36
  this.playwrightHelper = container.helpers('Playwright');
39
37
  this.recorder = recorder;
40
- }
41
- async caputrePageWithScreenshot() {
42
- return this.capturePageState({ includeScreenshot: true });
38
+ this.recovery = recovery || ((fn) => fn());
43
39
  }
44
40
  async saveScreenshot() {
45
41
  const currentState = this.stateManager.getCurrentState();
@@ -58,7 +54,13 @@ class Action {
58
54
  return undefined;
59
55
  }
60
56
  }
61
- async capturePageState({ includeScreenshot = false } = {}) {
57
+ async capturePageState(opts = {}) {
58
+ return this.recovery(() => this.captureOnce(opts));
59
+ }
60
+ async execute(code) {
61
+ return this.recovery(() => this.executeOnce(code));
62
+ }
63
+ async captureOnce({ includeScreenshot = false, codeBlock } = {}) {
62
64
  try {
63
65
  const currentState = this.stateManager.getCurrentState();
64
66
  const stateHash = currentState?.hash || 'screenshot';
@@ -145,11 +147,11 @@ class Action {
145
147
  ariaSnapshotFile,
146
148
  iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
147
149
  });
148
- this.stateManager.updateState(result);
150
+ this.stateManager.updateState(result, codeBlock);
149
151
  return result;
150
152
  }
151
153
  catch (err) {
152
- const msg = err instanceof Error ? err.message : String(err);
154
+ const msg = browserErrorMessage(err);
153
155
  if (isFatalBrowserError(err))
154
156
  throw err;
155
157
  debugLog('capturePageState failed with non-fatal error:', msg);
@@ -249,7 +251,7 @@ class Action {
249
251
  return [];
250
252
  }
251
253
  }
252
- async execute(code) {
254
+ async executeOnce(code) {
253
255
  let error = null;
254
256
  setActivity('🔎 Browsing...', 'action');
255
257
  let codeString = code.replace(/^\(I\) => /, '').trim();
@@ -272,21 +274,18 @@ class Action {
272
274
  }
273
275
  if (isPlaywright) {
274
276
  const page = this.playwrightHelper.page;
275
- const asyncFn = createSandboxedFunction(['page'], `return (async () => { ${sanitizedCode} })()`);
276
- await asyncFn(page);
277
+ await playwrightSandbox(page, sanitizedCode);
277
278
  await sleep(this.config.action?.delay || 500);
278
279
  }
279
280
  else {
280
- const codeFunction = createSandboxedFunction(['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker'], sanitizedCode);
281
- codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step, faker);
281
+ codeceptJSSandbox(this.actor, sanitizedCode);
282
282
  await recorder.add(() => sleep(this.config.action?.delay || 500));
283
283
  await recorder.promise();
284
284
  }
285
- const pageState = await this.capturePageState();
286
285
  if (executedSteps.length > 0) {
287
286
  codeString = executedSteps.join('\n');
288
287
  }
289
- this.stateManager.updateState(pageState, codeString);
288
+ const pageState = await this.captureOnce({ codeBlock: codeString });
290
289
  this.actionResult = pageState;
291
290
  this.assertionSteps = assertionSteps;
292
291
  }
@@ -313,98 +312,37 @@ class Action {
313
312
  }
314
313
  return this;
315
314
  }
316
- async expect(codeOrFunction) {
317
- const codeString = typeof codeOrFunction === 'string' ? codeOrFunction : codeOrFunction.toString();
318
- this.expectation = codeString.toString();
319
- const expectationPreview = sanitizeCodeBlock(codeString)
320
- .split('\n')
321
- .map((line) => line.trim())
322
- .filter(Boolean)
323
- .slice(0, 2)
324
- .join(' ');
325
- tag('step').log(`Expecting: ${expectationPreview || 'assertion'}`);
326
- try {
327
- debugLog('Executing expectation:', codeString);
328
- let codeFunction;
329
- if (typeof codeOrFunction === 'function') {
330
- codeFunction = codeOrFunction;
331
- }
332
- else {
333
- const sanitizedCode = sanitizeCodeBlock(codeString);
334
- if (!sanitizedCode) {
335
- throw new Error('No valid I.* commands found in code block');
336
- }
337
- codeFunction = createSandboxedFunction(['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step'], sanitizedCode);
338
- }
339
- codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step);
340
- await recorder.promise();
341
- debugLog('Expectation executed successfully');
342
- // Get current state from state manager
343
- const currentState = this.stateManager.getCurrentState();
344
- if (currentState) {
345
- // Create ActionResult from current state for compatibility
346
- this.actionResult = new ActionResult({
347
- url: currentState.fullUrl || '',
348
- title: currentState.title,
349
- timestamp: currentState.timestamp,
350
- html: '', // Empty HTML for expectation state
351
- });
352
- }
353
- return this;
354
- }
355
- catch (err) {
356
- tag('error').log('Expectation failed:', errorToString(err));
357
- this.lastError = err;
358
- await recorder.reset();
359
- await recorder.start();
360
- debugLog('Expectation failed:', errorToString(err));
361
- }
362
- finally {
363
- clearActivity();
364
- }
365
- return this;
366
- }
367
- async waitForInteraction() {
368
- // start with basic approach
369
- await this.actor.wait(0.5);
370
- return this;
371
- }
372
- async attempt(codeBlock, originalMessage, experience = true) {
315
+ async attempt(codeBlock, originalMessage) {
316
+ const wasInFrame = !!this.playwrightHelper.frame;
373
317
  try {
374
318
  debugLog('Resolution attempt...');
375
319
  setActivity('🦾 Acting in browser...', 'action');
376
320
  if (!this.actionResult) {
377
321
  this.actionResult = ActionResult.fromState(this.stateManager.getCurrentState());
378
322
  }
379
- const prevActionResult = this.actionResult;
380
323
  this.lastError = null;
381
324
  await this.execute(codeBlock);
382
- if (!this.expectation && originalMessage) {
383
- this.expectation = originalMessage;
384
- }
385
- if (!this.expectation) {
386
- return true;
387
- }
388
- debugLog('Resolved Expectation:', this.expectation);
389
325
  return true;
390
326
  }
391
327
  catch (error) {
392
328
  this.lastError = error;
393
329
  if (isFatalBrowserError(error))
394
330
  throw error;
331
+ if (!wasInFrame)
332
+ await this.exitIframe().catch(() => { });
395
333
  debugLog(`Attempt failed: ${codeBlock}: ${errorToString(error) || this.lastError?.toString()}`);
396
334
  return false;
397
335
  }
398
336
  }
337
+ async exitIframe() {
338
+ if (!this.playwrightHelper.frame)
339
+ return;
340
+ debugLog('Switching to main frame');
341
+ await this.playwrightHelper.switchTo();
342
+ }
399
343
  getActor() {
400
344
  return this.actor;
401
345
  }
402
- setActor(actor) {
403
- this.actor = actor;
404
- }
405
- getCurrentState() {
406
- return this.actionResult;
407
- }
408
346
  getActionResult() {
409
347
  return this.actionResult;
410
348
  }
@@ -423,6 +361,13 @@ function errorToString(error) {
423
361
  return error.message || error.toString();
424
362
  }
425
363
  async function captureHtml(page, frame, actor) {
364
+ for (const scope of [frame, page]) {
365
+ if (!scope?.evaluate)
366
+ continue;
367
+ const html = await scope.evaluate(captureHtmlForSnapshot).catch(() => null);
368
+ if (typeof html === 'string')
369
+ return html;
370
+ }
426
371
  if (frame?.content)
427
372
  return frame.content();
428
373
  if (page?.content)
@@ -438,27 +383,6 @@ async function captureTitle(page, actor) {
438
383
  return actor.grabTitle();
439
384
  return '';
440
385
  }
441
- const SHADOWED_GLOBALS = ['process', 'global', 'globalThis', 'fetch', 'Bun', 'require', 'module', 'exports'];
442
- export function sanitizeCodeBlock(code) {
443
- return code
444
- .split('\n')
445
- .map((line) => line.trim())
446
- .filter((line) => line.startsWith('I.') || line.startsWith('page.') || line.startsWith('await page.') || line.startsWith('await I.'))
447
- .join('\n');
448
- }
449
- export function createSandboxedFunction(argNames, body) {
450
- const fn = new Function(...argNames, ...SHADOWED_GLOBALS, `'use strict';\n${body}`);
451
- return (...args) => fn(...args);
452
- }
453
- function hasPlaywrightCommands(code) {
454
- return code.split('\n').some((line) => {
455
- const trimmed = line.trim();
456
- return trimmed.startsWith('page.') || trimmed.startsWith('await page.');
457
- });
458
- }
459
- function sleep(ms) {
460
- return new Promise((resolve) => setTimeout(resolve, ms));
461
- }
462
386
  const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
463
387
  const attachStepLogger = (target, assertionsTarget) => {
464
388
  const listener = (step, error) => {
@@ -1,3 +1,20 @@
1
+ import type { RequestStore } from '../api/request-store.js';
2
+ import type { ExplorbotConfig } from '../config.js';
3
+ import type Explorer from '../explorer.js';
4
+ import type { KnowledgeTracker } from '../knowledge-tracker.js';
5
+ import type { PlaywrightRecorder } from '../playwright-recorder.js';
6
+ import type { StateManager } from '../state-manager.js';
7
+ import type { AIProvider } from './provider.js';
1
8
  export interface Agent {
2
9
  emoji?: string;
3
10
  }
11
+ export interface AgentDeps {
12
+ explorer: Explorer;
13
+ ai: AIProvider;
14
+ config: ExplorbotConfig;
15
+ stateManager: StateManager;
16
+ knowledgeTracker: KnowledgeTracker;
17
+ requestStore: RequestStore;
18
+ playwrightRecorder: PlaywrightRecorder;
19
+ }
20
+ export type ToolDeps = Pick<AgentDeps, 'explorer' | 'stateManager' | 'ai'>;
@@ -60,7 +60,7 @@ export function WithIdleMode(Base) {
60
60
  if (!action || action === 'replace') {
61
61
  plan.tests.length = 0;
62
62
  }
63
- const currentUrl = ctx.explorBot.getExplorer().getStateManager().getCurrentState()?.url || '';
63
+ const currentUrl = ctx.explorBot.stateManager().getCurrentState()?.url || '';
64
64
  for (const testInput of tests) {
65
65
  const priority = testInput.priority || 'normal';
66
66
  const expected = testInput.expected?.length ? testInput.expected : [];
@@ -144,7 +144,7 @@ export function WithTestMode(Base) {
144
144
  states = t.states;
145
145
  }
146
146
  else {
147
- const stateManager = ctx.explorBot.getExplorer().getStateManager();
147
+ const stateManager = ctx.explorBot.stateManager();
148
148
  const history = stateManager.getStateHistory();
149
149
  const seen = new Set();
150
150
  states = history
@@ -1,7 +1,6 @@
1
1
  import { tool } from 'ai';
2
2
  import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
- import { ActionResult } from "../../action-result.js";
5
4
  import { actionRule, locatorRule, sectionContextRule } from "../rules.js";
6
5
  import { createAgentTools, createCodeceptJSTools } from "../tools.js";
7
6
  import { debugLog } from "./mixin.js";
@@ -9,17 +8,12 @@ export function WithWebMode(Base) {
9
8
  return class extends Base {
10
9
  webModeTools(ctx) {
11
10
  const explorer = ctx.explorBot.getExplorer();
12
- const stateManager = explorer.getStateManager();
13
- const codeceptTools = createCodeceptJSTools(explorer, ctx.task);
11
+ const toolDeps = { explorer, stateManager: ctx.explorBot.stateManager(), ai: ctx.explorBot.getProvider() };
12
+ const codeceptTools = createCodeceptJSTools(toolDeps, ctx.task);
14
13
  const agentTools = createAgentTools({
15
- explorer,
14
+ ...toolDeps,
16
15
  researcher: ctx.explorBot.agentResearcher(),
17
16
  navigator: ctx.explorBot.agentNavigator(),
18
- experienceTracker: stateManager.getExperienceTracker(),
19
- getState: () => {
20
- const state = stateManager.getCurrentState();
21
- return state ? ActionResult.fromState(state) : null;
22
- },
23
17
  });
24
18
  const { see, context, visualClick, learnExperience } = agentTools;
25
19
  return {
@@ -32,7 +26,7 @@ export function WithWebMode(Base) {
32
26
  try {
33
27
  debugLog('navigate', destination);
34
28
  await ctx.explorBot.agentNavigator().visit(destination);
35
- const stateManager = ctx.explorBot.getExplorer().getStateManager();
29
+ const stateManager = ctx.explorBot.stateManager();
36
30
  const state = stateManager.getCurrentState();
37
31
  return { success: true, url: state?.url, title: state?.title };
38
32
  }
@@ -50,43 +44,38 @@ export function WithWebMode(Base) {
50
44
  - closeTabs: Close all browser tabs except the current one
51
45
  - reload: Reload the current page
52
46
  - screenshot: Take a screenshot of current page
53
- - recover: Recover from a closed/crashed page using Explorer recovery
54
- - restart: Restart the browser when page/context recovery is not enough
55
- - openFreshTab: Open a fresh tab in the current browser context
47
+ - recover: Recover from a closed/crashed page, escalating to a full browser restart when needed
48
+ - openTab: Open a fresh tab in the current browser context
56
49
  `,
57
50
  inputSchema: z.object({
58
- action: z.enum(['status', 'evaluate', 'closeTabs', 'reload', 'screenshot', 'recover', 'restart', 'openFreshTab']).describe('Browser action to perform'),
51
+ action: z.enum(['status', 'evaluate', 'closeTabs', 'reload', 'screenshot', 'recover', 'openTab']).describe('Browser action to perform'),
59
52
  code: z.string().optional().describe('JavaScript code for evaluate action'),
60
53
  }),
61
54
  execute: async ({ action, code }) => {
62
55
  const explorer = ctx.explorBot.getExplorer();
63
56
  if (action === 'status') {
64
- const page = explorer.playwrightHelper?.page;
57
+ const page = explorer.page;
65
58
  const pages = page?.context?.().pages?.() || [];
66
59
  return {
67
60
  success: true,
68
61
  hasPage: !!page,
69
- isClosed: page?.isClosed?.() || false,
70
- url: page && !page.isClosed?.() ? await page.url() : null,
71
- title: page && !page.isClosed?.() ? await page.title().catch(() => null) : null,
62
+ isClosed: !page,
63
+ url: page ? await page.url() : null,
64
+ title: page ? await page.title().catch(() => null) : null,
72
65
  tabs: pages.length,
73
66
  };
74
67
  }
75
68
  if (action === 'recover') {
76
- const recovered = await explorer.recoverFromBrowserError();
77
- return { success: recovered, message: recovered ? 'Browser page recovered' : 'Browser recovery failed' };
69
+ const { ok } = await explorer.recover();
70
+ return { success: ok, message: ok ? 'Browser page recovered' : 'Browser recovery failed' };
78
71
  }
79
- if (action === 'restart') {
80
- const restarted = await explorer.restartBrowser();
81
- return { success: restarted, message: restarted ? 'Browser restarted' : 'Browser restart failed' };
82
- }
83
- if (action === 'openFreshTab') {
84
- await ctx.explorBot.openFreshTab();
85
- const state = explorer.getStateManager().getCurrentState();
72
+ if (action === 'openTab') {
73
+ await ctx.explorBot.openTab();
74
+ const state = ctx.explorBot.stateManager().getCurrentState();
86
75
  return { success: true, url: state?.url, title: state?.title };
87
76
  }
88
- const page = explorer.playwrightHelper?.page;
89
- if (!page || page.isClosed?.())
77
+ const page = explorer.page;
78
+ if (!page)
90
79
  return { success: false, message: 'No browser page available. Try browser({ action: "recover" }) first.' };
91
80
  if (action === 'evaluate') {
92
81
  if (!code)
@@ -1,4 +1,4 @@
1
- import { ExperienceTracker } from '../experience-tracker.js';
1
+ import type { ExperienceTracker } from '../experience-tracker.js';
2
2
  import type { ExplorBot } from '../explorbot.js';
3
3
  import type { WebPageState } from '../state-manager.js';
4
4
  import { Task, Test } from '../test-plan.js';
@@ -55,8 +55,6 @@ export declare class Captain extends CaptainBase implements Agent {
55
55
  systemPrompt(): string;
56
56
  resetConversation(): Conversation;
57
57
  ensureConversation(): Conversation;
58
- getConversation(): Conversation | null;
59
- cleanConversation(): void;
60
58
  getPageContext(): Promise<string>;
61
59
  planSummary(): string;
62
60
  reinjectContextIfNeeded(conversation: Conversation, currentState: WebPageState): Promise<void>;
@@ -2,8 +2,8 @@ import { tool } from 'ai';
2
2
  import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from '../action-result.js';
5
- import { ExperienceTracker } from '../experience-tracker.js';
6
5
  import { Task } from "../test-plan.js";
6
+ import { formatHeadings } from "../utils/context-formatter.js";
7
7
  import { HooksRunner } from "../utils/hooks-runner.js";
8
8
  import { startLogCapture, stopLogCapture, tag } from '../utils/logger.js';
9
9
  import { loop } from '../utils/loop.js';
@@ -28,7 +28,7 @@ export class Captain extends CaptainBase {
28
28
  constructor(explorBot) {
29
29
  super();
30
30
  this.explorBot = explorBot;
31
- this.experienceTracker = new ExperienceTracker();
31
+ this.experienceTracker = explorBot.experienceTracker();
32
32
  }
33
33
  setCommandExecutor(fn, descriptions) {
34
34
  this.commandExecutor = fn;
@@ -37,7 +37,7 @@ export class Captain extends CaptainBase {
37
37
  getHooksRunner() {
38
38
  if (!this.hooksRunner) {
39
39
  const explorer = this.explorBot.getExplorer();
40
- this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
40
+ this.hooksRunner = new HooksRunner(explorer, this.explorBot.getConfig());
41
41
  }
42
42
  return this.hooksRunner;
43
43
  }
@@ -48,7 +48,7 @@ export class Captain extends CaptainBase {
48
48
  return this.experienceTracker;
49
49
  }
50
50
  getKnowledgeTracker() {
51
- return this.explorBot.getExplorer().getKnowledgeTracker();
51
+ return this.explorBot.knowledgeTracker();
52
52
  }
53
53
  getProvider() {
54
54
  return this.explorBot.getProvider();
@@ -72,18 +72,17 @@ export class Captain extends CaptainBase {
72
72
  getMode() {
73
73
  const explorer = this.explorBot.getExplorer();
74
74
  const activeTest = explorer.activeTest;
75
- const page = explorer.playwrightHelper?.page;
76
- if (activeTest && (!page || page.isClosed?.()))
75
+ if (activeTest && !explorer.page)
77
76
  return 'heal';
78
77
  if (activeTest)
79
78
  return 'test';
80
- if (explorer.getStateManager().getCurrentState())
79
+ if (this.explorBot.stateManager().getCurrentState())
81
80
  return 'web';
82
81
  return 'idle';
83
82
  }
84
83
  systemPrompt() {
85
84
  const mode = this.getMode();
86
- const currentUrl = this.explorBot.getExplorer().getStateManager().getCurrentState()?.url;
85
+ const currentUrl = this.explorBot.stateManager().getCurrentState()?.url;
87
86
  const customPrompt = this.explorBot.getProvider().getSystemPromptForAgent('captain', currentUrl);
88
87
  return dedent `
89
88
  <role>
@@ -126,30 +125,15 @@ export class Captain extends CaptainBase {
126
125
  }
127
126
  return this.conversation;
128
127
  }
129
- getConversation() {
130
- return this.conversation;
131
- }
132
- cleanConversation() {
133
- this.conversation = null;
134
- tag('info').log('Conversation cleaned');
135
- }
136
128
  async getPageContext() {
137
- const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
129
+ const state = this.explorBot.stateManager().getCurrentState();
138
130
  if (!state) {
139
131
  return 'No page loaded';
140
132
  }
141
133
  const actionResult = ActionResult.fromState(state);
142
134
  const knowledge = this.getKnowledge(actionResult);
143
135
  const experience = this.getExperience(actionResult);
144
- const headingLines = [];
145
- if (state.h1)
146
- headingLines.push(`H1: ${state.h1}`);
147
- if (state.h2)
148
- headingLines.push(`H2: ${state.h2}`);
149
- if (state.h3)
150
- headingLines.push(`H3: ${state.h3}`);
151
- if (state.h4)
152
- headingLines.push(`H4: ${state.h4}`);
136
+ const headingLines = formatHeadings(state);
153
137
  const headingsBlock = headingLines.join('\n');
154
138
  let pageSummary = '';
155
139
  const cachedResearch = Researcher.getCachedResearch(state);
@@ -212,10 +196,9 @@ export class Captain extends CaptainBase {
212
196
  `;
213
197
  }
214
198
  async reinjectContextIfNeeded(conversation, currentState) {
215
- if (conversation.hasTag('page_html', 5))
216
- return Promise.resolve();
199
+ if (conversation.hasTag('page_aria', 5))
200
+ return;
217
201
  const actionResult = ActionResult.fromState(currentState);
218
- const html = await actionResult.combinedHtml();
219
202
  const context = dedent `
220
203
  Context:
221
204
 
@@ -227,13 +210,8 @@ export class Captain extends CaptainBase {
227
210
  <page_aria>
228
211
  ${actionResult.getInteractiveARIA()}
229
212
  </page_aria>
230
-
231
- <page_html>
232
- ${html}
233
- </page_html>
234
213
  `;
235
214
  conversation.addUserText(context);
236
- return Promise.resolve();
237
215
  }
238
216
  coreTools(task, onDone) {
239
217
  return {
@@ -263,15 +241,8 @@ export class Captain extends CaptainBase {
263
241
  runCommand: tool({
264
242
  description: dedent `
265
243
  Execute a TUI command. Returns log output from command execution.
266
- Use only when the user explicitly asks to run a slash command.
267
- Never use this to analyze files, reports, logs, plans, generated tests, knowledge, or experience.
268
- Never run a slash command unless the user request itself starts with that slash command.
269
- ${this.commandDescriptions
270
- .map((c) => {
271
- const opts = c.options ? ` (${c.options})` : '';
272
- return `${c.name} — ${c.description}${opts}`;
273
- })
274
- .join('\n')}
244
+ Use only when the user request itself starts with a slash command; pass it through verbatim.
245
+ Available commands: ${this.commandDescriptions.map((c) => c.name).join(', ')}
275
246
  `,
276
247
  inputSchema: z.object({
277
248
  command: z.string().describe('Slash command to execute, e.g. "/research", "/plan authentication", "/test brave-fox123"'),
@@ -329,7 +300,7 @@ export class Captain extends CaptainBase {
329
300
  recentToolSummary = execs.map((e) => `${e.toolName}: ${e.wasSuccessful ? 'ok' : 'fail'} ${truncateJson(e.input)}`).join('\n');
330
301
  }
331
302
  const pilotAnalysis = this.explorBot.agentPilot().getLastAnalysis() || '';
332
- const currentUrl = this.explorBot.getExplorer().getStateManager().getCurrentState()?.url || '';
303
+ const currentUrl = this.explorBot.stateManager().getCurrentState()?.url || '';
333
304
  const schema = z.object({
334
305
  action: z.enum(['inject', 'stop', 'pass', 'skip']),
335
306
  message: z.string().describe('Message to pass to the tester or display to user'),
@@ -371,7 +342,7 @@ export class Captain extends CaptainBase {
371
342
  }
372
343
  async processExecutionError(error, activeTest) {
373
344
  const explorer = this.explorBot.getExplorer();
374
- const result = await explorer.handleExecutionError(error);
345
+ const result = await explorer.recover(error);
375
346
  return {
376
347
  ...result,
377
348
  message: result.recovered ? `${result.message}\nContinue the test "${activeTest.scenario}" from the restored page.` : result.message,
@@ -381,7 +352,7 @@ export class Captain extends CaptainBase {
381
352
  return (this.recentToolCalls || []).some(hasBrowserCompletionEvidence);
382
353
  }
383
354
  async handle(input, options = {}) {
384
- const stateManager = this.explorBot.getExplorer().getStateManager();
355
+ const stateManager = this.explorBot.stateManager();
385
356
  const initialState = stateManager.getCurrentState();
386
357
  const conversation = options.reset ? this.resetConversation() : this.ensureConversation();
387
358
  let isDone = false;
@@ -441,6 +412,7 @@ export class Captain extends CaptainBase {
441
412
  const result = await this.explorBot.getProvider().invokeConversation(conversation, tools, {
442
413
  maxToolRoundtrips: 5,
443
414
  toolChoice: 'auto',
415
+ stopWhen: () => isDone,
444
416
  });
445
417
  if (!result) {
446
418
  stop();
@@ -5,18 +5,16 @@ export interface ToolExecution {
5
5
  output: any;
6
6
  wasSuccessful: boolean;
7
7
  }
8
+ export declare function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution;
8
9
  export declare function toolExecutionLabel(input: Record<string, any> | undefined): string;
9
10
  export declare class Conversation {
10
- id: string;
11
11
  messages: ModelMessage[];
12
12
  model: any;
13
- telemetryFunctionId?: string;
14
13
  protectedPrefixCount: number;
15
14
  autoTrimRules: Map<string, number>;
16
- constructor(messages?: ModelMessage[], model?: any, telemetryFunctionId?: string);
15
+ constructor(messages?: ModelMessage[], model?: any);
17
16
  protectPrefix(count: number): void;
18
17
  addUserText(text: string): void;
19
- addUserImage(image: string): void;
20
18
  addAssistantText(text: string): void;
21
19
  getLastMessage(): string;
22
20
  clone(): Conversation;
@@ -26,6 +24,5 @@ export declare class Conversation {
26
24
  markLastMessageCacheable(): void;
27
25
  hasTag(tagName: string, lastN?: number): boolean;
28
26
  applyAutoTrim(text: string): string;
29
- generateId(): string;
30
27
  getToolExecutions(): ToolExecution[];
31
28
  }