explorbot 0.2.4 → 0.3.0

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 (113) hide show
  1. package/bin/explorbot-cli.ts +19 -7
  2. package/boat/api-tester/src/cli.ts +17 -0
  3. package/boat/doc-collector/src/cli.ts +14 -1
  4. package/boat/prima/README.md +96 -0
  5. package/boat/prima/package.json +14 -10
  6. package/boat/prima/src/cli.ts +29 -12
  7. package/boat/prima/src/envelope.ts +35 -13
  8. package/boat/prima/src/prima.ts +78 -45
  9. package/dist/bin/explorbot-cli.js +19 -7
  10. package/dist/boat/api-tester/src/cli.js +17 -0
  11. package/dist/boat/doc-collector/src/cli.js +14 -1
  12. package/dist/boat/prima/src/cli.js +26 -7
  13. package/dist/boat/prima/src/envelope.js +32 -8
  14. package/dist/boat/prima/src/prima.js +75 -43
  15. package/dist/models.json +4 -4
  16. package/dist/package.json +6 -2
  17. package/dist/src/action-result.d.ts +13 -0
  18. package/dist/src/action-result.js +46 -15
  19. package/dist/src/action.d.ts +5 -2
  20. package/dist/src/action.js +53 -18
  21. package/dist/src/ai/captain/web-mode.js +1 -2
  22. package/dist/src/ai/captain.d.ts +20 -0
  23. package/dist/src/ai/captain.js +10 -1
  24. package/dist/src/ai/driller.js +6 -2
  25. package/dist/src/ai/fisherman-tools.d.ts +40 -1
  26. package/dist/src/ai/fisherman-tools.js +39 -0
  27. package/dist/src/ai/fisherman.js +2 -1
  28. package/dist/src/ai/navigator.d.ts +28 -0
  29. package/dist/src/ai/navigator.js +223 -175
  30. package/dist/src/ai/pilot.d.ts +7 -4
  31. package/dist/src/ai/pilot.js +89 -30
  32. package/dist/src/ai/planner/subpages.js +2 -16
  33. package/dist/src/ai/planner.js +1 -1
  34. package/dist/src/ai/provider.d.ts +2 -2
  35. package/dist/src/ai/provider.js +28 -22
  36. package/dist/src/ai/researcher/cache.d.ts +10 -3
  37. package/dist/src/ai/researcher/cache.js +23 -10
  38. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  39. package/dist/src/ai/researcher/fingerprint-worker.js +21 -4
  40. package/dist/src/ai/researcher.js +6 -4
  41. package/dist/src/ai/rules.js +1 -5
  42. package/dist/src/ai/session-analyst.js +2 -0
  43. package/dist/src/ai/tester.d.ts +6 -3
  44. package/dist/src/ai/tester.js +30 -35
  45. package/dist/src/ai/tools.d.ts +8 -5
  46. package/dist/src/ai/tools.js +83 -57
  47. package/dist/src/commands/config-command.d.ts +51 -0
  48. package/dist/src/commands/config-command.js +117 -0
  49. package/dist/src/commands/index.js +2 -0
  50. package/dist/src/commands/init-command.js +13 -20
  51. package/dist/src/config.d.ts +8 -1
  52. package/dist/src/config.js +43 -1
  53. package/dist/src/experience-tracker.d.ts +2 -0
  54. package/dist/src/experience-tracker.js +12 -0
  55. package/dist/src/explorbot.js +5 -2
  56. package/dist/src/playwright-recorder.js +6 -12
  57. package/dist/src/remote.d.ts +3 -2
  58. package/dist/src/remote.js +8 -2
  59. package/dist/src/state-manager.d.ts +1 -1
  60. package/dist/src/state-manager.js +3 -1
  61. package/dist/src/test-plan.d.ts +9 -0
  62. package/dist/src/test-plan.js +30 -0
  63. package/dist/src/utils/html-diff.d.ts +5 -0
  64. package/dist/src/utils/html-diff.js +65 -6
  65. package/dist/src/utils/logger.d.ts +1 -1
  66. package/dist/src/utils/logger.js +8 -0
  67. package/dist/src/utils/strings.d.ts +2 -0
  68. package/dist/src/utils/strings.js +32 -0
  69. package/dist/src/utils/url-matcher.d.ts +1 -0
  70. package/dist/src/utils/url-matcher.js +31 -2
  71. package/docs/basics/getting-started.md +33 -10
  72. package/docs/basics/providers.md +6 -4
  73. package/docs/contributing/npm-package.md +73 -4
  74. package/docs/index.json +2 -1
  75. package/docs/reference/commands.md +3 -0
  76. package/docs/reference/websocket.md +50 -0
  77. package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
  78. package/models.json +4 -4
  79. package/package.json +6 -2
  80. package/src/action-result.ts +61 -16
  81. package/src/action.ts +56 -18
  82. package/src/ai/captain/web-mode.ts +1 -2
  83. package/src/ai/captain.ts +9 -1
  84. package/src/ai/driller.ts +6 -2
  85. package/src/ai/fisherman-tools.ts +35 -0
  86. package/src/ai/fisherman.ts +2 -1
  87. package/src/ai/navigator.ts +238 -179
  88. package/src/ai/pilot.ts +104 -36
  89. package/src/ai/planner/subpages.ts +2 -13
  90. package/src/ai/planner.ts +1 -1
  91. package/src/ai/provider.ts +29 -21
  92. package/src/ai/researcher/cache.ts +29 -11
  93. package/src/ai/researcher/deep-analysis.ts +1 -1
  94. package/src/ai/researcher/fingerprint-worker.ts +23 -5
  95. package/src/ai/researcher.ts +6 -4
  96. package/src/ai/rules.ts +1 -5
  97. package/src/ai/session-analyst.ts +2 -0
  98. package/src/ai/tester.ts +33 -34
  99. package/src/ai/tools.ts +88 -61
  100. package/src/commands/config-command.ts +146 -0
  101. package/src/commands/index.ts +2 -0
  102. package/src/commands/init-command.ts +14 -20
  103. package/src/config.ts +47 -2
  104. package/src/experience-tracker.ts +13 -0
  105. package/src/explorbot.ts +4 -2
  106. package/src/playwright-recorder.ts +6 -11
  107. package/src/remote.ts +8 -2
  108. package/src/state-manager.ts +5 -2
  109. package/src/test-plan.ts +38 -0
  110. package/src/utils/html-diff.ts +72 -7
  111. package/src/utils/logger.ts +9 -1
  112. package/src/utils/strings.ts +36 -0
  113. package/src/utils/url-matcher.ts +27 -2
@@ -4,7 +4,7 @@ import { ConfigParser, type HtmlConfig, outputPath } from './config.ts';
4
4
  import type { Link, WebPageState } from './state-manager.ts';
5
5
  import { LARGE_ARIA_CHANGE_THRESHOLD, compactAriaSnapshot, diffAriaSnapshots } from './utils/aria.ts';
6
6
  import { TTLCache } from './utils/cache.ts';
7
- import { type HtmlDiffPart, type HtmlDiffResult, htmlDiff } from './utils/html-diff.ts';
7
+ import { type HtmlDiffPart, type HtmlDiffResult, htmlDiff, liveRegionMessages } from './utils/html-diff.ts';
8
8
  import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from './utils/html.ts';
9
9
  import { createDebug } from './utils/logger.ts';
10
10
  import { slugify } from './utils/strings.ts';
@@ -27,6 +27,7 @@ interface ActionResultData extends WebPageState {
27
27
  h3?: string | undefined;
28
28
  h4?: string | undefined;
29
29
  browserLogs?: any[];
30
+ networkRequests?: NetworkCall[];
30
31
  iframeSnapshots?: Array<{ src: string; html: string; id?: string }>;
31
32
  ariaSnapshot?: string | null;
32
33
  ariaSnapshotFile?: string;
@@ -41,6 +42,9 @@ export interface PageDiff {
41
42
  currentUrl: string;
42
43
  ariaChanges?: string | null;
43
44
  ariaChangeCount?: number;
45
+ messages?: string[];
46
+ requests?: NetworkCall[];
47
+ consoleErrors?: string[];
44
48
  htmlParts?: HtmlDiffPart[];
45
49
  iframes?: string;
46
50
  }
@@ -66,6 +70,7 @@ export class ActionResult implements ActionResultData {
66
70
  public url = '';
67
71
  public fullUrl: string | undefined = undefined;
68
72
  public browserLogs: any[] = [];
73
+ public networkRequests: NetworkCall[] = [];
69
74
  public iframeSnapshots: Array<{ src: string; html: string; id?: string }> = [];
70
75
  public iframeURL: string | undefined = undefined;
71
76
  readonly screenshotFile: string | undefined = undefined;
@@ -91,6 +96,7 @@ export class ActionResult implements ActionResultData {
91
96
  this.httpStatus = data.httpStatus;
92
97
  this.error = data.error ?? null;
93
98
  this.browserLogs = data.browserLogs ?? [];
99
+ this.networkRequests = data.networkRequests ?? [];
94
100
  this.iframeSnapshots = data.iframeSnapshots ?? [];
95
101
  this.iframeURL = data.iframeURL;
96
102
  this.notes = data.notes ?? [];
@@ -508,23 +514,30 @@ export class ActionResult implements ActionResultData {
508
514
  return result;
509
515
  }
510
516
 
511
- const urlChanged = previousState ? !this.isSameUrl({ url: previousState.url }) : true;
517
+ const pageDiff: PageDiff = {
518
+ urlChanged: previousState ? !this.isSameUrl({ url: previousState.url }) : true,
519
+ currentUrl: this.url,
520
+ };
521
+ result.pageDiff = pageDiff;
512
522
 
513
- if (!previousState) {
514
- result.pageDiff = {
515
- urlChanged: true,
516
- currentUrl: this.url,
517
- };
518
- return result;
523
+ if (this.networkRequests.length > 0) {
524
+ pageDiff.requests = this.networkRequests;
525
+ }
526
+
527
+ const consoleErrors = this.consoleErrors();
528
+ if (consoleErrors.length > 0) {
529
+ pageDiff.consoleErrors = consoleErrors;
519
530
  }
520
531
 
532
+ if (!previousState) return result;
533
+
534
+ pageDiff.previousUrl = previousState.url;
535
+
521
536
  const diff = await this.diff(previousState);
522
537
 
523
- const pageDiff: PageDiff = {
524
- urlChanged,
525
- previousUrl: previousState.url,
526
- currentUrl: this.url,
527
- };
538
+ if (diff.messages.length > 0) {
539
+ pageDiff.messages = diff.messages;
540
+ }
528
541
 
529
542
  if (diff.ariaChanged) {
530
543
  pageDiff.ariaChanges = diff.ariaChanged;
@@ -552,11 +565,28 @@ export class ActionResult implements ActionResultData {
552
565
  }
553
566
  }
554
567
 
555
- result.pageDiff = pageDiff;
556
568
  return result;
557
569
  }
570
+
571
+ private consoleErrors(): string[] {
572
+ const errors: string[] = [];
573
+
574
+ for (const log of this.browserLogs) {
575
+ if ((log.type || log.level) !== 'error') continue;
576
+ const text = String(log.text || log.message || log).trim();
577
+ if (!text) continue;
578
+ if (errors.includes(text)) continue;
579
+ errors.push(text.slice(0, CONSOLE_ERROR_MAX_LENGTH));
580
+ if (errors.length === CONSOLE_ERROR_LIMIT) break;
581
+ }
582
+
583
+ return errors;
584
+ }
558
585
  }
559
586
 
587
+ const CONSOLE_ERROR_MAX_LENGTH = 300;
588
+ const CONSOLE_ERROR_LIMIT = 3;
589
+
560
590
  const HTML_PARTS_TOTAL_BUDGET = 8000;
561
591
  const HTML_PARTS_COUNT_LIMIT = 8;
562
592
  const HTML_PART_SUBTREE_BUDGET = 2000;
@@ -584,6 +614,7 @@ function collapseHtmlParts(parts: HtmlDiffPart[]): HtmlDiffPart[] {
584
614
 
585
615
  export class Diff {
586
616
  private _htmlDiffResult: HtmlDiffResult | null = null;
617
+ private _messages: string[] = [];
587
618
  private _ariaDiffResult: string | null = null;
588
619
  private _ariaChangeCount = 0;
589
620
  private _isSameUrl: boolean;
@@ -636,19 +667,33 @@ export class Diff {
636
667
  return this._htmlDiffResult;
637
668
  }
638
669
 
670
+ get messages(): string[] {
671
+ return this._messages;
672
+ }
673
+
639
674
  async calculate(): Promise<void> {
640
675
  if (!this.previous) return;
641
676
 
642
- if (this._isSameUrl) {
643
- this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
677
+ if (!this._isSameUrl) {
678
+ this._messages = liveRegionMessages(this.previous.html, this.current.html);
679
+ return;
644
680
  }
645
681
 
682
+ this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
683
+ this._messages = this._htmlDiffResult.messages;
684
+
646
685
  const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
647
686
  this._ariaDiffResult = ariaDiff.text;
648
687
  this._ariaChangeCount = ariaDiff.count;
649
688
  }
650
689
  }
651
690
 
691
+ export interface NetworkCall {
692
+ method: string;
693
+ path: string;
694
+ status: number;
695
+ }
696
+
652
697
  export interface FocusedElement {
653
698
  role: string;
654
699
  name: string;
package/src/action.ts CHANGED
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { context, trace } from '@opentelemetry/api';
4
4
  import { container, recorder } from 'codeceptjs';
5
5
  import * as codeceptjs from 'codeceptjs';
6
- import { ActionResult, type FocusedElement } from './action-result.js';
6
+ import { ActionResult, type FocusedElement, type NetworkCall } from './action-result.js';
7
7
  import { clearActivity, setActivity } from './activity.ts';
8
8
  import { ConfigParser, outputPath } from './config.js';
9
9
  import type { ExplorbotConfig } from './config.js';
@@ -21,6 +21,8 @@ const debugLog = createDebug('explorbot:action');
21
21
  const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
22
22
  const DEFAULT_ACTION_TIMEOUT = 3000;
23
23
  const DEFAULT_PAGE_TIMEOUT = 3000;
24
+ const MAX_NETWORK_CALLS = 10;
25
+ const IMPORTANT_LOG_LEVELS = new Set(['info', 'error', 'warning', 'warn']);
24
26
 
25
27
  class Action {
26
28
  private actor: CodeceptJS.I;
@@ -38,6 +40,8 @@ class Action {
38
40
  private recorder?: PlaywrightRecorder;
39
41
  private recovery: RecoveryRunner;
40
42
  private mainDocumentStatus: number | undefined = undefined;
43
+ private networkRequests: NetworkCall[] = [];
44
+ private baseOrigin: string;
41
45
 
42
46
  constructor(actor: CodeceptJS.I, stateManager: StateManager, recorder?: PlaywrightRecorder, recovery?: RecoveryRunner) {
43
47
  this.actor = actor;
@@ -46,6 +50,7 @@ class Action {
46
50
  this.playwrightHelper = container.helpers('Playwright');
47
51
  this.recorder = recorder;
48
52
  this.recovery = recovery || ((fn) => fn());
53
+ this.baseOrigin = URL.parse(this.config.playwright?.url || '')?.origin || '';
49
54
  }
50
55
 
51
56
  async saveScreenshot(): Promise<string | undefined> {
@@ -57,6 +62,7 @@ class Action {
57
62
  try {
58
63
  await (this.actor as any).saveScreenshot(filename);
59
64
  if (currentState) currentState.screenshotFile = filename;
65
+ tag('data').log('screenshot', { path: outputPath('states', filename) });
60
66
  return filename;
61
67
  } catch (err) {
62
68
  debugLog('Screenshot failed:', err);
@@ -105,7 +111,10 @@ class Action {
105
111
  const screenshotPath = join(statesDir, filename);
106
112
  screenshotFile = await page
107
113
  ?.screenshot({ path: screenshotPath, fullPage: true })
108
- .then(() => filename)
114
+ .then(() => {
115
+ tag('data').log('screenshot', { path: screenshotPath });
116
+ return filename;
117
+ })
109
118
  .catch((err: Error) => {
110
119
  debugLog('Screenshot failed, continuing without it:', err);
111
120
  return undefined;
@@ -123,9 +132,7 @@ class Action {
123
132
  const logPath = join(statesDir, logFile);
124
133
  const formattedLogs = browserLogs.map((log: any) => {
125
134
  const logTimestamp = new Date().toISOString();
126
- const level = (log.type || log.level || 'LOG').toUpperCase();
127
- const message = log.text || log.message || String(log);
128
- return `[${logTimestamp}] ${level}: ${message}`;
135
+ return `[${logTimestamp}] ${log.type.toUpperCase()}: ${log.text}`;
129
136
  });
130
137
  fs.writeFileSync(logPath, `${formattedLogs.join('\n')}\n`, 'utf8');
131
138
 
@@ -153,12 +160,16 @@ class Action {
153
160
  ariaSnapshotFile = ariaFileName;
154
161
  }
155
162
 
163
+ const networkRequests = this.networkRequests;
164
+ this.networkRequests = [];
165
+
156
166
  const result = new ActionResult({
157
167
  html,
158
168
  title,
159
169
  httpStatus: await this.captureMainDocumentStatus(),
160
170
  url,
161
171
  browserLogs,
172
+ networkRequests,
162
173
  htmlFile,
163
174
  logFile,
164
175
  screenshotFile,
@@ -198,26 +209,53 @@ class Action {
198
209
  }
199
210
  }
200
211
 
201
- private captureMainDocumentResponse(): () => void {
212
+ private captureResponses(): () => void {
202
213
  const page = this.playwrightHelper.page;
203
214
  if (!page?.on || !page?.off) return () => {};
204
215
 
205
216
  this.mainDocumentStatus = undefined;
217
+ this.networkRequests = [];
206
218
 
207
219
  const handler = (response: any) => {
208
220
  const request = response.request();
209
- if (request.resourceType() !== 'document') return;
210
- if (response.frame() !== page.mainFrame()) return;
211
221
  const status = response.status();
212
222
  if (typeof status !== 'number') return;
213
223
  if (status <= 0) return;
214
- this.mainDocumentStatus = status;
224
+
225
+ if (request.resourceType() === 'document') {
226
+ if (response.frame() !== page.mainFrame()) return;
227
+ this.mainDocumentStatus = status;
228
+ return;
229
+ }
230
+
231
+ this.recordNetworkCall(request, status);
215
232
  };
216
233
 
217
234
  page.on('response', handler);
218
235
  return () => page.off('response', handler);
219
236
  }
220
237
 
238
+ private recordNetworkCall(request: any, status: number): void {
239
+ const resourceType = request.resourceType();
240
+ if (resourceType !== 'xhr' && resourceType !== 'fetch') return;
241
+
242
+ const url = URL.parse(request.url());
243
+ if (!url) return;
244
+ if (url.origin !== this.baseOrigin) return;
245
+
246
+ const call: NetworkCall = { method: request.method(), path: url.pathname, status };
247
+ if (this.networkRequests.some((r) => r.method === call.method && r.path === call.path && r.status === call.status)) return;
248
+
249
+ if (this.networkRequests.length >= MAX_NETWORK_CALLS) {
250
+ if (status < 400) return;
251
+ const succeeded = this.networkRequests.findIndex((r) => r.status < 400);
252
+ if (succeeded === -1) return;
253
+ this.networkRequests.splice(succeeded, 1);
254
+ }
255
+
256
+ this.networkRequests.push(call);
257
+ }
258
+
221
259
  /**
222
260
  * Capture HTML snapshots of all iframes on the page
223
261
  */
@@ -263,13 +301,7 @@ class Action {
263
301
  try {
264
302
  const logs = await (this.actor as any).grabBrowserLogs();
265
303
 
266
- // Filter for important logs (info, error, warning)
267
- const importantLogs = logs.filter((log: any) => {
268
- const level = log.type || log.level;
269
- return ['info', 'error', 'warning', 'warn'].includes(level);
270
- });
271
-
272
- return importantLogs;
304
+ return logs.map(toBrowserLog).filter((log: any) => IMPORTANT_LOG_LEVELS.has(log.type));
273
305
  } catch (error) {
274
306
  debugLog('Failed to capture browser logs:', error);
275
307
  return [];
@@ -288,7 +320,7 @@ class Action {
288
320
  const stepListener = attachStepLogger(executedSteps, assertionSteps);
289
321
  const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
290
322
  this.playwrightGroupId = groupId;
291
- const detachMainDocumentResponse = this.captureMainDocumentResponse();
323
+ const detachResponses = this.captureResponses();
292
324
  const activeSpan = Observability.getSpan();
293
325
  const tracer = trace.getTracer('ai');
294
326
  const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
@@ -337,7 +369,7 @@ class Action {
337
369
  throw err;
338
370
  } finally {
339
371
  this.restorePageTimeout();
340
- detachMainDocumentResponse();
372
+ detachResponses();
341
373
  if (groupId) await this.recorder!.endAction();
342
374
  detachStepLogger(stepListener);
343
375
  if (stepSpan) {
@@ -425,6 +457,12 @@ async function captureHtml(page: any, frame: any, actor: any): Promise<string> {
425
457
  throw new Error('Playwright page is unavailable for HTML capture');
426
458
  }
427
459
 
460
+ function toBrowserLog(log: any): { type: string; text: string } {
461
+ const type = typeof log.type === 'function' ? log.type() : log.type || log.level || 'log';
462
+ const text = typeof log.text === 'function' ? log.text() : log.text || log.message || String(log);
463
+ return { type, text: text.replace(/\s+/g, ' ').trim() };
464
+ }
465
+
428
466
  async function captureTitle(page: any, actor: any): Promise<string> {
429
467
  if (page?.title) return page.title();
430
468
  if (actor?.grabTitle) return actor.grabTitle();
@@ -16,7 +16,7 @@ export function WithWebMode<T extends Constructor>(Base: T) {
16
16
  researcher: ctx.explorBot.agentResearcher(),
17
17
  navigator: ctx.explorBot.agentNavigator(),
18
18
  });
19
- const { see, context, visualClick, learnExperience } = agentTools;
19
+ const { see, context, visualClick } = agentTools;
20
20
 
21
21
  const tools: Record<string, any> = {
22
22
  navigate: tool({
@@ -124,7 +124,6 @@ export function WithWebMode<T extends Constructor>(Base: T) {
124
124
 
125
125
  ...codeceptTools,
126
126
  context,
127
- learnExperience,
128
127
  };
129
128
 
130
129
  if (see) tools.see = see;
package/src/ai/captain.ts CHANGED
@@ -21,7 +21,7 @@ import type { Navigator } from './navigator.ts';
21
21
  import type { Provider } from './provider.ts';
22
22
  import { Researcher } from './researcher.ts';
23
23
  import { TaskAgent } from './task-agent.ts';
24
- import { withdrawVisionTools } from './tools.ts';
24
+ import { createLearnExperienceTool, withdrawVisionTools } from './tools.ts';
25
25
 
26
26
  const MAX_STEPS = 15;
27
27
 
@@ -241,6 +241,14 @@ export class Captain extends CaptainBase implements Agent {
241
241
 
242
242
  private coreTools(task: Task, onDone: (summary: string) => void) {
243
243
  return {
244
+ learnExperience: createLearnExperienceTool({
245
+ getExperienceTracker: () => this.getExperienceTracker(),
246
+ getState: () => {
247
+ const state = this.explorBot.stateManager().getCurrentState();
248
+ if (!state) return null;
249
+ return ActionResult.fromState(state);
250
+ },
251
+ }),
244
252
  done: tool({
245
253
  description: 'Call when the user request is fulfilled.',
246
254
  inputSchema: z.object({
package/src/ai/driller.ts CHANGED
@@ -30,7 +30,7 @@ import type { Navigator } from './navigator.ts';
30
30
  import type { Provider } from './provider.ts';
31
31
  import { drillLocatorRule } from './rules.ts';
32
32
  import { TaskAgent, isInteractive } from './task-agent.ts';
33
- import { createCodeceptJSTools } from './tools.ts';
33
+ import { createCodeceptJSTools, createLearnExperienceTool } from './tools.ts';
34
34
 
35
35
  const debugLog = createDebug('explorbot:driller');
36
36
 
@@ -306,7 +306,11 @@ export class Driller extends TaskAgent implements Agent {
306
306
 
307
307
  let finished = false;
308
308
  const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.toolDeps, test), component);
309
- const tools = { ...actionTools, ...this.createDrillFlowTools(originalState, test, interactive) };
309
+ const learnExperience = createLearnExperienceTool({
310
+ getExperienceTracker: () => this.getExperienceTracker(),
311
+ getState: () => ActionResult.fromState(this.stateManager.getCurrentState() || originalState),
312
+ });
313
+ const tools = { ...actionTools, learnExperience, ...this.createDrillFlowTools(originalState, test, interactive) };
310
314
 
311
315
  await loop(
312
316
  async ({ stop, iteration }) => {
@@ -29,6 +29,28 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request
29
29
 
30
30
  const captured = requestStore.findCapturedRequest(method, path);
31
31
  if (captured) {
32
+ if (captured.status >= 400) {
33
+ const rejectedCapture = {
34
+ status: captured.status,
35
+ requestBody: captured.requestBody || 'no body',
36
+ };
37
+ if (opts.spec) {
38
+ try {
39
+ const definition = extractEndpointDefinition(opts.spec, path, opts.baseEndpoint);
40
+ return { source: 'spec', method, path, definition, rejectedCapture };
41
+ } catch {
42
+ return { source: 'captured', method, path, usable: false, rejectedRequestBody: captured.requestBody || 'no body', status: captured.status };
43
+ }
44
+ }
45
+ return {
46
+ source: 'captured',
47
+ method: captured.method,
48
+ path: captured.path,
49
+ status: captured.status,
50
+ usable: false,
51
+ rejectedRequestBody: captured.requestBody || 'no body',
52
+ };
53
+ }
32
54
  return {
33
55
  source: 'captured',
34
56
  method: captured.method,
@@ -87,6 +109,7 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request
87
109
  success: false,
88
110
  status: reqResult.status,
89
111
  statusText: reqResult.statusText,
112
+ category: responseCategory(reqResult.status),
90
113
  errorPreview: reqResult.rawResponseBody.substring(0, 300),
91
114
  };
92
115
  }
@@ -149,6 +172,16 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request
149
172
  return { tools, getResult, isFinished };
150
173
  }
151
174
 
175
+ function responseCategory(status: number): ResponseCategory {
176
+ if (status === 400 || status === 422) return 'validation';
177
+ if (status === 401 || status === 403) return 'authorization';
178
+ if (status === 404) return 'not_found';
179
+ if (status === 409) return 'conflict';
180
+ if (status === 408 || status === 425 || status === 429) return 'temporary';
181
+ if (status >= 500) return 'server';
182
+ return 'client';
183
+ }
184
+
152
185
  function extractKeyFields(body: any, result: Record<string, any> = {}, depth = 0): Record<string, any> {
153
186
  if (!body || typeof body !== 'object' || depth > 5) return result;
154
187
 
@@ -179,3 +212,5 @@ export interface FishermanResult {
179
212
  created: Array<{ type: string; id?: string | number; title?: string }>;
180
213
  failed: Array<{ type: string; reason: string }>;
181
214
  }
215
+
216
+ type ResponseCategory = 'validation' | 'authorization' | 'not_found' | 'conflict' | 'temporary' | 'server' | 'client';
@@ -208,7 +208,8 @@ export class Fisherman implements Agent {
208
208
  RULES:
209
209
  - Always call getEndpointSpec before your first request to an unfamiliar endpoint
210
210
  - Chain requests logically — create parent resources before children
211
- - If a request fails, try once more with adjusted data before reporting failure
211
+ - Use the response category and error text to decide what failed: validation requires corrected data, authorization requires valid access, not_found requires a valid path or parent, and conflict requires resolving the conflicting state
212
+ - Retry temporary or server failures once. Retry other failures only when the specification or error text gives a concrete correction
212
213
  - Use realistic but unique data for each item (vary names, titles)
213
214
 
214
215
  ${dataProtectionRules}