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,22 @@
1
+ export function toToolExecution(toolName, input, rawOutput) {
2
+ let output = rawOutput;
3
+ if (rawOutput?.type === 'json' && rawOutput?.value)
4
+ output = rawOutput.value;
5
+ return { toolName, input, output, wasSuccessful: output?.success !== false };
6
+ }
1
7
  export function toolExecutionLabel(input) {
2
8
  return input?.explanation || input?.assertion || input?.reason || input?.request || '';
3
9
  }
4
10
  const AUTO_COMPACT_ARIA_CHANGES_CUTOFF = 500;
5
11
  const AUTO_COMPACT_TARGETED_HTML_CUTOFF = 500;
6
12
  export class Conversation {
7
- id;
8
13
  messages;
9
14
  model;
10
- telemetryFunctionId;
11
15
  protectedPrefixCount = 0;
12
16
  autoTrimRules;
13
- constructor(messages = [], model, telemetryFunctionId) {
14
- this.id = this.generateId();
17
+ constructor(messages = [], model) {
15
18
  this.messages = messages;
16
19
  this.model = model || '';
17
- this.telemetryFunctionId = telemetryFunctionId;
18
20
  this.autoTrimRules = new Map();
19
21
  }
20
22
  protectPrefix(count) {
@@ -26,17 +28,6 @@ export class Conversation {
26
28
  content: this.applyAutoTrim(text),
27
29
  });
28
30
  }
29
- addUserImage(image) {
30
- if (!image || image.trim() === '') {
31
- console.warn('Warning: Attempting to add empty image to conversation');
32
- return;
33
- }
34
- const imageData = image.startsWith('data:') ? image : `data:image/png;base64,${image}`;
35
- this.messages.push({
36
- role: 'user',
37
- content: [{ type: 'file', mediaType: 'image/png', data: imageData }],
38
- });
39
- }
40
31
  addAssistantText(text) {
41
32
  // Skip empty or whitespace-only messages
42
33
  if (!text || text.trim() === '') {
@@ -61,7 +52,7 @@ export class Conversation {
61
52
  return '';
62
53
  }
63
54
  clone() {
64
- return new Conversation([...this.messages], this.model, this.telemetryFunctionId);
55
+ return new Conversation([...this.messages], this.model);
65
56
  }
66
57
  cleanupTag(tagName, replacement, keepLast = 0) {
67
58
  const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -190,9 +181,6 @@ export class Conversation {
190
181
  }
191
182
  return result;
192
183
  }
193
- generateId() {
194
- return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
195
- }
196
184
  getToolExecutions() {
197
185
  const toolCalls = new Map();
198
186
  for (const message of this.messages) {
@@ -215,14 +203,7 @@ export class Conversation {
215
203
  for (const part of message.content) {
216
204
  if (part.type !== 'tool-result')
217
205
  continue;
218
- const rawOutput = part.output;
219
- const output = rawOutput?.type === 'json' && rawOutput?.value ? rawOutput.value : rawOutput;
220
- executions.push({
221
- toolName: part.toolName,
222
- input: toolCalls.get(part.toolCallId) || {},
223
- output,
224
- wasSuccessful: output?.success !== false,
225
- });
206
+ executions.push(toToolExecution(part.toolName, toolCalls.get(part.toolCallId) || {}, part.output));
226
207
  }
227
208
  }
228
209
  return executions;
@@ -1,14 +1,8 @@
1
1
  import { ActionResult } from '../action-result.js';
2
- import type { ExperienceTracker } from '../experience-tracker.js';
3
- import type Explorer from '../explorer.js';
4
- import type { KnowledgeTracker } from '../knowledge-tracker.js';
5
2
  import { Plan, Test } from '../test-plan.js';
6
- import { HooksRunner } from '../utils/hooks-runner.js';
7
3
  import { WebElement } from '../utils/web-element.js';
8
- import type { Agent } from './agent.js';
9
- import type { Conversation } from './conversation.js';
4
+ import type { Agent, AgentDeps } from './agent.js';
10
5
  import type { Navigator } from './navigator.js';
11
- import type { Provider } from './provider.js';
12
6
  import { TaskAgent } from './task-agent.js';
13
7
  interface ComponentInfo {
14
8
  id: string;
@@ -49,12 +43,8 @@ interface DrillOptions {
49
43
  export declare class Driller extends TaskAgent implements Agent {
50
44
  readonly ACTION_TOOLS: string[];
51
45
  emoji: string;
52
- explorer: Explorer;
53
- provider: Provider;
54
46
  navigator: Navigator;
55
- hooksRunner: HooksRunner;
56
47
  currentPlan?: Plan;
57
- currentConversation: Conversation | null;
58
48
  allResults: InteractionResult[];
59
49
  verifiedAction: {
60
50
  componentId: string;
@@ -64,11 +54,8 @@ export declare class Driller extends TaskAgent implements Agent {
64
54
  } | null;
65
55
  pendingNestedContext: string | null;
66
56
  MAX_COMPONENT_ITERATIONS: number;
67
- constructor(explorer: Explorer, provider: Provider, navigator: Navigator);
57
+ constructor(deps: AgentDeps, navigator: Navigator);
68
58
  getNavigator(): Navigator;
69
- getExperienceTracker(): ExperienceTracker;
70
- getKnowledgeTracker(): KnowledgeTracker;
71
- getProvider(): Provider;
72
59
  getSystemMessage(component?: ComponentInfo): string;
73
60
  drill(opts?: DrillOptions): Promise<Plan>;
74
61
  captureAnnotatedState(): Promise<ActionResult>;
@@ -153,8 +140,6 @@ export declare class Driller extends TaskAgent implements Agent {
153
140
  saveToKnowledge(knowledgePath: string, state: ActionResult, results: InteractionResult[]): Promise<void>;
154
141
  generateKnowledgeContent(state: ActionResult, interactions: InteractionResult[]): string;
155
142
  logSummary(): void;
156
- getCurrentPlan(): Plan | undefined;
157
- getConversation(): Conversation | null;
158
143
  }
159
144
  export declare function buildCanonicalClickCode(component: ComponentInfo): string;
160
145
  export declare function formatExperienceTitle(interaction: InteractionResult): string;
@@ -6,49 +6,34 @@ import { setActivity } from "../activity.js";
6
6
  import { Observability } from "../observability.js";
7
7
  import { Plan, Test, TestResult } from "../test-plan.js";
8
8
  import { collectInteractiveNodes } from "../utils/aria.js";
9
- import { HooksRunner } from "../utils/hooks-runner.js";
10
9
  import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, getComponentScopeHtmlExtractorSource, getVisibleOverlayHtmlExtractorSource, inferHtmlRole, } from "../utils/html.js";
11
10
  import { createDebug, tag } from "../utils/logger.js";
12
11
  import { loop, pause } from "../utils/loop.js";
12
+ import { annotatePageElements } from "../utils/web-annotate.js";
13
+ import { eidxInContainer } from "../utils/web-eidx.js";
13
14
  import { WebElement } from "../utils/web-element.js";
14
- import { locatorRule } from "./rules.js";
15
+ import { drillLocatorRule } from "./rules.js";
15
16
  import { TaskAgent, isInteractive } from "./task-agent.js";
16
17
  import { createCodeceptJSTools } from "./tools.js";
17
18
  const debugLog = createDebug('explorbot:driller');
18
19
  export class Driller extends TaskAgent {
19
20
  ACTION_TOOLS = ['click', 'pressKey', 'form'];
20
21
  emoji = 'D';
21
- explorer;
22
- provider;
23
22
  navigator;
24
- hooksRunner;
25
23
  currentPlan;
26
- currentConversation = null;
27
24
  allResults = [];
28
25
  verifiedAction = null;
29
26
  pendingNestedContext = null;
30
27
  MAX_COMPONENT_ITERATIONS = 12;
31
- constructor(explorer, provider, navigator) {
32
- super();
33
- this.explorer = explorer;
34
- this.provider = provider;
28
+ constructor(deps, navigator) {
29
+ super(deps);
35
30
  this.navigator = navigator;
36
- this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
37
31
  }
38
32
  getNavigator() {
39
33
  return this.navigator;
40
34
  }
41
- getExperienceTracker() {
42
- return this.explorer.getStateManager().getExperienceTracker();
43
- }
44
- getKnowledgeTracker() {
45
- return this.explorer.getKnowledgeTracker();
46
- }
47
- getProvider() {
48
- return this.provider;
49
- }
50
35
  getSystemMessage(component) {
51
- const currentUrl = this.explorer.getStateManager().getCurrentState()?.url;
36
+ const currentUrl = this.stateManager.getCurrentState()?.url;
52
37
  const customPrompt = this.provider.getSystemPromptForAgent('driller', currentUrl);
53
38
  return dedent `
54
39
  <role>
@@ -90,7 +75,7 @@ export class Driller extends TaskAgent {
90
75
  }
91
76
  async drill(opts = {}) {
92
77
  const { knowledgePath, maxComponents = 30, interactive = isInteractive() } = opts;
93
- const currentState = this.explorer.getStateManager().getCurrentState();
78
+ const currentState = this.stateManager.getCurrentState();
94
79
  if (!currentState)
95
80
  throw new Error('No page state available');
96
81
  const sessionName = `driller_${Date.now().toString(36)}`;
@@ -131,15 +116,15 @@ export class Driller extends TaskAgent {
131
116
  }
132
117
  async captureAnnotatedState() {
133
118
  setActivity(`${this.emoji} Capturing annotated page state...`, 'action');
134
- const action = this.explorer.createAction();
119
+ const action = this.explorer.action();
135
120
  try {
136
121
  const annotated = await Promise.race([
137
- this.explorer.annotateElements(),
122
+ this.explorer.withPage(annotatePageElements),
138
123
  new Promise((_, reject) => {
139
124
  setTimeout(() => reject(new Error('annotateElements timeout')), 15000);
140
125
  }),
141
126
  ]);
142
- return action.capturePageState({ ariaSnapshot: annotated.ariaSnapshot });
127
+ return action.capturePageState();
143
128
  }
144
129
  catch (error) {
145
130
  tag('warning').log(`Annotated capture failed, falling back to plain page state: ${error instanceof Error ? error.message : error}`);
@@ -151,9 +136,10 @@ export class Driller extends TaskAgent {
151
136
  }
152
137
  async collectComponents(state, maxComponents) {
153
138
  setActivity(`${this.emoji} Collecting components...`, 'action');
154
- const page = this.explorer.playwrightHelper.page;
155
- const eidxList = await this.explorer.getEidxInContainer(null);
156
- const webElements = await WebElement.fromEidxList(page, eidxList);
139
+ const webElements = await this.explorer.withPage(async (page) => {
140
+ const eidxList = await eidxInContainer(page, null);
141
+ return WebElement.fromEidxList(page, eidxList);
142
+ });
157
143
  const ariaNodes = collectInteractiveNodes(state.ariaSnapshot);
158
144
  const scored = webElements
159
145
  .filter((element) => isDrillableElement(element))
@@ -245,16 +231,15 @@ export class Driller extends TaskAgent {
245
231
  this.verifiedAction = null;
246
232
  this.pendingNestedContext = null;
247
233
  const conversation = this.provider.startConversation(this.getSystemMessage(component), 'driller');
248
- this.currentConversation = conversation;
249
234
  conversation.addUserText(await this.buildComponentPrompt(originalState, component));
250
235
  let finished = false;
251
- const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.explorer, test), component);
236
+ const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.toolDeps, test), component);
252
237
  const tools = { ...actionTools, ...this.createDrillFlowTools(originalState, test, interactive) };
253
238
  await loop(async ({ stop, iteration }) => {
254
239
  debugLog(`Drilling component ${component.name}, iteration ${iteration}`);
255
240
  setActivity(`${this.emoji} Drilling ${component.name}...`, 'action');
256
241
  if (iteration > 1) {
257
- const currentState = ActionResult.fromState(this.explorer.getStateManager().getCurrentState() || originalState);
242
+ const currentState = ActionResult.fromState(this.stateManager.getCurrentState() || originalState);
258
243
  conversation.addUserText(await this.buildContextUpdate(currentState, component));
259
244
  if (this.pendingNestedContext) {
260
245
  conversation.addUserText(this.pendingNestedContext);
@@ -481,7 +466,7 @@ export class Driller extends TaskAgent {
481
466
  execute: async ({ reason }) => {
482
467
  await this.restoreOriginalState(originalState, reason);
483
468
  await this.captureAnnotatedState();
484
- const currentState = this.explorer.getStateManager().getCurrentState();
469
+ const currentState = this.stateManager.getCurrentState();
485
470
  return { success: true, url: currentState?.url || originalState.url };
486
471
  },
487
472
  }),
@@ -502,14 +487,14 @@ export class Driller extends TaskAgent {
502
487
  };
503
488
  }
504
489
  async restoreOriginalState(originalState, reason) {
505
- const currentState = this.explorer.getStateManager().getCurrentState();
490
+ const currentState = this.stateManager.getCurrentState();
506
491
  const targetUrl = originalState.fullUrl || originalState.url;
507
- const action = this.explorer.createAction();
492
+ const action = this.explorer.action();
508
493
  if (currentState?.url !== originalState.url) {
509
- await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)`, false);
494
+ await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)`);
510
495
  return;
511
496
  }
512
- await action.attempt('I.pressKey("Escape")', `${reason} (restore state)`, false);
497
+ await action.attempt('I.pressKey("Escape")', `${reason} (restore state)`);
513
498
  }
514
499
  async saveToExperience(state, results) {
515
500
  const experienceTracker = this.getExperienceTracker();
@@ -560,7 +545,7 @@ export class Driller extends TaskAgent {
560
545
  const overlayHtml = await this.getVisibleOverlayHtml();
561
546
  if (!overlayHtml)
562
547
  return null;
563
- const state = this.explorer.getStateManager().getCurrentState();
548
+ const state = this.stateManager.getCurrentState();
564
549
  if (!state)
565
550
  return null;
566
551
  const currentState = ActionResult.fromState(state);
@@ -581,8 +566,7 @@ export class Driller extends TaskAgent {
581
566
  `;
582
567
  }
583
568
  async getVisibleOverlayHtml() {
584
- const page = this.explorer.playwrightHelper.page;
585
- return page.evaluate(({ extractorSource, config }) => {
569
+ return this.explorer.withPage((page) => page.evaluate(({ extractorSource, config }) => {
586
570
  const extract = new Function(`return ${extractorSource}`)();
587
571
  return extract(config);
588
572
  }, {
@@ -593,11 +577,10 @@ export class Driller extends TaskAgent {
593
577
  overlaySelectors: HTML_SELECTORS.semanticOverlays,
594
578
  visibilityLimits: HTML_VISIBILITY_LIMITS,
595
579
  },
596
- });
580
+ }));
597
581
  }
598
582
  async getComponentScopeHtml(component, originalState) {
599
- const page = this.explorer.playwrightHelper.page;
600
- const scopedHtml = await page.evaluate(({ eidx, extractorSource, config }) => {
583
+ const scopedHtml = await this.explorer.withPage((page) => page.evaluate(({ eidx, extractorSource, config }) => {
601
584
  const extract = new Function(`return ${extractorSource}`)();
602
585
  return extract(eidx, config);
603
586
  }, {
@@ -608,7 +591,7 @@ export class Driller extends TaskAgent {
608
591
  interactiveControlSelector: HTML_SELECTORS.interactiveControl,
609
592
  limits: HTML_EXTRACTION_LIMITS,
610
593
  },
611
- });
594
+ }));
612
595
  if (scopedHtml)
613
596
  return scopedHtml;
614
597
  return await originalState.combinedHtml();
@@ -667,12 +650,6 @@ export class Driller extends TaskAgent {
667
650
  tag('step').log(` ${status} ${componentTest.component?.name || test.scenario}`);
668
651
  }
669
652
  }
670
- getCurrentPlan() {
671
- return this.currentPlan;
672
- }
673
- getConversation() {
674
- return this.currentConversation;
675
- }
676
653
  }
677
654
  function formatAriaNode(node) {
678
655
  const role = typeof node.role === 'string' ? node.role : 'unknown';
@@ -1105,4 +1082,3 @@ function isInteractiveElement(element) {
1105
1082
  return true;
1106
1083
  return false;
1107
1084
  }
1108
- const drillLocatorRule = locatorRule.replace(/<context_simplification>[\s\S]*?<\/context_simplification>/, '').trim();
@@ -1,7 +1,7 @@
1
1
  import { type ExperienceFile, type ExperienceTracker } from '../experience-tracker.js';
2
2
  import type { Agent } from './agent.js';
3
3
  import type { Provider } from './provider.js';
4
- export type { ExperienceFile };
4
+ export declare const COMPACT_MAX_LENGTH = 5000;
5
5
  interface MergeGroup {
6
6
  pattern: string;
7
7
  files: ExperienceFile[];
@@ -10,7 +10,6 @@ export declare class ExperienceCompactor implements Agent {
10
10
  emoji: string;
11
11
  provider: Provider;
12
12
  experienceTracker: ExperienceTracker;
13
- MAX_LENGTH: number;
14
13
  constructor(provider: Provider, experienceTracker: ExperienceTracker);
15
14
  compactExperience(experience: string): Promise<string>;
16
15
  stripNonUsefulEntries(content: string): string;
@@ -51,3 +50,4 @@ export declare class ExperienceCompactor implements Agent {
51
50
  title?: string;
52
51
  }): string;
53
52
  }
53
+ export {};
@@ -1,25 +1,26 @@
1
1
  import { unlinkSync } from 'node:fs';
2
+ import { basename } from 'node:path';
2
3
  import dedent from 'dedent';
3
- import { marked } from 'marked';
4
4
  import { z } from 'zod';
5
5
  import { 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
  const debugLog = createDebug('explorbot:experience-compactor');
12
+ export const COMPACT_MAX_LENGTH = 5000;
11
13
  export class ExperienceCompactor {
12
14
  emoji = '🗜️';
13
15
  provider;
14
16
  experienceTracker;
15
- MAX_LENGTH = 5000;
16
17
  constructor(provider, experienceTracker) {
17
18
  this.provider = provider;
18
19
  this.experienceTracker = experienceTracker;
19
20
  }
20
21
  async compactExperience(experience) {
21
22
  const stripped = this.stripNonUsefulEntries(experience);
22
- if (stripped.length < this.MAX_LENGTH) {
23
+ if (stripped.length < COMPACT_MAX_LENGTH) {
23
24
  return stripped;
24
25
  }
25
26
  const prompt = this.buildCompactionPrompt(stripped);
@@ -101,7 +102,7 @@ export class ExperienceCompactor {
101
102
  const generalized = generalizeUrl(url);
102
103
  if (generalized === url)
103
104
  continue;
104
- const stateHash = file.filePath.split('/').pop()?.replace('.md', '') || '';
105
+ const stateHash = basename(file.filePath, '.md');
105
106
  const newData = { ...file.data, url: `~${generalized}~`, mergedFrom: [url] };
106
107
  this.experienceTracker.writeExperienceFile(stateHash, file.content, newData);
107
108
  tag('substep').log(`Generalized URL: ${url} → ~${generalized}~`);
@@ -110,7 +111,7 @@ export class ExperienceCompactor {
110
111
  return count;
111
112
  }
112
113
  async compactFiles(files, options) {
113
- const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >= this.MAX_LENGTH) : files;
114
+ const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >= COMPACT_MAX_LENGTH) : files;
114
115
  let compactedCount = 0;
115
116
  const total = workingSet.length;
116
117
  const aiReviewCount = workingSet.filter((f) => this.isRecent(f)).length;
@@ -119,7 +120,7 @@ export class ExperienceCompactor {
119
120
  }
120
121
  for (let i = 0; i < workingSet.length; i++) {
121
122
  const experience = workingSet[i];
122
- const shortName = experience.filePath.split('/').pop() || experience.filePath;
123
+ const shortName = basename(experience.filePath);
123
124
  const willReview = this.isRecent(experience);
124
125
  if (total > 1 && willReview) {
125
126
  tag('substep').log(`[${i + 1}/${total}] reviewing ${shortName}`);
@@ -128,9 +129,9 @@ export class ExperienceCompactor {
128
129
  if (willReview) {
129
130
  content = await this.reviewExperienceQuality(content, experience.data);
130
131
  }
131
- if (content.length >= this.MAX_LENGTH) {
132
+ if (content.length >= COMPACT_MAX_LENGTH) {
132
133
  if (total > 1)
133
- tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${this.MAX_LENGTH} chars)`);
134
+ tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${COMPACT_MAX_LENGTH} chars)`);
134
135
  const prompt = this.buildCompactionPrompt(content);
135
136
  const model = this.provider.getModelForAgent('experience-compactor');
136
137
  const response = await this.provider.chat([
@@ -141,7 +142,7 @@ export class ExperienceCompactor {
141
142
  }
142
143
  if (content === experience.content)
143
144
  continue;
144
- const stateHash = experience.filePath.split('/').pop()?.replace('.md', '') || '';
145
+ const stateHash = basename(experience.filePath, '.md');
145
146
  this.experienceTracker.writeExperienceFile(stateHash, content, experience.data);
146
147
  debugLog('Experience file compacted:', experience.filePath);
147
148
  compactedCount++;
@@ -250,7 +251,7 @@ export class ExperienceCompactor {
250
251
  async mergeExperienceGroup(group) {
251
252
  const [targetFile, ...sourceFiles] = group.files;
252
253
  const combinedContent = group.files.map((f) => f.content).join('\n\n---\n\n');
253
- const targetStateHash = targetFile.filePath.split('/').pop()?.replace('.md', '') || '';
254
+ const targetStateHash = basename(targetFile.filePath, '.md');
254
255
  const newFrontmatter = {
255
256
  ...targetFile.data,
256
257
  url: group.pattern,
@@ -283,7 +284,7 @@ export class ExperienceCompactor {
283
284
  <rules>
284
285
  - Use markdown h2 headers only (##) - NO XML tags or wrappers in output
285
286
  - Merge similar flows to remove duplicates
286
- - Keep output under ${this.MAX_LENGTH} characters
287
+ - Keep output under ${COMPACT_MAX_LENGTH} characters
287
288
  - Be explicit and short - no proposals or explanations
288
289
 
289
290
  KEEP only:
@@ -368,27 +369,9 @@ export class ExperienceCompactor {
368
369
  }
369
370
  }
370
371
  function listSections(content) {
371
- const tokens = marked.lexer(content);
372
- const sections = [];
373
- let currentHeading = null;
374
- let currentRaw = '';
375
- const flush = () => {
376
- if (currentHeading !== null)
377
- sections.push({ title: currentHeading, raw: currentRaw });
378
- };
379
- for (const token of tokens) {
380
- const raw = token.raw || '';
381
- if (token.type === 'heading' && token.depth === 2) {
382
- flush();
383
- currentHeading = token.text.trim();
384
- currentRaw = raw;
385
- continue;
386
- }
387
- if (currentHeading !== null)
388
- currentRaw += raw;
389
- }
390
- flush();
391
- return sections;
372
+ const sections = mdq(content).query('section2').each();
373
+ const metas = mdq(content).query('section2').meta();
374
+ return sections.map((q, i) => ({ title: metas[i].text.trim(), raw: q.text() }));
392
375
  }
393
376
  function dropEmptySections(content) {
394
377
  let result = content;
@@ -408,7 +391,7 @@ function dropNonReusableSections(content) {
408
391
  const sections = [...mdq(result).query('section2(~"FLOW:")').each(), ...mdq(result).query('section2(~"ACTION:")').each()];
409
392
  for (const section of sections) {
410
393
  const raw = section.text();
411
- if (!/\bI\.clickXY\s*\(/.test(raw))
394
+ if (!isNonReusableCode(raw))
412
395
  continue;
413
396
  result = result.replace(raw, '');
414
397
  }
@@ -136,7 +136,7 @@ function extractKeyFields(body, result = {}, depth = 0) {
136
136
  return result;
137
137
  }
138
138
  for (const [key, value] of Object.entries(body)) {
139
- if (value === null || value === undefined)
139
+ if (value == null)
140
140
  continue;
141
141
  if (typeof value === 'object') {
142
142
  extractKeyFields(value, result, depth + 1);
@@ -18,7 +18,6 @@ export declare class Fisherman implements Agent {
18
18
  hasApiConfig: boolean;
19
19
  constructor(provider: Provider, apiClient: ApiClient, requestStore: RequestStore, specLoader: () => Promise<any | null>, baseEndpoint: string, cookieProvider: () => Promise<Record<string, string>>, configHeaders?: Record<string, string>, hasApiConfig?: boolean);
20
20
  isAvailable(): boolean;
21
- getMode(): string;
22
21
  ensureReady(scopeUrl?: string): Promise<void>;
23
22
  getEndpointList(scopeUrl?: string): string;
24
23
  prepareData(instructions: string, scopeUrl?: string, sessionName?: string): Promise<FishermanResult>;
@@ -34,9 +34,6 @@ export class Fisherman {
34
34
  isAvailable() {
35
35
  return this.mode !== 'disabled';
36
36
  }
37
- getMode() {
38
- return this.mode;
39
- }
40
37
  async ensureReady(scopeUrl) {
41
38
  await this.detectMode(scopeUrl);
42
39
  this.spec ??= await this.specLoader();
@@ -1,3 +1,4 @@
1
+ import type { StateManager } from '../../state-manager.js';
1
2
  import type { Plan } from '../../test-plan.js';
2
3
  import type { Conversation } from '../conversation.js';
3
4
  import type { Constructor } from './mixin.js';
@@ -8,6 +9,7 @@ export interface CodeceptJSMethods {
8
9
  export declare function WithCodeceptJS<T extends Constructor>(Base: T): {
9
10
  new (...args: any[]): {
10
11
  savedFiles: Set<string>;
12
+ stateManager?: StateManager;
11
13
  toCode(conversation: Conversation, scenario: string): string;
12
14
  saveCodeceptPlanToFile(plan: Plan): string;
13
15
  getKnowledgeLines(url: string, indent?: string): string[];
@@ -2,11 +2,10 @@ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ActionResult } from "../../action-result.js";
4
4
  import { ConfigParser } from "../../config.js";
5
- import { KnowledgeTracker } from "../../knowledge-tracker.js";
6
5
  import { tag } from "../../utils/logger.js";
7
6
  import { relativeToCwd } from "../../utils/next-steps.js";
8
- import { safeFilename } from "../../utils/strings.js";
9
7
  import { CODECEPT_TOOLS, isNonReusableCode, stripComments } from "../../utils/step-analyzer.js";
8
+ import { safeFilename } from "../../utils/strings.js";
10
9
  import { ASSERTION_TOOLS } from "../tools.js";
11
10
  import { escapeString, getExecutionLabel } from "./utils.js";
12
11
  export function WithCodeceptJS(Base) {
@@ -87,7 +86,9 @@ export function WithCodeceptJS(Base) {
87
86
  return filePath;
88
87
  }
89
88
  getKnowledgeLines(url, indent = ' ') {
90
- const knowledgeTracker = new KnowledgeTracker();
89
+ const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
90
+ if (!knowledgeTracker)
91
+ return [];
91
92
  const state = new ActionResult({ url });
92
93
  const { wait, waitForElement, code } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement', 'code']);
93
94
  const lines = [];
@@ -1,4 +1,6 @@
1
+ import type Explorer from '../../explorer.js';
1
2
  import { type PlaywrightRecorder } from '../../playwright-recorder.js';
3
+ import type { StateManager } from '../../state-manager.js';
2
4
  import type { Plan } from '../../test-plan.js';
3
5
  import type { Conversation } from '../conversation.js';
4
6
  import type { Constructor } from './mixin.js';
@@ -10,9 +12,10 @@ export declare function WithPlaywright<T extends Constructor>(Base: T): {
10
12
  new (...args: any[]): {
11
13
  playwright: {
12
14
  recorder: PlaywrightRecorder;
13
- helper: any;
15
+ explorer: Explorer;
14
16
  } | undefined;
15
17
  savedFiles: Set<string>;
18
+ stateManager?: StateManager;
16
19
  toPlaywrightCode(conversation: Conversation, scenario: string): Promise<string>;
17
20
  savePlaywrightPlanToFile(plan: Plan): string;
18
21
  getPlaywrightKnowledgeLines(url: string, indent?: string): string[];
@@ -2,12 +2,11 @@ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ActionResult } from "../../action-result.js";
4
4
  import { ConfigParser } from "../../config.js";
5
- import { KnowledgeTracker } from "../../knowledge-tracker.js";
6
5
  import { renderAssertion, renderCall } from "../../playwright-recorder.js";
7
6
  import { tag } from "../../utils/logger.js";
8
7
  import { relativeToCwd } from "../../utils/next-steps.js";
9
- import { safeFilename } from "../../utils/strings.js";
10
8
  import { CODECEPT_TOOLS } from "../../utils/step-analyzer.js";
9
+ import { safeFilename } from "../../utils/strings.js";
11
10
  import { ASSERTION_TOOLS } from "../tools.js";
12
11
  import { escapeString, getExecutionLabel } from "./utils.js";
13
12
  const PLAYWRIGHT_EMITTED_TOOLS = [...CODECEPT_TOOLS, ...ASSERTION_TOOLS];
@@ -124,7 +123,9 @@ export function WithPlaywright(Base) {
124
123
  return filePath;
125
124
  }
126
125
  getPlaywrightKnowledgeLines(url, indent = ' ') {
127
- const knowledgeTracker = new KnowledgeTracker();
126
+ const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
127
+ if (!knowledgeTracker)
128
+ return [];
128
129
  const state = new ActionResult({ url });
129
130
  const { wait, waitForElement } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement']);
130
131
  const lines = [];
@@ -1,4 +1,5 @@
1
1
  import type { ExplorbotConfig } from '../../config.js';
2
+ import type Explorer from '../../explorer.js';
2
3
  import type { PlaywrightRecorder } from '../../playwright-recorder.js';
3
4
  import { type Constructor } from './mixin.js';
4
5
  export interface ScreencastMethods {
@@ -12,7 +13,7 @@ export declare function WithScreencast<T extends Constructor>(Base: T): {
12
13
  savedFiles: Set<string>;
13
14
  playwright: {
14
15
  recorder: PlaywrightRecorder;
15
- helper: any;
16
+ explorer: Explorer;
16
17
  } | undefined;
17
18
  screencastPage: any;
18
19
  screencastActive: boolean;
@@ -27,7 +27,7 @@ export function WithScreencast(Base) {
27
27
  return;
28
28
  if (!this.config?.ai?.agents?.historian?.screencast)
29
29
  return;
30
- if (!this.playwright?.helper)
30
+ if (!this.playwright?.explorer)
31
31
  return;
32
32
  this.onTestBefore = (test) => {
33
33
  void this.startScreencast(test);
@@ -46,7 +46,7 @@ export function WithScreencast(Base) {
46
46
  async startScreencast(test) {
47
47
  if (this.screencastActive)
48
48
  return;
49
- const page = this.playwright?.helper?.page;
49
+ const page = this.playwright?.explorer?.page;
50
50
  if (!page?.screencast?.start)
51
51
  return;
52
52
  const task = test?._explorbotTest;
@@ -1,4 +1,3 @@
1
1
  import type { ToolExecution } from '../conversation.js';
2
- export { isNonReusableCode, stripComments } from '../../utils/step-analyzer.js';
3
2
  export declare function escapeString(str: string): string;
4
3
  export declare function getExecutionLabel(exec: ToolExecution, fallback?: string): string;
@@ -1,4 +1,3 @@
1
- export { isNonReusableCode, stripComments } from "../../utils/step-analyzer.js";
2
1
  export function escapeString(str) {
3
2
  return str.replace(/'/g, "\\'").replace(/\n/g, ' ');
4
3
  }