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.
- package/bin/explorbot-cli.ts +3 -3
- package/dist/bin/explorbot-cli.js +3 -2
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +3 -0
- package/dist/src/action-result.js +3 -0
- package/dist/src/action.d.ts +1 -0
- package/dist/src/action.js +12 -1
- package/dist/src/ai/conversation.d.ts +1 -0
- package/dist/src/ai/conversation.js +3 -0
- package/dist/src/ai/driller.js +3 -1
- package/dist/src/ai/fisherman.js +1 -1
- package/dist/src/ai/pilot.js +14 -9
- package/dist/src/ai/provider.d.ts +5 -0
- package/dist/src/ai/provider.js +84 -19
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/tester.js +22 -6
- package/dist/src/commands/exit-command.js +1 -1
- package/dist/src/commands/explore-command.js +22 -17
- package/dist/src/config.d.ts +2 -1
- package/dist/src/explorbot.js +1 -0
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +3 -3
- package/dist/src/utils/html.d.ts +2 -1
- package/dist/src/utils/html.js +10 -4
- package/dist/src/utils/overlay.d.ts +24 -0
- package/dist/src/utils/overlay.js +43 -0
- package/package.json +1 -1
- package/src/action-result.ts +5 -0
- package/src/action.ts +15 -1
- package/src/ai/conversation.ts +3 -0
- package/src/ai/driller.ts +3 -1
- package/src/ai/fisherman.ts +1 -1
- package/src/ai/pilot.ts +15 -10
- package/src/ai/provider.ts +115 -43
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/tester.ts +20 -7
- package/src/commands/exit-command.ts +1 -1
- package/src/commands/explore-command.ts +17 -14
- package/src/config.ts +2 -1
- package/src/explorbot.ts +1 -0
- package/src/state-manager.ts +4 -3
- package/src/utils/html.ts +13 -4
- package/src/utils/overlay.ts +51 -0
package/dist/src/ai/tester.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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';
|
package/dist/src/config.d.ts
CHANGED
|
@@ -151,8 +151,9 @@ interface AIConfig {
|
|
|
151
151
|
vision?: boolean;
|
|
152
152
|
visionModel?: any;
|
|
153
153
|
agenticModel?: any;
|
|
154
|
-
|
|
154
|
+
retryAttempts?: number;
|
|
155
155
|
retryDelay?: number;
|
|
156
|
+
maxParallelRequests?: number;
|
|
156
157
|
agents?: AgentsConfig;
|
|
157
158
|
}
|
|
158
159
|
interface HtmlConfig {
|
package/dist/src/explorbot.js
CHANGED
|
@@ -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 =
|
|
118
|
-
const newFocus =
|
|
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
|
/**
|
package/dist/src/utils/html.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/utils/html.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
467
|
+
collected.push(element);
|
|
463
468
|
}
|
|
464
469
|
}
|
|
465
|
-
|
|
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
package/src/action-result.ts
CHANGED
|
@@ -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
|
|
package/src/ai/conversation.ts
CHANGED
|
@@ -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:
|
|
686
|
+
overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
|
|
687
|
+
overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
|
|
686
688
|
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
687
689
|
},
|
|
688
690
|
}
|
package/src/ai/fisherman.ts
CHANGED
|
@@ -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
|
|
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
|
|
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'
|
|
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:
|
|
578
|
-
if (finalState?.screenshot && this.provider.hasVision()) image =
|
|
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 =
|
|
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) =>
|
|
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(`
|
|
1009
|
+
if (passed) lines.push(`state verification (passed): ${assertion}`);
|
|
1005
1010
|
}
|
|
1006
1011
|
|
|
1007
1012
|
for (const exec of testerConversation.getToolExecutions()) {
|
|
1008
|
-
if (!
|
|
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(`
|
|
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
|
|
1145
|
+
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1141
1146
|
|
|
1142
1147
|
${capabilityGroundingRule}
|
|
1143
1148
|
|