explorbot 0.1.27 → 0.1.29
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 +83 -245
- package/bin/explorbot-cli.ts +1 -0
- package/dist/bin/explorbot-cli.js +1 -0
- package/dist/package.json +8 -6
- package/dist/rules/navigator/verification-actions.md +2 -0
- package/dist/src/action-result.js +2 -0
- package/dist/src/action.js +88 -7
- package/dist/src/ai/captain/file-tools.js +100 -0
- package/dist/src/ai/captain/idle-mode.js +70 -6
- package/dist/src/ai/captain/web-mode.js +36 -6
- package/dist/src/ai/captain.js +87 -19
- package/dist/src/ai/fisherman.js +14 -3
- package/dist/src/ai/historian/screencast.js +11 -2
- package/dist/src/ai/navigator.js +5 -2
- package/dist/src/ai/pilot.js +52 -9
- package/dist/src/ai/planner.js +16 -5
- package/dist/src/ai/provider.js +53 -18
- package/dist/src/ai/researcher/coordinates.js +2 -3
- package/dist/src/ai/researcher/deep-analysis.js +3 -4
- package/dist/src/ai/researcher/locators.js +1 -2
- package/dist/src/ai/researcher.js +24 -19
- package/dist/src/ai/rules.js +44 -0
- package/dist/src/ai/task-agent.js +1 -0
- package/dist/src/ai/tester.js +161 -46
- package/dist/src/ai/tools.js +84 -8
- package/dist/src/commands/explore-command.js +6 -1
- package/dist/src/components/LogPane.js +4 -3
- package/dist/src/explorbot.js +7 -2
- package/dist/src/explorer.js +270 -35
- package/dist/src/stats.js +16 -0
- package/dist/src/utils/aria.js +66 -6
- package/dist/src/utils/browser-errors.js +23 -0
- package/dist/src/utils/error-page.js +17 -2
- package/dist/src/utils/logger.js +2 -2
- package/package.json +8 -6
- package/rules/navigator/verification-actions.md +2 -0
- package/src/action-result.ts +2 -0
- package/src/action.ts +83 -7
- package/src/ai/captain/file-tools.ts +126 -0
- package/src/ai/captain/idle-mode.ts +72 -6
- package/src/ai/captain/mixin.ts +1 -1
- package/src/ai/captain/web-mode.ts +40 -5
- package/src/ai/captain.ts +94 -20
- package/src/ai/fisherman.ts +14 -3
- package/src/ai/historian/screencast.ts +11 -2
- package/src/ai/navigator.ts +6 -2
- package/src/ai/pilot.ts +53 -9
- package/src/ai/planner.ts +16 -5
- package/src/ai/provider.ts +51 -19
- package/src/ai/researcher/coordinates.ts +2 -3
- package/src/ai/researcher/deep-analysis.ts +3 -4
- package/src/ai/researcher/locators.ts +1 -2
- package/src/ai/researcher.ts +25 -19
- package/src/ai/rules.ts +46 -0
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tester.ts +175 -48
- package/src/ai/tools.ts +97 -8
- package/src/commands/explore-command.ts +6 -1
- package/src/components/LogPane.tsx +4 -3
- package/src/config.ts +1 -0
- package/src/explorbot.ts +6 -2
- package/src/explorer.ts +295 -38
- package/src/state-manager.ts +2 -0
- package/src/stats.ts +18 -0
- package/src/utils/aria.ts +63 -6
- package/src/utils/browser-errors.ts +25 -0
- package/src/utils/error-page.ts +16 -3
- package/src/utils/logger.ts +3 -3
package/src/ai/provider.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { OpenTelemetry } from '@ai-sdk/otel';
|
|
1
2
|
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
2
3
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
3
|
-
import { generateObject, generateText,
|
|
4
|
+
import { generateObject, generateText, isStepCount, registerTelemetry } from 'ai';
|
|
4
5
|
import type { ModelMessage } from 'ai';
|
|
5
6
|
import { clearActivity, setActivity } from '../activity.ts';
|
|
6
7
|
import type { AIConfig } from '../config.js';
|
|
@@ -19,9 +20,11 @@ const responseLog = createDebug('explorbot:provider:in');
|
|
|
19
20
|
class AiError extends Error {}
|
|
20
21
|
export class ContextLengthError extends Error {}
|
|
21
22
|
|
|
23
|
+
let telemetryRegistered = false;
|
|
24
|
+
|
|
22
25
|
function extractCachedTokens(usage: any): number {
|
|
23
26
|
if (!usage) return 0;
|
|
24
|
-
const direct = usage.
|
|
27
|
+
const direct = usage.inputTokenDetails?.cacheReadTokens ?? usage.cachedInputTokens;
|
|
25
28
|
if (typeof direct === 'number') return direct;
|
|
26
29
|
const raw = usage.raw;
|
|
27
30
|
const fromRaw = raw?.prompt_tokens_details?.cached_tokens ?? raw?.promptTokensDetails?.cachedTokens;
|
|
@@ -86,7 +89,7 @@ export class Provider {
|
|
|
86
89
|
await generateText({
|
|
87
90
|
model: this.config.model,
|
|
88
91
|
prompt: 'hi',
|
|
89
|
-
|
|
92
|
+
maxOutputTokens: 1,
|
|
90
93
|
});
|
|
91
94
|
} catch (error: any) {
|
|
92
95
|
throw new AiError(`AI connection failed: ${error.message}`);
|
|
@@ -110,6 +113,16 @@ export class Provider {
|
|
|
110
113
|
return this.config.agenticModel || this.config.model;
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
getConfiguredModels(): Record<string, string> {
|
|
117
|
+
const models: Record<string, string> = { model: this.getModelName(this.config.model) };
|
|
118
|
+
if (this.config.agenticModel) models.agenticModel = this.getModelName(this.config.agenticModel);
|
|
119
|
+
if (this.config.visionModel) models.visionModel = this.getModelName(this.config.visionModel);
|
|
120
|
+
for (const [agent, agentConfig] of Object.entries(this.config.agents || {})) {
|
|
121
|
+
if (agentConfig?.model) models[agent] = this.getModelName(agentConfig.model);
|
|
122
|
+
}
|
|
123
|
+
return models;
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
getSystemPromptForAgent(agentName: string, currentUrl?: string): string | undefined {
|
|
114
127
|
const agentConfig = this.config.agents?.[agentName as keyof typeof this.config.agents];
|
|
115
128
|
const parts: string[] = [];
|
|
@@ -129,6 +142,12 @@ export class Provider {
|
|
|
129
142
|
return agentConfig?.providerOptions;
|
|
130
143
|
}
|
|
131
144
|
|
|
145
|
+
getReasoningForAgent(agentName?: string): string | undefined {
|
|
146
|
+
if (!agentName) return undefined;
|
|
147
|
+
const agentConfig = this.config.agents?.[agentName as keyof typeof this.config.agents];
|
|
148
|
+
return agentConfig?.reasoning;
|
|
149
|
+
}
|
|
150
|
+
|
|
132
151
|
private getRetryOptions(options: any = {}): RetryOptions {
|
|
133
152
|
return {
|
|
134
153
|
...this.defaultRetryOptions,
|
|
@@ -146,6 +165,12 @@ export class Provider {
|
|
|
146
165
|
};
|
|
147
166
|
}
|
|
148
167
|
|
|
168
|
+
private finalizeConfig(config: Record<string, any>, options: any, telemetry: any): void {
|
|
169
|
+
if (telemetry) config.telemetry = telemetry;
|
|
170
|
+
const reasoning = this.getReasoningForAgent(options.agentName);
|
|
171
|
+
if (reasoning) config.reasoning ??= reasoning;
|
|
172
|
+
}
|
|
173
|
+
|
|
149
174
|
private initLangfuse() {
|
|
150
175
|
const langfuseConfig = this.config.langfuse;
|
|
151
176
|
const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
@@ -167,6 +192,10 @@ export class Provider {
|
|
|
167
192
|
instrumentations: [],
|
|
168
193
|
});
|
|
169
194
|
void this.otelSdk.start();
|
|
195
|
+
if (!telemetryRegistered) {
|
|
196
|
+
registerTelemetry(new OpenTelemetry());
|
|
197
|
+
telemetryRegistered = true;
|
|
198
|
+
}
|
|
170
199
|
this.telemetryEnabled = true;
|
|
171
200
|
}
|
|
172
201
|
|
|
@@ -177,20 +206,20 @@ export class Provider {
|
|
|
177
206
|
|
|
178
207
|
const runTelemetry = Observability.getTelemetry();
|
|
179
208
|
|
|
180
|
-
if (!options.
|
|
181
|
-
return runTelemetry
|
|
209
|
+
if (!options.telemetry) {
|
|
210
|
+
return runTelemetry;
|
|
182
211
|
}
|
|
183
212
|
|
|
184
213
|
if (!runTelemetry) {
|
|
185
|
-
return options.
|
|
214
|
+
return options.telemetry;
|
|
186
215
|
}
|
|
187
216
|
|
|
188
217
|
return {
|
|
189
218
|
...runTelemetry,
|
|
190
|
-
...options.
|
|
219
|
+
...options.telemetry,
|
|
191
220
|
metadata: {
|
|
192
221
|
...runTelemetry.metadata,
|
|
193
|
-
...options.
|
|
222
|
+
...options.telemetry.metadata,
|
|
194
223
|
},
|
|
195
224
|
};
|
|
196
225
|
}
|
|
@@ -242,15 +271,16 @@ export class Provider {
|
|
|
242
271
|
const telemetry = this.getTelemetry(options);
|
|
243
272
|
const config = this.mergeProviderOptions(
|
|
244
273
|
{
|
|
245
|
-
|
|
274
|
+
maxOutputTokens: 16384,
|
|
275
|
+
allowSystemInMessages: true,
|
|
246
276
|
...(this.config.config || {}),
|
|
247
277
|
...options,
|
|
248
|
-
...(telemetry ? { experimental_telemetry: telemetry } : {}),
|
|
249
278
|
model,
|
|
250
279
|
abortSignal: executionController.getAbortSignal(),
|
|
251
280
|
},
|
|
252
281
|
options.agentName
|
|
253
282
|
);
|
|
283
|
+
this.finalizeConfig(config, options, telemetry);
|
|
254
284
|
|
|
255
285
|
promptLog(messages[messages.length - 1].content);
|
|
256
286
|
try {
|
|
@@ -259,7 +289,7 @@ export class Provider {
|
|
|
259
289
|
if (!result.text) {
|
|
260
290
|
debugLog(result);
|
|
261
291
|
if (result.finishReason === 'length') {
|
|
262
|
-
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase
|
|
292
|
+
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
263
293
|
}
|
|
264
294
|
throw new Error('No response text from AI');
|
|
265
295
|
}
|
|
@@ -315,23 +345,24 @@ export class Provider {
|
|
|
315
345
|
const telemetry = this.getTelemetry(options);
|
|
316
346
|
const maxRoundtrips = options.maxToolRoundtrips ?? 5;
|
|
317
347
|
const extraStop = options.stopWhen;
|
|
318
|
-
const stopConditions: any[] = [
|
|
348
|
+
const stopConditions: any[] = [isStepCount(maxRoundtrips)];
|
|
319
349
|
if (extraStop) stopConditions.push(extraStop);
|
|
320
350
|
const { stopWhen: _ignoredStopWhen, ...optionsWithoutStop } = options;
|
|
321
351
|
const config = this.mergeProviderOptions(
|
|
322
352
|
{
|
|
323
353
|
tools,
|
|
324
|
-
|
|
354
|
+
maxOutputTokens: 16384,
|
|
325
355
|
toolChoice: 'auto',
|
|
356
|
+
allowSystemInMessages: true,
|
|
326
357
|
...(this.config.config || {}),
|
|
327
358
|
...optionsWithoutStop,
|
|
328
359
|
stopWhen: stopConditions,
|
|
329
|
-
...(telemetry ? { experimental_telemetry: telemetry } : {}),
|
|
330
360
|
model,
|
|
331
361
|
abortSignal: executionController.getAbortSignal(),
|
|
332
362
|
},
|
|
333
363
|
options.agentName
|
|
334
364
|
);
|
|
365
|
+
this.finalizeConfig(config, options, telemetry);
|
|
335
366
|
try {
|
|
336
367
|
const response = await withRetry(async () => {
|
|
337
368
|
const timeout = config.timeout || 30000;
|
|
@@ -346,7 +377,7 @@ export class Provider {
|
|
|
346
377
|
])) as any;
|
|
347
378
|
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
348
379
|
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
349
|
-
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase
|
|
380
|
+
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
350
381
|
}
|
|
351
382
|
return result;
|
|
352
383
|
} finally {
|
|
@@ -409,14 +440,15 @@ export class Provider {
|
|
|
409
440
|
const config = this.mergeProviderOptions(
|
|
410
441
|
{
|
|
411
442
|
schema,
|
|
443
|
+
allowSystemInMessages: true,
|
|
412
444
|
...(this.config.config || {}),
|
|
413
445
|
...options,
|
|
414
|
-
...(telemetry ? { experimental_telemetry: telemetry } : {}),
|
|
415
446
|
model: modelToUse,
|
|
416
447
|
abortSignal: executionController.getAbortSignal(),
|
|
417
448
|
},
|
|
418
449
|
options.agentName
|
|
419
450
|
);
|
|
451
|
+
this.finalizeConfig(config, options, telemetry);
|
|
420
452
|
|
|
421
453
|
try {
|
|
422
454
|
promptLog(messages[messages.length - 1].content);
|
|
@@ -617,13 +649,13 @@ export class Provider {
|
|
|
617
649
|
];
|
|
618
650
|
|
|
619
651
|
const telemetry = this.getTelemetry({});
|
|
620
|
-
const config = {
|
|
621
|
-
|
|
652
|
+
const config: Record<string, any> = {
|
|
653
|
+
maxOutputTokens: 16384,
|
|
622
654
|
...(this.config.config || {}),
|
|
623
|
-
...(telemetry ? { experimental_telemetry: telemetry } : {}),
|
|
624
655
|
model: this.config.visionModel,
|
|
625
656
|
abortSignal: executionController.getAbortSignal(),
|
|
626
657
|
};
|
|
658
|
+
if (telemetry) config.telemetry = telemetry;
|
|
627
659
|
|
|
628
660
|
try {
|
|
629
661
|
promptLog(`Processing image with prompt: ${prompt}`);
|
|
@@ -81,7 +81,7 @@ export function WithCoordinates<T extends Constructor>(Base: T) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async visuallyAnnotateElements(opts?: { containers?: Array<{ css: string; label: string }> }): Promise<number> {
|
|
84
|
-
return
|
|
84
|
+
return this.explorer.visuallyAnnotateElements(opts);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
private async _analyzeScreenshotForVisualProps(): Promise<VisualAnalysisResult> {
|
|
@@ -193,7 +193,6 @@ export function WithCoordinates<T extends Constructor>(Base: T) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
async backfillCoordinates(result: ResearchResult): Promise<void> {
|
|
196
|
-
const page = this.explorer.playwrightHelper.page;
|
|
197
196
|
const sections = parseResearchSections(result.text);
|
|
198
197
|
const eidxWithoutCoords: string[] = [];
|
|
199
198
|
for (const section of sections) {
|
|
@@ -203,7 +202,7 @@ export function WithCoordinates<T extends Constructor>(Base: T) {
|
|
|
203
202
|
}
|
|
204
203
|
if (eidxWithoutCoords.length === 0) return;
|
|
205
204
|
|
|
206
|
-
const webElements = await WebElement.fromEidxList(page, eidxWithoutCoords);
|
|
205
|
+
const webElements = await this.explorer.runWithBrowserRecovery('backfillCoordinates', () => WebElement.fromEidxList(this.explorer.playwrightHelper.page, eidxWithoutCoords));
|
|
207
206
|
if (webElements.length === 0) return;
|
|
208
207
|
|
|
209
208
|
const rectMap = new Map(webElements.map((w) => [w.eidx!, w]));
|
|
@@ -359,11 +359,10 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
359
359
|
const isCoordinateClick = el.commands[0].startsWith('I.clickXY(');
|
|
360
360
|
if (!isCoordinateClick) {
|
|
361
361
|
const hoverCmd = el.commands[0].replace('I.click(', 'I.moveCursorTo(');
|
|
362
|
-
|
|
363
|
-
await hoverAction.attempt(hoverCmd, undefined, false);
|
|
362
|
+
await this.explorer.attemptAction(hoverCmd, undefined, false);
|
|
364
363
|
await new Promise((r) => setTimeout(r, 500));
|
|
365
364
|
|
|
366
|
-
await this.explorer.
|
|
365
|
+
await this.explorer.capturePageState();
|
|
367
366
|
const hoverAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
368
367
|
const hoverDiff = await hoverAR.diff(previousState);
|
|
369
368
|
await hoverDiff.calculate();
|
|
@@ -452,7 +451,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
452
451
|
private async _restorePageState(url: string, originalAria: string): Promise<void> {
|
|
453
452
|
try {
|
|
454
453
|
await (this as any).cancelInUi();
|
|
455
|
-
await this.explorer.
|
|
454
|
+
await this.explorer.capturePageState();
|
|
456
455
|
const currentAria = this.stateManager.getCurrentState()?.ariaSnapshot || '';
|
|
457
456
|
if (!diffAriaSnapshots(originalAria, currentAria)) return;
|
|
458
457
|
} catch (err) {
|
|
@@ -194,8 +194,7 @@ export function WithLocators<T extends Constructor>(Base: T) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
if (needsXpath.length > 0) {
|
|
197
|
-
const
|
|
198
|
-
const webElements = await WebElement.fromEidxList(page, needsXpath);
|
|
197
|
+
const webElements = await this.explorer.runWithBrowserRecovery('backfillBrokenLocators', () => WebElement.fromEidxList(this.explorer.playwrightHelper.page, needsXpath));
|
|
199
198
|
const changedSections = new Set<(typeof sections)[0]>();
|
|
200
199
|
for (const w of webElements) {
|
|
201
200
|
const entry = needsXpathEls.get(w.eidx!);
|
package/src/ai/researcher.ts
CHANGED
|
@@ -68,6 +68,13 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
68
68
|
this.stateManager = explorer.getStateManager();
|
|
69
69
|
this.experienceTracker = this.stateManager.getExperienceTracker();
|
|
70
70
|
this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
|
|
71
|
+
|
|
72
|
+
const ai = explorer.getConfig().ai;
|
|
73
|
+
if (ai) {
|
|
74
|
+
ai.agents ??= {};
|
|
75
|
+
ai.agents.researcher ??= {};
|
|
76
|
+
ai.agents.researcher.reasoning ??= 'low';
|
|
77
|
+
}
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
protected getNavigator(): Navigator {
|
|
@@ -130,12 +137,12 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
130
137
|
|
|
131
138
|
const annotatedElements = await this.explorer.annotateElements();
|
|
132
139
|
debugLog(`Annotated ${annotatedElements.length} interactive elements with eidx`);
|
|
133
|
-
this.actionResult = await this.explorer.
|
|
140
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot && this.provider.hasVision() });
|
|
134
141
|
|
|
135
142
|
const condition = detectPageCondition(this.actionResult!);
|
|
136
143
|
if (condition === 'error') {
|
|
137
144
|
tag('warning').log(`Detected error page at ${state.url}`);
|
|
138
|
-
throw new ErrorPageError(state.url, this.actionResult!.title);
|
|
145
|
+
throw new ErrorPageError(state.url, this.actionResult!.title, this.actionResult!.httpStatus);
|
|
139
146
|
}
|
|
140
147
|
if (condition === 'loading') {
|
|
141
148
|
const settled = await this.waitUntilSettled(screenshot);
|
|
@@ -177,7 +184,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
177
184
|
} catch (error) {
|
|
178
185
|
if (!(error instanceof ContextLengthError) || retriesLeft <= 0) {
|
|
179
186
|
if (error instanceof ContextLengthError) {
|
|
180
|
-
tag('warning').log('Output truncated. Try lowering reasoning effort or increasing
|
|
187
|
+
tag('warning').log('Output truncated. Try lowering reasoning effort or increasing maxOutputTokens in ai.config.');
|
|
181
188
|
}
|
|
182
189
|
throw error;
|
|
183
190
|
}
|
|
@@ -239,7 +246,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
239
246
|
// Must run BEFORE visuallyAnnotateContainers — annotation overlays inject z-index 99998+ which would pollute the scoring.
|
|
240
247
|
if (!interrupted() && this.hasScreenshotToAnalyze) {
|
|
241
248
|
const sections = parseResearchSections(result.text);
|
|
242
|
-
const focused = await detectFocusedSection(this.explorer.playwrightHelper.page, sections);
|
|
249
|
+
const focused = await this.explorer.runWithBrowserRecovery('detectFocusedSection', () => detectFocusedSection(this.explorer.playwrightHelper.page, sections));
|
|
243
250
|
if (focused) markSectionAsFocused(result, focused);
|
|
244
251
|
}
|
|
245
252
|
|
|
@@ -252,7 +259,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
252
259
|
const freshBroken = freshContainerLocs.filter((l) => l.valid === false).map((l) => l.locator);
|
|
253
260
|
const containers = validContainers.filter((c) => !freshBroken.includes(c.css));
|
|
254
261
|
await this.visuallyAnnotateElements({ containers });
|
|
255
|
-
this.actionResult = await this.explorer.
|
|
262
|
+
this.actionResult = await this.explorer.capturePageWithScreenshot();
|
|
256
263
|
const visualResult = await this.analyzeScreenshotForVisualProps();
|
|
257
264
|
if (visualResult.elements.size > 0) {
|
|
258
265
|
await this.mergeVisualData(result, visualResult.elements);
|
|
@@ -331,7 +338,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
331
338
|
if (!this.actionResult) {
|
|
332
339
|
debugLog('No action result, navigating to URL');
|
|
333
340
|
await this.explorer.visit(url);
|
|
334
|
-
this.actionResult = await this.explorer.
|
|
341
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot });
|
|
335
342
|
return;
|
|
336
343
|
}
|
|
337
344
|
|
|
@@ -341,7 +348,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
341
348
|
|
|
342
349
|
if (!isEmpty && isOnCurrentState) {
|
|
343
350
|
if ((!this.actionResult.screenshot && screenshot) || !this.actionResult.ariaSnapshot) {
|
|
344
|
-
this.actionResult = await this.explorer.
|
|
351
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot });
|
|
345
352
|
}
|
|
346
353
|
return;
|
|
347
354
|
}
|
|
@@ -349,6 +356,8 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
349
356
|
if (isEmpty && isOnCurrentState) {
|
|
350
357
|
debugLog('HTML body empty on current URL, waiting for content');
|
|
351
358
|
tag('step').log('Page body is empty, waiting for content...');
|
|
359
|
+
await this.explorer.visit(url);
|
|
360
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot ?? false });
|
|
352
361
|
await this.waitUntilSettled(screenshot ?? false);
|
|
353
362
|
return;
|
|
354
363
|
}
|
|
@@ -357,36 +366,35 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
357
366
|
tag('step').log('Navigating to URL...');
|
|
358
367
|
|
|
359
368
|
await this.explorer.visit(url);
|
|
360
|
-
this.actionResult = await this.explorer.
|
|
369
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot: screenshot ?? false });
|
|
361
370
|
}
|
|
362
371
|
|
|
363
372
|
private async waitUntilSettled(screenshot: boolean): Promise<boolean> {
|
|
364
373
|
const errorPageTimeout = (this.explorer.getConfig().ai?.agents?.researcher as any)?.errorPageTimeout ?? 10;
|
|
365
374
|
if (errorPageTimeout <= 0) return false;
|
|
366
375
|
|
|
367
|
-
const page = this.explorer.playwrightHelper.page;
|
|
368
376
|
const includeScreenshot = screenshot && this.provider.hasVision();
|
|
369
377
|
|
|
370
378
|
try {
|
|
371
|
-
await page?.waitForLoadState('networkidle', { timeout: errorPageTimeout * 1000 });
|
|
379
|
+
await this.explorer.runWithBrowserRecovery('waitUntilSettled', () => this.explorer.playwrightHelper.page?.waitForLoadState('networkidle', { timeout: errorPageTimeout * 1000 }));
|
|
372
380
|
} catch {}
|
|
373
381
|
|
|
374
382
|
await this.explorer.annotateElements();
|
|
375
|
-
this.actionResult = await this.explorer.
|
|
383
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot });
|
|
376
384
|
|
|
377
385
|
let condition = detectPageCondition(this.actionResult!);
|
|
378
386
|
if (condition === 'error') {
|
|
379
|
-
throw new ErrorPageError(this.actionResult!.url, this.actionResult!.title);
|
|
387
|
+
throw new ErrorPageError(this.actionResult!.url, this.actionResult!.title, this.actionResult!.httpStatus);
|
|
380
388
|
}
|
|
381
389
|
if (condition === 'ok') return true;
|
|
382
390
|
|
|
383
391
|
for (let i = 0; i < 3; i++) {
|
|
384
392
|
await new Promise((r) => setTimeout(r, 1000));
|
|
385
393
|
await this.explorer.annotateElements();
|
|
386
|
-
this.actionResult = await this.explorer.
|
|
394
|
+
this.actionResult = await this.explorer.capturePageState({ includeScreenshot });
|
|
387
395
|
condition = detectPageCondition(this.actionResult!);
|
|
388
396
|
if (condition === 'error') {
|
|
389
|
-
throw new ErrorPageError(this.actionResult!.url, this.actionResult!.title);
|
|
397
|
+
throw new ErrorPageError(this.actionResult!.url, this.actionResult!.title, this.actionResult!.httpStatus);
|
|
390
398
|
}
|
|
391
399
|
if (condition === 'ok') return true;
|
|
392
400
|
}
|
|
@@ -762,17 +770,15 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
762
770
|
}
|
|
763
771
|
|
|
764
772
|
async navigateTo(url: string): Promise<void> {
|
|
765
|
-
|
|
766
|
-
await action.execute(`I.amOnPage("${url}")`);
|
|
773
|
+
await this.explorer.visit(url);
|
|
767
774
|
}
|
|
768
775
|
|
|
769
776
|
async cancelInUi() {
|
|
770
777
|
const beforeAria = this.stateManager.getCurrentState()?.ariaSnapshot || null;
|
|
771
|
-
const action = this.explorer.createAction();
|
|
772
778
|
|
|
773
|
-
await
|
|
779
|
+
await this.explorer.executeAction('I.clickXY(0, 0)');
|
|
774
780
|
if (diffAriaSnapshots(beforeAria, this.stateManager.getCurrentState()?.ariaSnapshot || null)) return;
|
|
775
781
|
|
|
776
|
-
await
|
|
782
|
+
await this.explorer.executeAction(`I.pressKey('Escape')`);
|
|
777
783
|
}
|
|
778
784
|
}
|
package/src/ai/rules.ts
CHANGED
|
@@ -153,6 +153,52 @@ export const protectionRule = dedent`
|
|
|
153
153
|
</important>
|
|
154
154
|
`;
|
|
155
155
|
|
|
156
|
+
export const dataProtectionRules = dedent`
|
|
157
|
+
<data_protection_rules>
|
|
158
|
+
${protectionRule}
|
|
159
|
+
|
|
160
|
+
If the user request, scenario, focus, or test instructions explicitly prohibit creating,
|
|
161
|
+
editing, updating, deleting, removing, or otherwise mutating data, do not perform those
|
|
162
|
+
actions through the UI, API preconditions, cleanup, fallback steps, or Fisherman.
|
|
163
|
+
|
|
164
|
+
Do not use Fisherman or API data preparation to bypass a no-mutation, read-only, search,
|
|
165
|
+
filter, tab, or list-inspection constraint. Use visible existing data when it is available.
|
|
166
|
+
If no suitable data exists, report the missing precondition instead of creating data.
|
|
167
|
+
|
|
168
|
+
Destructive actions are allowed only against disposable data created by the current scenario
|
|
169
|
+
or prepared for that scenario by Fisherman/API preconditions. Existing application data must
|
|
170
|
+
remain unchanged.
|
|
171
|
+
</data_protection_rules>
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
export const capabilityGroundingRule = dedent`
|
|
175
|
+
<capability_grounding>
|
|
176
|
+
When a scenario depends on a named action, menu item, status, option, workflow, or feature,
|
|
177
|
+
that capability must be visible or explicitly confirmed in the current research/page context
|
|
178
|
+
for the same target entity type.
|
|
179
|
+
|
|
180
|
+
Do not transfer capabilities between similar entities, rows, lists, detail pages, or menus.
|
|
181
|
+
Do not replace a requested action with a synonym or related action unless the UI explicitly
|
|
182
|
+
shows that action for the target entity.
|
|
183
|
+
|
|
184
|
+
When an action is described as applying to an item, row, card, record, node, or entity,
|
|
185
|
+
the target must be grounded as that kind of data entity in the current context. Do not use
|
|
186
|
+
navigation links, filter tabs, counters, breadcrumbs, headings, toolbar controls, or other
|
|
187
|
+
page controls as the subject of row/entity actions.
|
|
188
|
+
|
|
189
|
+
When a scenario asks to open, view, inspect, or navigate to an entity detail view, success
|
|
190
|
+
requires evidence of that entity detail context. An active filter, selected tab, visible count,
|
|
191
|
+
or filtered list is not enough to prove an entity detail view opened.
|
|
192
|
+
|
|
193
|
+
Do not rewrite a scenario goal to match a similar outcome that happened accidentally. If the
|
|
194
|
+
requested entity detail/action/workflow was not achieved, report that mismatch instead of
|
|
195
|
+
passing the test for a related filter, tab, navigation, or status view.
|
|
196
|
+
|
|
197
|
+
If the required capability is not available for the target entity after reasonable discovery,
|
|
198
|
+
record the missing capability and stop instead of repeatedly trying unrelated locators.
|
|
199
|
+
</capability_grounding>
|
|
200
|
+
`;
|
|
201
|
+
|
|
156
202
|
export const focusedElementRule = dedent`
|
|
157
203
|
<focused_element_actions>
|
|
158
204
|
When a text input element is focused (textbox, combobox, contenteditable):
|
package/src/ai/task-agent.ts
CHANGED
|
@@ -24,7 +24,7 @@ export abstract class TaskAgent {
|
|
|
24
24
|
protected consecutiveFailures = 0;
|
|
25
25
|
protected consecutiveEmptyResults = 0;
|
|
26
26
|
protected recentToolCalls: any[] = [];
|
|
27
|
-
protected
|
|
27
|
+
protected readonly ACTION_TOOLS: string[] = [];
|
|
28
28
|
|
|
29
29
|
private _historian: Historian | null = null;
|
|
30
30
|
private _quartermaster: Quartermaster | null = null;
|