explorbot 0.1.32-beta.1 → 0.2.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/README.md +14 -14
- package/bin/explorbot-cli.ts +4 -3
- package/boat/doc-collector/src/ai/documentarian.ts +22 -27
- package/boat/doc-collector/src/ai/tools.ts +67 -10
- package/boat/doc-collector/src/docbot.ts +28 -7
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +33 -1
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
- package/dist/boat/doc-collector/src/ai/tools.js +47 -10
- package/dist/boat/doc-collector/src/docbot.js +25 -6
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +29 -0
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/package.json +20 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +3 -10
- package/dist/src/action.js +18 -111
- package/dist/src/ai/captain/web-mode.js +0 -7
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +4 -19
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +0 -4
- package/dist/src/ai/driller.js +3 -12
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +3 -2
- package/dist/src/ai/historian/playwright.d.ts +2 -0
- package/dist/src/ai/historian/playwright.js +3 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +5 -4
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +3 -5
- package/dist/src/ai/navigator.js +11 -46
- package/dist/src/ai/pilot.d.ts +1 -3
- package/dist/src/ai/pilot.js +6 -17
- package/dist/src/ai/planner.d.ts +3 -3
- package/dist/src/ai/planner.js +49 -37
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -136
- package/dist/src/ai/quartermaster.d.ts +21 -22
- package/dist/src/ai/quartermaster.js +4 -10
- package/dist/src/ai/rerunner.d.ts +1 -2
- package/dist/src/ai/rerunner.js +14 -22
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/deep-analysis.js +3 -6
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.js +5 -1
- package/dist/src/ai/researcher.js +12 -53
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +1 -1
- package/dist/src/ai/task-agent.js +2 -27
- package/dist/src/ai/tester.d.ts +0 -2
- package/dist/src/ai/tester.js +10 -61
- package/dist/src/ai/tools.d.ts +18 -7
- package/dist/src/ai/tools.js +119 -101
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-command.js +1 -1
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +3 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +2 -2
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +81 -154
- package/dist/src/explorbot.d.ts +4 -5
- package/dist/src/explorbot.js +17 -51
- package/dist/src/explorer.d.ts +4 -15
- package/dist/src/explorer.js +18 -68
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +6 -31
- package/dist/src/state-manager.js +10 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- package/dist/src/utils/web-element.d.ts +2 -1
- package/dist/src/utils/web-element.js +3 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/package.json +20 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +16 -130
- package/src/ai/captain/web-mode.ts +0 -7
- package/src/ai/captain.ts +5 -17
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +3 -16
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +4 -2
- package/src/ai/historian/playwright.ts +4 -2
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +4 -7
- package/src/ai/navigator.ts +13 -49
- package/src/ai/pilot.ts +6 -16
- package/src/ai/planner.ts +56 -44
- package/src/ai/provider.ts +85 -145
- package/src/ai/quartermaster.ts +34 -41
- package/src/ai/rerunner.ts +17 -21
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/deep-analysis.ts +3 -6
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +5 -1
- package/src/ai/researcher.ts +11 -59
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +3 -30
- package/src/ai/tester.ts +12 -73
- package/src/ai/tools.ts +119 -106
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-command.ts +1 -1
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +3 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +2 -2
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +88 -151
- package/src/explorbot.ts +17 -51
- package/src/explorer.ts +20 -72
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +15 -156
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- package/src/utils/web-element.ts +4 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
package/src/action.ts
CHANGED
|
@@ -1,30 +1,21 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { faker } from '@faker-js/faker';
|
|
4
3
|
import { context, trace } from '@opentelemetry/api';
|
|
5
4
|
import { container, recorder } from 'codeceptjs';
|
|
6
5
|
import * as codeceptjs from 'codeceptjs';
|
|
7
|
-
import { hopeThat, retryTo, tryTo, within } from 'codeceptjs/lib/effects';
|
|
8
|
-
import step from 'codeceptjs/steps';
|
|
9
|
-
import dedent from 'dedent';
|
|
10
6
|
import { ActionResult } from './action-result.js';
|
|
11
7
|
import { clearActivity, setActivity } from './activity.ts';
|
|
12
|
-
import { ExperienceCompactor } from './ai/experience-compactor.js';
|
|
13
|
-
import { Navigator } from './ai/navigator.js';
|
|
14
|
-
import type { Provider } from './ai/provider.js';
|
|
15
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
16
9
|
import type { ExplorbotConfig } from './config.js';
|
|
17
|
-
import type { UserResolveFunction } from './explorbot.ts';
|
|
18
10
|
import { Observability } from './observability.ts';
|
|
19
11
|
import type { PlaywrightRecorder } from './playwright-recorder.ts';
|
|
20
12
|
import type { StateManager } from './state-manager.js';
|
|
21
|
-
import { isFatalBrowserError, isNavigationTransitionError } from './utils/browser-errors.ts';
|
|
22
|
-
import {
|
|
23
|
-
import { htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
13
|
+
import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from './utils/browser-errors.ts';
|
|
14
|
+
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
24
15
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
25
|
-
import { waitForPageReadiness } from './utils/page-readiness.ts';
|
|
16
|
+
import { sleep, waitForPageReadiness } from './utils/page-readiness.ts';
|
|
17
|
+
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from './utils/web-sandbox.ts';
|
|
26
18
|
import { safeFilename } from './utils/strings.ts';
|
|
27
|
-
import { throttle } from './utils/throttle.ts';
|
|
28
19
|
|
|
29
20
|
const debugLog = createDebug('explorbot:action');
|
|
30
21
|
const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
|
|
@@ -37,7 +28,6 @@ class Action {
|
|
|
37
28
|
|
|
38
29
|
// action info
|
|
39
30
|
private action: string | null = null;
|
|
40
|
-
private expectation: string | null = null;
|
|
41
31
|
public lastError: Error | null = null;
|
|
42
32
|
public playwrightHelper: any;
|
|
43
33
|
public playwrightGroupId: string | null = null;
|
|
@@ -53,10 +43,6 @@ class Action {
|
|
|
53
43
|
this.recorder = recorder;
|
|
54
44
|
}
|
|
55
45
|
|
|
56
|
-
async caputrePageWithScreenshot(): Promise<ActionResult> {
|
|
57
|
-
return this.capturePageState({ includeScreenshot: true });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
46
|
async saveScreenshot(): Promise<string | undefined> {
|
|
61
47
|
const currentState = this.stateManager.getCurrentState();
|
|
62
48
|
if (currentState?.screenshotFile) return currentState.screenshotFile;
|
|
@@ -73,7 +59,7 @@ class Action {
|
|
|
73
59
|
}
|
|
74
60
|
}
|
|
75
61
|
|
|
76
|
-
async capturePageState({ includeScreenshot = false }: { includeScreenshot?: boolean } = {}): Promise<ActionResult> {
|
|
62
|
+
async capturePageState({ includeScreenshot = false, codeBlock }: { includeScreenshot?: boolean; codeBlock?: string } = {}): Promise<ActionResult> {
|
|
77
63
|
try {
|
|
78
64
|
const currentState = this.stateManager.getCurrentState();
|
|
79
65
|
const stateHash = currentState?.hash || 'screenshot';
|
|
@@ -166,10 +152,10 @@ class Action {
|
|
|
166
152
|
ariaSnapshotFile,
|
|
167
153
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
168
154
|
});
|
|
169
|
-
this.stateManager.updateState(result);
|
|
155
|
+
this.stateManager.updateState(result, codeBlock);
|
|
170
156
|
return result;
|
|
171
157
|
} catch (err) {
|
|
172
|
-
const msg =
|
|
158
|
+
const msg = browserErrorMessage(err);
|
|
173
159
|
if (isFatalBrowserError(err)) throw err;
|
|
174
160
|
debugLog('capturePageState failed with non-fatal error:', msg);
|
|
175
161
|
const url = this.playwrightHelper.page?.url?.() || '';
|
|
@@ -303,22 +289,19 @@ class Action {
|
|
|
303
289
|
|
|
304
290
|
if (isPlaywright) {
|
|
305
291
|
const page = this.playwrightHelper.page;
|
|
306
|
-
|
|
307
|
-
await asyncFn(page);
|
|
292
|
+
await playwrightSandbox(page, sanitizedCode);
|
|
308
293
|
await sleep(this.config.action?.delay || 500);
|
|
309
294
|
} else {
|
|
310
|
-
|
|
311
|
-
codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
295
|
+
codeceptJSSandbox(this.actor, sanitizedCode);
|
|
312
296
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
313
297
|
await recorder.promise();
|
|
314
298
|
}
|
|
315
299
|
|
|
316
|
-
const pageState = await this.capturePageState();
|
|
317
300
|
if (executedSteps.length > 0) {
|
|
318
301
|
codeString = executedSteps.join('\n');
|
|
319
302
|
}
|
|
320
303
|
|
|
321
|
-
this.
|
|
304
|
+
const pageState = await this.capturePageState({ codeBlock: codeString });
|
|
322
305
|
|
|
323
306
|
this.actionResult = pageState;
|
|
324
307
|
this.assertionSteps = assertionSteps;
|
|
@@ -345,66 +328,7 @@ class Action {
|
|
|
345
328
|
return this;
|
|
346
329
|
}
|
|
347
330
|
|
|
348
|
-
async
|
|
349
|
-
const codeString = typeof codeOrFunction === 'string' ? codeOrFunction : codeOrFunction.toString();
|
|
350
|
-
this.expectation = codeString.toString();
|
|
351
|
-
const expectationPreview = sanitizeCodeBlock(codeString)
|
|
352
|
-
.split('\n')
|
|
353
|
-
.map((line) => line.trim())
|
|
354
|
-
.filter(Boolean)
|
|
355
|
-
.slice(0, 2)
|
|
356
|
-
.join(' ');
|
|
357
|
-
tag('step').log(`Expecting: ${expectationPreview || 'assertion'}`);
|
|
358
|
-
try {
|
|
359
|
-
debugLog('Executing expectation:', codeString);
|
|
360
|
-
|
|
361
|
-
let codeFunction: any;
|
|
362
|
-
if (typeof codeOrFunction === 'function') {
|
|
363
|
-
codeFunction = codeOrFunction;
|
|
364
|
-
} else {
|
|
365
|
-
const sanitizedCode = sanitizeCodeBlock(codeString);
|
|
366
|
-
if (!sanitizedCode) {
|
|
367
|
-
throw new Error('No valid I.* commands found in code block');
|
|
368
|
-
}
|
|
369
|
-
codeFunction = createSandboxedFunction(['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step'], sanitizedCode);
|
|
370
|
-
}
|
|
371
|
-
codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step);
|
|
372
|
-
await recorder.promise();
|
|
373
|
-
debugLog('Expectation executed successfully');
|
|
374
|
-
|
|
375
|
-
// Get current state from state manager
|
|
376
|
-
const currentState = this.stateManager.getCurrentState();
|
|
377
|
-
if (currentState) {
|
|
378
|
-
// Create ActionResult from current state for compatibility
|
|
379
|
-
this.actionResult = new ActionResult({
|
|
380
|
-
url: currentState.fullUrl || '',
|
|
381
|
-
title: currentState.title,
|
|
382
|
-
timestamp: currentState.timestamp,
|
|
383
|
-
html: '', // Empty HTML for expectation state
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return this;
|
|
388
|
-
} catch (err) {
|
|
389
|
-
tag('error').log('Expectation failed:', errorToString(err));
|
|
390
|
-
this.lastError = err as Error;
|
|
391
|
-
await recorder.reset();
|
|
392
|
-
await recorder.start();
|
|
393
|
-
debugLog('Expectation failed:', errorToString(err));
|
|
394
|
-
} finally {
|
|
395
|
-
clearActivity();
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return this;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
public async waitForInteraction(): Promise<Action> {
|
|
402
|
-
// start with basic approach
|
|
403
|
-
await this.actor.wait(0.5);
|
|
404
|
-
return this;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
public async attempt(codeBlock: string, originalMessage?: string, experience = true): Promise<boolean> {
|
|
331
|
+
public async attempt(codeBlock: string, originalMessage?: string): Promise<boolean> {
|
|
408
332
|
try {
|
|
409
333
|
debugLog('Resolution attempt...');
|
|
410
334
|
setActivity('🦾 Acting in browser...', 'action');
|
|
@@ -412,19 +336,9 @@ class Action {
|
|
|
412
336
|
if (!this.actionResult) {
|
|
413
337
|
this.actionResult = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
414
338
|
}
|
|
415
|
-
const prevActionResult = this.actionResult;
|
|
416
339
|
this.lastError = null;
|
|
417
340
|
await this.execute(codeBlock);
|
|
418
341
|
|
|
419
|
-
if (!this.expectation && originalMessage) {
|
|
420
|
-
this.expectation = originalMessage;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (!this.expectation) {
|
|
424
|
-
return true;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
debugLog('Resolved Expectation:', this.expectation);
|
|
428
342
|
return true;
|
|
429
343
|
} catch (error) {
|
|
430
344
|
this.lastError = error as Error;
|
|
@@ -438,14 +352,6 @@ class Action {
|
|
|
438
352
|
return this.actor;
|
|
439
353
|
}
|
|
440
354
|
|
|
441
|
-
setActor(actor: CodeceptJS.I): void {
|
|
442
|
-
this.actor = actor;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
getCurrentState(): ActionResult | null {
|
|
446
|
-
return this.actionResult;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
355
|
getActionResult(): ActionResult | null {
|
|
450
356
|
return this.actionResult;
|
|
451
357
|
}
|
|
@@ -468,6 +374,11 @@ function errorToString(error: any): string {
|
|
|
468
374
|
}
|
|
469
375
|
|
|
470
376
|
async function captureHtml(page: any, frame: any, actor: any): Promise<string> {
|
|
377
|
+
for (const scope of [frame, page]) {
|
|
378
|
+
if (!scope?.evaluate) continue;
|
|
379
|
+
const html = await scope.evaluate(captureHtmlForSnapshot).catch(() => null);
|
|
380
|
+
if (typeof html === 'string') return html;
|
|
381
|
+
}
|
|
471
382
|
if (frame?.content) return frame.content();
|
|
472
383
|
if (page?.content) return page.content();
|
|
473
384
|
if (actor?.grabSource) return actor.grabSource();
|
|
@@ -480,31 +391,6 @@ async function captureTitle(page: any, actor: any): Promise<string> {
|
|
|
480
391
|
return '';
|
|
481
392
|
}
|
|
482
393
|
|
|
483
|
-
const SHADOWED_GLOBALS = ['process', 'global', 'globalThis', 'fetch', 'Bun', 'require', 'module', 'exports'];
|
|
484
|
-
|
|
485
|
-
export function sanitizeCodeBlock(code: string): string {
|
|
486
|
-
return code
|
|
487
|
-
.split('\n')
|
|
488
|
-
.map((line) => line.trim())
|
|
489
|
-
.filter((line) => line.startsWith('I.') || line.startsWith('page.') || line.startsWith('await page.') || line.startsWith('await I.'))
|
|
490
|
-
.join('\n');
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export function createSandboxedFunction(argNames: string[], body: string): (...args: any[]) => any {
|
|
494
|
-
const fn = new Function(...argNames, ...SHADOWED_GLOBALS, `'use strict';\n${body}`);
|
|
495
|
-
return (...args: any[]) => fn(...args);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function hasPlaywrightCommands(code: string): boolean {
|
|
499
|
-
return code.split('\n').some((line) => {
|
|
500
|
-
const trimmed = line.trim();
|
|
501
|
-
return trimmed.startsWith('page.') || trimmed.startsWith('await page.');
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
function sleep(ms: number) {
|
|
505
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
506
|
-
}
|
|
507
|
-
|
|
508
394
|
const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
|
|
509
395
|
|
|
510
396
|
type StepListener = (step: any, error?: any) => void;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { tool } from 'ai';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { ActionResult } from '../../action-result.ts';
|
|
5
4
|
import { actionRule, locatorRule, sectionContextRule } from '../rules.ts';
|
|
6
5
|
import { createAgentTools, createCodeceptJSTools } from '../tools.ts';
|
|
7
6
|
import { type Constructor, type ModeContext, debugLog } from './mixin.ts';
|
|
@@ -10,17 +9,11 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
10
9
|
return class extends Base {
|
|
11
10
|
webModeTools(ctx: ModeContext): Record<string, any> {
|
|
12
11
|
const explorer = ctx.explorBot.getExplorer();
|
|
13
|
-
const stateManager = explorer.getStateManager();
|
|
14
12
|
const codeceptTools = createCodeceptJSTools(explorer, ctx.task);
|
|
15
13
|
const agentTools = createAgentTools({
|
|
16
14
|
explorer,
|
|
17
15
|
researcher: ctx.explorBot.agentResearcher(),
|
|
18
16
|
navigator: ctx.explorBot.agentNavigator(),
|
|
19
|
-
experienceTracker: stateManager.getExperienceTracker(),
|
|
20
|
-
getState: () => {
|
|
21
|
-
const state = stateManager.getCurrentState();
|
|
22
|
-
return state ? ActionResult.fromState(state) : null;
|
|
23
|
-
},
|
|
24
17
|
});
|
|
25
18
|
const { see, context, visualClick, learnExperience } = agentTools;
|
|
26
19
|
|
package/src/ai/captain.ts
CHANGED
|
@@ -2,10 +2,11 @@ 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 { ExperienceTracker } from '../experience-tracker.js';
|
|
5
|
+
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
6
6
|
import type { ExplorBot } from '../explorbot.ts';
|
|
7
7
|
import type { WebPageState } from '../state-manager.ts';
|
|
8
8
|
import { Task, Test } from '../test-plan.ts';
|
|
9
|
+
import { formatHeadings } from '../utils/context-formatter.ts';
|
|
9
10
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
10
11
|
import { startLogCapture, stopLogCapture, tag } from '../utils/logger.js';
|
|
11
12
|
import { loop } from '../utils/loop.js';
|
|
@@ -37,7 +38,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
37
38
|
constructor(explorBot: ExplorBot) {
|
|
38
39
|
super();
|
|
39
40
|
this.explorBot = explorBot;
|
|
40
|
-
this.experienceTracker =
|
|
41
|
+
this.experienceTracker = explorBot.experienceTracker();
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
setCommandExecutor(fn: (cmd: string) => Promise<void>, descriptions: { name: string; description: string; options: string }[]): void {
|
|
@@ -62,7 +63,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
protected getKnowledgeTracker() {
|
|
65
|
-
return this.explorBot.
|
|
66
|
+
return this.explorBot.knowledgeTracker();
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
protected getProvider(): Provider {
|
|
@@ -145,15 +146,6 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
145
146
|
return this.conversation;
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
getConversation(): Conversation | null {
|
|
149
|
-
return this.conversation;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
cleanConversation(): void {
|
|
153
|
-
this.conversation = null;
|
|
154
|
-
tag('info').log('Conversation cleaned');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
149
|
private async getPageContext(): Promise<string> {
|
|
158
150
|
const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
|
|
159
151
|
if (!state) {
|
|
@@ -164,11 +156,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
164
156
|
const knowledge = this.getKnowledge(actionResult);
|
|
165
157
|
const experience = this.getExperience(actionResult);
|
|
166
158
|
|
|
167
|
-
const headingLines
|
|
168
|
-
if (state.h1) headingLines.push(`H1: ${state.h1}`);
|
|
169
|
-
if (state.h2) headingLines.push(`H2: ${state.h2}`);
|
|
170
|
-
if (state.h3) headingLines.push(`H3: ${state.h3}`);
|
|
171
|
-
if (state.h4) headingLines.push(`H4: ${state.h4}`);
|
|
159
|
+
const headingLines = formatHeadings(state);
|
|
172
160
|
const headingsBlock = headingLines.join('\n');
|
|
173
161
|
|
|
174
162
|
let pageSummary = '';
|
package/src/ai/conversation.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface ToolExecution {
|
|
|
7
7
|
wasSuccessful: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution {
|
|
11
|
+
let output = rawOutput;
|
|
12
|
+
if (rawOutput?.type === 'json' && rawOutput?.value) output = rawOutput.value;
|
|
13
|
+
return { toolName, input, output, wasSuccessful: output?.success !== false };
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
export function toolExecutionLabel(input: Record<string, any> | undefined): string {
|
|
11
17
|
return input?.explanation || input?.assertion || input?.reason || input?.request || '';
|
|
12
18
|
}
|
|
@@ -15,18 +21,14 @@ const AUTO_COMPACT_ARIA_CHANGES_CUTOFF = 500;
|
|
|
15
21
|
const AUTO_COMPACT_TARGETED_HTML_CUTOFF = 500;
|
|
16
22
|
|
|
17
23
|
export class Conversation {
|
|
18
|
-
id: string;
|
|
19
24
|
messages: ModelMessage[];
|
|
20
25
|
model: any;
|
|
21
|
-
telemetryFunctionId?: string;
|
|
22
26
|
protectedPrefixCount = 0;
|
|
23
27
|
private autoTrimRules: Map<string, number>;
|
|
24
28
|
|
|
25
|
-
constructor(messages: ModelMessage[] = [], model?: any
|
|
26
|
-
this.id = this.generateId();
|
|
29
|
+
constructor(messages: ModelMessage[] = [], model?: any) {
|
|
27
30
|
this.messages = messages;
|
|
28
31
|
this.model = model || '';
|
|
29
|
-
this.telemetryFunctionId = telemetryFunctionId;
|
|
30
32
|
this.autoTrimRules = new Map();
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -41,20 +43,6 @@ export class Conversation {
|
|
|
41
43
|
});
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
addUserImage(image: string): void {
|
|
45
|
-
if (!image || image.trim() === '') {
|
|
46
|
-
console.warn('Warning: Attempting to add empty image to conversation');
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const imageData = image.startsWith('data:') ? image : `data:image/png;base64,${image}`;
|
|
51
|
-
|
|
52
|
-
this.messages.push({
|
|
53
|
-
role: 'user',
|
|
54
|
-
content: [{ type: 'file', mediaType: 'image/png', data: imageData }],
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
46
|
addAssistantText(text: string): void {
|
|
59
47
|
// Skip empty or whitespace-only messages
|
|
60
48
|
if (!text || text.trim() === '') {
|
|
@@ -83,7 +71,7 @@ export class Conversation {
|
|
|
83
71
|
}
|
|
84
72
|
|
|
85
73
|
clone(): Conversation {
|
|
86
|
-
return new Conversation([...this.messages], this.model
|
|
74
|
+
return new Conversation([...this.messages], this.model);
|
|
87
75
|
}
|
|
88
76
|
|
|
89
77
|
cleanupTag(tagName: string, replacement: string, keepLast = 0): void {
|
|
@@ -222,10 +210,6 @@ export class Conversation {
|
|
|
222
210
|
return result;
|
|
223
211
|
}
|
|
224
212
|
|
|
225
|
-
private generateId(): string {
|
|
226
|
-
return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
213
|
getToolExecutions(): ToolExecution[] {
|
|
230
214
|
const toolCalls = new Map<string, any>();
|
|
231
215
|
for (const message of this.messages) {
|
|
@@ -243,14 +227,7 @@ export class Conversation {
|
|
|
243
227
|
if (!Array.isArray(message.content)) continue;
|
|
244
228
|
for (const part of message.content) {
|
|
245
229
|
if (part.type !== 'tool-result') continue;
|
|
246
|
-
|
|
247
|
-
const output = rawOutput?.type === 'json' && rawOutput?.value ? rawOutput.value : rawOutput;
|
|
248
|
-
executions.push({
|
|
249
|
-
toolName: part.toolName,
|
|
250
|
-
input: toolCalls.get(part.toolCallId) || {},
|
|
251
|
-
output,
|
|
252
|
-
wasSuccessful: output?.success !== false,
|
|
253
|
-
});
|
|
230
|
+
executions.push(toToolExecution(part.toolName, toolCalls.get(part.toolCallId) || {}, part.output));
|
|
254
231
|
}
|
|
255
232
|
}
|
|
256
233
|
|
package/src/ai/driller.ts
CHANGED
|
@@ -28,10 +28,9 @@ import { createDebug, tag } from '../utils/logger.ts';
|
|
|
28
28
|
import { loop, pause } from '../utils/loop.ts';
|
|
29
29
|
import { WebElement } from '../utils/web-element.ts';
|
|
30
30
|
import type { Agent } from './agent.ts';
|
|
31
|
-
import type { Conversation } from './conversation.ts';
|
|
32
31
|
import type { Navigator } from './navigator.ts';
|
|
33
32
|
import type { Provider } from './provider.ts';
|
|
34
|
-
import {
|
|
33
|
+
import { drillLocatorRule } from './rules.ts';
|
|
35
34
|
import { TaskAgent, isInteractive } from './task-agent.ts';
|
|
36
35
|
import { createCodeceptJSTools } from './tools.ts';
|
|
37
36
|
|
|
@@ -85,7 +84,6 @@ export class Driller extends TaskAgent implements Agent {
|
|
|
85
84
|
private navigator: Navigator;
|
|
86
85
|
private hooksRunner: HooksRunner;
|
|
87
86
|
private currentPlan?: Plan;
|
|
88
|
-
private currentConversation: Conversation | null = null;
|
|
89
87
|
private allResults: InteractionResult[] = [];
|
|
90
88
|
private verifiedAction: { componentId: string; toolName: string; code?: string; canonicalCode?: string } | null = null;
|
|
91
89
|
private pendingNestedContext: string | null = null;
|
|
@@ -323,7 +321,6 @@ export class Driller extends TaskAgent implements Agent {
|
|
|
323
321
|
this.verifiedAction = null;
|
|
324
322
|
this.pendingNestedContext = null;
|
|
325
323
|
const conversation = this.provider.startConversation(this.getSystemMessage(component), 'driller');
|
|
326
|
-
this.currentConversation = conversation;
|
|
327
324
|
conversation.addUserText(await this.buildComponentPrompt(originalState, component));
|
|
328
325
|
|
|
329
326
|
let finished = false;
|
|
@@ -606,11 +603,11 @@ export class Driller extends TaskAgent implements Agent {
|
|
|
606
603
|
const action = this.explorer.createAction();
|
|
607
604
|
|
|
608
605
|
if (currentState?.url !== originalState.url) {
|
|
609
|
-
await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)
|
|
606
|
+
await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)`);
|
|
610
607
|
return;
|
|
611
608
|
}
|
|
612
609
|
|
|
613
|
-
await action.attempt('I.pressKey("Escape")', `${reason} (restore state)
|
|
610
|
+
await action.attempt('I.pressKey("Escape")', `${reason} (restore state)`);
|
|
614
611
|
}
|
|
615
612
|
|
|
616
613
|
private async saveToExperience(state: ActionResult, results: InteractionResult[]): Promise<void> {
|
|
@@ -789,14 +786,6 @@ export class Driller extends TaskAgent implements Agent {
|
|
|
789
786
|
tag('step').log(` ${status} ${componentTest.component?.name || test.scenario}`);
|
|
790
787
|
}
|
|
791
788
|
}
|
|
792
|
-
|
|
793
|
-
getCurrentPlan(): Plan | undefined {
|
|
794
|
-
return this.currentPlan;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
getConversation(): Conversation | null {
|
|
798
|
-
return this.currentConversation;
|
|
799
|
-
}
|
|
800
789
|
}
|
|
801
790
|
|
|
802
791
|
function formatAriaNode(node: Record<string, unknown>): string {
|
|
@@ -1190,5 +1179,3 @@ function isInteractiveElement(element: WebElement): boolean {
|
|
|
1190
1179
|
if (element.attrs['aria-haspopup'] || element.attrs['aria-expanded'] || element.attrs['aria-controls']) return true;
|
|
1191
1180
|
return false;
|
|
1192
1181
|
}
|
|
1193
|
-
|
|
1194
|
-
const drillLocatorRule = locatorRule.replace(/<context_simplification>[\s\S]*?<\/context_simplification>/, '').trim();
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { unlinkSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
2
3
|
import dedent from 'dedent';
|
|
3
|
-
import { type Tokens, marked } from 'marked';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { type ExperienceFile, type ExperienceTracker, RECENT_WINDOW_DAYS } from '../experience-tracker.js';
|
|
6
6
|
import { Observability } from '../observability.js';
|
|
7
7
|
import { createDebug, log, tag } from '../utils/logger.js';
|
|
8
8
|
import { mdq } from '../utils/markdown-query.js';
|
|
9
|
+
import { isNonReusableCode } from '../utils/step-analyzer.js';
|
|
9
10
|
import { generalizeUrl, hasDynamicUrlSegment } from '../utils/url-matcher.js';
|
|
10
11
|
import type { Agent } from './agent.js';
|
|
11
12
|
import type { Provider } from './provider.js';
|
|
12
13
|
|
|
13
14
|
const debugLog = createDebug('explorbot:experience-compactor');
|
|
14
15
|
|
|
15
|
-
export
|
|
16
|
+
export const COMPACT_MAX_LENGTH = 5000;
|
|
16
17
|
|
|
17
18
|
interface MergeGroup {
|
|
18
19
|
pattern: string;
|
|
@@ -23,7 +24,6 @@ export class ExperienceCompactor implements Agent {
|
|
|
23
24
|
emoji = '🗜️';
|
|
24
25
|
private provider: Provider;
|
|
25
26
|
private experienceTracker: ExperienceTracker;
|
|
26
|
-
private MAX_LENGTH = 5000;
|
|
27
27
|
|
|
28
28
|
constructor(provider: Provider, experienceTracker: ExperienceTracker) {
|
|
29
29
|
this.provider = provider;
|
|
@@ -32,7 +32,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
32
32
|
|
|
33
33
|
async compactExperience(experience: string): Promise<string> {
|
|
34
34
|
const stripped = this.stripNonUsefulEntries(experience);
|
|
35
|
-
if (stripped.length <
|
|
35
|
+
if (stripped.length < COMPACT_MAX_LENGTH) {
|
|
36
36
|
return stripped;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -129,7 +129,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
129
129
|
if (!url || url.startsWith('~')) continue;
|
|
130
130
|
const generalized = generalizeUrl(url);
|
|
131
131
|
if (generalized === url) continue;
|
|
132
|
-
const stateHash = file.filePath
|
|
132
|
+
const stateHash = basename(file.filePath, '.md');
|
|
133
133
|
const newData = { ...file.data, url: `~${generalized}~`, mergedFrom: [url] };
|
|
134
134
|
this.experienceTracker.writeExperienceFile(stateHash, file.content, newData);
|
|
135
135
|
tag('substep').log(`Generalized URL: ${url} → ~${generalized}~`);
|
|
@@ -139,7 +139,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
async compactFiles(files: ExperienceFile[], options?: { skipSmall?: boolean }): Promise<number> {
|
|
142
|
-
const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >=
|
|
142
|
+
const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >= COMPACT_MAX_LENGTH) : files;
|
|
143
143
|
|
|
144
144
|
let compactedCount = 0;
|
|
145
145
|
const total = workingSet.length;
|
|
@@ -151,7 +151,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
151
151
|
|
|
152
152
|
for (let i = 0; i < workingSet.length; i++) {
|
|
153
153
|
const experience = workingSet[i];
|
|
154
|
-
const shortName = experience.filePath
|
|
154
|
+
const shortName = basename(experience.filePath);
|
|
155
155
|
const willReview = this.isRecent(experience);
|
|
156
156
|
|
|
157
157
|
if (total > 1 && willReview) {
|
|
@@ -164,8 +164,8 @@ export class ExperienceCompactor implements Agent {
|
|
|
164
164
|
content = await this.reviewExperienceQuality(content, experience.data);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
if (content.length >=
|
|
168
|
-
if (total > 1) tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${
|
|
167
|
+
if (content.length >= COMPACT_MAX_LENGTH) {
|
|
168
|
+
if (total > 1) tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${COMPACT_MAX_LENGTH} chars)`);
|
|
169
169
|
const prompt = this.buildCompactionPrompt(content);
|
|
170
170
|
const model = this.provider.getModelForAgent('experience-compactor');
|
|
171
171
|
const response = await this.provider.chat(
|
|
@@ -181,7 +181,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
181
181
|
|
|
182
182
|
if (content === experience.content) continue;
|
|
183
183
|
|
|
184
|
-
const stateHash = experience.filePath
|
|
184
|
+
const stateHash = basename(experience.filePath, '.md');
|
|
185
185
|
this.experienceTracker.writeExperienceFile(stateHash, content, experience.data);
|
|
186
186
|
debugLog('Experience file compacted:', experience.filePath);
|
|
187
187
|
compactedCount++;
|
|
@@ -314,7 +314,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
314
314
|
const [targetFile, ...sourceFiles] = group.files;
|
|
315
315
|
const combinedContent = group.files.map((f) => f.content).join('\n\n---\n\n');
|
|
316
316
|
|
|
317
|
-
const targetStateHash = targetFile.filePath
|
|
317
|
+
const targetStateHash = basename(targetFile.filePath, '.md');
|
|
318
318
|
const newFrontmatter = {
|
|
319
319
|
...targetFile.data,
|
|
320
320
|
url: group.pattern,
|
|
@@ -350,7 +350,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
350
350
|
<rules>
|
|
351
351
|
- Use markdown h2 headers only (##) - NO XML tags or wrappers in output
|
|
352
352
|
- Merge similar flows to remove duplicates
|
|
353
|
-
- Keep output under ${
|
|
353
|
+
- Keep output under ${COMPACT_MAX_LENGTH} characters
|
|
354
354
|
- Be explicit and short - no proposals or explanations
|
|
355
355
|
|
|
356
356
|
KEEP only:
|
|
@@ -438,28 +438,9 @@ export class ExperienceCompactor implements Agent {
|
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
function listSections(content: string): { title: string; raw: string }[] {
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
let currentHeading: string | null = null;
|
|
445
|
-
let currentRaw = '';
|
|
446
|
-
|
|
447
|
-
const flush = () => {
|
|
448
|
-
if (currentHeading !== null) sections.push({ title: currentHeading, raw: currentRaw });
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
for (const token of tokens) {
|
|
452
|
-
const raw = (token as any).raw || '';
|
|
453
|
-
if (token.type === 'heading' && (token as Tokens.Heading).depth === 2) {
|
|
454
|
-
flush();
|
|
455
|
-
currentHeading = (token as Tokens.Heading).text.trim();
|
|
456
|
-
currentRaw = raw;
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
if (currentHeading !== null) currentRaw += raw;
|
|
460
|
-
}
|
|
461
|
-
flush();
|
|
462
|
-
return sections;
|
|
441
|
+
const sections = mdq(content).query('section2').each();
|
|
442
|
+
const metas = mdq(content).query('section2').meta();
|
|
443
|
+
return sections.map((q, i) => ({ title: metas[i].text.trim(), raw: q.text() }));
|
|
463
444
|
}
|
|
464
445
|
|
|
465
446
|
function dropEmptySections(content: string): string {
|
|
@@ -483,7 +464,7 @@ function dropNonReusableSections(content: string): string {
|
|
|
483
464
|
|
|
484
465
|
for (const section of sections) {
|
|
485
466
|
const raw = section.text();
|
|
486
|
-
if (
|
|
467
|
+
if (!isNonReusableCode(raw)) continue;
|
|
487
468
|
result = result.replace(raw, '');
|
|
488
469
|
}
|
|
489
470
|
|
|
@@ -158,7 +158,7 @@ function extractKeyFields(body: any, result: Record<string, any> = {}, depth = 0
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
for (const [key, value] of Object.entries(body)) {
|
|
161
|
-
if (value
|
|
161
|
+
if (value == null) continue;
|
|
162
162
|
if (typeof value === 'object') {
|
|
163
163
|
extractKeyFields(value, result, depth + 1);
|
|
164
164
|
continue;
|
package/src/ai/fisherman.ts
CHANGED
|
@@ -44,10 +44,6 @@ export class Fisherman implements Agent {
|
|
|
44
44
|
return this.mode !== 'disabled';
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
getMode(): string {
|
|
48
|
-
return this.mode;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
async ensureReady(scopeUrl?: string): Promise<void> {
|
|
52
48
|
await this.detectMode(scopeUrl);
|
|
53
49
|
this.spec ??= await this.specLoader();
|
|
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ActionResult } from '../../action-result.ts';
|
|
4
4
|
import { ConfigParser } from '../../config.ts';
|
|
5
|
-
import {
|
|
5
|
+
import type { StateManager } from '../../state-manager.ts';
|
|
6
6
|
import type { Plan } from '../../test-plan.ts';
|
|
7
7
|
import { tag } from '../../utils/logger.ts';
|
|
8
8
|
import { relativeToCwd } from '../../utils/next-steps.ts';
|
|
@@ -21,6 +21,7 @@ export interface CodeceptJSMethods {
|
|
|
21
21
|
export function WithCodeceptJS<T extends Constructor>(Base: T) {
|
|
22
22
|
return class extends Base {
|
|
23
23
|
declare savedFiles: Set<string>;
|
|
24
|
+
declare stateManager?: StateManager;
|
|
24
25
|
|
|
25
26
|
toCode(conversation: Conversation, scenario: string): string {
|
|
26
27
|
const toolExecutions = conversation.getToolExecutions();
|
|
@@ -108,7 +109,8 @@ export function WithCodeceptJS<T extends Constructor>(Base: T) {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
private getKnowledgeLines(url: string, indent = ' '): string[] {
|
|
111
|
-
const knowledgeTracker =
|
|
112
|
+
const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
|
|
113
|
+
if (!knowledgeTracker) return [];
|
|
112
114
|
const state = new ActionResult({ url });
|
|
113
115
|
const { wait, waitForElement, code } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement', 'code']);
|
|
114
116
|
|