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.
Files changed (68) hide show
  1. package/README.md +83 -245
  2. package/bin/explorbot-cli.ts +1 -0
  3. package/dist/bin/explorbot-cli.js +1 -0
  4. package/dist/package.json +8 -6
  5. package/dist/rules/navigator/verification-actions.md +2 -0
  6. package/dist/src/action-result.js +2 -0
  7. package/dist/src/action.js +88 -7
  8. package/dist/src/ai/captain/file-tools.js +100 -0
  9. package/dist/src/ai/captain/idle-mode.js +70 -6
  10. package/dist/src/ai/captain/web-mode.js +36 -6
  11. package/dist/src/ai/captain.js +87 -19
  12. package/dist/src/ai/fisherman.js +14 -3
  13. package/dist/src/ai/historian/screencast.js +11 -2
  14. package/dist/src/ai/navigator.js +5 -2
  15. package/dist/src/ai/pilot.js +52 -9
  16. package/dist/src/ai/planner.js +16 -5
  17. package/dist/src/ai/provider.js +53 -18
  18. package/dist/src/ai/researcher/coordinates.js +2 -3
  19. package/dist/src/ai/researcher/deep-analysis.js +3 -4
  20. package/dist/src/ai/researcher/locators.js +1 -2
  21. package/dist/src/ai/researcher.js +24 -19
  22. package/dist/src/ai/rules.js +44 -0
  23. package/dist/src/ai/task-agent.js +1 -0
  24. package/dist/src/ai/tester.js +161 -46
  25. package/dist/src/ai/tools.js +84 -8
  26. package/dist/src/commands/explore-command.js +6 -1
  27. package/dist/src/components/LogPane.js +4 -3
  28. package/dist/src/explorbot.js +7 -2
  29. package/dist/src/explorer.js +270 -35
  30. package/dist/src/stats.js +16 -0
  31. package/dist/src/utils/aria.js +66 -6
  32. package/dist/src/utils/browser-errors.js +23 -0
  33. package/dist/src/utils/error-page.js +17 -2
  34. package/dist/src/utils/logger.js +2 -2
  35. package/package.json +8 -6
  36. package/rules/navigator/verification-actions.md +2 -0
  37. package/src/action-result.ts +2 -0
  38. package/src/action.ts +83 -7
  39. package/src/ai/captain/file-tools.ts +126 -0
  40. package/src/ai/captain/idle-mode.ts +72 -6
  41. package/src/ai/captain/mixin.ts +1 -1
  42. package/src/ai/captain/web-mode.ts +40 -5
  43. package/src/ai/captain.ts +94 -20
  44. package/src/ai/fisherman.ts +14 -3
  45. package/src/ai/historian/screencast.ts +11 -2
  46. package/src/ai/navigator.ts +6 -2
  47. package/src/ai/pilot.ts +53 -9
  48. package/src/ai/planner.ts +16 -5
  49. package/src/ai/provider.ts +51 -19
  50. package/src/ai/researcher/coordinates.ts +2 -3
  51. package/src/ai/researcher/deep-analysis.ts +3 -4
  52. package/src/ai/researcher/locators.ts +1 -2
  53. package/src/ai/researcher.ts +25 -19
  54. package/src/ai/rules.ts +46 -0
  55. package/src/ai/task-agent.ts +1 -1
  56. package/src/ai/tester.ts +175 -48
  57. package/src/ai/tools.ts +97 -8
  58. package/src/commands/explore-command.ts +6 -1
  59. package/src/components/LogPane.tsx +4 -3
  60. package/src/config.ts +1 -0
  61. package/src/explorbot.ts +6 -2
  62. package/src/explorer.ts +295 -38
  63. package/src/state-manager.ts +2 -0
  64. package/src/stats.ts +18 -0
  65. package/src/utils/aria.ts +63 -6
  66. package/src/utils/browser-errors.ts +25 -0
  67. package/src/utils/error-page.ts +16 -3
  68. package/src/utils/logger.ts +3 -3
@@ -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, stepCountIs } from 'ai';
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.cachedInputTokens ?? usage.inputTokenDetails?.cacheReadTokens;
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
- maxTokens: 1,
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.experimental_telemetry) {
181
- return runTelemetry || { isEnabled: true };
209
+ if (!options.telemetry) {
210
+ return runTelemetry;
182
211
  }
183
212
 
184
213
  if (!runTelemetry) {
185
- return options.experimental_telemetry;
214
+ return options.telemetry;
186
215
  }
187
216
 
188
217
  return {
189
218
  ...runTelemetry,
190
- ...options.experimental_telemetry,
219
+ ...options.telemetry,
191
220
  metadata: {
192
221
  ...runTelemetry.metadata,
193
- ...options.experimental_telemetry.metadata,
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
- maxTokens: 16384,
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 maxTokens in config or use a model with higher output capacity.');
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[] = [stepCountIs(maxRoundtrips)];
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
- maxTokens: 16384,
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 maxTokens in config or use a model with higher output capacity.');
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
- maxTokens: 16384,
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 visuallyAnnotateContainers(this.explorer.playwrightHelper.page, opts?.containers || []);
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
- const hoverAction = this.explorer.createAction();
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.createAction().capturePageState();
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.createAction().capturePageState();
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 page = this.explorer.playwrightHelper.page;
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!);
@@ -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.createAction().capturePageState({ includeScreenshot: screenshot && this.provider.hasVision() });
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 maxTokens in ai.config.');
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.createAction().caputrePageWithScreenshot();
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.createAction().capturePageState({ includeScreenshot: screenshot });
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.createAction().capturePageState({ includeScreenshot: screenshot });
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.createAction().capturePageState({ includeScreenshot: screenshot ?? false });
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.createAction().capturePageState({ includeScreenshot });
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.createAction().capturePageState({ includeScreenshot });
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
- const action = this.explorer.createAction();
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 action.execute('I.clickXY(0, 0)');
779
+ await this.explorer.executeAction('I.clickXY(0, 0)');
774
780
  if (diffAriaSnapshots(beforeAria, this.stateManager.getCurrentState()?.ariaSnapshot || null)) return;
775
781
 
776
- await action.execute(`I.pressKey('Escape')`);
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):
@@ -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 abstract readonly ACTION_TOOLS: string[];
27
+ protected readonly ACTION_TOOLS: string[] = [];
28
28
 
29
29
  private _historian: Historian | null = null;
30
30
  private _quartermaster: Quartermaster | null = null;