explorbot 0.2.4 → 0.3.0
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 +19 -7
- package/boat/api-tester/src/cli.ts +17 -0
- package/boat/doc-collector/src/cli.ts +14 -1
- package/boat/prima/README.md +96 -0
- package/boat/prima/package.json +14 -10
- package/boat/prima/src/cli.ts +29 -12
- package/boat/prima/src/envelope.ts +35 -13
- package/boat/prima/src/prima.ts +78 -45
- package/dist/bin/explorbot-cli.js +19 -7
- package/dist/boat/api-tester/src/cli.js +17 -0
- package/dist/boat/doc-collector/src/cli.js +14 -1
- package/dist/boat/prima/src/cli.js +26 -7
- package/dist/boat/prima/src/envelope.js +32 -8
- package/dist/boat/prima/src/prima.js +75 -43
- package/dist/models.json +4 -4
- package/dist/package.json +6 -2
- package/dist/src/action-result.d.ts +13 -0
- package/dist/src/action-result.js +46 -15
- package/dist/src/action.d.ts +5 -2
- package/dist/src/action.js +53 -18
- package/dist/src/ai/captain/web-mode.js +1 -2
- package/dist/src/ai/captain.d.ts +20 -0
- package/dist/src/ai/captain.js +10 -1
- package/dist/src/ai/driller.js +6 -2
- package/dist/src/ai/fisherman-tools.d.ts +40 -1
- package/dist/src/ai/fisherman-tools.js +39 -0
- package/dist/src/ai/fisherman.js +2 -1
- package/dist/src/ai/navigator.d.ts +28 -0
- package/dist/src/ai/navigator.js +223 -175
- package/dist/src/ai/pilot.d.ts +7 -4
- package/dist/src/ai/pilot.js +89 -30
- package/dist/src/ai/planner/subpages.js +2 -16
- package/dist/src/ai/planner.js +1 -1
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +28 -22
- package/dist/src/ai/researcher/cache.d.ts +10 -3
- package/dist/src/ai/researcher/cache.js +23 -10
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +21 -4
- package/dist/src/ai/researcher.js +6 -4
- package/dist/src/ai/rules.js +1 -5
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/tester.d.ts +6 -3
- package/dist/src/ai/tester.js +30 -35
- package/dist/src/ai/tools.d.ts +8 -5
- package/dist/src/ai/tools.js +83 -57
- package/dist/src/commands/config-command.d.ts +51 -0
- package/dist/src/commands/config-command.js +117 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +13 -20
- package/dist/src/config.d.ts +8 -1
- package/dist/src/config.js +43 -1
- package/dist/src/experience-tracker.d.ts +2 -0
- package/dist/src/experience-tracker.js +12 -0
- package/dist/src/explorbot.js +5 -2
- package/dist/src/playwright-recorder.js +6 -12
- package/dist/src/remote.d.ts +3 -2
- package/dist/src/remote.js +8 -2
- package/dist/src/state-manager.d.ts +1 -1
- package/dist/src/state-manager.js +3 -1
- package/dist/src/test-plan.d.ts +9 -0
- package/dist/src/test-plan.js +30 -0
- package/dist/src/utils/html-diff.d.ts +5 -0
- package/dist/src/utils/html-diff.js +65 -6
- package/dist/src/utils/logger.d.ts +1 -1
- package/dist/src/utils/logger.js +8 -0
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +32 -0
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +31 -2
- package/docs/basics/getting-started.md +33 -10
- package/docs/basics/providers.md +6 -4
- package/docs/contributing/npm-package.md +73 -4
- package/docs/index.json +2 -1
- package/docs/reference/commands.md +3 -0
- package/docs/reference/websocket.md +50 -0
- package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
- package/models.json +4 -4
- package/package.json +6 -2
- package/src/action-result.ts +61 -16
- package/src/action.ts +56 -18
- package/src/ai/captain/web-mode.ts +1 -2
- package/src/ai/captain.ts +9 -1
- package/src/ai/driller.ts +6 -2
- package/src/ai/fisherman-tools.ts +35 -0
- package/src/ai/fisherman.ts +2 -1
- package/src/ai/navigator.ts +238 -179
- package/src/ai/pilot.ts +104 -36
- package/src/ai/planner/subpages.ts +2 -13
- package/src/ai/planner.ts +1 -1
- package/src/ai/provider.ts +29 -21
- package/src/ai/researcher/cache.ts +29 -11
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +23 -5
- package/src/ai/researcher.ts +6 -4
- package/src/ai/rules.ts +1 -5
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/tester.ts +33 -34
- package/src/ai/tools.ts +88 -61
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +14 -20
- package/src/config.ts +47 -2
- package/src/experience-tracker.ts +13 -0
- package/src/explorbot.ts +4 -2
- package/src/playwright-recorder.ts +6 -11
- package/src/remote.ts +8 -2
- package/src/state-manager.ts +5 -2
- package/src/test-plan.ts +38 -0
- package/src/utils/html-diff.ts +72 -7
- package/src/utils/logger.ts +9 -1
- package/src/utils/strings.ts +36 -0
- package/src/utils/url-matcher.ts +27 -2
package/src/action-result.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ConfigParser, type HtmlConfig, outputPath } from './config.ts';
|
|
|
4
4
|
import type { Link, WebPageState } from './state-manager.ts';
|
|
5
5
|
import { LARGE_ARIA_CHANGE_THRESHOLD, compactAriaSnapshot, diffAriaSnapshots } from './utils/aria.ts';
|
|
6
6
|
import { TTLCache } from './utils/cache.ts';
|
|
7
|
-
import { type HtmlDiffPart, type HtmlDiffResult, htmlDiff } from './utils/html-diff.ts';
|
|
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
10
|
import { slugify } from './utils/strings.ts';
|
|
@@ -27,6 +27,7 @@ interface ActionResultData extends WebPageState {
|
|
|
27
27
|
h3?: string | undefined;
|
|
28
28
|
h4?: string | undefined;
|
|
29
29
|
browserLogs?: any[];
|
|
30
|
+
networkRequests?: NetworkCall[];
|
|
30
31
|
iframeSnapshots?: Array<{ src: string; html: string; id?: string }>;
|
|
31
32
|
ariaSnapshot?: string | null;
|
|
32
33
|
ariaSnapshotFile?: string;
|
|
@@ -41,6 +42,9 @@ export interface PageDiff {
|
|
|
41
42
|
currentUrl: string;
|
|
42
43
|
ariaChanges?: string | null;
|
|
43
44
|
ariaChangeCount?: number;
|
|
45
|
+
messages?: string[];
|
|
46
|
+
requests?: NetworkCall[];
|
|
47
|
+
consoleErrors?: string[];
|
|
44
48
|
htmlParts?: HtmlDiffPart[];
|
|
45
49
|
iframes?: string;
|
|
46
50
|
}
|
|
@@ -66,6 +70,7 @@ export class ActionResult implements ActionResultData {
|
|
|
66
70
|
public url = '';
|
|
67
71
|
public fullUrl: string | undefined = undefined;
|
|
68
72
|
public browserLogs: any[] = [];
|
|
73
|
+
public networkRequests: NetworkCall[] = [];
|
|
69
74
|
public iframeSnapshots: Array<{ src: string; html: string; id?: string }> = [];
|
|
70
75
|
public iframeURL: string | undefined = undefined;
|
|
71
76
|
readonly screenshotFile: string | undefined = undefined;
|
|
@@ -91,6 +96,7 @@ export class ActionResult implements ActionResultData {
|
|
|
91
96
|
this.httpStatus = data.httpStatus;
|
|
92
97
|
this.error = data.error ?? null;
|
|
93
98
|
this.browserLogs = data.browserLogs ?? [];
|
|
99
|
+
this.networkRequests = data.networkRequests ?? [];
|
|
94
100
|
this.iframeSnapshots = data.iframeSnapshots ?? [];
|
|
95
101
|
this.iframeURL = data.iframeURL;
|
|
96
102
|
this.notes = data.notes ?? [];
|
|
@@ -508,23 +514,30 @@ export class ActionResult implements ActionResultData {
|
|
|
508
514
|
return result;
|
|
509
515
|
}
|
|
510
516
|
|
|
511
|
-
const
|
|
517
|
+
const pageDiff: PageDiff = {
|
|
518
|
+
urlChanged: previousState ? !this.isSameUrl({ url: previousState.url }) : true,
|
|
519
|
+
currentUrl: this.url,
|
|
520
|
+
};
|
|
521
|
+
result.pageDiff = pageDiff;
|
|
512
522
|
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
523
|
+
if (this.networkRequests.length > 0) {
|
|
524
|
+
pageDiff.requests = this.networkRequests;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const consoleErrors = this.consoleErrors();
|
|
528
|
+
if (consoleErrors.length > 0) {
|
|
529
|
+
pageDiff.consoleErrors = consoleErrors;
|
|
519
530
|
}
|
|
520
531
|
|
|
532
|
+
if (!previousState) return result;
|
|
533
|
+
|
|
534
|
+
pageDiff.previousUrl = previousState.url;
|
|
535
|
+
|
|
521
536
|
const diff = await this.diff(previousState);
|
|
522
537
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
currentUrl: this.url,
|
|
527
|
-
};
|
|
538
|
+
if (diff.messages.length > 0) {
|
|
539
|
+
pageDiff.messages = diff.messages;
|
|
540
|
+
}
|
|
528
541
|
|
|
529
542
|
if (diff.ariaChanged) {
|
|
530
543
|
pageDiff.ariaChanges = diff.ariaChanged;
|
|
@@ -552,11 +565,28 @@ export class ActionResult implements ActionResultData {
|
|
|
552
565
|
}
|
|
553
566
|
}
|
|
554
567
|
|
|
555
|
-
result.pageDiff = pageDiff;
|
|
556
568
|
return result;
|
|
557
569
|
}
|
|
570
|
+
|
|
571
|
+
private consoleErrors(): string[] {
|
|
572
|
+
const errors: string[] = [];
|
|
573
|
+
|
|
574
|
+
for (const log of this.browserLogs) {
|
|
575
|
+
if ((log.type || log.level) !== 'error') continue;
|
|
576
|
+
const text = String(log.text || log.message || log).trim();
|
|
577
|
+
if (!text) continue;
|
|
578
|
+
if (errors.includes(text)) continue;
|
|
579
|
+
errors.push(text.slice(0, CONSOLE_ERROR_MAX_LENGTH));
|
|
580
|
+
if (errors.length === CONSOLE_ERROR_LIMIT) break;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return errors;
|
|
584
|
+
}
|
|
558
585
|
}
|
|
559
586
|
|
|
587
|
+
const CONSOLE_ERROR_MAX_LENGTH = 300;
|
|
588
|
+
const CONSOLE_ERROR_LIMIT = 3;
|
|
589
|
+
|
|
560
590
|
const HTML_PARTS_TOTAL_BUDGET = 8000;
|
|
561
591
|
const HTML_PARTS_COUNT_LIMIT = 8;
|
|
562
592
|
const HTML_PART_SUBTREE_BUDGET = 2000;
|
|
@@ -584,6 +614,7 @@ function collapseHtmlParts(parts: HtmlDiffPart[]): HtmlDiffPart[] {
|
|
|
584
614
|
|
|
585
615
|
export class Diff {
|
|
586
616
|
private _htmlDiffResult: HtmlDiffResult | null = null;
|
|
617
|
+
private _messages: string[] = [];
|
|
587
618
|
private _ariaDiffResult: string | null = null;
|
|
588
619
|
private _ariaChangeCount = 0;
|
|
589
620
|
private _isSameUrl: boolean;
|
|
@@ -636,19 +667,33 @@ export class Diff {
|
|
|
636
667
|
return this._htmlDiffResult;
|
|
637
668
|
}
|
|
638
669
|
|
|
670
|
+
get messages(): string[] {
|
|
671
|
+
return this._messages;
|
|
672
|
+
}
|
|
673
|
+
|
|
639
674
|
async calculate(): Promise<void> {
|
|
640
675
|
if (!this.previous) return;
|
|
641
676
|
|
|
642
|
-
if (this._isSameUrl) {
|
|
643
|
-
this.
|
|
677
|
+
if (!this._isSameUrl) {
|
|
678
|
+
this._messages = liveRegionMessages(this.previous.html, this.current.html);
|
|
679
|
+
return;
|
|
644
680
|
}
|
|
645
681
|
|
|
682
|
+
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
|
|
683
|
+
this._messages = this._htmlDiffResult.messages;
|
|
684
|
+
|
|
646
685
|
const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
|
|
647
686
|
this._ariaDiffResult = ariaDiff.text;
|
|
648
687
|
this._ariaChangeCount = ariaDiff.count;
|
|
649
688
|
}
|
|
650
689
|
}
|
|
651
690
|
|
|
691
|
+
export interface NetworkCall {
|
|
692
|
+
method: string;
|
|
693
|
+
path: string;
|
|
694
|
+
status: number;
|
|
695
|
+
}
|
|
696
|
+
|
|
652
697
|
export interface FocusedElement {
|
|
653
698
|
role: string;
|
|
654
699
|
name: string;
|
package/src/action.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { context, trace } from '@opentelemetry/api';
|
|
4
4
|
import { container, recorder } from 'codeceptjs';
|
|
5
5
|
import * as codeceptjs from 'codeceptjs';
|
|
6
|
-
import { ActionResult, type FocusedElement } from './action-result.js';
|
|
6
|
+
import { ActionResult, type FocusedElement, type NetworkCall } from './action-result.js';
|
|
7
7
|
import { clearActivity, setActivity } from './activity.ts';
|
|
8
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
9
9
|
import type { ExplorbotConfig } from './config.js';
|
|
@@ -21,6 +21,8 @@ const debugLog = createDebug('explorbot:action');
|
|
|
21
21
|
const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
|
|
22
22
|
const DEFAULT_ACTION_TIMEOUT = 3000;
|
|
23
23
|
const DEFAULT_PAGE_TIMEOUT = 3000;
|
|
24
|
+
const MAX_NETWORK_CALLS = 10;
|
|
25
|
+
const IMPORTANT_LOG_LEVELS = new Set(['info', 'error', 'warning', 'warn']);
|
|
24
26
|
|
|
25
27
|
class Action {
|
|
26
28
|
private actor: CodeceptJS.I;
|
|
@@ -38,6 +40,8 @@ class Action {
|
|
|
38
40
|
private recorder?: PlaywrightRecorder;
|
|
39
41
|
private recovery: RecoveryRunner;
|
|
40
42
|
private mainDocumentStatus: number | undefined = undefined;
|
|
43
|
+
private networkRequests: NetworkCall[] = [];
|
|
44
|
+
private baseOrigin: string;
|
|
41
45
|
|
|
42
46
|
constructor(actor: CodeceptJS.I, stateManager: StateManager, recorder?: PlaywrightRecorder, recovery?: RecoveryRunner) {
|
|
43
47
|
this.actor = actor;
|
|
@@ -46,6 +50,7 @@ class Action {
|
|
|
46
50
|
this.playwrightHelper = container.helpers('Playwright');
|
|
47
51
|
this.recorder = recorder;
|
|
48
52
|
this.recovery = recovery || ((fn) => fn());
|
|
53
|
+
this.baseOrigin = URL.parse(this.config.playwright?.url || '')?.origin || '';
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
async saveScreenshot(): Promise<string | undefined> {
|
|
@@ -57,6 +62,7 @@ class Action {
|
|
|
57
62
|
try {
|
|
58
63
|
await (this.actor as any).saveScreenshot(filename);
|
|
59
64
|
if (currentState) currentState.screenshotFile = filename;
|
|
65
|
+
tag('data').log('screenshot', { path: outputPath('states', filename) });
|
|
60
66
|
return filename;
|
|
61
67
|
} catch (err) {
|
|
62
68
|
debugLog('Screenshot failed:', err);
|
|
@@ -105,7 +111,10 @@ class Action {
|
|
|
105
111
|
const screenshotPath = join(statesDir, filename);
|
|
106
112
|
screenshotFile = await page
|
|
107
113
|
?.screenshot({ path: screenshotPath, fullPage: true })
|
|
108
|
-
.then(() =>
|
|
114
|
+
.then(() => {
|
|
115
|
+
tag('data').log('screenshot', { path: screenshotPath });
|
|
116
|
+
return filename;
|
|
117
|
+
})
|
|
109
118
|
.catch((err: Error) => {
|
|
110
119
|
debugLog('Screenshot failed, continuing without it:', err);
|
|
111
120
|
return undefined;
|
|
@@ -123,9 +132,7 @@ class Action {
|
|
|
123
132
|
const logPath = join(statesDir, logFile);
|
|
124
133
|
const formattedLogs = browserLogs.map((log: any) => {
|
|
125
134
|
const logTimestamp = new Date().toISOString();
|
|
126
|
-
|
|
127
|
-
const message = log.text || log.message || String(log);
|
|
128
|
-
return `[${logTimestamp}] ${level}: ${message}`;
|
|
135
|
+
return `[${logTimestamp}] ${log.type.toUpperCase()}: ${log.text}`;
|
|
129
136
|
});
|
|
130
137
|
fs.writeFileSync(logPath, `${formattedLogs.join('\n')}\n`, 'utf8');
|
|
131
138
|
|
|
@@ -153,12 +160,16 @@ class Action {
|
|
|
153
160
|
ariaSnapshotFile = ariaFileName;
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
const networkRequests = this.networkRequests;
|
|
164
|
+
this.networkRequests = [];
|
|
165
|
+
|
|
156
166
|
const result = new ActionResult({
|
|
157
167
|
html,
|
|
158
168
|
title,
|
|
159
169
|
httpStatus: await this.captureMainDocumentStatus(),
|
|
160
170
|
url,
|
|
161
171
|
browserLogs,
|
|
172
|
+
networkRequests,
|
|
162
173
|
htmlFile,
|
|
163
174
|
logFile,
|
|
164
175
|
screenshotFile,
|
|
@@ -198,26 +209,53 @@ class Action {
|
|
|
198
209
|
}
|
|
199
210
|
}
|
|
200
211
|
|
|
201
|
-
private
|
|
212
|
+
private captureResponses(): () => void {
|
|
202
213
|
const page = this.playwrightHelper.page;
|
|
203
214
|
if (!page?.on || !page?.off) return () => {};
|
|
204
215
|
|
|
205
216
|
this.mainDocumentStatus = undefined;
|
|
217
|
+
this.networkRequests = [];
|
|
206
218
|
|
|
207
219
|
const handler = (response: any) => {
|
|
208
220
|
const request = response.request();
|
|
209
|
-
if (request.resourceType() !== 'document') return;
|
|
210
|
-
if (response.frame() !== page.mainFrame()) return;
|
|
211
221
|
const status = response.status();
|
|
212
222
|
if (typeof status !== 'number') return;
|
|
213
223
|
if (status <= 0) return;
|
|
214
|
-
|
|
224
|
+
|
|
225
|
+
if (request.resourceType() === 'document') {
|
|
226
|
+
if (response.frame() !== page.mainFrame()) return;
|
|
227
|
+
this.mainDocumentStatus = status;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.recordNetworkCall(request, status);
|
|
215
232
|
};
|
|
216
233
|
|
|
217
234
|
page.on('response', handler);
|
|
218
235
|
return () => page.off('response', handler);
|
|
219
236
|
}
|
|
220
237
|
|
|
238
|
+
private recordNetworkCall(request: any, status: number): void {
|
|
239
|
+
const resourceType = request.resourceType();
|
|
240
|
+
if (resourceType !== 'xhr' && resourceType !== 'fetch') return;
|
|
241
|
+
|
|
242
|
+
const url = URL.parse(request.url());
|
|
243
|
+
if (!url) return;
|
|
244
|
+
if (url.origin !== this.baseOrigin) return;
|
|
245
|
+
|
|
246
|
+
const call: NetworkCall = { method: request.method(), path: url.pathname, status };
|
|
247
|
+
if (this.networkRequests.some((r) => r.method === call.method && r.path === call.path && r.status === call.status)) return;
|
|
248
|
+
|
|
249
|
+
if (this.networkRequests.length >= MAX_NETWORK_CALLS) {
|
|
250
|
+
if (status < 400) return;
|
|
251
|
+
const succeeded = this.networkRequests.findIndex((r) => r.status < 400);
|
|
252
|
+
if (succeeded === -1) return;
|
|
253
|
+
this.networkRequests.splice(succeeded, 1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.networkRequests.push(call);
|
|
257
|
+
}
|
|
258
|
+
|
|
221
259
|
/**
|
|
222
260
|
* Capture HTML snapshots of all iframes on the page
|
|
223
261
|
*/
|
|
@@ -263,13 +301,7 @@ class Action {
|
|
|
263
301
|
try {
|
|
264
302
|
const logs = await (this.actor as any).grabBrowserLogs();
|
|
265
303
|
|
|
266
|
-
|
|
267
|
-
const importantLogs = logs.filter((log: any) => {
|
|
268
|
-
const level = log.type || log.level;
|
|
269
|
-
return ['info', 'error', 'warning', 'warn'].includes(level);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
return importantLogs;
|
|
304
|
+
return logs.map(toBrowserLog).filter((log: any) => IMPORTANT_LOG_LEVELS.has(log.type));
|
|
273
305
|
} catch (error) {
|
|
274
306
|
debugLog('Failed to capture browser logs:', error);
|
|
275
307
|
return [];
|
|
@@ -288,7 +320,7 @@ class Action {
|
|
|
288
320
|
const stepListener = attachStepLogger(executedSteps, assertionSteps);
|
|
289
321
|
const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
|
|
290
322
|
this.playwrightGroupId = groupId;
|
|
291
|
-
const
|
|
323
|
+
const detachResponses = this.captureResponses();
|
|
292
324
|
const activeSpan = Observability.getSpan();
|
|
293
325
|
const tracer = trace.getTracer('ai');
|
|
294
326
|
const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
|
|
@@ -337,7 +369,7 @@ class Action {
|
|
|
337
369
|
throw err;
|
|
338
370
|
} finally {
|
|
339
371
|
this.restorePageTimeout();
|
|
340
|
-
|
|
372
|
+
detachResponses();
|
|
341
373
|
if (groupId) await this.recorder!.endAction();
|
|
342
374
|
detachStepLogger(stepListener);
|
|
343
375
|
if (stepSpan) {
|
|
@@ -425,6 +457,12 @@ async function captureHtml(page: any, frame: any, actor: any): Promise<string> {
|
|
|
425
457
|
throw new Error('Playwright page is unavailable for HTML capture');
|
|
426
458
|
}
|
|
427
459
|
|
|
460
|
+
function toBrowserLog(log: any): { type: string; text: string } {
|
|
461
|
+
const type = typeof log.type === 'function' ? log.type() : log.type || log.level || 'log';
|
|
462
|
+
const text = typeof log.text === 'function' ? log.text() : log.text || log.message || String(log);
|
|
463
|
+
return { type, text: text.replace(/\s+/g, ' ').trim() };
|
|
464
|
+
}
|
|
465
|
+
|
|
428
466
|
async function captureTitle(page: any, actor: any): Promise<string> {
|
|
429
467
|
if (page?.title) return page.title();
|
|
430
468
|
if (actor?.grabTitle) return actor.grabTitle();
|
|
@@ -16,7 +16,7 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
16
16
|
researcher: ctx.explorBot.agentResearcher(),
|
|
17
17
|
navigator: ctx.explorBot.agentNavigator(),
|
|
18
18
|
});
|
|
19
|
-
const { see, context, visualClick
|
|
19
|
+
const { see, context, visualClick } = agentTools;
|
|
20
20
|
|
|
21
21
|
const tools: Record<string, any> = {
|
|
22
22
|
navigate: tool({
|
|
@@ -124,7 +124,6 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
124
124
|
|
|
125
125
|
...codeceptTools,
|
|
126
126
|
context,
|
|
127
|
-
learnExperience,
|
|
128
127
|
};
|
|
129
128
|
|
|
130
129
|
if (see) tools.see = see;
|
package/src/ai/captain.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type { Navigator } from './navigator.ts';
|
|
|
21
21
|
import type { Provider } from './provider.ts';
|
|
22
22
|
import { Researcher } from './researcher.ts';
|
|
23
23
|
import { TaskAgent } from './task-agent.ts';
|
|
24
|
-
import { withdrawVisionTools } from './tools.ts';
|
|
24
|
+
import { createLearnExperienceTool, withdrawVisionTools } from './tools.ts';
|
|
25
25
|
|
|
26
26
|
const MAX_STEPS = 15;
|
|
27
27
|
|
|
@@ -241,6 +241,14 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
241
241
|
|
|
242
242
|
private coreTools(task: Task, onDone: (summary: string) => void) {
|
|
243
243
|
return {
|
|
244
|
+
learnExperience: createLearnExperienceTool({
|
|
245
|
+
getExperienceTracker: () => this.getExperienceTracker(),
|
|
246
|
+
getState: () => {
|
|
247
|
+
const state = this.explorBot.stateManager().getCurrentState();
|
|
248
|
+
if (!state) return null;
|
|
249
|
+
return ActionResult.fromState(state);
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
244
252
|
done: tool({
|
|
245
253
|
description: 'Call when the user request is fulfilled.',
|
|
246
254
|
inputSchema: z.object({
|
package/src/ai/driller.ts
CHANGED
|
@@ -30,7 +30,7 @@ import type { Navigator } from './navigator.ts';
|
|
|
30
30
|
import type { Provider } from './provider.ts';
|
|
31
31
|
import { drillLocatorRule } from './rules.ts';
|
|
32
32
|
import { TaskAgent, isInteractive } from './task-agent.ts';
|
|
33
|
-
import { createCodeceptJSTools } from './tools.ts';
|
|
33
|
+
import { createCodeceptJSTools, createLearnExperienceTool } from './tools.ts';
|
|
34
34
|
|
|
35
35
|
const debugLog = createDebug('explorbot:driller');
|
|
36
36
|
|
|
@@ -306,7 +306,11 @@ export class Driller extends TaskAgent implements Agent {
|
|
|
306
306
|
|
|
307
307
|
let finished = false;
|
|
308
308
|
const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.toolDeps, test), component);
|
|
309
|
-
const
|
|
309
|
+
const learnExperience = createLearnExperienceTool({
|
|
310
|
+
getExperienceTracker: () => this.getExperienceTracker(),
|
|
311
|
+
getState: () => ActionResult.fromState(this.stateManager.getCurrentState() || originalState),
|
|
312
|
+
});
|
|
313
|
+
const tools = { ...actionTools, learnExperience, ...this.createDrillFlowTools(originalState, test, interactive) };
|
|
310
314
|
|
|
311
315
|
await loop(
|
|
312
316
|
async ({ stop, iteration }) => {
|
|
@@ -29,6 +29,28 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request
|
|
|
29
29
|
|
|
30
30
|
const captured = requestStore.findCapturedRequest(method, path);
|
|
31
31
|
if (captured) {
|
|
32
|
+
if (captured.status >= 400) {
|
|
33
|
+
const rejectedCapture = {
|
|
34
|
+
status: captured.status,
|
|
35
|
+
requestBody: captured.requestBody || 'no body',
|
|
36
|
+
};
|
|
37
|
+
if (opts.spec) {
|
|
38
|
+
try {
|
|
39
|
+
const definition = extractEndpointDefinition(opts.spec, path, opts.baseEndpoint);
|
|
40
|
+
return { source: 'spec', method, path, definition, rejectedCapture };
|
|
41
|
+
} catch {
|
|
42
|
+
return { source: 'captured', method, path, usable: false, rejectedRequestBody: captured.requestBody || 'no body', status: captured.status };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
source: 'captured',
|
|
47
|
+
method: captured.method,
|
|
48
|
+
path: captured.path,
|
|
49
|
+
status: captured.status,
|
|
50
|
+
usable: false,
|
|
51
|
+
rejectedRequestBody: captured.requestBody || 'no body',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
32
54
|
return {
|
|
33
55
|
source: 'captured',
|
|
34
56
|
method: captured.method,
|
|
@@ -87,6 +109,7 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request
|
|
|
87
109
|
success: false,
|
|
88
110
|
status: reqResult.status,
|
|
89
111
|
statusText: reqResult.statusText,
|
|
112
|
+
category: responseCategory(reqResult.status),
|
|
90
113
|
errorPreview: reqResult.rawResponseBody.substring(0, 300),
|
|
91
114
|
};
|
|
92
115
|
}
|
|
@@ -149,6 +172,16 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request
|
|
|
149
172
|
return { tools, getResult, isFinished };
|
|
150
173
|
}
|
|
151
174
|
|
|
175
|
+
function responseCategory(status: number): ResponseCategory {
|
|
176
|
+
if (status === 400 || status === 422) return 'validation';
|
|
177
|
+
if (status === 401 || status === 403) return 'authorization';
|
|
178
|
+
if (status === 404) return 'not_found';
|
|
179
|
+
if (status === 409) return 'conflict';
|
|
180
|
+
if (status === 408 || status === 425 || status === 429) return 'temporary';
|
|
181
|
+
if (status >= 500) return 'server';
|
|
182
|
+
return 'client';
|
|
183
|
+
}
|
|
184
|
+
|
|
152
185
|
function extractKeyFields(body: any, result: Record<string, any> = {}, depth = 0): Record<string, any> {
|
|
153
186
|
if (!body || typeof body !== 'object' || depth > 5) return result;
|
|
154
187
|
|
|
@@ -179,3 +212,5 @@ export interface FishermanResult {
|
|
|
179
212
|
created: Array<{ type: string; id?: string | number; title?: string }>;
|
|
180
213
|
failed: Array<{ type: string; reason: string }>;
|
|
181
214
|
}
|
|
215
|
+
|
|
216
|
+
type ResponseCategory = 'validation' | 'authorization' | 'not_found' | 'conflict' | 'temporary' | 'server' | 'client';
|
package/src/ai/fisherman.ts
CHANGED
|
@@ -208,7 +208,8 @@ export class Fisherman implements Agent {
|
|
|
208
208
|
RULES:
|
|
209
209
|
- Always call getEndpointSpec before your first request to an unfamiliar endpoint
|
|
210
210
|
- Chain requests logically — create parent resources before children
|
|
211
|
-
-
|
|
211
|
+
- Use the response category and error text to decide what failed: validation requires corrected data, authorization requires valid access, not_found requires a valid path or parent, and conflict requires resolving the conflicting state
|
|
212
|
+
- Retry temporary or server failures once. Retry other failures only when the specification or error text gives a concrete correction
|
|
212
213
|
- Use realistic but unique data for each item (vary names, titles)
|
|
213
214
|
|
|
214
215
|
${dataProtectionRules}
|