explorbot 0.1.9 → 0.1.10
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 +70 -8
- package/boat/api-tester/src/ai/curler-tools.ts +3 -3
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/apibot.ts +2 -2
- package/boat/api-tester/src/config.ts +1 -1
- package/dist/bin/explorbot-cli.js +70 -7
- package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
- package/dist/boat/api-tester/src/apibot.js +2 -2
- package/dist/package.json +1 -1
- package/dist/src/ai/bosun.js +5 -1
- package/dist/src/ai/experience-compactor.js +235 -50
- package/dist/src/ai/historian.js +13 -6
- package/dist/src/ai/navigator.js +62 -62
- package/dist/src/ai/pilot.js +22 -0
- package/dist/src/ai/planner/subpages.js +1 -30
- package/dist/src/ai/planner.js +4 -4
- package/dist/src/ai/provider.js +1 -1
- package/dist/src/ai/rerunner.js +3 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/sections.js +8 -1
- package/dist/src/ai/researcher.js +4 -11
- package/dist/src/ai/tools.js +5 -3
- package/dist/src/api/request-store.js +20 -0
- package/dist/src/api/xhr-capture.js +19 -3
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/add-rule-command.js +1 -1
- package/dist/src/commands/base-command.js +20 -0
- package/dist/src/commands/clean-command.js +1 -1
- package/dist/src/commands/compact-command.js +138 -0
- package/dist/src/commands/context-command.js +7 -1
- package/dist/src/commands/drill-command.js +4 -1
- package/dist/src/commands/experience-command.js +104 -0
- package/dist/src/commands/explore-command.js +28 -5
- package/dist/src/commands/freesail-command.js +2 -0
- package/dist/src/commands/index.js +7 -3
- package/dist/src/commands/init-command.js +2 -2
- package/dist/src/commands/learn-command.js +1 -1
- package/dist/src/commands/navigate-command.js +4 -1
- package/dist/src/commands/plan-clear-command.js +4 -1
- package/dist/src/commands/plan-command.js +11 -4
- package/dist/src/commands/plan-edit-command.js +1 -1
- package/dist/src/commands/plan-load-command.js +4 -1
- package/dist/src/commands/plan-reload-command.js +4 -1
- package/dist/src/commands/plan-save-command.js +1 -1
- package/dist/src/commands/research-command.js +5 -2
- package/dist/src/commands/start-command.js +5 -1
- package/dist/src/commands/test-command.js +7 -1
- package/dist/src/experience-tracker.js +191 -56
- package/dist/src/explorbot.js +26 -14
- package/dist/src/explorer.js +3 -3
- package/dist/src/reporter.js +17 -2
- package/dist/src/stats.js +2 -0
- package/dist/src/suite.js +1 -1
- package/dist/src/utils/error-page.js +10 -0
- package/dist/src/utils/logger.js +1 -1
- package/dist/src/utils/rules-loader.js +1 -1
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/url-matcher.js +50 -0
- package/package.json +1 -1
- package/src/ai/bosun.ts +5 -1
- package/src/ai/experience-compactor.ts +270 -63
- package/src/ai/historian.ts +12 -7
- package/src/ai/navigator.ts +68 -66
- package/src/ai/pilot.ts +22 -0
- package/src/ai/planner/subpages.ts +1 -24
- package/src/ai/planner.ts +5 -5
- package/src/ai/provider.ts +1 -1
- package/src/ai/rerunner.ts +3 -3
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +1 -1
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/sections.ts +7 -1
- package/src/ai/researcher.ts +4 -11
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tools.ts +6 -4
- package/src/api/request-store.ts +22 -0
- package/src/api/xhr-capture.ts +21 -3
- package/src/command-handler.ts +1 -1
- package/src/commands/add-rule-command.ts +2 -2
- package/src/commands/base-command.ts +26 -1
- package/src/commands/clean-command.ts +2 -2
- package/src/commands/compact-command.ts +156 -0
- package/src/commands/context-command.ts +8 -2
- package/src/commands/drill-command.ts +5 -2
- package/src/commands/experience-command.ts +125 -0
- package/src/commands/explore-command.ts +30 -7
- package/src/commands/freesail-command.ts +2 -0
- package/src/commands/index.ts +7 -3
- package/src/commands/init-command.ts +2 -2
- package/src/commands/learn-command.ts +2 -2
- package/src/commands/navigate-command.ts +5 -2
- package/src/commands/plan-clear-command.ts +5 -2
- package/src/commands/plan-command.ts +12 -5
- package/src/commands/plan-edit-command.ts +2 -2
- package/src/commands/plan-load-command.ts +5 -2
- package/src/commands/plan-reload-command.ts +5 -2
- package/src/commands/plan-save-command.ts +2 -2
- package/src/commands/research-command.ts +6 -3
- package/src/commands/start-command.ts +6 -2
- package/src/commands/test-command.ts +8 -2
- package/src/experience-tracker.ts +220 -71
- package/src/explorbot.ts +28 -15
- package/src/explorer.ts +3 -3
- package/src/reporter.ts +17 -3
- package/src/stats.ts +4 -0
- package/src/suite.ts +1 -1
- package/src/utils/error-page.ts +10 -0
- package/src/utils/logger.ts +1 -1
- package/src/utils/rules-loader.ts +1 -1
- package/src/utils/test-files.ts +1 -1
- package/src/utils/url-matcher.ts +43 -0
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unlinkSync } from 'node:fs';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
|
-
import
|
|
3
|
+
import { type Tokens, marked } from 'marked';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import type
|
|
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
|
+
import { mdq } from '../utils/markdown-query.js';
|
|
9
|
+
import { generalizeUrl, hasDynamicUrlSegment } from '../utils/url-matcher.js';
|
|
8
10
|
import type { Agent } from './agent.js';
|
|
9
11
|
import type { Provider } from './provider.js';
|
|
10
12
|
|
|
11
13
|
const debugLog = createDebug('explorbot:experience-compactor');
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
filePath: string;
|
|
15
|
-
data: { url?: string; title?: string; [key: string]: any };
|
|
16
|
-
content: string;
|
|
17
|
-
}
|
|
15
|
+
export type { ExperienceFile };
|
|
18
16
|
|
|
19
17
|
interface MergeGroup {
|
|
20
18
|
pattern: string;
|
|
@@ -33,11 +31,12 @@ export class ExperienceCompactor implements Agent {
|
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
async compactExperience(experience: string): Promise<string> {
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const stripped = this.stripNonUsefulEntries(experience);
|
|
35
|
+
if (stripped.length < this.MAX_LENGTH) {
|
|
36
|
+
return stripped;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
const prompt = this.buildCompactionPrompt(
|
|
39
|
+
const prompt = this.buildCompactionPrompt(stripped);
|
|
41
40
|
const model = this.provider.getModelForAgent('experience-compactor');
|
|
42
41
|
const response = await this.provider.chat(
|
|
43
42
|
[
|
|
@@ -50,38 +49,174 @@ export class ExperienceCompactor implements Agent {
|
|
|
50
49
|
return response.text;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
stripNonUsefulEntries(content: string): string {
|
|
53
|
+
let result = dropNonReusableSections(content);
|
|
54
|
+
result = dropEmptySections(result);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
isRecent(file: ExperienceFile): boolean {
|
|
59
|
+
const ageDays = (Date.now() - file.mtime.getTime()) / (1000 * 60 * 60 * 24);
|
|
60
|
+
return ageDays <= RECENT_WINDOW_DAYS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async reviewExperienceQuality(content: string, context: { url?: string; title?: string }): Promise<string> {
|
|
64
|
+
const sections = listSections(content);
|
|
65
|
+
if (sections.length === 0) return content;
|
|
66
|
+
|
|
67
|
+
const prompt = this.buildReviewPrompt(sections, context);
|
|
68
|
+
const schema = z.object({
|
|
69
|
+
sections: z.array(
|
|
70
|
+
z.object({
|
|
71
|
+
index: z.number().int(),
|
|
72
|
+
keep: z.boolean(),
|
|
73
|
+
reason: z.string(),
|
|
74
|
+
})
|
|
75
|
+
),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const model = this.provider.getModelForAgent('experience-compactor');
|
|
80
|
+
const response = await this.provider.generateObject(
|
|
81
|
+
[
|
|
82
|
+
{ role: 'user', content: this.getSystemPrompt() },
|
|
83
|
+
{ role: 'user', content: prompt },
|
|
84
|
+
],
|
|
85
|
+
schema,
|
|
86
|
+
model,
|
|
87
|
+
{ telemetryFunctionId: 'experience.reviewQuality' }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const decisions = response?.object?.sections || [];
|
|
91
|
+
let result = content;
|
|
92
|
+
for (const decision of decisions) {
|
|
93
|
+
if (decision.keep) continue;
|
|
94
|
+
const target = sections[decision.index];
|
|
95
|
+
if (!target) continue;
|
|
96
|
+
debugLog('Dropping section: %s (reason: %s)', target.title, decision.reason);
|
|
97
|
+
result = result.replace(target.raw, '');
|
|
71
98
|
}
|
|
99
|
+
return result;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
debugLog('AI quality review failed, keeping content unchanged: %s', error);
|
|
102
|
+
return content;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async compactAllExperiences(): Promise<{ merged: number; compacted: number }> {
|
|
107
|
+
return Observability.run('experience-compactor.compactAll', { tags: ['experience-compactor'] }, async () => {
|
|
108
|
+
const aiMerged = await this.mergeSimilarExperiences();
|
|
109
|
+
const generalized = this.generalizeDynamicUrls();
|
|
110
|
+
const compacted = await this.compactFiles(this.experienceTracker.getAllExperience());
|
|
111
|
+
return { merged: aiMerged + generalized, compacted };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
72
114
|
|
|
73
|
-
|
|
115
|
+
async autocompact(): Promise<{ merged: number; compacted: number }> {
|
|
116
|
+
return Observability.run('experience-compactor.autocompact', { tags: ['experience-compactor'] }, async () => {
|
|
117
|
+
const aiMerged = await this.mergeSimilarExperiences({ onlyDynamic: true });
|
|
118
|
+
const generalized = this.generalizeDynamicUrls();
|
|
119
|
+
const compacted = await this.compactFiles(this.experienceTracker.getAllExperience(), { skipSmall: true });
|
|
120
|
+
return { merged: aiMerged + generalized, compacted };
|
|
74
121
|
});
|
|
75
122
|
}
|
|
76
123
|
|
|
77
|
-
|
|
124
|
+
generalizeDynamicUrls(): number {
|
|
125
|
+
const files = this.experienceTracker.getAllExperience();
|
|
126
|
+
let count = 0;
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const url = file.data.url as string | undefined;
|
|
129
|
+
if (!url || url.startsWith('~')) continue;
|
|
130
|
+
const generalized = generalizeUrl(url);
|
|
131
|
+
if (generalized === url) continue;
|
|
132
|
+
const stateHash = file.filePath.split('/').pop()?.replace('.md', '') || '';
|
|
133
|
+
const newData = { ...file.data, url: `~${generalized}~`, mergedFrom: [url] };
|
|
134
|
+
this.experienceTracker.writeExperienceFile(stateHash, file.content, newData);
|
|
135
|
+
tag('substep').log(`Generalized URL: ${url} → ~${generalized}~`);
|
|
136
|
+
count++;
|
|
137
|
+
}
|
|
138
|
+
return count;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async compactFiles(files: ExperienceFile[], options?: { skipSmall?: boolean }): Promise<number> {
|
|
142
|
+
const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >= this.MAX_LENGTH) : files;
|
|
143
|
+
|
|
144
|
+
let compactedCount = 0;
|
|
145
|
+
const total = workingSet.length;
|
|
146
|
+
const aiReviewCount = workingSet.filter((f) => this.isRecent(f)).length;
|
|
147
|
+
|
|
148
|
+
if (total > 1) {
|
|
149
|
+
tag('info').log(`Processing ${total} experience file${total === 1 ? '' : 's'} (${aiReviewCount} will get AI review — ~${aiReviewCount * 3}s minimum)…`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < workingSet.length; i++) {
|
|
153
|
+
const experience = workingSet[i];
|
|
154
|
+
const shortName = experience.filePath.split('/').pop() || experience.filePath;
|
|
155
|
+
const willReview = this.isRecent(experience);
|
|
156
|
+
|
|
157
|
+
if (total > 1 && willReview) {
|
|
158
|
+
tag('substep').log(`[${i + 1}/${total}] reviewing ${shortName}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let content = this.stripNonUsefulEntries(experience.content);
|
|
162
|
+
|
|
163
|
+
if (willReview) {
|
|
164
|
+
content = await this.reviewExperienceQuality(content, experience.data);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (content.length >= this.MAX_LENGTH) {
|
|
168
|
+
if (total > 1) tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${this.MAX_LENGTH} chars)`);
|
|
169
|
+
const prompt = this.buildCompactionPrompt(content);
|
|
170
|
+
const model = this.provider.getModelForAgent('experience-compactor');
|
|
171
|
+
const response = await this.provider.chat(
|
|
172
|
+
[
|
|
173
|
+
{ role: 'user', content: this.getSystemPrompt() },
|
|
174
|
+
{ role: 'user', content: prompt },
|
|
175
|
+
],
|
|
176
|
+
model,
|
|
177
|
+
{ telemetryFunctionId: 'experience.compact' }
|
|
178
|
+
);
|
|
179
|
+
content = response.text;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (content === experience.content) continue;
|
|
183
|
+
|
|
184
|
+
const stateHash = experience.filePath.split('/').pop()?.replace('.md', '') || '';
|
|
185
|
+
this.experienceTracker.writeExperienceFile(stateHash, content, experience.data);
|
|
186
|
+
debugLog('Experience file compacted:', experience.filePath);
|
|
187
|
+
compactedCount++;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return compactedCount;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async mergeSimilarExperiences(options?: { onlyDynamic?: boolean }): Promise<number> {
|
|
78
194
|
return Observability.run('experience-compactor.merge', { tags: ['experience-compactor'] }, async () => {
|
|
79
195
|
const experienceFiles = this.experienceTracker.getAllExperience();
|
|
80
196
|
if (experienceFiles.length < 2) {
|
|
81
197
|
return 0;
|
|
82
198
|
}
|
|
83
199
|
|
|
84
|
-
|
|
200
|
+
let candidates = experienceFiles.filter((f) => f.data.url && !f.data.url.startsWith('~'));
|
|
201
|
+
if (options?.onlyDynamic) {
|
|
202
|
+
candidates = candidates.filter((f) => hasDynamicUrlSegment(f.data.url as string));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (candidates.length < 2) {
|
|
206
|
+
debugLog('No mergeable URL patterns — skipping merge.');
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
tag('info').log(`Experience compaction: checking ${candidates.length} file${candidates.length === 1 ? '' : 's'} for mergeable URL patterns…`);
|
|
211
|
+
|
|
212
|
+
const mergeGroups = await this.identifyMergeGroups(candidates);
|
|
213
|
+
|
|
214
|
+
if (mergeGroups.length === 0) {
|
|
215
|
+
tag('info').log('No URL groups to merge.');
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
tag('info').log(`Merging ${mergeGroups.length} URL group${mergeGroups.length === 1 ? '' : 's'}…`);
|
|
85
220
|
let mergedCount = 0;
|
|
86
221
|
|
|
87
222
|
for (const group of mergeGroups) {
|
|
@@ -110,7 +245,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
110
245
|
const usedFiles = new Set<string>();
|
|
111
246
|
|
|
112
247
|
for (const decision of mergeDecisions) {
|
|
113
|
-
const matchingFiles =
|
|
248
|
+
const matchingFiles = decision.urls.map((url) => filesWithUrl.find((f) => f.data.url === url && !usedFiles.has(f.filePath))).filter((f): f is ExperienceFile => Boolean(f));
|
|
114
249
|
|
|
115
250
|
if (matchingFiles.length >= 2) {
|
|
116
251
|
groups.push({
|
|
@@ -200,28 +335,6 @@ export class ExperienceCompactor implements Agent {
|
|
|
200
335
|
}
|
|
201
336
|
}
|
|
202
337
|
|
|
203
|
-
async compactExperienceFile(filePath: string): Promise<string> {
|
|
204
|
-
try {
|
|
205
|
-
const fileContent = readFileSync(filePath, 'utf8');
|
|
206
|
-
const parsed = matter(fileContent);
|
|
207
|
-
|
|
208
|
-
if (parsed.content.length < this.MAX_LENGTH) {
|
|
209
|
-
return parsed.content;
|
|
210
|
-
}
|
|
211
|
-
debugLog('Experience file to compact:', filePath);
|
|
212
|
-
|
|
213
|
-
const text = await this.compactExperience(parsed.content);
|
|
214
|
-
|
|
215
|
-
tag('substep').log('Experience file compacted:', filePath);
|
|
216
|
-
debugLog('Experience file compacted:', text);
|
|
217
|
-
|
|
218
|
-
return text;
|
|
219
|
-
} catch (error) {
|
|
220
|
-
debugLog('Error compacting experience file:', error);
|
|
221
|
-
return '';
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
338
|
private getSystemPrompt(): string {
|
|
226
339
|
const customPrompt = this.provider.getSystemPromptForAgent('experience-compactor', '*');
|
|
227
340
|
return dedent`
|
|
@@ -235,7 +348,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
235
348
|
private buildCompactionPrompt(content: string): string {
|
|
236
349
|
return dedent`
|
|
237
350
|
<rules>
|
|
238
|
-
- Use markdown headers only (
|
|
351
|
+
- Use markdown h2 headers only (##) - NO XML tags or wrappers in output
|
|
239
352
|
- Merge similar flows to remove duplicates
|
|
240
353
|
- Keep output under ${this.MAX_LENGTH} characters
|
|
241
354
|
- Be explicit and short - no proposals or explanations
|
|
@@ -255,20 +368,18 @@ export class ExperienceCompactor implements Agent {
|
|
|
255
368
|
</rules>
|
|
256
369
|
|
|
257
370
|
<output_format>
|
|
258
|
-
Use this markdown structure:
|
|
371
|
+
Use this markdown structure. Titles must be imperative verb phrases, lowercase-first (e.g. "create a new user"):
|
|
259
372
|
|
|
260
|
-
##
|
|
373
|
+
## FLOW: <multi-step imperative title>
|
|
261
374
|
|
|
262
|
-
|
|
263
|
-
- Purpose: what was accomplished
|
|
375
|
+
* <step message>
|
|
264
376
|
\`\`\`js
|
|
265
377
|
// working code
|
|
266
378
|
\`\`\`
|
|
379
|
+
---
|
|
267
380
|
|
|
268
|
-
##
|
|
381
|
+
## ACTION: <single-step imperative title>
|
|
269
382
|
|
|
270
|
-
For each reusable interaction pattern not covered by flows:
|
|
271
|
-
- Purpose: what was accomplished
|
|
272
383
|
\`\`\`js
|
|
273
384
|
// working code
|
|
274
385
|
\`\`\`
|
|
@@ -278,7 +389,103 @@ export class ExperienceCompactor implements Agent {
|
|
|
278
389
|
${content}
|
|
279
390
|
</context>
|
|
280
391
|
|
|
281
|
-
Compact this experience data following the format above.
|
|
392
|
+
Compact this experience data following the format above. Every section must be either a multi-step FLOW or a single-step ACTION.
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private buildReviewPrompt(sections: Array<{ title: string; raw: string }>, context: { url?: string; title?: string }): string {
|
|
397
|
+
const url = context.url || 'unknown';
|
|
398
|
+
const title = context.title || 'unknown';
|
|
399
|
+
const sectionsList = sections
|
|
400
|
+
.map((s, i) => {
|
|
401
|
+
return `Section ${i}: ${s.title}\n${s.raw.trim()}`;
|
|
402
|
+
})
|
|
403
|
+
.join('\n\n---\n\n');
|
|
404
|
+
|
|
405
|
+
return dedent`
|
|
406
|
+
Review each experience section stored for the page below. For each, decide whether to keep it for future test automation use.
|
|
407
|
+
|
|
408
|
+
<page>
|
|
409
|
+
url: ${url}
|
|
410
|
+
title: ${title}
|
|
411
|
+
</page>
|
|
412
|
+
|
|
413
|
+
<mental_model>
|
|
414
|
+
Every section answers the question "HOW to <title>?". If the title does not complete that question naturally, the section has no value. A FLOW teaches a multi-step procedure; an ACTION teaches a single atomic step. Verifications and one-off recoveries are out of scope.
|
|
415
|
+
</mental_model>
|
|
416
|
+
|
|
417
|
+
<drop_if>
|
|
418
|
+
- Title does not read as a reusable instruction — it describes a transient recovery, retry, or navigation step. Any title starting with "attempt", "try", "retry", "need to", "ensure", "go back", "return (to)", or ending with "(RESET)" is not a teaching.
|
|
419
|
+
- Title is a verification / assertion rather than an action. Any "verify", "verification", "see that", "check that", "expect", "assert", or "and verify" phrasing means the teaching is about asserting state, not doing something — drop.
|
|
420
|
+
- ACTION describes more than one atomic step. Multiple verb phrases, "and", "then", or comma-joined actions mean it is actually a FLOW, not an ACTION — drop so the flow gets re-captured correctly.
|
|
421
|
+
- Locator is dynamic / brittle: ember IDs, random UUIDs, numeric data-testid, positional XPaths like div[3].
|
|
422
|
+
- Too generic to reuse: "click button", "fill input" with no specific target.
|
|
423
|
+
- Duplicates another section on this same page.
|
|
424
|
+
- Body has no executable CodeceptJS code block.
|
|
425
|
+
</drop_if>
|
|
426
|
+
|
|
427
|
+
<keep_if>
|
|
428
|
+
Everything else. Prefer keeping sections — only drop when clearly low value.
|
|
429
|
+
</keep_if>
|
|
430
|
+
|
|
431
|
+
<sections>
|
|
432
|
+
${sectionsList}
|
|
433
|
+
</sections>
|
|
434
|
+
|
|
435
|
+
Return a decision per section: { index, keep (boolean), reason (short) }.
|
|
282
436
|
`;
|
|
283
437
|
}
|
|
284
438
|
}
|
|
439
|
+
|
|
440
|
+
function listSections(content: string): { title: string; raw: string }[] {
|
|
441
|
+
const tokens = marked.lexer(content);
|
|
442
|
+
const sections: { title: string; raw: string }[] = [];
|
|
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;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function dropEmptySections(content: string): string {
|
|
466
|
+
let result = content;
|
|
467
|
+
const sections = [...mdq(result).query('section2(~"FLOW:")').each(), ...mdq(result).query('section2(~"ACTION:")').each()];
|
|
468
|
+
|
|
469
|
+
for (const section of sections) {
|
|
470
|
+
const raw = section.text();
|
|
471
|
+
const hasCode = mdq(raw).query('code').count() > 0;
|
|
472
|
+
const hasList = mdq(raw).query('list').count() > 0;
|
|
473
|
+
if (hasCode || hasList) continue;
|
|
474
|
+
result = result.replace(raw, '');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function dropNonReusableSections(content: string): string {
|
|
481
|
+
let result = content;
|
|
482
|
+
const sections = [...mdq(result).query('section2(~"FLOW:")').each(), ...mdq(result).query('section2(~"ACTION:")').each()];
|
|
483
|
+
|
|
484
|
+
for (const section of sections) {
|
|
485
|
+
const raw = section.text();
|
|
486
|
+
if (!/\bI\.clickXY\s*\(/.test(raw)) continue;
|
|
487
|
+
result = result.replace(raw, '');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return result;
|
|
491
|
+
}
|
package/src/ai/historian.ts
CHANGED
|
@@ -4,15 +4,15 @@ import dedent from 'dedent';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { ActionResult } from '../action-result.ts';
|
|
6
6
|
import { ConfigParser } from '../config.ts';
|
|
7
|
+
import { ExperienceTracker, type SessionStep } from '../experience-tracker.ts';
|
|
7
8
|
import { KnowledgeTracker } from '../knowledge-tracker.ts';
|
|
8
|
-
import { ExperienceTracker, type SessionExperienceEntry, type SessionStep } from '../experience-tracker.ts';
|
|
9
9
|
import { type Reporter, type ReporterStep } from '../reporter.ts';
|
|
10
10
|
import type { StateManager } from '../state-manager.ts';
|
|
11
11
|
import { type Plan, type Task, Test } from '../test-plan.ts';
|
|
12
12
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
13
|
+
import { extractStatePath } from '../utils/url-matcher.ts';
|
|
13
14
|
import type { Conversation, ToolExecution } from './conversation.ts';
|
|
14
15
|
import type { Provider } from './provider.ts';
|
|
15
|
-
import { extractStatePath } from '../utils/url-matcher.ts';
|
|
16
16
|
import { ASSERTION_TOOLS, CODECEPT_TOOLS } from './tools.ts';
|
|
17
17
|
|
|
18
18
|
const debugLog = createDebug('explorbot:historian');
|
|
@@ -51,13 +51,11 @@ export class Historian {
|
|
|
51
51
|
|
|
52
52
|
if (verifiedSteps.length > 0) {
|
|
53
53
|
const relatedUrls = this.extractVisitedUrls(toolExecutions, initialState.url || '');
|
|
54
|
-
|
|
54
|
+
this.experienceTracker.writeFlow(initialState, {
|
|
55
55
|
scenario: task.description,
|
|
56
|
-
result,
|
|
57
56
|
steps: verifiedSteps,
|
|
58
57
|
relatedUrls,
|
|
59
|
-
};
|
|
60
|
-
this.experienceTracker.saveSessionExperience(initialState, entry);
|
|
58
|
+
});
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
if (task instanceof Test && result !== 'failed') {
|
|
@@ -87,6 +85,7 @@ export class Historian {
|
|
|
87
85
|
if (!CODECEPT_TOOLS.includes(exec.toolName as any)) continue;
|
|
88
86
|
if (!exec.output?.code) continue;
|
|
89
87
|
if (!exec.wasSuccessful) continue;
|
|
88
|
+
if (isNonReusableCode(exec.output.code)) continue;
|
|
90
89
|
|
|
91
90
|
const message = this.getExecutionLabel(exec, `Executed ${exec.toolName}`);
|
|
92
91
|
const ariaDiff = exec.output?.pageDiff?.ariaChanges || null;
|
|
@@ -252,7 +251,8 @@ export class Historian {
|
|
|
252
251
|
}
|
|
253
252
|
}
|
|
254
253
|
|
|
255
|
-
|
|
254
|
+
if (isNonReusableCode(candidate.success.output.code)) continue;
|
|
255
|
+
this.experienceTracker.writeAction(state, { title: pattern.intent, code: candidate.success.output.code, explanation: pattern.explanation });
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
debugLog('Detected %d retry patterns', response?.object?.retryPatterns?.length || 0);
|
|
@@ -372,6 +372,7 @@ export class Historian {
|
|
|
372
372
|
lines.push(`Scenario('${this.escapeString(scenario)}', ({ I }) => {`);
|
|
373
373
|
|
|
374
374
|
for (const exec of successfulSteps) {
|
|
375
|
+
if (isNonReusableCode(exec.output.code)) continue;
|
|
375
376
|
const explanation = this.getExecutionLabel(exec);
|
|
376
377
|
if (explanation) {
|
|
377
378
|
lines.push('');
|
|
@@ -496,3 +497,7 @@ export class Historian {
|
|
|
496
497
|
.join('\n');
|
|
497
498
|
}
|
|
498
499
|
}
|
|
500
|
+
|
|
501
|
+
export function isNonReusableCode(code: string): boolean {
|
|
502
|
+
return /\bI\.clickXY\s*\(/.test(code);
|
|
503
|
+
}
|