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
|
@@ -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 type { Plan } from '../test-plan.js';
|
|
6
7
|
import { getCliName } from '../utils/cli-name.ts';
|
|
@@ -11,6 +12,8 @@ import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/ne
|
|
|
11
12
|
import { safeFilename } from '../utils/strings.ts';
|
|
12
13
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
13
14
|
|
|
15
|
+
const MAX_SUB_PAGE_ATTEMPTS = 30;
|
|
16
|
+
|
|
14
17
|
export class ExploreCommand extends BaseCommand {
|
|
15
18
|
name = 'explore';
|
|
16
19
|
description = 'Start web exploration';
|
|
@@ -27,6 +30,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
27
30
|
maxTests?: number;
|
|
28
31
|
private testsRun = 0;
|
|
29
32
|
private completedPlans: Plan[] = [];
|
|
33
|
+
private failedSubPages = new Set<string>();
|
|
30
34
|
|
|
31
35
|
async execute(args: string): Promise<void> {
|
|
32
36
|
const { opts, args: remaining } = this.parseArgs(args);
|
|
@@ -46,10 +50,12 @@ export class ExploreCommand extends BaseCommand {
|
|
|
46
50
|
|
|
47
51
|
if (!feature && !this.isLimitReached()) {
|
|
48
52
|
const planner = this.explorBot.agentPlanner();
|
|
49
|
-
|
|
53
|
+
let attempts = 0;
|
|
54
|
+
while (attempts < MAX_SUB_PAGE_ATTEMPTS) {
|
|
55
|
+
attempts++;
|
|
50
56
|
if (this.isLimitReached()) break;
|
|
51
57
|
|
|
52
|
-
const candidates = planner.collectSubPageCandidates(mainPlan, mainUrl || '/');
|
|
58
|
+
const candidates = planner.collectSubPageCandidates(mainPlan, mainUrl || '/').filter((c) => !this.failedSubPages.has(normalizeUrl(c.url)));
|
|
53
59
|
if (candidates.length === 0) break;
|
|
54
60
|
|
|
55
61
|
const pick = await planner.pickNextSubPage(candidates);
|
|
@@ -64,6 +70,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
64
70
|
this.completedPlans.push(subPlan);
|
|
65
71
|
}
|
|
66
72
|
} catch (err) {
|
|
73
|
+
this.failedSubPages.add(normalizeUrl(pick.url));
|
|
67
74
|
tag('warning').log(`Sub-page exploration failed: ${err instanceof Error ? err.message : err}`);
|
|
68
75
|
}
|
|
69
76
|
}
|
|
@@ -52,9 +52,12 @@ export const StatusPane: React.FC<{ onComplete?: () => void }> = ({ onComplete }
|
|
|
52
52
|
<Text bold>Usage</Text>
|
|
53
53
|
</Box>
|
|
54
54
|
<Row label="Time" value={Stats.getElapsedTime()} />
|
|
55
|
-
{tokenRows.map(([model, tokens]) =>
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
{tokenRows.map(([model, tokens]) => {
|
|
56
|
+
const cached = tokens.cached ?? 0;
|
|
57
|
+
const cachePct = tokens.input > 0 ? Math.round((cached / tokens.input) * 100) : 0;
|
|
58
|
+
const suffix = cached > 0 ? ` (${Stats.humanizeTokens(cached)} cached, ${cachePct}%)` : '';
|
|
59
|
+
return <Row key={model} label={model} value={`${Stats.humanizeTokens(tokens.total)} tokens${suffix}`} />;
|
|
60
|
+
})}
|
|
58
61
|
</>
|
|
59
62
|
)}
|
|
60
63
|
</Box>
|
|
@@ -3,6 +3,7 @@ import { basename, dirname, join } from 'node:path';
|
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
import { type Tokens, marked } from 'marked';
|
|
5
5
|
import type { ActionResult } from './action-result.js';
|
|
6
|
+
import { isNonReusableCode } from './ai/historian/utils.ts';
|
|
6
7
|
import { ConfigParser } from './config.js';
|
|
7
8
|
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
8
9
|
import type { WebPageState } from './state-manager.js';
|
|
@@ -166,6 +167,10 @@ export class ExperienceTracker {
|
|
|
166
167
|
writeAction(state: ActionResult, action: ActionInput): void {
|
|
167
168
|
if (this.disabled || this.isWritingDisabled(state)) return;
|
|
168
169
|
if (!action.code?.trim()) return;
|
|
170
|
+
if (isNonReusableCode(action.code)) {
|
|
171
|
+
debugLog('Skipping action with non-reusable code: %s', action.code);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
169
174
|
|
|
170
175
|
this.ensureExperienceFile(state);
|
|
171
176
|
const stateHash = state.getStateHash();
|
|
@@ -189,6 +194,10 @@ export class ExperienceTracker {
|
|
|
189
194
|
writeFlow(state: ActionResult, body: string, relatedUrls?: string[]): void {
|
|
190
195
|
if (this.disabled || this.isWritingDisabled(state)) return;
|
|
191
196
|
if (!body?.trim()) return;
|
|
197
|
+
if (isNonReusableCode(body)) {
|
|
198
|
+
debugLog('Skipping flow body with non-reusable code');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
192
201
|
|
|
193
202
|
this.ensureExperienceFile(state);
|
|
194
203
|
const stateHash = state.getStateHash();
|
package/src/explorer.ts
CHANGED
|
@@ -549,10 +549,7 @@ class Explorer {
|
|
|
549
549
|
if (!this.stateManager.getCurrentState()) return;
|
|
550
550
|
|
|
551
551
|
const lastScreenshot = ActionResult.fromState(this.stateManager.getCurrentState()!).screenshotFile;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const screenshotPath = outputPath('states', lastScreenshot);
|
|
555
|
-
test.addArtifact(screenshotPath);
|
|
552
|
+
test.setActiveNoteScreenshot(lastScreenshot);
|
|
556
553
|
};
|
|
557
554
|
|
|
558
555
|
const dialogHandler = (dialog: any) => {
|
package/src/reporter.ts
CHANGED
|
@@ -110,7 +110,7 @@ export class Reporter {
|
|
|
110
110
|
const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
|
|
111
111
|
const timeoutPromise = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
112
112
|
|
|
113
|
-
const result = await Promise.race([this.client.createRun().then(() => 'success' as const), timeoutPromise]);
|
|
113
|
+
const result = await Promise.race([this.client.createRun({ configuration: { exploratory: true } }).then(() => 'success' as const), timeoutPromise]);
|
|
114
114
|
|
|
115
115
|
if (result === 'timeout') {
|
|
116
116
|
debugLog('Reporter run creation timed out');
|
|
@@ -145,6 +145,7 @@ export class Reporter {
|
|
|
145
145
|
message: note.message,
|
|
146
146
|
status: note.status,
|
|
147
147
|
screenshot: note.screenshot,
|
|
148
|
+
log: note.log,
|
|
148
149
|
}))
|
|
149
150
|
.sort((a, b) => a.startTime - b.startTime);
|
|
150
151
|
|
|
@@ -180,9 +181,18 @@ export class Reporter {
|
|
|
180
181
|
if (noteEntry.screenshot) {
|
|
181
182
|
step.artifacts = [outputPath('states', noteEntry.screenshot)];
|
|
182
183
|
}
|
|
184
|
+
if (noteEntry.log) {
|
|
185
|
+
step.log = noteEntry.log;
|
|
186
|
+
}
|
|
183
187
|
steps.push(step);
|
|
184
188
|
}
|
|
185
189
|
|
|
190
|
+
const verificationStep = this.buildVerificationStep(test, lastScreenshotFile);
|
|
191
|
+
if (verificationStep) {
|
|
192
|
+
steps.push(verificationStep);
|
|
193
|
+
return steps;
|
|
194
|
+
}
|
|
195
|
+
|
|
186
196
|
if (lastScreenshotFile && steps.length > 0) {
|
|
187
197
|
const lastStep = steps[steps.length - 1];
|
|
188
198
|
const screenshotPath = outputPath('states', lastScreenshotFile);
|
|
@@ -196,6 +206,39 @@ export class Reporter {
|
|
|
196
206
|
return steps;
|
|
197
207
|
}
|
|
198
208
|
|
|
209
|
+
private buildVerificationStep(test: Test, lastScreenshotFile?: string): Step | undefined {
|
|
210
|
+
const v = test.verification;
|
|
211
|
+
if (!v) return undefined;
|
|
212
|
+
|
|
213
|
+
const subSteps: Step[] = [];
|
|
214
|
+
if (v.message) subSteps.push({ category: 'framework', title: v.message, duration: 0 });
|
|
215
|
+
if (v.url) {
|
|
216
|
+
subSteps.push({
|
|
217
|
+
category: 'framework',
|
|
218
|
+
title: v.pageLabel ? `Navigated to ${v.pageLabel}` : 'Final page',
|
|
219
|
+
log: v.url,
|
|
220
|
+
duration: 0,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
for (const detail of v.details) {
|
|
224
|
+
subSteps.push({ category: 'framework', title: detail, duration: 0 });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const screenshotFile = v.screenshot || lastScreenshotFile;
|
|
228
|
+
|
|
229
|
+
const step: Step = {
|
|
230
|
+
category: 'user',
|
|
231
|
+
title: 'Verification',
|
|
232
|
+
duration: 0,
|
|
233
|
+
status: v.status || 'none',
|
|
234
|
+
steps: subSteps.length > 0 ? subSteps : undefined,
|
|
235
|
+
};
|
|
236
|
+
if (screenshotFile) {
|
|
237
|
+
step.artifacts = [outputPath('states', screenshotFile)];
|
|
238
|
+
}
|
|
239
|
+
return step;
|
|
240
|
+
}
|
|
241
|
+
|
|
199
242
|
async reportTest(test: Test, meta?: ReporterMeta): Promise<void> {
|
|
200
243
|
await this.startRun();
|
|
201
244
|
|
package/src/stats.ts
CHANGED
|
@@ -4,6 +4,7 @@ interface TokenUsage {
|
|
|
4
4
|
input: number;
|
|
5
5
|
output: number;
|
|
6
6
|
total: number;
|
|
7
|
+
cached?: number;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export type ExplorbotMode = 'explore' | 'test' | 'freesail' | 'tui';
|
|
@@ -20,11 +21,12 @@ export class Stats {
|
|
|
20
21
|
|
|
21
22
|
static recordTokens(_agent: string, model: string, usage: TokenUsage): void {
|
|
22
23
|
if (!Stats.models[model]) {
|
|
23
|
-
Stats.models[model] = { input: 0, output: 0, total: 0 };
|
|
24
|
+
Stats.models[model] = { input: 0, output: 0, total: 0, cached: 0 };
|
|
24
25
|
}
|
|
25
26
|
Stats.models[model].input += usage.input;
|
|
26
27
|
Stats.models[model].output += usage.output;
|
|
27
28
|
Stats.models[model].total += usage.total;
|
|
29
|
+
Stats.models[model].cached = (Stats.models[model].cached ?? 0) + (usage.cached ?? 0);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
static getElapsedTime(): string {
|
package/src/test-plan.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface Note {
|
|
|
26
26
|
startTime: number;
|
|
27
27
|
endTime: number;
|
|
28
28
|
screenshot?: string;
|
|
29
|
+
log?: string;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export class ActiveNote {
|
|
@@ -34,6 +35,7 @@ export class ActiveNote {
|
|
|
34
35
|
message: string;
|
|
35
36
|
status?: TestResultType;
|
|
36
37
|
screenshot?: string;
|
|
38
|
+
log?: string;
|
|
37
39
|
|
|
38
40
|
constructor(task: Task, message: string, status?: TestResultType) {
|
|
39
41
|
this.task = task;
|
|
@@ -73,6 +75,7 @@ export class Task {
|
|
|
73
75
|
steps: Record<string, StepData>;
|
|
74
76
|
states: WebPageState[];
|
|
75
77
|
startUrl: string;
|
|
78
|
+
verification?: Verification;
|
|
76
79
|
protected timestampCounter = 0;
|
|
77
80
|
private activeNote?: ActiveNote;
|
|
78
81
|
|
|
@@ -102,6 +105,7 @@ export class Task {
|
|
|
102
105
|
startTime: activeNote.getStartTime(),
|
|
103
106
|
endTime,
|
|
104
107
|
screenshot: activeNote.screenshot,
|
|
108
|
+
log: activeNote.log,
|
|
105
109
|
};
|
|
106
110
|
this.activeNote = undefined;
|
|
107
111
|
}
|
|
@@ -118,13 +122,28 @@ export class Task {
|
|
|
118
122
|
.join('\n');
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
addNote(message: string, status: TestResultType = null, screenshot?: string): void {
|
|
122
|
-
const isDuplicate = Object.values(this.notes).some((note) => note.message === message && note.status === status);
|
|
125
|
+
addNote(message: string, status: TestResultType = null, screenshot?: string, log?: string): void {
|
|
126
|
+
const isDuplicate = Object.values(this.notes).some((note) => note.message === message && note.status === status && note.log === log);
|
|
123
127
|
if (isDuplicate) return;
|
|
124
128
|
|
|
125
129
|
const now = performance.now();
|
|
126
130
|
const timestamp = `${now}_${this.timestampCounter++}`;
|
|
127
|
-
this.notes[timestamp] = { message, status, startTime: now, endTime: now, screenshot };
|
|
131
|
+
this.notes[timestamp] = { message, status, startTime: now, endTime: now, screenshot, log };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
addUrlNote(state: UrlNoteState, prevState?: { title?: string; h1?: string; h2?: string }): void {
|
|
135
|
+
const fullUrl = state.fullUrl || state.url;
|
|
136
|
+
if (!fullUrl) return;
|
|
137
|
+
|
|
138
|
+
let label: string | undefined;
|
|
139
|
+
if (state.title && state.title !== prevState?.title) label = state.title;
|
|
140
|
+
else if (state.h1 && state.h1 !== prevState?.h1) label = state.h1;
|
|
141
|
+
else if (state.h2 && state.h2 !== prevState?.h2) label = state.h2;
|
|
142
|
+
else label = state.title || state.h1 || state.h2;
|
|
143
|
+
|
|
144
|
+
if (!label) return;
|
|
145
|
+
|
|
146
|
+
this.addNote(`Navigated to ${label}`, TestResult.PASSED, state.screenshotFile, fullUrl);
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
addState(state: WebPageState): void {
|
|
@@ -136,6 +155,28 @@ export class Task {
|
|
|
136
155
|
this.steps[timestamp] = { text, duration, status, error, log, artifacts, noteStartTime: this.activeNote?.getStartTime() };
|
|
137
156
|
}
|
|
138
157
|
|
|
158
|
+
setActiveNoteScreenshot(screenshotFile?: string): void {
|
|
159
|
+
if (!this.activeNote || !screenshotFile) return;
|
|
160
|
+
this.activeNote.screenshot = screenshotFile;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setVerification(message: string, status: TestResultType, state?: UrlNoteState): void {
|
|
164
|
+
this.verification ||= { message: '', status: null, details: [] };
|
|
165
|
+
this.verification.message = message;
|
|
166
|
+
this.verification.status = status;
|
|
167
|
+
if (!state) return;
|
|
168
|
+
if (state.screenshotFile) this.verification.screenshot = state.screenshotFile;
|
|
169
|
+
const fullUrl = state.fullUrl || state.url;
|
|
170
|
+
if (fullUrl) this.verification.url = fullUrl;
|
|
171
|
+
this.verification.pageLabel = state.title || state.h1 || state.h2 || undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
addVerificationDetail(detail: string): void {
|
|
175
|
+
if (!detail) return;
|
|
176
|
+
this.verification ||= { message: '', status: null, details: [] };
|
|
177
|
+
this.verification.details.push(detail);
|
|
178
|
+
}
|
|
179
|
+
|
|
139
180
|
getLog(): Array<{ type: 'step' | 'note' | 'artifact'; content: string; timestamp: number }> {
|
|
140
181
|
const merged: Record<string, { type: 'step' | 'note' | 'artifact'; content: string }> = {};
|
|
141
182
|
|
|
@@ -442,3 +483,21 @@ export class Plan {
|
|
|
442
483
|
return planToAiContext(this, options);
|
|
443
484
|
}
|
|
444
485
|
}
|
|
486
|
+
|
|
487
|
+
interface Verification {
|
|
488
|
+
message: string;
|
|
489
|
+
status: TestResultType;
|
|
490
|
+
screenshot?: string;
|
|
491
|
+
url?: string;
|
|
492
|
+
pageLabel?: string;
|
|
493
|
+
details: string[];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
interface UrlNoteState {
|
|
497
|
+
url?: string;
|
|
498
|
+
fullUrl?: string;
|
|
499
|
+
title?: string;
|
|
500
|
+
h1?: string;
|
|
501
|
+
h2?: string;
|
|
502
|
+
screenshotFile?: string;
|
|
503
|
+
}
|