explorbot 0.1.13 → 0.1.16
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/dist/package.json +3 -2
- package/dist/src/action.js +3 -2
- package/dist/src/ai/conversation.js +20 -4
- package/dist/src/ai/historian/utils.js +8 -1
- package/dist/src/ai/pilot.js +198 -260
- package/dist/src/ai/provider.js +25 -12
- package/dist/src/ai/quartermaster.js +2 -2
- package/dist/src/ai/researcher/focus.js +51 -10
- package/dist/src/ai/researcher/sections.js +8 -4
- package/dist/src/ai/researcher.js +9 -24
- package/dist/src/ai/rules.js +2 -0
- package/dist/src/ai/session-analyst.js +46 -41
- package/dist/src/ai/tester.js +63 -22
- package/dist/src/ai/tools.js +19 -4
- package/dist/src/commands/explore-command.js +8 -2
- package/dist/src/components/StatusPane.js +6 -1
- package/dist/src/experience-tracker.js +9 -0
- package/dist/src/explorer.js +2 -5
- package/dist/src/reporter.js +41 -1
- package/dist/src/stats.js +2 -1
- package/dist/src/test-plan.js +47 -3
- package/package.json +3 -2
- package/src/action.ts +3 -2
- package/src/ai/conversation.ts +21 -4
- package/src/ai/historian/utils.ts +8 -1
- package/src/ai/pilot.ts +199 -259
- package/src/ai/provider.ts +24 -12
- package/src/ai/quartermaster.ts +2 -2
- package/src/ai/researcher/focus.ts +57 -8
- package/src/ai/researcher/sections.ts +7 -3
- package/src/ai/researcher.ts +8 -23
- package/src/ai/rules.ts +2 -0
- package/src/ai/session-analyst.ts +47 -41
- package/src/ai/tester.ts +55 -20
- package/src/ai/tools.ts +18 -4
- package/src/commands/explore-command.ts +9 -2
- package/src/components/StatusPane.tsx +6 -3
- package/src/experience-tracker.ts +9 -0
- package/src/explorer.ts +1 -4
- package/src/reporter.ts +44 -1
- package/src/stats.ts +3 -1
- package/src/test-plan.ts +62 -3
package/dist/src/ai/tools.js
CHANGED
|
@@ -423,7 +423,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
|
|
|
423
423
|
return failedToolResult('see', 'AI analysis failed to process the screenshot');
|
|
424
424
|
}
|
|
425
425
|
return successToolResult('see', {
|
|
426
|
-
analysis: analysisResult,
|
|
426
|
+
analysis: cap(analysisResult, ANALYSIS_OUTPUT_CAP),
|
|
427
427
|
message: `Successfully analyzed screenshot for: ${request}`,
|
|
428
428
|
suggestion: 'Visual confirmation is valid evidence for test results. Use record() to note the visual findings.',
|
|
429
429
|
});
|
|
@@ -469,8 +469,8 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
|
|
|
469
469
|
url: currentState.url,
|
|
470
470
|
title: currentState.title,
|
|
471
471
|
suggestion: 'If not enough context received, call see() to visually identify elements in page contents',
|
|
472
|
-
aria,
|
|
473
|
-
html,
|
|
472
|
+
aria: cap(aria, ARIA_OUTPUT_CAP),
|
|
473
|
+
html: cap(html, HTML_OUTPUT_CAP),
|
|
474
474
|
reminder: 'Context provided. Do not call context() again until you perform actions or suspect page changed.',
|
|
475
475
|
});
|
|
476
476
|
}
|
|
@@ -556,7 +556,7 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
|
|
|
556
556
|
const researchResult = await researcher.research(currentState, { screenshot: true, data: true });
|
|
557
557
|
return successToolResult('research', {
|
|
558
558
|
analysis: researchResult,
|
|
559
|
-
aria: ActionResult.fromState(currentState).getInteractiveARIA(),
|
|
559
|
+
aria: cap(ActionResult.fromState(currentState).getInteractiveARIA(), ARIA_OUTPUT_CAP),
|
|
560
560
|
message: `Successfully researched page: ${currentState.url}.`,
|
|
561
561
|
suggestion: dedent `
|
|
562
562
|
You received comprehensive UI map report. Use it to understand the page structure and navigate to the elements.
|
|
@@ -859,6 +859,16 @@ export function createAgentTools({ explorer, researcher, navigator, experienceTr
|
|
|
859
859
|
return tools;
|
|
860
860
|
}
|
|
861
861
|
const PAGE_DIFF_SUGGESTION = 'Analyze page diff. htmlParts shows what changed and WHERE — each part has a container selector. Use the container as context when clicking elements from the diff.';
|
|
862
|
+
const ARIA_OUTPUT_CAP = 4000;
|
|
863
|
+
const HTML_OUTPUT_CAP = 6000;
|
|
864
|
+
const ANALYSIS_OUTPUT_CAP = 2000;
|
|
865
|
+
function cap(text, max) {
|
|
866
|
+
if (!text)
|
|
867
|
+
return '';
|
|
868
|
+
if (text.length <= max)
|
|
869
|
+
return text;
|
|
870
|
+
return `${text.slice(0, max)}\n[...truncated; ${text.length - max} chars omitted...]`;
|
|
871
|
+
}
|
|
862
872
|
function transformContainsCommand(command) {
|
|
863
873
|
if (!command.includes(':contains('))
|
|
864
874
|
return command;
|
|
@@ -897,9 +907,14 @@ function successToolResult(action, data, source) {
|
|
|
897
907
|
if (data?.pageDiff) {
|
|
898
908
|
let suggestion = PAGE_DIFF_SUGGESTION;
|
|
899
909
|
const ariaChanges = data.pageDiff.ariaChanges || '';
|
|
910
|
+
const urlChanged = data.pageDiff.urlChanged === true;
|
|
911
|
+
const hasHtmlParts = Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
|
|
900
912
|
if (countAriaChanges(ariaChanges) >= 50) {
|
|
901
913
|
suggestion = `MAJOR PAGE CHANGE. Page entered a different mode. Check htmlParts and iframes in pageDiff before next action. ${suggestion}`;
|
|
902
914
|
}
|
|
915
|
+
else if (!urlChanged && !ariaChanges && !hasHtmlParts) {
|
|
916
|
+
suggestion = 'Action ran without error but produced no observable change (URL, ARIA and HTML all unchanged). The locator likely matched a non-interactive ancestor or an element outside the intended control. Re-locate via xpathCheck() or verify with see() before treating this as success.';
|
|
917
|
+
}
|
|
903
918
|
else if (ariaChanges.includes('heading') && ariaChanges.includes('added')) {
|
|
904
919
|
suggestion += ' WARNING: A new panel or modal may have appeared. If this was not the intended action, close it and try a different element.';
|
|
905
920
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import figureSet from 'figures';
|
|
2
2
|
import { getStyles } from '../ai/planner/styles.js';
|
|
3
3
|
import { outputPath } from '../config.js';
|
|
4
|
+
import { normalizeUrl } from '../state-manager.js';
|
|
4
5
|
import { Stats } from '../stats.js';
|
|
5
6
|
import { getCliName } from "../utils/cli-name.js";
|
|
6
7
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
@@ -9,6 +10,7 @@ import { jsonToTable } from '../utils/markdown-parser.js';
|
|
|
9
10
|
import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
|
|
10
11
|
import { safeFilename } from "../utils/strings.js";
|
|
11
12
|
import { BaseCommand } from './base-command.js';
|
|
13
|
+
const MAX_SUB_PAGE_ATTEMPTS = 30;
|
|
12
14
|
export class ExploreCommand extends BaseCommand {
|
|
13
15
|
name = 'explore';
|
|
14
16
|
description = 'Start web exploration';
|
|
@@ -24,6 +26,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
24
26
|
maxTests;
|
|
25
27
|
testsRun = 0;
|
|
26
28
|
completedPlans = [];
|
|
29
|
+
failedSubPages = new Set();
|
|
27
30
|
async execute(args) {
|
|
28
31
|
const { opts, args: remaining } = this.parseArgs(args);
|
|
29
32
|
if (opts.maxTests) {
|
|
@@ -40,10 +43,12 @@ export class ExploreCommand extends BaseCommand {
|
|
|
40
43
|
this.completedPlans.push(mainPlan);
|
|
41
44
|
if (!feature && !this.isLimitReached()) {
|
|
42
45
|
const planner = this.explorBot.agentPlanner();
|
|
43
|
-
|
|
46
|
+
let attempts = 0;
|
|
47
|
+
while (attempts < MAX_SUB_PAGE_ATTEMPTS) {
|
|
48
|
+
attempts++;
|
|
44
49
|
if (this.isLimitReached())
|
|
45
50
|
break;
|
|
46
|
-
const candidates = planner.collectSubPageCandidates(mainPlan, mainUrl || '/');
|
|
51
|
+
const candidates = planner.collectSubPageCandidates(mainPlan, mainUrl || '/').filter((c) => !this.failedSubPages.has(normalizeUrl(c.url)));
|
|
47
52
|
if (candidates.length === 0)
|
|
48
53
|
break;
|
|
49
54
|
const pick = await planner.pickNextSubPage(candidates);
|
|
@@ -59,6 +64,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
catch (err) {
|
|
67
|
+
this.failedSubPages.add(normalizeUrl(pick.url));
|
|
62
68
|
tag('warning').log(`Sub-page exploration failed: ${err instanceof Error ? err.message : err}`);
|
|
63
69
|
}
|
|
64
70
|
}
|
|
@@ -32,5 +32,10 @@ export const StatusPane = ({ onComplete }) => {
|
|
|
32
32
|
React.createElement(Box, { marginTop: 1, marginBottom: 1 },
|
|
33
33
|
React.createElement(Text, { bold: true }, "Usage")),
|
|
34
34
|
React.createElement(Row, { label: "Time", value: Stats.getElapsedTime() }),
|
|
35
|
-
tokenRows.map(([model, tokens]) =>
|
|
35
|
+
tokenRows.map(([model, tokens]) => {
|
|
36
|
+
const cached = tokens.cached ?? 0;
|
|
37
|
+
const cachePct = tokens.input > 0 ? Math.round((cached / tokens.input) * 100) : 0;
|
|
38
|
+
const suffix = cached > 0 ? ` (${Stats.humanizeTokens(cached)} cached, ${cachePct}%)` : '';
|
|
39
|
+
return React.createElement(Row, { key: model, label: model, value: `${Stats.humanizeTokens(tokens.total)} tokens${suffix}` });
|
|
40
|
+
})))));
|
|
36
41
|
};
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
|
|
|
2
2
|
import { basename, dirname, join } from 'node:path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
import { marked } from 'marked';
|
|
5
|
+
import { isNonReusableCode } from "./ai/historian/utils.js";
|
|
5
6
|
import { ConfigParser } from './config.js';
|
|
6
7
|
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
7
8
|
import { createDebug, tag } from './utils/logger.js';
|
|
@@ -145,6 +146,10 @@ export class ExperienceTracker {
|
|
|
145
146
|
return;
|
|
146
147
|
if (!action.code?.trim())
|
|
147
148
|
return;
|
|
149
|
+
if (isNonReusableCode(action.code)) {
|
|
150
|
+
debugLog('Skipping action with non-reusable code: %s', action.code);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
148
153
|
this.ensureExperienceFile(state);
|
|
149
154
|
const stateHash = state.getStateHash();
|
|
150
155
|
const { content, data } = this.readExperienceFile(stateHash);
|
|
@@ -166,6 +171,10 @@ export class ExperienceTracker {
|
|
|
166
171
|
return;
|
|
167
172
|
if (!body?.trim())
|
|
168
173
|
return;
|
|
174
|
+
if (isNonReusableCode(body)) {
|
|
175
|
+
debugLog('Skipping flow body with non-reusable code');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
169
178
|
this.ensureExperienceFile(state);
|
|
170
179
|
const stateHash = state.getStateHash();
|
|
171
180
|
const { content, data } = this.readExperienceFile(stateHash);
|
package/dist/src/explorer.js
CHANGED
|
@@ -10,7 +10,7 @@ import Action from './action.js';
|
|
|
10
10
|
import { visuallyAnnotateContainers } from "./ai/researcher/coordinates.js";
|
|
11
11
|
import { RequestStore } from "./api/request-store.js";
|
|
12
12
|
import { XhrCapture } from "./api/xhr-capture.js";
|
|
13
|
-
import { ConfigParser
|
|
13
|
+
import { ConfigParser } from './config.js';
|
|
14
14
|
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
15
15
|
import { PlaywrightRecorder } from "./playwright-recorder.js";
|
|
16
16
|
import { Reporter } from "./reporter.js";
|
|
@@ -467,10 +467,7 @@ class Explorer {
|
|
|
467
467
|
if (!this.stateManager.getCurrentState())
|
|
468
468
|
return;
|
|
469
469
|
const lastScreenshot = ActionResult.fromState(this.stateManager.getCurrentState()).screenshotFile;
|
|
470
|
-
|
|
471
|
-
return;
|
|
472
|
-
const screenshotPath = outputPath('states', lastScreenshot);
|
|
473
|
-
test.addArtifact(screenshotPath);
|
|
470
|
+
test.setActiveNoteScreenshot(lastScreenshot);
|
|
474
471
|
};
|
|
475
472
|
const dialogHandler = (dialog) => {
|
|
476
473
|
const dialogType = dialog.type();
|
package/dist/src/reporter.js
CHANGED
|
@@ -87,7 +87,7 @@ export class Reporter {
|
|
|
87
87
|
this.client = new Client({ apiKey: process.env.TESTOMATIO || '', title: this.buildTitle() });
|
|
88
88
|
const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
|
|
89
89
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
90
|
-
const result = await Promise.race([this.client.createRun().then(() => 'success'), timeoutPromise]);
|
|
90
|
+
const result = await Promise.race([this.client.createRun({ configuration: { exploratory: true } }).then(() => 'success'), timeoutPromise]);
|
|
91
91
|
if (result === 'timeout') {
|
|
92
92
|
debugLog('Reporter run creation timed out');
|
|
93
93
|
return;
|
|
@@ -117,6 +117,7 @@ export class Reporter {
|
|
|
117
117
|
message: note.message,
|
|
118
118
|
status: note.status,
|
|
119
119
|
screenshot: note.screenshot,
|
|
120
|
+
log: note.log,
|
|
120
121
|
}))
|
|
121
122
|
.sort((a, b) => a.startTime - b.startTime);
|
|
122
123
|
const stepEntries = Object.entries(test.steps)
|
|
@@ -148,8 +149,16 @@ export class Reporter {
|
|
|
148
149
|
if (noteEntry.screenshot) {
|
|
149
150
|
step.artifacts = [outputPath('states', noteEntry.screenshot)];
|
|
150
151
|
}
|
|
152
|
+
if (noteEntry.log) {
|
|
153
|
+
step.log = noteEntry.log;
|
|
154
|
+
}
|
|
151
155
|
steps.push(step);
|
|
152
156
|
}
|
|
157
|
+
const verificationStep = this.buildVerificationStep(test, lastScreenshotFile);
|
|
158
|
+
if (verificationStep) {
|
|
159
|
+
steps.push(verificationStep);
|
|
160
|
+
return steps;
|
|
161
|
+
}
|
|
153
162
|
if (lastScreenshotFile && steps.length > 0) {
|
|
154
163
|
const lastStep = steps[steps.length - 1];
|
|
155
164
|
const screenshotPath = outputPath('states', lastScreenshotFile);
|
|
@@ -162,6 +171,37 @@ export class Reporter {
|
|
|
162
171
|
}
|
|
163
172
|
return steps;
|
|
164
173
|
}
|
|
174
|
+
buildVerificationStep(test, lastScreenshotFile) {
|
|
175
|
+
const v = test.verification;
|
|
176
|
+
if (!v)
|
|
177
|
+
return undefined;
|
|
178
|
+
const subSteps = [];
|
|
179
|
+
if (v.message)
|
|
180
|
+
subSteps.push({ category: 'framework', title: v.message, duration: 0 });
|
|
181
|
+
if (v.url) {
|
|
182
|
+
subSteps.push({
|
|
183
|
+
category: 'framework',
|
|
184
|
+
title: v.pageLabel ? `Navigated to ${v.pageLabel}` : 'Final page',
|
|
185
|
+
log: v.url,
|
|
186
|
+
duration: 0,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for (const detail of v.details) {
|
|
190
|
+
subSteps.push({ category: 'framework', title: detail, duration: 0 });
|
|
191
|
+
}
|
|
192
|
+
const screenshotFile = v.screenshot || lastScreenshotFile;
|
|
193
|
+
const step = {
|
|
194
|
+
category: 'user',
|
|
195
|
+
title: 'Verification',
|
|
196
|
+
duration: 0,
|
|
197
|
+
status: v.status || 'none',
|
|
198
|
+
steps: subSteps.length > 0 ? subSteps : undefined,
|
|
199
|
+
};
|
|
200
|
+
if (screenshotFile) {
|
|
201
|
+
step.artifacts = [outputPath('states', screenshotFile)];
|
|
202
|
+
}
|
|
203
|
+
return step;
|
|
204
|
+
}
|
|
165
205
|
async reportTest(test, meta) {
|
|
166
206
|
await this.startRun();
|
|
167
207
|
if (!this.isRunStarted) {
|
package/dist/src/stats.js
CHANGED
|
@@ -10,11 +10,12 @@ export class Stats {
|
|
|
10
10
|
static models = {};
|
|
11
11
|
static recordTokens(_agent, model, usage) {
|
|
12
12
|
if (!Stats.models[model]) {
|
|
13
|
-
Stats.models[model] = { input: 0, output: 0, total: 0 };
|
|
13
|
+
Stats.models[model] = { input: 0, output: 0, total: 0, cached: 0 };
|
|
14
14
|
}
|
|
15
15
|
Stats.models[model].input += usage.input;
|
|
16
16
|
Stats.models[model].output += usage.output;
|
|
17
17
|
Stats.models[model].total += usage.total;
|
|
18
|
+
Stats.models[model].cached = (Stats.models[model].cached ?? 0) + (usage.cached ?? 0);
|
|
18
19
|
}
|
|
19
20
|
static getElapsedTime() {
|
|
20
21
|
const elapsed = Date.now() - Stats.startTime;
|
package/dist/src/test-plan.js
CHANGED
|
@@ -17,6 +17,7 @@ export class ActiveNote {
|
|
|
17
17
|
message;
|
|
18
18
|
status;
|
|
19
19
|
screenshot;
|
|
20
|
+
log;
|
|
20
21
|
constructor(task, message, status) {
|
|
21
22
|
this.task = task;
|
|
22
23
|
this.startTime = performance.now();
|
|
@@ -41,6 +42,7 @@ export class Task {
|
|
|
41
42
|
steps;
|
|
42
43
|
states;
|
|
43
44
|
startUrl;
|
|
45
|
+
verification;
|
|
44
46
|
timestampCounter = 0;
|
|
45
47
|
activeNote;
|
|
46
48
|
constructor(description, startUrl = '') {
|
|
@@ -67,6 +69,7 @@ export class Task {
|
|
|
67
69
|
startTime: activeNote.getStartTime(),
|
|
68
70
|
endTime,
|
|
69
71
|
screenshot: activeNote.screenshot,
|
|
72
|
+
log: activeNote.log,
|
|
70
73
|
};
|
|
71
74
|
this.activeNote = undefined;
|
|
72
75
|
}
|
|
@@ -80,13 +83,30 @@ export class Task {
|
|
|
80
83
|
.map((n) => `- ${n}`)
|
|
81
84
|
.join('\n');
|
|
82
85
|
}
|
|
83
|
-
addNote(message, status = null, screenshot) {
|
|
84
|
-
const isDuplicate = Object.values(this.notes).some((note) => note.message === message && note.status === status);
|
|
86
|
+
addNote(message, status = null, screenshot, log) {
|
|
87
|
+
const isDuplicate = Object.values(this.notes).some((note) => note.message === message && note.status === status && note.log === log);
|
|
85
88
|
if (isDuplicate)
|
|
86
89
|
return;
|
|
87
90
|
const now = performance.now();
|
|
88
91
|
const timestamp = `${now}_${this.timestampCounter++}`;
|
|
89
|
-
this.notes[timestamp] = { message, status, startTime: now, endTime: now, screenshot };
|
|
92
|
+
this.notes[timestamp] = { message, status, startTime: now, endTime: now, screenshot, log };
|
|
93
|
+
}
|
|
94
|
+
addUrlNote(state, prevState) {
|
|
95
|
+
const fullUrl = state.fullUrl || state.url;
|
|
96
|
+
if (!fullUrl)
|
|
97
|
+
return;
|
|
98
|
+
let label;
|
|
99
|
+
if (state.title && state.title !== prevState?.title)
|
|
100
|
+
label = state.title;
|
|
101
|
+
else if (state.h1 && state.h1 !== prevState?.h1)
|
|
102
|
+
label = state.h1;
|
|
103
|
+
else if (state.h2 && state.h2 !== prevState?.h2)
|
|
104
|
+
label = state.h2;
|
|
105
|
+
else
|
|
106
|
+
label = state.title || state.h1 || state.h2;
|
|
107
|
+
if (!label)
|
|
108
|
+
return;
|
|
109
|
+
this.addNote(`Navigated to ${label}`, TestResult.PASSED, state.screenshotFile, fullUrl);
|
|
90
110
|
}
|
|
91
111
|
addState(state) {
|
|
92
112
|
this.states.push(state);
|
|
@@ -95,6 +115,30 @@ export class Task {
|
|
|
95
115
|
const timestamp = `${performance.now()}_${this.timestampCounter++}`;
|
|
96
116
|
this.steps[timestamp] = { text, duration, status, error, log, artifacts, noteStartTime: this.activeNote?.getStartTime() };
|
|
97
117
|
}
|
|
118
|
+
setActiveNoteScreenshot(screenshotFile) {
|
|
119
|
+
if (!this.activeNote || !screenshotFile)
|
|
120
|
+
return;
|
|
121
|
+
this.activeNote.screenshot = screenshotFile;
|
|
122
|
+
}
|
|
123
|
+
setVerification(message, status, state) {
|
|
124
|
+
this.verification ||= { message: '', status: null, details: [] };
|
|
125
|
+
this.verification.message = message;
|
|
126
|
+
this.verification.status = status;
|
|
127
|
+
if (!state)
|
|
128
|
+
return;
|
|
129
|
+
if (state.screenshotFile)
|
|
130
|
+
this.verification.screenshot = state.screenshotFile;
|
|
131
|
+
const fullUrl = state.fullUrl || state.url;
|
|
132
|
+
if (fullUrl)
|
|
133
|
+
this.verification.url = fullUrl;
|
|
134
|
+
this.verification.pageLabel = state.title || state.h1 || state.h2 || undefined;
|
|
135
|
+
}
|
|
136
|
+
addVerificationDetail(detail) {
|
|
137
|
+
if (!detail)
|
|
138
|
+
return;
|
|
139
|
+
this.verification ||= { message: '', status: null, details: [] };
|
|
140
|
+
this.verification.details.push(detail);
|
|
141
|
+
}
|
|
98
142
|
getLog() {
|
|
99
143
|
const merged = {};
|
|
100
144
|
for (const [key, stepData] of Object.entries(this.steps)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@ai-sdk/openai": "^3.0",
|
|
68
68
|
"@axe-core/playwright": "^4.11.0",
|
|
69
69
|
"@codeceptjs/reflection": "^0.5.2",
|
|
70
|
+
"@faker-js/faker": "^10.4.0",
|
|
70
71
|
"@inkjs/ui": "^2.0.0",
|
|
71
72
|
"@langfuse/otel": "^4.5.1",
|
|
72
73
|
"@openrouter/ai-sdk-provider": "^2.3.3",
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
79
80
|
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
80
81
|
"@scalar/openapi-parser": "^0.25.6",
|
|
81
|
-
"@testomatio/reporter": "^2.7.9-beta.
|
|
82
|
+
"@testomatio/reporter": "^2.7.9-beta.3-markdown",
|
|
82
83
|
"ai": "^6.0.6",
|
|
83
84
|
"axe-core": "^4.11.1",
|
|
84
85
|
"bash-tool": "^1.3.15",
|
package/src/action.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { faker } from '@faker-js/faker';
|
|
3
4
|
import { context, trace } from '@opentelemetry/api';
|
|
4
5
|
import { highlight } from 'cli-highlight';
|
|
5
6
|
import { container, recorder } from 'codeceptjs';
|
|
@@ -255,8 +256,8 @@ class Action {
|
|
|
255
256
|
await asyncFn(page);
|
|
256
257
|
await sleep(this.config.action?.delay || 500);
|
|
257
258
|
} else {
|
|
258
|
-
const codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', sanitizedCode);
|
|
259
|
-
codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step);
|
|
259
|
+
const codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker', sanitizedCode);
|
|
260
|
+
codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
260
261
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
261
262
|
await recorder.promise();
|
|
262
263
|
}
|
package/src/ai/conversation.ts
CHANGED
|
@@ -19,6 +19,7 @@ export class Conversation {
|
|
|
19
19
|
messages: ModelMessage[];
|
|
20
20
|
model: any;
|
|
21
21
|
telemetryFunctionId?: string;
|
|
22
|
+
protectedPrefixCount = 0;
|
|
22
23
|
private autoTrimRules: Map<string, number>;
|
|
23
24
|
|
|
24
25
|
constructor(messages: ModelMessage[] = [], model?: any, telemetryFunctionId?: string) {
|
|
@@ -29,6 +30,10 @@ export class Conversation {
|
|
|
29
30
|
this.autoTrimRules = new Map();
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
protectPrefix(count: number): void {
|
|
34
|
+
this.protectedPrefixCount = count;
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
addUserText(text: string): void {
|
|
33
38
|
this.messages.push({
|
|
34
39
|
role: 'user',
|
|
@@ -85,9 +90,11 @@ export class Conversation {
|
|
|
85
90
|
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
91
|
const regex = new RegExp(`<${escapedTag}>[\\s\\S]*?<\\/${escapedTag}>`, 'g');
|
|
87
92
|
const replacementText = `<${tagName}>${replacement}</${tagName}>`;
|
|
93
|
+
const start = this.protectedPrefixCount;
|
|
88
94
|
|
|
89
95
|
if (keepLast === 0) {
|
|
90
|
-
for (
|
|
96
|
+
for (let i = start; i < this.messages.length; i++) {
|
|
97
|
+
const message = this.messages[i];
|
|
91
98
|
if (typeof message.content === 'string') {
|
|
92
99
|
message.content = message.content.replace(regex, replacementText);
|
|
93
100
|
}
|
|
@@ -96,7 +103,7 @@ export class Conversation {
|
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
const allMatches: Array<{ messageIndex: number; startIndex: number; endIndex: number }> = [];
|
|
99
|
-
for (let i =
|
|
106
|
+
for (let i = start; i < this.messages.length; i++) {
|
|
100
107
|
const message = this.messages[i];
|
|
101
108
|
if (typeof message.content === 'string') {
|
|
102
109
|
const matches = [...message.content.matchAll(new RegExp(`<${escapedTag}>[\\s\\S]*?<\\/${escapedTag}>`, 'g'))];
|
|
@@ -112,7 +119,7 @@ export class Conversation {
|
|
|
112
119
|
const keepMatches = allMatches.slice(-keepCount);
|
|
113
120
|
const keepSet = new Set(keepMatches.map((m) => `${m.messageIndex}:${m.startIndex}`));
|
|
114
121
|
|
|
115
|
-
for (let i =
|
|
122
|
+
for (let i = start; i < this.messages.length; i++) {
|
|
116
123
|
const message = this.messages[i];
|
|
117
124
|
if (typeof message.content === 'string') {
|
|
118
125
|
const matches = [...message.content.matchAll(new RegExp(`<${escapedTag}>[\\s\\S]*?<\\/${escapedTag}>`, 'g'))];
|
|
@@ -137,7 +144,7 @@ export class Conversation {
|
|
|
137
144
|
|
|
138
145
|
compactToolResults(keepLastN: number): void {
|
|
139
146
|
const toolMessageIndexes: number[] = [];
|
|
140
|
-
for (let i =
|
|
147
|
+
for (let i = this.protectedPrefixCount; i < this.messages.length; i++) {
|
|
141
148
|
if (this.messages[i].role === 'tool') toolMessageIndexes.push(i);
|
|
142
149
|
}
|
|
143
150
|
const compactUpTo = toolMessageIndexes.length - Math.max(0, keepLastN);
|
|
@@ -169,6 +176,16 @@ export class Conversation {
|
|
|
169
176
|
}
|
|
170
177
|
}
|
|
171
178
|
|
|
179
|
+
markLastMessageCacheable(): void {
|
|
180
|
+
const last = this.messages[this.messages.length - 1];
|
|
181
|
+
if (!last) return;
|
|
182
|
+
(last as any).providerOptions = {
|
|
183
|
+
...(last as any).providerOptions,
|
|
184
|
+
anthropic: { cacheControl: { type: 'ephemeral' } },
|
|
185
|
+
bedrock: { cachePoint: { type: 'default' } },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
hasTag(tagName: string, lastN?: number): boolean {
|
|
173
190
|
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
174
191
|
const regex = new RegExp(`<${escapedTag}>`, 'g');
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { isDynamicId } from '../../utils/xpath.ts';
|
|
1
2
|
import type { ToolExecution } from '../conversation.ts';
|
|
2
3
|
|
|
3
4
|
export function isNonReusableCode(code: string): boolean {
|
|
4
|
-
|
|
5
|
+
if (/\bI\.clickXY\s*\(/.test(code)) return true;
|
|
6
|
+
|
|
7
|
+
for (const m of code.matchAll(/#([A-Za-z_][\w-]*)/g)) {
|
|
8
|
+
if (isDynamicId(m[1])) return true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return false;
|
|
5
12
|
}
|
|
6
13
|
|
|
7
14
|
export function escapeString(str: string): string {
|