explorbot 0.3.1 → 0.3.4
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 +16 -5
- package/dist/bin/explorbot-cli.js +14 -4
- package/dist/models.json +2 -0
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +4 -0
- package/dist/src/action-result.js +20 -12
- package/dist/src/action.d.ts +11 -0
- package/dist/src/action.js +28 -11
- package/dist/src/ai/conversation.d.ts +2 -1
- package/dist/src/ai/conversation.js +9 -4
- package/dist/src/ai/driller.js +3 -1
- package/dist/src/ai/navigator.d.ts +3 -0
- package/dist/src/ai/navigator.js +20 -4
- package/dist/src/ai/pilot.js +9 -2
- package/dist/src/ai/provider.d.ts +2 -0
- package/dist/src/ai/provider.js +18 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -3
- package/dist/src/ai/researcher/sections.js +0 -1
- package/dist/src/ai/researcher.js +0 -1
- package/dist/src/ai/rules.js +0 -1
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +9 -11
- package/dist/src/ai/tools.d.ts +2 -0
- package/dist/src/ai/tools.js +21 -4
- package/dist/src/commands/exit-command.js +1 -1
- package/dist/src/commands/init-command.js +74 -24
- package/dist/src/components/InitWizard.d.ts +2 -1
- package/dist/src/components/InitWizard.js +8 -4
- package/dist/src/explorbot.js +1 -0
- package/dist/src/explorer.js +1 -0
- package/dist/src/knowledge-tracker.d.ts +3 -1
- package/dist/src/knowledge-tracker.js +4 -4
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +3 -3
- package/dist/src/utils/aria.js +1 -1
- 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/docs/basics/providers.md +2 -4
- package/models.json +2 -0
- package/package.json +1 -1
- package/src/action-result.ts +25 -15
- package/src/action.ts +36 -13
- package/src/ai/conversation.ts +11 -5
- package/src/ai/driller.ts +3 -1
- package/src/ai/navigator.ts +22 -4
- package/src/ai/pilot.ts +9 -2
- package/src/ai/provider.ts +19 -1
- package/src/ai/researcher/deep-analysis.ts +8 -3
- package/src/ai/researcher/sections.ts +0 -1
- package/src/ai/researcher.ts +0 -1
- package/src/ai/rules.ts +0 -1
- package/src/ai/tester.ts +9 -9
- package/src/ai/tools.ts +20 -4
- package/src/commands/exit-command.ts +1 -1
- package/src/commands/init-command.ts +81 -22
- package/src/components/InitWizard.tsx +8 -4
- package/src/explorbot.ts +1 -0
- package/src/explorer.ts +1 -0
- package/src/knowledge-tracker.ts +4 -4
- package/src/state-manager.ts +4 -3
- package/src/utils/aria.ts +1 -1
- package/src/utils/html.ts +13 -4
- package/src/utils/overlay.ts +51 -0
package/bin/explorbot-cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Command } from 'commander';
|
|
|
7
7
|
import figureSet from 'figures';
|
|
8
8
|
import { render } from 'ink';
|
|
9
9
|
import React from 'react';
|
|
10
|
+
import { flushTelemetry } from '../src/ai/provider.js';
|
|
10
11
|
import { App } from '../src/components/App.js';
|
|
11
12
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
13
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
@@ -15,7 +16,7 @@ import { remote } from '../src/remote.js';
|
|
|
15
16
|
import { Stats } from '../src/stats.js';
|
|
16
17
|
import { Plan } from '../src/test-plan.js';
|
|
17
18
|
import { getCliName } from '../src/utils/cli-name.ts';
|
|
18
|
-
import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode } from '../src/utils/logger.js';
|
|
19
|
+
import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode, tag } from '../src/utils/logger.js';
|
|
19
20
|
import { jsonToTable } from '../src/utils/markdown-parser.js';
|
|
20
21
|
import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
|
|
21
22
|
import { type NextStepSection, printNextSteps, relativeToCwd } from '../src/utils/next-steps.ts';
|
|
@@ -29,6 +30,16 @@ const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as stri
|
|
|
29
30
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
30
31
|
remote.registerOption(program);
|
|
31
32
|
|
|
33
|
+
process.on('uncaughtException', async (error) => {
|
|
34
|
+
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
35
|
+
await flushTelemetry();
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
process.on('unhandledRejection', (reason) => {
|
|
40
|
+
tag('error').log(`Unhandled rejection: ${reason instanceof Error ? `${reason.message}\n${reason.stack}` : String(reason)}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
32
43
|
if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
|
|
33
44
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
34
45
|
}
|
|
@@ -103,9 +114,7 @@ async function startTUI(explorBot: ExplorBot): Promise<void> {
|
|
|
103
114
|
async function showStatsAndExit(code: number): Promise<never> {
|
|
104
115
|
if (remote.isAttached()) {
|
|
105
116
|
await remote.close(code);
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
if (Stats.hasActivity()) {
|
|
117
|
+
} else if (Stats.hasActivity()) {
|
|
109
118
|
await new Promise<void>((resolve) => {
|
|
110
119
|
const { unmount } = render(
|
|
111
120
|
React.createElement(StatusPane, {
|
|
@@ -121,6 +130,7 @@ async function showStatsAndExit(code: number): Promise<never> {
|
|
|
121
130
|
);
|
|
122
131
|
});
|
|
123
132
|
}
|
|
133
|
+
await flushTelemetry();
|
|
124
134
|
process.exit(code);
|
|
125
135
|
}
|
|
126
136
|
|
|
@@ -546,6 +556,7 @@ program
|
|
|
546
556
|
.command('learn [url] [description]')
|
|
547
557
|
.description('Add knowledge for URLs')
|
|
548
558
|
.option('-p, --path <path>', 'Working directory path')
|
|
559
|
+
.option('--replace', 'Replace existing knowledge for this URL instead of appending')
|
|
549
560
|
.action(async (url, description, options) => {
|
|
550
561
|
try {
|
|
551
562
|
await ConfigParser.getInstance().loadConfig({
|
|
@@ -556,7 +567,7 @@ program
|
|
|
556
567
|
const tracker = new KnowledgeTracker();
|
|
557
568
|
|
|
558
569
|
if (url && description) {
|
|
559
|
-
const result = tracker.addKnowledge(url, description);
|
|
570
|
+
const result = tracker.addKnowledge(url, description, { replace: options.replace });
|
|
560
571
|
const action = result.isNewFile ? 'Created' : 'Updated';
|
|
561
572
|
console.log(`Knowledge ${action} in: ${result.filename}`);
|
|
562
573
|
return;
|
|
@@ -7,6 +7,7 @@ import { Command } from 'commander';
|
|
|
7
7
|
import figureSet from 'figures';
|
|
8
8
|
import { render } from 'ink';
|
|
9
9
|
import React from 'react';
|
|
10
|
+
import { flushTelemetry } from '../src/ai/provider.js';
|
|
10
11
|
import { App } from '../src/components/App.js';
|
|
11
12
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
13
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
@@ -15,7 +16,7 @@ import { remote } from '../src/remote.js';
|
|
|
15
16
|
import { Stats } from '../src/stats.js';
|
|
16
17
|
import { Plan } from '../src/test-plan.js';
|
|
17
18
|
import { getCliName } from "../src/utils/cli-name.js";
|
|
18
|
-
import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode } from '../src/utils/logger.js';
|
|
19
|
+
import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode, tag } from '../src/utils/logger.js';
|
|
19
20
|
import { jsonToTable } from '../src/utils/markdown-parser.js';
|
|
20
21
|
import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
|
|
21
22
|
import { printNextSteps, relativeToCwd } from "../src/utils/next-steps.js";
|
|
@@ -25,6 +26,14 @@ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../p
|
|
|
25
26
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
26
27
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
27
28
|
remote.registerOption(program);
|
|
29
|
+
process.on('uncaughtException', async (error) => {
|
|
30
|
+
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
31
|
+
await flushTelemetry();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
process.on('unhandledRejection', (reason) => {
|
|
35
|
+
tag('error').log(`Unhandled rejection: ${reason instanceof Error ? `${reason.message}\n${reason.stack}` : String(reason)}`);
|
|
36
|
+
});
|
|
28
37
|
if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
|
|
29
38
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
30
39
|
}
|
|
@@ -78,9 +87,8 @@ async function startTUI(explorBot) {
|
|
|
78
87
|
async function showStatsAndExit(code) {
|
|
79
88
|
if (remote.isAttached()) {
|
|
80
89
|
await remote.close(code);
|
|
81
|
-
process.exit(code);
|
|
82
90
|
}
|
|
83
|
-
if (Stats.hasActivity()) {
|
|
91
|
+
else if (Stats.hasActivity()) {
|
|
84
92
|
await new Promise((resolve) => {
|
|
85
93
|
const { unmount } = render(React.createElement(StatusPane, {
|
|
86
94
|
onComplete: () => {
|
|
@@ -93,6 +101,7 @@ async function showStatsAndExit(code) {
|
|
|
93
101
|
});
|
|
94
102
|
});
|
|
95
103
|
}
|
|
104
|
+
await flushTelemetry();
|
|
96
105
|
process.exit(code);
|
|
97
106
|
}
|
|
98
107
|
addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
|
|
@@ -492,6 +501,7 @@ program
|
|
|
492
501
|
.command('learn [url] [description]')
|
|
493
502
|
.description('Add knowledge for URLs')
|
|
494
503
|
.option('-p, --path <path>', 'Working directory path')
|
|
504
|
+
.option('--replace', 'Replace existing knowledge for this URL instead of appending')
|
|
495
505
|
.action(async (url, description, options) => {
|
|
496
506
|
try {
|
|
497
507
|
await ConfigParser.getInstance().loadConfig({
|
|
@@ -500,7 +510,7 @@ program
|
|
|
500
510
|
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
|
|
501
511
|
const tracker = new KnowledgeTracker();
|
|
502
512
|
if (url && description) {
|
|
503
|
-
const result = tracker.addKnowledge(url, description);
|
|
513
|
+
const result = tracker.addKnowledge(url, description, { replace: options.replace });
|
|
504
514
|
const action = result.isNewFile ? 'Created' : 'Updated';
|
|
505
515
|
console.log(`Knowledge ${action} in: ${result.filename}`);
|
|
506
516
|
return;
|
package/dist/models.json
CHANGED
package/dist/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { type HtmlConfig } from './config.js';
|
|
|
2
2
|
import type { Link, WebPageState } from './state-manager.js';
|
|
3
3
|
import { TTLCache } from './utils/cache.js';
|
|
4
4
|
import { type HtmlDiffPart, type HtmlDiffResult } from './utils/html-diff.js';
|
|
5
|
+
import { Overlay } from './utils/overlay.js';
|
|
5
6
|
interface ActionResultData extends WebPageState {
|
|
6
7
|
html?: string;
|
|
7
8
|
fullUrl?: string | undefined;
|
|
@@ -28,6 +29,7 @@ interface ActionResultData extends WebPageState {
|
|
|
28
29
|
focusedElement?: FocusedElement | null;
|
|
29
30
|
iframeURL?: string;
|
|
30
31
|
links?: Link[];
|
|
32
|
+
overlayHtml?: string;
|
|
31
33
|
}
|
|
32
34
|
export interface PageDiff {
|
|
33
35
|
urlChanged: boolean;
|
|
@@ -81,6 +83,7 @@ export declare class ActionResult implements ActionResultData {
|
|
|
81
83
|
notes: string[];
|
|
82
84
|
links: Link[];
|
|
83
85
|
verifications?: Record<string, boolean>;
|
|
86
|
+
overlay: Overlay;
|
|
84
87
|
constructor(data: ActionResultData);
|
|
85
88
|
get hash(): string;
|
|
86
89
|
get html(): string;
|
|
@@ -132,6 +135,7 @@ export declare class Diff {
|
|
|
132
135
|
isSameUrl(): boolean;
|
|
133
136
|
urlHasChanged(): boolean;
|
|
134
137
|
get htmlParts(): HtmlDiffPart[];
|
|
138
|
+
cleanedHtmlParts(): Promise<HtmlDiffPart[]>;
|
|
135
139
|
get ariaChanged(): string | null;
|
|
136
140
|
get ariaChangeCount(): number;
|
|
137
141
|
get htmlDiff(): HtmlDiffResult | null;
|
|
@@ -5,6 +5,7 @@ import { TTLCache } from "./utils/cache.js";
|
|
|
5
5
|
import { htmlDiff, liveRegionMessages } from "./utils/html-diff.js";
|
|
6
6
|
import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from "./utils/html.js";
|
|
7
7
|
import { createDebug } from "./utils/logger.js";
|
|
8
|
+
import { Overlay } from "./utils/overlay.js";
|
|
8
9
|
import { slugify } from "./utils/strings.js";
|
|
9
10
|
import { extractStatePath, matchesUrl } from "./utils/url-matcher.js";
|
|
10
11
|
const debugLog = createDebug('explorbot:state');
|
|
@@ -37,6 +38,7 @@ export class ActionResult {
|
|
|
37
38
|
notes = [];
|
|
38
39
|
links = [];
|
|
39
40
|
verifications;
|
|
41
|
+
overlay = new Overlay();
|
|
40
42
|
constructor(data) {
|
|
41
43
|
this.id = data.id;
|
|
42
44
|
this.timestamp = data.timestamp ?? new Date();
|
|
@@ -77,6 +79,7 @@ export class ActionResult {
|
|
|
77
79
|
if (data.ariaSnapshot !== undefined) {
|
|
78
80
|
this._ariaSnapshot = data.ariaSnapshot;
|
|
79
81
|
}
|
|
82
|
+
this.overlay = Overlay.resolve(data);
|
|
80
83
|
if (!this.fullUrl && this.url) {
|
|
81
84
|
this.fullUrl = this.url;
|
|
82
85
|
}
|
|
@@ -441,17 +444,9 @@ export class ActionResult {
|
|
|
441
444
|
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
442
445
|
}
|
|
443
446
|
if (diff.htmlParts.length > 0) {
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const filteredHtml = htmlCombinedSnapshot(part.subtree, htmlConfig?.combined);
|
|
448
|
-
const minified = await minifyHtml(filteredHtml);
|
|
449
|
-
if (minified) {
|
|
450
|
-
processedParts.push({ ...part, subtree: minified });
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
if (processedParts.length > 0) {
|
|
454
|
-
pageDiff.htmlParts = collapseHtmlParts(processedParts);
|
|
447
|
+
const collapsed = collapseHtmlParts(await diff.cleanedHtmlParts());
|
|
448
|
+
if (collapsed.length > 0) {
|
|
449
|
+
pageDiff.htmlParts = collapsed;
|
|
455
450
|
}
|
|
456
451
|
}
|
|
457
452
|
if (pageDiff.ariaChanges && this.iframeSnapshots.length > 0) {
|
|
@@ -487,7 +482,9 @@ function collapseHtmlParts(parts) {
|
|
|
487
482
|
const total = parts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
488
483
|
const fullPageReRender = total > HTML_PARTS_TOTAL_BUDGET || parts.length > HTML_PARTS_COUNT_LIMIT;
|
|
489
484
|
if (fullPageReRender) {
|
|
490
|
-
return parts
|
|
485
|
+
return parts
|
|
486
|
+
.filter((part) => part.added.length > 0 || part.removed.length > 0)
|
|
487
|
+
.map((part) => ({
|
|
491
488
|
...part,
|
|
492
489
|
subtree: `<html><head></head><body>...collapsed (${part.subtree.length} chars, ${part.added.length} added, ${part.removed.length} removed)...</body></html>`,
|
|
493
490
|
}));
|
|
@@ -540,6 +537,17 @@ export class Diff {
|
|
|
540
537
|
return [];
|
|
541
538
|
return this._htmlDiffResult.parts;
|
|
542
539
|
}
|
|
540
|
+
async cleanedHtmlParts() {
|
|
541
|
+
const htmlConfig = ConfigParser.getInstance().getConfig().html;
|
|
542
|
+
const cleaned = [];
|
|
543
|
+
for (const part of this.htmlParts) {
|
|
544
|
+
const minified = await minifyHtml(htmlCombinedSnapshot(part.subtree, htmlConfig?.combined));
|
|
545
|
+
if (!minified)
|
|
546
|
+
continue;
|
|
547
|
+
cleaned.push({ ...part, subtree: minified });
|
|
548
|
+
}
|
|
549
|
+
return cleaned;
|
|
550
|
+
}
|
|
543
551
|
get ariaChanged() {
|
|
544
552
|
return this._ariaDiffResult;
|
|
545
553
|
}
|
package/dist/src/action.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ declare class Action {
|
|
|
15
15
|
name: string;
|
|
16
16
|
args: any[];
|
|
17
17
|
}>;
|
|
18
|
+
executedSteps: ExecutedStep[];
|
|
18
19
|
lastValue: unknown;
|
|
19
20
|
recorder?: PlaywrightRecorder;
|
|
20
21
|
recovery: RecoveryRunner;
|
|
@@ -32,6 +33,7 @@ declare class Action {
|
|
|
32
33
|
includeScreenshot?: boolean;
|
|
33
34
|
codeBlock?: string;
|
|
34
35
|
}): Promise<ActionResult>;
|
|
36
|
+
captureOverlayHtml(): Promise<string>;
|
|
35
37
|
captureMainDocumentStatus(): Promise<number | undefined>;
|
|
36
38
|
captureResponses(): () => void;
|
|
37
39
|
recordNetworkCall(request: any, status: number): void;
|
|
@@ -57,3 +59,12 @@ export type RecoveryRunner = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
|
57
59
|
export interface ExecuteOptions {
|
|
58
60
|
verbatim?: boolean;
|
|
59
61
|
}
|
|
62
|
+
export declare const attachStepLogger: (target: ExecutedStep[], assertionsTarget?: Array<{
|
|
63
|
+
name: string;
|
|
64
|
+
args: any[];
|
|
65
|
+
}>) => (() => void);
|
|
66
|
+
export interface ExecutedStep {
|
|
67
|
+
command: string;
|
|
68
|
+
success: boolean;
|
|
69
|
+
error?: string;
|
|
70
|
+
}
|
package/dist/src/action.js
CHANGED
|
@@ -8,8 +8,9 @@ import { clearActivity, setActivity } from "./activity.js";
|
|
|
8
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
9
9
|
import { Observability } from "./observability.js";
|
|
10
10
|
import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
11
|
-
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
11
|
+
import { captureHtmlForSnapshot, getVisibleOverlayHtmlExtractorSource, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
12
12
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
13
|
+
import { Overlay } from './utils/overlay.js';
|
|
13
14
|
import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
|
|
14
15
|
import { safeFilename } from "./utils/strings.js";
|
|
15
16
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
|
|
@@ -30,6 +31,7 @@ class Action {
|
|
|
30
31
|
playwrightHelper;
|
|
31
32
|
playwrightGroupId = null;
|
|
32
33
|
assertionSteps = [];
|
|
34
|
+
executedSteps = [];
|
|
33
35
|
lastValue;
|
|
34
36
|
recorder;
|
|
35
37
|
recovery;
|
|
@@ -131,10 +133,13 @@ class Action {
|
|
|
131
133
|
let ariaSnapshot = null;
|
|
132
134
|
let ariaSnapshotFile = undefined;
|
|
133
135
|
let focusedElement = null;
|
|
136
|
+
let overlayHtml = '';
|
|
134
137
|
try {
|
|
135
138
|
const page = this.playwrightHelper.page;
|
|
136
139
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
137
140
|
focusedElement = await page.evaluate(readFocusedElement);
|
|
141
|
+
if (!frame)
|
|
142
|
+
overlayHtml = await this.captureOverlayHtml();
|
|
138
143
|
}
|
|
139
144
|
catch (err) {
|
|
140
145
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
@@ -161,6 +166,7 @@ class Action {
|
|
|
161
166
|
ariaSnapshot,
|
|
162
167
|
ariaSnapshotFile,
|
|
163
168
|
focusedElement,
|
|
169
|
+
overlayHtml: overlayHtml || undefined,
|
|
164
170
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
165
171
|
});
|
|
166
172
|
this.stateManager.updateState(result, codeBlock);
|
|
@@ -175,6 +181,12 @@ class Action {
|
|
|
175
181
|
return new ActionResult({ url, error: msg });
|
|
176
182
|
}
|
|
177
183
|
}
|
|
184
|
+
async captureOverlayHtml() {
|
|
185
|
+
return this.playwrightHelper.page.evaluate(({ extractorSource, config }) => {
|
|
186
|
+
const extract = new Function(`return ${extractorSource}`)();
|
|
187
|
+
return extract(config);
|
|
188
|
+
}, { extractorSource: getVisibleOverlayHtmlExtractorSource(), config: Overlay.captureConfig() });
|
|
189
|
+
}
|
|
178
190
|
async captureMainDocumentStatus() {
|
|
179
191
|
if (this.mainDocumentStatus)
|
|
180
192
|
return this.mainDocumentStatus;
|
|
@@ -293,7 +305,7 @@ class Action {
|
|
|
293
305
|
let codeString = code.replace(/^\(I\) => /, '').trim();
|
|
294
306
|
const executedSteps = [];
|
|
295
307
|
const assertionSteps = [];
|
|
296
|
-
const
|
|
308
|
+
const detachSteps = attachStepLogger(executedSteps, assertionSteps);
|
|
297
309
|
const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
|
|
298
310
|
this.playwrightGroupId = groupId;
|
|
299
311
|
const detachResponses = this.captureResponses();
|
|
@@ -319,10 +331,12 @@ class Action {
|
|
|
319
331
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
320
332
|
await recorder.promise();
|
|
321
333
|
this.lastValue = await returned;
|
|
334
|
+
if (!recorder.isRunning())
|
|
335
|
+
throw new Error('CodeceptJS recorder is stopped, commands were skipped and never reached the browser');
|
|
322
336
|
}
|
|
323
337
|
this.restorePageTimeout();
|
|
324
338
|
if (executedSteps.length > 0) {
|
|
325
|
-
codeString = executedSteps.join('\n');
|
|
339
|
+
codeString = executedSteps.map((step) => step.command).join('\n');
|
|
326
340
|
}
|
|
327
341
|
const pageState = await this.captureOnce({ codeBlock: codeString });
|
|
328
342
|
this.actionResult = pageState;
|
|
@@ -339,11 +353,12 @@ class Action {
|
|
|
339
353
|
throw err;
|
|
340
354
|
}
|
|
341
355
|
finally {
|
|
356
|
+
this.executedSteps = executedSteps;
|
|
342
357
|
this.restorePageTimeout();
|
|
343
358
|
detachResponses();
|
|
344
359
|
if (groupId)
|
|
345
360
|
await this.recorder.endAction();
|
|
346
|
-
|
|
361
|
+
detachSteps();
|
|
347
362
|
if (stepSpan) {
|
|
348
363
|
stepSpan.end();
|
|
349
364
|
}
|
|
@@ -432,13 +447,16 @@ async function captureTitle(page, actor) {
|
|
|
432
447
|
return '';
|
|
433
448
|
}
|
|
434
449
|
const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
|
|
435
|
-
const attachStepLogger = (target, assertionsTarget) => {
|
|
450
|
+
export const attachStepLogger = (target, assertionsTarget) => {
|
|
436
451
|
const listener = (step, error) => {
|
|
437
452
|
if (!step?.toCode)
|
|
438
453
|
return;
|
|
439
454
|
if (step.name?.startsWith('grab'))
|
|
440
455
|
return;
|
|
441
|
-
|
|
456
|
+
const executed = { command: step.toCode(), success: !error };
|
|
457
|
+
if (error)
|
|
458
|
+
executed.error = errorToString(error);
|
|
459
|
+
target.push(executed);
|
|
442
460
|
if (assertionsTarget && ASSERTION_STEP_NAMES.has(step.name)) {
|
|
443
461
|
assertionsTarget.push({ name: step.name, args: step.args || [] });
|
|
444
462
|
}
|
|
@@ -450,11 +468,10 @@ const attachStepLogger = (target, assertionsTarget) => {
|
|
|
450
468
|
};
|
|
451
469
|
codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, listener);
|
|
452
470
|
codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, listener);
|
|
453
|
-
return
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
471
|
+
return () => {
|
|
472
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
|
|
473
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
474
|
+
};
|
|
458
475
|
};
|
|
459
476
|
const readFocusedElement = () => {
|
|
460
477
|
const el = document.activeElement;
|
|
@@ -4,8 +4,9 @@ export interface ToolExecution {
|
|
|
4
4
|
input: any;
|
|
5
5
|
output: any;
|
|
6
6
|
wasSuccessful: boolean;
|
|
7
|
+
reasoning?: string;
|
|
7
8
|
}
|
|
8
|
-
export declare function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution;
|
|
9
|
+
export declare function toToolExecution(toolName: string, input: any, rawOutput: any, reasoning?: string): ToolExecution;
|
|
9
10
|
export declare function toolExecutionLabel(input: Record<string, any> | undefined): string;
|
|
10
11
|
export declare const NARRATION_TOOL = "commentary";
|
|
11
12
|
export declare class Conversation {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export function toToolExecution(toolName, input, rawOutput) {
|
|
1
|
+
export function toToolExecution(toolName, input, rawOutput, reasoning) {
|
|
2
2
|
let output = rawOutput;
|
|
3
3
|
if (rawOutput?.type === 'json' && rawOutput?.value)
|
|
4
4
|
output = rawOutput.value;
|
|
5
|
-
return { toolName, input, output, wasSuccessful: output?.success !== false };
|
|
5
|
+
return { toolName, input, output, wasSuccessful: output?.success !== false, reasoning };
|
|
6
6
|
}
|
|
7
7
|
export function toolExecutionLabel(input) {
|
|
8
8
|
return input?.explanation || input?.assertion || input?.reason || input?.request || '';
|
|
@@ -189,10 +189,14 @@ export class Conversation {
|
|
|
189
189
|
continue;
|
|
190
190
|
if (!Array.isArray(message.content))
|
|
191
191
|
continue;
|
|
192
|
+
const reasoning = message.content
|
|
193
|
+
.filter((part) => part.type === 'reasoning' && part.text?.trim())
|
|
194
|
+
.map((part) => part.text.trim())
|
|
195
|
+
.join('\n');
|
|
192
196
|
for (const part of message.content) {
|
|
193
197
|
if (part.type !== 'tool-call')
|
|
194
198
|
continue;
|
|
195
|
-
toolCalls.set(part.toolCallId, part.input);
|
|
199
|
+
toolCalls.set(part.toolCallId, { input: part.input, reasoning });
|
|
196
200
|
}
|
|
197
201
|
}
|
|
198
202
|
const executions = [];
|
|
@@ -206,7 +210,8 @@ export class Conversation {
|
|
|
206
210
|
continue;
|
|
207
211
|
if (part.toolName === NARRATION_TOOL)
|
|
208
212
|
continue;
|
|
209
|
-
|
|
213
|
+
const call = toolCalls.get(part.toolCallId);
|
|
214
|
+
executions.push(toToolExecution(part.toolName, call?.input || {}, part.output, call?.reasoning));
|
|
210
215
|
}
|
|
211
216
|
}
|
|
212
217
|
return executions;
|
package/dist/src/ai/driller.js
CHANGED
|
@@ -9,6 +9,7 @@ import { collectInteractiveNodes } from "../utils/aria.js";
|
|
|
9
9
|
import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, getComponentScopeHtmlExtractorSource, getVisibleOverlayHtmlExtractorSource, inferHtmlRole, } from "../utils/html.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
import { loop, pause } from "../utils/loop.js";
|
|
12
|
+
import { OVERLAY_SELECTORS } from "../utils/overlay.js";
|
|
12
13
|
import { annotatePageElements } from "../utils/web-annotate.js";
|
|
13
14
|
import { eidxInContainer } from "../utils/web-eidx.js";
|
|
14
15
|
import { WebElement } from "../utils/web-element.js";
|
|
@@ -578,7 +579,8 @@ export class Driller extends TaskAgent {
|
|
|
578
579
|
config: {
|
|
579
580
|
interactiveContentSelector: HTML_SELECTORS.interactiveContent,
|
|
580
581
|
limits: HTML_EXTRACTION_LIMITS,
|
|
581
|
-
overlaySelectors:
|
|
582
|
+
overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
|
|
583
|
+
overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
|
|
582
584
|
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
583
585
|
},
|
|
584
586
|
}));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ActionResult } from '../action-result.js';
|
|
2
2
|
import type Action from '../action.js';
|
|
3
|
+
import type { ExecutedStep } from '../action.js';
|
|
3
4
|
import type { ExplorbotConfig } from '../config.js';
|
|
4
5
|
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
5
6
|
import Explorer from '../explorer.js';
|
|
@@ -16,6 +17,7 @@ declare class Navigator implements Agent {
|
|
|
16
17
|
hooksRunner: HooksRunner;
|
|
17
18
|
MAX_ATTEMPTS: number;
|
|
18
19
|
lastFailureReason: string | null;
|
|
20
|
+
executedSteps: ExecutedStep[];
|
|
19
21
|
systemPrompt: string;
|
|
20
22
|
freeSailSystemPrompt: string;
|
|
21
23
|
explorer: Explorer;
|
|
@@ -55,6 +57,7 @@ declare class Navigator implements Agent {
|
|
|
55
57
|
freshState: ActionResult;
|
|
56
58
|
urlMatches: boolean;
|
|
57
59
|
}>;
|
|
60
|
+
targetUrlReached(action: Action, expectedUrl: string, state: ActionResult): boolean;
|
|
58
61
|
ariaDiff(freshState: ActionResult, previous: ActionResult): Promise<string | null>;
|
|
59
62
|
saveFlow(message: string, expectedUrl: string | undefined, actionResult: ActionResult, progressBlocks: string[]): void;
|
|
60
63
|
rescueDelayedRedirect(action: Action, expectedUrl: string): Promise<boolean>;
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -2,8 +2,8 @@ import { tool } from 'ai';
|
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ActionResult } from '../action-result.js';
|
|
5
|
-
import { normalizeUrl } from '../state-manager.js';
|
|
6
5
|
import { renderAssertion } from "../playwright-recorder.js";
|
|
6
|
+
import { normalizeUrl } from '../state-manager.js';
|
|
7
7
|
import { isFatalBrowserError } from "../utils/browser-errors.js";
|
|
8
8
|
import { getCliName } from "../utils/cli-name.js";
|
|
9
9
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
@@ -26,6 +26,7 @@ class Navigator {
|
|
|
26
26
|
hooksRunner;
|
|
27
27
|
MAX_ATTEMPTS = Number.parseInt(process.env.MAX_ATTEMPTS || '5');
|
|
28
28
|
lastFailureReason = null;
|
|
29
|
+
executedSteps = [];
|
|
29
30
|
systemPrompt = dedent `
|
|
30
31
|
<role>
|
|
31
32
|
You are senior test automation engineer with master QA skills.
|
|
@@ -194,10 +195,15 @@ class Navigator {
|
|
|
194
195
|
if (!this.provider)
|
|
195
196
|
throw new Error('AI-assisted recovery is unavailable: no AI model is configured.');
|
|
196
197
|
this.lastFailureReason = null;
|
|
198
|
+
this.executedSteps = [];
|
|
197
199
|
tag('info').log('AI Navigator resolving state at', actionResult.url);
|
|
198
200
|
debugLog('Resolution message:', message);
|
|
199
201
|
const action = opts?.action ?? this.explorer.action();
|
|
200
202
|
const expectedUrl = opts?.expectedUrl;
|
|
203
|
+
if (expectedUrl && this.targetUrlReached(action, expectedUrl, actionResult)) {
|
|
204
|
+
tag('success').log(`Already at ${expectedUrl} — navigation resolved`);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
201
207
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
202
208
|
const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
|
|
203
209
|
conversation.addUserText(await this.buildResolutionPrompt(message, actionResult, opts?.experience));
|
|
@@ -278,14 +284,20 @@ class Navigator {
|
|
|
278
284
|
const freshHash = check.freshState.getStateHash();
|
|
279
285
|
resolved = check.urlMatches && freshHash !== actionResult.getStateHash();
|
|
280
286
|
if (!resolved && attempt.ok) {
|
|
281
|
-
|
|
287
|
+
if (check.urlMatches) {
|
|
288
|
+
lastFailure = `Reached ${check.freshState.url} but the page state did not change`;
|
|
289
|
+
tag('warning').log(`Page state did not change at ${check.freshState.url}`);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
lastFailure = `Reached ${check.freshState.url}, expected ${expectedUrl}`;
|
|
293
|
+
tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${check.freshState.url}`);
|
|
294
|
+
}
|
|
282
295
|
batchFailures.push({
|
|
283
296
|
code: codeBlock,
|
|
284
297
|
error: lastFailure,
|
|
285
298
|
ariaChanges: await this.ariaDiff(check.freshState, prevActionResult),
|
|
286
299
|
urlAfter: check.freshState.url,
|
|
287
300
|
});
|
|
288
|
-
tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${check.freshState.url}`);
|
|
289
301
|
}
|
|
290
302
|
if (freshHash !== prevHash && (attempt.ok || check.urlMatches)) {
|
|
291
303
|
progressBlocks.push(codeBlock);
|
|
@@ -411,6 +423,7 @@ class Navigator {
|
|
|
411
423
|
await action.exitIframe();
|
|
412
424
|
debugLog(`Attempting resolution: ${codeBlock}`);
|
|
413
425
|
const ok = await action.attempt(codeBlock, message);
|
|
426
|
+
this.executedSteps.push(...action.executedSteps);
|
|
414
427
|
const page = action.playwrightHelper?.page;
|
|
415
428
|
if (page) {
|
|
416
429
|
try {
|
|
@@ -437,9 +450,12 @@ class Navigator {
|
|
|
437
450
|
}
|
|
438
451
|
}
|
|
439
452
|
const freshState = await this.explorer.capture();
|
|
440
|
-
const urlMatches = this.
|
|
453
|
+
const urlMatches = this.targetUrlReached(action, expectedUrl, freshState);
|
|
441
454
|
return { freshState, urlMatches };
|
|
442
455
|
}
|
|
456
|
+
targetUrlReached(action, expectedUrl, state) {
|
|
457
|
+
return this.isSameExpectedOrigin(expectedUrl, action.stateManager) && matchesNavigationUrl(expectedUrl, this.comparableUrl(state, expectedUrl));
|
|
458
|
+
}
|
|
443
459
|
async ariaDiff(freshState, previous) {
|
|
444
460
|
if (freshState.getStateHash() === previous.getStateHash())
|
|
445
461
|
return null;
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ActionResult } from "../action-result.js";
|
|
|
5
5
|
import { ConfigParser } from "../config.js";
|
|
6
6
|
import { Stats } from "../stats.js";
|
|
7
7
|
import { TestResult } from "../test-plan.js";
|
|
8
|
-
import { collectInteractiveNodes
|
|
8
|
+
import { collectInteractiveNodes } from "../utils/aria.js";
|
|
9
9
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
const debugLog = createDebug('explorbot:pilot');
|
|
@@ -16,6 +16,7 @@ import { withdrawVisionTools } from "./tools.js";
|
|
|
16
16
|
const CHECK_TOOLS = ['verify', 'see', 'research'];
|
|
17
17
|
const EVIDENCE_TOOLS = ['verify', 'see'];
|
|
18
18
|
const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
|
|
19
|
+
const PILOT_REASONING_LIMIT = 500;
|
|
19
20
|
const PILOT_MESSAGE_LIMIT = 2;
|
|
20
21
|
const PILOT_MESSAGE_MAX_LENGTH = 160;
|
|
21
22
|
export class Pilot {
|
|
@@ -738,7 +739,7 @@ export class Pilot {
|
|
|
738
739
|
lines.push(`h2: ${state.h2 || ''}`);
|
|
739
740
|
lines.push(`h3: ${state.h3 || ''}`);
|
|
740
741
|
lines.push(`h4: ${state.h4 || ''}`);
|
|
741
|
-
const focusArea =
|
|
742
|
+
const focusArea = state.overlay;
|
|
742
743
|
if (focusArea.detected) {
|
|
743
744
|
lines.push(`modal: ${focusArea.name || focusArea.type}`);
|
|
744
745
|
}
|
|
@@ -941,6 +942,12 @@ export class Pilot {
|
|
|
941
942
|
line += `\n result: ${resultMessage}`;
|
|
942
943
|
if (errorDetail && errorDetail !== resultMessage)
|
|
943
944
|
line += `\n error: ${errorDetail}`;
|
|
945
|
+
if (!t.wasSuccessful && t.reasoning) {
|
|
946
|
+
let rationale = t.reasoning;
|
|
947
|
+
if (rationale.length > PILOT_REASONING_LIMIT)
|
|
948
|
+
rationale = `...${rationale.slice(-PILOT_REASONING_LIMIT)}`;
|
|
949
|
+
line += `\n tester reasoned: ${rationale.replace(/\n+/g, ' ')}`;
|
|
950
|
+
}
|
|
944
951
|
const attempts = t.output?.attempts;
|
|
945
952
|
if (attempts && attempts.length > 1 && t.wasSuccessful) {
|
|
946
953
|
const failedBefore = attempts.filter((a) => !a.success);
|
|
@@ -7,6 +7,7 @@ declare class AiError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
export declare class ContextLengthError extends Error {
|
|
9
9
|
}
|
|
10
|
+
export declare function flushTelemetry(): Promise<void>;
|
|
10
11
|
export declare class Provider {
|
|
11
12
|
config: AIConfig;
|
|
12
13
|
telemetryEnabled: boolean;
|
|
@@ -17,6 +18,7 @@ export declare class Provider {
|
|
|
17
18
|
modelCallWaiters: (() => void)[];
|
|
18
19
|
constructor(config: AIConfig);
|
|
19
20
|
validateConnection(): Promise<void>;
|
|
21
|
+
stop(): Promise<void>;
|
|
20
22
|
getModelForAgent(agentName?: string): any;
|
|
21
23
|
getAgenticModel(agentName?: string): any;
|
|
22
24
|
getVisionModel(): any;
|