explorbot 0.1.8 → 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 +33 -7
- 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 +35 -9
- 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,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unlinkSync } from 'node:fs';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
|
-
import
|
|
3
|
+
import { marked } from 'marked';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { RECENT_WINDOW_DAYS } from '../experience-tracker.js';
|
|
5
6
|
import { Observability } from '../observability.js';
|
|
6
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';
|
|
7
10
|
const debugLog = createDebug('explorbot:experience-compactor');
|
|
8
11
|
export class ExperienceCompactor {
|
|
9
12
|
emoji = '🗜️';
|
|
@@ -15,10 +18,11 @@ export class ExperienceCompactor {
|
|
|
15
18
|
this.experienceTracker = experienceTracker;
|
|
16
19
|
}
|
|
17
20
|
async compactExperience(experience) {
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const stripped = this.stripNonUsefulEntries(experience);
|
|
22
|
+
if (stripped.length < this.MAX_LENGTH) {
|
|
23
|
+
return stripped;
|
|
20
24
|
}
|
|
21
|
-
const prompt = this.buildCompactionPrompt(
|
|
25
|
+
const prompt = this.buildCompactionPrompt(stripped);
|
|
22
26
|
const model = this.provider.getModelForAgent('experience-compactor');
|
|
23
27
|
const response = await this.provider.chat([
|
|
24
28
|
{ role: 'user', content: this.getSystemPrompt() },
|
|
@@ -26,32 +30,145 @@ export class ExperienceCompactor {
|
|
|
26
30
|
], model, { telemetryFunctionId: 'experience.compact' });
|
|
27
31
|
return response.text;
|
|
28
32
|
}
|
|
33
|
+
stripNonUsefulEntries(content) {
|
|
34
|
+
let result = dropNonReusableSections(content);
|
|
35
|
+
result = dropEmptySections(result);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
isRecent(file) {
|
|
39
|
+
const ageDays = (Date.now() - file.mtime.getTime()) / (1000 * 60 * 60 * 24);
|
|
40
|
+
return ageDays <= RECENT_WINDOW_DAYS;
|
|
41
|
+
}
|
|
42
|
+
async reviewExperienceQuality(content, context) {
|
|
43
|
+
const sections = listSections(content);
|
|
44
|
+
if (sections.length === 0)
|
|
45
|
+
return content;
|
|
46
|
+
const prompt = this.buildReviewPrompt(sections, context);
|
|
47
|
+
const schema = z.object({
|
|
48
|
+
sections: z.array(z.object({
|
|
49
|
+
index: z.number().int(),
|
|
50
|
+
keep: z.boolean(),
|
|
51
|
+
reason: z.string(),
|
|
52
|
+
})),
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
const model = this.provider.getModelForAgent('experience-compactor');
|
|
56
|
+
const response = await this.provider.generateObject([
|
|
57
|
+
{ role: 'user', content: this.getSystemPrompt() },
|
|
58
|
+
{ role: 'user', content: prompt },
|
|
59
|
+
], schema, model, { telemetryFunctionId: 'experience.reviewQuality' });
|
|
60
|
+
const decisions = response?.object?.sections || [];
|
|
61
|
+
let result = content;
|
|
62
|
+
for (const decision of decisions) {
|
|
63
|
+
if (decision.keep)
|
|
64
|
+
continue;
|
|
65
|
+
const target = sections[decision.index];
|
|
66
|
+
if (!target)
|
|
67
|
+
continue;
|
|
68
|
+
debugLog('Dropping section: %s (reason: %s)', target.title, decision.reason);
|
|
69
|
+
result = result.replace(target.raw, '');
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
debugLog('AI quality review failed, keeping content unchanged: %s', error);
|
|
75
|
+
return content;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
29
78
|
async compactAllExperiences() {
|
|
30
79
|
return Observability.run('experience-compactor.compactAll', { tags: ['experience-compactor'] }, async () => {
|
|
31
|
-
await this.mergeSimilarExperiences();
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return compactedCount;
|
|
80
|
+
const aiMerged = await this.mergeSimilarExperiences();
|
|
81
|
+
const generalized = this.generalizeDynamicUrls();
|
|
82
|
+
const compacted = await this.compactFiles(this.experienceTracker.getAllExperience());
|
|
83
|
+
return { merged: aiMerged + generalized, compacted };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async autocompact() {
|
|
87
|
+
return Observability.run('experience-compactor.autocompact', { tags: ['experience-compactor'] }, async () => {
|
|
88
|
+
const aiMerged = await this.mergeSimilarExperiences({ onlyDynamic: true });
|
|
89
|
+
const generalized = this.generalizeDynamicUrls();
|
|
90
|
+
const compacted = await this.compactFiles(this.experienceTracker.getAllExperience(), { skipSmall: true });
|
|
91
|
+
return { merged: aiMerged + generalized, compacted };
|
|
46
92
|
});
|
|
47
93
|
}
|
|
48
|
-
|
|
94
|
+
generalizeDynamicUrls() {
|
|
95
|
+
const files = this.experienceTracker.getAllExperience();
|
|
96
|
+
let count = 0;
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
const url = file.data.url;
|
|
99
|
+
if (!url || url.startsWith('~'))
|
|
100
|
+
continue;
|
|
101
|
+
const generalized = generalizeUrl(url);
|
|
102
|
+
if (generalized === url)
|
|
103
|
+
continue;
|
|
104
|
+
const stateHash = file.filePath.split('/').pop()?.replace('.md', '') || '';
|
|
105
|
+
const newData = { ...file.data, url: `~${generalized}~`, mergedFrom: [url] };
|
|
106
|
+
this.experienceTracker.writeExperienceFile(stateHash, file.content, newData);
|
|
107
|
+
tag('substep').log(`Generalized URL: ${url} → ~${generalized}~`);
|
|
108
|
+
count++;
|
|
109
|
+
}
|
|
110
|
+
return count;
|
|
111
|
+
}
|
|
112
|
+
async compactFiles(files, options) {
|
|
113
|
+
const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >= this.MAX_LENGTH) : files;
|
|
114
|
+
let compactedCount = 0;
|
|
115
|
+
const total = workingSet.length;
|
|
116
|
+
const aiReviewCount = workingSet.filter((f) => this.isRecent(f)).length;
|
|
117
|
+
if (total > 1) {
|
|
118
|
+
tag('info').log(`Processing ${total} experience file${total === 1 ? '' : 's'} (${aiReviewCount} will get AI review — ~${aiReviewCount * 3}s minimum)…`);
|
|
119
|
+
}
|
|
120
|
+
for (let i = 0; i < workingSet.length; i++) {
|
|
121
|
+
const experience = workingSet[i];
|
|
122
|
+
const shortName = experience.filePath.split('/').pop() || experience.filePath;
|
|
123
|
+
const willReview = this.isRecent(experience);
|
|
124
|
+
if (total > 1 && willReview) {
|
|
125
|
+
tag('substep').log(`[${i + 1}/${total}] reviewing ${shortName}`);
|
|
126
|
+
}
|
|
127
|
+
let content = this.stripNonUsefulEntries(experience.content);
|
|
128
|
+
if (willReview) {
|
|
129
|
+
content = await this.reviewExperienceQuality(content, experience.data);
|
|
130
|
+
}
|
|
131
|
+
if (content.length >= this.MAX_LENGTH) {
|
|
132
|
+
if (total > 1)
|
|
133
|
+
tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${this.MAX_LENGTH} chars)`);
|
|
134
|
+
const prompt = this.buildCompactionPrompt(content);
|
|
135
|
+
const model = this.provider.getModelForAgent('experience-compactor');
|
|
136
|
+
const response = await this.provider.chat([
|
|
137
|
+
{ role: 'user', content: this.getSystemPrompt() },
|
|
138
|
+
{ role: 'user', content: prompt },
|
|
139
|
+
], model, { telemetryFunctionId: 'experience.compact' });
|
|
140
|
+
content = response.text;
|
|
141
|
+
}
|
|
142
|
+
if (content === experience.content)
|
|
143
|
+
continue;
|
|
144
|
+
const stateHash = experience.filePath.split('/').pop()?.replace('.md', '') || '';
|
|
145
|
+
this.experienceTracker.writeExperienceFile(stateHash, content, experience.data);
|
|
146
|
+
debugLog('Experience file compacted:', experience.filePath);
|
|
147
|
+
compactedCount++;
|
|
148
|
+
}
|
|
149
|
+
return compactedCount;
|
|
150
|
+
}
|
|
151
|
+
async mergeSimilarExperiences(options) {
|
|
49
152
|
return Observability.run('experience-compactor.merge', { tags: ['experience-compactor'] }, async () => {
|
|
50
153
|
const experienceFiles = this.experienceTracker.getAllExperience();
|
|
51
154
|
if (experienceFiles.length < 2) {
|
|
52
155
|
return 0;
|
|
53
156
|
}
|
|
54
|
-
|
|
157
|
+
let candidates = experienceFiles.filter((f) => f.data.url && !f.data.url.startsWith('~'));
|
|
158
|
+
if (options?.onlyDynamic) {
|
|
159
|
+
candidates = candidates.filter((f) => hasDynamicUrlSegment(f.data.url));
|
|
160
|
+
}
|
|
161
|
+
if (candidates.length < 2) {
|
|
162
|
+
debugLog('No mergeable URL patterns — skipping merge.');
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
tag('info').log(`Experience compaction: checking ${candidates.length} file${candidates.length === 1 ? '' : 's'} for mergeable URL patterns…`);
|
|
166
|
+
const mergeGroups = await this.identifyMergeGroups(candidates);
|
|
167
|
+
if (mergeGroups.length === 0) {
|
|
168
|
+
tag('info').log('No URL groups to merge.');
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
tag('info').log(`Merging ${mergeGroups.length} URL group${mergeGroups.length === 1 ? '' : 's'}…`);
|
|
55
172
|
let mergedCount = 0;
|
|
56
173
|
for (const group of mergeGroups) {
|
|
57
174
|
if (group.files.length < 2) {
|
|
@@ -73,7 +190,7 @@ export class ExperienceCompactor {
|
|
|
73
190
|
const groups = [];
|
|
74
191
|
const usedFiles = new Set();
|
|
75
192
|
for (const decision of mergeDecisions) {
|
|
76
|
-
const matchingFiles =
|
|
193
|
+
const matchingFiles = decision.urls.map((url) => filesWithUrl.find((f) => f.data.url === url && !usedFiles.has(f.filePath))).filter((f) => Boolean(f));
|
|
77
194
|
if (matchingFiles.length >= 2) {
|
|
78
195
|
groups.push({
|
|
79
196
|
pattern: decision.pattern,
|
|
@@ -152,24 +269,6 @@ export class ExperienceCompactor {
|
|
|
152
269
|
}
|
|
153
270
|
}
|
|
154
271
|
}
|
|
155
|
-
async compactExperienceFile(filePath) {
|
|
156
|
-
try {
|
|
157
|
-
const fileContent = readFileSync(filePath, 'utf8');
|
|
158
|
-
const parsed = matter(fileContent);
|
|
159
|
-
if (parsed.content.length < this.MAX_LENGTH) {
|
|
160
|
-
return parsed.content;
|
|
161
|
-
}
|
|
162
|
-
debugLog('Experience file to compact:', filePath);
|
|
163
|
-
const text = await this.compactExperience(parsed.content);
|
|
164
|
-
tag('substep').log('Experience file compacted:', filePath);
|
|
165
|
-
debugLog('Experience file compacted:', text);
|
|
166
|
-
return text;
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
debugLog('Error compacting experience file:', error);
|
|
170
|
-
return '';
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
272
|
getSystemPrompt() {
|
|
174
273
|
const customPrompt = this.provider.getSystemPromptForAgent('experience-compactor', '*');
|
|
175
274
|
return dedent `
|
|
@@ -182,7 +281,7 @@ export class ExperienceCompactor {
|
|
|
182
281
|
buildCompactionPrompt(content) {
|
|
183
282
|
return dedent `
|
|
184
283
|
<rules>
|
|
185
|
-
- Use markdown headers only (
|
|
284
|
+
- Use markdown h2 headers only (##) - NO XML tags or wrappers in output
|
|
186
285
|
- Merge similar flows to remove duplicates
|
|
187
286
|
- Keep output under ${this.MAX_LENGTH} characters
|
|
188
287
|
- Be explicit and short - no proposals or explanations
|
|
@@ -202,20 +301,18 @@ export class ExperienceCompactor {
|
|
|
202
301
|
</rules>
|
|
203
302
|
|
|
204
303
|
<output_format>
|
|
205
|
-
Use this markdown structure:
|
|
304
|
+
Use this markdown structure. Titles must be imperative verb phrases, lowercase-first (e.g. "create a new user"):
|
|
206
305
|
|
|
207
|
-
##
|
|
306
|
+
## FLOW: <multi-step imperative title>
|
|
208
307
|
|
|
209
|
-
|
|
210
|
-
- Purpose: what was accomplished
|
|
308
|
+
* <step message>
|
|
211
309
|
\`\`\`js
|
|
212
310
|
// working code
|
|
213
311
|
\`\`\`
|
|
312
|
+
---
|
|
214
313
|
|
|
215
|
-
##
|
|
314
|
+
## ACTION: <single-step imperative title>
|
|
216
315
|
|
|
217
|
-
For each reusable interaction pattern not covered by flows:
|
|
218
|
-
- Purpose: what was accomplished
|
|
219
316
|
\`\`\`js
|
|
220
317
|
// working code
|
|
221
318
|
\`\`\`
|
|
@@ -225,7 +322,95 @@ export class ExperienceCompactor {
|
|
|
225
322
|
${content}
|
|
226
323
|
</context>
|
|
227
324
|
|
|
228
|
-
Compact this experience data following the format above.
|
|
325
|
+
Compact this experience data following the format above. Every section must be either a multi-step FLOW or a single-step ACTION.
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
buildReviewPrompt(sections, context) {
|
|
329
|
+
const url = context.url || 'unknown';
|
|
330
|
+
const title = context.title || 'unknown';
|
|
331
|
+
const sectionsList = sections
|
|
332
|
+
.map((s, i) => {
|
|
333
|
+
return `Section ${i}: ${s.title}\n${s.raw.trim()}`;
|
|
334
|
+
})
|
|
335
|
+
.join('\n\n---\n\n');
|
|
336
|
+
return dedent `
|
|
337
|
+
Review each experience section stored for the page below. For each, decide whether to keep it for future test automation use.
|
|
338
|
+
|
|
339
|
+
<page>
|
|
340
|
+
url: ${url}
|
|
341
|
+
title: ${title}
|
|
342
|
+
</page>
|
|
343
|
+
|
|
344
|
+
<mental_model>
|
|
345
|
+
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.
|
|
346
|
+
</mental_model>
|
|
347
|
+
|
|
348
|
+
<drop_if>
|
|
349
|
+
- 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.
|
|
350
|
+
- 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.
|
|
351
|
+
- 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.
|
|
352
|
+
- Locator is dynamic / brittle: ember IDs, random UUIDs, numeric data-testid, positional XPaths like div[3].
|
|
353
|
+
- Too generic to reuse: "click button", "fill input" with no specific target.
|
|
354
|
+
- Duplicates another section on this same page.
|
|
355
|
+
- Body has no executable CodeceptJS code block.
|
|
356
|
+
</drop_if>
|
|
357
|
+
|
|
358
|
+
<keep_if>
|
|
359
|
+
Everything else. Prefer keeping sections — only drop when clearly low value.
|
|
360
|
+
</keep_if>
|
|
361
|
+
|
|
362
|
+
<sections>
|
|
363
|
+
${sectionsList}
|
|
364
|
+
</sections>
|
|
365
|
+
|
|
366
|
+
Return a decision per section: { index, keep (boolean), reason (short) }.
|
|
229
367
|
`;
|
|
230
368
|
}
|
|
231
369
|
}
|
|
370
|
+
function listSections(content) {
|
|
371
|
+
const tokens = marked.lexer(content);
|
|
372
|
+
const sections = [];
|
|
373
|
+
let currentHeading = null;
|
|
374
|
+
let currentRaw = '';
|
|
375
|
+
const flush = () => {
|
|
376
|
+
if (currentHeading !== null)
|
|
377
|
+
sections.push({ title: currentHeading, raw: currentRaw });
|
|
378
|
+
};
|
|
379
|
+
for (const token of tokens) {
|
|
380
|
+
const raw = token.raw || '';
|
|
381
|
+
if (token.type === 'heading' && token.depth === 2) {
|
|
382
|
+
flush();
|
|
383
|
+
currentHeading = token.text.trim();
|
|
384
|
+
currentRaw = raw;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (currentHeading !== null)
|
|
388
|
+
currentRaw += raw;
|
|
389
|
+
}
|
|
390
|
+
flush();
|
|
391
|
+
return sections;
|
|
392
|
+
}
|
|
393
|
+
function dropEmptySections(content) {
|
|
394
|
+
let result = content;
|
|
395
|
+
const sections = [...mdq(result).query('section2(~"FLOW:")').each(), ...mdq(result).query('section2(~"ACTION:")').each()];
|
|
396
|
+
for (const section of sections) {
|
|
397
|
+
const raw = section.text();
|
|
398
|
+
const hasCode = mdq(raw).query('code').count() > 0;
|
|
399
|
+
const hasList = mdq(raw).query('list').count() > 0;
|
|
400
|
+
if (hasCode || hasList)
|
|
401
|
+
continue;
|
|
402
|
+
result = result.replace(raw, '');
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
function dropNonReusableSections(content) {
|
|
407
|
+
let result = content;
|
|
408
|
+
const sections = [...mdq(result).query('section2(~"FLOW:")').each(), ...mdq(result).query('section2(~"ACTION:")').each()];
|
|
409
|
+
for (const section of sections) {
|
|
410
|
+
const raw = section.text();
|
|
411
|
+
if (!/\bI\.clickXY\s*\(/.test(raw))
|
|
412
|
+
continue;
|
|
413
|
+
result = result.replace(raw, '');
|
|
414
|
+
}
|
|
415
|
+
return result;
|
|
416
|
+
}
|
package/dist/src/ai/historian.js
CHANGED
|
@@ -4,8 +4,8 @@ import dedent from 'dedent';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { ActionResult } from "../action-result.js";
|
|
6
6
|
import { ConfigParser } from "../config.js";
|
|
7
|
-
import { KnowledgeTracker } from "../knowledge-tracker.js";
|
|
8
7
|
import { ExperienceTracker } from "../experience-tracker.js";
|
|
8
|
+
import { KnowledgeTracker } from "../knowledge-tracker.js";
|
|
9
9
|
import { Test } from "../test-plan.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
import { extractStatePath } from "../utils/url-matcher.js";
|
|
@@ -38,13 +38,11 @@ export class Historian {
|
|
|
38
38
|
const verifiedSteps = await this.verifySteps(steps, initialState);
|
|
39
39
|
if (verifiedSteps.length > 0) {
|
|
40
40
|
const relatedUrls = this.extractVisitedUrls(toolExecutions, initialState.url || '');
|
|
41
|
-
|
|
41
|
+
this.experienceTracker.writeFlow(initialState, {
|
|
42
42
|
scenario: task.description,
|
|
43
|
-
result,
|
|
44
43
|
steps: verifiedSteps,
|
|
45
44
|
relatedUrls,
|
|
46
|
-
};
|
|
47
|
-
this.experienceTracker.saveSessionExperience(initialState, entry);
|
|
45
|
+
});
|
|
48
46
|
}
|
|
49
47
|
if (task instanceof Test && result !== 'failed') {
|
|
50
48
|
await this.reportSession(task, steps);
|
|
@@ -71,6 +69,8 @@ export class Historian {
|
|
|
71
69
|
continue;
|
|
72
70
|
if (!exec.wasSuccessful)
|
|
73
71
|
continue;
|
|
72
|
+
if (isNonReusableCode(exec.output.code))
|
|
73
|
+
continue;
|
|
74
74
|
const message = this.getExecutionLabel(exec, `Executed ${exec.toolName}`);
|
|
75
75
|
const ariaDiff = exec.output?.pageDiff?.ariaChanges || null;
|
|
76
76
|
const urlChanged = exec.output?.pageDiff?.urlChanged || false;
|
|
@@ -203,7 +203,9 @@ export class Historian {
|
|
|
203
203
|
state = ActionResult.fromState(transition.toState);
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
-
|
|
206
|
+
if (isNonReusableCode(candidate.success.output.code))
|
|
207
|
+
continue;
|
|
208
|
+
this.experienceTracker.writeAction(state, { title: pattern.intent, code: candidate.success.output.code, explanation: pattern.explanation });
|
|
207
209
|
}
|
|
208
210
|
debugLog('Detected %d retry patterns', response?.object?.retryPatterns?.length || 0);
|
|
209
211
|
}
|
|
@@ -307,6 +309,8 @@ export class Historian {
|
|
|
307
309
|
const lines = [];
|
|
308
310
|
lines.push(`Scenario('${this.escapeString(scenario)}', ({ I }) => {`);
|
|
309
311
|
for (const exec of successfulSteps) {
|
|
312
|
+
if (isNonReusableCode(exec.output.code))
|
|
313
|
+
continue;
|
|
310
314
|
const explanation = this.getExecutionLabel(exec);
|
|
311
315
|
if (explanation) {
|
|
312
316
|
lines.push('');
|
|
@@ -418,3 +422,6 @@ export class Historian {
|
|
|
418
422
|
.join('\n');
|
|
419
423
|
}
|
|
420
424
|
}
|
|
425
|
+
export function isNonReusableCode(code) {
|
|
426
|
+
return /\bI\.clickXY\s*\(/.test(code);
|
|
427
|
+
}
|