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
@@ -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
@@ -3,13 +3,9 @@ import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from '../action-result.ts';
5
5
  import { setActivity } from '../activity.ts';
6
- import type { ExperienceTracker } from '../experience-tracker.ts';
7
- import type Explorer from '../explorer.ts';
8
- import type { KnowledgeTracker } from '../knowledge-tracker.ts';
9
6
  import { Observability } from '../observability.ts';
10
7
  import { Plan, Test, TestResult } from '../test-plan.ts';
11
8
  import { collectInteractiveNodes } from '../utils/aria.ts';
12
- import { HooksRunner } from '../utils/hooks-runner.ts';
13
9
  import {
14
10
  EXPLORBOT_ATTRS,
15
11
  HTML_COMPOSITE_AREA_HINTS,
@@ -26,12 +22,13 @@ import {
26
22
  } from '../utils/html.ts';
27
23
  import { createDebug, tag } from '../utils/logger.ts';
28
24
  import { loop, pause } from '../utils/loop.ts';
25
+ import { annotatePageElements } from '../utils/web-annotate.ts';
26
+ import { eidxInContainer } from '../utils/web-eidx.ts';
29
27
  import { WebElement } from '../utils/web-element.ts';
30
- import type { Agent } from './agent.ts';
31
- import type { Conversation } from './conversation.ts';
28
+ import type { Agent, AgentDeps } from './agent.ts';
32
29
  import type { Navigator } from './navigator.ts';
33
30
  import type { Provider } from './provider.ts';
34
- import { locatorRule } from './rules.ts';
31
+ import { drillLocatorRule } from './rules.ts';
35
32
  import { TaskAgent, isInteractive } from './task-agent.ts';
36
33
  import { createCodeceptJSTools } from './tools.ts';
37
34
 
@@ -80,44 +77,25 @@ interface DrillOptions {
80
77
  export class Driller extends TaskAgent implements Agent {
81
78
  protected readonly ACTION_TOOLS = ['click', 'pressKey', 'form'];
82
79
  emoji = 'D';
83
- private explorer: Explorer;
84
- private provider: Provider;
85
80
  private navigator: Navigator;
86
- private hooksRunner: HooksRunner;
87
81
  private currentPlan?: Plan;
88
- private currentConversation: Conversation | null = null;
89
82
  private allResults: InteractionResult[] = [];
90
83
  private verifiedAction: { componentId: string; toolName: string; code?: string; canonicalCode?: string } | null = null;
91
84
  private pendingNestedContext: string | null = null;
92
85
 
93
86
  MAX_COMPONENT_ITERATIONS = 12;
94
87
 
95
- constructor(explorer: Explorer, provider: Provider, navigator: Navigator) {
96
- super();
97
- this.explorer = explorer;
98
- this.provider = provider;
88
+ constructor(deps: AgentDeps, navigator: Navigator) {
89
+ super(deps);
99
90
  this.navigator = navigator;
100
- this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
101
91
  }
102
92
 
103
93
  protected getNavigator(): Navigator {
104
94
  return this.navigator;
105
95
  }
106
96
 
107
- protected getExperienceTracker(): ExperienceTracker {
108
- return this.explorer.getStateManager().getExperienceTracker();
109
- }
110
-
111
- protected getKnowledgeTracker(): KnowledgeTracker {
112
- return this.explorer.getKnowledgeTracker();
113
- }
114
-
115
- protected getProvider(): Provider {
116
- return this.provider;
117
- }
118
-
119
97
  getSystemMessage(component?: ComponentInfo): string {
120
- const currentUrl = this.explorer.getStateManager().getCurrentState()?.url;
98
+ const currentUrl = this.stateManager.getCurrentState()?.url;
121
99
  const customPrompt = this.provider.getSystemPromptForAgent('driller', currentUrl);
122
100
 
123
101
  return dedent`
@@ -161,7 +139,7 @@ export class Driller extends TaskAgent implements Agent {
161
139
 
162
140
  async drill(opts: DrillOptions = {}): Promise<Plan> {
163
141
  const { knowledgePath, maxComponents = 30, interactive = isInteractive() } = opts;
164
- const currentState = this.explorer.getStateManager().getCurrentState();
142
+ const currentState = this.stateManager.getCurrentState();
165
143
  if (!currentState) throw new Error('No page state available');
166
144
 
167
145
  const sessionName = `driller_${Date.now().toString(36)}`;
@@ -209,15 +187,15 @@ export class Driller extends TaskAgent implements Agent {
209
187
 
210
188
  private async captureAnnotatedState(): Promise<ActionResult> {
211
189
  setActivity(`${this.emoji} Capturing annotated page state...`, 'action');
212
- const action = this.explorer.createAction();
190
+ const action = this.explorer.action();
213
191
  try {
214
192
  const annotated = await Promise.race([
215
- this.explorer.annotateElements(),
193
+ this.explorer.withPage(annotatePageElements),
216
194
  new Promise<never>((_, reject) => {
217
195
  setTimeout(() => reject(new Error('annotateElements timeout')), 15000);
218
196
  }),
219
197
  ]);
220
- return action.capturePageState({ ariaSnapshot: annotated.ariaSnapshot });
198
+ return action.capturePageState();
221
199
  } catch (error) {
222
200
  tag('warning').log(`Annotated capture failed, falling back to plain page state: ${error instanceof Error ? error.message : error}`);
223
201
  return action.capturePageState();
@@ -228,9 +206,10 @@ export class Driller extends TaskAgent implements Agent {
228
206
 
229
207
  private async collectComponents(state: ActionResult, maxComponents: number): Promise<ComponentInfo[]> {
230
208
  setActivity(`${this.emoji} Collecting components...`, 'action');
231
- const page = this.explorer.playwrightHelper.page;
232
- const eidxList = await this.explorer.getEidxInContainer(null);
233
- const webElements = await WebElement.fromEidxList(page, eidxList);
209
+ const webElements = await this.explorer.withPage(async (page) => {
210
+ const eidxList = await eidxInContainer(page, null);
211
+ return WebElement.fromEidxList(page, eidxList);
212
+ });
234
213
  const ariaNodes = collectInteractiveNodes(state.ariaSnapshot);
235
214
  const scored = webElements
236
215
  .filter((element) => isDrillableElement(element))
@@ -323,11 +302,10 @@ export class Driller extends TaskAgent implements Agent {
323
302
  this.verifiedAction = null;
324
303
  this.pendingNestedContext = null;
325
304
  const conversation = this.provider.startConversation(this.getSystemMessage(component), 'driller');
326
- this.currentConversation = conversation;
327
305
  conversation.addUserText(await this.buildComponentPrompt(originalState, component));
328
306
 
329
307
  let finished = false;
330
- const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.explorer, test), component);
308
+ const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.toolDeps, test), component);
331
309
  const tools = { ...actionTools, ...this.createDrillFlowTools(originalState, test, interactive) };
332
310
 
333
311
  await loop(
@@ -336,7 +314,7 @@ export class Driller extends TaskAgent implements Agent {
336
314
  setActivity(`${this.emoji} Drilling ${component.name}...`, 'action');
337
315
 
338
316
  if (iteration > 1) {
339
- const currentState = ActionResult.fromState(this.explorer.getStateManager().getCurrentState() || originalState);
317
+ const currentState = ActionResult.fromState(this.stateManager.getCurrentState() || originalState);
340
318
  conversation.addUserText(await this.buildContextUpdate(currentState, component));
341
319
  if (this.pendingNestedContext) {
342
320
  conversation.addUserText(this.pendingNestedContext);
@@ -580,7 +558,7 @@ export class Driller extends TaskAgent implements Agent {
580
558
  execute: async ({ reason }) => {
581
559
  await this.restoreOriginalState(originalState, reason);
582
560
  await this.captureAnnotatedState();
583
- const currentState = this.explorer.getStateManager().getCurrentState();
561
+ const currentState = this.stateManager.getCurrentState();
584
562
  return { success: true, url: currentState?.url || originalState.url };
585
563
  },
586
564
  }),
@@ -601,16 +579,16 @@ export class Driller extends TaskAgent implements Agent {
601
579
  }
602
580
 
603
581
  private async restoreOriginalState(originalState: ActionResult, reason: string): Promise<void> {
604
- const currentState = this.explorer.getStateManager().getCurrentState();
582
+ const currentState = this.stateManager.getCurrentState();
605
583
  const targetUrl = originalState.fullUrl || originalState.url;
606
- const action = this.explorer.createAction();
584
+ const action = this.explorer.action();
607
585
 
608
586
  if (currentState?.url !== originalState.url) {
609
- await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)`, false);
587
+ await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)`);
610
588
  return;
611
589
  }
612
590
 
613
- await action.attempt('I.pressKey("Escape")', `${reason} (restore state)`, false);
591
+ await action.attempt('I.pressKey("Escape")', `${reason} (restore state)`);
614
592
  }
615
593
 
616
594
  private async saveToExperience(state: ActionResult, results: InteractionResult[]): Promise<void> {
@@ -668,7 +646,7 @@ export class Driller extends TaskAgent implements Agent {
668
646
  const overlayHtml = await this.getVisibleOverlayHtml();
669
647
  if (!overlayHtml) return null;
670
648
 
671
- const state = this.explorer.getStateManager().getCurrentState();
649
+ const state = this.stateManager.getCurrentState();
672
650
  if (!state) return null;
673
651
  const currentState = ActionResult.fromState(state);
674
652
  return dedent`
@@ -689,40 +667,42 @@ export class Driller extends TaskAgent implements Agent {
689
667
  }
690
668
 
691
669
  private async getVisibleOverlayHtml(): Promise<string> {
692
- const page = this.explorer.playwrightHelper.page;
693
- return page.evaluate(
694
- ({ extractorSource, config }) => {
695
- const extract = new Function(`return ${extractorSource}`)() as (config: any) => string;
696
- return extract(config);
697
- },
698
- {
699
- extractorSource: getVisibleOverlayHtmlExtractorSource(),
700
- config: {
701
- interactiveContentSelector: HTML_SELECTORS.interactiveContent,
702
- limits: HTML_EXTRACTION_LIMITS,
703
- overlaySelectors: HTML_SELECTORS.semanticOverlays,
704
- visibilityLimits: HTML_VISIBILITY_LIMITS,
670
+ return this.explorer.withPage((page) =>
671
+ page.evaluate(
672
+ ({ extractorSource, config }) => {
673
+ const extract = new Function(`return ${extractorSource}`)() as (config: any) => string;
674
+ return extract(config);
705
675
  },
706
- }
676
+ {
677
+ extractorSource: getVisibleOverlayHtmlExtractorSource(),
678
+ config: {
679
+ interactiveContentSelector: HTML_SELECTORS.interactiveContent,
680
+ limits: HTML_EXTRACTION_LIMITS,
681
+ overlaySelectors: HTML_SELECTORS.semanticOverlays,
682
+ visibilityLimits: HTML_VISIBILITY_LIMITS,
683
+ },
684
+ }
685
+ )
707
686
  );
708
687
  }
709
688
 
710
689
  private async getComponentScopeHtml(component: ComponentInfo, originalState: ActionResult): Promise<string> {
711
- const page = this.explorer.playwrightHelper.page;
712
- const scopedHtml = await page.evaluate(
713
- ({ eidx, extractorSource, config }) => {
714
- const extract = new Function(`return ${extractorSource}`)() as (eidx: string, config: any) => string;
715
- return extract(eidx, config);
716
- },
717
- {
718
- eidx: component.eidx,
719
- extractorSource: getComponentScopeHtmlExtractorSource(),
720
- config: {
721
- eidxAttr: EXPLORBOT_ATTRS.eidx,
722
- interactiveControlSelector: HTML_SELECTORS.interactiveControl,
723
- limits: HTML_EXTRACTION_LIMITS,
690
+ const scopedHtml = await this.explorer.withPage((page) =>
691
+ page.evaluate(
692
+ ({ eidx, extractorSource, config }) => {
693
+ const extract = new Function(`return ${extractorSource}`)() as (eidx: string, config: any) => string;
694
+ return extract(eidx, config);
724
695
  },
725
- }
696
+ {
697
+ eidx: component.eidx,
698
+ extractorSource: getComponentScopeHtmlExtractorSource(),
699
+ config: {
700
+ eidxAttr: EXPLORBOT_ATTRS.eidx,
701
+ interactiveControlSelector: HTML_SELECTORS.interactiveControl,
702
+ limits: HTML_EXTRACTION_LIMITS,
703
+ },
704
+ }
705
+ )
726
706
  );
727
707
 
728
708
  if (scopedHtml) return scopedHtml;
@@ -789,14 +769,6 @@ export class Driller extends TaskAgent implements Agent {
789
769
  tag('step').log(` ${status} ${componentTest.component?.name || test.scenario}`);
790
770
  }
791
771
  }
792
-
793
- getCurrentPlan(): Plan | undefined {
794
- return this.currentPlan;
795
- }
796
-
797
- getConversation(): Conversation | null {
798
- return this.currentConversation;
799
- }
800
772
  }
801
773
 
802
774
  function formatAriaNode(node: Record<string, unknown>): string {
@@ -1190,5 +1162,3 @@ function isInteractiveElement(element: WebElement): boolean {
1190
1162
  if (element.attrs['aria-haspopup'] || element.attrs['aria-expanded'] || element.attrs['aria-controls']) return true;
1191
1163
  return false;
1192
1164
  }
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,12 +2,12 @@ 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';
9
- import { safeFilename } from '../../utils/strings.ts';
10
9
  import { CODECEPT_TOOLS, isNonReusableCode, stripComments } from '../../utils/step-analyzer.ts';
10
+ import { safeFilename } from '../../utils/strings.ts';
11
11
  import type { Conversation } from '../conversation.ts';
12
12
  import { ASSERTION_TOOLS } from '../tools.ts';
13
13
  import type { Constructor } from './mixin.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
 
@@ -2,13 +2,14 @@ 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 Explorer from '../../explorer.ts';
6
6
  import { type PlaywrightRecorder, type TraceCall, renderAssertion, renderCall } from '../../playwright-recorder.ts';
7
+ import type { StateManager } from '../../state-manager.ts';
7
8
  import type { Plan } from '../../test-plan.ts';
8
9
  import { tag } from '../../utils/logger.ts';
9
10
  import { relativeToCwd } from '../../utils/next-steps.ts';
10
- import { safeFilename } from '../../utils/strings.ts';
11
11
  import { CODECEPT_TOOLS } from '../../utils/step-analyzer.ts';
12
+ import { safeFilename } from '../../utils/strings.ts';
12
13
  import type { Conversation } from '../conversation.ts';
13
14
  import { ASSERTION_TOOLS } from '../tools.ts';
14
15
  import type { Constructor } from './mixin.ts';
@@ -23,8 +24,9 @@ export interface PlaywrightMethods {
23
24
 
24
25
  export function WithPlaywright<T extends Constructor>(Base: T) {
25
26
  return class extends Base {
26
- declare playwright: { recorder: PlaywrightRecorder; helper: any } | undefined;
27
+ declare playwright: { recorder: PlaywrightRecorder; explorer: Explorer } | undefined;
27
28
  declare savedFiles: Set<string>;
29
+ declare stateManager?: StateManager;
28
30
 
29
31
  async toPlaywrightCode(conversation: Conversation, scenario: string): Promise<string> {
30
32
  const toolExecutions = conversation.getToolExecutions();
@@ -146,7 +148,8 @@ export function WithPlaywright<T extends Constructor>(Base: T) {
146
148
  }
147
149
 
148
150
  private getPlaywrightKnowledgeLines(url: string, indent = ' '): string[] {
149
- const knowledgeTracker = new KnowledgeTracker();
151
+ const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
152
+ if (!knowledgeTracker) return [];
150
153
  const state = new ActionResult({ url });
151
154
  const { wait, waitForElement } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement']);
152
155
 
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import * as codeceptjs from 'codeceptjs';
5
5
  import { outputPath } from '../../config.ts';
6
6
  import type { ExplorbotConfig } from '../../config.ts';
7
+ import type Explorer from '../../explorer.ts';
7
8
  import type { PlaywrightRecorder } from '../../playwright-recorder.ts';
8
9
  import { tag } from '../../utils/logger.ts';
9
10
  import { relativeToCwd } from '../../utils/next-steps.ts';
@@ -22,7 +23,7 @@ export function WithScreencast<T extends Constructor>(Base: T) {
22
23
  return class extends Base {
23
24
  declare config: ExplorbotConfig | undefined;
24
25
  declare savedFiles: Set<string>;
25
- declare playwright: { recorder: PlaywrightRecorder; helper: any } | undefined;
26
+ declare playwright: { recorder: PlaywrightRecorder; explorer: Explorer } | undefined;
26
27
 
27
28
  private screencastPage: any = null;
28
29
  private screencastActive = false;
@@ -41,7 +42,7 @@ export function WithScreencast<T extends Constructor>(Base: T) {
41
42
  attachScreencast(): void {
42
43
  if (this.screencastListenersInstalled) return;
43
44
  if (!this.config?.ai?.agents?.historian?.screencast) return;
44
- if (!this.playwright?.helper) return;
45
+ if (!this.playwright?.explorer) return;
45
46
 
46
47
  this.onTestBefore = (test: any) => {
47
48
  void this.startScreencast(test);
@@ -62,7 +63,7 @@ export function WithScreencast<T extends Constructor>(Base: T) {
62
63
 
63
64
  private async startScreencast(test: any): Promise<void> {
64
65
  if (this.screencastActive) return;
65
- const page = this.playwright?.helper?.page;
66
+ const page = this.playwright?.explorer?.page;
66
67
  if (!page?.screencast?.start) return;
67
68
 
68
69
  const task = test?._explorbotTest;
@@ -1,5 +1,4 @@
1
1
  import type { ToolExecution } from '../conversation.ts';
2
- export { isNonReusableCode, stripComments } from '../../utils/step-analyzer.ts';
3
2
 
4
3
  export function escapeString(str: string): string {
5
4
  return str.replace(/'/g, "\\'").replace(/\n/g, ' ');