browserclaw-agent 0.7.0 → 0.7.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.
@@ -26,6 +26,8 @@ export type TerminationJudgment = {
26
26
  export declare function shouldCheckTermination(step: number): boolean;
27
27
  export interface PageHolder {
28
28
  page: CrawlPage;
29
+ browser?: BrowserClaw;
30
+ ensureProxyForUrl?: (url: string) => Promise<void>;
29
31
  }
30
32
  /**
31
33
  * Extension points for consumers of the agent loop.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAmB1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAU9C,OAAO,KAAK,EAAE,WAAW,EAAa,eAAe,EAAiB,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAkB/C;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiC5F;AA0rBD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAsB1D;AAMD,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAIvE;AAiTD,MAAM,MAAM,mBAAmB,GAAG;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtG;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG5D;AA2CD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,aAAa,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;IAC5D,aAAa,CAAC,EAAE,MAAM,CACpB,MAAM,EACN,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAC3G,CAAC;IACF,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3D,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,aAAa,EAAE,MAAM,WAAW,EAAE,CAAC;IACnC,kIAAkI;IAClI,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,SAAS,GAAG,UAAU,EACpC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,EAC5C,MAAM,EAAE,WAAW,EACnB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EACnC,OAAO,CAAC,EAAE,WAAW,EACrB,kBAAkB,CAAC,EAAE,YAAY,GAAG,IAAI,EACxC,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,aAAa,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CAumC1B"}
1
+ {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAmB1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAU9C,OAAO,KAAK,EAAE,WAAW,EAAa,eAAe,EAAiB,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAkB/C;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiC5F;AAwtBD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAsB1D;AAMD,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAIvE;AAiTD,MAAM,MAAM,mBAAmB,GAAG;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtG;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG5D;AA2CD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,aAAa,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;IAC5D,aAAa,CAAC,EAAE,MAAM,CACpB,MAAM,EACN,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAC3G,CAAC;IACF,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3D,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,aAAa,EAAE,MAAM,WAAW,EAAE,CAAC;IACnC,kIAAkI;IAClI,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,SAAS,GAAG,UAAU,EACpC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,EAC5C,MAAM,EAAE,WAAW,EACnB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EACnC,OAAO,CAAC,EAAE,WAAW,EACrB,kBAAkB,CAAC,EAAE,YAAY,GAAG,IAAI,EACxC,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,aAAa,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CAirC1B"}
@@ -89,7 +89,7 @@ Respond with valid JSON:
89
89
  "tab_id": "target ID of the tab to switch to or close (for switch_tab / close_tab)",
90
90
  "hold_ms": "optional hold duration in ms (for press_and_hold) — set when the button/UI specifies a hold time or a prior attempt was too short",
91
91
  "query": "search query (for web_search) — e.g. 'best apartment listing sites NYC'",
92
- "answer": "direct answer to the user's question (for done)"
92
+ "answer": "string — direct answer to the user's question (for done). Always a single string; use markdown sections and bullet points to structure findings. Never an array or object."
93
93
  }
94
94
  ]
95
95
  }
@@ -241,7 +241,7 @@ Respond with valid JSON:
241
241
  "memory": "Your accumulated findings",
242
242
  "reasoning": "Final assessment",
243
243
  "action": "done" or "fail",
244
- "answer": "Your complete answer with all findings. Structure with sections and bullet points. Only include data you saw on actual pages — never fill gaps with training knowledge."
244
+ "answer": "A SINGLE STRING containing your complete answer with all findings. Use markdown sections and bullet points to structure the prose — but the value must be one string, never a JSON array or object. Only include data you saw on actual pages — never fill gaps with training knowledge."
245
245
  }
246
246
 
247
247
  If you gathered useful data, use "done" with a structured answer. If not, use "fail" with what you tried.`;
@@ -312,6 +312,24 @@ const SCROLL_AT_VIEWPORT_FN = `
312
312
  window.scrollBy(0, dy);
313
313
  })
314
314
  `;
315
+ async function safeUrl(page, fallback = '') {
316
+ try {
317
+ return await page.url();
318
+ }
319
+ catch (err) {
320
+ logger.warn({ err, fallback }, 'page.url failed — using fallback');
321
+ return fallback;
322
+ }
323
+ }
324
+ async function safeTitle(page) {
325
+ try {
326
+ return await page.title();
327
+ }
328
+ catch (err) {
329
+ logger.warn({ err }, 'page.title failed — returning empty');
330
+ return '';
331
+ }
332
+ }
315
333
  async function safeSnapshot(page) {
316
334
  let snapshot;
317
335
  const t0 = Date.now();
@@ -637,11 +655,20 @@ function parseActions(parsed) {
637
655
  if (typeof item.action !== 'string') {
638
656
  throw new Error(`Action at index ${String(i)} missing "action" field`);
639
657
  }
658
+ const rawAnswer = item.answer;
659
+ const answerIsBadShape = rawAnswer !== undefined && rawAnswer !== null && typeof rawAnswer !== 'string';
660
+ if (item.action === 'done' && answerIsBadShape) {
661
+ return {
662
+ action: 'wait',
663
+ ...(i === 0 ? thinking : { reasoning: `Batch action ${String(i + 1)}: wait` }),
664
+ error_feedback: `Your "done.answer" was a ${typeof rawAnswer} (likely an array or object). It MUST be a single string — flatten every finding into markdown with sections and bullet points, then re-emit "done" with that string. Preserve every result you have already gathered.`,
665
+ };
666
+ }
640
667
  return {
641
668
  action: item.action,
642
669
  // Thinking fields only on the first action
643
670
  ...(i === 0 ? thinking : { reasoning: `Batch action ${String(i + 1)}: ${item.action}` }),
644
- answer: item.answer,
671
+ answer: typeof rawAnswer === 'string' ? rawAnswer : undefined,
645
672
  ref: item.ref,
646
673
  text: item.text,
647
674
  url: item.url,
@@ -1011,7 +1038,20 @@ export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUs
1011
1038
  : { page: pageOrHolder };
1012
1039
  const history = [];
1013
1040
  const startTime = Date.now();
1014
- const tabManager = browser !== undefined ? new TabManager(holder.page) : null;
1041
+ const wallsHit = [];
1042
+ const sitesVisited = new Set();
1043
+ let recordsExtracted = 0;
1044
+ let lastKnownUrl = '';
1045
+ let activeBrowser = browser;
1046
+ let tabManager = activeBrowser !== undefined ? new TabManager(holder.page) : null;
1047
+ const refreshBrowserHandle = () => {
1048
+ const current = holder.browser ?? browser;
1049
+ if (current !== activeBrowser) {
1050
+ activeBrowser = current;
1051
+ tabManager = current !== undefined ? new TabManager(holder.page) : null;
1052
+ }
1053
+ return activeBrowser;
1054
+ };
1015
1055
  let consecutiveParseFailures = 0;
1016
1056
  let crossSiteParseFailures = 0;
1017
1057
  let consecutiveApiFailures = 0;
@@ -1034,12 +1074,12 @@ export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUs
1034
1074
  reasoning: reason,
1035
1075
  answer,
1036
1076
  };
1037
- const currentUrl = await holder.page.url();
1077
+ const currentUrl = await safeUrl(holder.page, lastKnownUrl);
1038
1078
  history.push({
1039
1079
  step,
1040
1080
  action: doneAction,
1041
1081
  url: currentUrl,
1042
- page_title: await holder.page.title(),
1082
+ page_title: await safeTitle(holder.page),
1043
1083
  timestamp: new Date().toISOString(),
1044
1084
  });
1045
1085
  emit('step', { step, action: 'done', reasoning: reason, answer });
@@ -1049,6 +1089,9 @@ export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUs
1049
1089
  answer,
1050
1090
  duration_ms: Date.now() - startTime,
1051
1091
  final_url: currentUrl,
1092
+ walls_hit: wallsHit,
1093
+ sites_visited: [...sitesVisited],
1094
+ records_extracted: recordsExtracted,
1052
1095
  };
1053
1096
  };
1054
1097
  let domainSkill = initialDomainSkill ?? null;
@@ -1158,6 +1201,9 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
1158
1201
  steps: history,
1159
1202
  error: 'Session aborted',
1160
1203
  duration_ms: Date.now() - startTime,
1204
+ walls_hit: wallsHit,
1205
+ sites_visited: [...sitesVisited],
1206
+ records_extracted: recordsExtracted,
1161
1207
  };
1162
1208
  }
1163
1209
  if (!(await isBrowserAlive(holder.page))) {
@@ -1169,6 +1215,9 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
1169
1215
  error: 'Browser connection lost',
1170
1216
  duration_ms: Date.now() - startTime,
1171
1217
  final_url: history.length > 0 ? history[history.length - 1].url : undefined,
1218
+ walls_hit: wallsHit,
1219
+ sites_visited: [...sitesVisited],
1220
+ records_extracted: recordsExtracted,
1172
1221
  };
1173
1222
  }
1174
1223
  baselineModalSigs ??= await capturePopupSignatures(holder.page);
@@ -1176,8 +1225,13 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
1176
1225
  await dismissPopup(holder.page);
1177
1226
  }
1178
1227
  let snapshot = await safeSnapshot(holder.page);
1179
- const url = await holder.page.url();
1180
- const title = await holder.page.title();
1228
+ const url = await safeUrl(holder.page, lastKnownUrl);
1229
+ if (url !== '')
1230
+ lastKnownUrl = url;
1231
+ const visitedDomain = extractDomain(url);
1232
+ if (visitedDomain !== '')
1233
+ sitesVisited.add(visitedDomain);
1234
+ const title = await safeTitle(holder.page);
1181
1235
  if (options?.getSkill !== undefined) {
1182
1236
  const currentDomain = extractDomain(url);
1183
1237
  if (currentDomain !== '' && currentDomain !== lastLoadedSkillDomain) {
@@ -1202,6 +1256,10 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
1202
1256
  const domText = await getPageText(holder.page);
1203
1257
  const antiBotType = detectAntiBot(domText);
1204
1258
  if (antiBotType !== null) {
1259
+ const wallDomain = extractDomain(url);
1260
+ if (wallDomain !== '' && !wallsHit.some((w) => w.domain === wallDomain && w.type === antiBotType)) {
1261
+ wallsHit.push({ domain: wallDomain, type: antiBotType });
1262
+ }
1205
1263
  snapshot = enrichSnapshot(snapshot, domText, antiBotType);
1206
1264
  const currentDomain = extractDomain(url);
1207
1265
  const hits = (antiBotHitsByDomain.get(currentDomain) ?? 0) + 1;
@@ -1373,9 +1431,10 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1373
1431
  lastStateSig = currentStateSig;
1374
1432
  let tabCount;
1375
1433
  let tabs;
1376
- if (browser !== undefined) {
1434
+ const browserForStep = refreshBrowserHandle();
1435
+ if (browserForStep !== undefined) {
1377
1436
  try {
1378
- const allTabs = (await browser.tabs());
1437
+ const allTabs = (await browserForStep.tabs());
1379
1438
  tabCount = allTabs.length;
1380
1439
  if (tabCount > 1)
1381
1440
  tabs = allTabs;
@@ -1573,6 +1632,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1573
1632
  steps: history,
1574
1633
  error: userMessage,
1575
1634
  duration_ms: Date.now() - startTime,
1635
+ walls_hit: wallsHit,
1636
+ sites_visited: [...sitesVisited],
1637
+ records_extracted: recordsExtracted,
1576
1638
  };
1577
1639
  }
1578
1640
  consecutiveApiFailures++;
@@ -1589,6 +1651,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1589
1651
  steps: history,
1590
1652
  error: `Unable to reach the AI service after ${String(MAX_API_FAILURES)} attempts. Please try again.`,
1591
1653
  duration_ms: Date.now() - startTime,
1654
+ walls_hit: wallsHit,
1655
+ sites_visited: [...sitesVisited],
1656
+ records_extracted: recordsExtracted,
1592
1657
  };
1593
1658
  }
1594
1659
  continue;
@@ -1602,8 +1667,8 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1602
1667
  const agentStep = {
1603
1668
  step,
1604
1669
  action,
1605
- url: await holder.page.url(),
1606
- page_title: await holder.page.title(),
1670
+ url: await safeUrl(holder.page, lastKnownUrl),
1671
+ page_title: await safeTitle(holder.page),
1607
1672
  timestamp: new Date().toISOString(),
1608
1673
  };
1609
1674
  history.push(agentStep);
@@ -1649,6 +1714,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1649
1714
  duration_ms: Date.now() - startTime,
1650
1715
  final_url: agentStep.url,
1651
1716
  validation_warnings: validationWarnings,
1717
+ walls_hit: wallsHit,
1718
+ sites_visited: [...sitesVisited],
1719
+ records_extracted: recordsExtracted,
1652
1720
  };
1653
1721
  }
1654
1722
  if (action.action === 'fail') {
@@ -1658,6 +1726,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1658
1726
  error: action.reasoning,
1659
1727
  duration_ms: Date.now() - startTime,
1660
1728
  final_url: agentStep.url,
1729
+ walls_hit: wallsHit,
1730
+ sites_visited: [...sitesVisited],
1731
+ records_extracted: recordsExtracted,
1661
1732
  };
1662
1733
  }
1663
1734
  if (action.action === 'ask_user') {
@@ -1687,6 +1758,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1687
1758
  error: `User did not respond to question: "${question}"`,
1688
1759
  duration_ms: Date.now() - startTime,
1689
1760
  final_url: history.length > 0 ? history[history.length - 1].url : undefined,
1761
+ walls_hit: wallsHit,
1762
+ sites_visited: [...sitesVisited],
1763
+ records_extracted: recordsExtracted,
1690
1764
  };
1691
1765
  }
1692
1766
  emit('step_error', { step, error: message });
@@ -1695,13 +1769,16 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1695
1769
  steps: history,
1696
1770
  error: message,
1697
1771
  duration_ms: Date.now() - startTime,
1772
+ walls_hit: wallsHit,
1773
+ sites_visited: [...sitesVisited],
1774
+ records_extracted: recordsExtracted,
1698
1775
  };
1699
1776
  }
1700
1777
  step++;
1701
1778
  break; // Need new snapshot after user response
1702
1779
  }
1703
1780
  if (action.action === 'press_and_hold') {
1704
- const pahDomain = extractDomain(await holder.page.url());
1781
+ const pahDomain = extractDomain(await safeUrl(holder.page, lastKnownUrl));
1705
1782
  const priorFailures = pahFailuresByDomain.get(pahDomain) ?? 0;
1706
1783
  if (priorFailures >= PAH_MAX_FAILURES) {
1707
1784
  logger.warn({ step, pahDomain, pahFailureCount: priorFailures }, 'press_and_hold short-circuited — bail limit reached');
@@ -1756,10 +1833,23 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1756
1833
  agentStep.action.extract_result = `Error: ${evalErr instanceof Error ? evalErr.message : 'evaluation failed'}`;
1757
1834
  }
1758
1835
  }
1759
- else if (action.urls !== undefined && action.urls.length > 0 && browser !== undefined) {
1836
+ else if (action.urls !== undefined && action.urls.length > 0) {
1760
1837
  for (const url of action.urls)
1761
1838
  assertNavigateUrlAllowed(url);
1762
- const batch = await extractItemsFromUrls(browser, action.urls);
1839
+ if (holder.ensureProxyForUrl !== undefined) {
1840
+ for (const url of action.urls) {
1841
+ await holder.ensureProxyForUrl(url);
1842
+ }
1843
+ }
1844
+ const extractBrowser = refreshBrowserHandle();
1845
+ if (extractBrowser === undefined) {
1846
+ agentStep.action.extract_result = 'Error: extract with urls requires browser context';
1847
+ recentFailureCount++;
1848
+ step++;
1849
+ break;
1850
+ }
1851
+ const batch = await extractItemsFromUrls(extractBrowser, action.urls);
1852
+ recordsExtracted += batch.count;
1763
1853
  const header = batch.count >= 5
1764
1854
  ? `${String(batch.count)} records fetched across ${String(action.urls.length)} URLs. This is your data — parse the records and respond. Do NOT open individual URLs again; fields the user asked for (price, address, name) are embedded in each record's text or fields.\n`
1765
1855
  : '';
@@ -1777,6 +1867,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1777
1867
  }
1778
1868
  else {
1779
1869
  const items = await extractItems(holder.page);
1870
+ recordsExtracted += items.count;
1780
1871
  const header = items.count >= 5
1781
1872
  ? `${String(items.count)} records extracted from ${items.source}. This is your data — parse and respond. Before drilling individual detail pages, check the active page-wide filters (URL path/query AND visible filter chips count): any user-prompt constraint that is already enforced by an active filter (price cap, neighborhood, pets allowed, etc.) propagates to every record — re-verifying it per detail page is wasted work. For constraints not enforced by any active filter, you do need to verify per record. Drill ONLY for missing per-record fields the user asked for (e.g. exact bedrooms, address, URL) or for constraints the active filters didn't enforce.\n`
1782
1873
  : '';
@@ -1814,9 +1905,10 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1814
1905
  }
1815
1906
  // #2 Tab switching
1816
1907
  if (action.action === 'switch_tab') {
1817
- if (browser !== undefined && action.tab_id !== undefined && action.tab_id !== '') {
1908
+ const tabBrowser = refreshBrowserHandle();
1909
+ if (tabBrowser !== undefined && action.tab_id !== undefined && action.tab_id !== '') {
1818
1910
  try {
1819
- const newPage = browser.page(action.tab_id);
1911
+ const newPage = tabBrowser.page(action.tab_id);
1820
1912
  const cdpBase = getCdpBaseUrl(holder.page);
1821
1913
  await activateCdpTarget(cdpBase, action.tab_id);
1822
1914
  holder.page = newPage;
@@ -1835,7 +1927,8 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1835
1927
  break;
1836
1928
  }
1837
1929
  if (action.action === 'close_tab') {
1838
- if (browser !== undefined && action.tab_id !== undefined && action.tab_id !== '') {
1930
+ const closeBrowser = refreshBrowserHandle();
1931
+ if (closeBrowser !== undefined && action.tab_id !== undefined && action.tab_id !== '') {
1839
1932
  if (action.tab_id === holder.page.id) {
1840
1933
  agentStep.action.error_feedback =
1841
1934
  'close_tab: cannot close the active tab; switch to another tab first, then close this one';
@@ -1843,7 +1936,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1843
1936
  }
1844
1937
  else {
1845
1938
  try {
1846
- await browser.close(action.tab_id);
1939
+ await closeBrowser.close(action.tab_id);
1847
1940
  logger.info({ step, tab_id: action.tab_id }, 'Closed tab');
1848
1941
  }
1849
1942
  catch (tabErr) {
@@ -1859,7 +1952,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1859
1952
  step++;
1860
1953
  break;
1861
1954
  }
1862
- const preActionUrl = await holder.page.url();
1955
+ const preActionUrl = await safeUrl(holder.page, url);
1863
1956
  if (action.action === 'navigate' && action.url !== undefined && action.url !== '') {
1864
1957
  const targetDomain = extractDomain(action.url);
1865
1958
  if (targetDomain !== '' && walledDomains.has(targetDomain)) {
@@ -1870,9 +1963,12 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1870
1963
  }
1871
1964
  }
1872
1965
  try {
1966
+ if (action.action === 'navigate' && action.url !== undefined && holder.ensureProxyForUrl !== undefined) {
1967
+ await holder.ensureProxyForUrl(action.url);
1968
+ }
1873
1969
  const customHandler = options?.customActions?.[action.action];
1874
1970
  if (customHandler !== undefined) {
1875
- const result = await customHandler(action, holder.page, browser);
1971
+ const result = await customHandler(action, holder.page, refreshBrowserHandle());
1876
1972
  if (result?.outcome !== undefined)
1877
1973
  agentStep.outcome = result.outcome;
1878
1974
  }
@@ -1898,7 +1994,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1898
1994
  // Validate action outcome — provide natural language feedback
1899
1995
  let postActionUrl = preActionUrl;
1900
1996
  try {
1901
- postActionUrl = await holder.page.url();
1997
+ postActionUrl = await safeUrl(holder.page, preActionUrl);
1902
1998
  const postSnapshot = (await holder.page.snapshot({ interactive: true, compact: true, timeoutMs: SNAPSHOT_TIMEOUT_MS })).snapshot;
1903
1999
  const outcome = validateAction(action, preActionUrl, postActionUrl, snapshot.length, postSnapshot.length);
1904
2000
  if (outcome !== undefined) {
@@ -1969,8 +2065,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
1969
2065
  break; // Break batch on failure — need new snapshot
1970
2066
  }
1971
2067
  // Check for new tabs after click
1972
- if (tabManager !== null && browser !== undefined && action.action === 'click') {
1973
- const newPage = await tabManager.checkForNewTab(browser);
2068
+ const tabClickBrowser = refreshBrowserHandle();
2069
+ if (tabManager !== null && tabClickBrowser !== undefined && action.action === 'click') {
2070
+ const newPage = await tabManager.checkForNewTab(tabClickBrowser);
1974
2071
  if (newPage !== null) {
1975
2072
  try {
1976
2073
  const newUrl = await newPage.url();
@@ -2015,6 +2112,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
2015
2112
  answer: judgment.answer,
2016
2113
  duration_ms: Date.now() - startTime,
2017
2114
  final_url: history.length > 0 ? history[history.length - 1].url : undefined,
2115
+ walls_hit: wallsHit,
2116
+ sites_visited: [...sitesVisited],
2117
+ records_extracted: recordsExtracted,
2018
2118
  };
2019
2119
  }
2020
2120
  const fallback = await getFinalSummary(refinedPrompt, history);
@@ -2025,6 +2125,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
2025
2125
  error: `Reached maximum step limit (${String(maxSteps)}); still missing: ${judgment.missing}`,
2026
2126
  duration_ms: Date.now() - startTime,
2027
2127
  final_url: history.length > 0 ? history[history.length - 1].url : undefined,
2128
+ walls_hit: wallsHit,
2129
+ sites_visited: [...sitesVisited],
2130
+ records_extracted: recordsExtracted,
2028
2131
  };
2029
2132
  }
2030
2133
  //# sourceMappingURL=agent-loop.js.map