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,16 +1,18 @@
1
1
  import dedent from 'dedent';
2
2
  import { ActionResult } from '../action-result.js';
3
- import { ExperienceTracker } from '../experience-tracker.js';
3
+ import { ExperienceTracker, renderExperienceToc } from '../experience-tracker.js';
4
4
  import { KnowledgeTracker } from '../knowledge-tracker.js';
5
5
  import { normalizeUrl } from '../state-manager.js';
6
6
  import { extractCodeBlocks } from '../utils/code-extractor.js';
7
7
  import { HooksRunner } from "../utils/hooks-runner.js";
8
8
  import { createDebug, pluralize, tag } from '../utils/logger.js';
9
9
  import { loop, pause } from '../utils/loop.js';
10
- import { Researcher } from "./researcher.js";
11
- import { actionRule, locatorRule } from './rules.js';
12
10
  import { RulesLoader } from "../utils/rules-loader.js";
11
+ import { extractStatePath } from '../utils/url-matcher.js';
12
+ import { Researcher } from "./researcher.js";
13
+ import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
13
14
  import { isInteractive } from './task-agent.js';
15
+ import { createAgentTools } from "./tools.js";
14
16
  const debugLog = createDebug('explorbot:navigator');
15
17
  class Navigator {
16
18
  emoji = '🧭';
@@ -18,8 +20,6 @@ class Navigator {
18
20
  experienceCompactor;
19
21
  knowledgeTracker;
20
22
  experienceTracker;
21
- currentAction = null;
22
- currentUrl = null;
23
23
  hooksRunner;
24
24
  MAX_ATTEMPTS = Number.parseInt(process.env.MAX_ATTEMPTS || '5');
25
25
  systemPrompt = dedent `
@@ -85,9 +85,7 @@ class Navigator {
85
85
  const actualPath = action.stateManager.getCurrentState()?.url || '';
86
86
  const actionResult = action.actionResult || ActionResult.fromState(action.stateManager.getCurrentState());
87
87
  const originalMessage = `Navigate to: ${url}. Current page: ${actualPath}`;
88
- this.currentAction = action;
89
- this.currentUrl = url;
90
- const resolved = await this.resolveState(originalMessage, actionResult);
88
+ const resolved = await this.resolveState(originalMessage, actionResult, { action, expectedUrl: url });
91
89
  if (!resolved) {
92
90
  throw new Error(`Navigation to ${url} failed: redirected to ${actualPath} and could not resolve`);
93
91
  }
@@ -99,9 +97,7 @@ class Navigator {
99
97
  And I expected to see the URL in the browser
100
98
  But I got error: ${action.lastError?.message || 'Navigation failed'}.
101
99
  `.trim();
102
- this.currentAction = action;
103
- this.currentUrl = url;
104
- const resolved = await this.resolveState(originalMessage, actionResult);
100
+ const resolved = await this.resolveState(originalMessage, actionResult, { action, expectedUrl: url });
105
101
  if (!resolved) {
106
102
  throw new Error(`Navigation to ${url} failed: ${action.lastError?.message}`);
107
103
  }
@@ -119,9 +115,11 @@ class Navigator {
119
115
  throw error;
120
116
  }
121
117
  }
122
- async resolveState(message, actionResult) {
118
+ async resolveState(message, actionResult, opts) {
123
119
  tag('info').log('AI Navigator resolving state at', actionResult.url);
124
120
  debugLog('Resolution message:', message);
121
+ const action = opts?.action ?? this.explorer.createAction();
122
+ const expectedUrl = opts?.expectedUrl;
125
123
  let knowledge = '';
126
124
  let experience = '';
127
125
  const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
@@ -133,24 +131,12 @@ class Navigator {
133
131
  ${knowledgeContent}
134
132
  </hint>`;
135
133
  }
136
- const relevantExperience = this.experienceTracker.getRelevantExperience(actionResult).map((experience) => experience.content);
137
- if (relevantExperience.length > 0) {
138
- const experienceContent = relevantExperience.join('\n\n---\n\n');
139
- experience = await this.experienceCompactor.compactExperience(experienceContent);
140
- tag('substep').log(`Found ${relevantExperience.length} experience ${pluralize(relevantExperience.length, 'file')} for: ${actionResult.url}`);
141
- experience = dedent `
142
- <experience>
143
- Here is the experience of interacting with the page.
144
- Learn from it AND DO NOT REPEAT THE SAME MISTAKES.
145
- If there was found successful solution to an issue, propose it as a first solution.
146
- If there are no successful solutions, analyze failed intentions and actions to avoid them.
147
- Do not try again same failed solutions
148
-
149
- Focus on successful solutions and avoid actions and locators that caused errors in past.
150
-
151
- ${experienceContent}
152
-
153
- </experience>`;
134
+ if (!actionResult.isInsideIframe) {
135
+ const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
136
+ if (successful.length > 0) {
137
+ tag('substep').log(`Found ${successful.length} experience ${pluralize(successful.length, 'file')} for: ${actionResult.url}`);
138
+ 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>`;
139
+ }
154
140
  }
155
141
  const prompt = dedent `
156
142
  <message>
@@ -177,6 +163,8 @@ class Navigator {
177
163
 
178
164
  ${actionRule}
179
165
 
166
+ ${unexpectedPopupRule}
167
+
180
168
  ${RulesLoader.loadRules('navigator', ['multiple-locator', 'output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.MAX_ATTEMPTS))}
181
169
 
182
170
  ${experience}
@@ -185,19 +173,22 @@ class Navigator {
185
173
  `;
186
174
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
187
175
  conversation.addUserText(prompt);
176
+ const tools = undefined;
188
177
  let codeBlocks = [];
189
178
  let htmlContextAdded = false;
190
179
  let codeBlockIndex = 0;
191
180
  let totalAttempts = 0;
181
+ const progressBlocks = [];
182
+ const batchFailures = [];
192
183
  let resolved = false;
193
184
  await loop(async ({ stop }) => {
194
185
  if (codeBlocks.length === 0) {
195
- const result = await this.provider.invokeConversation(conversation);
186
+ const result = await this.provider.invokeConversation(conversation, tools);
196
187
  if (!result)
197
188
  return;
198
189
  const aiResponse = result?.response?.text;
199
190
  debugLog('AI:', aiResponse?.split('\n')[0]);
200
- debugLog('Received AI response:', aiResponse.length, 'characters');
191
+ debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
201
192
  codeBlocks = extractCodeBlocks(aiResponse ?? '');
202
193
  codeBlockIndex = 0;
203
194
  }
@@ -207,42 +198,90 @@ class Navigator {
207
198
  }
208
199
  const codeBlock = codeBlocks[codeBlockIndex];
209
200
  if (!codeBlock) {
201
+ if (batchFailures.length === 0 && htmlContextAdded) {
202
+ stop();
203
+ return;
204
+ }
205
+ tag('substep').log('Feeding failures back to AI for a new batch...');
206
+ let contextMsg = 'Previous solutions did not work. Analyze the failures and try DIFFERENT strategies (not syntactic variants of the same locator).\n\n';
207
+ if (batchFailures.length > 0) {
208
+ const lines = batchFailures.map((f) => `- \`${f.code.split('\n')[0]}\` → ${f.error}`).join('\n');
209
+ contextMsg += `<previous_failures>\n${lines}\n</previous_failures>\n\n`;
210
+ }
210
211
  if (!htmlContextAdded) {
211
212
  htmlContextAdded = true;
212
- tag('substep').log('Adding HTML context for better resolution...');
213
- conversation.addUserText(dedent `
214
- Previous solutions did not work. Here is the full HTML context:
215
-
216
- <page_html>
217
- ${await actionResult.combinedHtml()}
218
- </page_html>
219
-
220
- Please suggest new solutions based on this additional context.
221
- `);
222
- codeBlocks = [];
223
- return;
213
+ contextMsg += `Full HTML context:\n\n<page_html>\n${await actionResult.combinedHtml()}\n</page_html>\n\n`;
224
214
  }
225
- stop();
215
+ 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.';
216
+ conversation.addUserText(contextMsg);
217
+ codeBlocks = [];
218
+ batchFailures.length = 0;
226
219
  return;
227
220
  }
228
221
  codeBlockIndex++;
229
222
  totalAttempts++;
223
+ await this.explorer.switchToMainFrame();
224
+ const prevHash = action.actionResult?.getStateHash() ?? actionResult.getStateHash();
230
225
  debugLog(`Attempting resolution: ${codeBlock}`);
231
- resolved = await this.currentAction.attempt(codeBlock, message);
232
- if (this.currentUrl) {
233
- await this.currentAction.getActor().wait(2);
234
- const freshState = await this.currentAction.capturePageState();
235
- if (normalizeUrl(freshState.url || '') === normalizeUrl(this.currentUrl)) {
236
- resolved = true;
226
+ const attemptOk = await action.attempt(codeBlock, message);
227
+ const page = action.playwrightHelper?.page;
228
+ if (page) {
229
+ try {
230
+ await page.waitForLoadState('load', { timeout: 5000 });
237
231
  }
238
- else if (resolved) {
239
- tag('warning').log(`URL verification failed: expected ${this.currentUrl}, got ${freshState.url}`);
240
- resolved = false;
232
+ catch {
233
+ // Navigation did not reach 'load' state within timeout; continue and verify URL
241
234
  }
242
235
  }
236
+ if (!attemptOk) {
237
+ const raw = action.lastError?.message || 'attempt failed';
238
+ const firstMeaningful = raw.split('\n').find((l) => l.trim() && !l.trim().startsWith('at ')) || raw;
239
+ const shortErr = firstMeaningful.replace(/\s+/g, ' ').trim().slice(0, 220);
240
+ batchFailures.push({ code: codeBlock, error: shortErr });
241
+ }
242
+ if (expectedUrl) {
243
+ if (page) {
244
+ try {
245
+ await page.waitForURL((url) => normalizeUrl(url.pathname) === normalizeUrl(expectedUrl), { timeout: 5000 });
246
+ }
247
+ catch {
248
+ // URL did not transition to expectedUrl within timeout
249
+ }
250
+ }
251
+ const freshState = await action.capturePageState();
252
+ const urlMatches = normalizeUrl(freshState.url || '') === normalizeUrl(expectedUrl);
253
+ const stateChanged = freshState.getStateHash() !== actionResult.getStateHash();
254
+ resolved = urlMatches && stateChanged;
255
+ if (!resolved && attemptOk) {
256
+ tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${freshState.url}`);
257
+ }
258
+ if (freshState.getStateHash() !== prevHash && (attemptOk || urlMatches)) {
259
+ progressBlocks.push(codeBlock);
260
+ }
261
+ }
262
+ else {
263
+ resolved = attemptOk;
264
+ if (attemptOk)
265
+ progressBlocks.push(codeBlock);
266
+ }
243
267
  if (resolved) {
244
268
  tag('success').log('Navigation resolved successfully');
245
- await this.experienceTracker.saveSuccessfulResolution(actionResult, message, codeBlock);
269
+ let scenario = message.split('\n')[0];
270
+ if (expectedUrl) {
271
+ const fromPath = extractStatePath(actionResult.url || '');
272
+ const toPath = extractStatePath(expectedUrl);
273
+ scenario = `reach ${toPath} from ${fromPath}`;
274
+ }
275
+ const recipe = progressBlocks
276
+ .join('\n')
277
+ .split('\n')
278
+ .filter((line) => !/^\s*I\.amOnPage\s*\(/.test(line))
279
+ .join('\n')
280
+ .trim();
281
+ if (recipe) {
282
+ const body = `## FLOW: ${scenario}\n\n* ${scenario}\n\n\`\`\`js\n${recipe}\n\`\`\`\n\n---\n`;
283
+ this.experienceTracker.writeFlow(actionResult, body);
284
+ }
246
285
  stop();
247
286
  return;
248
287
  }
@@ -256,9 +295,9 @@ class Navigator {
256
295
  resolved = false;
257
296
  },
258
297
  });
259
- if (!resolved && this.currentUrl) {
260
- await this.currentAction.getActor().wait(1);
261
- if (this.isOnExpectedPage(this.currentUrl, this.currentAction.stateManager)) {
298
+ if (!resolved && expectedUrl) {
299
+ await action.getActor().wait(1);
300
+ if (this.isOnExpectedPage(expectedUrl, action.stateManager)) {
262
301
  resolved = true;
263
302
  tag('success').log('Navigation resolved after delayed redirect');
264
303
  }
@@ -267,12 +306,12 @@ class Navigator {
267
306
  tag('error').log(`Navigation failed after ${totalAttempts} attempts`);
268
307
  }
269
308
  if (!resolved && isInteractive()) {
270
- 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):`);
309
+ 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):`);
271
310
  if (userInput?.trim()) {
272
- resolved = await this.currentAction.attempt(userInput, message);
273
- if (resolved && this.currentUrl) {
274
- await this.currentAction.getActor().wait(1);
275
- if (!this.isOnExpectedPage(this.currentUrl, this.currentAction.stateManager)) {
311
+ resolved = await action.attempt(userInput, message);
312
+ if (resolved && expectedUrl) {
313
+ await action.getActor().wait(1);
314
+ if (!this.isOnExpectedPage(expectedUrl, action.stateManager)) {
276
315
  resolved = false;
277
316
  }
278
317
  }
@@ -280,6 +319,23 @@ class Navigator {
280
319
  }
281
320
  return resolved;
282
321
  }
322
+ buildExperienceTools() {
323
+ const stateManager = this.explorer.getStateManager();
324
+ const getState = () => {
325
+ const s = stateManager.getCurrentState();
326
+ return s ? ActionResult.fromState(s) : null;
327
+ };
328
+ const { learn_experience } = createAgentTools({
329
+ explorer: this.explorer,
330
+ researcher: null,
331
+ navigator: this,
332
+ experienceTracker: this.experienceTracker,
333
+ getState,
334
+ });
335
+ if (!learn_experience)
336
+ return undefined;
337
+ return { learn_experience };
338
+ }
283
339
  async freeSail(opts, actionResult) {
284
340
  const stateManager = this.explorer.getStateManager();
285
341
  const state = stateManager.getCurrentState();
@@ -403,20 +459,13 @@ class Navigator {
403
459
  ${knowledgeContent}
404
460
  </hint>`;
405
461
  }
406
- const relevantExperience = this.experienceTracker.getRelevantExperience(actionResult).map((exp) => exp.content);
407
- if (relevantExperience.length > 0) {
408
- const experienceContent = relevantExperience.join('\n\n---\n\n');
409
- experience = await this.experienceCompactor.compactExperience(experienceContent);
410
- tag('substep').log(`Found ${relevantExperience.length} experience ${pluralize(relevantExperience.length, 'file')} for: ${actionResult.url}`);
411
- experience = dedent `
412
- <experience>
413
- Here is the experience of interacting with the page.
414
- Learn from it AND DO NOT REPEAT THE SAME MISTAKES.
415
- If there was found successful solution to an issue, propose it as a first solution.
416
-
417
- ${experienceContent}
418
-
419
- </experience>`;
462
+ if (!actionResult.isInsideIframe) {
463
+ const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
464
+ if (toc.length > 0) {
465
+ const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
466
+ tag('substep').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
467
+ experience = renderExperienceToc(toc);
468
+ }
420
469
  }
421
470
  const prompt = dedent `
422
471
  <message>
@@ -454,16 +503,18 @@ class Navigator {
454
503
  tag('debug').log('Prompt:', prompt);
455
504
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
456
505
  conversation.addUserText(prompt);
506
+ const tools = this.buildExperienceTools();
457
507
  let codeBlocks = [];
458
508
  const successfulCodes = [];
509
+ const assertionSteps = [];
459
510
  const action = this.explorer.createAction();
460
511
  await loop(async ({ stop, iteration }) => {
461
512
  if (codeBlocks.length === 0) {
462
- const result = await this.provider.invokeConversation(conversation);
513
+ const result = await this.provider.invokeConversation(conversation, tools);
463
514
  if (!result)
464
515
  return;
465
516
  const aiResponse = result?.response?.text;
466
- debugLog('Received AI response:', aiResponse.length, 'characters');
517
+ debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
467
518
  tag('step').log('Verifying assertion...');
468
519
  codeBlocks = extractCodeBlocks(aiResponse ?? '');
469
520
  }
@@ -475,10 +526,12 @@ class Navigator {
475
526
  stop();
476
527
  return;
477
528
  }
529
+ await this.explorer.switchToMainFrame();
478
530
  const verified = await action.attempt(codeBlock, message, false);
479
531
  if (verified) {
480
532
  tag('success').log('Verification passed');
481
533
  successfulCodes.push(codeBlock);
534
+ assertionSteps.push(...action.assertionSteps);
482
535
  }
483
536
  }, {
484
537
  maxAttempts: this.MAX_ATTEMPTS,
@@ -493,7 +546,7 @@ class Navigator {
493
546
  const verified = totalAttempted <= 1 ? successfulCodes.length > 0 : successfulCodes.length > totalAttempted / 2;
494
547
  actionResult.addVerification(message, verified);
495
548
  this.explorer.getStateManager().updateState(actionResult);
496
- return { verified, successfulCodes, totalAttempted };
549
+ return { verified, successfulCodes, assertionSteps, totalAttempted };
497
550
  }
498
551
  }
499
552
  export { Navigator };