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
@@ -17,6 +17,8 @@ const debugLog = createDebug('explorbot:action');
17
17
  const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
18
18
  const DEFAULT_ACTION_TIMEOUT = 3000;
19
19
  const DEFAULT_PAGE_TIMEOUT = 3000;
20
+ const MAX_NETWORK_CALLS = 10;
21
+ const IMPORTANT_LOG_LEVELS = new Set(['info', 'error', 'warning', 'warn']);
20
22
  class Action {
21
23
  actor;
22
24
  stateManager;
@@ -32,6 +34,8 @@ class Action {
32
34
  recorder;
33
35
  recovery;
34
36
  mainDocumentStatus = undefined;
37
+ networkRequests = [];
38
+ baseOrigin;
35
39
  constructor(actor, stateManager, recorder, recovery) {
36
40
  this.actor = actor;
37
41
  this.stateManager = stateManager;
@@ -39,6 +43,7 @@ class Action {
39
43
  this.playwrightHelper = container.helpers('Playwright');
40
44
  this.recorder = recorder;
41
45
  this.recovery = recovery || ((fn) => fn());
46
+ this.baseOrigin = URL.parse(this.config.playwright?.url || '')?.origin || '';
42
47
  }
43
48
  async saveScreenshot() {
44
49
  const currentState = this.stateManager.getCurrentState();
@@ -50,6 +55,7 @@ class Action {
50
55
  await this.actor.saveScreenshot(filename);
51
56
  if (currentState)
52
57
  currentState.screenshotFile = filename;
58
+ tag('data').log('screenshot', { path: outputPath('states', filename) });
53
59
  return filename;
54
60
  }
55
61
  catch (err) {
@@ -97,7 +103,10 @@ class Action {
97
103
  const screenshotPath = join(statesDir, filename);
98
104
  screenshotFile = await page
99
105
  ?.screenshot({ path: screenshotPath, fullPage: true })
100
- .then(() => filename)
106
+ .then(() => {
107
+ tag('data').log('screenshot', { path: screenshotPath });
108
+ return filename;
109
+ })
101
110
  .catch((err) => {
102
111
  debugLog('Screenshot failed, continuing without it:', err);
103
112
  return undefined;
@@ -113,9 +122,7 @@ class Action {
113
122
  const logPath = join(statesDir, logFile);
114
123
  const formattedLogs = browserLogs.map((log) => {
115
124
  const logTimestamp = new Date().toISOString();
116
- const level = (log.type || log.level || 'LOG').toUpperCase();
117
- const message = log.text || log.message || String(log);
118
- return `[${logTimestamp}] ${level}: ${message}`;
125
+ return `[${logTimestamp}] ${log.type.toUpperCase()}: ${log.text}`;
119
126
  });
120
127
  fs.writeFileSync(logPath, `${formattedLogs.join('\n')}\n`, 'utf8');
121
128
  debugLog('Page:', { url, title, size: html.length, html: html.substring(0, 100) });
@@ -138,12 +145,15 @@ class Action {
138
145
  fs.writeFileSync(ariaPath, ariaSnapshot, 'utf8');
139
146
  ariaSnapshotFile = ariaFileName;
140
147
  }
148
+ const networkRequests = this.networkRequests;
149
+ this.networkRequests = [];
141
150
  const result = new ActionResult({
142
151
  html,
143
152
  title,
144
153
  httpStatus: await this.captureMainDocumentStatus(),
145
154
  url,
146
155
  browserLogs,
156
+ networkRequests,
147
157
  htmlFile,
148
158
  logFile,
149
159
  screenshotFile,
@@ -188,27 +198,52 @@ class Action {
188
198
  return undefined;
189
199
  }
190
200
  }
191
- captureMainDocumentResponse() {
201
+ captureResponses() {
192
202
  const page = this.playwrightHelper.page;
193
203
  if (!page?.on || !page?.off)
194
204
  return () => { };
195
205
  this.mainDocumentStatus = undefined;
206
+ this.networkRequests = [];
196
207
  const handler = (response) => {
197
208
  const request = response.request();
198
- if (request.resourceType() !== 'document')
199
- return;
200
- if (response.frame() !== page.mainFrame())
201
- return;
202
209
  const status = response.status();
203
210
  if (typeof status !== 'number')
204
211
  return;
205
212
  if (status <= 0)
206
213
  return;
207
- this.mainDocumentStatus = status;
214
+ if (request.resourceType() === 'document') {
215
+ if (response.frame() !== page.mainFrame())
216
+ return;
217
+ this.mainDocumentStatus = status;
218
+ return;
219
+ }
220
+ this.recordNetworkCall(request, status);
208
221
  };
209
222
  page.on('response', handler);
210
223
  return () => page.off('response', handler);
211
224
  }
225
+ recordNetworkCall(request, status) {
226
+ const resourceType = request.resourceType();
227
+ if (resourceType !== 'xhr' && resourceType !== 'fetch')
228
+ return;
229
+ const url = URL.parse(request.url());
230
+ if (!url)
231
+ return;
232
+ if (url.origin !== this.baseOrigin)
233
+ return;
234
+ const call = { method: request.method(), path: url.pathname, status };
235
+ if (this.networkRequests.some((r) => r.method === call.method && r.path === call.path && r.status === call.status))
236
+ return;
237
+ if (this.networkRequests.length >= MAX_NETWORK_CALLS) {
238
+ if (status < 400)
239
+ return;
240
+ const succeeded = this.networkRequests.findIndex((r) => r.status < 400);
241
+ if (succeeded === -1)
242
+ return;
243
+ this.networkRequests.splice(succeeded, 1);
244
+ }
245
+ this.networkRequests.push(call);
246
+ }
212
247
  /**
213
248
  * Capture HTML snapshots of all iframes on the page
214
249
  */
@@ -245,12 +280,7 @@ class Action {
245
280
  async captureBrowserLogs() {
246
281
  try {
247
282
  const logs = await this.actor.grabBrowserLogs();
248
- // Filter for important logs (info, error, warning)
249
- const importantLogs = logs.filter((log) => {
250
- const level = log.type || log.level;
251
- return ['info', 'error', 'warning', 'warn'].includes(level);
252
- });
253
- return importantLogs;
283
+ return logs.map(toBrowserLog).filter((log) => IMPORTANT_LOG_LEVELS.has(log.type));
254
284
  }
255
285
  catch (error) {
256
286
  debugLog('Failed to capture browser logs:', error);
@@ -266,7 +296,7 @@ class Action {
266
296
  const stepListener = attachStepLogger(executedSteps, assertionSteps);
267
297
  const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
268
298
  this.playwrightGroupId = groupId;
269
- const detachMainDocumentResponse = this.captureMainDocumentResponse();
299
+ const detachResponses = this.captureResponses();
270
300
  const activeSpan = Observability.getSpan();
271
301
  const tracer = trace.getTracer('ai');
272
302
  const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
@@ -310,7 +340,7 @@ class Action {
310
340
  }
311
341
  finally {
312
342
  this.restorePageTimeout();
313
- detachMainDocumentResponse();
343
+ detachResponses();
314
344
  if (groupId)
315
345
  await this.recorder.endAction();
316
346
  detachStepLogger(stepListener);
@@ -389,6 +419,11 @@ async function captureHtml(page, frame, actor) {
389
419
  return actor.grabSource();
390
420
  throw new Error('Playwright page is unavailable for HTML capture');
391
421
  }
422
+ function toBrowserLog(log) {
423
+ const type = typeof log.type === 'function' ? log.type() : log.type || log.level || 'log';
424
+ const text = typeof log.text === 'function' ? log.text() : log.text || log.message || String(log);
425
+ return { type, text: text.replace(/\s+/g, ' ').trim() };
426
+ }
392
427
  async function captureTitle(page, actor) {
393
428
  if (page?.title)
394
429
  return page.title();
@@ -15,7 +15,7 @@ export function WithWebMode(Base) {
15
15
  researcher: ctx.explorBot.agentResearcher(),
16
16
  navigator: ctx.explorBot.agentNavigator(),
17
17
  });
18
- const { see, context, visualClick, learnExperience } = agentTools;
18
+ const { see, context, visualClick } = agentTools;
19
19
  const tools = {
20
20
  navigate: tool({
21
21
  description: 'Navigate to a URL or page description using AI-powered navigation.',
@@ -115,7 +115,6 @@ export function WithWebMode(Base) {
115
115
  }),
116
116
  ...codeceptTools,
117
117
  context,
118
- learnExperience,
119
118
  };
120
119
  if (see)
121
120
  tools.see = see;
@@ -59,6 +59,16 @@ export declare class Captain extends CaptainBase implements Agent {
59
59
  planSummary(): string;
60
60
  reinjectContextIfNeeded(conversation: Conversation, currentState: WebPageState): Promise<void>;
61
61
  coreTools(task: Task, onDone: (summary: string) => void): {
62
+ learnExperience: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
63
+ fileTag: any;
64
+ sectionIndex: any;
65
+ }, {
66
+ title: string;
67
+ url: string;
68
+ content: string;
69
+ } | {
70
+ error: string;
71
+ }, import("@ai-sdk/provider-utils").Context>>;
62
72
  done: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
63
73
  summary: string;
64
74
  details?: string;
@@ -91,6 +101,16 @@ export declare class Captain extends CaptainBase implements Agent {
91
101
  }, import("@ai-sdk/provider-utils").Context>>;
92
102
  };
93
103
  tools(task: Task, onDone: (summary: string) => void): Promise<{
104
+ learnExperience: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
105
+ fileTag: any;
106
+ sectionIndex: any;
107
+ }, {
108
+ title: string;
109
+ url: string;
110
+ content: string;
111
+ } | {
112
+ error: string;
113
+ }, import("@ai-sdk/provider-utils").Context>>;
94
114
  done: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
95
115
  summary: string;
96
116
  details?: string;
@@ -15,7 +15,7 @@ import { WithWebMode } from "./captain/web-mode.js";
15
15
  import { toolExecutionLabel } from './conversation.js';
16
16
  import { Researcher } from "./researcher.js";
17
17
  import { TaskAgent } from "./task-agent.js";
18
- import { withdrawVisionTools } from "./tools.js";
18
+ import { createLearnExperienceTool, withdrawVisionTools } from "./tools.js";
19
19
  const MAX_STEPS = 15;
20
20
  const CaptainBase = WithTestMode(WithWebMode(WithIdleMode(TaskAgent)));
21
21
  export class Captain extends CaptainBase {
@@ -216,6 +216,15 @@ export class Captain extends CaptainBase {
216
216
  }
217
217
  coreTools(task, onDone) {
218
218
  return {
219
+ learnExperience: createLearnExperienceTool({
220
+ getExperienceTracker: () => this.getExperienceTracker(),
221
+ getState: () => {
222
+ const state = this.explorBot.stateManager().getCurrentState();
223
+ if (!state)
224
+ return null;
225
+ return ActionResult.fromState(state);
226
+ },
227
+ }),
219
228
  done: tool({
220
229
  description: 'Call when the user request is fulfilled.',
221
230
  inputSchema: z.object({
@@ -14,7 +14,7 @@ import { eidxInContainer } from "../utils/web-eidx.js";
14
14
  import { WebElement } from "../utils/web-element.js";
15
15
  import { drillLocatorRule } from "./rules.js";
16
16
  import { TaskAgent, isInteractive } from "./task-agent.js";
17
- import { createCodeceptJSTools } from "./tools.js";
17
+ import { createCodeceptJSTools, createLearnExperienceTool } from "./tools.js";
18
18
  const debugLog = createDebug('explorbot:driller');
19
19
  export class Driller extends TaskAgent {
20
20
  ACTION_TOOLS = ['click', 'pressKey', 'form'];
@@ -234,7 +234,11 @@ export class Driller extends TaskAgent {
234
234
  conversation.addUserText(await this.buildComponentPrompt(originalState, component));
235
235
  let finished = false;
236
236
  const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.toolDeps, test), component);
237
- const tools = { ...actionTools, ...this.createDrillFlowTools(originalState, test, interactive) };
237
+ const learnExperience = createLearnExperienceTool({
238
+ getExperienceTracker: () => this.getExperienceTracker(),
239
+ getState: () => ActionResult.fromState(this.stateManager.getCurrentState() || originalState),
240
+ });
241
+ const tools = { ...actionTools, learnExperience, ...this.createDrillFlowTools(originalState, test, interactive) };
238
242
  await loop(async ({ stop, iteration }) => {
239
243
  debugLog(`Drilling component ${component.name}, iteration ${iteration}`);
240
244
  setActivity(`${this.emoji} Drilling ${component.name}...`, 'action');
@@ -9,18 +9,49 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
9
9
  method: any;
10
10
  path: any;
11
11
  }, {
12
+ source: string;
13
+ method: any;
14
+ path: any;
15
+ definition: string;
16
+ rejectedCapture: {
17
+ status: number;
18
+ requestBody: any;
19
+ };
20
+ usable?: undefined;
21
+ rejectedRequestBody?: undefined;
22
+ status?: undefined;
23
+ requestBody?: undefined;
24
+ error?: undefined;
25
+ } | {
26
+ source: string;
27
+ method: any;
28
+ path: any;
29
+ usable: boolean;
30
+ rejectedRequestBody: any;
31
+ status: number;
32
+ definition?: undefined;
33
+ rejectedCapture?: undefined;
34
+ requestBody?: undefined;
35
+ error?: undefined;
36
+ } | {
12
37
  source: string;
13
38
  method: string;
14
39
  path: string;
15
40
  status: number;
16
41
  requestBody: any;
17
42
  definition?: undefined;
43
+ rejectedCapture?: undefined;
44
+ usable?: undefined;
45
+ rejectedRequestBody?: undefined;
18
46
  error?: undefined;
19
47
  } | {
20
48
  source: string;
21
49
  definition: string;
22
50
  method?: undefined;
23
51
  path?: undefined;
52
+ rejectedCapture?: undefined;
53
+ usable?: undefined;
54
+ rejectedRequestBody?: undefined;
24
55
  status?: undefined;
25
56
  requestBody?: undefined;
26
57
  error?: undefined;
@@ -29,20 +60,25 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
29
60
  error: any;
30
61
  method?: undefined;
31
62
  path?: undefined;
63
+ definition?: undefined;
64
+ rejectedCapture?: undefined;
65
+ usable?: undefined;
66
+ rejectedRequestBody?: undefined;
32
67
  status?: undefined;
33
68
  requestBody?: undefined;
34
- definition?: undefined;
35
69
  }, import("@ai-sdk/provider-utils").Context>>;
36
70
  request: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<any, {
37
71
  success: boolean;
38
72
  error: string;
39
73
  status?: undefined;
40
74
  statusText?: undefined;
75
+ category?: undefined;
41
76
  errorPreview?: undefined;
42
77
  } | {
43
78
  success: boolean;
44
79
  status: number;
45
80
  statusText: string;
81
+ category: ResponseCategory;
46
82
  errorPreview: string;
47
83
  error?: undefined;
48
84
  } | {
@@ -50,6 +86,7 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
50
86
  status: number;
51
87
  error?: undefined;
52
88
  statusText?: undefined;
89
+ category?: undefined;
53
90
  errorPreview?: undefined;
54
91
  }, import("@ai-sdk/provider-utils").Context>>;
55
92
  finish: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
@@ -88,3 +125,5 @@ export interface FishermanResult {
88
125
  reason: string;
89
126
  }>;
90
127
  }
128
+ type ResponseCategory = 'validation' | 'authorization' | 'not_found' | 'conflict' | 'temporary' | 'server' | 'client';
129
+ export {};
@@ -23,6 +23,29 @@ export function createFishermanTools(apiClient, requestStore, opts) {
23
23
  tag('step').log(`Fisherman: spec lookup ${method} ${path}`);
24
24
  const captured = requestStore.findCapturedRequest(method, path);
25
25
  if (captured) {
26
+ if (captured.status >= 400) {
27
+ const rejectedCapture = {
28
+ status: captured.status,
29
+ requestBody: captured.requestBody || 'no body',
30
+ };
31
+ if (opts.spec) {
32
+ try {
33
+ const definition = extractEndpointDefinition(opts.spec, path, opts.baseEndpoint);
34
+ return { source: 'spec', method, path, definition, rejectedCapture };
35
+ }
36
+ catch {
37
+ return { source: 'captured', method, path, usable: false, rejectedRequestBody: captured.requestBody || 'no body', status: captured.status };
38
+ }
39
+ }
40
+ return {
41
+ source: 'captured',
42
+ method: captured.method,
43
+ path: captured.path,
44
+ status: captured.status,
45
+ usable: false,
46
+ rejectedRequestBody: captured.requestBody || 'no body',
47
+ };
48
+ }
26
49
  return {
27
50
  source: 'captured',
28
51
  method: captured.method,
@@ -74,6 +97,7 @@ export function createFishermanTools(apiClient, requestStore, opts) {
74
97
  success: false,
75
98
  status: reqResult.status,
76
99
  statusText: reqResult.statusText,
100
+ category: responseCategory(reqResult.status),
77
101
  errorPreview: reqResult.rawResponseBody.substring(0, 300),
78
102
  };
79
103
  }
@@ -127,6 +151,21 @@ export function createFishermanTools(apiClient, requestStore, opts) {
127
151
  };
128
152
  return { tools, getResult, isFinished };
129
153
  }
154
+ function responseCategory(status) {
155
+ if (status === 400 || status === 422)
156
+ return 'validation';
157
+ if (status === 401 || status === 403)
158
+ return 'authorization';
159
+ if (status === 404)
160
+ return 'not_found';
161
+ if (status === 409)
162
+ return 'conflict';
163
+ if (status === 408 || status === 425 || status === 429)
164
+ return 'temporary';
165
+ if (status >= 500)
166
+ return 'server';
167
+ return 'client';
168
+ }
130
169
  function extractKeyFields(body, result = {}, depth = 0) {
131
170
  if (!body || typeof body !== 'object' || depth > 5)
132
171
  return result;
@@ -170,7 +170,8 @@ export class Fisherman {
170
170
  RULES:
171
171
  - Always call getEndpointSpec before your first request to an unfamiliar endpoint
172
172
  - Chain requests logically — create parent resources before children
173
- - If a request fails, try once more with adjusted data before reporting failure
173
+ - 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
174
+ - Retry temporary or server failures once. Retry other failures only when the specification or error text gives a concrete correction
174
175
  - Use realistic but unique data for each item (vary names, titles)
175
176
 
176
177
  ${dataProtectionRules}
@@ -15,6 +15,7 @@ declare class Navigator implements Agent {
15
15
  experienceTracker: ExperienceTracker;
16
16
  hooksRunner: HooksRunner;
17
17
  MAX_ATTEMPTS: number;
18
+ lastFailureReason: string | null;
18
19
  systemPrompt: string;
19
20
  freeSailSystemPrompt: string;
20
21
  explorer: Explorer;
@@ -25,18 +26,39 @@ declare class Navigator implements Agent {
25
26
  get verifyTimeout(): number;
26
27
  getBaseOrigin(): string | null;
27
28
  getComparableCurrentUrl(stateManager: any, expectedUrl: string): string;
29
+ comparableUrl(state: {
30
+ url?: string;
31
+ fullUrl?: string;
32
+ }, expectedUrl: string): string;
28
33
  isSameExpectedOrigin(expectedUrl: string, stateManager: any): boolean;
29
34
  isOnExpectedPage(expectedUrl: string, stateManager: any): boolean;
30
35
  visit(url: string): Promise<void>;
31
36
  visitOnce(url: string): Promise<void>;
37
+ navigationError(url: string, fallback: string): Error;
38
+ failureReason(stopReason: string | null, knowledge: string, url: string): string | null;
32
39
  resolveState(message: string, actionResult: ActionResult, opts?: {
33
40
  action?: Action;
34
41
  expectedUrl?: string;
42
+ experience?: string;
35
43
  onAttempt?: (attempt: {
36
44
  code: string;
37
45
  error?: string;
38
46
  }) => void;
39
47
  }): Promise<boolean>;
48
+ buildResolutionPrompt(message: string, actionResult: ActionResult, injectedExperience?: string): Promise<string>;
49
+ buildRetryFeedback(failures: BatchFailure[], includeHtml: boolean, actionResult: ActionResult): Promise<string>;
50
+ executeAttempt(action: Action, codeBlock: string, message: string): Promise<{
51
+ ok: boolean;
52
+ error?: string;
53
+ }>;
54
+ verifyNavigation(action: Action, expectedUrl: string): Promise<{
55
+ freshState: ActionResult;
56
+ urlMatches: boolean;
57
+ }>;
58
+ ariaDiff(freshState: ActionResult, previous: ActionResult): Promise<string | null>;
59
+ saveFlow(message: string, expectedUrl: string | undefined, actionResult: ActionResult, progressBlocks: string[]): void;
60
+ rescueDelayedRedirect(action: Action, expectedUrl: string): Promise<boolean>;
61
+ askUserToResolve(action: Action, message: string, expectedUrl: string | undefined, stopReason: string | null): Promise<boolean>;
40
62
  buildExperienceTools(): {
41
63
  learnExperience: unknown;
42
64
  } | undefined;
@@ -61,6 +83,12 @@ declare class Navigator implements Agent {
61
83
  }>;
62
84
  checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean;
63
85
  }
86
+ type BatchFailure = {
87
+ code: string;
88
+ error: string;
89
+ ariaChanges?: string | null;
90
+ urlAfter?: string;
91
+ };
64
92
  export type AssertionResult = {
65
93
  code: string;
66
94
  passed: boolean;