explorbot 0.2.5 → 0.3.1

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 (89) hide show
  1. package/boat/prima/README.md +96 -0
  2. package/boat/prima/package.json +14 -10
  3. package/boat/prima/src/cli.ts +5 -0
  4. package/boat/prima/src/prima.ts +17 -4
  5. package/dist/boat/prima/src/cli.js +7 -0
  6. package/dist/boat/prima/src/prima.js +18 -4
  7. package/dist/models.json +4 -4
  8. package/dist/package.json +6 -2
  9. package/dist/src/action-result.d.ts +13 -0
  10. package/dist/src/action-result.js +46 -15
  11. package/dist/src/action.d.ts +5 -2
  12. package/dist/src/action.js +48 -17
  13. package/dist/src/ai/captain/web-mode.js +1 -2
  14. package/dist/src/ai/captain.d.ts +20 -0
  15. package/dist/src/ai/captain.js +10 -1
  16. package/dist/src/ai/conversation.d.ts +1 -0
  17. package/dist/src/ai/conversation.js +3 -0
  18. package/dist/src/ai/driller.js +6 -2
  19. package/dist/src/ai/fisherman-tools.d.ts +40 -1
  20. package/dist/src/ai/fisherman-tools.js +39 -0
  21. package/dist/src/ai/fisherman.js +3 -2
  22. package/dist/src/ai/navigator.d.ts +2 -1
  23. package/dist/src/ai/navigator.js +5 -9
  24. package/dist/src/ai/pilot.js +51 -29
  25. package/dist/src/ai/planner/subpages.js +2 -16
  26. package/dist/src/ai/planner.js +1 -1
  27. package/dist/src/ai/provider.d.ts +3 -0
  28. package/dist/src/ai/provider.js +80 -17
  29. package/dist/src/ai/researcher/cache.d.ts +8 -3
  30. package/dist/src/ai/researcher/cache.js +13 -8
  31. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  32. package/dist/src/ai/researcher/fingerprint-worker.js +21 -4
  33. package/dist/src/ai/researcher.js +4 -3
  34. package/dist/src/ai/rules.js +1 -5
  35. package/dist/src/ai/tester.d.ts +1 -1
  36. package/dist/src/ai/tester.js +34 -26
  37. package/dist/src/ai/tools.d.ts +8 -5
  38. package/dist/src/ai/tools.js +79 -56
  39. package/dist/src/commands/explore-command.js +22 -17
  40. package/dist/src/commands/init-command.js +13 -20
  41. package/dist/src/config.d.ts +2 -1
  42. package/dist/src/config.js +3 -1
  43. package/dist/src/experience-tracker.d.ts +2 -0
  44. package/dist/src/experience-tracker.js +12 -0
  45. package/dist/src/explorbot.js +1 -1
  46. package/dist/src/playwright-recorder.js +6 -12
  47. package/dist/src/test-plan.d.ts +8 -0
  48. package/dist/src/test-plan.js +11 -0
  49. package/dist/src/utils/html-diff.d.ts +5 -0
  50. package/dist/src/utils/html-diff.js +65 -6
  51. package/dist/src/utils/strings.d.ts +2 -0
  52. package/dist/src/utils/strings.js +32 -0
  53. package/dist/src/utils/url-matcher.d.ts +1 -0
  54. package/dist/src/utils/url-matcher.js +31 -2
  55. package/docs/basics/getting-started.md +33 -10
  56. package/docs/basics/providers.md +6 -4
  57. package/docs/contributing/npm-package.md +73 -4
  58. package/models.json +4 -4
  59. package/package.json +6 -2
  60. package/src/action-result.ts +61 -16
  61. package/src/action.ts +51 -17
  62. package/src/ai/captain/web-mode.ts +1 -2
  63. package/src/ai/captain.ts +9 -1
  64. package/src/ai/conversation.ts +3 -0
  65. package/src/ai/driller.ts +6 -2
  66. package/src/ai/fisherman-tools.ts +35 -0
  67. package/src/ai/fisherman.ts +3 -2
  68. package/src/ai/navigator.ts +6 -10
  69. package/src/ai/pilot.ts +54 -32
  70. package/src/ai/planner/subpages.ts +2 -13
  71. package/src/ai/planner.ts +1 -1
  72. package/src/ai/provider.ts +111 -41
  73. package/src/ai/researcher/cache.ts +17 -9
  74. package/src/ai/researcher/deep-analysis.ts +1 -1
  75. package/src/ai/researcher/fingerprint-worker.ts +23 -5
  76. package/src/ai/researcher.ts +4 -3
  77. package/src/ai/rules.ts +1 -5
  78. package/src/ai/tester.ts +32 -27
  79. package/src/ai/tools.ts +84 -60
  80. package/src/commands/explore-command.ts +17 -14
  81. package/src/commands/init-command.ts +14 -20
  82. package/src/config.ts +4 -2
  83. package/src/experience-tracker.ts +13 -0
  84. package/src/explorbot.ts +1 -1
  85. package/src/playwright-recorder.ts +6 -11
  86. package/src/test-plan.ts +18 -0
  87. package/src/utils/html-diff.ts +72 -7
  88. package/src/utils/strings.ts +36 -0
  89. 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();
@@ -117,9 +122,7 @@ class Action {
117
122
  const logPath = join(statesDir, logFile);
118
123
  const formattedLogs = browserLogs.map((log) => {
119
124
  const logTimestamp = new Date().toISOString();
120
- const level = (log.type || log.level || 'LOG').toUpperCase();
121
- const message = log.text || log.message || String(log);
122
- return `[${logTimestamp}] ${level}: ${message}`;
125
+ return `[${logTimestamp}] ${log.type.toUpperCase()}: ${log.text}`;
123
126
  });
124
127
  fs.writeFileSync(logPath, `${formattedLogs.join('\n')}\n`, 'utf8');
125
128
  debugLog('Page:', { url, title, size: html.length, html: html.substring(0, 100) });
@@ -142,12 +145,15 @@ class Action {
142
145
  fs.writeFileSync(ariaPath, ariaSnapshot, 'utf8');
143
146
  ariaSnapshotFile = ariaFileName;
144
147
  }
148
+ const networkRequests = this.networkRequests;
149
+ this.networkRequests = [];
145
150
  const result = new ActionResult({
146
151
  html,
147
152
  title,
148
153
  httpStatus: await this.captureMainDocumentStatus(),
149
154
  url,
150
155
  browserLogs,
156
+ networkRequests,
151
157
  htmlFile,
152
158
  logFile,
153
159
  screenshotFile,
@@ -192,27 +198,52 @@ class Action {
192
198
  return undefined;
193
199
  }
194
200
  }
195
- captureMainDocumentResponse() {
201
+ captureResponses() {
196
202
  const page = this.playwrightHelper.page;
197
203
  if (!page?.on || !page?.off)
198
204
  return () => { };
199
205
  this.mainDocumentStatus = undefined;
206
+ this.networkRequests = [];
200
207
  const handler = (response) => {
201
208
  const request = response.request();
202
- if (request.resourceType() !== 'document')
203
- return;
204
- if (response.frame() !== page.mainFrame())
205
- return;
206
209
  const status = response.status();
207
210
  if (typeof status !== 'number')
208
211
  return;
209
212
  if (status <= 0)
210
213
  return;
211
- 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);
212
221
  };
213
222
  page.on('response', handler);
214
223
  return () => page.off('response', handler);
215
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
+ }
216
247
  /**
217
248
  * Capture HTML snapshots of all iframes on the page
218
249
  */
@@ -249,12 +280,7 @@ class Action {
249
280
  async captureBrowserLogs() {
250
281
  try {
251
282
  const logs = await this.actor.grabBrowserLogs();
252
- // Filter for important logs (info, error, warning)
253
- const importantLogs = logs.filter((log) => {
254
- const level = log.type || log.level;
255
- return ['info', 'error', 'warning', 'warn'].includes(level);
256
- });
257
- return importantLogs;
283
+ return logs.map(toBrowserLog).filter((log) => IMPORTANT_LOG_LEVELS.has(log.type));
258
284
  }
259
285
  catch (error) {
260
286
  debugLog('Failed to capture browser logs:', error);
@@ -270,7 +296,7 @@ class Action {
270
296
  const stepListener = attachStepLogger(executedSteps, assertionSteps);
271
297
  const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
272
298
  this.playwrightGroupId = groupId;
273
- const detachMainDocumentResponse = this.captureMainDocumentResponse();
299
+ const detachResponses = this.captureResponses();
274
300
  const activeSpan = Observability.getSpan();
275
301
  const tracer = trace.getTracer('ai');
276
302
  const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
@@ -314,7 +340,7 @@ class Action {
314
340
  }
315
341
  finally {
316
342
  this.restorePageTimeout();
317
- detachMainDocumentResponse();
343
+ detachResponses();
318
344
  if (groupId)
319
345
  await this.recorder.endAction();
320
346
  detachStepLogger(stepListener);
@@ -393,6 +419,11 @@ async function captureHtml(page, frame, actor) {
393
419
  return actor.grabSource();
394
420
  throw new Error('Playwright page is unavailable for HTML capture');
395
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
+ }
396
427
  async function captureTitle(page, actor) {
397
428
  if (page?.title)
398
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({
@@ -7,6 +7,7 @@ export interface ToolExecution {
7
7
  }
8
8
  export declare function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution;
9
9
  export declare function toolExecutionLabel(input: Record<string, any> | undefined): string;
10
+ export declare const NARRATION_TOOL = "commentary";
10
11
  export declare class Conversation {
11
12
  messages: ModelMessage[];
12
13
  model: any;
@@ -7,6 +7,7 @@ export function toToolExecution(toolName, input, rawOutput) {
7
7
  export function toolExecutionLabel(input) {
8
8
  return input?.explanation || input?.assertion || input?.reason || input?.request || '';
9
9
  }
10
+ export const NARRATION_TOOL = 'commentary';
10
11
  const AUTO_COMPACT_ARIA_CHANGES_CUTOFF = 500;
11
12
  const AUTO_COMPACT_TARGETED_HTML_CUTOFF = 500;
12
13
  export class Conversation {
@@ -203,6 +204,8 @@ export class Conversation {
203
204
  for (const part of message.content) {
204
205
  if (part.type !== 'tool-result')
205
206
  continue;
207
+ if (part.toolName === NARRATION_TOOL)
208
+ continue;
206
209
  executions.push(toToolExecution(part.toolName, toolCalls.get(part.toolCallId) || {}, part.output));
207
210
  }
208
211
  }
@@ -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;
@@ -158,7 +158,7 @@ export class Fisherman {
158
158
 
159
159
  AVAILABLE TOOLS:
160
160
  ${toolNames.join(', ')}.
161
- Use tool names exactly as listed. Do not invent aliases, combined names, or names with channel markers such as "commentary".
161
+ Use tool names exactly as listed. Do not invent aliases or combined names.
162
162
  Match each tool input schema exactly. Do not invent parameter names or pass extra fields.
163
163
 
164
164
  WORKFLOW:
@@ -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}
@@ -39,12 +39,13 @@ declare class Navigator implements Agent {
39
39
  resolveState(message: string, actionResult: ActionResult, opts?: {
40
40
  action?: Action;
41
41
  expectedUrl?: string;
42
+ experience?: string;
42
43
  onAttempt?: (attempt: {
43
44
  code: string;
44
45
  error?: string;
45
46
  }) => void;
46
47
  }): Promise<boolean>;
47
- buildResolutionPrompt(message: string, actionResult: ActionResult): Promise<string>;
48
+ buildResolutionPrompt(message: string, actionResult: ActionResult, injectedExperience?: string): Promise<string>;
48
49
  buildRetryFeedback(failures: BatchFailure[], includeHtml: boolean, actionResult: ActionResult): Promise<string>;
49
50
  executeAttempt(action: Action, codeBlock: string, message: string): Promise<{
50
51
  ok: boolean;
@@ -200,7 +200,7 @@ class Navigator {
200
200
  const expectedUrl = opts?.expectedUrl;
201
201
  const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
202
202
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
203
- conversation.addUserText(await this.buildResolutionPrompt(message, actionResult));
203
+ conversation.addUserText(await this.buildResolutionPrompt(message, actionResult, opts?.experience));
204
204
  let stopReason = null;
205
205
  const tools = {
206
206
  stop: tool({
@@ -333,14 +333,10 @@ class Navigator {
333
333
  }
334
334
  return resolved;
335
335
  }
336
- async buildResolutionPrompt(message, actionResult) {
337
- let experience = '';
338
- if (!actionResult.isInsideIframe) {
339
- const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
340
- if (successful.length > 0) {
341
- tag('operation').log(`Found ${successful.length} experience ${pluralize(successful.length, 'file')} for: ${actionResult.url}`);
342
- experience = `<experience>\nPast successful recipes recorded from prior runs for this page. Prefer these solutions first if they match the goal.\n\n${successful.join('\n\n')}\n</experience>`;
343
- }
336
+ async buildResolutionPrompt(message, actionResult, injectedExperience) {
337
+ let experience = injectedExperience || '';
338
+ if (!experience && !actionResult.isInsideIframe) {
339
+ experience = this.experienceTracker.renderExperienceFor(actionResult);
344
340
  }
345
341
  return dedent `
346
342
  <message>