explorbot 0.1.32-beta.1 → 0.2.0

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 (168) hide show
  1. package/README.md +14 -14
  2. package/bin/explorbot-cli.ts +4 -3
  3. package/boat/doc-collector/src/ai/documentarian.ts +22 -27
  4. package/boat/doc-collector/src/ai/tools.ts +67 -10
  5. package/boat/doc-collector/src/docbot.ts +28 -7
  6. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  7. package/boat/doc-collector/src/screenshots.ts +33 -1
  8. package/boat/doc-collector/src/state-diagram.ts +281 -0
  9. package/dist/bin/explorbot-cli.js +3 -3
  10. package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
  11. package/dist/boat/doc-collector/src/ai/tools.js +47 -10
  12. package/dist/boat/doc-collector/src/docbot.js +25 -6
  13. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  14. package/dist/boat/doc-collector/src/screenshots.js +29 -0
  15. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  16. package/dist/package.json +20 -3
  17. package/dist/src/action-result.d.ts +8 -10
  18. package/dist/src/action-result.js +72 -164
  19. package/dist/src/action.d.ts +3 -10
  20. package/dist/src/action.js +18 -111
  21. package/dist/src/ai/captain/web-mode.js +0 -7
  22. package/dist/src/ai/captain.d.ts +1 -3
  23. package/dist/src/ai/captain.js +4 -19
  24. package/dist/src/ai/conversation.d.ts +2 -5
  25. package/dist/src/ai/conversation.js +9 -28
  26. package/dist/src/ai/driller.d.ts +0 -4
  27. package/dist/src/ai/driller.js +3 -12
  28. package/dist/src/ai/experience-compactor.d.ts +2 -2
  29. package/dist/src/ai/experience-compactor.js +16 -33
  30. package/dist/src/ai/fisherman-tools.js +1 -1
  31. package/dist/src/ai/fisherman.d.ts +0 -1
  32. package/dist/src/ai/fisherman.js +0 -3
  33. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  34. package/dist/src/ai/historian/codeceptjs.js +3 -2
  35. package/dist/src/ai/historian/playwright.d.ts +2 -0
  36. package/dist/src/ai/historian/playwright.js +3 -2
  37. package/dist/src/ai/historian/utils.d.ts +0 -1
  38. package/dist/src/ai/historian/utils.js +0 -1
  39. package/dist/src/ai/historian.d.ts +5 -4
  40. package/dist/src/ai/historian.js +1 -5
  41. package/dist/src/ai/navigator.d.ts +3 -5
  42. package/dist/src/ai/navigator.js +11 -46
  43. package/dist/src/ai/pilot.d.ts +1 -3
  44. package/dist/src/ai/pilot.js +6 -17
  45. package/dist/src/ai/planner.d.ts +3 -3
  46. package/dist/src/ai/planner.js +49 -37
  47. package/dist/src/ai/provider.d.ts +4 -1
  48. package/dist/src/ai/provider.js +77 -136
  49. package/dist/src/ai/quartermaster.d.ts +21 -22
  50. package/dist/src/ai/quartermaster.js +4 -10
  51. package/dist/src/ai/rerunner.d.ts +1 -2
  52. package/dist/src/ai/rerunner.js +14 -22
  53. package/dist/src/ai/researcher/cache.js +11 -18
  54. package/dist/src/ai/researcher/deep-analysis.js +3 -6
  55. package/dist/src/ai/researcher/parser.js +1 -1
  56. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  57. package/dist/src/ai/researcher/research-result.js +0 -19
  58. package/dist/src/ai/researcher/sections.js +5 -1
  59. package/dist/src/ai/researcher.js +12 -53
  60. package/dist/src/ai/rules.d.ts +1 -0
  61. package/dist/src/ai/rules.js +22 -15
  62. package/dist/src/ai/session-analyst.js +8 -5
  63. package/dist/src/ai/task-agent.d.ts +1 -1
  64. package/dist/src/ai/task-agent.js +2 -27
  65. package/dist/src/ai/tester.d.ts +0 -2
  66. package/dist/src/ai/tester.js +10 -61
  67. package/dist/src/ai/tools.d.ts +18 -7
  68. package/dist/src/ai/tools.js +119 -101
  69. package/dist/src/commands/compact-command.d.ts +1 -1
  70. package/dist/src/commands/compact-command.js +6 -6
  71. package/dist/src/commands/context-command.js +1 -1
  72. package/dist/src/commands/context-knowledge-command.js +1 -1
  73. package/dist/src/commands/experience-command.js +1 -1
  74. package/dist/src/commands/knows-command.js +1 -1
  75. package/dist/src/commands/learn-command.js +3 -1
  76. package/dist/src/components/AddKnowledge.d.ts +2 -0
  77. package/dist/src/components/AddKnowledge.js +3 -7
  78. package/dist/src/components/App.js +2 -2
  79. package/dist/src/config.d.ts +2 -0
  80. package/dist/src/config.js +12 -0
  81. package/dist/src/experience-tracker.d.ts +11 -6
  82. package/dist/src/experience-tracker.js +81 -154
  83. package/dist/src/explorbot.d.ts +4 -5
  84. package/dist/src/explorbot.js +17 -51
  85. package/dist/src/explorer.d.ts +4 -15
  86. package/dist/src/explorer.js +18 -68
  87. package/dist/src/knowledge-tracker.d.ts +2 -2
  88. package/dist/src/knowledge-tracker.js +38 -45
  89. package/dist/src/state-manager.d.ts +6 -31
  90. package/dist/src/state-manager.js +10 -129
  91. package/dist/src/utils/aria.d.ts +6 -1
  92. package/dist/src/utils/aria.js +4 -1
  93. package/dist/src/utils/cache.d.ts +15 -0
  94. package/dist/src/utils/cache.js +34 -0
  95. package/dist/src/utils/context-formatter.d.ts +6 -0
  96. package/dist/src/utils/context-formatter.js +15 -27
  97. package/dist/src/utils/html.d.ts +5 -0
  98. package/dist/src/utils/html.js +71 -0
  99. package/dist/src/utils/markdown-files.d.ts +10 -0
  100. package/dist/src/utils/markdown-files.js +21 -0
  101. package/dist/src/utils/markdown-query.d.ts +6 -0
  102. package/dist/src/utils/markdown-query.js +14 -1
  103. package/dist/src/utils/page-readiness.d.ts +1 -0
  104. package/dist/src/utils/page-readiness.js +1 -1
  105. package/dist/src/utils/secrets.js +2 -2
  106. package/dist/src/utils/strings.d.ts +2 -0
  107. package/dist/src/utils/strings.js +10 -0
  108. package/dist/src/utils/web-element.d.ts +2 -1
  109. package/dist/src/utils/web-element.js +3 -0
  110. package/dist/src/utils/web-sandbox.d.ts +4 -0
  111. package/dist/src/utils/web-sandbox.js +37 -0
  112. package/package.json +20 -3
  113. package/src/action-result.ts +70 -173
  114. package/src/action.ts +16 -130
  115. package/src/ai/captain/web-mode.ts +0 -7
  116. package/src/ai/captain.ts +5 -17
  117. package/src/ai/conversation.ts +9 -32
  118. package/src/ai/driller.ts +3 -16
  119. package/src/ai/experience-compactor.ts +16 -35
  120. package/src/ai/fisherman-tools.ts +1 -1
  121. package/src/ai/fisherman.ts +0 -4
  122. package/src/ai/historian/codeceptjs.ts +4 -2
  123. package/src/ai/historian/playwright.ts +4 -2
  124. package/src/ai/historian/utils.ts +0 -1
  125. package/src/ai/historian.ts +4 -7
  126. package/src/ai/navigator.ts +13 -49
  127. package/src/ai/pilot.ts +6 -16
  128. package/src/ai/planner.ts +56 -44
  129. package/src/ai/provider.ts +85 -145
  130. package/src/ai/quartermaster.ts +34 -41
  131. package/src/ai/rerunner.ts +17 -21
  132. package/src/ai/researcher/cache.ts +10 -16
  133. package/src/ai/researcher/deep-analysis.ts +3 -6
  134. package/src/ai/researcher/parser.ts +1 -1
  135. package/src/ai/researcher/research-result.ts +0 -18
  136. package/src/ai/researcher/sections.ts +5 -1
  137. package/src/ai/researcher.ts +11 -59
  138. package/src/ai/rules.ts +24 -13
  139. package/src/ai/session-analyst.ts +8 -5
  140. package/src/ai/task-agent.ts +3 -30
  141. package/src/ai/tester.ts +12 -73
  142. package/src/ai/tools.ts +119 -106
  143. package/src/commands/compact-command.ts +7 -8
  144. package/src/commands/context-command.ts +1 -1
  145. package/src/commands/context-knowledge-command.ts +1 -1
  146. package/src/commands/experience-command.ts +1 -1
  147. package/src/commands/knows-command.ts +1 -1
  148. package/src/commands/learn-command.ts +3 -1
  149. package/src/components/AddKnowledge.tsx +5 -7
  150. package/src/components/App.tsx +2 -2
  151. package/src/config.ts +12 -0
  152. package/src/experience-tracker.ts +88 -151
  153. package/src/explorbot.ts +17 -51
  154. package/src/explorer.ts +20 -72
  155. package/src/knowledge-tracker.ts +42 -49
  156. package/src/state-manager.ts +15 -156
  157. package/src/utils/aria.ts +11 -2
  158. package/src/utils/cache.ts +40 -0
  159. package/src/utils/context-formatter.ts +12 -15
  160. package/src/utils/html.ts +79 -0
  161. package/src/utils/markdown-files.ts +30 -0
  162. package/src/utils/markdown-query.ts +15 -1
  163. package/src/utils/page-readiness.ts +1 -1
  164. package/src/utils/secrets.ts +2 -3
  165. package/src/utils/strings.ts +12 -0
  166. package/src/utils/web-element.ts +4 -0
  167. package/src/utils/web-sandbox.ts +43 -0
  168. package/dist/boat/api-tester/example/apibot.config.js +0 -30
package/src/action.ts CHANGED
@@ -1,30 +1,21 @@
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
- import dedent from 'dedent';
10
6
  import { ActionResult } from './action-result.js';
11
7
  import { clearActivity, setActivity } from './activity.ts';
12
- import { ExperienceCompactor } from './ai/experience-compactor.js';
13
- import { Navigator } from './ai/navigator.js';
14
- import type { Provider } from './ai/provider.js';
15
8
  import { ConfigParser, outputPath } from './config.js';
16
9
  import type { ExplorbotConfig } from './config.js';
17
- import type { UserResolveFunction } from './explorbot.ts';
18
10
  import { Observability } from './observability.ts';
19
11
  import type { PlaywrightRecorder } from './playwright-recorder.ts';
20
12
  import type { StateManager } from './state-manager.js';
21
- import { isFatalBrowserError, isNavigationTransitionError } from './utils/browser-errors.ts';
22
- import { extractCodeBlocks } from './utils/code-extractor.js';
23
- import { htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
13
+ import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from './utils/browser-errors.ts';
14
+ import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
24
15
  import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
25
- import { waitForPageReadiness } from './utils/page-readiness.ts';
16
+ import { sleep, waitForPageReadiness } from './utils/page-readiness.ts';
17
+ import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from './utils/web-sandbox.ts';
26
18
  import { safeFilename } from './utils/strings.ts';
27
- import { throttle } from './utils/throttle.ts';
28
19
 
29
20
  const debugLog = createDebug('explorbot:action');
30
21
  const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
@@ -37,7 +28,6 @@ class Action {
37
28
 
38
29
  // action info
39
30
  private action: string | null = null;
40
- private expectation: string | null = null;
41
31
  public lastError: Error | null = null;
42
32
  public playwrightHelper: any;
43
33
  public playwrightGroupId: string | null = null;
@@ -53,10 +43,6 @@ class Action {
53
43
  this.recorder = recorder;
54
44
  }
55
45
 
56
- async caputrePageWithScreenshot(): Promise<ActionResult> {
57
- return this.capturePageState({ includeScreenshot: true });
58
- }
59
-
60
46
  async saveScreenshot(): Promise<string | undefined> {
61
47
  const currentState = this.stateManager.getCurrentState();
62
48
  if (currentState?.screenshotFile) return currentState.screenshotFile;
@@ -73,7 +59,7 @@ class Action {
73
59
  }
74
60
  }
75
61
 
76
- async capturePageState({ includeScreenshot = false }: { includeScreenshot?: boolean } = {}): Promise<ActionResult> {
62
+ async capturePageState({ includeScreenshot = false, codeBlock }: { includeScreenshot?: boolean; codeBlock?: string } = {}): Promise<ActionResult> {
77
63
  try {
78
64
  const currentState = this.stateManager.getCurrentState();
79
65
  const stateHash = currentState?.hash || 'screenshot';
@@ -166,10 +152,10 @@ class Action {
166
152
  ariaSnapshotFile,
167
153
  iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
168
154
  });
169
- this.stateManager.updateState(result);
155
+ this.stateManager.updateState(result, codeBlock);
170
156
  return result;
171
157
  } catch (err) {
172
- const msg = err instanceof Error ? err.message : String(err);
158
+ const msg = browserErrorMessage(err);
173
159
  if (isFatalBrowserError(err)) throw err;
174
160
  debugLog('capturePageState failed with non-fatal error:', msg);
175
161
  const url = this.playwrightHelper.page?.url?.() || '';
@@ -303,22 +289,19 @@ class Action {
303
289
 
304
290
  if (isPlaywright) {
305
291
  const page = this.playwrightHelper.page;
306
- const asyncFn = createSandboxedFunction(['page'], `return (async () => { ${sanitizedCode} })()`);
307
- await asyncFn(page);
292
+ await playwrightSandbox(page, sanitizedCode);
308
293
  await sleep(this.config.action?.delay || 500);
309
294
  } else {
310
- const codeFunction = createSandboxedFunction(['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker'], sanitizedCode);
311
- codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step, faker);
295
+ codeceptJSSandbox(this.actor, sanitizedCode);
312
296
  await recorder.add(() => sleep(this.config.action?.delay || 500));
313
297
  await recorder.promise();
314
298
  }
315
299
 
316
- const pageState = await this.capturePageState();
317
300
  if (executedSteps.length > 0) {
318
301
  codeString = executedSteps.join('\n');
319
302
  }
320
303
 
321
- this.stateManager.updateState(pageState, codeString);
304
+ const pageState = await this.capturePageState({ codeBlock: codeString });
322
305
 
323
306
  this.actionResult = pageState;
324
307
  this.assertionSteps = assertionSteps;
@@ -345,66 +328,7 @@ class Action {
345
328
  return this;
346
329
  }
347
330
 
348
- async expect(codeOrFunction: string | ((I: CodeceptJS.I) => void)): Promise<Action> {
349
- const codeString = typeof codeOrFunction === 'string' ? codeOrFunction : codeOrFunction.toString();
350
- this.expectation = codeString.toString();
351
- const expectationPreview = sanitizeCodeBlock(codeString)
352
- .split('\n')
353
- .map((line) => line.trim())
354
- .filter(Boolean)
355
- .slice(0, 2)
356
- .join(' ');
357
- tag('step').log(`Expecting: ${expectationPreview || 'assertion'}`);
358
- try {
359
- debugLog('Executing expectation:', codeString);
360
-
361
- let codeFunction: any;
362
- if (typeof codeOrFunction === 'function') {
363
- codeFunction = codeOrFunction;
364
- } else {
365
- const sanitizedCode = sanitizeCodeBlock(codeString);
366
- if (!sanitizedCode) {
367
- throw new Error('No valid I.* commands found in code block');
368
- }
369
- codeFunction = createSandboxedFunction(['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step'], sanitizedCode);
370
- }
371
- codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step);
372
- await recorder.promise();
373
- debugLog('Expectation executed successfully');
374
-
375
- // Get current state from state manager
376
- const currentState = this.stateManager.getCurrentState();
377
- if (currentState) {
378
- // Create ActionResult from current state for compatibility
379
- this.actionResult = new ActionResult({
380
- url: currentState.fullUrl || '',
381
- title: currentState.title,
382
- timestamp: currentState.timestamp,
383
- html: '', // Empty HTML for expectation state
384
- });
385
- }
386
-
387
- return this;
388
- } catch (err) {
389
- tag('error').log('Expectation failed:', errorToString(err));
390
- this.lastError = err as Error;
391
- await recorder.reset();
392
- await recorder.start();
393
- debugLog('Expectation failed:', errorToString(err));
394
- } finally {
395
- clearActivity();
396
- }
397
-
398
- return this;
399
- }
400
-
401
- public async waitForInteraction(): Promise<Action> {
402
- // start with basic approach
403
- await this.actor.wait(0.5);
404
- return this;
405
- }
406
-
407
- public async attempt(codeBlock: string, originalMessage?: string, experience = true): Promise<boolean> {
331
+ public async attempt(codeBlock: string, originalMessage?: string): Promise<boolean> {
408
332
  try {
409
333
  debugLog('Resolution attempt...');
410
334
  setActivity('🦾 Acting in browser...', 'action');
@@ -412,19 +336,9 @@ class Action {
412
336
  if (!this.actionResult) {
413
337
  this.actionResult = ActionResult.fromState(this.stateManager.getCurrentState()!);
414
338
  }
415
- const prevActionResult = this.actionResult;
416
339
  this.lastError = null;
417
340
  await this.execute(codeBlock);
418
341
 
419
- if (!this.expectation && originalMessage) {
420
- this.expectation = originalMessage;
421
- }
422
-
423
- if (!this.expectation) {
424
- return true;
425
- }
426
-
427
- debugLog('Resolved Expectation:', this.expectation);
428
342
  return true;
429
343
  } catch (error) {
430
344
  this.lastError = error as Error;
@@ -438,14 +352,6 @@ class Action {
438
352
  return this.actor;
439
353
  }
440
354
 
441
- setActor(actor: CodeceptJS.I): void {
442
- this.actor = actor;
443
- }
444
-
445
- getCurrentState(): ActionResult | null {
446
- return this.actionResult;
447
- }
448
-
449
355
  getActionResult(): ActionResult | null {
450
356
  return this.actionResult;
451
357
  }
@@ -468,6 +374,11 @@ function errorToString(error: any): string {
468
374
  }
469
375
 
470
376
  async function captureHtml(page: any, frame: any, actor: any): Promise<string> {
377
+ for (const scope of [frame, page]) {
378
+ if (!scope?.evaluate) continue;
379
+ const html = await scope.evaluate(captureHtmlForSnapshot).catch(() => null);
380
+ if (typeof html === 'string') return html;
381
+ }
471
382
  if (frame?.content) return frame.content();
472
383
  if (page?.content) return page.content();
473
384
  if (actor?.grabSource) return actor.grabSource();
@@ -480,31 +391,6 @@ async function captureTitle(page: any, actor: any): Promise<string> {
480
391
  return '';
481
392
  }
482
393
 
483
- const SHADOWED_GLOBALS = ['process', 'global', 'globalThis', 'fetch', 'Bun', 'require', 'module', 'exports'];
484
-
485
- export function sanitizeCodeBlock(code: string): string {
486
- return code
487
- .split('\n')
488
- .map((line) => line.trim())
489
- .filter((line) => line.startsWith('I.') || line.startsWith('page.') || line.startsWith('await page.') || line.startsWith('await I.'))
490
- .join('\n');
491
- }
492
-
493
- export function createSandboxedFunction(argNames: string[], body: string): (...args: any[]) => any {
494
- const fn = new Function(...argNames, ...SHADOWED_GLOBALS, `'use strict';\n${body}`);
495
- return (...args: any[]) => fn(...args);
496
- }
497
-
498
- function hasPlaywrightCommands(code: string): boolean {
499
- return code.split('\n').some((line) => {
500
- const trimmed = line.trim();
501
- return trimmed.startsWith('page.') || trimmed.startsWith('await page.');
502
- });
503
- }
504
- function sleep(ms: number) {
505
- return new Promise((resolve) => setTimeout(resolve, ms));
506
- }
507
-
508
394
  const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
509
395
 
510
396
  type StepListener = (step: any, error?: any) => void;
@@ -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.ts';
5
4
  import { actionRule, locatorRule, sectionContextRule } from '../rules.ts';
6
5
  import { createAgentTools, createCodeceptJSTools } from '../tools.ts';
7
6
  import { type Constructor, type ModeContext, debugLog } from './mixin.ts';
@@ -10,17 +9,11 @@ export function WithWebMode<T extends Constructor>(Base: T) {
10
9
  return class extends Base {
11
10
  webModeTools(ctx: ModeContext): Record<string, any> {
12
11
  const explorer = ctx.explorBot.getExplorer();
13
- const stateManager = explorer.getStateManager();
14
12
  const codeceptTools = createCodeceptJSTools(explorer, ctx.task);
15
13
  const agentTools = createAgentTools({
16
14
  explorer,
17
15
  researcher: ctx.explorBot.agentResearcher(),
18
16
  navigator: ctx.explorBot.agentNavigator(),
19
- experienceTracker: stateManager.getExperienceTracker(),
20
- getState: () => {
21
- const state = stateManager.getCurrentState();
22
- return state ? ActionResult.fromState(state) : null;
23
- },
24
17
  });
25
18
  const { see, context, visualClick, learnExperience } = agentTools;
26
19
 
package/src/ai/captain.ts CHANGED
@@ -2,10 +2,11 @@ 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';
5
+ import type { ExperienceTracker } from '../experience-tracker.js';
6
6
  import type { ExplorBot } from '../explorbot.ts';
7
7
  import type { WebPageState } from '../state-manager.ts';
8
8
  import { Task, Test } from '../test-plan.ts';
9
+ import { formatHeadings } from '../utils/context-formatter.ts';
9
10
  import { HooksRunner } from '../utils/hooks-runner.ts';
10
11
  import { startLogCapture, stopLogCapture, tag } from '../utils/logger.js';
11
12
  import { loop } from '../utils/loop.js';
@@ -37,7 +38,7 @@ export class Captain extends CaptainBase implements Agent {
37
38
  constructor(explorBot: ExplorBot) {
38
39
  super();
39
40
  this.explorBot = explorBot;
40
- this.experienceTracker = new ExperienceTracker();
41
+ this.experienceTracker = explorBot.experienceTracker();
41
42
  }
42
43
 
43
44
  setCommandExecutor(fn: (cmd: string) => Promise<void>, descriptions: { name: string; description: string; options: string }[]): void {
@@ -62,7 +63,7 @@ export class Captain extends CaptainBase implements Agent {
62
63
  }
63
64
 
64
65
  protected getKnowledgeTracker() {
65
- return this.explorBot.getExplorer().getKnowledgeTracker();
66
+ return this.explorBot.knowledgeTracker();
66
67
  }
67
68
 
68
69
  protected getProvider(): Provider {
@@ -145,15 +146,6 @@ export class Captain extends CaptainBase implements Agent {
145
146
  return this.conversation;
146
147
  }
147
148
 
148
- getConversation(): Conversation | null {
149
- return this.conversation;
150
- }
151
-
152
- cleanConversation(): void {
153
- this.conversation = null;
154
- tag('info').log('Conversation cleaned');
155
- }
156
-
157
149
  private async getPageContext(): Promise<string> {
158
150
  const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
159
151
  if (!state) {
@@ -164,11 +156,7 @@ export class Captain extends CaptainBase implements Agent {
164
156
  const knowledge = this.getKnowledge(actionResult);
165
157
  const experience = this.getExperience(actionResult);
166
158
 
167
- const headingLines: string[] = [];
168
- if (state.h1) headingLines.push(`H1: ${state.h1}`);
169
- if (state.h2) headingLines.push(`H2: ${state.h2}`);
170
- if (state.h3) headingLines.push(`H3: ${state.h3}`);
171
- if (state.h4) headingLines.push(`H4: ${state.h4}`);
159
+ const headingLines = formatHeadings(state);
172
160
  const headingsBlock = headingLines.join('\n');
173
161
 
174
162
  let pageSummary = '';
@@ -7,6 +7,12 @@ export interface ToolExecution {
7
7
  wasSuccessful: boolean;
8
8
  }
9
9
 
10
+ export function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution {
11
+ let output = rawOutput;
12
+ if (rawOutput?.type === 'json' && rawOutput?.value) output = rawOutput.value;
13
+ return { toolName, input, output, wasSuccessful: output?.success !== false };
14
+ }
15
+
10
16
  export function toolExecutionLabel(input: Record<string, any> | undefined): string {
11
17
  return input?.explanation || input?.assertion || input?.reason || input?.request || '';
12
18
  }
@@ -15,18 +21,14 @@ const AUTO_COMPACT_ARIA_CHANGES_CUTOFF = 500;
15
21
  const AUTO_COMPACT_TARGETED_HTML_CUTOFF = 500;
16
22
 
17
23
  export class Conversation {
18
- id: string;
19
24
  messages: ModelMessage[];
20
25
  model: any;
21
- telemetryFunctionId?: string;
22
26
  protectedPrefixCount = 0;
23
27
  private autoTrimRules: Map<string, number>;
24
28
 
25
- constructor(messages: ModelMessage[] = [], model?: any, telemetryFunctionId?: string) {
26
- this.id = this.generateId();
29
+ constructor(messages: ModelMessage[] = [], model?: any) {
27
30
  this.messages = messages;
28
31
  this.model = model || '';
29
- this.telemetryFunctionId = telemetryFunctionId;
30
32
  this.autoTrimRules = new Map();
31
33
  }
32
34
 
@@ -41,20 +43,6 @@ export class Conversation {
41
43
  });
42
44
  }
43
45
 
44
- addUserImage(image: string): void {
45
- if (!image || image.trim() === '') {
46
- console.warn('Warning: Attempting to add empty image to conversation');
47
- return;
48
- }
49
-
50
- const imageData = image.startsWith('data:') ? image : `data:image/png;base64,${image}`;
51
-
52
- this.messages.push({
53
- role: 'user',
54
- content: [{ type: 'file', mediaType: 'image/png', data: imageData }],
55
- });
56
- }
57
-
58
46
  addAssistantText(text: string): void {
59
47
  // Skip empty or whitespace-only messages
60
48
  if (!text || text.trim() === '') {
@@ -83,7 +71,7 @@ export class Conversation {
83
71
  }
84
72
 
85
73
  clone(): Conversation {
86
- return new Conversation([...this.messages], this.model, this.telemetryFunctionId);
74
+ return new Conversation([...this.messages], this.model);
87
75
  }
88
76
 
89
77
  cleanupTag(tagName: string, replacement: string, keepLast = 0): void {
@@ -222,10 +210,6 @@ export class Conversation {
222
210
  return result;
223
211
  }
224
212
 
225
- private generateId(): string {
226
- return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
227
- }
228
-
229
213
  getToolExecutions(): ToolExecution[] {
230
214
  const toolCalls = new Map<string, any>();
231
215
  for (const message of this.messages) {
@@ -243,14 +227,7 @@ export class Conversation {
243
227
  if (!Array.isArray(message.content)) continue;
244
228
  for (const part of message.content) {
245
229
  if (part.type !== 'tool-result') continue;
246
- const rawOutput = part.output as Record<string, any>;
247
- const output = rawOutput?.type === 'json' && rawOutput?.value ? rawOutput.value : rawOutput;
248
- executions.push({
249
- toolName: part.toolName,
250
- input: toolCalls.get(part.toolCallId) || {},
251
- output,
252
- wasSuccessful: output?.success !== false,
253
- });
230
+ executions.push(toToolExecution(part.toolName, toolCalls.get(part.toolCallId) || {}, part.output));
254
231
  }
255
232
  }
256
233
 
package/src/ai/driller.ts CHANGED
@@ -28,10 +28,9 @@ import { createDebug, tag } from '../utils/logger.ts';
28
28
  import { loop, pause } from '../utils/loop.ts';
29
29
  import { WebElement } from '../utils/web-element.ts';
30
30
  import type { Agent } from './agent.ts';
31
- import type { Conversation } from './conversation.ts';
32
31
  import type { Navigator } from './navigator.ts';
33
32
  import type { Provider } from './provider.ts';
34
- import { locatorRule } from './rules.ts';
33
+ import { drillLocatorRule } from './rules.ts';
35
34
  import { TaskAgent, isInteractive } from './task-agent.ts';
36
35
  import { createCodeceptJSTools } from './tools.ts';
37
36
 
@@ -85,7 +84,6 @@ export class Driller extends TaskAgent implements Agent {
85
84
  private navigator: Navigator;
86
85
  private hooksRunner: HooksRunner;
87
86
  private currentPlan?: Plan;
88
- private currentConversation: Conversation | null = null;
89
87
  private allResults: InteractionResult[] = [];
90
88
  private verifiedAction: { componentId: string; toolName: string; code?: string; canonicalCode?: string } | null = null;
91
89
  private pendingNestedContext: string | null = null;
@@ -323,7 +321,6 @@ export class Driller extends TaskAgent implements Agent {
323
321
  this.verifiedAction = null;
324
322
  this.pendingNestedContext = null;
325
323
  const conversation = this.provider.startConversation(this.getSystemMessage(component), 'driller');
326
- this.currentConversation = conversation;
327
324
  conversation.addUserText(await this.buildComponentPrompt(originalState, component));
328
325
 
329
326
  let finished = false;
@@ -606,11 +603,11 @@ export class Driller extends TaskAgent implements Agent {
606
603
  const action = this.explorer.createAction();
607
604
 
608
605
  if (currentState?.url !== originalState.url) {
609
- await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)`, false);
606
+ await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)`);
610
607
  return;
611
608
  }
612
609
 
613
- await action.attempt('I.pressKey("Escape")', `${reason} (restore state)`, false);
610
+ await action.attempt('I.pressKey("Escape")', `${reason} (restore state)`);
614
611
  }
615
612
 
616
613
  private async saveToExperience(state: ActionResult, results: InteractionResult[]): Promise<void> {
@@ -789,14 +786,6 @@ export class Driller extends TaskAgent implements Agent {
789
786
  tag('step').log(` ${status} ${componentTest.component?.name || test.scenario}`);
790
787
  }
791
788
  }
792
-
793
- getCurrentPlan(): Plan | undefined {
794
- return this.currentPlan;
795
- }
796
-
797
- getConversation(): Conversation | null {
798
- return this.currentConversation;
799
- }
800
789
  }
801
790
 
802
791
  function formatAriaNode(node: Record<string, unknown>): string {
@@ -1190,5 +1179,3 @@ function isInteractiveElement(element: WebElement): boolean {
1190
1179
  if (element.attrs['aria-haspopup'] || element.attrs['aria-expanded'] || element.attrs['aria-controls']) return true;
1191
1180
  return false;
1192
1181
  }
1193
-
1194
- const drillLocatorRule = locatorRule.replace(/<context_simplification>[\s\S]*?<\/context_simplification>/, '').trim();
@@ -1,18 +1,19 @@
1
1
  import { unlinkSync } from 'node:fs';
2
+ import { basename } from 'node:path';
2
3
  import dedent from 'dedent';
3
- import { type Tokens, marked } from 'marked';
4
4
  import { z } from 'zod';
5
5
  import { type ExperienceFile, type ExperienceTracker, RECENT_WINDOW_DAYS } from '../experience-tracker.js';
6
6
  import { Observability } from '../observability.js';
7
7
  import { createDebug, log, tag } from '../utils/logger.js';
8
8
  import { mdq } from '../utils/markdown-query.js';
9
+ import { isNonReusableCode } from '../utils/step-analyzer.js';
9
10
  import { generalizeUrl, hasDynamicUrlSegment } from '../utils/url-matcher.js';
10
11
  import type { Agent } from './agent.js';
11
12
  import type { Provider } from './provider.js';
12
13
 
13
14
  const debugLog = createDebug('explorbot:experience-compactor');
14
15
 
15
- export type { ExperienceFile };
16
+ export const COMPACT_MAX_LENGTH = 5000;
16
17
 
17
18
  interface MergeGroup {
18
19
  pattern: string;
@@ -23,7 +24,6 @@ export class ExperienceCompactor implements Agent {
23
24
  emoji = '🗜️';
24
25
  private provider: Provider;
25
26
  private experienceTracker: ExperienceTracker;
26
- private MAX_LENGTH = 5000;
27
27
 
28
28
  constructor(provider: Provider, experienceTracker: ExperienceTracker) {
29
29
  this.provider = provider;
@@ -32,7 +32,7 @@ export class ExperienceCompactor implements Agent {
32
32
 
33
33
  async compactExperience(experience: string): Promise<string> {
34
34
  const stripped = this.stripNonUsefulEntries(experience);
35
- if (stripped.length < this.MAX_LENGTH) {
35
+ if (stripped.length < COMPACT_MAX_LENGTH) {
36
36
  return stripped;
37
37
  }
38
38
 
@@ -129,7 +129,7 @@ export class ExperienceCompactor implements Agent {
129
129
  if (!url || url.startsWith('~')) continue;
130
130
  const generalized = generalizeUrl(url);
131
131
  if (generalized === url) continue;
132
- const stateHash = file.filePath.split('/').pop()?.replace('.md', '') || '';
132
+ const stateHash = basename(file.filePath, '.md');
133
133
  const newData = { ...file.data, url: `~${generalized}~`, mergedFrom: [url] };
134
134
  this.experienceTracker.writeExperienceFile(stateHash, file.content, newData);
135
135
  tag('substep').log(`Generalized URL: ${url} → ~${generalized}~`);
@@ -139,7 +139,7 @@ export class ExperienceCompactor implements Agent {
139
139
  }
140
140
 
141
141
  async compactFiles(files: ExperienceFile[], options?: { skipSmall?: boolean }): Promise<number> {
142
- const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >= this.MAX_LENGTH) : files;
142
+ const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >= COMPACT_MAX_LENGTH) : files;
143
143
 
144
144
  let compactedCount = 0;
145
145
  const total = workingSet.length;
@@ -151,7 +151,7 @@ export class ExperienceCompactor implements Agent {
151
151
 
152
152
  for (let i = 0; i < workingSet.length; i++) {
153
153
  const experience = workingSet[i];
154
- const shortName = experience.filePath.split('/').pop() || experience.filePath;
154
+ const shortName = basename(experience.filePath);
155
155
  const willReview = this.isRecent(experience);
156
156
 
157
157
  if (total > 1 && willReview) {
@@ -164,8 +164,8 @@ export class ExperienceCompactor implements Agent {
164
164
  content = await this.reviewExperienceQuality(content, experience.data);
165
165
  }
166
166
 
167
- if (content.length >= this.MAX_LENGTH) {
168
- if (total > 1) tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${this.MAX_LENGTH} chars)`);
167
+ if (content.length >= COMPACT_MAX_LENGTH) {
168
+ if (total > 1) tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${COMPACT_MAX_LENGTH} chars)`);
169
169
  const prompt = this.buildCompactionPrompt(content);
170
170
  const model = this.provider.getModelForAgent('experience-compactor');
171
171
  const response = await this.provider.chat(
@@ -181,7 +181,7 @@ export class ExperienceCompactor implements Agent {
181
181
 
182
182
  if (content === experience.content) continue;
183
183
 
184
- const stateHash = experience.filePath.split('/').pop()?.replace('.md', '') || '';
184
+ const stateHash = basename(experience.filePath, '.md');
185
185
  this.experienceTracker.writeExperienceFile(stateHash, content, experience.data);
186
186
  debugLog('Experience file compacted:', experience.filePath);
187
187
  compactedCount++;
@@ -314,7 +314,7 @@ export class ExperienceCompactor implements Agent {
314
314
  const [targetFile, ...sourceFiles] = group.files;
315
315
  const combinedContent = group.files.map((f) => f.content).join('\n\n---\n\n');
316
316
 
317
- const targetStateHash = targetFile.filePath.split('/').pop()?.replace('.md', '') || '';
317
+ const targetStateHash = basename(targetFile.filePath, '.md');
318
318
  const newFrontmatter = {
319
319
  ...targetFile.data,
320
320
  url: group.pattern,
@@ -350,7 +350,7 @@ export class ExperienceCompactor implements Agent {
350
350
  <rules>
351
351
  - Use markdown h2 headers only (##) - NO XML tags or wrappers in output
352
352
  - Merge similar flows to remove duplicates
353
- - Keep output under ${this.MAX_LENGTH} characters
353
+ - Keep output under ${COMPACT_MAX_LENGTH} characters
354
354
  - Be explicit and short - no proposals or explanations
355
355
 
356
356
  KEEP only:
@@ -438,28 +438,9 @@ export class ExperienceCompactor implements Agent {
438
438
  }
439
439
 
440
440
  function listSections(content: string): { title: string; raw: string }[] {
441
- const tokens = marked.lexer(content);
442
- const sections: { title: string; raw: string }[] = [];
443
-
444
- let currentHeading: string | null = null;
445
- let currentRaw = '';
446
-
447
- const flush = () => {
448
- if (currentHeading !== null) sections.push({ title: currentHeading, raw: currentRaw });
449
- };
450
-
451
- for (const token of tokens) {
452
- const raw = (token as any).raw || '';
453
- if (token.type === 'heading' && (token as Tokens.Heading).depth === 2) {
454
- flush();
455
- currentHeading = (token as Tokens.Heading).text.trim();
456
- currentRaw = raw;
457
- continue;
458
- }
459
- if (currentHeading !== null) currentRaw += raw;
460
- }
461
- flush();
462
- return sections;
441
+ const sections = mdq(content).query('section2').each();
442
+ const metas = mdq(content).query('section2').meta();
443
+ return sections.map((q, i) => ({ title: metas[i].text.trim(), raw: q.text() }));
463
444
  }
464
445
 
465
446
  function dropEmptySections(content: string): string {
@@ -483,7 +464,7 @@ function dropNonReusableSections(content: string): string {
483
464
 
484
465
  for (const section of sections) {
485
466
  const raw = section.text();
486
- if (!/\bI\.clickXY\s*\(/.test(raw)) continue;
467
+ if (!isNonReusableCode(raw)) continue;
487
468
  result = result.replace(raw, '');
488
469
  }
489
470
 
@@ -158,7 +158,7 @@ function extractKeyFields(body: any, result: Record<string, any> = {}, depth = 0
158
158
  }
159
159
 
160
160
  for (const [key, value] of Object.entries(body)) {
161
- if (value === null || value === undefined) continue;
161
+ if (value == null) continue;
162
162
  if (typeof value === 'object') {
163
163
  extractKeyFields(value, result, depth + 1);
164
164
  continue;
@@ -44,10 +44,6 @@ export class Fisherman implements Agent {
44
44
  return this.mode !== 'disabled';
45
45
  }
46
46
 
47
- getMode(): string {
48
- return this.mode;
49
- }
50
-
51
47
  async ensureReady(scopeUrl?: string): Promise<void> {
52
48
  await this.detectMode(scopeUrl);
53
49
  this.spec ??= await this.specLoader();
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ActionResult } from '../../action-result.ts';
4
4
  import { ConfigParser } from '../../config.ts';
5
- import { KnowledgeTracker } from '../../knowledge-tracker.ts';
5
+ import type { StateManager } from '../../state-manager.ts';
6
6
  import type { Plan } from '../../test-plan.ts';
7
7
  import { tag } from '../../utils/logger.ts';
8
8
  import { relativeToCwd } from '../../utils/next-steps.ts';
@@ -21,6 +21,7 @@ export interface CodeceptJSMethods {
21
21
  export function WithCodeceptJS<T extends Constructor>(Base: T) {
22
22
  return class extends Base {
23
23
  declare savedFiles: Set<string>;
24
+ declare stateManager?: StateManager;
24
25
 
25
26
  toCode(conversation: Conversation, scenario: string): string {
26
27
  const toolExecutions = conversation.getToolExecutions();
@@ -108,7 +109,8 @@ export function WithCodeceptJS<T extends Constructor>(Base: T) {
108
109
  }
109
110
 
110
111
  private getKnowledgeLines(url: string, indent = ' '): string[] {
111
- const knowledgeTracker = new KnowledgeTracker();
112
+ const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
113
+ if (!knowledgeTracker) return [];
112
114
  const state = new ActionResult({ url });
113
115
  const { wait, waitForElement, code } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement', 'code']);
114
116