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
@@ -3,7 +3,7 @@ import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from "../action-result.js";
5
5
  import { TestResult } from '../test-plan.js';
6
- import { extractFocusedElement } from "../utils/aria.js";
6
+ import { LARGE_ARIA_CHANGE_THRESHOLD, extractFocusedElement } from "../utils/aria.js";
7
7
  import { isFatalBrowserError } from "../utils/browser-errors.js";
8
8
  import { createDebug, tag } from '../utils/logger.js';
9
9
  import { pause } from '../utils/loop.js';
@@ -12,8 +12,7 @@ import { sectionContextRule } from "./rules.js";
12
12
  import { isInteractive } from "./task-agent.js";
13
13
  const debugLog = createDebug('explorbot:tools');
14
14
  export const ASSERTION_TOOLS = ['verify'];
15
- export function createCodeceptJSTools(explorer, task) {
16
- const stateManager = explorer.getStateManager();
15
+ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
17
16
  return {
18
17
  click: tool({
19
18
  description: dedent `
@@ -31,6 +30,7 @@ export function createCodeceptJSTools(explorer, task) {
31
30
  CRITICAL: All commands MUST target the SAME element using different locators.
32
31
  This is a FALLBACK list, NOT a sequence of different clicks.
33
32
  If you need to click multiple different elements, make SEPARATE click() calls.
33
+ Inline create/edit UIs may use icon-only confirm controls near the edited field instead of text buttons. Target the nearest actionable icon/control in the same row or form.
34
34
  `,
35
35
  inputSchema: z.object({
36
36
  commands: z.array(z.string()).describe(dedent `
@@ -67,29 +67,24 @@ export function createCodeceptJSTools(explorer, task) {
67
67
  return `I.click(${JSON.stringify(trimmed)})`;
68
68
  });
69
69
  const previousState = ActionResult.fromState(stateManager.getCurrentState());
70
- const action = explorer.createAction();
70
+ const action = explorer.action();
71
71
  const attempts = [];
72
72
  for (let i = 0; i < commands.length; i++) {
73
73
  const command = transformContainsCommand(commands[i]);
74
- const isLast = i === commands.length - 1;
75
- const success = await action.attempt(command, explanation, isLast);
76
- attempts.push({
77
- command,
78
- success,
79
- ...(action.lastError && { error: action.lastError.toString() }),
80
- });
74
+ const success = await action.attempt(command, explanation);
75
+ const attempt = { command, success };
76
+ if (action.lastError)
77
+ attempt.error = action.lastError.toString();
78
+ attempts.push(attempt);
81
79
  if (success) {
82
80
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, command);
83
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
84
- activeNote.screenshot = await action.saveScreenshot();
85
- }
86
- activeNote.commit(TestResult.PASSED);
81
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
87
82
  return successToolResult('click', { ...toolResult, attempts, code: command }, action);
88
83
  }
89
84
  }
90
85
  let disambiguated = null;
91
86
  if (attempts.some((a) => a.error?.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN))) {
92
- disambiguated = await disambiguateElements(action.lastError, explanation, explorer.getAIProvider());
87
+ disambiguated = await disambiguateElements(action.lastError, explanation, ai);
93
88
  }
94
89
  if (disambiguated) {
95
90
  debugLog('Disambiguation picked element %d', disambiguated.position);
@@ -100,23 +95,17 @@ export function createCodeceptJSTools(explorer, task) {
100
95
  }
101
96
  retryCommands.push(`I.click('${disambiguated.xpath.replace(/'/g, "\\'")}')`);
102
97
  for (const retryCmd of retryCommands) {
103
- if (!(await action.attempt(retryCmd, explanation, true))) {
98
+ if (!(await action.attempt(retryCmd, explanation))) {
104
99
  attempts.push({ command: retryCmd, success: false, error: action.lastError?.toString() });
105
100
  continue;
106
101
  }
107
102
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, retryCmd);
108
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
109
- activeNote.screenshot = await action.saveScreenshot();
110
- }
111
- activeNote.commit(TestResult.PASSED);
103
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
112
104
  return successToolResult('click', { ...toolResult, attempts, code: retryCmd, disambiguated: true }, action);
113
105
  }
114
106
  }
115
107
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, commands[0]);
116
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
117
- activeNote.screenshot = await action.saveScreenshot();
118
- }
119
- activeNote.commit(TestResult.FAILED);
108
+ await commitNote(activeNote, TestResult.FAILED, toolResult, action);
120
109
  let suggestion = "Try xpathCheck() to find the element's actual position, see() for visual analysis, or visualClick() to click by visual appearance.";
121
110
  const lastError = attempts[attempts.length - 1]?.error || '';
122
111
  if (lastError.includes('was not found') || lastError.includes('not found by text')) {
@@ -173,15 +162,14 @@ export function createCodeceptJSTools(explorer, task) {
173
162
  return `I.moveCursorTo(${JSON.stringify(trimmed)})`;
174
163
  });
175
164
  const previousState = ActionResult.fromState(stateManager.getCurrentState());
176
- const action = explorer.createAction();
165
+ const action = explorer.action();
177
166
  const attempts = [];
178
167
  for (const command of commands) {
179
- const success = await action.attempt(command, explanation, true);
180
- attempts.push({
181
- command,
182
- success,
183
- ...(action.lastError && { error: action.lastError.toString() }),
184
- });
168
+ const success = await action.attempt(command, explanation);
169
+ const attempt = { command, success };
170
+ if (action.lastError)
171
+ attempt.error = action.lastError.toString();
172
+ attempts.push(attempt);
185
173
  if (!success)
186
174
  continue;
187
175
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, command);
@@ -234,15 +222,12 @@ export function createCodeceptJSTools(explorer, task) {
234
222
  const keyToUse = matchingStandardKey || key;
235
223
  if (!isSingleChar && !isStandardKey) {
236
224
  const previousState = ActionResult.fromState(stateManager.getCurrentState());
237
- const action = explorer.createAction();
225
+ const action = explorer.action();
238
226
  const typeCommand = `I.type(${JSON.stringify(key)})`;
239
227
  await action.attempt(typeCommand, explanation);
240
228
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, key);
241
229
  if (!action.lastError) {
242
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
243
- activeNote.screenshot = await action.saveScreenshot();
244
- }
245
- activeNote.commit(TestResult.PASSED);
230
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
246
231
  return successToolResult('pressKey', {
247
232
  ...toolResult,
248
233
  message: `Automatically used type() for "${key}" (not a standard key press)`,
@@ -251,10 +236,7 @@ export function createCodeceptJSTools(explorer, task) {
251
236
  }, action);
252
237
  }
253
238
  const errorMsg = `pressKey fallback to type() failed: ${action.lastError?.toString()}`;
254
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
255
- activeNote.screenshot = await action.saveScreenshot();
256
- }
257
- activeNote.commit(TestResult.FAILED);
239
+ await commitNote(activeNote, TestResult.FAILED, toolResult, action);
258
240
  return failedToolResult('pressKey', errorMsg, {
259
241
  ...toolResult,
260
242
  code: typeCommand,
@@ -274,7 +256,7 @@ export function createCodeceptJSTools(explorer, task) {
274
256
  }
275
257
  }
276
258
  const previousState = ActionResult.fromState(stateManager.getCurrentState());
277
- const action = explorer.createAction();
259
+ const action = explorer.action();
278
260
  let pressKeyCommand;
279
261
  if (modifier) {
280
262
  const modifiers = Array.isArray(modifier) ? modifier : [modifier];
@@ -286,10 +268,7 @@ export function createCodeceptJSTools(explorer, task) {
286
268
  await action.attempt(pressKeyCommand, explanation);
287
269
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, key);
288
270
  if (!action.lastError) {
289
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
290
- activeNote.screenshot = await action.saveScreenshot();
291
- }
292
- activeNote.commit(TestResult.PASSED);
271
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
293
272
  return successToolResult('pressKey', {
294
273
  ...toolResult,
295
274
  message: `Pressed key: ${key}${modifier ? ` with modifier(s): ${Array.isArray(modifier) ? modifier.join('+') : modifier}` : ''}`,
@@ -297,10 +276,7 @@ export function createCodeceptJSTools(explorer, task) {
297
276
  }, action);
298
277
  }
299
278
  const errorMsg = `pressKey() failed: ${action.lastError?.toString()}`;
300
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
301
- activeNote.screenshot = await action.saveScreenshot();
302
- }
303
- activeNote.commit(TestResult.FAILED);
279
+ await commitNote(activeNote, TestResult.FAILED, toolResult, action);
304
280
  return failedToolResult('pressKey', errorMsg, {
305
281
  ...toolResult,
306
282
  code: pressKeyCommand,
@@ -310,7 +286,7 @@ export function createCodeceptJSTools(explorer, task) {
310
286
  catch (error) {
311
287
  throwIfFatalBrowserError(error);
312
288
  activeNote.commit(TestResult.FAILED);
313
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
289
+ const errorMessage = errorText(error);
314
290
  return failedToolResult('pressKey', `PressKey tool failed: ${errorMessage}`);
315
291
  }
316
292
  },
@@ -370,22 +346,15 @@ export function createCodeceptJSTools(explorer, task) {
370
346
  }
371
347
  const previousState = ActionResult.fromState(stateManager.getCurrentState());
372
348
  const formLocator = codeLines[0] || 'form';
373
- const action = explorer.createAction();
374
- const wasInIframe = await explorer.isInsideIframe();
349
+ const action = explorer.action();
375
350
  await action.attempt(codeBlock, explanation);
376
- if (action.lastError && !wasInIframe && (await explorer.isInsideIframe())) {
377
- await explorer.switchToMainFrame();
378
- }
379
351
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, formLocator);
380
352
  if (action.lastError) {
381
353
  const message = action.lastError ? String(action.lastError) : 'Unknown error';
382
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
383
- activeNote.screenshot = await action.saveScreenshot();
384
- }
385
- activeNote.commit(TestResult.FAILED);
354
+ await commitNote(activeNote, TestResult.FAILED, toolResult, action);
386
355
  let formSuggestion = 'Look into error message and identify which commands passed and which failed. Continue execution using step-by-step approach using click() and form() tools.';
387
356
  if (message.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN)) {
388
- const disambiguated = await disambiguateElements(action.lastError, explanation, explorer.getAIProvider());
357
+ const disambiguated = await disambiguateElements(action.lastError, explanation, ai);
389
358
  if (disambiguated) {
390
359
  formSuggestion = `Multiple elements matched. Add step.opts({ elementIndex: ${disambiguated.position} }) to the failing command. Fallback locator: ${disambiguated.xpath}`;
391
360
  }
@@ -396,10 +365,15 @@ export function createCodeceptJSTools(explorer, task) {
396
365
  suggestion: formSuggestion,
397
366
  }, action.lastError);
398
367
  }
399
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
400
- activeNote.screenshot = await action.saveScreenshot();
368
+ if (!hasObservablePageChange(toolResult)) {
369
+ activeNote.commit(TestResult.FAILED);
370
+ return failedToolResult('form', 'Form command executed, but no observable page or form-state change was captured.', {
371
+ ...toolResult,
372
+ code: codeBlock,
373
+ suggestion: 'Treat the field/form action as not completed. Re-locate the editable control, check whether another UI layer is active, then retry and verify the field value before submitting.',
374
+ });
401
375
  }
402
- activeNote.commit(TestResult.PASSED);
376
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
403
377
  return successToolResult('form', {
404
378
  ...toolResult,
405
379
  message: `Form completed successfully with ${lines.length} commands.`,
@@ -411,18 +385,14 @@ export function createCodeceptJSTools(explorer, task) {
411
385
  catch (error) {
412
386
  throwIfFatalBrowserError(error);
413
387
  activeNote.commit(TestResult.FAILED);
414
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
388
+ const errorMessage = errorText(error);
415
389
  return failedToolResult('form', `Form tool failed: ${errorMessage}`);
416
390
  }
417
391
  },
418
392
  }),
419
393
  };
420
394
  }
421
- export function createSpecialContextTools(explorer, context) {
422
- const stateManager = explorer.getStateManager();
423
- if (context !== 'iframe') {
424
- return {};
425
- }
395
+ export function createIframeTools({ explorer, stateManager }) {
426
396
  return {
427
397
  exitIframe: tool({
428
398
  description: dedent `
@@ -442,8 +412,9 @@ export function createSpecialContextTools(explorer, context) {
442
412
  suggestion: 'Continue interacting with the current page context.',
443
413
  });
444
414
  }
445
- await explorer.switchToMainFrame();
446
- const nextState = await explorer.capturePageState();
415
+ const action = explorer.action();
416
+ await action.execute('I.switchTo()');
417
+ const nextState = action.getActionResult();
447
418
  const toolResult = await nextState.toToolResult(previousState, 'I.switchTo()');
448
419
  return successToolResult('exitIframe', {
449
420
  ...toolResult,
@@ -453,14 +424,38 @@ export function createSpecialContextTools(explorer, context) {
453
424
  }
454
425
  catch (error) {
455
426
  throwIfFatalBrowserError(error);
456
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
427
+ const errorMessage = errorText(error);
457
428
  return failedToolResult('exitIframe', `Failed to exit iframe: ${errorMessage}`);
458
429
  }
459
430
  },
460
431
  }),
461
432
  };
462
433
  }
463
- export function createAgentTools({ explorer, researcher, navigator, experienceTracker, getState, supervisor, }) {
434
+ export function createLearnExperienceTool({ getExperienceTracker, getState }) {
435
+ return tool({
436
+ description: dedent `
437
+ Read the full body of a specific experience section listed in <experience>.
438
+ The TOC shows entries like "A.1 ## FLOW: ..." or "A.2 ## ACTION: ...". Pass the fileTag and sectionIndex.
439
+ Only call when a TOC entry looks directly relevant to the current step.
440
+ `,
441
+ inputSchema: z.object({
442
+ fileTag: z.string().describe('File tag from the TOC, e.g. "A", "B", "AA"'),
443
+ sectionIndex: z.number().int().positive().describe('1-based section index within that file'),
444
+ }),
445
+ execute: async ({ fileTag, sectionIndex }) => {
446
+ const state = getState();
447
+ if (!state) {
448
+ return { error: 'No current page state available.' };
449
+ }
450
+ const section = getExperienceTracker().getExperienceSection(fileTag, sectionIndex, state);
451
+ if (!section) {
452
+ return { error: 'Section not found. Experience may have been updated; re-read the latest TOC.' };
453
+ }
454
+ return section;
455
+ },
456
+ });
457
+ }
458
+ export function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }) {
464
459
  let visionDisabled = false;
465
460
  const tools = {
466
461
  see: tool({
@@ -483,7 +478,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
483
478
  return failedToolResult('see', 'Vision tools are disabled for this session. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.');
484
479
  }
485
480
  try {
486
- const actionResult = await explorer.capturePageWithScreenshot();
481
+ const actionResult = await explorer.capture({ screenshot: true });
487
482
  if (!actionResult.screenshot) {
488
483
  return failedToolResult('see', 'Failed to capture screenshot for analysis');
489
484
  }
@@ -499,7 +494,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
499
494
  }
500
495
  catch (error) {
501
496
  throwIfFatalBrowserError(error);
502
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
497
+ const errorMessage = errorText(error);
503
498
  visionDisabled = true;
504
499
  tag('warning').log('⚠️ Vision model is not available. Visual checks are disabled for this session.');
505
500
  return failedToolResult('see', `See tool failed: ${errorMessage}`, {
@@ -527,7 +522,6 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
527
522
  }),
528
523
  execute: async ({ reason }) => {
529
524
  try {
530
- const stateManager = explorer.getStateManager();
531
525
  const currentState = stateManager.getCurrentState();
532
526
  if (!currentState) {
533
527
  return failedToolResult('context', 'No current page state available.');
@@ -545,7 +539,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
545
539
  });
546
540
  }
547
541
  catch (error) {
548
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
542
+ const errorMessage = errorText(error);
549
543
  return failedToolResult('context', `Context tool failed: ${errorMessage}`);
550
544
  }
551
545
  },
@@ -565,7 +559,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
565
559
  }),
566
560
  execute: async ({ assertion }) => {
567
561
  try {
568
- const currentState = explorer.getStateManager().getCurrentState();
562
+ const currentState = stateManager.getCurrentState();
569
563
  const verifications = currentState?.verifications;
570
564
  if (verifications?.[assertion] !== undefined) {
571
565
  return failedToolResult('verify', `Already verified: "${assertion}" → ${verifications[assertion] ? 'PASS' : 'FAIL'}`, {
@@ -574,7 +568,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
574
568
  suggestion: verifications[assertion] ? 'This verification already passed. Call finish() to complete the test.' : 'This verification already failed. Perform actions to change the page state, then try again.',
575
569
  });
576
570
  }
577
- const actionResult = await explorer.capturePageState();
571
+ const actionResult = await explorer.capture();
578
572
  const result = await navigator.verifyState(assertion, actionResult);
579
573
  if (result.verified) {
580
574
  return successToolResult('verify', {
@@ -588,7 +582,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
588
582
  }
589
583
  catch (error) {
590
584
  throwIfFatalBrowserError(error);
591
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
585
+ const errorMessage = errorText(error);
592
586
  return failedToolResult('verify', `Verify tool failed: ${errorMessage}`, {
593
587
  error: errorMessage,
594
588
  });
@@ -618,7 +612,6 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
618
612
  }),
619
613
  execute: async ({ reason }) => {
620
614
  try {
621
- const stateManager = explorer.getStateManager();
622
615
  const currentState = stateManager.getCurrentState();
623
616
  if (!currentState) {
624
617
  return failedToolResult('research', 'No current page state available. Navigate to a page first.');
@@ -641,7 +634,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
641
634
  }
642
635
  catch (error) {
643
636
  throwIfFatalBrowserError(error);
644
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
637
+ const errorMessage = errorText(error);
645
638
  return failedToolResult('research', `Research tool failed: ${errorMessage}`, {
646
639
  error: errorMessage,
647
640
  });
@@ -659,7 +652,6 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
659
652
  }),
660
653
  execute: async ({ instruction }) => {
661
654
  try {
662
- const stateManager = explorer.getStateManager();
663
655
  const currentState = stateManager.getCurrentState();
664
656
  if (!currentState) {
665
657
  return failedToolResult('interact', 'No current page state available. Navigate to a page first.');
@@ -681,7 +673,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
681
673
  }
682
674
  catch (error) {
683
675
  throwIfFatalBrowserError(error);
684
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
676
+ const errorMessage = errorText(error);
685
677
  return failedToolResult('interact', `Interact tool failed: ${errorMessage}`, {
686
678
  error: errorMessage,
687
679
  });
@@ -708,14 +700,13 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
708
700
  return failedToolResult('visualClick', 'Vision tools are disabled for this session. Use xpathCheck() to find the element, then click() with the discovered locator.');
709
701
  }
710
702
  try {
711
- const stateManager = explorer.getStateManager();
712
703
  const currentState = stateManager.getCurrentState();
713
704
  if (!currentState) {
714
705
  return failedToolResult('visualClick', 'No current page state available.');
715
706
  }
716
707
  const previousState = ActionResult.fromState(currentState);
717
- const action = explorer.createAction();
718
- const actionResult = await explorer.capturePageWithScreenshot();
708
+ const action = explorer.action();
709
+ const actionResult = await explorer.capture({ screenshot: true });
719
710
  if (!actionResult.screenshot) {
720
711
  return failedToolResult('visualClick', 'Failed to capture screenshot for visual analysis');
721
712
  }
@@ -749,7 +740,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
749
740
  }
750
741
  catch (error) {
751
742
  throwIfFatalBrowserError(error);
752
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
743
+ const errorMessage = errorText(error);
753
744
  visionDisabled = true;
754
745
  tag('warning').log('⚠️ Vision model is not available. Visual clicks are disabled for this session.');
755
746
  return failedToolResult('visualClick', `visualClick tool failed: ${errorMessage}`, {
@@ -768,7 +759,6 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
768
759
  reason: z.string().describe('Why you need to go back'),
769
760
  }),
770
761
  execute: async ({ reason }) => {
771
- const stateManager = explorer.getStateManager();
772
762
  const currentState = stateManager.getCurrentState();
773
763
  const currentUrl = currentState?.fullUrl || currentState?.url;
774
764
  const history = stateManager.getStateHistory();
@@ -785,27 +775,30 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
785
775
  suggestion: 'Use reset() to navigate to the starting page.',
786
776
  });
787
777
  }
788
- const action = explorer.createAction();
778
+ let previousState = null;
779
+ if (currentState)
780
+ previousState = ActionResult.fromState(currentState);
781
+ const action = explorer.action();
789
782
  const success = await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (BACK to ${targetUrl})`);
790
783
  if (success) {
791
- const previousState = ActionResult.fromState(stateManager.getCurrentState());
792
- const toolResult = await previousState.toToolResult(previousState, `I.amOnPage("${targetUrl}")`);
784
+ const newState = ActionResult.fromState(stateManager.getCurrentState());
785
+ const toolResult = await newState.toToolResult(previousState, `I.amOnPage("${targetUrl}")`);
793
786
  return successToolResult('back', {
794
787
  ...toolResult,
795
788
  message: `Navigated back to ${targetUrl}`,
796
789
  });
797
790
  }
798
- return failedToolResult('back', `Failed to navigate back to ${targetUrl}`, {
799
- suggestion: 'Try reset() to return to the starting page.',
800
- ...(action.lastError && { error: action.lastError.toString() }),
801
- });
791
+ const failData = { suggestion: 'Try reset() to return to the starting page.' };
792
+ if (action.lastError)
793
+ failData.error = action.lastError.toString();
794
+ return failedToolResult('back', `Failed to navigate back to ${targetUrl}`, failData);
802
795
  },
803
796
  }),
804
797
  getVisitedStates: tool({
805
798
  description: 'List all previously visited page states (deduped by URL). Use to find pages to navigate back to.',
806
799
  inputSchema: z.object({}),
807
800
  execute: async () => {
808
- const history = explorer.getStateManager().getStateHistory();
801
+ const history = stateManager.getStateHistory();
809
802
  const seen = new Set();
810
803
  const states = history
811
804
  .map((t) => t.toState)
@@ -816,7 +809,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
816
809
  return true;
817
810
  })
818
811
  .map((s, i) => ({ index: i, url: s.url, title: s.title, h1: s.h1 }));
819
- return { success: true, states };
812
+ return successToolResult('getVisitedStates', { states });
820
813
  },
821
814
  }),
822
815
  xpathCheck: tool({
@@ -838,7 +831,6 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
838
831
  reason: z.string().describe('What element you are looking for and why'),
839
832
  }),
840
833
  execute: async ({ xpath, reason }) => {
841
- const stateManager = explorer.getStateManager();
842
834
  const currentState = stateManager.getCurrentState();
843
835
  if (!currentState) {
844
836
  return failedToolResult('xpathCheck', 'No current page state available.');
@@ -858,40 +850,37 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
858
850
  suggestion: 'Try a broader expression. Examples: //*[contains(@class, "btn")], //button, //*[contains(text(), "keyword")]',
859
851
  });
860
852
  }
861
- const action = explorer.createAction();
862
- const visible = await action.attempt(`I.seeElement(${JSON.stringify(xpath)})`, 'xpathCheck visibility', false);
853
+ const action = explorer.action();
854
+ const visible = await action.attempt(`I.seeElement(${JSON.stringify(xpath)})`, 'xpathCheck visibility');
855
+ const liveElement = await explorer.withPage((page) => WebElement.fromPlaywrightLocator(page.locator(`xpath=${xpath}`)));
863
856
  const matchesSummary = result.elements.map((el, i) => `${i + 1}. <${el.tag} ${el.keyAttrs}> text="${el.text}" html: ${el.outerHTML}`).join('\n');
864
857
  const visibilityNote = visible ? 'Element IS visible in browser — Tester can use this XPath as locator.' : 'Element exists in DOM but is NOT visible. May need scrolling, a click to reveal, or is hidden.';
858
+ const hit = liveElement?.ourAttr('hit') || null;
859
+ const coveredBy = liveElement?.ourAttr('coveredBy') || null;
860
+ let resolvedVisibilityNote = visibilityNote;
861
+ if (hit === 'covered') {
862
+ resolvedVisibilityNote = `Element exists in DOM and is visible, but is covered by ${coveredBy || 'another UI layer'}. Dismiss or move the covering UI before interacting with it.`;
863
+ }
864
+ if (hit === 'offscreen') {
865
+ resolvedVisibilityNote = 'Element exists in DOM but its interaction point is outside the viewport. Scroll or reveal it before interacting.';
866
+ }
865
867
  return successToolResult('xpathCheck', {
866
868
  totalFound: result.totalFound,
867
869
  matches: matchesSummary,
868
- visibilityNote,
870
+ visibilityNote: resolvedVisibilityNote,
871
+ hit,
872
+ coveredBy,
869
873
  xpath,
870
874
  });
871
875
  },
872
876
  }),
873
877
  };
874
- if (experienceTracker && getState) {
875
- tools.learnExperience = tool({
876
- description: dedent `
877
- Read the full body of a specific experience section listed in <experience>.
878
- The TOC shows entries like "A.1 ## FLOW: ..." or "A.2 ## ACTION: ...". Pass the fileTag and sectionIndex.
879
- Only call when a TOC entry looks directly relevant to the current step.
880
- `,
881
- inputSchema: z.object({
882
- fileTag: z.string().describe('File tag from the TOC, e.g. "A", "B", "AA"'),
883
- sectionIndex: z.number().int().positive().describe('1-based section index within that file'),
884
- }),
885
- execute: async ({ fileTag, sectionIndex }) => {
886
- const state = getState();
887
- if (!state) {
888
- return { error: 'No current page state available.' };
889
- }
890
- const section = experienceTracker.getExperienceSection(fileTag, sectionIndex, state);
891
- if (!section) {
892
- return { error: 'Section not found. Experience may have been updated; re-read the latest TOC.' };
893
- }
894
- return section;
878
+ if (withExperience !== false) {
879
+ tools.learnExperience = createLearnExperienceTool({
880
+ getExperienceTracker: () => stateManager.getExperienceTracker(),
881
+ getState: () => {
882
+ const currentState = stateManager.getCurrentState();
883
+ return currentState ? ActionResult.fromState(currentState) : null;
895
884
  },
896
885
  });
897
886
  }
@@ -970,11 +959,16 @@ function transformContainsCommand(command) {
970
959
  }
971
960
  return command;
972
961
  }
973
- function countAriaChanges(ariaChanges) {
974
- const addedCount = (ariaChanges.match(/\n {4}- /g) || []).length;
975
- const removedMatch = ariaChanges.match(/removed: (\d+) interactive/);
976
- const removedCount = removedMatch ? Number.parseInt(removedMatch[1]) : 0;
977
- return addedCount + removedCount;
962
+ function errorText(error) {
963
+ if (error instanceof Error)
964
+ return error.toString();
965
+ return 'Unknown error occurred';
966
+ }
967
+ async function commitNote(activeNote, result, toolResult, action) {
968
+ if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
969
+ activeNote.screenshot = await action.saveScreenshot();
970
+ }
971
+ activeNote.commit(result);
978
972
  }
979
973
  function successToolResult(action, data, source) {
980
974
  const result = { success: true, action, ...data };
@@ -989,7 +983,7 @@ function successToolResult(action, data, source) {
989
983
  const ariaChanges = data.pageDiff.ariaChanges || '';
990
984
  const urlChanged = data.pageDiff.urlChanged === true;
991
985
  const hasHtmlParts = Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
992
- if (countAriaChanges(ariaChanges) >= 50) {
986
+ if (isMajorPageChange(data.pageDiff)) {
993
987
  suggestion = `MAJOR PAGE CHANGE. Page entered a different mode. Check htmlParts and iframes in pageDiff before next action. ${suggestion}`;
994
988
  }
995
989
  else if (!urlChanged && !ariaChanges && !hasHtmlParts) {
@@ -1002,6 +996,18 @@ function successToolResult(action, data, source) {
1002
996
  }
1003
997
  return result;
1004
998
  }
999
+ export function isMajorPageChange(pageDiff) {
1000
+ return pageDiff.urlChanged !== true && (pageDiff.ariaChangeCount ?? 0) >= LARGE_ARIA_CHANGE_THRESHOLD;
1001
+ }
1002
+ function hasObservablePageChange(data) {
1003
+ if (!data?.pageDiff)
1004
+ return false;
1005
+ if (data.pageDiff.urlChanged === true)
1006
+ return true;
1007
+ if (data.pageDiff.ariaChanges)
1008
+ return true;
1009
+ return Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
1010
+ }
1005
1011
  async function failedToolResult(action, message, data, error) {
1006
1012
  const result = { success: false, action, message, ...data };
1007
1013
  if (data?.pageDiff) {
@@ -1085,7 +1091,7 @@ async function disambiguateElements(error, explanation, provider) {
1085
1091
  Return the position number, or null if none of them match.
1086
1092
  `,
1087
1093
  },
1088
- ], schema, provider.getAgenticModel(), { agentName: 'disambiguator', timeout: 15000 });
1094
+ ], schema, provider.getModelForAgent(), { agentName: 'disambiguator', timeout: 15000 });
1089
1095
  const position = result?.object?.position;
1090
1096
  if (position && position >= 1 && position <= elementDetails.length) {
1091
1097
  return { position, xpath: elementDetails[position - 1].xpath };
@@ -1098,7 +1104,7 @@ async function disambiguateElements(error, explanation, provider) {
1098
1104
  }
1099
1105
  }
1100
1106
  function getNotFoundSuggestion(errorMessage) {
1101
- if (!errorMessage.includes('was not found') && !errorMessage.includes('not found by text|CSS|XPath')) {
1107
+ if (!errorMessage.includes('not found')) {
1102
1108
  return null;
1103
1109
  }
1104
1110
  return dedent `
@@ -131,7 +131,7 @@ export class CommandHandler {
131
131
  }
132
132
  }
133
133
  async executeBrowserCommand(input) {
134
- const action = this.explorBot.getExplorer().createAction();
134
+ const action = this.explorBot.getExplorer().action();
135
135
  await action.execute(input);
136
136
  }
137
137
  isCommand(input) {
@@ -1,4 +1,4 @@
1
- import type { ExperienceFile } from '../ai/experience-compactor.js';
1
+ import type { ExperienceFile } from '../experience-tracker.js';
2
2
  import type { ExperienceTracker } from '../experience-tracker.js';
3
3
  import { BaseCommand, type Suggestion } from './base-command.js';
4
4
  export declare class CompactCommand extends BaseCommand {