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,6 @@ import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from "../action-result.js";
5
5
  import { ConfigParser } from "../config.js";
6
- import { renderExperienceToc } from "../experience-tracker.js";
7
6
  import { TestResult } from "../test-plan.js";
8
7
  import { collectInteractiveNodes, detectFocusArea, extractFocusedElement } from "../utils/aria.js";
9
8
  import { ErrorPageError } from "../utils/error-page.js";
@@ -21,14 +20,18 @@ export class Pilot {
21
20
  conversation = null;
22
21
  researcher;
23
22
  explorer;
23
+ stateManager;
24
+ requestStore;
25
+ playwrightRecorder;
24
26
  fisherman = null;
25
- experienceTracker;
26
- constructor(provider, agentTools, researcher, explorer, experienceTracker) {
27
- this.provider = provider;
27
+ constructor(deps, agentTools, researcher) {
28
+ this.provider = deps.ai;
28
29
  this.agentTools = agentTools;
29
30
  this.researcher = researcher;
30
- this.explorer = explorer;
31
- this.experienceTracker = experienceTracker || null;
31
+ this.explorer = deps.explorer;
32
+ this.stateManager = deps.stateManager;
33
+ this.requestStore = deps.requestStore;
34
+ this.playwrightRecorder = deps.playwrightRecorder;
32
35
  }
33
36
  setFisherman(fisherman) {
34
37
  this.fisherman = fisherman;
@@ -74,7 +77,7 @@ export class Pilot {
74
77
  let screenshotState = null;
75
78
  if (type === 'finish' && this.provider.hasVision()) {
76
79
  try {
77
- screenshotState = await this.explorer.capturePageWithScreenshot();
80
+ screenshotState = await this.explorer.capture({ screenshot: true });
78
81
  if (screenshotState.screenshot) {
79
82
  visualAnalysis = (await this.researcher.answerQuestionAboutScreenshot(screenshotState, `Describe current page state relevant to: ${task.scenario}`)) || '';
80
83
  }
@@ -90,7 +93,7 @@ export class Pilot {
90
93
  requestVerification: z
91
94
  .string()
92
95
  .nullable()
93
- .describe('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".'),
96
+ .describe('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".'),
94
97
  });
95
98
  const userContent = dedent `
96
99
  Tester wants to ${type} the test.
@@ -124,7 +127,7 @@ export class Pilot {
124
127
  - Mixed evidence + final state shows success → pass. Mixed + final state unclear → continue with guidance.
125
128
 
126
129
  When deciding "pass", you MUST also set requestVerification to a one-sentence natural-language
127
- claim about the current page (e.g., "New test suite Foo is visible in the suites list"). NOT
130
+ claim about the current page (e.g., "New item Foo is visible in the items list"). NOT
128
131
  code — do not write I.*, expect(), .then(), or any JavaScript. Choose the strongest single
129
132
  piece of evidence (a unique element/text that exists ONLY because the scenario succeeded).
130
133
  Navigator translates the claim into CodeceptJS assertions; without it the generated test has
@@ -133,7 +136,7 @@ export class Pilot {
133
136
  const messages = [
134
137
  {
135
138
  role: 'system',
136
- content: this.buildVerdictSystemPrompt(type, task),
139
+ content: this.buildVerdictSystemPrompt(task),
137
140
  },
138
141
  { role: 'user', content: userContent },
139
142
  ];
@@ -151,7 +154,7 @@ export class Pilot {
151
154
  tag('substep').log(`Pilot requesting verification: ${result.requestVerification}`);
152
155
  const verifyResult = await navigator.verifyState(result.requestVerification, currentState).catch(() => null);
153
156
  if (verifyResult?.verified && verifyResult.assertionSteps?.length) {
154
- this.explorer.getPlaywrightRecorder().recordVerification(verifyResult.assertionSteps);
157
+ this.playwrightRecorder.recordVerification(verifyResult.assertionSteps);
155
158
  }
156
159
  }
157
160
  tag('info').log(`Pilot: ${result.decision} - ${result.reason}`);
@@ -261,10 +264,8 @@ export class Pilot {
261
264
  return true;
262
265
  }
263
266
  }
264
- buildSharedEvidenceRules(task) {
267
+ buildSharedEvidenceRules() {
265
268
  return dedent `
266
- SCENARIO: ${task.scenario}
267
-
268
269
  EVIDENCE PRIORITY (strict):
269
270
  1) Final observable state proving the scenario goal
270
271
  2) verify()/see() results in the LAST few actions before stop/finish
@@ -297,6 +298,11 @@ export class Pilot {
297
298
 
298
299
  Expected results are MILESTONES, not the goal. Never fail because a milestone (toast, icon, styling)
299
300
  didn't match if the scenario goal IS accomplished.
301
+ `;
302
+ }
303
+ buildTaskContext(task) {
304
+ return dedent `
305
+ SCENARIO: ${task.scenario}
300
306
 
301
307
  ${this.buildDeletionScope(task)}
302
308
 
@@ -310,7 +316,7 @@ export class Pilot {
310
316
  iteration's work, but server-side side effects (records created, forms submitted) persist.
311
317
  Unnecessary resets create duplicate data and infinite loops.
312
318
 
313
- ${this.buildSharedEvidenceRules(task)}
319
+ ${this.buildSharedEvidenceRules()}
314
320
 
315
321
  DECISION:
316
322
  - "allow": current page cannot host the scenario, irrecoverable error, or no path back.
@@ -327,16 +333,18 @@ export class Pilot {
327
333
  to verify, how to record. Do not suggest repeating actions that already succeeded.
328
334
  If progress is blocked only because the page lacks target data for the scenario, prefer precondition()
329
335
  over repeated UI attempts.
336
+
337
+ ${this.buildTaskContext(task)}
330
338
  `;
331
339
  }
332
- buildVerdictSystemPrompt(type, task) {
340
+ buildVerdictSystemPrompt(task) {
333
341
  return dedent `
334
- You are Pilot — final decision maker for test pass/fail. Tester requested ${type}. Review the
335
- evidence and commit to a verdict; "continue" only when evidence is genuinely insufficient.
342
+ You are Pilot — final decision maker for test pass/fail. Review the evidence and commit to a
343
+ verdict; "continue" only when evidence is genuinely insufficient.
336
344
 
337
345
  ${capabilityGroundingRule}
338
346
 
339
- ${this.buildSharedEvidenceRules(task)}
347
+ ${this.buildSharedEvidenceRules()}
340
348
 
341
349
  DECISION:
342
350
  - "pass": scenario goal is fully accomplished. Set requestVerification to a one-sentence claim about
@@ -355,6 +363,8 @@ export class Pilot {
355
363
  reason field: one short sentence, maximum 120 characters. Do NOT restate the decision
356
364
  ("scenario goal achieved/not achieved"). State what happened: what was verified, what failed,
357
365
  or what evidence was found.
366
+
367
+ ${this.buildTaskContext(task)}
358
368
  `;
359
369
  }
360
370
  async planTest(task, currentState) {
@@ -379,10 +389,6 @@ export class Pilot {
379
389
 
380
390
  FIRST: Decide if precondition() is needed.
381
391
 
382
- ${capabilityGroundingRule}
383
-
384
- ${dataProtectionRules}
385
-
386
392
  Call precondition() WHEN:
387
393
  - The scenario edits/deletes/modifies an item, and you want a DISPOSABLE item to act on safely
388
394
  - The scenario needs specific data clearly NOT on the current page (e.g., items with specific statuses for filtering)
@@ -425,8 +431,6 @@ export class Pilot {
425
431
  .filter((t) => t.wasSuccessful)
426
432
  .slice(-this.stepsToReview);
427
433
  const actionsContext = this.formatActions(toolCalls);
428
- this.conversation.cleanupTag('page_summary', '...trimmed...', 1);
429
- this.conversation.cleanupTag('recent_actions', '...trimmed...', 2);
430
434
  return this.sendToPilot(dedent `
431
435
  Navigated to new page.
432
436
  START URL: ${task.startUrl}
@@ -446,7 +450,7 @@ export class Pilot {
446
450
  ${this.formatExpectations(task)}
447
451
 
448
452
  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.
449
- `, 'pilot.reviewNewPage');
453
+ `, 'pilot.reviewNewPage', { task });
450
454
  }
451
455
  async analyzeProgress(task, currentState, testerConversation) {
452
456
  tag('substep').log('Pilot analyzing progress...');
@@ -458,7 +462,6 @@ export class Pilot {
458
462
  const toolCalls = testerConversation.getToolExecutions().slice(-this.stepsToReview);
459
463
  const actionsContext = this.formatActions(toolCalls);
460
464
  const stateContext = this.buildStateContext(currentState);
461
- this.conversation.cleanupTag('recent_actions', '...trimmed...', 2);
462
465
  const hasFailures = toolCalls.length === 0 || toolCalls.some((t) => !t.wasSuccessful);
463
466
  const text = await this.sendToPilot(dedent `
464
467
  START URL: ${task.startUrl}
@@ -486,7 +489,7 @@ export class Pilot {
486
489
  const remaining = task.getRemainingExpectations();
487
490
  return `CHECKED: ${checked.length > 0 ? checked.join(', ') : 'none'}\nREMAINING: ${remaining.length > 0 ? remaining.join(', ') : 'none'}`;
488
491
  }
489
- async sendToPilot(userText, functionId, opts = {}) {
492
+ async sendToPilot(userText, functionId, opts) {
490
493
  debugLog(`sendToPilot: ${functionId}, tools: ${!!opts.tools}, roundtrips: ${opts.maxToolRoundtrips ?? 0}`);
491
494
  let finalUserText = userText;
492
495
  if (opts.tools) {
@@ -496,17 +499,12 @@ export class Pilot {
496
499
  }
497
500
  }
498
501
  this.conversation.addUserText(finalUserText);
499
- let tools;
500
- if (opts.tools) {
501
- tools = this.pickPlanningTools();
502
- }
503
- if (opts.tools && opts.task) {
504
- tools = { ...tools, ...this.buildPreconditionTool(opts.task) };
505
- }
502
+ const tools = { ...this.pickPlanningTools(), ...this.buildPreconditionTool(opts.task) };
506
503
  const result = await this.provider.invokeConversation(this.conversation, tools, {
507
504
  maxToolRoundtrips: opts.maxToolRoundtrips ?? 0,
505
+ toolChoice: opts.tools ? 'auto' : 'none',
508
506
  agentName: 'pilot',
509
- stopWhen: opts.task ? () => opts.task.hasFinished : undefined,
507
+ stopWhen: () => opts.task.hasFinished,
510
508
  telemetry: { functionId },
511
509
  });
512
510
  const text = result?.response?.text || '';
@@ -525,14 +523,10 @@ export class Pilot {
525
523
  `;
526
524
  }
527
525
  getExperienceToc() {
528
- if (!this.experienceTracker)
529
- return '';
530
- const state = this.explorer.getStateManager().getCurrentState();
526
+ const state = this.stateManager.getCurrentState();
531
527
  if (!state)
532
528
  return '';
533
- const actionResult = ActionResult.fromState(state);
534
- const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
535
- return renderExperienceToc(toc);
529
+ return this.stateManager.getExperienceTracker().renderExperienceTocFor(ActionResult.fromState(state));
536
530
  }
537
531
  pickPlanningTools() {
538
532
  const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser } = this.agentTools ?? {};
@@ -600,7 +594,7 @@ export class Pilot {
600
594
  async checkDataAvailability(task, requestedData, fishermanReason) {
601
595
  if (!this.provider.hasVision())
602
596
  return null;
603
- const screenshotState = await this.explorer.capturePageWithScreenshot().catch(() => null);
597
+ const screenshotState = await this.explorer.capture({ screenshot: true }).catch(() => null);
604
598
  if (!screenshotState?.screenshot)
605
599
  return null;
606
600
  const question = dedent `
@@ -646,8 +640,8 @@ export class Pilot {
646
640
  else {
647
641
  lines.push('modal: none');
648
642
  }
649
- if (this.explorer.hasOtherTabs()) {
650
- const tabs = this.explorer.getOtherTabsInfo();
643
+ const tabs = this.stateManager.otherTabs;
644
+ if (tabs.length > 0) {
651
645
  lines.push(`other tabs: ${tabs.length} (${tabs.map((t) => `${t.url} - ${t.title}`).join(', ')})`);
652
646
  }
653
647
  else {
@@ -669,7 +663,7 @@ export class Pilot {
669
663
  else {
670
664
  lines.push('console errors: none');
671
665
  }
672
- const failedRequests = this.explorer.getRequestStore()?.getFailedRequests() ?? [];
666
+ const failedRequests = this.requestStore.getFailedRequests();
673
667
  if (failedRequests.length > 0) {
674
668
  const sample = failedRequests
675
669
  .slice(-5)
@@ -744,7 +738,7 @@ export class Pilot {
744
738
  }
745
739
  formatSessionLog(testerConversation) {
746
740
  const executions = testerConversation.getToolExecutions().filter((t) => !META_TOOLS.includes(t.toolName));
747
- const stateHistory = this.explorer.getStateManager().getStateHistory();
741
+ const stateHistory = this.stateManager.getStateHistory();
748
742
  const initialUrl = stateHistory[0]?.toState?.url || '';
749
743
  let currentUrl = initialUrl;
750
744
  const groups = new Map();
@@ -886,16 +880,6 @@ export class Pilot {
886
880
  return dedent `
887
881
  You are Pilot — a supervisor that detects problems and intervenes only when needed.
888
882
 
889
- SCENARIO: ${task.scenario}
890
- START URL: ${initialState.url}
891
- PAGE: ${initialState.title || ''} | ${initialState.h1 || ''}
892
-
893
- EXPECTED RESULTS:
894
- ${task.expected.map((e) => `- ${e}`).join('\n')}
895
-
896
- PLANNED STEPS:
897
- ${stepsText}
898
-
899
883
  Your job: plan, review new pages, detect stuck patterns, suggest concrete next steps. Track which
900
884
  expectations are checked. When things go well, encourage briefly and let Tester continue. The current
901
885
  page is usually richer than the page summary lists — prefer exploring it before navigating away.
@@ -957,14 +941,24 @@ export class Pilot {
957
941
 
958
942
  ${dataProtectionRules}
959
943
 
960
- Describe WHAT to create, not what exists. RIGHT: precondition("1 test"). WRONG:
961
- precondition("1 test suite named Updated Suite with existing tests"). Keep descriptions short.
944
+ Describe WHAT to create, not what exists. RIGHT: precondition("1 post"). WRONG:
945
+ precondition("1 post named Updated Post with existing comments"). Keep descriptions short.
962
946
 
963
947
  Response format:
964
948
  PROGRESS: <1 sentence assessment>
965
949
  NEXT: <specific actionable instruction for Tester>
966
950
 
967
951
  Keep user-facing reasons concise: one short sentence, maximum 120 characters, evidence only, no repeated verdict wording.
952
+
953
+ SCENARIO: ${task.scenario}
954
+ START URL: ${initialState.url}
955
+ PAGE: ${initialState.title || ''} | ${initialState.h1 || ''}
956
+
957
+ EXPECTED RESULTS:
958
+ ${task.expected.map((e) => `- ${e}`).join('\n')}
959
+
960
+ PLANNED STEPS:
961
+ ${stepsText}
968
962
  `;
969
963
  }
970
964
  }
@@ -1,14 +1,14 @@
1
1
  import { ActionResult } from '../action-result.js';
2
- import { ExperienceTracker } from '../experience-tracker.js';
2
+ import type { ExperienceTracker } from '../experience-tracker.js';
3
3
  import type Explorer from '../explorer.js';
4
4
  import type { StateManager } from '../state-manager.js';
5
5
  import { Suite } from '../suite.js';
6
6
  import { Plan, Test } from '../test-plan.js';
7
- import type { Agent } from './agent.js';
7
+ import type { Agent, AgentDeps } from './agent.js';
8
8
  import { Conversation } from './conversation.js';
9
9
  import type { Fisherman } from './fisherman.js';
10
10
  import type { Provider } from './provider.js';
11
- import { Researcher } from './researcher.js';
11
+ import { type Researcher } from './researcher.js';
12
12
  declare const PlannerBase: {
13
13
  new (...args: any[]): {
14
14
  currentPlan: Plan | null;
@@ -58,7 +58,7 @@ export declare class Planner extends PlannerBase implements Agent {
58
58
  lastSuite: Suite | null;
59
59
  researcher: Researcher;
60
60
  fisherman: Fisherman | null;
61
- constructor(explorer: Explorer, provider: Provider);
61
+ constructor(deps: AgentDeps, researcher: Researcher);
62
62
  setFisherman(fisherman: Fisherman): void;
63
63
  get sectionOrder(): string[];
64
64
  getDefaultStartUrl(state: {
@@ -75,5 +75,6 @@ export declare class Planner extends PlannerBase implements Agent {
75
75
  cleanExperienceFlows(text: string): string | null;
76
76
  buildApproach(style?: string): string;
77
77
  buildConversation(state: ActionResult, style?: string, parentPlan?: Plan, feature?: string): Promise<Conversation>;
78
+ getTasksMessage(): string;
78
79
  }
79
80
  export {};
@@ -3,7 +3,6 @@ import { z } from 'zod';
3
3
  import { ActionResult } from "../action-result.js";
4
4
  import { setActivity } from "../activity.js";
5
5
  import { ConfigParser } from "../config.js";
6
- import { ExperienceTracker } from "../experience-tracker.js";
7
6
  import { Observability } from "../observability.js";
8
7
  import { Stats } from "../stats.js";
9
8
  import { Suite } from "../suite.js";
@@ -16,7 +15,7 @@ import { Conversation } from "./conversation.js";
16
15
  import { WithSessionDedup } from "./planner/session-dedup.js";
17
16
  import { getActiveStyle } from "./planner/styles.js";
18
17
  import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from "./planner/subpages.js";
19
- import { POSSIBLE_SECTIONS, Researcher } from "./researcher.js";
18
+ import { POSSIBLE_SECTIONS } from "./researcher.js";
20
19
  import { findSimilarStateHash } from "./researcher/cache.js";
21
20
  import { hasFocusedSection } from "./researcher/focus.js";
22
21
  import { capabilityGroundingRule, dataProtectionRules, fileUploadRule } from "./rules.js";
@@ -50,13 +49,13 @@ export class Planner extends PlannerBase {
50
49
  lastSuite = null;
51
50
  researcher;
52
51
  fisherman = null;
53
- constructor(explorer, provider) {
52
+ constructor(deps, researcher) {
54
53
  super();
55
- this.explorer = explorer;
56
- this.provider = provider;
57
- this.researcher = new Researcher(explorer, provider);
58
- this.stateManager = explorer.getStateManager();
59
- this.experienceTracker = new ExperienceTracker();
54
+ this.explorer = deps.explorer;
55
+ this.provider = deps.ai;
56
+ this.researcher = researcher;
57
+ this.stateManager = deps.stateManager;
58
+ this.experienceTracker = deps.stateManager.getExperienceTracker();
60
59
  }
61
60
  setFisherman(fisherman) {
62
61
  this.fisherman = fisherman;
@@ -81,7 +80,7 @@ export class Planner extends PlannerBase {
81
80
  You are ISTQB certified senior manual QA planning exploratory testing session of a web application.
82
81
  Each test scenario must complete a meaningful user workflow — not just open a UI element and verify it exists.
83
82
  Bad: "Open the Help panel" — just opens something.
84
- Good: "Create a new suite and verify it appears in the list" — completes a business action.
83
+ Good: "Create a new post and verify it appears in the list" — completes a business action.
85
84
  </role>
86
85
  <task>
87
86
  List possible testing scenarios for the web page.
@@ -149,6 +148,7 @@ export class Planner extends PlannerBase {
149
148
  const tags = ['planner'];
150
149
  if (style)
151
150
  tags.push(style);
151
+ const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
152
152
  const result = await Observability.run(`planner: ${state.url}`, { tags, sessionId: state.url }, async () => {
153
153
  const actionResult = ActionResult.fromState(state);
154
154
  const conversation = await this.buildConversation(actionResult, style, parentPlan, feature);
@@ -182,7 +182,6 @@ export class Planner extends PlannerBase {
182
182
  if (parentPlan)
183
183
  this.currentPlan.parentPlan = parentPlan;
184
184
  const allPreviousScenarios = this.getPreviousSessionScenarios();
185
- const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
186
185
  for (const s of existingTestScenarios)
187
186
  allPreviousScenarios.add(s);
188
187
  for (const t of tests) {
@@ -232,6 +231,7 @@ export class Planner extends PlannerBase {
232
231
  return added;
233
232
  }
234
233
  getExistingTestFileScenarios(currentUrl) {
234
+ this.lastSuite = null;
235
235
  if (!currentUrl)
236
236
  return new Set();
237
237
  try {
@@ -244,27 +244,37 @@ export class Planner extends PlannerBase {
244
244
  }
245
245
  }
246
246
  cleanExperienceFlows(text) {
247
- const seenTitles = new Set();
248
247
  let result = text;
249
- for (const section of [...mdq(text).query('section2').each(), ...mdq(text).query('section3').each()]) {
250
- const heading = section.query('heading').text().trim();
251
- const body = mdq(section.text())
252
- .query('heading')
253
- .replace('')
254
- .replace(/^---\s*$/gm, '')
255
- .trim();
256
- if (!body || seenTitles.has(heading)) {
257
- result = result.replace(section.text(), '');
258
- continue;
259
- }
260
- seenTitles.add(heading);
261
- const blockquotes = section.query('blockquote').each();
262
- if (blockquotes.length <= 10)
263
- continue;
264
- for (const bq of blockquotes.slice(10)) {
265
- result = result.replace(bq.text(), '');
266
- }
267
- result = result.replace(section.text().trim(), `${section.text().trim()}\n> ... and ${blockquotes.length - 10} more discoveries`);
248
+ const seenTitles = new Set();
249
+ for (const selector of ['section2', 'section3']) {
250
+ result = mdq(result)
251
+ .query(selector)
252
+ .replaceEach((section) => {
253
+ const heading = section.query('heading').text().trim();
254
+ const withoutHeadings = mdq(section.text()).query('heading').replace('');
255
+ const body = mdq(withoutHeadings).query('hr').replace('').trim();
256
+ if (body && !seenTitles.has(heading)) {
257
+ seenTitles.add(heading);
258
+ return section.text();
259
+ }
260
+ return '';
261
+ });
262
+ }
263
+ const trimmedTitles = new Set();
264
+ for (const selector of ['section2', 'section3']) {
265
+ result = mdq(result)
266
+ .query(selector)
267
+ .replaceEach((section) => {
268
+ const heading = section.query('heading').text().trim();
269
+ if (trimmedTitles.has(heading))
270
+ return section.text();
271
+ const count = section.query('blockquote').count();
272
+ if (count <= 10)
273
+ return section.text();
274
+ const kept = mdq(section.text()).query('blockquote[10:]').replace('');
275
+ trimmedTitles.add(heading);
276
+ return `${kept.trimEnd()}\n> ... and ${count - 10} more discoveries\n`;
277
+ });
268
278
  }
269
279
  return result.trim() || null;
270
280
  }
@@ -298,6 +308,7 @@ export class Planner extends PlannerBase {
298
308
  If there are subpages (pages with same URL path) plan testing of those subpages as well
299
309
  If you plan to test CRUD operations, plan them in correct order: create, read, update.
300
310
  Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
311
+ When validation placement or wording was not observed, require feedback associated with the invalid input without inventing a specific location or message.
301
312
  If exact wording is unknown, describe the expected result generically, for example "an authentication error is shown" or "the user stays on the login page" instead of guessing the literal text.
302
313
  If exact redirect destination is unknown, describe the destination by visible page identity, for example "the dashboard page opens" or "the current workspace home page opens" instead of inventing a URL slug.
303
314
  Only propose scenarios whose prerequisites are evident from page research, visited pages, or API data preparation context.
@@ -323,31 +334,26 @@ export class Planner extends PlannerBase {
323
334
  ${dataProtectionRules}
324
335
  ${fileUploadRule}
325
336
  </rules>
326
-
327
- ${this.buildApproach(style)}
328
-
329
- <context>
330
- URL: ${state.url || 'Unknown'}
331
- Title: ${state.title || 'Unknown'}
332
- </context>
333
337
  `;
334
338
  conversation.addUserText(planningPrompt);
339
+ conversation.addUserText(this.getTasksMessage());
335
340
  const currentState = this.stateManager.getCurrentState();
336
341
  const research = await this.researcher.research(currentState || state, {
337
342
  deep: true,
338
343
  });
339
344
  let plannerResearch = mdq(research).query('code').replace('');
340
- for (const table of mdq(plannerResearch).query('table').each()) {
341
- const rawTable = table.text();
345
+ plannerResearch = mdq(plannerResearch)
346
+ .query('table')
347
+ .replaceEach((table) => {
342
348
  const rows = table.toJson();
343
349
  if (rows.length === 0 || !rows[0].Element)
344
- continue;
350
+ return table.text();
345
351
  const elementWithType = rows.map((r) => ({
346
352
  Element: r.Element,
347
353
  Type: r.Type || '',
348
354
  }));
349
- plannerResearch = plannerResearch.replace(rawTable, jsonToTable(elementWithType, ['Element', 'Type']));
350
- }
355
+ return jsonToTable(elementWithType, ['Element', 'Type']);
356
+ });
351
357
  const hasFocusedOverlay = hasFocusedSection(plannerResearch);
352
358
  const focusNote = hasFocusedOverlay ? "IMPORTANT: One section is marked as **Focused** — this is the user's current focus area. Concentrate testing on the Focused section FIRST — test all interactions inside it before planning tests for the rest of the page." : '';
353
359
  const featureFilter = feature ? `FOCUS FILTER: Only propose scenarios using elements relevant to "${feature}". Ignore all other elements.` : '';
@@ -361,6 +367,14 @@ export class Planner extends PlannerBase {
361
367
 
362
368
  ${plannerResearch}
363
369
  </page_research>
370
+ `);
371
+ conversation.addUserText(dedent `
372
+ ${this.buildApproach(style)}
373
+
374
+ <context>
375
+ URL: ${state.url || 'Unknown'}
376
+ Title: ${state.title || 'Unknown'}
377
+ </context>
364
378
  `);
365
379
  if (this.fisherman) {
366
380
  await this.fisherman.ensureReady(state.url);
@@ -505,8 +519,10 @@ export class Planner extends PlannerBase {
505
519
  </parent_page_context>
506
520
  `);
507
521
  }
508
- const hasCurrentPlan = !!this.currentPlan;
509
- const tasksMessage = dedent `
522
+ return conversation;
523
+ }
524
+ getTasksMessage() {
525
+ return dedent `
510
526
  <task>
511
527
  Provide testing scenarios as structured data with the following requirements:
512
528
  1. Create a short, descriptive plan name that summarizes what will be tested (e.g., "User Authentication Testing", "Product Catalog Navigation", "Form Validation Tests")
@@ -525,17 +541,17 @@ export class Planner extends PlannerBase {
525
541
 
526
542
  STEPS - actionable commands:
527
543
  - Each step should be a specific action to perform
528
- - Good: "Click Create Suite button", "Enter 'My New Suite' as suite name", "Click Save button"
529
- - Good: "Click Star icon on 'Template API Testing' suite"
544
+ - Good: "Click Create Post button", "Enter 'My New Post' as post title", "Click Save button"
545
+ - Good: "Click Star icon on 'Quarterly Report' item"
530
546
  - Bad: "Open the dropdown menu" (opening is not a goal, it's a means)
531
547
  - Bad: "Verify modal appears" (verification belongs in expected outcomes, not steps)
532
548
  - Steps should form a complete workflow, not stop at opening a UI element
533
549
  - NEVER split a workflow across two tests. One test = one complete action + its verification
534
550
 
535
551
  EXPECTED OUTCOMES - verifiable results:
536
- - Good: "New suite 'My New Suite' appears in the suite list"
537
- - Good: "Suite appears under Starred filter tab"
538
- - Good: "Success message 'Suite created' is displayed"
552
+ - Good: "New post 'My New Post' appears in the posts list"
553
+ - Good: "Item appears under Starred filter tab"
554
+ - Good: "Success message 'Post created' is displayed"
539
555
  - Good when wording is unknown: "An authentication error is displayed"
540
556
  - Good when route is unknown: "The workspace home page is displayed"
541
557
  - Bad: "Modal is displayed" (just verifying existence, no business value)
@@ -552,10 +568,8 @@ export class Planner extends PlannerBase {
552
568
  - Do not wrap text in ** or * quotes, ( or ) brackets.
553
569
  - Avoid using emojis or special characters.
554
570
  7. Only tests that can be tested from web UI should be proposed.
555
- ${hasCurrentPlan ? '8. CRITICAL: Return ONLY NEW scenarios not in the existing tests list. Return empty array if no new tests needed.' : `8. At least ${this.MIN_TASKS} tests should be proposed.`}
571
+ 8. For a fresh plan propose at least ${this.MIN_TASKS} tests; when expanding an existing plan return ONLY new scenarios not in the tested list, and an empty array if none remain.
556
572
  </task>
557
573
  `;
558
- conversation.addUserText(tasksMessage);
559
- return conversation;
560
574
  }
561
575
  }
@@ -12,7 +12,6 @@ export declare class Provider {
12
12
  telemetryEnabled: boolean;
13
13
  otelSdk: NodeSDK | null;
14
14
  defaultRetryOptions: RetryOptions;
15
- static readonly CONTEXT_LENGTH_PATTERNS: string[];
16
15
  lastConversation: Conversation | null;
17
16
  constructor(config: AIConfig);
18
17
  getModelName(model: any): string;
@@ -26,6 +25,10 @@ export declare class Provider {
26
25
  getRetryOptions(options?: any): RetryOptions;
27
26
  mergeProviderOptions(config: Record<string, any>, agentName?: string): Record<string, any>;
28
27
  finalizeConfig(config: Record<string, any>, options: any, telemetry: any): void;
28
+ buildGenerateConfig(defaults: Record<string, any>, overrides: Record<string, any>, options: any): Record<string, any>;
29
+ recordUsage(agentName: string, modelName: string, usage: any): void;
30
+ raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number): Promise<T>;
31
+ recoverFromContextLength(error: any, messages: ModelMessage[], options: any, retry: (messages: ModelMessage[], options: any) => Promise<any>): Promise<any>;
29
32
  initLangfuse(): void;
30
33
  getTelemetry(options: any): any;
31
34
  startConversation(systemMessage: string, agentName?: string, model?: any): Conversation;