explorbot 0.3.0 โ†’ 0.3.2

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 (43) hide show
  1. package/bin/explorbot-cli.ts +3 -3
  2. package/dist/bin/explorbot-cli.js +3 -2
  3. package/dist/package.json +1 -1
  4. package/dist/src/action-result.d.ts +3 -0
  5. package/dist/src/action-result.js +3 -0
  6. package/dist/src/action.d.ts +1 -0
  7. package/dist/src/action.js +12 -1
  8. package/dist/src/ai/conversation.d.ts +1 -0
  9. package/dist/src/ai/conversation.js +3 -0
  10. package/dist/src/ai/driller.js +3 -1
  11. package/dist/src/ai/fisherman.js +1 -1
  12. package/dist/src/ai/pilot.js +14 -9
  13. package/dist/src/ai/provider.d.ts +5 -0
  14. package/dist/src/ai/provider.js +84 -19
  15. package/dist/src/ai/researcher/deep-analysis.js +2 -2
  16. package/dist/src/ai/tester.js +22 -6
  17. package/dist/src/commands/exit-command.js +1 -1
  18. package/dist/src/commands/explore-command.js +22 -17
  19. package/dist/src/config.d.ts +2 -1
  20. package/dist/src/explorbot.js +1 -0
  21. package/dist/src/state-manager.d.ts +2 -0
  22. package/dist/src/state-manager.js +3 -3
  23. package/dist/src/utils/html.d.ts +2 -1
  24. package/dist/src/utils/html.js +10 -4
  25. package/dist/src/utils/overlay.d.ts +24 -0
  26. package/dist/src/utils/overlay.js +43 -0
  27. package/package.json +1 -1
  28. package/src/action-result.ts +5 -0
  29. package/src/action.ts +15 -1
  30. package/src/ai/conversation.ts +3 -0
  31. package/src/ai/driller.ts +3 -1
  32. package/src/ai/fisherman.ts +1 -1
  33. package/src/ai/pilot.ts +15 -10
  34. package/src/ai/provider.ts +115 -43
  35. package/src/ai/researcher/deep-analysis.ts +2 -2
  36. package/src/ai/tester.ts +20 -7
  37. package/src/commands/exit-command.ts +1 -1
  38. package/src/commands/explore-command.ts +17 -14
  39. package/src/config.ts +2 -1
  40. package/src/explorbot.ts +1 -0
  41. package/src/state-manager.ts +4 -3
  42. package/src/utils/html.ts +13 -4
  43. package/src/utils/overlay.ts +51 -0
@@ -6,9 +6,9 @@ import { z } from 'zod';
6
6
  import { ActionResult } from "../action-result.js";
7
7
  import { clearActivity, setActivity } from "../activity.js";
8
8
  import { Observability } from "../observability.js";
9
+ import { normalizeUrl } from "../state-manager.js";
9
10
  import { Stats } from "../stats.js";
10
11
  import { TestResult } from "../test-plan.js";
11
- import { detectFocusArea } from "../utils/aria.js";
12
12
  import { ErrorPageError, isErrorPage } from "../utils/error-page.js";
13
13
  import { createDebug, tag } from "../utils/logger.js";
14
14
  import { loop } from "../utils/loop.js";
@@ -78,7 +78,7 @@ export class Tester extends TaskAgent {
78
78
  }
79
79
  async test(task, opts = {}) {
80
80
  Stats.tests++;
81
- const state = this.stateManager.getCurrentState();
81
+ let state = this.stateManager.getCurrentState();
82
82
  if (!state)
83
83
  throw new Error('No state found');
84
84
  setActivity(`๐Ÿงช Testing: ${task.scenario}`, 'action');
@@ -97,7 +97,23 @@ export class Tester extends TaskAgent {
97
97
  const offFailedRequest = requestStore.onFailedRequest((r) => {
98
98
  task.addObservation(`Network error: ${r.method} ${r.path} โ†’ ${r.status}`);
99
99
  });
100
- const initialState = ActionResult.fromState(state);
100
+ let initialState = ActionResult.fromState(state);
101
+ const currentUrl = state.fullUrl || state.url;
102
+ let startOnCurrentPage = opts.startOnCurrentPage;
103
+ if (isErrorPage(initialState) && !startOnCurrentPage && task.startUrl && normalizeUrl(currentUrl) !== normalizeUrl(task.startUrl)) {
104
+ debugLog(`Recovering from error page at ${currentUrl} by navigating to ${task.startUrl}`);
105
+ try {
106
+ await this.explorer.visit(task.startUrl);
107
+ state = this.stateManager.getCurrentState();
108
+ if (!state)
109
+ throw new Error('No state found after navigating to test start URL');
110
+ initialState = ActionResult.fromState(state);
111
+ startOnCurrentPage = true;
112
+ }
113
+ catch (error) {
114
+ debugLog(`Could not recover from error page: ${compactErrorMessage(error)}`);
115
+ }
116
+ }
101
117
  if (isErrorPage(initialState)) {
102
118
  task.start();
103
119
  this.testRun = await this.explorer.beginTest(task);
@@ -122,7 +138,7 @@ export class Tester extends TaskAgent {
122
138
  startUrl: task.startUrl,
123
139
  expected: task.expected,
124
140
  },
125
- }, async () => this.runTestSession(task, initialState, conversation, { offFailedRequest }, opts));
141
+ }, async () => this.runTestSession(task, initialState, conversation, { offFailedRequest }, { ...opts, startOnCurrentPage }));
126
142
  }
127
143
  async runTestSession(task, initialState, conversation, handlers, opts) {
128
144
  const { offFailedRequest } = handlers;
@@ -421,7 +437,7 @@ export class Tester extends TaskAgent {
421
437
  <rules>
422
438
  Use tools ${this.ACTION_TOOLS.join(', ')} to interact with the page.
423
439
  Fall back to interact() when those fail, when the step needs a sequence of actions, or when your context is not enough to locate the element.
424
- Use tool names exactly as listed in this prompt. Do not invent combined tool names, aliases, or names with channel markers such as "commentary".
440
+ Use tool names exactly as listed in this prompt. Do not invent combined tool names or aliases.
425
441
  Match each tool input schema exactly. Do not invent parameter names or pass extra fields.
426
442
  Do not do unsuccesful clicks again.
427
443
  Do not run same tool calls with same parameters again.
@@ -449,7 +465,7 @@ export class Tester extends TaskAgent {
449
465
  this.previousUrl = currentUrl;
450
466
  this.previousStateHash = currentStateHash;
451
467
  let context = '';
452
- const focusArea = detectFocusArea(currentState.ariaSnapshot);
468
+ const focusArea = currentState.overlay;
453
469
  const focusedElement = currentState.focusedElement;
454
470
  if (focusedElement) {
455
471
  const isTextInput = ['textbox', 'combobox', 'searchbox'].includes(focusedElement.role);
@@ -9,7 +9,7 @@ export class ExitCommand extends BaseCommand {
9
9
  aliases = ['quit'];
10
10
  async execute(_args) {
11
11
  await this.explorBot.printSessionAnalysis();
12
- await this.explorBot.getExplorer().stop();
12
+ await this.explorBot.stop();
13
13
  if (Stats.hasActivity()) {
14
14
  await new Promise((resolve) => {
15
15
  const { unmount } = render(React.createElement(StatusPane, {
@@ -59,25 +59,30 @@ export class ExploreCommand extends BaseCommand {
59
59
  tag('warning').log(error.message);
60
60
  return;
61
61
  }
62
- if (cfg.enabled) {
63
- await this.runReuseMode(mainUrl, feature, cfg);
64
- }
65
- else {
66
- await this.runFreshMode(mainUrl, feature, cfg.styles);
67
- }
68
- const mainPlan = this.completedPlans[0];
69
- if (mainPlan)
70
- this.explorBot.setCurrentPlan(mainPlan);
71
- if (this.dryRun) {
62
+ try {
63
+ if (cfg.enabled) {
64
+ await this.runReuseMode(mainUrl, feature, cfg);
65
+ }
66
+ else {
67
+ await this.runFreshMode(mainUrl, feature, cfg.styles);
68
+ }
69
+ const mainPlan = this.completedPlans[0];
70
+ if (mainPlan)
71
+ this.explorBot.setCurrentPlan(mainPlan);
72
+ if (this.dryRun) {
73
+ this.printResults();
74
+ return;
75
+ }
76
+ if (mainUrl)
77
+ await this.explorBot.visit(mainUrl).catch((err) => tag('warning').log(`Could not return to ${mainUrl}: ${browserErrorMessage(err)}`));
78
+ const savedPath = this.explorBot.savePlans(this.completedPlans);
72
79
  this.printResults();
73
- return;
80
+ this.printNextSteps(savedPath);
81
+ }
82
+ finally {
83
+ if (!this.dryRun)
84
+ await this.explorBot.printSessionAnalysis();
74
85
  }
75
- if (mainUrl)
76
- await this.explorBot.visit(mainUrl).catch((err) => tag('warning').log(`Could not return to ${mainUrl}: ${browserErrorMessage(err)}`));
77
- const savedPath = this.explorBot.savePlans(this.completedPlans);
78
- this.printResults();
79
- await this.explorBot.printSessionAnalysis();
80
- this.printNextSteps(savedPath);
81
86
  }
82
87
  originLabel(test) {
83
88
  return this.oldTestRefs.has(test) ? 'OLD' : 'NEW';
@@ -151,8 +151,9 @@ interface AIConfig {
151
151
  vision?: boolean;
152
152
  visionModel?: any;
153
153
  agenticModel?: any;
154
- maxAttempts?: number;
154
+ retryAttempts?: number;
155
155
  retryDelay?: number;
156
+ maxParallelRequests?: number;
156
157
  agents?: AgentsConfig;
157
158
  }
158
159
  interface HtmlConfig {
@@ -114,6 +114,7 @@ export class ExplorBot {
114
114
  async stop() {
115
115
  this.agents.quartermaster?.stop();
116
116
  await this.explorer?.stop();
117
+ await this.provider?.stop();
117
118
  }
118
119
  async visitInitialState() {
119
120
  await this.visit(this.options.from || '/');
@@ -1,6 +1,7 @@
1
1
  import { ActionResult, type FocusedElement } from './action-result.js';
2
2
  import type { ExperienceTracker } from './experience-tracker.js';
3
3
  import type { Knowledge, KnowledgeTracker } from './knowledge-tracker.js';
4
+ import { Overlay } from './utils/overlay.js';
4
5
  export interface Link {
5
6
  title: string;
6
7
  url: string;
@@ -40,6 +41,7 @@ export interface WebPageState {
40
41
  focusedElement?: FocusedElement | null;
41
42
  links?: Link[];
42
43
  verifications?: Record<string, boolean>;
44
+ overlay?: Overlay;
43
45
  }
44
46
  export interface StateTransition {
45
47
  /** Previous state (null if this is the first state) */
@@ -1,6 +1,6 @@
1
1
  import { ActionResult } from './action-result.js';
2
- import { detectFocusArea } from './utils/aria.js';
3
2
  import { createDebug, tag } from './utils/logger.js';
3
+ import { Overlay } from './utils/overlay.js';
4
4
  import { slugify } from './utils/strings.js';
5
5
  import { extractStatePath } from './utils/url-matcher.js';
6
6
  const debugLog = createDebug('explorbot:state');
@@ -114,8 +114,8 @@ export class StateManager {
114
114
  return newState;
115
115
  }
116
116
  hasDialogAppeared(previousState, newState) {
117
- const prevFocus = detectFocusArea(previousState?.ariaSnapshot ?? null);
118
- const newFocus = detectFocusArea(newState.ariaSnapshot ?? null);
117
+ const prevFocus = previousState?.overlay ?? Overlay.fromAria(previousState?.ariaSnapshot ?? null);
118
+ const newFocus = newState.overlay ?? Overlay.fromAria(newState.ariaSnapshot ?? null);
119
119
  return !prevFocus.detected && newFocus.detected;
120
120
  }
121
121
  /**
@@ -14,7 +14,6 @@ export declare const HTML_SELECTORS: {
14
14
  readonly interactiveControl: "button, a[href], input, select, textarea, [role=\"button\"], [role=\"link\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"], [role=\"tab\"], [role=\"menuitem\"]";
15
15
  readonly labelLike: "h1, h2, h3, h4, h5, h6, legend, caption, label, [role=\"heading\"], [class*=\"title\"], [class*=\"label\"], [class*=\"header\"], [class*=\"name\"]";
16
16
  readonly semanticContextContainer: "section, article, form, fieldset, li, tr, td, th, [role=\"group\"], [role=\"tabpanel\"], [role=\"region\"], [class*=\"card\"], [class*=\"panel\"], [class*=\"item\"], [class*=\"usage\"], [class*=\"group\"]";
17
- readonly semanticOverlays: readonly ["[role=\"dialog\"]", "[role=\"listbox\"]", "[role=\"menu\"]", "[role=\"tooltip\"]:not([style*=\"display: none\"]):not([style*=\"visibility: hidden\"])"];
18
17
  };
19
18
  export declare const HTML_VISIBILITY_LIMITS: {
20
19
  readonly maxViewportOverlayRatio: 0.95;
@@ -67,7 +66,9 @@ export type VisibleOverlayExtractionConfig = {
67
66
  interactiveContentSelector: string;
68
67
  limits: typeof HTML_EXTRACTION_LIMITS;
69
68
  overlaySelectors: readonly string[];
69
+ overlaySemanticSelector: string;
70
70
  visibilityLimits: typeof HTML_VISIBILITY_LIMITS;
71
+ geometryFallback?: boolean;
71
72
  };
72
73
  export type ComponentScopeExtractionConfig = {
73
74
  eidxAttr: string;
@@ -79,7 +79,6 @@ export const HTML_SELECTORS = {
79
79
  interactiveControl: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"]',
80
80
  labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]',
81
81
  semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]',
82
- semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])'],
83
82
  };
84
83
  export const HTML_VISIBILITY_LIMITS = {
85
84
  maxViewportOverlayRatio: 0.95,
@@ -447,8 +446,12 @@ export function extractVisibleOverlayHtml(config) {
447
446
  const { interactiveCount, text } = getUsefulContent(element);
448
447
  return interactiveCount > 0 || text.length > 0;
449
448
  }
450
- const overlays = [];
449
+ function isFloatingOverlay(element) {
450
+ const style = window.getComputedStyle(element);
451
+ return style.position === 'fixed' || style.position === 'absolute' || Number.parseInt(style.zIndex || '0', 10) > 0;
452
+ }
451
453
  const seen = new Set();
454
+ const collected = [];
452
455
  for (const selector of config.overlaySelectors) {
453
456
  for (const element of Array.from(document.querySelectorAll(selector))) {
454
457
  if (seen.has(element))
@@ -456,13 +459,16 @@ export function extractVisibleOverlayHtml(config) {
456
459
  seen.add(element);
457
460
  if (!isVisible(element))
458
461
  continue;
462
+ if (!element.matches(config.overlaySemanticSelector) && !isFloatingOverlay(element))
463
+ continue;
459
464
  const { interactiveCount, text } = getUsefulContent(element);
460
465
  if (interactiveCount === 0 && text.length === 0)
461
466
  continue;
462
- overlays.push(element.outerHTML.slice(0, config.limits.overlayHtmlLength));
467
+ collected.push(element);
463
468
  }
464
469
  }
465
- if (overlays.length === 0) {
470
+ const overlays = collected.filter((element) => !collected.some((other) => other !== element && element.contains(other))).map((element) => element.outerHTML.slice(0, config.limits.overlayHtmlLength));
471
+ if (overlays.length === 0 && config.geometryFallback !== false) {
466
472
  const floatingCandidates = Array.from(document.body.querySelectorAll('*'))
467
473
  .filter((element) => !seen.has(element) && isVisible(element) && isLikelyFloatingOverlay(element))
468
474
  .sort((left, right) => {
@@ -0,0 +1,24 @@
1
+ import { type VisibleOverlayExtractionConfig } from './html.js';
2
+ export declare const OVERLAY_SELECTORS: {
3
+ readonly semanticOverlays: readonly ["[role=\"dialog\"]", "[role=\"listbox\"]", "[role=\"menu\"]", "[role=\"tooltip\"]:not([style*=\"display: none\"]):not([style*=\"visibility: hidden\"])", "[class*=\"modal\"]", "[class*=\"dialog\"]", "[class*=\"overlay\"]", "[class*=\"popup\"]", "[class*=\"drawer\"]", "[class*=\"lightbox\"]"];
4
+ readonly modalOverlays: readonly ["[role=\"dialog\"]", "[role=\"alertdialog\"]", "[aria-modal=\"true\"]", "[class*=\"modal\"]", "[class*=\"dialog\"]", "[class*=\"overlay\"]", "[class*=\"popup\"]", "[class*=\"drawer\"]", "[class*=\"lightbox\"]"];
5
+ readonly overlaySemanticSelector: "[role=\"dialog\"], [role=\"alertdialog\"], [aria-modal=\"true\"], [role=\"listbox\"], [role=\"menu\"], [role=\"tooltip\"]";
6
+ };
7
+ export type OverlayData = {
8
+ type?: 'dialog' | 'modal' | null;
9
+ name?: string | null;
10
+ };
11
+ export declare class Overlay {
12
+ readonly type: 'dialog' | 'modal' | null;
13
+ readonly name: string | null;
14
+ constructor(data?: OverlayData);
15
+ get detected(): boolean;
16
+ static fromHtml(html: string): Overlay;
17
+ static fromAria(snapshot: string | null): Overlay;
18
+ static resolve(data: {
19
+ overlayHtml?: string;
20
+ overlay?: OverlayData | null;
21
+ ariaSnapshot?: string | null;
22
+ }): Overlay;
23
+ static captureConfig(): VisibleOverlayExtractionConfig;
24
+ }
@@ -0,0 +1,43 @@
1
+ import { detectFocusArea } from './aria.js';
2
+ import { HTML_EXTRACTION_LIMITS, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, extractHeadings } from './html.js';
3
+ export const OVERLAY_SELECTORS = {
4
+ semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])', '[class*="modal"]', '[class*="dialog"]', '[class*="overlay"]', '[class*="popup"]', '[class*="drawer"]', '[class*="lightbox"]'],
5
+ modalOverlays: ['[role="dialog"]', '[role="alertdialog"]', '[aria-modal="true"]', '[class*="modal"]', '[class*="dialog"]', '[class*="overlay"]', '[class*="popup"]', '[class*="drawer"]', '[class*="lightbox"]'],
6
+ overlaySemanticSelector: '[role="dialog"], [role="alertdialog"], [aria-modal="true"], [role="listbox"], [role="menu"], [role="tooltip"]',
7
+ };
8
+ export class Overlay {
9
+ type;
10
+ name;
11
+ constructor(data = {}) {
12
+ this.type = data.type ?? null;
13
+ this.name = data.name ?? null;
14
+ }
15
+ get detected() {
16
+ return this.type !== null;
17
+ }
18
+ static fromHtml(html) {
19
+ const headings = extractHeadings(html);
20
+ const name = [headings.h1, headings.h2, headings.h3, headings.h4].filter(Boolean).join(' ');
21
+ return new Overlay({ type: 'modal', name: name || null });
22
+ }
23
+ static fromAria(snapshot) {
24
+ return new Overlay(detectFocusArea(snapshot));
25
+ }
26
+ static resolve(data) {
27
+ if (data.overlayHtml)
28
+ return Overlay.fromHtml(data.overlayHtml);
29
+ if (data.overlay)
30
+ return new Overlay(data.overlay);
31
+ return Overlay.fromAria(data.ariaSnapshot ?? null);
32
+ }
33
+ static captureConfig() {
34
+ return {
35
+ interactiveContentSelector: HTML_SELECTORS.interactiveContent,
36
+ limits: HTML_EXTRACTION_LIMITS,
37
+ overlaySelectors: OVERLAY_SELECTORS.modalOverlays,
38
+ overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
39
+ visibilityLimits: HTML_VISIBILITY_LIMITS,
40
+ geometryFallback: false,
41
+ };
42
+ }
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -7,6 +7,7 @@ import { TTLCache } from './utils/cache.ts';
7
7
  import { type HtmlDiffPart, type HtmlDiffResult, htmlDiff, liveRegionMessages } from './utils/html-diff.ts';
8
8
  import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from './utils/html.ts';
9
9
  import { createDebug } from './utils/logger.ts';
10
+ import { Overlay } from './utils/overlay.ts';
10
11
  import { slugify } from './utils/strings.ts';
11
12
  import { extractStatePath, matchesUrl } from './utils/url-matcher.ts';
12
13
 
@@ -34,6 +35,7 @@ interface ActionResultData extends WebPageState {
34
35
  focusedElement?: FocusedElement | null;
35
36
  iframeURL?: string;
36
37
  links?: Link[];
38
+ overlayHtml?: string;
37
39
  }
38
40
 
39
41
  export interface PageDiff {
@@ -86,6 +88,7 @@ export class ActionResult implements ActionResultData {
86
88
  notes: string[] = [];
87
89
  public links: Link[] = [];
88
90
  public verifications?: Record<string, boolean>;
91
+ public overlay: Overlay = new Overlay();
89
92
 
90
93
  constructor(data: ActionResultData) {
91
94
  this.id = data.id;
@@ -131,6 +134,8 @@ export class ActionResult implements ActionResultData {
131
134
  this._ariaSnapshot = data.ariaSnapshot;
132
135
  }
133
136
 
137
+ this.overlay = Overlay.resolve(data);
138
+
134
139
  if (!this.fullUrl && this.url) {
135
140
  this.fullUrl = this.url;
136
141
  }
package/src/action.ts CHANGED
@@ -11,8 +11,9 @@ import { Observability } from './observability.ts';
11
11
  import type { PlaywrightRecorder } from './playwright-recorder.ts';
12
12
  import type { StateManager } from './state-manager.js';
13
13
  import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from './utils/browser-errors.ts';
14
- import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
14
+ import { captureHtmlForSnapshot, getVisibleOverlayHtmlExtractorSource, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
15
15
  import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
16
+ import { Overlay } from './utils/overlay.js';
16
17
  import { sleep, waitForPageReadiness } from './utils/page-readiness.ts';
17
18
  import { safeFilename } from './utils/strings.ts';
18
19
  import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from './utils/web-sandbox.ts';
@@ -144,11 +145,13 @@ class Action {
144
145
  let ariaSnapshot: string | null = null;
145
146
  let ariaSnapshotFile: string | undefined = undefined;
146
147
  let focusedElement: FocusedElement | null = null;
148
+ let overlayHtml = '';
147
149
 
148
150
  try {
149
151
  const page = this.playwrightHelper.page;
150
152
  ariaSnapshot = await page.locator('body').ariaSnapshot();
151
153
  focusedElement = await page.evaluate(readFocusedElement);
154
+ if (!frame) overlayHtml = await this.captureOverlayHtml();
152
155
  } catch (err) {
153
156
  debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
154
157
  }
@@ -177,6 +180,7 @@ class Action {
177
180
  ariaSnapshot,
178
181
  ariaSnapshotFile,
179
182
  focusedElement,
183
+ overlayHtml: overlayHtml || undefined,
180
184
  iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
181
185
  });
182
186
  this.stateManager.updateState(result, codeBlock);
@@ -190,6 +194,16 @@ class Action {
190
194
  }
191
195
  }
192
196
 
197
+ private async captureOverlayHtml(): Promise<string> {
198
+ return this.playwrightHelper.page.evaluate(
199
+ ({ extractorSource, config }: { extractorSource: string; config: any }) => {
200
+ const extract = new Function(`return ${extractorSource}`)() as (config: any) => string;
201
+ return extract(config);
202
+ },
203
+ { extractorSource: getVisibleOverlayHtmlExtractorSource(), config: Overlay.captureConfig() }
204
+ );
205
+ }
206
+
193
207
  private async captureMainDocumentStatus(): Promise<number | undefined> {
194
208
  if (this.mainDocumentStatus) return this.mainDocumentStatus;
195
209
 
@@ -17,6 +17,8 @@ export function toolExecutionLabel(input: Record<string, any> | undefined): stri
17
17
  return input?.explanation || input?.assertion || input?.reason || input?.request || '';
18
18
  }
19
19
 
20
+ export const NARRATION_TOOL = 'commentary';
21
+
20
22
  const AUTO_COMPACT_ARIA_CHANGES_CUTOFF = 500;
21
23
  const AUTO_COMPACT_TARGETED_HTML_CUTOFF = 500;
22
24
 
@@ -227,6 +229,7 @@ export class Conversation {
227
229
  if (!Array.isArray(message.content)) continue;
228
230
  for (const part of message.content) {
229
231
  if (part.type !== 'tool-result') continue;
232
+ if (part.toolName === NARRATION_TOOL) continue;
230
233
  executions.push(toToolExecution(part.toolName, toolCalls.get(part.toolCallId) || {}, part.output));
231
234
  }
232
235
  }
package/src/ai/driller.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  } from '../utils/html.ts';
23
23
  import { createDebug, tag } from '../utils/logger.ts';
24
24
  import { loop, pause } from '../utils/loop.ts';
25
+ import { OVERLAY_SELECTORS } from '../utils/overlay.ts';
25
26
  import { annotatePageElements } from '../utils/web-annotate.ts';
26
27
  import { eidxInContainer } from '../utils/web-eidx.ts';
27
28
  import { WebElement } from '../utils/web-element.ts';
@@ -682,7 +683,8 @@ export class Driller extends TaskAgent implements Agent {
682
683
  config: {
683
684
  interactiveContentSelector: HTML_SELECTORS.interactiveContent,
684
685
  limits: HTML_EXTRACTION_LIMITS,
685
- overlaySelectors: HTML_SELECTORS.semanticOverlays,
686
+ overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
687
+ overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
686
688
  visibilityLimits: HTML_VISIBILITY_LIMITS,
687
689
  },
688
690
  }
@@ -196,7 +196,7 @@ export class Fisherman implements Agent {
196
196
 
197
197
  AVAILABLE TOOLS:
198
198
  ${toolNames.join(', ')}.
199
- Use tool names exactly as listed. Do not invent aliases, combined names, or names with channel markers such as "commentary".
199
+ Use tool names exactly as listed. Do not invent aliases or combined names.
200
200
  Match each tool input schema exactly. Do not invent parameter names or pass extra fields.
201
201
 
202
202
  WORKFLOW:
package/src/ai/pilot.ts CHANGED
@@ -9,7 +9,7 @@ import type { PlaywrightRecorder } from '../playwright-recorder.ts';
9
9
  import type { StateManager } from '../state-manager.ts';
10
10
  import { Stats } from '../stats.ts';
11
11
  import { type Test, TestResult } from '../test-plan.ts';
12
- import { collectInteractiveNodes, detectFocusArea } from '../utils/aria.ts';
12
+ import { collectInteractiveNodes } from '../utils/aria.ts';
13
13
  import { ErrorPageError } from '../utils/error-page.ts';
14
14
  import { createDebug, tag } from '../utils/logger.ts';
15
15
 
@@ -25,7 +25,8 @@ import { capabilityGroundingRule, dataProtectionRules } from './rules.ts';
25
25
  import { isInteractive } from './task-agent.ts';
26
26
  import { withdrawVisionTools } from './tools.ts';
27
27
 
28
- const CHECK_TOOLS = ['verify', 'see', 'research', 'context'];
28
+ const CHECK_TOOLS = ['verify', 'see', 'research'];
29
+ const EVIDENCE_TOOLS = ['verify', 'see'];
29
30
  const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
30
31
  const PILOT_MESSAGE_LIMIT = 2;
31
32
  const PILOT_MESSAGE_MAX_LENGTH = 160;
@@ -326,6 +327,10 @@ export class Pilot implements Agent {
326
327
  overrides the others โ€” weigh them together. Tester's record() notes are the LEAST reliable; always
327
328
  cross-check against actual actions and state. Visual screenshot analysis is strong for UI state
328
329
  (active tabs, visible counts, colors).
330
+ Judge every check by WHAT IT ESTABLISHES, never by the fact that it ran. A check that executed
331
+ successfully is failure evidence when its content negates the scenario goal โ€” the goal's object
332
+ absent, the action not performed, the interaction impossible. "The check passed" and "the goal was
333
+ met" are different claims.
329
334
  If the final page clearly shows an equivalent success state in a different UI form, do not fail only
330
335
  because one narrow assertion targeted a specific badge, count, toast, or wording that the product
331
336
  represents differently.
@@ -574,8 +579,8 @@ export class Pilot implements Agent {
574
579
  }
575
580
 
576
581
  async settleExpectations(task: Test, finalState?: ActionResult): Promise<SettledExpectation[]> {
577
- let image: string | null = null;
578
- if (finalState?.screenshot && this.provider.hasVision()) image = `data:image/png;base64,${finalState.screenshot.toString('base64')}`;
582
+ let image: Buffer | null = null;
583
+ if (finalState?.screenshot && this.provider.hasVision()) image = finalState.screenshot;
579
584
 
580
585
  const decided = (text: string): 'passed' | 'failed' => {
581
586
  if (task.hasAchievedAny() && !task.getRemainingExpectations().includes(text)) return 'passed';
@@ -819,7 +824,7 @@ export class Pilot implements Agent {
819
824
  lines.push(`h3: ${state.h3 || ''}`);
820
825
  lines.push(`h4: ${state.h4 || ''}`);
821
826
 
822
- const focusArea = detectFocusArea(state.ariaSnapshot);
827
+ const focusArea = state.overlay;
823
828
  if (focusArea.detected) {
824
829
  lines.push(`modal: ${focusArea.name || focusArea.type}`);
825
830
  } else {
@@ -995,20 +1000,20 @@ export class Pilot implements Agent {
995
1000
 
996
1001
  private hasSuccessfulCheckEvidence(currentState: ActionResult, testerConversation: Conversation): boolean {
997
1002
  if (Object.values(currentState.verifications ?? {}).some(Boolean)) return true;
998
- return testerConversation.getToolExecutions().some((t) => CHECK_TOOLS.includes(t.toolName) && t.wasSuccessful);
1003
+ return testerConversation.getToolExecutions().some((t) => EVIDENCE_TOOLS.includes(t.toolName) && t.wasSuccessful);
999
1004
  }
1000
1005
 
1001
1006
  private formatSuccessfulAssertions(currentState: ActionResult, testerConversation: Conversation): string {
1002
1007
  const lines: string[] = [];
1003
1008
  for (const [assertion, passed] of Object.entries(currentState.verifications ?? {})) {
1004
- if (passed) lines.push(`PASS state verification: ${assertion}`);
1009
+ if (passed) lines.push(`state verification (passed): ${assertion}`);
1005
1010
  }
1006
1011
 
1007
1012
  for (const exec of testerConversation.getToolExecutions()) {
1008
- if (!CHECK_TOOLS.includes(exec.toolName) || !exec.wasSuccessful) continue;
1013
+ if (!EVIDENCE_TOOLS.includes(exec.toolName) || !exec.wasSuccessful) continue;
1009
1014
  const description = exec.input?.assertion || exec.input?.request || truncateJson(exec.input);
1010
1015
  const result = exec.output?.message || exec.output?.analysis || exec.output?.result;
1011
- lines.push(`PASS ${exec.toolName}: ${description}${result ? ` -> ${result}` : ''}`);
1016
+ lines.push(`CHECK ${exec.toolName} (executed successfully): ${description}${result ? ` -> ${result}` : ''}`);
1012
1017
  }
1013
1018
 
1014
1019
  return [...new Set(lines)].join('\n');
@@ -1137,7 +1142,7 @@ export class Pilot implements Agent {
1137
1142
 
1138
1143
  Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
1139
1144
  visualClick, back, getVisitedStates, reset, stop, finish, record.
1140
- Use tool names exactly as listed. Do not invent combined names, aliases, or names with channel markers such as "commentary".
1145
+ Use tool names exactly as listed. Do not invent combined names or aliases.
1141
1146
 
1142
1147
  ${capabilityGroundingRule}
1143
1148