explorbot 0.1.9 → 0.1.11

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 (157) hide show
  1. package/README.md +27 -1
  2. package/bin/explorbot-cli.ts +86 -15
  3. package/boat/api-tester/src/ai/curler-tools.ts +3 -3
  4. package/boat/api-tester/src/ai/curler.ts +1 -1
  5. package/boat/api-tester/src/apibot.ts +2 -2
  6. package/boat/api-tester/src/config.ts +1 -1
  7. package/dist/bin/explorbot-cli.js +85 -14
  8. package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
  9. package/dist/boat/api-tester/src/apibot.js +2 -2
  10. package/dist/package.json +2 -2
  11. package/dist/rules/navigator/output.md +9 -0
  12. package/dist/rules/navigator/verification-actions.md +2 -0
  13. package/dist/src/action-result.js +23 -1
  14. package/dist/src/action.js +46 -38
  15. package/dist/src/ai/bosun.js +16 -2
  16. package/dist/src/ai/conversation.js +39 -0
  17. package/dist/src/ai/experience-compactor.js +235 -50
  18. package/dist/src/ai/historian/codeceptjs.js +109 -0
  19. package/dist/src/ai/historian/experience.js +320 -0
  20. package/dist/src/ai/historian/mixin.js +2 -0
  21. package/dist/src/ai/historian/playwright.js +145 -0
  22. package/dist/src/ai/historian/utils.js +18 -0
  23. package/dist/src/ai/historian.js +19 -398
  24. package/dist/src/ai/navigator.js +133 -80
  25. package/dist/src/ai/pilot.js +254 -13
  26. package/dist/src/ai/planner/subpages.js +1 -30
  27. package/dist/src/ai/planner.js +33 -13
  28. package/dist/src/ai/provider.js +55 -18
  29. package/dist/src/ai/rerunner.js +3 -3
  30. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  31. package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
  32. package/dist/src/ai/researcher/locators.js +1 -1
  33. package/dist/src/ai/researcher/sections.js +8 -1
  34. package/dist/src/ai/researcher.js +43 -41
  35. package/dist/src/ai/rules.js +26 -14
  36. package/dist/src/ai/tester.js +90 -26
  37. package/dist/src/ai/tools.js +18 -10
  38. package/dist/src/api/request-store.js +20 -0
  39. package/dist/src/api/xhr-capture.js +19 -3
  40. package/dist/src/browser-server.js +16 -3
  41. package/dist/src/command-handler.js +1 -1
  42. package/dist/src/commands/add-rule-command.js +12 -9
  43. package/dist/src/commands/base-command.js +20 -0
  44. package/dist/src/commands/clean-command.js +3 -2
  45. package/dist/src/commands/compact-command.js +138 -0
  46. package/dist/src/commands/context-command.js +7 -1
  47. package/dist/src/commands/drill-command.js +4 -1
  48. package/dist/src/commands/experience-command.js +104 -0
  49. package/dist/src/commands/explore-command.js +54 -19
  50. package/dist/src/commands/freesail-command.js +2 -0
  51. package/dist/src/commands/index.js +7 -3
  52. package/dist/src/commands/init-command.js +11 -10
  53. package/dist/src/commands/learn-command.js +1 -1
  54. package/dist/src/commands/navigate-command.js +4 -1
  55. package/dist/src/commands/plan-clear-command.js +4 -1
  56. package/dist/src/commands/plan-command.js +43 -4
  57. package/dist/src/commands/plan-edit-command.js +1 -1
  58. package/dist/src/commands/plan-load-command.js +4 -1
  59. package/dist/src/commands/plan-reload-command.js +4 -1
  60. package/dist/src/commands/plan-save-command.js +20 -8
  61. package/dist/src/commands/rerun-command.js +4 -0
  62. package/dist/src/commands/research-command.js +5 -2
  63. package/dist/src/commands/start-command.js +5 -1
  64. package/dist/src/commands/test-command.js +7 -1
  65. package/dist/src/components/App.js +15 -5
  66. package/dist/src/execution-controller.js +13 -2
  67. package/dist/src/experience-tracker.js +174 -83
  68. package/dist/src/explorbot.js +31 -22
  69. package/dist/src/explorer.js +12 -5
  70. package/dist/src/observability.js +50 -99
  71. package/dist/src/playwright-recorder.js +309 -0
  72. package/dist/src/reporter.js +17 -2
  73. package/dist/src/stats.js +2 -0
  74. package/dist/src/suite.js +1 -1
  75. package/dist/src/test-plan.js +12 -0
  76. package/dist/src/utils/aria.js +37 -1
  77. package/dist/src/utils/error-page.js +30 -7
  78. package/dist/src/utils/logger.js +1 -1
  79. package/dist/src/utils/next-steps.js +37 -0
  80. package/dist/src/utils/rules-loader.js +1 -1
  81. package/dist/src/utils/test-files.js +1 -1
  82. package/dist/src/utils/url-matcher.js +50 -0
  83. package/package.json +2 -2
  84. package/rules/navigator/output.md +9 -0
  85. package/rules/navigator/verification-actions.md +2 -0
  86. package/src/action-result.ts +26 -1
  87. package/src/action.ts +44 -37
  88. package/src/ai/bosun.ts +16 -2
  89. package/src/ai/conversation.ts +37 -0
  90. package/src/ai/experience-compactor.ts +270 -63
  91. package/src/ai/historian/codeceptjs.ts +130 -0
  92. package/src/ai/historian/experience.ts +383 -0
  93. package/src/ai/historian/mixin.ts +4 -0
  94. package/src/ai/historian/playwright.ts +169 -0
  95. package/src/ai/historian/utils.ts +23 -0
  96. package/src/ai/historian.ts +35 -468
  97. package/src/ai/navigator.ts +140 -85
  98. package/src/ai/pilot.ts +259 -14
  99. package/src/ai/planner/subpages.ts +1 -24
  100. package/src/ai/planner.ts +34 -14
  101. package/src/ai/provider.ts +52 -18
  102. package/src/ai/rerunner.ts +3 -3
  103. package/src/ai/researcher/deep-analysis.ts +1 -1
  104. package/src/ai/researcher/fingerprint-worker.ts +1 -1
  105. package/src/ai/researcher/locators.ts +2 -2
  106. package/src/ai/researcher/sections.ts +7 -1
  107. package/src/ai/researcher.ts +47 -42
  108. package/src/ai/rules.ts +27 -14
  109. package/src/ai/task-agent.ts +1 -1
  110. package/src/ai/tester.ts +94 -26
  111. package/src/ai/tools.ts +53 -29
  112. package/src/api/request-store.ts +22 -0
  113. package/src/api/xhr-capture.ts +21 -3
  114. package/src/browser-server.ts +17 -3
  115. package/src/command-handler.ts +1 -1
  116. package/src/commands/add-rule-command.ts +13 -9
  117. package/src/commands/base-command.ts +26 -1
  118. package/src/commands/clean-command.ts +4 -3
  119. package/src/commands/compact-command.ts +156 -0
  120. package/src/commands/context-command.ts +8 -2
  121. package/src/commands/drill-command.ts +5 -2
  122. package/src/commands/experience-command.ts +125 -0
  123. package/src/commands/explore-command.ts +58 -21
  124. package/src/commands/freesail-command.ts +2 -0
  125. package/src/commands/index.ts +7 -3
  126. package/src/commands/init-command.ts +11 -10
  127. package/src/commands/learn-command.ts +2 -2
  128. package/src/commands/navigate-command.ts +5 -2
  129. package/src/commands/plan-clear-command.ts +5 -2
  130. package/src/commands/plan-command.ts +47 -5
  131. package/src/commands/plan-edit-command.ts +2 -2
  132. package/src/commands/plan-load-command.ts +5 -2
  133. package/src/commands/plan-reload-command.ts +5 -2
  134. package/src/commands/plan-save-command.ts +20 -9
  135. package/src/commands/rerun-command.ts +5 -0
  136. package/src/commands/research-command.ts +6 -3
  137. package/src/commands/start-command.ts +6 -2
  138. package/src/commands/test-command.ts +8 -2
  139. package/src/components/App.tsx +16 -5
  140. package/src/config.ts +6 -1
  141. package/src/execution-controller.ts +14 -3
  142. package/src/experience-tracker.ts +198 -100
  143. package/src/explorbot.ts +33 -23
  144. package/src/explorer.ts +14 -5
  145. package/src/observability.ts +50 -109
  146. package/src/playwright-recorder.ts +305 -0
  147. package/src/reporter.ts +17 -3
  148. package/src/stats.ts +4 -0
  149. package/src/suite.ts +1 -1
  150. package/src/test-plan.ts +12 -0
  151. package/src/utils/aria.ts +38 -1
  152. package/src/utils/error-page.ts +32 -7
  153. package/src/utils/logger.ts +1 -1
  154. package/src/utils/next-steps.ts +51 -0
  155. package/src/utils/rules-loader.ts +1 -1
  156. package/src/utils/test-files.ts +1 -1
  157. package/src/utils/url-matcher.ts +43 -0
@@ -1,6 +1,7 @@
1
1
  import dedent from 'dedent';
2
2
  import { ActionResult } from '../action-result.js';
3
- import { ExperienceTracker } from '../experience-tracker.js';
3
+ import type Action from '../action.ts';
4
+ import { ExperienceTracker, renderExperienceToc } from '../experience-tracker.js';
4
5
  import Explorer from '../explorer.ts';
5
6
  import { KnowledgeTracker } from '../knowledge-tracker.js';
6
7
  import { type WebPageState, normalizeUrl } from '../state-manager.js';
@@ -8,14 +9,16 @@ import { extractCodeBlocks } from '../utils/code-extractor.js';
8
9
  import { HooksRunner } from '../utils/hooks-runner.ts';
9
10
  import { createDebug, pluralize, tag } from '../utils/logger.js';
10
11
  import { loop, pause } from '../utils/loop.js';
12
+ import { RulesLoader } from '../utils/rules-loader.ts';
13
+ import { extractStatePath } from '../utils/url-matcher.js';
11
14
  import type { Agent } from './agent.js';
12
15
  import type { Conversation } from './conversation.js';
13
16
  import { ExperienceCompactor } from './experience-compactor.js';
14
17
  import type { Provider } from './provider.js';
15
18
  import { Researcher } from './researcher.ts';
16
- import { actionRule, locatorRule } from './rules.js';
17
- import { RulesLoader } from '../utils/rules-loader.ts';
19
+ import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
18
20
  import { isInteractive } from './task-agent.js';
21
+ import { createAgentTools } from './tools.ts';
19
22
 
20
23
  const debugLog = createDebug('explorbot:navigator');
21
24
 
@@ -25,8 +28,6 @@ class Navigator implements Agent {
25
28
  private experienceCompactor: ExperienceCompactor;
26
29
  private knowledgeTracker: KnowledgeTracker;
27
30
  private experienceTracker: ExperienceTracker;
28
- private currentAction: any = null;
29
- private currentUrl: string | null = null;
30
31
  private hooksRunner: HooksRunner;
31
32
 
32
33
  private MAX_ATTEMPTS = Number.parseInt(process.env.MAX_ATTEMPTS || '5');
@@ -102,9 +103,7 @@ class Navigator implements Agent {
102
103
  const actionResult = action.actionResult || ActionResult.fromState(action.stateManager.getCurrentState()!);
103
104
  const originalMessage = `Navigate to: ${url}. Current page: ${actualPath}`;
104
105
 
105
- this.currentAction = action;
106
- this.currentUrl = url;
107
- const resolved = await this.resolveState(originalMessage, actionResult);
106
+ const resolved = await this.resolveState(originalMessage, actionResult, { action, expectedUrl: url });
108
107
  if (!resolved) {
109
108
  throw new Error(`Navigation to ${url} failed: redirected to ${actualPath} and could not resolve`);
110
109
  }
@@ -116,9 +115,7 @@ class Navigator implements Agent {
116
115
  But I got error: ${action.lastError?.message || 'Navigation failed'}.
117
116
  `.trim();
118
117
 
119
- this.currentAction = action;
120
- this.currentUrl = url;
121
- const resolved = await this.resolveState(originalMessage, actionResult);
118
+ const resolved = await this.resolveState(originalMessage, actionResult, { action, expectedUrl: url });
122
119
  if (!resolved) {
123
120
  throw new Error(`Navigation to ${url} failed: ${action.lastError?.message}`);
124
121
  }
@@ -136,10 +133,13 @@ class Navigator implements Agent {
136
133
  }
137
134
  }
138
135
 
139
- async resolveState(message: string, actionResult: ActionResult): Promise<boolean> {
136
+ async resolveState(message: string, actionResult: ActionResult, opts?: { action?: Action; expectedUrl?: string }): Promise<boolean> {
140
137
  tag('info').log('AI Navigator resolving state at', actionResult.url);
141
138
  debugLog('Resolution message:', message);
142
139
 
140
+ const action = opts?.action ?? this.explorer.createAction();
141
+ const expectedUrl = opts?.expectedUrl;
142
+
143
143
  let knowledge = '';
144
144
  let experience = '';
145
145
 
@@ -153,26 +153,12 @@ class Navigator implements Agent {
153
153
  </hint>`;
154
154
  }
155
155
 
156
- const relevantExperience = this.experienceTracker.getRelevantExperience(actionResult).map((experience) => experience.content);
157
-
158
- if (relevantExperience.length > 0) {
159
- const experienceContent = relevantExperience.join('\n\n---\n\n');
160
- experience = await this.experienceCompactor.compactExperience(experienceContent);
161
- tag('substep').log(`Found ${relevantExperience.length} experience ${pluralize(relevantExperience.length, 'file')} for: ${actionResult.url}`);
162
-
163
- experience = dedent`
164
- <experience>
165
- Here is the experience of interacting with the page.
166
- Learn from it AND DO NOT REPEAT THE SAME MISTAKES.
167
- If there was found successful solution to an issue, propose it as a first solution.
168
- If there are no successful solutions, analyze failed intentions and actions to avoid them.
169
- Do not try again same failed solutions
170
-
171
- Focus on successful solutions and avoid actions and locators that caused errors in past.
172
-
173
- ${experienceContent}
174
-
175
- </experience>`;
156
+ if (!actionResult.isInsideIframe) {
157
+ const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
158
+ if (successful.length > 0) {
159
+ tag('substep').log(`Found ${successful.length} experience ${pluralize(successful.length, 'file')} for: ${actionResult.url}`);
160
+ 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>`;
161
+ }
176
162
  }
177
163
 
178
164
  const prompt = dedent`
@@ -200,6 +186,8 @@ class Navigator implements Agent {
200
186
 
201
187
  ${actionRule}
202
188
 
189
+ ${unexpectedPopupRule}
190
+
203
191
  ${RulesLoader.loadRules('navigator', ['multiple-locator', 'output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.MAX_ATTEMPTS))}
204
192
 
205
193
  ${experience}
@@ -210,20 +198,24 @@ class Navigator implements Agent {
210
198
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
211
199
  conversation.addUserText(prompt);
212
200
 
201
+ const tools = undefined;
202
+
213
203
  let codeBlocks: string[] = [];
214
204
  let htmlContextAdded = false;
215
205
  let codeBlockIndex = 0;
216
206
  let totalAttempts = 0;
207
+ const progressBlocks: string[] = [];
208
+ const batchFailures: Array<{ code: string; error: string }> = [];
217
209
 
218
210
  let resolved = false;
219
211
  await loop(
220
212
  async ({ stop }) => {
221
213
  if (codeBlocks.length === 0) {
222
- const result = await this.provider.invokeConversation(conversation);
214
+ const result = await this.provider.invokeConversation(conversation, tools);
223
215
  if (!result) return;
224
216
  const aiResponse = result?.response?.text;
225
217
  debugLog('AI:', aiResponse?.split('\n')[0]);
226
- debugLog('Received AI response:', aiResponse.length, 'characters');
218
+ debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
227
219
  codeBlocks = extractCodeBlocks(aiResponse ?? '');
228
220
  codeBlockIndex = 0;
229
221
  }
@@ -235,45 +227,94 @@ class Navigator implements Agent {
235
227
 
236
228
  const codeBlock = codeBlocks[codeBlockIndex];
237
229
  if (!codeBlock) {
230
+ if (batchFailures.length === 0 && htmlContextAdded) {
231
+ stop();
232
+ return;
233
+ }
234
+ tag('substep').log('Feeding failures back to AI for a new batch...');
235
+ let contextMsg = 'Previous solutions did not work. Analyze the failures and try DIFFERENT strategies (not syntactic variants of the same locator).\n\n';
236
+ if (batchFailures.length > 0) {
237
+ const lines = batchFailures.map((f) => `- \`${f.code.split('\n')[0]}\` → ${f.error}`).join('\n');
238
+ contextMsg += `<previous_failures>\n${lines}\n</previous_failures>\n\n`;
239
+ }
238
240
  if (!htmlContextAdded) {
239
241
  htmlContextAdded = true;
240
- tag('substep').log('Adding HTML context for better resolution...');
241
- conversation.addUserText(dedent`
242
- Previous solutions did not work. Here is the full HTML context:
243
-
244
- <page_html>
245
- ${await actionResult.combinedHtml()}
246
- </page_html>
247
-
248
- Please suggest new solutions based on this additional context.
249
- `);
250
- codeBlocks = [];
251
- return;
242
+ contextMsg += `Full HTML context:\n\n<page_html>\n${await actionResult.combinedHtml()}\n</page_html>\n\n`;
252
243
  }
253
- stop();
244
+ contextMsg += 'Propose new solutions. If errors mention "intercepts pointer events" or timeouts on visible elements, an overlay is blocking — dismiss it first (Escape, click outside, Close button) before retrying the original action.';
245
+ conversation.addUserText(contextMsg);
246
+ codeBlocks = [];
247
+ batchFailures.length = 0;
254
248
  return;
255
249
  }
256
250
  codeBlockIndex++;
257
251
  totalAttempts++;
258
252
 
253
+ await this.explorer.switchToMainFrame();
254
+
255
+ const prevHash = action.actionResult?.getStateHash() ?? actionResult.getStateHash();
256
+
259
257
  debugLog(`Attempting resolution: ${codeBlock}`);
260
- resolved = await this.currentAction.attempt(codeBlock, message);
258
+ const attemptOk = await action.attempt(codeBlock, message);
259
+
260
+ const page = action.playwrightHelper?.page;
261
+ if (page) {
262
+ try {
263
+ await page.waitForLoadState('load', { timeout: 5000 });
264
+ } catch {
265
+ // Navigation did not reach 'load' state within timeout; continue and verify URL
266
+ }
267
+ }
261
268
 
262
- if (this.currentUrl) {
263
- await this.currentAction.getActor().wait(2);
264
- const freshState = await this.currentAction.capturePageState();
269
+ if (!attemptOk) {
270
+ const raw = action.lastError?.message || 'attempt failed';
271
+ const firstMeaningful = raw.split('\n').find((l) => l.trim() && !l.trim().startsWith('at ')) || raw;
272
+ const shortErr = firstMeaningful.replace(/\s+/g, ' ').trim().slice(0, 220);
273
+ batchFailures.push({ code: codeBlock, error: shortErr });
274
+ }
265
275
 
266
- if (normalizeUrl(freshState.url || '') === normalizeUrl(this.currentUrl)) {
267
- resolved = true;
268
- } else if (resolved) {
269
- tag('warning').log(`URL verification failed: expected ${this.currentUrl}, got ${freshState.url}`);
270
- resolved = false;
276
+ if (expectedUrl) {
277
+ if (page) {
278
+ try {
279
+ await page.waitForURL((url: URL) => normalizeUrl(url.pathname) === normalizeUrl(expectedUrl), { timeout: 5000 });
280
+ } catch {
281
+ // URL did not transition to expectedUrl within timeout
282
+ }
283
+ }
284
+ const freshState = await action.capturePageState();
285
+ const urlMatches = normalizeUrl(freshState.url || '') === normalizeUrl(expectedUrl);
286
+ const stateChanged = freshState.getStateHash() !== actionResult.getStateHash();
287
+ resolved = urlMatches && stateChanged;
288
+
289
+ if (!resolved && attemptOk) {
290
+ tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${freshState.url}`);
291
+ }
292
+ if (freshState.getStateHash() !== prevHash && (attemptOk || urlMatches)) {
293
+ progressBlocks.push(codeBlock);
271
294
  }
295
+ } else {
296
+ resolved = attemptOk;
297
+ if (attemptOk) progressBlocks.push(codeBlock);
272
298
  }
273
299
 
274
300
  if (resolved) {
275
301
  tag('success').log('Navigation resolved successfully');
276
- await this.experienceTracker.saveSuccessfulResolution(actionResult, message, codeBlock);
302
+ let scenario = message.split('\n')[0];
303
+ if (expectedUrl) {
304
+ const fromPath = extractStatePath(actionResult.url || '');
305
+ const toPath = extractStatePath(expectedUrl);
306
+ scenario = `reach ${toPath} from ${fromPath}`;
307
+ }
308
+ const recipe = progressBlocks
309
+ .join('\n')
310
+ .split('\n')
311
+ .filter((line) => !/^\s*I\.amOnPage\s*\(/.test(line))
312
+ .join('\n')
313
+ .trim();
314
+ if (recipe) {
315
+ const body = `## FLOW: ${scenario}\n\n* ${scenario}\n\n\`\`\`js\n${recipe}\n\`\`\`\n\n---\n`;
316
+ this.experienceTracker.writeFlow(actionResult, body);
317
+ }
277
318
  stop();
278
319
  return;
279
320
  }
@@ -290,9 +331,9 @@ class Navigator implements Agent {
290
331
  }
291
332
  );
292
333
 
293
- if (!resolved && this.currentUrl) {
294
- await this.currentAction.getActor().wait(1);
295
- if (this.isOnExpectedPage(this.currentUrl, this.currentAction.stateManager)) {
334
+ if (!resolved && expectedUrl) {
335
+ await (action.getActor() as any).wait(1);
336
+ if (this.isOnExpectedPage(expectedUrl, action.stateManager)) {
296
337
  resolved = true;
297
338
  tag('success').log('Navigation resolved after delayed redirect');
298
339
  }
@@ -303,13 +344,13 @@ class Navigator implements Agent {
303
344
  }
304
345
 
305
346
  if (!resolved && isInteractive()) {
306
- const userInput = await pause(`Navigator failed to resolve. Current: ${this.currentAction.stateManager.getCurrentState()?.url}\n` + `Target: ${this.currentUrl}\nEnter CodeceptJS commands (or press Enter to skip):`);
347
+ const userInput = await pause(`Navigator failed to resolve. Current: ${action.stateManager.getCurrentState()?.url}\n` + `Target: ${expectedUrl ?? '(none)'}\nEnter CodeceptJS commands (or press Enter to skip):`);
307
348
 
308
349
  if (userInput?.trim()) {
309
- resolved = await this.currentAction.attempt(userInput, message);
310
- if (resolved && this.currentUrl) {
311
- await this.currentAction.getActor().wait(1);
312
- if (!this.isOnExpectedPage(this.currentUrl, this.currentAction.stateManager)) {
350
+ resolved = await action.attempt(userInput, message);
351
+ if (resolved && expectedUrl) {
352
+ await (action.getActor() as any).wait(1);
353
+ if (!this.isOnExpectedPage(expectedUrl, action.stateManager)) {
313
354
  resolved = false;
314
355
  }
315
356
  }
@@ -319,6 +360,23 @@ class Navigator implements Agent {
319
360
  return resolved;
320
361
  }
321
362
 
363
+ private buildExperienceTools(): { learn_experience: unknown } | undefined {
364
+ const stateManager = this.explorer.getStateManager();
365
+ const getState = () => {
366
+ const s = stateManager.getCurrentState();
367
+ return s ? ActionResult.fromState(s) : null;
368
+ };
369
+ const { learn_experience } = createAgentTools({
370
+ explorer: this.explorer,
371
+ researcher: null as unknown as Researcher,
372
+ navigator: this,
373
+ experienceTracker: this.experienceTracker,
374
+ getState,
375
+ });
376
+ if (!learn_experience) return undefined;
377
+ return { learn_experience };
378
+ }
379
+
322
380
  async freeSail(opts?: { strategy?: 'deep' | 'shallow'; scope?: string; visitedUrls?: Set<string> }, actionResult?: ActionResult): Promise<{ target: string; reason: string } | null> {
323
381
  const stateManager = this.explorer.getStateManager();
324
382
  const state = stateManager.getCurrentState();
@@ -448,7 +506,7 @@ class Navigator implements Agent {
448
506
  return suggestion;
449
507
  }
450
508
 
451
- async verifyState(message: string, actionResult: ActionResult): Promise<{ verified: boolean; successfulCodes: string[]; totalAttempted: number }> {
509
+ async verifyState(message: string, actionResult: ActionResult): Promise<{ verified: boolean; successfulCodes: string[]; assertionSteps: Array<{ name: string; args: any[] }>; totalAttempted: number }> {
452
510
  tag('info').log('AI Navigator verifying state at', actionResult.url);
453
511
  debugLog('Verification message:', message);
454
512
 
@@ -465,22 +523,13 @@ class Navigator implements Agent {
465
523
  </hint>`;
466
524
  }
467
525
 
468
- const relevantExperience = this.experienceTracker.getRelevantExperience(actionResult).map((exp) => exp.content);
469
-
470
- if (relevantExperience.length > 0) {
471
- const experienceContent = relevantExperience.join('\n\n---\n\n');
472
- experience = await this.experienceCompactor.compactExperience(experienceContent);
473
- tag('substep').log(`Found ${relevantExperience.length} experience ${pluralize(relevantExperience.length, 'file')} for: ${actionResult.url}`);
474
-
475
- experience = dedent`
476
- <experience>
477
- Here is the experience of interacting with the page.
478
- Learn from it AND DO NOT REPEAT THE SAME MISTAKES.
479
- If there was found successful solution to an issue, propose it as a first solution.
480
-
481
- ${experienceContent}
482
-
483
- </experience>`;
526
+ if (!actionResult.isInsideIframe) {
527
+ const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
528
+ if (toc.length > 0) {
529
+ const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
530
+ tag('substep').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
531
+ experience = renderExperienceToc(toc);
532
+ }
484
533
  }
485
534
 
486
535
  const prompt = dedent`
@@ -522,18 +571,21 @@ class Navigator implements Agent {
522
571
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
523
572
  conversation.addUserText(prompt);
524
573
 
574
+ const tools = this.buildExperienceTools();
575
+
525
576
  let codeBlocks: string[] = [];
526
577
  const successfulCodes: string[] = [];
578
+ const assertionSteps: Array<{ name: string; args: any[] }> = [];
527
579
 
528
580
  const action = this.explorer.createAction();
529
581
 
530
582
  await loop(
531
583
  async ({ stop, iteration }) => {
532
584
  if (codeBlocks.length === 0) {
533
- const result = await this.provider.invokeConversation(conversation);
585
+ const result = await this.provider.invokeConversation(conversation, tools);
534
586
  if (!result) return;
535
587
  const aiResponse = result?.response?.text;
536
- debugLog('Received AI response:', aiResponse.length, 'characters');
588
+ debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
537
589
  tag('step').log('Verifying assertion...');
538
590
  codeBlocks = extractCodeBlocks(aiResponse ?? '');
539
591
  }
@@ -548,11 +600,14 @@ class Navigator implements Agent {
548
600
  return;
549
601
  }
550
602
 
603
+ await this.explorer.switchToMainFrame();
604
+
551
605
  const verified = await action.attempt(codeBlock, message, false);
552
606
 
553
607
  if (verified) {
554
608
  tag('success').log('Verification passed');
555
609
  successfulCodes.push(codeBlock);
610
+ assertionSteps.push(...action.assertionSteps);
556
611
  }
557
612
  },
558
613
  {
@@ -572,7 +627,7 @@ class Navigator implements Agent {
572
627
  actionResult.addVerification(message, verified);
573
628
  this.explorer.getStateManager().updateState(actionResult);
574
629
 
575
- return { verified, successfulCodes, totalAttempted };
630
+ return { verified, successfulCodes, assertionSteps, totalAttempted };
576
631
  }
577
632
  }
578
633