explorbot 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/bin/explorbot-cli.ts +27 -18
- package/dist/bin/explorbot-cli.js +26 -18
- package/dist/package.json +2 -2
- package/dist/rules/navigator/output.md +9 -0
- package/dist/rules/navigator/verification-actions.md +2 -0
- package/dist/src/action-result.js +23 -1
- package/dist/src/action.js +46 -38
- package/dist/src/ai/bosun.js +11 -1
- package/dist/src/ai/conversation.js +39 -0
- package/dist/src/ai/historian/codeceptjs.js +109 -0
- package/dist/src/ai/historian/experience.js +320 -0
- package/dist/src/ai/historian/mixin.js +2 -0
- package/dist/src/ai/historian/playwright.js +145 -0
- package/dist/src/ai/historian/utils.js +18 -0
- package/dist/src/ai/historian.js +19 -405
- package/dist/src/ai/navigator.js +82 -29
- package/dist/src/ai/pilot.js +232 -13
- package/dist/src/ai/planner.js +29 -9
- package/dist/src/ai/provider.js +54 -17
- package/dist/src/ai/researcher.js +41 -32
- package/dist/src/ai/rules.js +26 -14
- package/dist/src/ai/tester.js +90 -26
- package/dist/src/ai/tools.js +13 -7
- package/dist/src/browser-server.js +16 -3
- package/dist/src/commands/add-rule-command.js +11 -8
- package/dist/src/commands/clean-command.js +2 -1
- package/dist/src/commands/explore-command.js +27 -15
- package/dist/src/commands/init-command.js +9 -8
- package/dist/src/commands/plan-command.js +32 -0
- package/dist/src/commands/plan-save-command.js +19 -7
- package/dist/src/commands/rerun-command.js +4 -0
- package/dist/src/components/App.js +15 -5
- package/dist/src/execution-controller.js +13 -2
- package/dist/src/experience-tracker.js +20 -64
- package/dist/src/explorbot.js +5 -8
- package/dist/src/explorer.js +9 -2
- package/dist/src/observability.js +50 -99
- package/dist/src/playwright-recorder.js +309 -0
- package/dist/src/test-plan.js +12 -0
- package/dist/src/utils/aria.js +37 -1
- package/dist/src/utils/error-page.js +20 -7
- package/dist/src/utils/next-steps.js +37 -0
- package/package.json +2 -2
- package/rules/navigator/output.md +9 -0
- package/rules/navigator/verification-actions.md +2 -0
- package/src/action-result.ts +26 -1
- package/src/action.ts +44 -37
- package/src/ai/bosun.ts +11 -1
- package/src/ai/conversation.ts +37 -0
- package/src/ai/historian/codeceptjs.ts +130 -0
- package/src/ai/historian/experience.ts +383 -0
- package/src/ai/historian/mixin.ts +4 -0
- package/src/ai/historian/playwright.ts +169 -0
- package/src/ai/historian/utils.ts +23 -0
- package/src/ai/historian.ts +35 -473
- package/src/ai/navigator.ts +82 -29
- package/src/ai/pilot.ts +237 -14
- package/src/ai/planner.ts +29 -9
- package/src/ai/provider.ts +51 -17
- package/src/ai/researcher.ts +45 -33
- package/src/ai/rules.ts +27 -14
- package/src/ai/tester.ts +94 -26
- package/src/ai/tools.ts +47 -25
- package/src/browser-server.ts +17 -3
- package/src/commands/add-rule-command.ts +11 -7
- package/src/commands/clean-command.ts +2 -1
- package/src/commands/explore-command.ts +29 -15
- package/src/commands/init-command.ts +9 -8
- package/src/commands/plan-command.ts +35 -0
- package/src/commands/plan-save-command.ts +18 -7
- package/src/commands/rerun-command.ts +5 -0
- package/src/components/App.tsx +16 -5
- package/src/config.ts +6 -1
- package/src/execution-controller.ts +14 -3
- package/src/experience-tracker.ts +21 -72
- package/src/explorbot.ts +5 -8
- package/src/explorer.ts +11 -2
- package/src/observability.ts +50 -109
- package/src/playwright-recorder.ts +305 -0
- package/src/test-plan.ts +12 -0
- package/src/utils/aria.ts +38 -1
- package/src/utils/error-page.ts +22 -7
- package/src/utils/next-steps.ts +51 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { getCliName } from "../utils/cli-name.js";
|
|
2
|
+
import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
|
|
3
3
|
import { BaseCommand } from './base-command.js';
|
|
4
4
|
export class PlanSaveCommand extends BaseCommand {
|
|
5
5
|
name = 'plan:save';
|
|
@@ -12,10 +12,22 @@ export class PlanSaveCommand extends BaseCommand {
|
|
|
12
12
|
}
|
|
13
13
|
const filename = args.trim() || undefined;
|
|
14
14
|
const savedPath = this.explorBot.savePlan(filename);
|
|
15
|
-
if (savedPath)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
if (!savedPath)
|
|
16
|
+
return;
|
|
17
|
+
const cli = getCliName();
|
|
18
|
+
const relPlan = relativeToCwd(savedPath);
|
|
19
|
+
const sections = [
|
|
20
|
+
{
|
|
21
|
+
label: 'Plan',
|
|
22
|
+
path: savedPath,
|
|
23
|
+
commands: [
|
|
24
|
+
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
25
|
+
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
26
|
+
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
27
|
+
{ label: 'Reload', command: `/plan:load ${relPlan}` },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
printNextSteps(sections);
|
|
20
32
|
}
|
|
21
33
|
}
|
|
@@ -19,6 +19,10 @@ export class RerunCommand extends BaseCommand {
|
|
|
19
19
|
if (!existsSync(filePath)) {
|
|
20
20
|
filePath = resolve(ConfigParser.getInstance().getTestsDir(), filename);
|
|
21
21
|
}
|
|
22
|
+
if (filePath.endsWith('.spec.ts') || filePath.endsWith('.spec.js')) {
|
|
23
|
+
tag('error').log(`Rerun does not support Playwright tests. Run them with: npx playwright test ${filePath}`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
|
|
23
27
|
await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
|
|
24
28
|
}
|
|
@@ -72,16 +72,27 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
72
72
|
setInterruptPrompt(prompt);
|
|
73
73
|
setShowInput(true);
|
|
74
74
|
return new Promise((resolve) => {
|
|
75
|
-
interruptResolveRef.current =
|
|
75
|
+
interruptResolveRef.current = (value) => {
|
|
76
|
+
interruptResolveRef.current = null;
|
|
77
|
+
setInterruptPrompt(null);
|
|
78
|
+
resolve(value);
|
|
79
|
+
};
|
|
76
80
|
});
|
|
77
81
|
});
|
|
78
82
|
const handleIdle = () => {
|
|
79
83
|
setShowInput(true);
|
|
80
84
|
};
|
|
85
|
+
const handleInterrupt = () => {
|
|
86
|
+
if (interruptResolveRef.current) {
|
|
87
|
+
interruptResolveRef.current(null);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
81
90
|
executionController.on('idle', handleIdle);
|
|
91
|
+
executionController.on('interrupt', handleInterrupt);
|
|
82
92
|
setInputCallbackReady(true);
|
|
83
93
|
return () => {
|
|
84
94
|
executionController.off('idle', handleIdle);
|
|
95
|
+
executionController.off('interrupt', handleInterrupt);
|
|
85
96
|
executionController.reset();
|
|
86
97
|
};
|
|
87
98
|
}, []);
|
|
@@ -244,9 +255,10 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
244
255
|
return;
|
|
245
256
|
}
|
|
246
257
|
if (isCommand) {
|
|
247
|
-
|
|
258
|
+
if (interruptResolveRef.current) {
|
|
259
|
+
interruptResolveRef.current(null);
|
|
260
|
+
}
|
|
248
261
|
setShowInput(false);
|
|
249
|
-
interruptResolveRef.current = null;
|
|
250
262
|
executionController.startExecution();
|
|
251
263
|
try {
|
|
252
264
|
await commandHandler.executeCommand(trimmed);
|
|
@@ -264,8 +276,6 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
264
276
|
}
|
|
265
277
|
if (interruptResolveRef.current) {
|
|
266
278
|
interruptResolveRef.current(input);
|
|
267
|
-
interruptResolveRef.current = null;
|
|
268
|
-
setInterruptPrompt(null);
|
|
269
279
|
setShowInput(false);
|
|
270
280
|
return;
|
|
271
281
|
}
|
|
@@ -7,6 +7,7 @@ export class ExecutionController extends EventEmitter {
|
|
|
7
7
|
inputCallback = null;
|
|
8
8
|
interruptResolvers = [];
|
|
9
9
|
abortController = null;
|
|
10
|
+
awaitingInput = false;
|
|
10
11
|
constructor() {
|
|
11
12
|
super();
|
|
12
13
|
}
|
|
@@ -39,6 +40,9 @@ export class ExecutionController extends EventEmitter {
|
|
|
39
40
|
this.interruptResolvers = [];
|
|
40
41
|
this.emit('idle');
|
|
41
42
|
}
|
|
43
|
+
isAwaitingInput() {
|
|
44
|
+
return this.awaitingInput;
|
|
45
|
+
}
|
|
42
46
|
isInterrupted() {
|
|
43
47
|
return this.interrupted;
|
|
44
48
|
}
|
|
@@ -64,10 +68,16 @@ export class ExecutionController extends EventEmitter {
|
|
|
64
68
|
return userInput;
|
|
65
69
|
}
|
|
66
70
|
async requestInput(prompt) {
|
|
67
|
-
if (this.inputCallback) {
|
|
71
|
+
if (!this.inputCallback) {
|
|
72
|
+
return await this.readlineInput(prompt);
|
|
73
|
+
}
|
|
74
|
+
this.awaitingInput = true;
|
|
75
|
+
try {
|
|
68
76
|
return await this.inputCallback(prompt);
|
|
69
77
|
}
|
|
70
|
-
|
|
78
|
+
finally {
|
|
79
|
+
this.awaitingInput = false;
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
async readlineInput(prompt) {
|
|
73
83
|
const rl = readline.createInterface({
|
|
@@ -86,6 +96,7 @@ export class ExecutionController extends EventEmitter {
|
|
|
86
96
|
this.interrupted = false;
|
|
87
97
|
this.interruptResolvers = [];
|
|
88
98
|
this.abortController = null;
|
|
99
|
+
this.awaitingInput = false;
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
102
|
export const executionController = ExecutionController.getInstance();
|
|
@@ -13,15 +13,17 @@ export const RECENT_WINDOW_DAYS = 30;
|
|
|
13
13
|
/**
|
|
14
14
|
* Stores and reads per-page experience files (`./experience/<stateHash>.md`).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* Two writers, two contracts:
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* writeFlow(state, body, relatedUrls?) — caller hands in a fully-formatted
|
|
19
|
+
* `## FLOW: <imperative title>` block (multi-step,
|
|
20
|
+
* `*` bullets + optional ```js``` + `>` discovery,
|
|
21
|
+
* ends with `---`). Tracker dedups + prepends.
|
|
22
|
+
* writeAction(state, ActionInput) — `## ACTION: <imperative title>`, single-step,
|
|
23
|
+
* optional `Solution:` line + one ```js``` code block.
|
|
24
|
+
* Title normalized via normalizeTitle().
|
|
20
25
|
*
|
|
21
26
|
* - Always h2. Never h3 for FLOW/ACTION.
|
|
22
|
-
* - Title is an imperative verb phrase, lowercase-first, no trailing punctuation.
|
|
23
|
-
* Writers normalize automatically (strip own `FLOW:` / `ACTION:` prefix if the caller included it,
|
|
24
|
-
* lowercase first char, trim trailing `.!?,;:`).
|
|
25
27
|
* - On read (getSuccessfulExperience), headings are rendered as
|
|
26
28
|
* `## HOW to <title> (multi-step|single-step)` so prompts get natural phrasing.
|
|
27
29
|
*/
|
|
@@ -159,27 +161,25 @@ export class ExperienceTracker {
|
|
|
159
161
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
160
162
|
tag('substep').log(` Added ACTION to: ${stateHash}.md`);
|
|
161
163
|
}
|
|
162
|
-
writeFlow(state,
|
|
164
|
+
writeFlow(state, body, relatedUrls) {
|
|
163
165
|
if (this.disabled || this.isWritingDisabled(state))
|
|
164
166
|
return;
|
|
165
|
-
if (!
|
|
167
|
+
if (!body?.trim())
|
|
166
168
|
return;
|
|
167
169
|
this.ensureExperienceFile(state);
|
|
168
170
|
const stateHash = state.getStateHash();
|
|
169
171
|
const { content, data } = this.readExperienceFile(stateHash);
|
|
170
|
-
if (
|
|
172
|
+
if (content.includes(body)) {
|
|
173
|
+
debugLog('Skipping duplicate flow body');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (relatedUrls?.length) {
|
|
171
177
|
const currentPath = extractStatePath(state.url || '');
|
|
172
178
|
const existingRelated = Array.isArray(data.related) ? data.related : [];
|
|
173
|
-
const allRelated = [...new Set([...existingRelated, ...
|
|
179
|
+
const allRelated = [...new Set([...existingRelated, ...relatedUrls])];
|
|
174
180
|
data.related = allRelated.filter((url) => url !== currentPath);
|
|
175
181
|
}
|
|
176
|
-
const
|
|
177
|
-
if (!title)
|
|
178
|
-
return;
|
|
179
|
-
const sessionContent = this.trimSessionContent(generateFlowContent(title, flow.steps));
|
|
180
|
-
if (!sessionContent)
|
|
181
|
-
return;
|
|
182
|
-
const updatedContent = `${sessionContent}\n${content}`;
|
|
182
|
+
const updatedContent = `${body}\n${content}`;
|
|
183
183
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
184
184
|
tag('substep').log(`Added FLOW to: ${stateHash}.md`);
|
|
185
185
|
}
|
|
@@ -245,33 +245,6 @@ export class ExperienceTracker {
|
|
|
245
245
|
// Clear any in-memory state if needed
|
|
246
246
|
// The actual files will be cleaned up by test cleanup
|
|
247
247
|
}
|
|
248
|
-
trimSessionContent(content) {
|
|
249
|
-
const q = mdq(content);
|
|
250
|
-
if (q.query('heading').count() === 0)
|
|
251
|
-
return null;
|
|
252
|
-
if (q.query('code').count() === 0)
|
|
253
|
-
return null;
|
|
254
|
-
let result = content;
|
|
255
|
-
const codeBlocks = q.query('code').each();
|
|
256
|
-
if (codeBlocks.length > 2) {
|
|
257
|
-
for (const block of codeBlocks.slice(2)) {
|
|
258
|
-
result = result.replace(block.text(), '');
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const blockquotes = mdq(result).query('blockquote').each();
|
|
262
|
-
if (blockquotes.length > 5) {
|
|
263
|
-
for (const bq of blockquotes.slice(5)) {
|
|
264
|
-
result = result.replace(bq.text(), '');
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const lines = result.split('\n');
|
|
268
|
-
if (lines.length > 40) {
|
|
269
|
-
result = lines.slice(0, 40).join('\n');
|
|
270
|
-
}
|
|
271
|
-
if (!result.trim())
|
|
272
|
-
return null;
|
|
273
|
-
return result;
|
|
274
|
-
}
|
|
275
248
|
getSuccessfulExperience(state, options) {
|
|
276
249
|
const records = this.getRelevantExperience(state, {
|
|
277
250
|
includeDescendantExperience: options?.includeDescendants,
|
|
@@ -469,7 +442,9 @@ export function renderExperienceToc(toc) {
|
|
|
469
442
|
return '';
|
|
470
443
|
const lines = [];
|
|
471
444
|
lines.push('<experience>');
|
|
472
|
-
lines.push('Past experience for this page —
|
|
445
|
+
lines.push('Past experience for this page — recipes recorded from prior successful runs.');
|
|
446
|
+
lines.push('Locators and step ordering worked then; the page may have changed since.');
|
|
447
|
+
lines.push('Treat as a starting hypothesis, not ground truth. If a step fails, fall back to ARIA/UI-map.');
|
|
473
448
|
lines.push('FLOW: = multi-step recipe (bullets + code + discovery). ACTION: = single-step snippet (one code block).');
|
|
474
449
|
lines.push('Call learn_experience({ fileTag, sectionIndex }) to read a section when it looks relevant to the current step.');
|
|
475
450
|
lines.push('');
|
|
@@ -513,25 +488,6 @@ function generateActionContent(title, code, explanation) {
|
|
|
513
488
|
lines.push('');
|
|
514
489
|
return lines.join('\n');
|
|
515
490
|
}
|
|
516
|
-
function generateFlowContent(title, steps) {
|
|
517
|
-
let content = `## FLOW: ${title}\n\n`;
|
|
518
|
-
for (const step of steps) {
|
|
519
|
-
content += `* ${step.message}\n\n`;
|
|
520
|
-
if (step.code) {
|
|
521
|
-
content += '```js\n';
|
|
522
|
-
content += `${step.code}\n`;
|
|
523
|
-
content += '```\n\n';
|
|
524
|
-
}
|
|
525
|
-
if (step.discovery) {
|
|
526
|
-
const discoveries = step.discovery.split('\n').filter((d) => d.trim());
|
|
527
|
-
for (const discovery of discoveries) {
|
|
528
|
-
content += `> ${discovery.trim()}\n\n`;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
content += '---\n';
|
|
533
|
-
return content;
|
|
534
|
-
}
|
|
535
491
|
function renderAsHowTo(content) {
|
|
536
492
|
const tokens = marked.lexer(content);
|
|
537
493
|
let result = '';
|
package/dist/src/explorbot.js
CHANGED
|
@@ -36,6 +36,7 @@ export class ExplorBot {
|
|
|
36
36
|
currentPlan;
|
|
37
37
|
planFeature;
|
|
38
38
|
lastPlanError = null;
|
|
39
|
+
lastSavedPlanPath = null;
|
|
39
40
|
agents = {};
|
|
40
41
|
constructor(options = {}) {
|
|
41
42
|
this.options = options;
|
|
@@ -214,10 +215,10 @@ export class ExplorBot {
|
|
|
214
215
|
return this.agents.quartermaster;
|
|
215
216
|
}
|
|
216
217
|
agentHistorian() {
|
|
217
|
-
return (this.agents.historian ||= this.createAgent(({ ai, explorer }) => {
|
|
218
|
+
return (this.agents.historian ||= this.createAgent(({ ai, explorer, config }) => {
|
|
218
219
|
const experienceTracker = explorer.getStateManager().getExperienceTracker();
|
|
219
220
|
const reporter = explorer.getReporter();
|
|
220
|
-
return new Historian(ai, experienceTracker, reporter, explorer.getStateManager());
|
|
221
|
+
return new Historian(ai, experienceTracker, reporter, explorer.getStateManager(), config, explorer.getPlaywrightRecorder());
|
|
221
222
|
}));
|
|
222
223
|
}
|
|
223
224
|
agentRerunner() {
|
|
@@ -314,12 +315,7 @@ export class ExplorBot {
|
|
|
314
315
|
return undefined;
|
|
315
316
|
return this.currentPlan;
|
|
316
317
|
}
|
|
317
|
-
|
|
318
|
-
if (savedPath) {
|
|
319
|
-
const relativePath = path.relative(process.cwd(), savedPath);
|
|
320
|
-
tag('info').log(`Plan saved to: ${relativePath}`);
|
|
321
|
-
tag('info').log(`Edit the plan file and run /plan:load ${relativePath} to reload it`);
|
|
322
|
-
}
|
|
318
|
+
this.savePlan();
|
|
323
319
|
return this.currentPlan;
|
|
324
320
|
}
|
|
325
321
|
getPlansDir() {
|
|
@@ -341,6 +337,7 @@ export class ExplorBot {
|
|
|
341
337
|
const planFilename = filename || this.generatePlanFilename();
|
|
342
338
|
const planPath = path.join(plansDir, planFilename);
|
|
343
339
|
Plan.saveMultipleToMarkdown(plans, planPath);
|
|
340
|
+
this.lastSavedPlanPath = planPath;
|
|
344
341
|
return planPath;
|
|
345
342
|
}
|
|
346
343
|
generatePlanFilename() {
|
package/dist/src/explorer.js
CHANGED
|
@@ -12,6 +12,7 @@ import { RequestStore } from "./api/request-store.js";
|
|
|
12
12
|
import { XhrCapture } from "./api/xhr-capture.js";
|
|
13
13
|
import { ConfigParser, outputPath } from './config.js';
|
|
14
14
|
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
15
|
+
import { PlaywrightRecorder } from "./playwright-recorder.js";
|
|
15
16
|
import { Reporter } from "./reporter.js";
|
|
16
17
|
import { StateManager } from './state-manager.js';
|
|
17
18
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
@@ -35,6 +36,7 @@ class Explorer {
|
|
|
35
36
|
_activeTest = null;
|
|
36
37
|
xhrCapture = null;
|
|
37
38
|
requestStore = null;
|
|
39
|
+
playwrightRecorder = new PlaywrightRecorder();
|
|
38
40
|
constructor(config, aiProvider, options) {
|
|
39
41
|
this.config = config;
|
|
40
42
|
this.aiProvider = aiProvider;
|
|
@@ -89,7 +91,7 @@ class Explorer {
|
|
|
89
91
|
tag('substep').log(debugInfo);
|
|
90
92
|
}
|
|
91
93
|
const PlaywrightConfig = {
|
|
92
|
-
timeout:
|
|
94
|
+
timeout: 3000,
|
|
93
95
|
highlightElement: true,
|
|
94
96
|
waitForAction: 500,
|
|
95
97
|
...playwrightConfig,
|
|
@@ -188,6 +190,7 @@ class Explorer {
|
|
|
188
190
|
const hasSession = this.options?.session && existsSync(this.options.session);
|
|
189
191
|
const contextOptions = hasSession ? { storageState: this.options.session } : undefined;
|
|
190
192
|
await this.playwrightHelper._createContextPage(contextOptions);
|
|
193
|
+
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
191
194
|
this.setupXhrCapture();
|
|
192
195
|
if (hasSession) {
|
|
193
196
|
tag('info').log(`Session restored from ${path.relative(process.cwd(), this.options.session)}`);
|
|
@@ -216,7 +219,10 @@ class Explorer {
|
|
|
216
219
|
await this.playwrightHelper._startBrowser();
|
|
217
220
|
}
|
|
218
221
|
createAction() {
|
|
219
|
-
return new Action(this.actor, this.stateManager);
|
|
222
|
+
return new Action(this.actor, this.stateManager, this.playwrightRecorder);
|
|
223
|
+
}
|
|
224
|
+
getPlaywrightRecorder() {
|
|
225
|
+
return this.playwrightRecorder;
|
|
220
226
|
}
|
|
221
227
|
async visit(url) {
|
|
222
228
|
await this.closeOtherTabs();
|
|
@@ -411,6 +417,7 @@ class Explorer {
|
|
|
411
417
|
if (this.xhrCapture && this.playwrightHelper?.page) {
|
|
412
418
|
this.xhrCapture.detach(this.playwrightHelper.page);
|
|
413
419
|
}
|
|
420
|
+
await this.playwrightRecorder.stop();
|
|
414
421
|
if (this.options?.session && this.playwrightHelper?.browserContext) {
|
|
415
422
|
const dir = path.dirname(this.options.session);
|
|
416
423
|
if (!existsSync(dir))
|
|
@@ -1,125 +1,76 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { context, trace } from '@opentelemetry/api';
|
|
1
|
+
import { trace } from '@opentelemetry/api';
|
|
3
2
|
let current = null;
|
|
4
|
-
let depth = 0;
|
|
5
3
|
export const Observability = {
|
|
6
4
|
async run(name, metadata, fn) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const savedName = current.name;
|
|
17
|
-
current.span = childSpan;
|
|
18
|
-
current.name = name;
|
|
19
|
-
return await context.with(trace.setSpan(context.active(), childSpan), async () => {
|
|
20
|
-
try {
|
|
21
|
-
return await fn();
|
|
22
|
-
}
|
|
23
|
-
finally {
|
|
24
|
-
childSpan.end();
|
|
25
|
-
current.span = savedSpan;
|
|
26
|
-
current.name = savedName;
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
const tracer = trace.getTracer('ai');
|
|
31
|
-
const spanContext = {
|
|
32
|
-
traceId: current?.traceId || randomBytes(16).toString('hex'),
|
|
33
|
-
spanId: randomBytes(8).toString('hex'),
|
|
34
|
-
traceFlags: 1,
|
|
35
|
-
};
|
|
36
|
-
const rootContext = trace.setSpanContext(context.active(), spanContext);
|
|
37
|
-
const initSpan = tracer.startSpan(name, undefined, rootContext);
|
|
38
|
-
initSpan.setAttribute('langfuse.trace.name', name);
|
|
39
|
-
initSpan.setAttribute('langfuse.trace.id', current?.traceId || '');
|
|
40
|
-
if (current?.metadata?.sessionId) {
|
|
41
|
-
initSpan.setAttribute('langfuse.trace.session_id', String(current.metadata.sessionId));
|
|
42
|
-
}
|
|
43
|
-
if (current?.metadata?.userId) {
|
|
44
|
-
initSpan.setAttribute('langfuse.trace.user_id', String(current.metadata.userId));
|
|
45
|
-
}
|
|
46
|
-
if (current?.metadata?.tags && Array.isArray(current.metadata.tags)) {
|
|
47
|
-
initSpan.setAttribute('langfuse.trace.tags', current.metadata.tags);
|
|
48
|
-
}
|
|
49
|
-
if (current?.metadata?.input) {
|
|
50
|
-
initSpan.setAttribute('langfuse.trace.input', JSON.stringify(current.metadata.input));
|
|
51
|
-
}
|
|
52
|
-
initSpan.end();
|
|
53
|
-
const span = tracer.startSpan(name, undefined, rootContext);
|
|
54
|
-
current.span = span;
|
|
55
|
-
return await context.with(trace.setSpan(rootContext, span), async () => {
|
|
5
|
+
const tracer = trace.getTracer('ai');
|
|
6
|
+
if (current) {
|
|
7
|
+
return await tracer.startActiveSpan(name, {}, async (span) => {
|
|
8
|
+
const saved = current;
|
|
9
|
+
current = {
|
|
10
|
+
metadata: { ...saved.metadata, ...metadata },
|
|
11
|
+
name,
|
|
12
|
+
span,
|
|
13
|
+
};
|
|
56
14
|
try {
|
|
57
15
|
return await fn();
|
|
58
16
|
}
|
|
59
17
|
finally {
|
|
60
18
|
span.end();
|
|
61
|
-
current
|
|
19
|
+
current = saved;
|
|
62
20
|
}
|
|
63
21
|
});
|
|
64
22
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
metadata: {
|
|
77
|
-
...metadata,
|
|
78
|
-
langfuseTraceId,
|
|
79
|
-
},
|
|
80
|
-
traceId: langfuseTraceId,
|
|
81
|
-
updateParent: true,
|
|
82
|
-
name,
|
|
83
|
-
};
|
|
84
|
-
depth = 1;
|
|
85
|
-
return true;
|
|
86
|
-
},
|
|
87
|
-
endTrace(started) {
|
|
88
|
-
if (!current) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (!started) {
|
|
92
|
-
depth -= 1;
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
depth -= 1;
|
|
96
|
-
if (depth <= 0) {
|
|
97
|
-
current = null;
|
|
98
|
-
depth = 0;
|
|
99
|
-
}
|
|
23
|
+
const attributes = buildRootSpanAttributes(name, metadata);
|
|
24
|
+
return await tracer.startActiveSpan(name, { attributes }, async (span) => {
|
|
25
|
+
current = { metadata, name, span };
|
|
26
|
+
try {
|
|
27
|
+
return await fn();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
span.end();
|
|
31
|
+
current = null;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
100
34
|
},
|
|
101
35
|
getTelemetry() {
|
|
102
36
|
if (!current) {
|
|
103
37
|
return undefined;
|
|
104
38
|
}
|
|
105
|
-
const
|
|
39
|
+
const metadata = {};
|
|
40
|
+
if (current.metadata.sessionId)
|
|
41
|
+
metadata.sessionId = current.metadata.sessionId;
|
|
42
|
+
if (current.metadata.userId)
|
|
43
|
+
metadata.userId = current.metadata.userId;
|
|
44
|
+
if (Array.isArray(current.metadata.tags))
|
|
45
|
+
metadata.tags = current.metadata.tags;
|
|
46
|
+
return {
|
|
106
47
|
isEnabled: true,
|
|
107
48
|
functionId: current.name,
|
|
108
|
-
metadata
|
|
109
|
-
...current.metadata,
|
|
110
|
-
langfuseTraceId: current.traceId,
|
|
111
|
-
langfuseUpdateParent: current.updateParent,
|
|
112
|
-
},
|
|
49
|
+
metadata,
|
|
113
50
|
};
|
|
114
|
-
if (current.updateParent) {
|
|
115
|
-
current.updateParent = false;
|
|
116
|
-
}
|
|
117
|
-
return telemetry;
|
|
118
51
|
},
|
|
119
52
|
isTracing() {
|
|
120
53
|
return Boolean(current);
|
|
121
54
|
},
|
|
122
55
|
getSpan() {
|
|
123
|
-
return current?.span;
|
|
56
|
+
return current?.span ?? trace.getActiveSpan();
|
|
124
57
|
},
|
|
125
58
|
};
|
|
59
|
+
function buildRootSpanAttributes(name, metadata) {
|
|
60
|
+
const attributes = {
|
|
61
|
+
'langfuse.trace.name': name,
|
|
62
|
+
};
|
|
63
|
+
if (metadata.sessionId) {
|
|
64
|
+
attributes['session.id'] = String(metadata.sessionId);
|
|
65
|
+
}
|
|
66
|
+
if (metadata.userId) {
|
|
67
|
+
attributes['user.id'] = String(metadata.userId);
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(metadata.tags)) {
|
|
70
|
+
attributes['langfuse.trace.tags'] = metadata.tags;
|
|
71
|
+
}
|
|
72
|
+
if (metadata.input !== undefined) {
|
|
73
|
+
attributes['langfuse.trace.input'] = JSON.stringify(metadata.input);
|
|
74
|
+
}
|
|
75
|
+
return attributes;
|
|
76
|
+
}
|