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
package/src/ai/tools.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import { tool } from 'ai';
2
2
  import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
- import { ActionResult, type ToolResultMetadata } from '../action-result.ts';
4
+ import { ActionResult, type PageDiff, type ToolResultMetadata } from '../action-result.ts';
5
5
  import type { ExperienceTracker } from '../experience-tracker.ts';
6
- import type Explorer from '../explorer.ts';
7
6
  import { type Task, TestResult } from '../test-plan.js';
8
- import { extractFocusedElement } from '../utils/aria.ts';
7
+ import { LARGE_ARIA_CHANGE_THRESHOLD, extractFocusedElement } from '../utils/aria.ts';
9
8
  import { isFatalBrowserError } from '../utils/browser-errors.ts';
10
9
  import { createDebug, tag } from '../utils/logger.js';
11
10
  import { pause } from '../utils/loop.js';
12
11
  import { WebElement } from '../utils/web-element.ts';
12
+ import type { ToolDeps } from './agent.ts';
13
13
  import { Navigator } from './navigator.ts';
14
14
  import type { AIProvider } from './provider.ts';
15
15
  import { Researcher } from './researcher.ts';
@@ -18,11 +18,16 @@ import { isInteractive } from './task-agent.ts';
18
18
 
19
19
  const debugLog = createDebug('explorbot:tools');
20
20
 
21
- export const ASSERTION_TOOLS = ['verify'] as const;
21
+ interface AgentToolDeps extends ToolDeps {
22
+ researcher: Researcher;
23
+ navigator: Navigator;
24
+ supervisor?: boolean;
25
+ withExperience?: boolean;
26
+ }
22
27
 
23
- export function createCodeceptJSTools(explorer: Explorer, task: Task) {
24
- const stateManager = explorer.getStateManager();
28
+ export const ASSERTION_TOOLS = ['verify'] as const;
25
29
 
30
+ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps, task: Task) {
26
31
  return {
27
32
  click: tool({
28
33
  description: dedent`
@@ -40,6 +45,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
40
45
  CRITICAL: All commands MUST target the SAME element using different locators.
41
46
  This is a FALLBACK list, NOT a sequence of different clicks.
42
47
  If you need to click multiple different elements, make SEPARATE click() calls.
48
+ 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.
43
49
  `,
44
50
  inputSchema: z.object({
45
51
  commands: z.array(z.string()).describe(dedent`
@@ -80,33 +86,27 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
80
86
  });
81
87
 
82
88
  const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
83
- const action = explorer.createAction();
89
+ const action = explorer.action();
84
90
  const attempts: Array<{ command: string; success: boolean; error?: string }> = [];
85
91
 
86
92
  for (let i = 0; i < commands.length; i++) {
87
93
  const command = transformContainsCommand(commands[i]);
88
- const isLast = i === commands.length - 1;
89
- const success = await action.attempt(command, explanation, isLast);
94
+ const success = await action.attempt(command, explanation);
90
95
 
91
- attempts.push({
92
- command,
93
- success,
94
- ...(action.lastError && { error: action.lastError.toString() }),
95
- });
96
+ const attempt: { command: string; success: boolean; error?: string } = { command, success };
97
+ if (action.lastError) attempt.error = action.lastError.toString();
98
+ attempts.push(attempt);
96
99
 
97
100
  if (success) {
98
101
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, command);
99
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
100
- activeNote.screenshot = await action.saveScreenshot();
101
- }
102
- activeNote.commit(TestResult.PASSED);
102
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
103
103
  return successToolResult('click', { ...toolResult, attempts, code: command }, action);
104
104
  }
105
105
  }
106
106
 
107
107
  let disambiguated = null;
108
108
  if (attempts.some((a) => a.error?.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN))) {
109
- disambiguated = await disambiguateElements(action.lastError, explanation, explorer.getAIProvider());
109
+ disambiguated = await disambiguateElements(action.lastError, explanation, ai);
110
110
  }
111
111
 
112
112
  if (disambiguated) {
@@ -119,24 +119,18 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
119
119
  retryCommands.push(`I.click('${disambiguated.xpath.replace(/'/g, "\\'")}')`);
120
120
 
121
121
  for (const retryCmd of retryCommands) {
122
- if (!(await action.attempt(retryCmd, explanation, true))) {
122
+ if (!(await action.attempt(retryCmd, explanation))) {
123
123
  attempts.push({ command: retryCmd, success: false, error: action.lastError?.toString() });
124
124
  continue;
125
125
  }
126
126
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, retryCmd);
127
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
128
- activeNote.screenshot = await action.saveScreenshot();
129
- }
130
- activeNote.commit(TestResult.PASSED);
127
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
131
128
  return successToolResult('click', { ...toolResult, attempts, code: retryCmd, disambiguated: true }, action);
132
129
  }
133
130
  }
134
131
 
135
132
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, commands[0]);
136
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
137
- activeNote.screenshot = await action.saveScreenshot();
138
- }
139
- activeNote.commit(TestResult.FAILED);
133
+ await commitNote(activeNote, TestResult.FAILED, toolResult, action);
140
134
 
141
135
  let suggestion = "Try xpathCheck() to find the element's actual position, see() for visual analysis, or visualClick() to click by visual appearance.";
142
136
  const lastError = attempts[attempts.length - 1]?.error || '';
@@ -204,16 +198,14 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
204
198
  });
205
199
 
206
200
  const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
207
- const action = explorer.createAction();
201
+ const action = explorer.action();
208
202
  const attempts: Array<{ command: string; success: boolean; error?: string }> = [];
209
203
 
210
204
  for (const command of commands) {
211
- const success = await action.attempt(command, explanation, true);
212
- attempts.push({
213
- command,
214
- success,
215
- ...(action.lastError && { error: action.lastError.toString() }),
216
- });
205
+ const success = await action.attempt(command, explanation);
206
+ const attempt: { command: string; success: boolean; error?: string } = { command, success };
207
+ if (action.lastError) attempt.error = action.lastError.toString();
208
+ attempts.push(attempt);
217
209
 
218
210
  if (!success) continue;
219
211
 
@@ -276,16 +268,13 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
276
268
 
277
269
  if (!isSingleChar && !isStandardKey) {
278
270
  const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
279
- const action = explorer.createAction();
271
+ const action = explorer.action();
280
272
  const typeCommand = `I.type(${JSON.stringify(key)})`;
281
273
  await action.attempt(typeCommand, explanation);
282
274
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, key);
283
275
 
284
276
  if (!action.lastError) {
285
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
286
- activeNote.screenshot = await action.saveScreenshot();
287
- }
288
- activeNote.commit(TestResult.PASSED);
277
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
289
278
  return successToolResult(
290
279
  'pressKey',
291
280
  {
@@ -299,10 +288,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
299
288
  }
300
289
 
301
290
  const errorMsg = `pressKey fallback to type() failed: ${action.lastError?.toString()}`;
302
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
303
- activeNote.screenshot = await action.saveScreenshot();
304
- }
305
- activeNote.commit(TestResult.FAILED);
291
+ await commitNote(activeNote, TestResult.FAILED, toolResult, action);
306
292
  return failedToolResult('pressKey', errorMsg, {
307
293
  ...toolResult,
308
294
  code: typeCommand,
@@ -325,7 +311,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
325
311
  }
326
312
 
327
313
  const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
328
- const action = explorer.createAction();
314
+ const action = explorer.action();
329
315
 
330
316
  let pressKeyCommand: string;
331
317
  if (modifier) {
@@ -339,10 +325,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
339
325
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, key);
340
326
 
341
327
  if (!action.lastError) {
342
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
343
- activeNote.screenshot = await action.saveScreenshot();
344
- }
345
- activeNote.commit(TestResult.PASSED);
328
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
346
329
  return successToolResult(
347
330
  'pressKey',
348
331
  {
@@ -355,10 +338,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
355
338
  }
356
339
 
357
340
  const errorMsg = `pressKey() failed: ${action.lastError?.toString()}`;
358
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
359
- activeNote.screenshot = await action.saveScreenshot();
360
- }
361
- activeNote.commit(TestResult.FAILED);
341
+ await commitNote(activeNote, TestResult.FAILED, toolResult, action);
362
342
  return failedToolResult('pressKey', errorMsg, {
363
343
  ...toolResult,
364
344
  code: pressKeyCommand,
@@ -367,7 +347,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
367
347
  } catch (error) {
368
348
  throwIfFatalBrowserError(error);
369
349
  activeNote.commit(TestResult.FAILED);
370
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
350
+ const errorMessage = errorText(error);
371
351
  return failedToolResult('pressKey', `PressKey tool failed: ${errorMessage}`);
372
352
  }
373
353
  },
@@ -431,26 +411,18 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
431
411
 
432
412
  const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
433
413
  const formLocator = codeLines[0] || 'form';
434
- const action = explorer.createAction();
435
- const wasInIframe = await explorer.isInsideIframe();
414
+ const action = explorer.action();
436
415
  await action.attempt(codeBlock, explanation);
437
416
 
438
- if (action.lastError && !wasInIframe && (await explorer.isInsideIframe())) {
439
- await explorer.switchToMainFrame();
440
- }
441
-
442
417
  const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, formLocator);
443
418
 
444
419
  if (action.lastError) {
445
420
  const message = action.lastError ? String(action.lastError) : 'Unknown error';
446
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
447
- activeNote.screenshot = await action.saveScreenshot();
448
- }
449
- activeNote.commit(TestResult.FAILED);
421
+ await commitNote(activeNote, TestResult.FAILED, toolResult, action);
450
422
 
451
423
  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.';
452
424
  if (message.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN)) {
453
- const disambiguated = await disambiguateElements(action.lastError, explanation, explorer.getAIProvider());
425
+ const disambiguated = await disambiguateElements(action.lastError, explanation, ai);
454
426
  if (disambiguated) {
455
427
  formSuggestion = `Multiple elements matched. Add step.opts({ elementIndex: ${disambiguated.position} }) to the failing command. Fallback locator: ${disambiguated.xpath}`;
456
428
  }
@@ -468,10 +440,15 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
468
440
  );
469
441
  }
470
442
 
471
- if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
472
- activeNote.screenshot = await action.saveScreenshot();
443
+ if (!hasObservablePageChange(toolResult)) {
444
+ activeNote.commit(TestResult.FAILED);
445
+ return failedToolResult('form', 'Form command executed, but no observable page or form-state change was captured.', {
446
+ ...toolResult,
447
+ code: codeBlock,
448
+ 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.',
449
+ });
473
450
  }
474
- activeNote.commit(TestResult.PASSED);
451
+ await commitNote(activeNote, TestResult.PASSED, toolResult, action);
475
452
  return successToolResult(
476
453
  'form',
477
454
  {
@@ -486,7 +463,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
486
463
  } catch (error) {
487
464
  throwIfFatalBrowserError(error);
488
465
  activeNote.commit(TestResult.FAILED);
489
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
466
+ const errorMessage = errorText(error);
490
467
  return failedToolResult('form', `Form tool failed: ${errorMessage}`);
491
468
  }
492
469
  },
@@ -494,13 +471,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
494
471
  };
495
472
  }
496
473
 
497
- export function createSpecialContextTools(explorer: Explorer, context: 'iframe') {
498
- const stateManager = explorer.getStateManager();
499
-
500
- if (context !== 'iframe') {
501
- return {};
502
- }
503
-
474
+ export function createIframeTools({ explorer, stateManager }: ToolDeps) {
504
475
  return {
505
476
  exitIframe: tool({
506
477
  description: dedent`
@@ -522,9 +493,9 @@ export function createSpecialContextTools(explorer: Explorer, context: 'iframe')
522
493
  });
523
494
  }
524
495
 
525
- await explorer.switchToMainFrame();
526
-
527
- const nextState = await explorer.capturePageState();
496
+ const action = explorer.action();
497
+ await action.execute('I.switchTo()');
498
+ const nextState = action.getActionResult()!;
528
499
  const toolResult = await nextState.toToolResult(previousState, 'I.switchTo()');
529
500
 
530
501
  return successToolResult('exitIframe', {
@@ -534,7 +505,7 @@ export function createSpecialContextTools(explorer: Explorer, context: 'iframe')
534
505
  });
535
506
  } catch (error) {
536
507
  throwIfFatalBrowserError(error);
537
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
508
+ const errorMessage = errorText(error);
538
509
  return failedToolResult('exitIframe', `Failed to exit iframe: ${errorMessage}`);
539
510
  }
540
511
  },
@@ -542,21 +513,32 @@ export function createSpecialContextTools(explorer: Explorer, context: 'iframe')
542
513
  };
543
514
  }
544
515
 
545
- export function createAgentTools({
546
- explorer,
547
- researcher,
548
- navigator,
549
- experienceTracker,
550
- getState,
551
- supervisor,
552
- }: {
553
- explorer: Explorer;
554
- researcher: Researcher;
555
- navigator: Navigator;
556
- experienceTracker?: ExperienceTracker;
557
- getState?: () => ActionResult | null;
558
- supervisor?: boolean;
559
- }): any {
516
+ export function createLearnExperienceTool({ getExperienceTracker, getState }: { getExperienceTracker: () => ExperienceTracker; getState: () => ActionResult | null }) {
517
+ return tool({
518
+ description: dedent`
519
+ Read the full body of a specific experience section listed in <experience>.
520
+ The TOC shows entries like "A.1 ## FLOW: ..." or "A.2 ## ACTION: ...". Pass the fileTag and sectionIndex.
521
+ Only call when a TOC entry looks directly relevant to the current step.
522
+ `,
523
+ inputSchema: z.object({
524
+ fileTag: z.string().describe('File tag from the TOC, e.g. "A", "B", "AA"'),
525
+ sectionIndex: z.number().int().positive().describe('1-based section index within that file'),
526
+ }),
527
+ execute: async ({ fileTag, sectionIndex }) => {
528
+ const state = getState();
529
+ if (!state) {
530
+ return { error: 'No current page state available.' };
531
+ }
532
+ const section = getExperienceTracker().getExperienceSection(fileTag, sectionIndex, state);
533
+ if (!section) {
534
+ return { error: 'Section not found. Experience may have been updated; re-read the latest TOC.' };
535
+ }
536
+ return section;
537
+ },
538
+ });
539
+ }
540
+
541
+ export function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }: AgentToolDeps): any {
560
542
  let visionDisabled = false;
561
543
 
562
544
  const tools: Record<string, any> = {
@@ -581,7 +563,7 @@ export function createAgentTools({
581
563
  }
582
564
 
583
565
  try {
584
- const actionResult = await explorer.capturePageWithScreenshot();
566
+ const actionResult = await explorer.capture({ screenshot: true });
585
567
 
586
568
  if (!actionResult.screenshot) {
587
569
  return failedToolResult('see', 'Failed to capture screenshot for analysis');
@@ -600,7 +582,7 @@ export function createAgentTools({
600
582
  });
601
583
  } catch (error) {
602
584
  throwIfFatalBrowserError(error);
603
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
585
+ const errorMessage = errorText(error);
604
586
  visionDisabled = true;
605
587
  tag('warning').log('⚠️ Vision model is not available. Visual checks are disabled for this session.');
606
588
  return failedToolResult('see', `See tool failed: ${errorMessage}`, {
@@ -629,7 +611,6 @@ export function createAgentTools({
629
611
  }),
630
612
  execute: async ({ reason }) => {
631
613
  try {
632
- const stateManager = explorer.getStateManager();
633
614
  const currentState = stateManager.getCurrentState();
634
615
 
635
616
  if (!currentState) {
@@ -649,7 +630,7 @@ export function createAgentTools({
649
630
  reminder: 'Context provided. Do not call context() again until you perform actions or suspect page changed.',
650
631
  });
651
632
  } catch (error) {
652
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
633
+ const errorMessage = errorText(error);
653
634
  return failedToolResult('context', `Context tool failed: ${errorMessage}`);
654
635
  }
655
636
  },
@@ -670,7 +651,7 @@ export function createAgentTools({
670
651
  }),
671
652
  execute: async ({ assertion }) => {
672
653
  try {
673
- const currentState = explorer.getStateManager().getCurrentState();
654
+ const currentState = stateManager.getCurrentState();
674
655
  const verifications = currentState?.verifications;
675
656
 
676
657
  if (verifications?.[assertion] !== undefined) {
@@ -681,7 +662,7 @@ export function createAgentTools({
681
662
  });
682
663
  }
683
664
 
684
- const actionResult = await explorer.capturePageState();
665
+ const actionResult = await explorer.capture();
685
666
  const result = await navigator.verifyState(assertion, actionResult);
686
667
 
687
668
  if (result.verified) {
@@ -700,7 +681,7 @@ export function createAgentTools({
700
681
  });
701
682
  } catch (error) {
702
683
  throwIfFatalBrowserError(error);
703
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
684
+ const errorMessage = errorText(error);
704
685
  return failedToolResult('verify', `Verify tool failed: ${errorMessage}`, {
705
686
  error: errorMessage,
706
687
  });
@@ -731,7 +712,6 @@ export function createAgentTools({
731
712
  }),
732
713
  execute: async ({ reason }) => {
733
714
  try {
734
- const stateManager = explorer.getStateManager();
735
715
  const currentState = stateManager.getCurrentState();
736
716
 
737
717
  if (!currentState) {
@@ -756,7 +736,7 @@ export function createAgentTools({
756
736
  });
757
737
  } catch (error) {
758
738
  throwIfFatalBrowserError(error);
759
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
739
+ const errorMessage = errorText(error);
760
740
  return failedToolResult('research', `Research tool failed: ${errorMessage}`, {
761
741
  error: errorMessage,
762
742
  });
@@ -775,7 +755,6 @@ export function createAgentTools({
775
755
  }),
776
756
  execute: async ({ instruction }) => {
777
757
  try {
778
- const stateManager = explorer.getStateManager();
779
758
  const currentState = stateManager.getCurrentState();
780
759
 
781
760
  if (!currentState) {
@@ -801,7 +780,7 @@ export function createAgentTools({
801
780
  });
802
781
  } catch (error) {
803
782
  throwIfFatalBrowserError(error);
804
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
783
+ const errorMessage = errorText(error);
805
784
  return failedToolResult('interact', `Interact tool failed: ${errorMessage}`, {
806
785
  error: errorMessage,
807
786
  });
@@ -830,7 +809,6 @@ export function createAgentTools({
830
809
  }
831
810
 
832
811
  try {
833
- const stateManager = explorer.getStateManager();
834
812
  const currentState = stateManager.getCurrentState();
835
813
 
836
814
  if (!currentState) {
@@ -838,8 +816,8 @@ export function createAgentTools({
838
816
  }
839
817
 
840
818
  const previousState = ActionResult.fromState(currentState);
841
- const action = explorer.createAction();
842
- const actionResult = await explorer.capturePageWithScreenshot();
819
+ const action = explorer.action();
820
+ const actionResult = await explorer.capture({ screenshot: true });
843
821
 
844
822
  if (!actionResult.screenshot) {
845
823
  return failedToolResult('visualClick', 'Failed to capture screenshot for visual analysis');
@@ -881,7 +859,7 @@ export function createAgentTools({
881
859
  });
882
860
  } catch (error) {
883
861
  throwIfFatalBrowserError(error);
884
- const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
862
+ const errorMessage = errorText(error);
885
863
  visionDisabled = true;
886
864
  tag('warning').log('⚠️ Vision model is not available. Visual clicks are disabled for this session.');
887
865
  return failedToolResult('visualClick', `visualClick tool failed: ${errorMessage}`, {
@@ -901,7 +879,6 @@ export function createAgentTools({
901
879
  reason: z.string().describe('Why you need to go back'),
902
880
  }),
903
881
  execute: async ({ reason }) => {
904
- const stateManager = explorer.getStateManager();
905
882
  const currentState = stateManager.getCurrentState();
906
883
  const currentUrl = currentState?.fullUrl || currentState?.url;
907
884
  const history = stateManager.getStateHistory();
@@ -921,22 +898,24 @@ export function createAgentTools({
921
898
  });
922
899
  }
923
900
 
924
- const action = explorer.createAction();
901
+ let previousState: ActionResult | null = null;
902
+ if (currentState) previousState = ActionResult.fromState(currentState);
903
+
904
+ const action = explorer.action();
925
905
  const success = await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (BACK to ${targetUrl})`);
926
906
 
927
907
  if (success) {
928
- const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
929
- const toolResult = await previousState.toToolResult(previousState, `I.amOnPage("${targetUrl}")`);
908
+ const newState = ActionResult.fromState(stateManager.getCurrentState()!);
909
+ const toolResult = await newState.toToolResult(previousState, `I.amOnPage("${targetUrl}")`);
930
910
  return successToolResult('back', {
931
911
  ...toolResult,
932
912
  message: `Navigated back to ${targetUrl}`,
933
913
  });
934
914
  }
935
915
 
936
- return failedToolResult('back', `Failed to navigate back to ${targetUrl}`, {
937
- suggestion: 'Try reset() to return to the starting page.',
938
- ...(action.lastError && { error: action.lastError.toString() }),
939
- });
916
+ const failData: Record<string, any> = { suggestion: 'Try reset() to return to the starting page.' };
917
+ if (action.lastError) failData.error = action.lastError.toString();
918
+ return failedToolResult('back', `Failed to navigate back to ${targetUrl}`, failData);
940
919
  },
941
920
  }),
942
921
 
@@ -944,7 +923,7 @@ export function createAgentTools({
944
923
  description: 'List all previously visited page states (deduped by URL). Use to find pages to navigate back to.',
945
924
  inputSchema: z.object({}),
946
925
  execute: async () => {
947
- const history = explorer.getStateManager().getStateHistory();
926
+ const history = stateManager.getStateHistory();
948
927
  const seen = new Set<string>();
949
928
  const states = history
950
929
  .map((t) => t.toState)
@@ -954,7 +933,7 @@ export function createAgentTools({
954
933
  return true;
955
934
  })
956
935
  .map((s, i) => ({ index: i, url: s.url, title: s.title, h1: s.h1 }));
957
- return { success: true, states };
936
+ return successToolResult('getVisitedStates', { states });
958
937
  },
959
938
  }),
960
939
 
@@ -977,7 +956,6 @@ export function createAgentTools({
977
956
  reason: z.string().describe('What element you are looking for and why'),
978
957
  }),
979
958
  execute: async ({ xpath, reason }) => {
980
- const stateManager = explorer.getStateManager();
981
959
  const currentState = stateManager.getCurrentState();
982
960
 
983
961
  if (!currentState) {
@@ -1003,44 +981,42 @@ export function createAgentTools({
1003
981
  });
1004
982
  }
1005
983
 
1006
- const action = explorer.createAction();
1007
- const visible = await action.attempt(`I.seeElement(${JSON.stringify(xpath)})`, 'xpathCheck visibility', false);
984
+ const action = explorer.action();
985
+ const visible = await action.attempt(`I.seeElement(${JSON.stringify(xpath)})`, 'xpathCheck visibility');
986
+ const liveElement = await explorer.withPage((page) => WebElement.fromPlaywrightLocator(page.locator(`xpath=${xpath}`)));
1008
987
 
1009
988
  const matchesSummary = result.elements.map((el, i) => `${i + 1}. <${el.tag} ${el.keyAttrs}> text="${el.text}" html: ${el.outerHTML}`).join('\n');
1010
989
 
1011
990
  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.';
1012
991
 
992
+ const hit = liveElement?.ourAttr('hit') || null;
993
+ const coveredBy = liveElement?.ourAttr('coveredBy') || null;
994
+ let resolvedVisibilityNote = visibilityNote;
995
+ if (hit === 'covered') {
996
+ 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.`;
997
+ }
998
+ if (hit === 'offscreen') {
999
+ resolvedVisibilityNote = 'Element exists in DOM but its interaction point is outside the viewport. Scroll or reveal it before interacting.';
1000
+ }
1001
+
1013
1002
  return successToolResult('xpathCheck', {
1014
1003
  totalFound: result.totalFound,
1015
1004
  matches: matchesSummary,
1016
- visibilityNote,
1005
+ visibilityNote: resolvedVisibilityNote,
1006
+ hit,
1007
+ coveredBy,
1017
1008
  xpath,
1018
1009
  });
1019
1010
  },
1020
1011
  }),
1021
1012
  };
1022
1013
 
1023
- if (experienceTracker && getState) {
1024
- tools.learnExperience = tool({
1025
- description: dedent`
1026
- Read the full body of a specific experience section listed in <experience>.
1027
- The TOC shows entries like "A.1 ## FLOW: ..." or "A.2 ## ACTION: ...". Pass the fileTag and sectionIndex.
1028
- Only call when a TOC entry looks directly relevant to the current step.
1029
- `,
1030
- inputSchema: z.object({
1031
- fileTag: z.string().describe('File tag from the TOC, e.g. "A", "B", "AA"'),
1032
- sectionIndex: z.number().int().positive().describe('1-based section index within that file'),
1033
- }),
1034
- execute: async ({ fileTag, sectionIndex }) => {
1035
- const state = getState();
1036
- if (!state) {
1037
- return { error: 'No current page state available.' };
1038
- }
1039
- const section = experienceTracker.getExperienceSection(fileTag, sectionIndex, state);
1040
- if (!section) {
1041
- return { error: 'Section not found. Experience may have been updated; re-read the latest TOC.' };
1042
- }
1043
- return section;
1014
+ if (withExperience !== false) {
1015
+ tools.learnExperience = createLearnExperienceTool({
1016
+ getExperienceTracker: () => stateManager.getExperienceTracker(),
1017
+ getState: () => {
1018
+ const currentState = stateManager.getCurrentState();
1019
+ return currentState ? ActionResult.fromState(currentState) : null;
1044
1020
  },
1045
1021
  });
1046
1022
  }
@@ -1131,11 +1107,16 @@ function transformContainsCommand(command: string): string {
1131
1107
  return command;
1132
1108
  }
1133
1109
 
1134
- function countAriaChanges(ariaChanges: string): number {
1135
- const addedCount = (ariaChanges.match(/\n {4}- /g) || []).length;
1136
- const removedMatch = ariaChanges.match(/removed: (\d+) interactive/);
1137
- const removedCount = removedMatch ? Number.parseInt(removedMatch[1]) : 0;
1138
- return addedCount + removedCount;
1110
+ function errorText(error: unknown): string {
1111
+ if (error instanceof Error) return error.toString();
1112
+ return 'Unknown error occurred';
1113
+ }
1114
+
1115
+ async function commitNote(activeNote: any, result: TestResult, toolResult: any, action: any): Promise<void> {
1116
+ if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
1117
+ activeNote.screenshot = await action.saveScreenshot();
1118
+ }
1119
+ activeNote.commit(result);
1139
1120
  }
1140
1121
 
1141
1122
  function successToolResult(action: string, data?: Record<string, any>, source?: { playwrightGroupId?: string | null; assertionSteps?: any[] }) {
@@ -1151,7 +1132,7 @@ function successToolResult(action: string, data?: Record<string, any>, source?:
1151
1132
  const ariaChanges = data.pageDiff.ariaChanges || '';
1152
1133
  const urlChanged = data.pageDiff.urlChanged === true;
1153
1134
  const hasHtmlParts = Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
1154
- if (countAriaChanges(ariaChanges) >= 50) {
1135
+ if (isMajorPageChange(data.pageDiff)) {
1155
1136
  suggestion = `MAJOR PAGE CHANGE. Page entered a different mode. Check htmlParts and iframes in pageDiff before next action. ${suggestion}`;
1156
1137
  } else if (!urlChanged && !ariaChanges && !hasHtmlParts) {
1157
1138
  suggestion = 'Action ran without error but produced no observable change (URL, ARIA and HTML all unchanged). The locator likely matched a non-interactive ancestor or an element outside the intended control. Re-locate via xpathCheck() or verify with see() before treating this as success.';
@@ -1163,6 +1144,17 @@ function successToolResult(action: string, data?: Record<string, any>, source?:
1163
1144
  return result;
1164
1145
  }
1165
1146
 
1147
+ export function isMajorPageChange(pageDiff: PageDiff): boolean {
1148
+ return pageDiff.urlChanged !== true && (pageDiff.ariaChangeCount ?? 0) >= LARGE_ARIA_CHANGE_THRESHOLD;
1149
+ }
1150
+
1151
+ function hasObservablePageChange(data?: Record<string, any>): boolean {
1152
+ if (!data?.pageDiff) return false;
1153
+ if (data.pageDiff.urlChanged === true) return true;
1154
+ if (data.pageDiff.ariaChanges) return true;
1155
+ return Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
1156
+ }
1157
+
1166
1158
  async function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null) {
1167
1159
  const result: Record<string, any> = { success: false, action, message, ...data };
1168
1160
  if (data?.pageDiff) {
@@ -1257,7 +1249,7 @@ async function disambiguateElements(error: Error | null | undefined, explanation
1257
1249
  },
1258
1250
  ],
1259
1251
  schema,
1260
- provider.getAgenticModel(),
1252
+ provider.getModelForAgent(),
1261
1253
  { agentName: 'disambiguator', timeout: 15000 }
1262
1254
  );
1263
1255
 
@@ -1273,7 +1265,7 @@ async function disambiguateElements(error: Error | null | undefined, explanation
1273
1265
  }
1274
1266
 
1275
1267
  function getNotFoundSuggestion(errorMessage: string): string | null {
1276
- if (!errorMessage.includes('was not found') && !errorMessage.includes('not found by text|CSS|XPath')) {
1268
+ if (!errorMessage.includes('not found')) {
1277
1269
  return null;
1278
1270
  }
1279
1271