explorbot 0.1.32-beta.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +35 -19
  2. package/bin/explorbot-cli.ts +33 -10
  3. package/boat/api-tester/src/config.ts +45 -3
  4. package/boat/doc-collector/src/ai/documentarian.ts +26 -28
  5. package/boat/doc-collector/src/ai/tools.ts +74 -16
  6. package/boat/doc-collector/src/cli.ts +1 -6
  7. package/boat/doc-collector/src/docbot.ts +30 -9
  8. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  9. package/boat/doc-collector/src/screenshots.ts +34 -2
  10. package/boat/doc-collector/src/state-diagram.ts +281 -0
  11. package/dist/bin/explorbot-cli.js +29 -9
  12. package/dist/boat/api-tester/src/config.js +43 -4
  13. package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
  14. package/dist/boat/doc-collector/src/ai/tools.js +53 -16
  15. package/dist/boat/doc-collector/src/cli.js +1 -5
  16. package/dist/boat/doc-collector/src/docbot.js +27 -8
  17. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  18. package/dist/boat/doc-collector/src/screenshots.js +30 -1
  19. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  20. package/dist/models.json +30 -0
  21. package/dist/package.json +23 -3
  22. package/dist/src/action-result.d.ts +8 -10
  23. package/dist/src/action-result.js +72 -164
  24. package/dist/src/action.d.ts +13 -12
  25. package/dist/src/action.js +37 -113
  26. package/dist/src/ai/agent.d.ts +17 -0
  27. package/dist/src/ai/captain/idle-mode.js +1 -1
  28. package/dist/src/ai/captain/test-mode.js +1 -1
  29. package/dist/src/ai/captain/web-mode.js +18 -29
  30. package/dist/src/ai/captain.d.ts +1 -3
  31. package/dist/src/ai/captain.js +17 -45
  32. package/dist/src/ai/conversation.d.ts +2 -5
  33. package/dist/src/ai/conversation.js +9 -28
  34. package/dist/src/ai/driller.d.ts +2 -17
  35. package/dist/src/ai/driller.js +26 -50
  36. package/dist/src/ai/experience-compactor.d.ts +2 -2
  37. package/dist/src/ai/experience-compactor.js +16 -33
  38. package/dist/src/ai/fisherman-tools.js +1 -1
  39. package/dist/src/ai/fisherman.d.ts +0 -1
  40. package/dist/src/ai/fisherman.js +0 -3
  41. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  42. package/dist/src/ai/historian/codeceptjs.js +4 -3
  43. package/dist/src/ai/historian/playwright.d.ts +4 -1
  44. package/dist/src/ai/historian/playwright.js +4 -3
  45. package/dist/src/ai/historian/screencast.d.ts +2 -1
  46. package/dist/src/ai/historian/screencast.js +2 -2
  47. package/dist/src/ai/historian/utils.d.ts +0 -1
  48. package/dist/src/ai/historian/utils.js +0 -1
  49. package/dist/src/ai/historian.d.ts +10 -8
  50. package/dist/src/ai/historian.js +1 -5
  51. package/dist/src/ai/navigator.d.ts +8 -6
  52. package/dist/src/ai/navigator.js +34 -65
  53. package/dist/src/ai/pilot.d.ts +13 -8
  54. package/dist/src/ai/pilot.js +53 -59
  55. package/dist/src/ai/planner.d.ts +5 -4
  56. package/dist/src/ai/planner.js +66 -52
  57. package/dist/src/ai/provider.d.ts +4 -1
  58. package/dist/src/ai/provider.js +77 -137
  59. package/dist/src/ai/quartermaster.d.ts +22 -22
  60. package/dist/src/ai/quartermaster.js +8 -14
  61. package/dist/src/ai/rerunner.d.ts +3 -13
  62. package/dist/src/ai/rerunner.js +23 -44
  63. package/dist/src/ai/researcher/cache.js +11 -18
  64. package/dist/src/ai/researcher/coordinates.js +4 -3
  65. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
  66. package/dist/src/ai/researcher/deep-analysis.js +8 -11
  67. package/dist/src/ai/researcher/locators.d.ts +9 -2
  68. package/dist/src/ai/researcher/locators.js +65 -8
  69. package/dist/src/ai/researcher/parser.js +1 -1
  70. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  71. package/dist/src/ai/researcher/research-result.js +0 -19
  72. package/dist/src/ai/researcher/sections.d.ts +2 -0
  73. package/dist/src/ai/researcher/sections.js +7 -3
  74. package/dist/src/ai/researcher.js +40 -106
  75. package/dist/src/ai/rules.d.ts +1 -0
  76. package/dist/src/ai/rules.js +22 -15
  77. package/dist/src/ai/session-analyst.js +8 -5
  78. package/dist/src/ai/task-agent.d.ts +19 -6
  79. package/dist/src/ai/task-agent.js +37 -33
  80. package/dist/src/ai/tester.d.ts +6 -14
  81. package/dist/src/ai/tester.js +46 -111
  82. package/dist/src/ai/tools.d.ts +26 -14
  83. package/dist/src/ai/tools.js +138 -132
  84. package/dist/src/command-handler.js +1 -1
  85. package/dist/src/commands/compact-command.d.ts +1 -1
  86. package/dist/src/commands/compact-command.js +6 -6
  87. package/dist/src/commands/context-aria-command.js +1 -1
  88. package/dist/src/commands/context-command.js +8 -6
  89. package/dist/src/commands/context-data-command.js +2 -2
  90. package/dist/src/commands/context-experience-command.js +2 -2
  91. package/dist/src/commands/context-html-command.js +2 -2
  92. package/dist/src/commands/context-knowledge-command.js +2 -2
  93. package/dist/src/commands/drill-command.js +1 -1
  94. package/dist/src/commands/experience-command.js +1 -1
  95. package/dist/src/commands/explore-command.js +4 -3
  96. package/dist/src/commands/freesail-command.js +2 -2
  97. package/dist/src/commands/knows-command.js +1 -1
  98. package/dist/src/commands/learn-command.js +5 -3
  99. package/dist/src/commands/path-command.js +1 -1
  100. package/dist/src/commands/research-command.js +1 -1
  101. package/dist/src/commands/test-command.js +1 -1
  102. package/dist/src/components/AddKnowledge.d.ts +2 -0
  103. package/dist/src/components/AddKnowledge.js +3 -7
  104. package/dist/src/components/App.js +3 -3
  105. package/dist/src/config.d.ts +19 -0
  106. package/dist/src/config.js +156 -10
  107. package/dist/src/experience-tracker.d.ts +11 -6
  108. package/dist/src/experience-tracker.js +82 -155
  109. package/dist/src/explorbot.d.ts +21 -13
  110. package/dist/src/explorbot.js +84 -80
  111. package/dist/src/explorer.d.ts +66 -102
  112. package/dist/src/explorer.js +332 -623
  113. package/dist/src/knowledge-tracker.d.ts +2 -2
  114. package/dist/src/knowledge-tracker.js +38 -45
  115. package/dist/src/state-manager.d.ts +10 -30
  116. package/dist/src/state-manager.js +11 -129
  117. package/dist/src/utils/aria.d.ts +6 -1
  118. package/dist/src/utils/aria.js +4 -1
  119. package/dist/src/utils/cache.d.ts +15 -0
  120. package/dist/src/utils/cache.js +34 -0
  121. package/dist/src/utils/context-formatter.d.ts +6 -0
  122. package/dist/src/utils/context-formatter.js +15 -27
  123. package/dist/src/utils/hooks-runner.js +2 -4
  124. package/dist/src/utils/html.d.ts +5 -0
  125. package/dist/src/utils/html.js +71 -0
  126. package/dist/src/utils/markdown-files.d.ts +10 -0
  127. package/dist/src/utils/markdown-files.js +21 -0
  128. package/dist/src/utils/markdown-query.d.ts +6 -0
  129. package/dist/src/utils/markdown-query.js +14 -1
  130. package/dist/src/utils/page-readiness.d.ts +1 -0
  131. package/dist/src/utils/page-readiness.js +1 -1
  132. package/dist/src/utils/secrets.js +2 -2
  133. package/dist/src/utils/strings.d.ts +2 -0
  134. package/dist/src/utils/strings.js +10 -0
  135. package/dist/src/utils/test-files.js +1 -1
  136. package/dist/src/utils/web-annotate.d.ts +5 -0
  137. package/dist/src/utils/web-annotate.js +58 -0
  138. package/dist/src/utils/web-eidx.d.ts +2 -0
  139. package/dist/src/utils/web-eidx.js +20 -0
  140. package/dist/src/utils/web-element.d.ts +3 -1
  141. package/dist/src/utils/web-element.js +32 -0
  142. package/dist/src/utils/web-sandbox.d.ts +4 -0
  143. package/dist/src/utils/web-sandbox.js +37 -0
  144. package/models.json +30 -0
  145. package/package.json +23 -3
  146. package/src/action-result.ts +70 -173
  147. package/src/action.ts +38 -132
  148. package/src/ai/agent.ts +20 -0
  149. package/src/ai/captain/idle-mode.ts +1 -1
  150. package/src/ai/captain/test-mode.ts +1 -1
  151. package/src/ai/captain/web-mode.ts +18 -30
  152. package/src/ai/captain.ts +17 -42
  153. package/src/ai/conversation.ts +9 -32
  154. package/src/ai/driller.ts +53 -83
  155. package/src/ai/experience-compactor.ts +16 -35
  156. package/src/ai/fisherman-tools.ts +1 -1
  157. package/src/ai/fisherman.ts +0 -4
  158. package/src/ai/historian/codeceptjs.ts +5 -3
  159. package/src/ai/historian/playwright.ts +7 -4
  160. package/src/ai/historian/screencast.ts +4 -3
  161. package/src/ai/historian/utils.ts +0 -1
  162. package/src/ai/historian.ts +6 -8
  163. package/src/ai/navigator.ts +39 -70
  164. package/src/ai/pilot.ts +59 -61
  165. package/src/ai/planner.ts +76 -62
  166. package/src/ai/provider.ts +85 -146
  167. package/src/ai/quartermaster.ts +39 -45
  168. package/src/ai/rerunner.ts +27 -50
  169. package/src/ai/researcher/cache.ts +10 -16
  170. package/src/ai/researcher/coordinates.ts +4 -3
  171. package/src/ai/researcher/deep-analysis.ts +10 -11
  172. package/src/ai/researcher/locators.ts +66 -9
  173. package/src/ai/researcher/parser.ts +1 -1
  174. package/src/ai/researcher/research-result.ts +0 -18
  175. package/src/ai/researcher/sections.ts +9 -3
  176. package/src/ai/researcher.ts +42 -119
  177. package/src/ai/rules.ts +24 -13
  178. package/src/ai/session-analyst.ts +8 -5
  179. package/src/ai/task-agent.ts +45 -38
  180. package/src/ai/tester.ts +51 -129
  181. package/src/ai/tools.ts +145 -153
  182. package/src/command-handler.ts +1 -1
  183. package/src/commands/compact-command.ts +7 -8
  184. package/src/commands/context-aria-command.ts +1 -1
  185. package/src/commands/context-command.ts +8 -6
  186. package/src/commands/context-data-command.ts +2 -2
  187. package/src/commands/context-experience-command.ts +2 -2
  188. package/src/commands/context-html-command.ts +2 -2
  189. package/src/commands/context-knowledge-command.ts +2 -2
  190. package/src/commands/drill-command.ts +1 -1
  191. package/src/commands/experience-command.ts +1 -1
  192. package/src/commands/explore-command.ts +4 -3
  193. package/src/commands/freesail-command.ts +2 -2
  194. package/src/commands/knows-command.ts +1 -1
  195. package/src/commands/learn-command.ts +5 -3
  196. package/src/commands/path-command.ts +1 -1
  197. package/src/commands/research-command.ts +1 -1
  198. package/src/commands/test-command.ts +1 -1
  199. package/src/components/AddKnowledge.tsx +5 -7
  200. package/src/components/App.tsx +3 -3
  201. package/src/config.ts +185 -11
  202. package/src/experience-tracker.ts +89 -152
  203. package/src/explorbot.ts +88 -83
  204. package/src/explorer.ts +402 -693
  205. package/src/knowledge-tracker.ts +42 -49
  206. package/src/state-manager.ts +20 -155
  207. package/src/utils/aria.ts +11 -2
  208. package/src/utils/cache.ts +40 -0
  209. package/src/utils/context-formatter.ts +12 -15
  210. package/src/utils/hooks-runner.ts +2 -4
  211. package/src/utils/html.ts +79 -0
  212. package/src/utils/markdown-files.ts +30 -0
  213. package/src/utils/markdown-query.ts +15 -1
  214. package/src/utils/page-readiness.ts +1 -1
  215. package/src/utils/secrets.ts +2 -3
  216. package/src/utils/strings.ts +12 -0
  217. package/src/utils/test-files.ts +1 -1
  218. package/src/utils/web-annotate.ts +64 -0
  219. package/src/utils/web-eidx.ts +21 -0
  220. package/src/utils/web-element.ts +34 -0
  221. package/src/utils/web-sandbox.ts +43 -0
  222. package/dist/boat/api-tester/example/apibot.config.js +0 -30
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import type { ExplorbotConfig } from '../config.ts';
3
- import { ExperienceTracker } from '../experience-tracker.ts';
3
+ import type { ExperienceTracker } from '../experience-tracker.ts';
4
+ import type Explorer from '../explorer.ts';
4
5
  import type { PlaywrightRecorder } from '../playwright-recorder.ts';
5
6
  import type { Reporter } from '../reporter.ts';
6
7
  import type { StateManager } from '../state-manager.ts';
@@ -13,8 +14,6 @@ import { type PlaywrightMethods, WithPlaywright } from './historian/playwright.t
13
14
  import { type ScreencastMethods, WithScreencast } from './historian/screencast.ts';
14
15
  import type { Provider } from './provider.ts';
15
16
 
16
- export { isNonReusableCode } from '../utils/step-analyzer.ts';
17
-
18
17
  const HistorianBase = WithScreencast(WithPlaywright(WithCodeceptJS(WithExperience(Object as unknown as new (...args: any[]) => object))));
19
18
 
20
19
  export interface Historian extends ExperienceMethods, CodeceptJSMethods, PlaywrightMethods, ScreencastMethods {}
@@ -25,13 +24,13 @@ export class Historian extends HistorianBase {
25
24
  declare reporter: Reporter | undefined;
26
25
  declare stateManager: StateManager | undefined;
27
26
  declare config: ExplorbotConfig | undefined;
28
- declare playwright: { recorder: PlaywrightRecorder; helper: any } | undefined;
27
+ declare playwright: { recorder: PlaywrightRecorder; explorer: Explorer } | undefined;
29
28
  declare savedFiles: Set<string>;
30
29
 
31
- constructor(provider: Provider, experienceTracker?: ExperienceTracker, reporter?: Reporter, stateManager?: StateManager, config?: ExplorbotConfig, playwright?: { recorder: PlaywrightRecorder; helper: any }) {
30
+ constructor(provider: Provider, experienceTracker: ExperienceTracker, reporter?: Reporter, stateManager?: StateManager, config?: ExplorbotConfig, playwright?: { recorder: PlaywrightRecorder; explorer: Explorer }) {
32
31
  super();
33
32
  this.provider = provider;
34
- this.experienceTracker = experienceTracker || new ExperienceTracker();
33
+ this.experienceTracker = experienceTracker;
35
34
  this.reporter = reporter;
36
35
  this.stateManager = stateManager;
37
36
  this.config = config;
@@ -52,11 +51,10 @@ export class Historian extends HistorianBase {
52
51
  return this.isPlaywrightFramework() ? this.savePlaywrightPlanToFile(plan) : this.saveCodeceptPlanToFile(plan);
53
52
  }
54
53
 
55
- rewriteScenarioInFile(filePath: string, healedSteps: Array<{ test: string; original: string; healed: string }>): void {
54
+ rewriteScenarioInFile(filePath: string, healedSteps: Array<{ original: string; healed: string }>): void {
56
55
  let content = readFileSync(filePath, 'utf-8');
57
56
 
58
57
  for (const step of healedSteps) {
59
- if (!content.includes(step.original)) continue;
60
58
  content = content.replace(step.original, step.healed);
61
59
  }
62
60
 
@@ -3,31 +3,30 @@ import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from '../action-result.js';
5
5
  import type Action from '../action.ts';
6
- import { ExperienceTracker, renderExperienceToc } from '../experience-tracker.js';
6
+ import type { ExplorbotConfig } from '../config.ts';
7
+ import type { ExperienceTracker } from '../experience-tracker.js';
7
8
  import Explorer from '../explorer.ts';
8
- import { KnowledgeTracker } from '../knowledge-tracker.js';
9
- import { type WebPageState, normalizeUrl } from '../state-manager.js';
9
+ import type { KnowledgeTracker } from '../knowledge-tracker.js';
10
+ import { type StateManager, normalizeUrl } from '../state-manager.js';
10
11
  import { extractCodeBlocks } from '../utils/code-extractor.js';
11
12
  import { HooksRunner } from '../utils/hooks-runner.ts';
12
13
  import { createDebug, pluralize, tag } from '../utils/logger.js';
13
14
  import { loop, pause } from '../utils/loop.js';
14
15
  import { RulesLoader } from '../utils/rules-loader.ts';
15
16
  import { extractStatePath } from '../utils/url-matcher.js';
16
- import type { Agent } from './agent.js';
17
+ import type { Agent, AgentDeps } from './agent.js';
17
18
  import type { Conversation } from './conversation.js';
18
- import { ExperienceCompactor } from './experience-compactor.js';
19
19
  import type { Provider } from './provider.js';
20
20
  import { Researcher } from './researcher.ts';
21
21
  import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
22
22
  import { isInteractive } from './task-agent.js';
23
- import { createAgentTools } from './tools.ts';
23
+ import { createLearnExperienceTool } from './tools.ts';
24
24
 
25
25
  const debugLog = createDebug('explorbot:navigator');
26
26
 
27
27
  class Navigator implements Agent {
28
28
  emoji = '🧭';
29
29
  private provider: Provider;
30
- private experienceCompactor: ExperienceCompactor;
31
30
  private knowledgeTracker: KnowledgeTracker;
32
31
  private experienceTracker: ExperienceTracker;
33
32
  private hooksRunner: HooksRunner;
@@ -72,26 +71,29 @@ class Navigator implements Agent {
72
71
  </rules>
73
72
  `;
74
73
  private explorer: Explorer;
75
-
76
- constructor(explorer: Explorer, provider: Provider, experienceCompactor: ExperienceCompactor, experienceTracker?: ExperienceTracker) {
77
- this.provider = provider;
78
- this.explorer = explorer;
79
- this.experienceCompactor = experienceCompactor;
80
- this.knowledgeTracker = new KnowledgeTracker();
81
- this.experienceTracker = experienceTracker || new ExperienceTracker();
82
- this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
74
+ private config: ExplorbotConfig;
75
+ private stateManager: StateManager;
76
+
77
+ constructor(deps: AgentDeps) {
78
+ this.provider = deps.ai;
79
+ this.explorer = deps.explorer;
80
+ this.config = deps.config;
81
+ this.stateManager = deps.stateManager;
82
+ this.knowledgeTracker = deps.knowledgeTracker;
83
+ this.experienceTracker = deps.stateManager.getExperienceTracker();
84
+ this.hooksRunner = new HooksRunner(deps.explorer, deps.config);
83
85
  }
84
86
 
85
87
  private get verifyAttempts(): number {
86
- return this.explorer.getConfig().ai?.agents?.navigator?.verifyAttempts ?? 3;
88
+ return this.config.ai?.agents?.navigator?.verifyAttempts ?? 3;
87
89
  }
88
90
 
89
91
  private get verifyTimeout(): number {
90
- return this.explorer.getConfig().ai?.agents?.navigator?.verifyTimeout ?? 1500;
92
+ return this.config.ai?.agents?.navigator?.verifyTimeout ?? 1500;
91
93
  }
92
94
 
93
95
  private getBaseOrigin(): string | null {
94
- const baseUrl = this.explorer.getConfig().playwright.url;
96
+ const baseUrl = this.config.playwright.url;
95
97
  try {
96
98
  return new URL(baseUrl).origin;
97
99
  } catch {
@@ -136,12 +138,12 @@ class Navigator implements Agent {
136
138
  }
137
139
 
138
140
  async visit(url: string): Promise<void> {
139
- return this.explorer.runWithBrowserRecovery('navigator.visit', () => this.visitOnce(url));
141
+ return this.visitOnce(url);
140
142
  }
141
143
 
142
144
  private async visitOnce(url: string): Promise<void> {
143
145
  try {
144
- const action = this.explorer.createAction();
146
+ const action = this.explorer.action();
145
147
 
146
148
  await action.execute(`I.amOnPage('${url}')`);
147
149
 
@@ -174,7 +176,7 @@ class Navigator implements Agent {
174
176
  throw new Error(`Navigation to ${url} failed: ${action.lastError?.message}`);
175
177
  }
176
178
  }
177
- await this.explorer.capturePageWithScreenshot();
179
+ await this.explorer.capture({ screenshot: true });
178
180
  await this.hooksRunner.runAfterHook('navigator', url);
179
181
  } catch (error) {
180
182
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -191,22 +193,12 @@ class Navigator implements Agent {
191
193
  tag('info').log('AI Navigator resolving state at', actionResult.url);
192
194
  debugLog('Resolution message:', message);
193
195
 
194
- const action = opts?.action ?? this.explorer.createAction();
196
+ const action = opts?.action ?? this.explorer.action();
195
197
  const expectedUrl = opts?.expectedUrl;
196
198
 
197
- let knowledge = '';
199
+ const knowledge = this.knowledgeTracker.renderRelevantKnowledge(actionResult);
198
200
  let experience = '';
199
201
 
200
- const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
201
- if (relevantKnowledge.length > 0) {
202
- const knowledgeContent = relevantKnowledge.map((k) => k.content).join('\n\n');
203
- knowledge = `
204
- <hint>
205
- Here is relevant knowledge for this page:
206
- ${knowledgeContent}
207
- </hint>`;
208
- }
209
-
210
202
  if (!actionResult.isInsideIframe) {
211
203
  const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
212
204
  if (successful.length > 0) {
@@ -354,7 +346,7 @@ class Navigator implements Agent {
354
346
  codeBlockIndex++;
355
347
  totalAttempts++;
356
348
 
357
- await this.explorer.switchToMainFrame();
349
+ await action.exitIframe();
358
350
 
359
351
  const prevActionResult = action.actionResult ?? actionResult;
360
352
  const prevHash = prevActionResult.getStateHash();
@@ -386,7 +378,7 @@ class Navigator implements Agent {
386
378
  // URL did not transition to expectedUrl within timeout
387
379
  }
388
380
  }
389
- const freshState = await this.explorer.capturePageState();
381
+ const freshState = await this.explorer.capture();
390
382
  const currentUrl = /^https?:\/\//i.test(expectedUrl) ? freshState.fullUrl || freshState.url || '' : freshState.url || '';
391
383
  const urlMatches = this.isSameExpectedOrigin(expectedUrl, action.stateManager) && normalizeUrl(currentUrl) === normalizeUrl(expectedUrl);
392
384
  const stateChanged = freshState.getStateHash() !== actionResult.getStateHash();
@@ -397,7 +389,6 @@ class Navigator implements Agent {
397
389
  if (freshState.getStateHash() !== prevHash) {
398
390
  try {
399
391
  const diff = await freshState.diff(prevActionResult);
400
- await diff.calculate();
401
392
  ariaChanges = diff.ariaChanged;
402
393
  } catch (err) {
403
394
  debugLog('Failed to compute pageDiff for failed URL verification:', err);
@@ -486,24 +477,17 @@ class Navigator implements Agent {
486
477
  }
487
478
 
488
479
  private buildExperienceTools(): { learnExperience: unknown } | undefined {
489
- const stateManager = this.explorer.getStateManager();
480
+ if (!this.experienceTracker) return undefined;
481
+ const stateManager = this.stateManager;
490
482
  const getState = () => {
491
483
  const s = stateManager.getCurrentState();
492
484
  return s ? ActionResult.fromState(s) : null;
493
485
  };
494
- const { learnExperience } = createAgentTools({
495
- explorer: this.explorer,
496
- researcher: null as unknown as Researcher,
497
- navigator: this,
498
- experienceTracker: this.experienceTracker,
499
- getState,
500
- });
501
- if (!learnExperience) return undefined;
502
- return { learnExperience };
486
+ return { learnExperience: createLearnExperienceTool({ getExperienceTracker: () => this.experienceTracker, getState }) };
503
487
  }
504
488
 
505
489
  async freeSail(opts?: { strategy?: 'deep' | 'shallow'; scope?: string; visitedUrls?: Set<string> }, actionResult?: ActionResult): Promise<{ target: string; reason: string } | null> {
506
- const stateManager = this.explorer.getStateManager();
490
+ const stateManager = this.stateManager;
507
491
  const state = stateManager.getCurrentState();
508
492
  if (!state) {
509
493
  return null;
@@ -641,26 +625,11 @@ class Navigator implements Agent {
641
625
  return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
642
626
  }
643
627
 
644
- let knowledge = '';
628
+ const knowledge = this.knowledgeTracker.renderRelevantKnowledge(actionResult);
645
629
  let experience = '';
646
630
 
647
- const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
648
- if (relevantKnowledge.length > 0) {
649
- const knowledgeContent = relevantKnowledge.map((k) => k.content).join('\n\n');
650
- knowledge = `
651
- <hint>
652
- Here is relevant knowledge for this page:
653
- ${knowledgeContent}
654
- </hint>`;
655
- }
656
-
657
631
  if (!actionResult.isInsideIframe) {
658
- const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
659
- if (toc.length > 0) {
660
- const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
661
- tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
662
- experience = renderExperienceToc(toc);
663
- }
632
+ experience = this.experienceTracker.renderExperienceTocFor(actionResult);
664
633
  }
665
634
 
666
635
  const priorVerifications = Object.entries(actionResult.verifications ?? {});
@@ -726,11 +695,11 @@ class Navigator implements Agent {
726
695
  const successfulCodes: string[] = [];
727
696
  const assertionSteps: Array<{ name: string; args: any[] }> = [];
728
697
 
729
- const action = this.explorer.createAction();
698
+ const action = this.explorer.action();
730
699
  let failures = 0;
731
700
 
732
- const page = this.explorer.playwrightHelper?.page;
733
- const originalTimeout = this.explorer.playwrightHelper?.options?.timeout ?? 3000;
701
+ const page = this.explorer.page;
702
+ const originalTimeout = this.config.playwright.timeout ?? 3000;
734
703
  page?.setDefaultTimeout(this.verifyTimeout);
735
704
 
736
705
  try {
@@ -762,9 +731,9 @@ class Navigator implements Agent {
762
731
  return;
763
732
  }
764
733
 
765
- await this.explorer.switchToMainFrame();
734
+ await action.exitIframe();
766
735
 
767
- const verified = await action.attempt(codeBlock, message, false);
736
+ const verified = await action.attempt(codeBlock, message);
768
737
 
769
738
  if (verified) {
770
739
  tag('success').log('Verification passed');
@@ -800,7 +769,7 @@ class Navigator implements Agent {
800
769
  if (alreadyVerified) verified = true;
801
770
 
802
771
  actionResult.addVerification(message, verified);
803
- this.explorer.getStateManager().updateState(actionResult);
772
+ this.stateManager.updateState(actionResult);
804
773
 
805
774
  return { verified, successfulCodes, assertionSteps, totalAttempted };
806
775
  }
package/src/ai/pilot.ts CHANGED
@@ -2,9 +2,11 @@ import { tool } from 'ai';
2
2
  import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from '../action-result.ts';
5
+ import type { RequestStore } from '../api/request-store.ts';
5
6
  import { ConfigParser } from '../config.ts';
6
- import { type ExperienceTracker, renderExperienceToc } from '../experience-tracker.ts';
7
7
  import type Explorer from '../explorer.ts';
8
+ import type { PlaywrightRecorder } from '../playwright-recorder.ts';
9
+ import type { StateManager } from '../state-manager.ts';
8
10
  import { type Test, TestResult } from '../test-plan.ts';
9
11
  import { collectInteractiveNodes, detectFocusArea, extractFocusedElement } from '../utils/aria.ts';
10
12
  import { ErrorPageError } from '../utils/error-page.ts';
@@ -12,7 +14,7 @@ import { createDebug, tag } from '../utils/logger.ts';
12
14
 
13
15
  const debugLog = createDebug('explorbot:pilot');
14
16
  import { truncateJson } from '../utils/strings.ts';
15
- import type { Agent } from './agent.ts';
17
+ import type { Agent, AgentDeps } from './agent.ts';
16
18
  import type { Conversation } from './conversation.ts';
17
19
  import type { Fisherman } from './fisherman.ts';
18
20
  import type { Navigator } from './navigator.ts';
@@ -31,15 +33,19 @@ export class Pilot implements Agent {
31
33
  private conversation: Conversation | null = null;
32
34
  private researcher: Researcher;
33
35
  private explorer: Explorer;
36
+ private stateManager: StateManager;
37
+ private requestStore: RequestStore;
38
+ private playwrightRecorder: PlaywrightRecorder;
34
39
  private fisherman: Fisherman | null = null;
35
- private experienceTracker: ExperienceTracker | null;
36
40
 
37
- constructor(provider: Provider, agentTools: any, researcher: Researcher, explorer: Explorer, experienceTracker?: ExperienceTracker) {
38
- this.provider = provider;
41
+ constructor(deps: AgentDeps, agentTools: any, researcher: Researcher) {
42
+ this.provider = deps.ai;
39
43
  this.agentTools = agentTools;
40
44
  this.researcher = researcher;
41
- this.explorer = explorer;
42
- this.experienceTracker = experienceTracker || null;
45
+ this.explorer = deps.explorer;
46
+ this.stateManager = deps.stateManager;
47
+ this.requestStore = deps.requestStore;
48
+ this.playwrightRecorder = deps.playwrightRecorder;
43
49
  }
44
50
 
45
51
  setFisherman(fisherman: Fisherman): void {
@@ -94,7 +100,7 @@ export class Pilot implements Agent {
94
100
  let screenshotState: ActionResult | null = null;
95
101
  if (type === 'finish' && this.provider.hasVision()) {
96
102
  try {
97
- screenshotState = await this.explorer.capturePageWithScreenshot();
103
+ screenshotState = await this.explorer.capture({ screenshot: true });
98
104
  if (screenshotState.screenshot) {
99
105
  visualAnalysis = (await this.researcher.answerQuestionAboutScreenshot(screenshotState, `Describe current page state relevant to: ${task.scenario}`)) || '';
100
106
  }
@@ -111,7 +117,7 @@ export class Pilot implements Agent {
111
117
  .string()
112
118
  .nullable()
113
119
  .describe(
114
- 'REQUIRED whenever decision is "pass" — a one-sentence natural-language claim about the current page that, if true, proves the scenario goal (e.g., "New test suite \\"Foo\\" is visible in the suites list"). NOT code: do not write I.*, expect(), .then(), grabTitle, or any JavaScript. Navigator translates the claim into CodeceptJS assertions and runs them; passing assertions are saved to the generated test file. Also use when evidence is insufficient before deciding pass/fail. Leave null for "continue", "fail", or "skipped".'
120
+ 'REQUIRED whenever decision is "pass" — a one-sentence natural-language claim about the current page that, if true, proves the scenario goal (e.g., "New item \\"Foo\\" is visible in the items list"). NOT code: do not write I.*, expect(), .then(), grabTitle, or any JavaScript. Navigator translates the claim into CodeceptJS assertions and runs them; passing assertions are saved to the generated test file. Also use when evidence is insufficient before deciding pass/fail. Leave null for "continue", "fail", or "skipped".'
115
121
  ),
116
122
  });
117
123
 
@@ -147,7 +153,7 @@ export class Pilot implements Agent {
147
153
  - Mixed evidence + final state shows success → pass. Mixed + final state unclear → continue with guidance.
148
154
 
149
155
  When deciding "pass", you MUST also set requestVerification to a one-sentence natural-language
150
- claim about the current page (e.g., "New test suite Foo is visible in the suites list"). NOT
156
+ claim about the current page (e.g., "New item Foo is visible in the items list"). NOT
151
157
  code — do not write I.*, expect(), .then(), or any JavaScript. Choose the strongest single
152
158
  piece of evidence (a unique element/text that exists ONLY because the scenario succeeded).
153
159
  Navigator translates the claim into CodeceptJS assertions; without it the generated test has
@@ -157,7 +163,7 @@ export class Pilot implements Agent {
157
163
  const messages = [
158
164
  {
159
165
  role: 'system' as const,
160
- content: this.buildVerdictSystemPrompt(type, task),
166
+ content: this.buildVerdictSystemPrompt(task),
161
167
  },
162
168
  { role: 'user' as const, content: userContent },
163
169
  ];
@@ -178,7 +184,7 @@ export class Pilot implements Agent {
178
184
  tag('substep').log(`Pilot requesting verification: ${result.requestVerification}`);
179
185
  const verifyResult = await navigator.verifyState(result.requestVerification, currentState).catch(() => null);
180
186
  if (verifyResult?.verified && verifyResult.assertionSteps?.length) {
181
- this.explorer.getPlaywrightRecorder().recordVerification(verifyResult.assertionSteps);
187
+ this.playwrightRecorder.recordVerification(verifyResult.assertionSteps);
182
188
  }
183
189
  }
184
190
 
@@ -304,10 +310,8 @@ export class Pilot implements Agent {
304
310
  }
305
311
  }
306
312
 
307
- private buildSharedEvidenceRules(task: Test): string {
313
+ private buildSharedEvidenceRules(): string {
308
314
  return dedent`
309
- SCENARIO: ${task.scenario}
310
-
311
315
  EVIDENCE PRIORITY (strict):
312
316
  1) Final observable state proving the scenario goal
313
317
  2) verify()/see() results in the LAST few actions before stop/finish
@@ -340,6 +344,12 @@ export class Pilot implements Agent {
340
344
 
341
345
  Expected results are MILESTONES, not the goal. Never fail because a milestone (toast, icon, styling)
342
346
  didn't match if the scenario goal IS accomplished.
347
+ `;
348
+ }
349
+
350
+ private buildTaskContext(task: Test): string {
351
+ return dedent`
352
+ SCENARIO: ${task.scenario}
343
353
 
344
354
  ${this.buildDeletionScope(task)}
345
355
 
@@ -354,7 +364,7 @@ export class Pilot implements Agent {
354
364
  iteration's work, but server-side side effects (records created, forms submitted) persist.
355
365
  Unnecessary resets create duplicate data and infinite loops.
356
366
 
357
- ${this.buildSharedEvidenceRules(task)}
367
+ ${this.buildSharedEvidenceRules()}
358
368
 
359
369
  DECISION:
360
370
  - "allow": current page cannot host the scenario, irrecoverable error, or no path back.
@@ -371,17 +381,19 @@ export class Pilot implements Agent {
371
381
  to verify, how to record. Do not suggest repeating actions that already succeeded.
372
382
  If progress is blocked only because the page lacks target data for the scenario, prefer precondition()
373
383
  over repeated UI attempts.
384
+
385
+ ${this.buildTaskContext(task)}
374
386
  `;
375
387
  }
376
388
 
377
- private buildVerdictSystemPrompt(type: string, task: Test): string {
389
+ private buildVerdictSystemPrompt(task: Test): string {
378
390
  return dedent`
379
- You are Pilot — final decision maker for test pass/fail. Tester requested ${type}. Review the
380
- evidence and commit to a verdict; "continue" only when evidence is genuinely insufficient.
391
+ You are Pilot — final decision maker for test pass/fail. Review the evidence and commit to a
392
+ verdict; "continue" only when evidence is genuinely insufficient.
381
393
 
382
394
  ${capabilityGroundingRule}
383
395
 
384
- ${this.buildSharedEvidenceRules(task)}
396
+ ${this.buildSharedEvidenceRules()}
385
397
 
386
398
  DECISION:
387
399
  - "pass": scenario goal is fully accomplished. Set requestVerification to a one-sentence claim about
@@ -400,6 +412,8 @@ export class Pilot implements Agent {
400
412
  reason field: one short sentence, maximum 120 characters. Do NOT restate the decision
401
413
  ("scenario goal achieved/not achieved"). State what happened: what was verified, what failed,
402
414
  or what evidence was found.
415
+
416
+ ${this.buildTaskContext(task)}
403
417
  `;
404
418
  }
405
419
 
@@ -429,10 +443,6 @@ export class Pilot implements Agent {
429
443
 
430
444
  FIRST: Decide if precondition() is needed.
431
445
 
432
- ${capabilityGroundingRule}
433
-
434
- ${dataProtectionRules}
435
-
436
446
  Call precondition() WHEN:
437
447
  - The scenario edits/deletes/modifies an item, and you want a DISPOSABLE item to act on safely
438
448
  - The scenario needs specific data clearly NOT on the current page (e.g., items with specific statuses for filtering)
@@ -481,9 +491,6 @@ export class Pilot implements Agent {
481
491
  .slice(-this.stepsToReview);
482
492
  const actionsContext = this.formatActions(toolCalls);
483
493
 
484
- this.conversation.cleanupTag('page_summary', '...trimmed...', 1);
485
- this.conversation.cleanupTag('recent_actions', '...trimmed...', 2);
486
-
487
494
  return this.sendToPilot(
488
495
  dedent`
489
496
  Navigated to new page.
@@ -505,7 +512,8 @@ export class Pilot implements Agent {
505
512
 
506
513
  First: evaluate whether this navigation makes sense for the scenario goal. If the page is unrelated, instruct Tester to back() or reset(). Then plan next steps.
507
514
  `,
508
- 'pilot.reviewNewPage'
515
+ 'pilot.reviewNewPage',
516
+ { task }
509
517
  );
510
518
  }
511
519
 
@@ -522,8 +530,6 @@ export class Pilot implements Agent {
522
530
  const actionsContext = this.formatActions(toolCalls);
523
531
  const stateContext = this.buildStateContext(currentState);
524
532
 
525
- this.conversation.cleanupTag('recent_actions', '...trimmed...', 2);
526
-
527
533
  const hasFailures = toolCalls.length === 0 || toolCalls.some((t) => !t.wasSuccessful);
528
534
 
529
535
  const text = await this.sendToPilot(
@@ -561,7 +567,7 @@ export class Pilot implements Agent {
561
567
  return `CHECKED: ${checked.length > 0 ? checked.join(', ') : 'none'}\nREMAINING: ${remaining.length > 0 ? remaining.join(', ') : 'none'}`;
562
568
  }
563
569
 
564
- private async sendToPilot(userText: string, functionId: string, opts: { tools?: boolean; maxToolRoundtrips?: number; task?: Test } = {}): Promise<string> {
570
+ private async sendToPilot(userText: string, functionId: string, opts: { tools?: boolean; maxToolRoundtrips?: number; task: Test }): Promise<string> {
565
571
  debugLog(`sendToPilot: ${functionId}, tools: ${!!opts.tools}, roundtrips: ${opts.maxToolRoundtrips ?? 0}`);
566
572
 
567
573
  let finalUserText = userText;
@@ -572,19 +578,14 @@ export class Pilot implements Agent {
572
578
  }
573
579
  }
574
580
  this.conversation!.addUserText(finalUserText);
575
- let tools: any;
576
- if (opts.tools) {
577
- tools = this.pickPlanningTools();
578
- }
579
581
 
580
- if (opts.tools && opts.task) {
581
- tools = { ...tools, ...this.buildPreconditionTool(opts.task) };
582
- }
582
+ const tools = { ...this.pickPlanningTools(), ...this.buildPreconditionTool(opts.task) };
583
583
 
584
584
  const result = await this.provider.invokeConversation(this.conversation!, tools, {
585
585
  maxToolRoundtrips: opts.maxToolRoundtrips ?? 0,
586
+ toolChoice: opts.tools ? 'auto' : 'none',
586
587
  agentName: 'pilot',
587
- stopWhen: opts.task ? () => opts.task!.hasFinished : undefined,
588
+ stopWhen: () => opts.task.hasFinished,
588
589
  telemetry: { functionId },
589
590
  });
590
591
  const text = result?.response?.text || '';
@@ -603,12 +604,9 @@ export class Pilot implements Agent {
603
604
  }
604
605
 
605
606
  private getExperienceToc(): string {
606
- if (!this.experienceTracker) return '';
607
- const state = this.explorer.getStateManager().getCurrentState();
607
+ const state = this.stateManager.getCurrentState();
608
608
  if (!state) return '';
609
- const actionResult = ActionResult.fromState(state);
610
- const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
611
- return renderExperienceToc(toc);
609
+ return this.stateManager.getExperienceTracker().renderExperienceTocFor(ActionResult.fromState(state));
612
610
  }
613
611
 
614
612
  private pickPlanningTools() {
@@ -671,7 +669,7 @@ export class Pilot implements Agent {
671
669
  private async checkDataAvailability(task: Test, requestedData: string, fishermanReason: string | undefined): Promise<string | null> {
672
670
  if (!this.provider.hasVision()) return null;
673
671
 
674
- const screenshotState = await this.explorer.capturePageWithScreenshot().catch(() => null);
672
+ const screenshotState = await this.explorer.capture({ screenshot: true }).catch(() => null);
675
673
  if (!screenshotState?.screenshot) return null;
676
674
 
677
675
  const question = dedent`
@@ -723,8 +721,8 @@ export class Pilot implements Agent {
723
721
  lines.push('modal: none');
724
722
  }
725
723
 
726
- if (this.explorer.hasOtherTabs()) {
727
- const tabs = this.explorer.getOtherTabsInfo();
724
+ const tabs = this.stateManager.otherTabs;
725
+ if (tabs.length > 0) {
728
726
  lines.push(`other tabs: ${tabs.length} (${tabs.map((t) => `${t.url} - ${t.title}`).join(', ')})`);
729
727
  } else {
730
728
  lines.push('other tabs: none');
@@ -747,7 +745,7 @@ export class Pilot implements Agent {
747
745
  lines.push('console errors: none');
748
746
  }
749
747
 
750
- const failedRequests = this.explorer.getRequestStore()?.getFailedRequests() ?? [];
748
+ const failedRequests = this.requestStore.getFailedRequests();
751
749
  if (failedRequests.length > 0) {
752
750
  const sample = failedRequests
753
751
  .slice(-5)
@@ -828,7 +826,7 @@ export class Pilot implements Agent {
828
826
 
829
827
  private formatSessionLog(testerConversation: Conversation): string {
830
828
  const executions = testerConversation.getToolExecutions().filter((t) => !META_TOOLS.includes(t.toolName));
831
- const stateHistory = this.explorer.getStateManager().getStateHistory();
829
+ const stateHistory = this.stateManager.getStateHistory();
832
830
 
833
831
  const initialUrl = stateHistory[0]?.toState?.url || '';
834
832
  let currentUrl = initialUrl;
@@ -985,16 +983,6 @@ export class Pilot implements Agent {
985
983
  return dedent`
986
984
  You are Pilot — a supervisor that detects problems and intervenes only when needed.
987
985
 
988
- SCENARIO: ${task.scenario}
989
- START URL: ${initialState.url}
990
- PAGE: ${initialState.title || ''} | ${initialState.h1 || ''}
991
-
992
- EXPECTED RESULTS:
993
- ${task.expected.map((e) => `- ${e}`).join('\n')}
994
-
995
- PLANNED STEPS:
996
- ${stepsText}
997
-
998
986
  Your job: plan, review new pages, detect stuck patterns, suggest concrete next steps. Track which
999
987
  expectations are checked. When things go well, encourage briefly and let Tester continue. The current
1000
988
  page is usually richer than the page summary lists — prefer exploring it before navigating away.
@@ -1056,14 +1044,24 @@ export class Pilot implements Agent {
1056
1044
 
1057
1045
  ${dataProtectionRules}
1058
1046
 
1059
- Describe WHAT to create, not what exists. RIGHT: precondition("1 test"). WRONG:
1060
- precondition("1 test suite named Updated Suite with existing tests"). Keep descriptions short.
1047
+ Describe WHAT to create, not what exists. RIGHT: precondition("1 post"). WRONG:
1048
+ precondition("1 post named Updated Post with existing comments"). Keep descriptions short.
1061
1049
 
1062
1050
  Response format:
1063
1051
  PROGRESS: <1 sentence assessment>
1064
1052
  NEXT: <specific actionable instruction for Tester>
1065
1053
 
1066
1054
  Keep user-facing reasons concise: one short sentence, maximum 120 characters, evidence only, no repeated verdict wording.
1055
+
1056
+ SCENARIO: ${task.scenario}
1057
+ START URL: ${initialState.url}
1058
+ PAGE: ${initialState.title || ''} | ${initialState.h1 || ''}
1059
+
1060
+ EXPECTED RESULTS:
1061
+ ${task.expected.map((e) => `- ${e}`).join('\n')}
1062
+
1063
+ PLANNED STEPS:
1064
+ ${stepsText}
1067
1065
  `;
1068
1066
  }
1069
1067
  }