browserclaw-agent 0.6.2 → 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.
- package/dist/agent-loop.d.ts +2 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +257 -39
- package/dist/agent-loop.js.map +1 -1
- package/dist/api-types.d.ts +81 -0
- package/dist/api-types.d.ts.map +1 -0
- package/dist/api-types.js +6 -0
- package/dist/api-types.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -10
- package/dist/config.js.map +1 -1
- package/dist/idempotency.d.ts +48 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +99 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/llm.d.ts +7 -10
- package/dist/llm.d.ts.map +1 -1
- package/dist/llm.js +104 -272
- package/dist/llm.js.map +1 -1
- package/dist/parse-json-response.d.ts +4 -0
- package/dist/parse-json-response.d.ts.map +1 -1
- package/dist/parse-json-response.js +21 -3
- package/dist/parse-json-response.js.map +1 -1
- package/dist/prompt-log.d.ts +9 -1
- package/dist/prompt-log.d.ts.map +1 -1
- package/dist/prompt-log.js.map +1 -1
- package/dist/proxy.d.ts +9 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +85 -0
- package/dist/proxy.js.map +1 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +116 -30
- package/dist/routes.js.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/session-manager.d.ts +2 -8
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +196 -51
- package/dist/session-manager.js.map +1 -1
- package/dist/skills/press-and-hold.d.ts.map +1 -1
- package/dist/skills/press-and-hold.js +20 -14
- package/dist/skills/press-and-hold.js.map +1 -1
- package/dist/sse-stamp.d.ts +2 -0
- package/dist/sse-stamp.d.ts.map +1 -0
- package/dist/sse-stamp.js +9 -0
- package/dist/sse-stamp.js.map +1 -0
- package/dist/trajectory-store.d.ts +1 -0
- package/dist/trajectory-store.d.ts.map +1 -1
- package/dist/trajectory-store.js +1 -0
- package/dist/trajectory-store.js.map +1 -1
- package/dist/types.d.ts +9 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -3
package/dist/agent-loop.d.ts
CHANGED
|
@@ -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.
|
package/dist/agent-loop.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/agent-loop.js
CHANGED
|
@@ -10,9 +10,9 @@ import { extractItems, extractItemsFromUrls } from './skills/extract-items.js';
|
|
|
10
10
|
import { extractDomain } from './skill-store.js';
|
|
11
11
|
import { extractSchema, validateAnswer } from './answer-validator.js';
|
|
12
12
|
import { webSearch } from './web-search.js';
|
|
13
|
-
import { llmJson, llmVision, sanitizeErrorText, getTokenUsage } from './llm.js';
|
|
13
|
+
import { llmJson, llmVision, sanitizeErrorText, getTokenUsage, isFailFastError, extractProviderMessage, } from './llm.js';
|
|
14
14
|
import { LlmParseError } from './types.js';
|
|
15
|
-
import { defaultAgentConfig, INTERJECTION_INJECTION_MAX_CHARS } from './config.js';
|
|
15
|
+
import { defaultAgentConfig, INTERJECTION_INJECTION_MAX_CHARS, INTERJECTION_TIMEOUT_CANCEL, MAX_STEPS_HARD_CEILING, STRICT_NONIDEMPOTENT_BAN, } from './config.js';
|
|
16
16
|
import { logger } from './logger.js';
|
|
17
17
|
function formatLessonForPrompt(lesson) {
|
|
18
18
|
const worked = lesson.domains.filter((d) => d.status === 'worked');
|
|
@@ -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": "
|
|
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:
|
|
671
|
+
answer: typeof rawAnswer === 'string' ? rawAnswer : undefined,
|
|
645
672
|
ref: item.ref,
|
|
646
673
|
text: item.text,
|
|
647
674
|
url: item.url,
|
|
@@ -815,6 +842,33 @@ function isRefFailure(action, err) {
|
|
|
815
842
|
const lower = raw.toLowerCase();
|
|
816
843
|
return REF_FAILURE_KEYWORDS.some((kw) => lower.includes(kw));
|
|
817
844
|
}
|
|
845
|
+
const TRANSIENT_ERROR_KEYWORDS = [
|
|
846
|
+
'timeout',
|
|
847
|
+
'timed out',
|
|
848
|
+
'net::',
|
|
849
|
+
'network error',
|
|
850
|
+
'network timeout',
|
|
851
|
+
'network unreachable',
|
|
852
|
+
'navigation failed',
|
|
853
|
+
'econnreset',
|
|
854
|
+
'econnrefused',
|
|
855
|
+
'etimedout',
|
|
856
|
+
'socket hang up',
|
|
857
|
+
];
|
|
858
|
+
function isTransientError(err) {
|
|
859
|
+
const raw = err instanceof Error ? err.message : '';
|
|
860
|
+
const lower = raw.toLowerCase();
|
|
861
|
+
return TRANSIENT_ERROR_KEYWORDS.some((kw) => lower.includes(kw));
|
|
862
|
+
}
|
|
863
|
+
function isNonIdempotentAction(action) {
|
|
864
|
+
return (action.action === 'click' ||
|
|
865
|
+
action.action === 'type' ||
|
|
866
|
+
action.action === 'select' ||
|
|
867
|
+
action.action === 'keyboard' ||
|
|
868
|
+
action.action === 'navigate' ||
|
|
869
|
+
action.action === 'press_and_hold' ||
|
|
870
|
+
action.action === 'click_cloudflare');
|
|
871
|
+
}
|
|
818
872
|
function describeActionError(action, err) {
|
|
819
873
|
const raw = err instanceof Error ? err.message : 'Action execution failed';
|
|
820
874
|
const lower = raw.toLowerCase();
|
|
@@ -897,11 +951,12 @@ Be concise but preserve all important data points. Respond with JSON: {"summary"
|
|
|
897
951
|
message: `Task: ${prompt}${progressStr}\n\nFull history (${String(history.length)} steps):\n${stepSummaries}`,
|
|
898
952
|
maxTokens: 1024,
|
|
899
953
|
});
|
|
900
|
-
return result.summary;
|
|
954
|
+
return { ok: true, summary: result.summary };
|
|
901
955
|
}
|
|
902
|
-
catch {
|
|
903
|
-
|
|
904
|
-
|
|
956
|
+
catch (err) {
|
|
957
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
958
|
+
logger.warn({ err: message }, 'Context compression failed — falling back to simple truncation');
|
|
959
|
+
return { ok: false, error: message };
|
|
905
960
|
}
|
|
906
961
|
}
|
|
907
962
|
async function getFinalSummary(prompt, history) {
|
|
@@ -972,6 +1027,10 @@ Use ONLY data that appears in the memory or steps below — never invent values.
|
|
|
972
1027
|
export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUser, browser, initialDomainSkill, maxSteps, userChat, options) {
|
|
973
1028
|
const cfg = defaultAgentConfig(options?.config);
|
|
974
1029
|
maxSteps ??= cfg.maxSteps;
|
|
1030
|
+
if (MAX_STEPS_HARD_CEILING > 0 && maxSteps > MAX_STEPS_HARD_CEILING) {
|
|
1031
|
+
logger.warn({ configured: maxSteps, ceiling: MAX_STEPS_HARD_CEILING }, 'maxSteps exceeded hard ceiling — clamping');
|
|
1032
|
+
maxSteps = MAX_STEPS_HARD_CEILING;
|
|
1033
|
+
}
|
|
975
1034
|
// Accept either a bare CrawlPage or a PageHolder. When a PageHolder is
|
|
976
1035
|
// provided the caller's reference is updated on tab switches.
|
|
977
1036
|
const holder = 'page' in pageOrHolder && typeof pageOrHolder.page === 'object'
|
|
@@ -979,7 +1038,20 @@ export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUs
|
|
|
979
1038
|
: { page: pageOrHolder };
|
|
980
1039
|
const history = [];
|
|
981
1040
|
const startTime = Date.now();
|
|
982
|
-
const
|
|
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
|
+
};
|
|
983
1055
|
let consecutiveParseFailures = 0;
|
|
984
1056
|
let crossSiteParseFailures = 0;
|
|
985
1057
|
let consecutiveApiFailures = 0;
|
|
@@ -1002,12 +1074,12 @@ export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUs
|
|
|
1002
1074
|
reasoning: reason,
|
|
1003
1075
|
answer,
|
|
1004
1076
|
};
|
|
1005
|
-
const currentUrl = await holder.page
|
|
1077
|
+
const currentUrl = await safeUrl(holder.page, lastKnownUrl);
|
|
1006
1078
|
history.push({
|
|
1007
1079
|
step,
|
|
1008
1080
|
action: doneAction,
|
|
1009
1081
|
url: currentUrl,
|
|
1010
|
-
page_title: await holder.page
|
|
1082
|
+
page_title: await safeTitle(holder.page),
|
|
1011
1083
|
timestamp: new Date().toISOString(),
|
|
1012
1084
|
});
|
|
1013
1085
|
emit('step', { step, action: 'done', reasoning: reason, answer });
|
|
@@ -1017,9 +1089,32 @@ export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUs
|
|
|
1017
1089
|
answer,
|
|
1018
1090
|
duration_ms: Date.now() - startTime,
|
|
1019
1091
|
final_url: currentUrl,
|
|
1092
|
+
walls_hit: wallsHit,
|
|
1093
|
+
sites_visited: [...sitesVisited],
|
|
1094
|
+
records_extracted: recordsExtracted,
|
|
1020
1095
|
};
|
|
1021
1096
|
};
|
|
1022
1097
|
let domainSkill = initialDomainSkill ?? null;
|
|
1098
|
+
if (domainSkill !== null && process.env.SKILL_VALIDATION_ENABLED === 'true') {
|
|
1099
|
+
try {
|
|
1100
|
+
const verdict = await llmJson({
|
|
1101
|
+
system: 'You validate whether a saved playbook plausibly solves the user task. Reply with JSON {"ok": "YES"|"NO", "reason": "<one short line>"}. Be permissive — only say NO when the playbook is clearly for a different goal.',
|
|
1102
|
+
message: `Task: ${prompt}\n\nDomain: ${domainSkill.domain}\nTags: ${domainSkill.tags.join(', ')}\n\nPlaybook:\n${JSON.stringify(domainSkill.skill, null, 2)}`,
|
|
1103
|
+
maxTokens: 80,
|
|
1104
|
+
});
|
|
1105
|
+
if (verdict.ok === 'NO') {
|
|
1106
|
+
logger.info({ domain: domainSkill.domain, reason: verdict.reason }, 'Skill validation skipped playbook');
|
|
1107
|
+
emit('skill_skipped', {
|
|
1108
|
+
domain: domainSkill.domain,
|
|
1109
|
+
reason: verdict.reason,
|
|
1110
|
+
});
|
|
1111
|
+
domainSkill = null;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
catch (err) {
|
|
1115
|
+
logger.warn({ err }, 'Skill validation gate errored — keeping skill');
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1023
1118
|
const antiBotHitsByDomain = new Map();
|
|
1024
1119
|
const walledDomains = new Set();
|
|
1025
1120
|
let answerSchema = null;
|
|
@@ -1106,6 +1201,9 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1106
1201
|
steps: history,
|
|
1107
1202
|
error: 'Session aborted',
|
|
1108
1203
|
duration_ms: Date.now() - startTime,
|
|
1204
|
+
walls_hit: wallsHit,
|
|
1205
|
+
sites_visited: [...sitesVisited],
|
|
1206
|
+
records_extracted: recordsExtracted,
|
|
1109
1207
|
};
|
|
1110
1208
|
}
|
|
1111
1209
|
if (!(await isBrowserAlive(holder.page))) {
|
|
@@ -1117,6 +1215,9 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1117
1215
|
error: 'Browser connection lost',
|
|
1118
1216
|
duration_ms: Date.now() - startTime,
|
|
1119
1217
|
final_url: history.length > 0 ? history[history.length - 1].url : undefined,
|
|
1218
|
+
walls_hit: wallsHit,
|
|
1219
|
+
sites_visited: [...sitesVisited],
|
|
1220
|
+
records_extracted: recordsExtracted,
|
|
1120
1221
|
};
|
|
1121
1222
|
}
|
|
1122
1223
|
baselineModalSigs ??= await capturePopupSignatures(holder.page);
|
|
@@ -1124,8 +1225,13 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1124
1225
|
await dismissPopup(holder.page);
|
|
1125
1226
|
}
|
|
1126
1227
|
let snapshot = await safeSnapshot(holder.page);
|
|
1127
|
-
const url = await holder.page
|
|
1128
|
-
|
|
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);
|
|
1129
1235
|
if (options?.getSkill !== undefined) {
|
|
1130
1236
|
const currentDomain = extractDomain(url);
|
|
1131
1237
|
if (currentDomain !== '' && currentDomain !== lastLoadedSkillDomain) {
|
|
@@ -1150,6 +1256,10 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1150
1256
|
const domText = await getPageText(holder.page);
|
|
1151
1257
|
const antiBotType = detectAntiBot(domText);
|
|
1152
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
|
+
}
|
|
1153
1263
|
snapshot = enrichSnapshot(snapshot, domText, antiBotType);
|
|
1154
1264
|
const currentDomain = extractDomain(url);
|
|
1155
1265
|
const hits = (antiBotHitsByDomain.get(currentDomain) ?? 0) + 1;
|
|
@@ -1172,10 +1282,21 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1172
1282
|
}
|
|
1173
1283
|
emit('thinking', { step, message: `Analyzing page: ${title}` });
|
|
1174
1284
|
if (step > 0 && step % CONTEXT_COMPRESS_INTERVAL === 0) {
|
|
1175
|
-
|
|
1176
|
-
if (
|
|
1177
|
-
|
|
1178
|
-
|
|
1285
|
+
const compressResult = await compressContext(refinedPrompt, history, lastProgress);
|
|
1286
|
+
if (compressResult.ok) {
|
|
1287
|
+
const droppedSteps = Math.max(0, history.length - HISTORY_RECENT_WINDOW);
|
|
1288
|
+
contextSummary = compressResult.summary;
|
|
1289
|
+
logger.info({ type: 'prompt_log', event: 'context_compressed', step, droppedSteps, summary: contextSummary }, 'Context compressed');
|
|
1290
|
+
emit('context_compressed', {
|
|
1291
|
+
step,
|
|
1292
|
+
droppedSteps,
|
|
1293
|
+
summary: contextSummary,
|
|
1294
|
+
summary_length: contextSummary.length,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
contextSummary = '';
|
|
1299
|
+
emit('context_compress_failed', { step, error: compressResult.error });
|
|
1179
1300
|
}
|
|
1180
1301
|
}
|
|
1181
1302
|
let domainChanged = false;
|
|
@@ -1310,9 +1431,10 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1310
1431
|
lastStateSig = currentStateSig;
|
|
1311
1432
|
let tabCount;
|
|
1312
1433
|
let tabs;
|
|
1313
|
-
|
|
1434
|
+
const browserForStep = refreshBrowserHandle();
|
|
1435
|
+
if (browserForStep !== undefined) {
|
|
1314
1436
|
try {
|
|
1315
|
-
const allTabs = (await
|
|
1437
|
+
const allTabs = (await browserForStep.tabs());
|
|
1316
1438
|
tabCount = allTabs.length;
|
|
1317
1439
|
if (tabCount > 1)
|
|
1318
1440
|
tabs = allTabs;
|
|
@@ -1468,7 +1590,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1468
1590
|
emit('step_error', {
|
|
1469
1591
|
step,
|
|
1470
1592
|
error: 'LLM response was not valid JSON',
|
|
1471
|
-
|
|
1593
|
+
error_kind: 'parse_error',
|
|
1472
1594
|
...(process.env.DEBUG !== undefined && { rawText: err.responseSnippet }),
|
|
1473
1595
|
});
|
|
1474
1596
|
// Absolute last resort: too many parse failures across all sites
|
|
@@ -1496,6 +1618,25 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1496
1618
|
continue;
|
|
1497
1619
|
}
|
|
1498
1620
|
// API/network error — don't burn a step, the agent never got to act
|
|
1621
|
+
if (isFailFastError(err)) {
|
|
1622
|
+
const userMessage = extractProviderMessage(err) ?? (err instanceof Error ? err.message : 'Unknown error');
|
|
1623
|
+
const errorType = err instanceof Error && /insufficient_quota/i.test(err.message) ? 'quota_exhausted' : 'auth_error';
|
|
1624
|
+
logger.error({
|
|
1625
|
+
step,
|
|
1626
|
+
type: errorType,
|
|
1627
|
+
errorMessage: err instanceof Error ? sanitizeErrorText(err.message).slice(0, 300) : 'unknown',
|
|
1628
|
+
}, 'LLM fail-fast error');
|
|
1629
|
+
emit('step_error', { step, error: userMessage, error_kind: errorType });
|
|
1630
|
+
return {
|
|
1631
|
+
success: false,
|
|
1632
|
+
steps: history,
|
|
1633
|
+
error: userMessage,
|
|
1634
|
+
duration_ms: Date.now() - startTime,
|
|
1635
|
+
walls_hit: wallsHit,
|
|
1636
|
+
sites_visited: [...sitesVisited],
|
|
1637
|
+
records_extracted: recordsExtracted,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1499
1640
|
consecutiveApiFailures++;
|
|
1500
1641
|
logger.error({
|
|
1501
1642
|
step,
|
|
@@ -1503,13 +1644,16 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1503
1644
|
maxAttempts: MAX_API_FAILURES,
|
|
1504
1645
|
errorMessage: err instanceof Error ? sanitizeErrorText(err.message).slice(0, 300) : 'unknown',
|
|
1505
1646
|
}, 'LLM API error');
|
|
1506
|
-
emit('step_error', { step, error: 'AI service temporarily unavailable',
|
|
1647
|
+
emit('step_error', { step, error: 'AI service temporarily unavailable', error_kind: 'api_error' });
|
|
1507
1648
|
if (consecutiveApiFailures >= MAX_API_FAILURES) {
|
|
1508
1649
|
return {
|
|
1509
1650
|
success: false,
|
|
1510
1651
|
steps: history,
|
|
1511
1652
|
error: `Unable to reach the AI service after ${String(MAX_API_FAILURES)} attempts. Please try again.`,
|
|
1512
1653
|
duration_ms: Date.now() - startTime,
|
|
1654
|
+
walls_hit: wallsHit,
|
|
1655
|
+
sites_visited: [...sitesVisited],
|
|
1656
|
+
records_extracted: recordsExtracted,
|
|
1513
1657
|
};
|
|
1514
1658
|
}
|
|
1515
1659
|
continue;
|
|
@@ -1523,8 +1667,8 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1523
1667
|
const agentStep = {
|
|
1524
1668
|
step,
|
|
1525
1669
|
action,
|
|
1526
|
-
url: await holder.page
|
|
1527
|
-
page_title: await holder.page
|
|
1670
|
+
url: await safeUrl(holder.page, lastKnownUrl),
|
|
1671
|
+
page_title: await safeTitle(holder.page),
|
|
1528
1672
|
timestamp: new Date().toISOString(),
|
|
1529
1673
|
};
|
|
1530
1674
|
history.push(agentStep);
|
|
@@ -1570,6 +1714,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1570
1714
|
duration_ms: Date.now() - startTime,
|
|
1571
1715
|
final_url: agentStep.url,
|
|
1572
1716
|
validation_warnings: validationWarnings,
|
|
1717
|
+
walls_hit: wallsHit,
|
|
1718
|
+
sites_visited: [...sitesVisited],
|
|
1719
|
+
records_extracted: recordsExtracted,
|
|
1573
1720
|
};
|
|
1574
1721
|
}
|
|
1575
1722
|
if (action.action === 'fail') {
|
|
@@ -1579,10 +1726,14 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1579
1726
|
error: action.reasoning,
|
|
1580
1727
|
duration_ms: Date.now() - startTime,
|
|
1581
1728
|
final_url: agentStep.url,
|
|
1729
|
+
walls_hit: wallsHit,
|
|
1730
|
+
sites_visited: [...sitesVisited],
|
|
1731
|
+
records_extracted: recordsExtracted,
|
|
1582
1732
|
};
|
|
1583
1733
|
}
|
|
1584
1734
|
if (action.action === 'ask_user') {
|
|
1585
|
-
|
|
1735
|
+
const question = action.text ?? action.reasoning;
|
|
1736
|
+
emit('ask_user', { step, question });
|
|
1586
1737
|
if (waitForUser === undefined) {
|
|
1587
1738
|
agentStep.action.error_feedback =
|
|
1588
1739
|
'No interactive user is attached to this session — ask_user cannot receive a response. Do NOT use ask_user again. Pivot: try a different site, a different URL for the same site, web_search for an alternative source, or use the data you already have to finish with a partial answer.';
|
|
@@ -1596,24 +1747,49 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1596
1747
|
}
|
|
1597
1748
|
catch (err) {
|
|
1598
1749
|
const message = err instanceof Error ? err.message : 'Failed to get user response';
|
|
1750
|
+
const lowerMessage = message.toLowerCase();
|
|
1751
|
+
const isTimeout = lowerMessage.includes('timed out') || lowerMessage.includes('timeout');
|
|
1752
|
+
if (isTimeout && INTERJECTION_TIMEOUT_CANCEL) {
|
|
1753
|
+
emit('user_interjection_timeout', { step, question });
|
|
1754
|
+
return {
|
|
1755
|
+
success: false,
|
|
1756
|
+
status: 'canceled-timeout',
|
|
1757
|
+
steps: history,
|
|
1758
|
+
error: `User did not respond to question: "${question}"`,
|
|
1759
|
+
duration_ms: Date.now() - startTime,
|
|
1760
|
+
final_url: history.length > 0 ? history[history.length - 1].url : undefined,
|
|
1761
|
+
walls_hit: wallsHit,
|
|
1762
|
+
sites_visited: [...sitesVisited],
|
|
1763
|
+
records_extracted: recordsExtracted,
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1599
1766
|
emit('step_error', { step, error: message });
|
|
1600
1767
|
return {
|
|
1601
1768
|
success: false,
|
|
1602
1769
|
steps: history,
|
|
1603
1770
|
error: message,
|
|
1604
1771
|
duration_ms: Date.now() - startTime,
|
|
1772
|
+
walls_hit: wallsHit,
|
|
1773
|
+
sites_visited: [...sitesVisited],
|
|
1774
|
+
records_extracted: recordsExtracted,
|
|
1605
1775
|
};
|
|
1606
1776
|
}
|
|
1607
1777
|
step++;
|
|
1608
1778
|
break; // Need new snapshot after user response
|
|
1609
1779
|
}
|
|
1610
1780
|
if (action.action === 'press_and_hold') {
|
|
1611
|
-
const pahDomain = extractDomain(await holder.page
|
|
1781
|
+
const pahDomain = extractDomain(await safeUrl(holder.page, lastKnownUrl));
|
|
1612
1782
|
const priorFailures = pahFailuresByDomain.get(pahDomain) ?? 0;
|
|
1613
1783
|
if (priorFailures >= PAH_MAX_FAILURES) {
|
|
1614
1784
|
logger.warn({ step, pahDomain, pahFailureCount: priorFailures }, 'press_and_hold short-circuited — bail limit reached');
|
|
1615
|
-
if (pahDomain !== '')
|
|
1785
|
+
if (pahDomain !== '' && !walledDomains.has(pahDomain)) {
|
|
1616
1786
|
walledDomains.add(pahDomain);
|
|
1787
|
+
emit('domain_blocked', {
|
|
1788
|
+
domain: pahDomain,
|
|
1789
|
+
reason: 'press_and_hold_bail_limit',
|
|
1790
|
+
attempt: priorFailures,
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1617
1793
|
agentStep.action.error_feedback = PAH_BAIL_FEEDBACK;
|
|
1618
1794
|
step++;
|
|
1619
1795
|
break;
|
|
@@ -1622,8 +1798,14 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1622
1798
|
if (!solved) {
|
|
1623
1799
|
const newFailures = priorFailures + 1;
|
|
1624
1800
|
pahFailuresByDomain.set(pahDomain, newFailures);
|
|
1625
|
-
if (pahDomain !== '' && newFailures >= PAH_MAX_FAILURES)
|
|
1801
|
+
if (pahDomain !== '' && newFailures >= PAH_MAX_FAILURES && !walledDomains.has(pahDomain)) {
|
|
1626
1802
|
walledDomains.add(pahDomain);
|
|
1803
|
+
emit('domain_blocked', {
|
|
1804
|
+
domain: pahDomain,
|
|
1805
|
+
reason: 'press_and_hold_failed',
|
|
1806
|
+
attempt: newFailures,
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1627
1809
|
const intermittent = await isIntermittentError(holder.page);
|
|
1628
1810
|
logger.info({ step, pahDomain, intermittent, pahFailureCount: newFailures }, 'press_and_hold failed — post-check');
|
|
1629
1811
|
agentStep.action.error_feedback = intermittent ? PAH_INTERMITTENT_FEEDBACK : PAH_BLOCKED_FEEDBACK;
|
|
@@ -1651,10 +1833,23 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1651
1833
|
agentStep.action.extract_result = `Error: ${evalErr instanceof Error ? evalErr.message : 'evaluation failed'}`;
|
|
1652
1834
|
}
|
|
1653
1835
|
}
|
|
1654
|
-
else if (action.urls !== undefined && action.urls.length > 0
|
|
1836
|
+
else if (action.urls !== undefined && action.urls.length > 0) {
|
|
1655
1837
|
for (const url of action.urls)
|
|
1656
1838
|
assertNavigateUrlAllowed(url);
|
|
1657
|
-
|
|
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;
|
|
1658
1853
|
const header = batch.count >= 5
|
|
1659
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`
|
|
1660
1855
|
: '';
|
|
@@ -1672,6 +1867,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1672
1867
|
}
|
|
1673
1868
|
else {
|
|
1674
1869
|
const items = await extractItems(holder.page);
|
|
1870
|
+
recordsExtracted += items.count;
|
|
1675
1871
|
const header = items.count >= 5
|
|
1676
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`
|
|
1677
1873
|
: '';
|
|
@@ -1709,9 +1905,10 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1709
1905
|
}
|
|
1710
1906
|
// #2 Tab switching
|
|
1711
1907
|
if (action.action === 'switch_tab') {
|
|
1712
|
-
|
|
1908
|
+
const tabBrowser = refreshBrowserHandle();
|
|
1909
|
+
if (tabBrowser !== undefined && action.tab_id !== undefined && action.tab_id !== '') {
|
|
1713
1910
|
try {
|
|
1714
|
-
const newPage =
|
|
1911
|
+
const newPage = tabBrowser.page(action.tab_id);
|
|
1715
1912
|
const cdpBase = getCdpBaseUrl(holder.page);
|
|
1716
1913
|
await activateCdpTarget(cdpBase, action.tab_id);
|
|
1717
1914
|
holder.page = newPage;
|
|
@@ -1730,7 +1927,8 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1730
1927
|
break;
|
|
1731
1928
|
}
|
|
1732
1929
|
if (action.action === 'close_tab') {
|
|
1733
|
-
|
|
1930
|
+
const closeBrowser = refreshBrowserHandle();
|
|
1931
|
+
if (closeBrowser !== undefined && action.tab_id !== undefined && action.tab_id !== '') {
|
|
1734
1932
|
if (action.tab_id === holder.page.id) {
|
|
1735
1933
|
agentStep.action.error_feedback =
|
|
1736
1934
|
'close_tab: cannot close the active tab; switch to another tab first, then close this one';
|
|
@@ -1738,7 +1936,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1738
1936
|
}
|
|
1739
1937
|
else {
|
|
1740
1938
|
try {
|
|
1741
|
-
await
|
|
1939
|
+
await closeBrowser.close(action.tab_id);
|
|
1742
1940
|
logger.info({ step, tab_id: action.tab_id }, 'Closed tab');
|
|
1743
1941
|
}
|
|
1744
1942
|
catch (tabErr) {
|
|
@@ -1754,7 +1952,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1754
1952
|
step++;
|
|
1755
1953
|
break;
|
|
1756
1954
|
}
|
|
1757
|
-
const preActionUrl = await holder.page
|
|
1955
|
+
const preActionUrl = await safeUrl(holder.page, url);
|
|
1758
1956
|
if (action.action === 'navigate' && action.url !== undefined && action.url !== '') {
|
|
1759
1957
|
const targetDomain = extractDomain(action.url);
|
|
1760
1958
|
if (targetDomain !== '' && walledDomains.has(targetDomain)) {
|
|
@@ -1765,9 +1963,12 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1765
1963
|
}
|
|
1766
1964
|
}
|
|
1767
1965
|
try {
|
|
1966
|
+
if (action.action === 'navigate' && action.url !== undefined && holder.ensureProxyForUrl !== undefined) {
|
|
1967
|
+
await holder.ensureProxyForUrl(action.url);
|
|
1968
|
+
}
|
|
1768
1969
|
const customHandler = options?.customActions?.[action.action];
|
|
1769
1970
|
if (customHandler !== undefined) {
|
|
1770
|
-
const result = await customHandler(action, holder.page,
|
|
1971
|
+
const result = await customHandler(action, holder.page, refreshBrowserHandle());
|
|
1771
1972
|
if (result?.outcome !== undefined)
|
|
1772
1973
|
agentStep.outcome = result.outcome;
|
|
1773
1974
|
}
|
|
@@ -1793,7 +1994,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1793
1994
|
// Validate action outcome — provide natural language feedback
|
|
1794
1995
|
let postActionUrl = preActionUrl;
|
|
1795
1996
|
try {
|
|
1796
|
-
postActionUrl = await holder.page
|
|
1997
|
+
postActionUrl = await safeUrl(holder.page, preActionUrl);
|
|
1797
1998
|
const postSnapshot = (await holder.page.snapshot({ interactive: true, compact: true, timeoutMs: SNAPSHOT_TIMEOUT_MS })).snapshot;
|
|
1798
1999
|
const outcome = validateAction(action, preActionUrl, postActionUrl, snapshot.length, postSnapshot.length);
|
|
1799
2000
|
if (outcome !== undefined) {
|
|
@@ -1846,6 +2047,16 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1846
2047
|
entry.action = action.action;
|
|
1847
2048
|
bannedRefs.set(action.ref, entry);
|
|
1848
2049
|
}
|
|
2050
|
+
else if (STRICT_NONIDEMPOTENT_BAN &&
|
|
2051
|
+
action.ref !== undefined &&
|
|
2052
|
+
action.ref !== '' &&
|
|
2053
|
+
isNonIdempotentAction(action) &&
|
|
2054
|
+
!isTransientError(err)) {
|
|
2055
|
+
const entry = bannedRefs.get(action.ref) ?? { action: action.action, failures: 0 };
|
|
2056
|
+
entry.failures = Math.max(entry.failures + 1, BAN_THRESHOLD);
|
|
2057
|
+
entry.action = action.action;
|
|
2058
|
+
bannedRefs.set(action.ref, entry);
|
|
2059
|
+
}
|
|
1849
2060
|
await holder.page.waitFor({ timeMs: 1000 });
|
|
1850
2061
|
if (await detectPopup(holder.page, baselineModalSigs)) {
|
|
1851
2062
|
await dismissPopup(holder.page);
|
|
@@ -1854,8 +2065,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1854
2065
|
break; // Break batch on failure — need new snapshot
|
|
1855
2066
|
}
|
|
1856
2067
|
// Check for new tabs after click
|
|
1857
|
-
|
|
1858
|
-
|
|
2068
|
+
const tabClickBrowser = refreshBrowserHandle();
|
|
2069
|
+
if (tabManager !== null && tabClickBrowser !== undefined && action.action === 'click') {
|
|
2070
|
+
const newPage = await tabManager.checkForNewTab(tabClickBrowser);
|
|
1859
2071
|
if (newPage !== null) {
|
|
1860
2072
|
try {
|
|
1861
2073
|
const newUrl = await newPage.url();
|
|
@@ -1900,6 +2112,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1900
2112
|
answer: judgment.answer,
|
|
1901
2113
|
duration_ms: Date.now() - startTime,
|
|
1902
2114
|
final_url: history.length > 0 ? history[history.length - 1].url : undefined,
|
|
2115
|
+
walls_hit: wallsHit,
|
|
2116
|
+
sites_visited: [...sitesVisited],
|
|
2117
|
+
records_extracted: recordsExtracted,
|
|
1903
2118
|
};
|
|
1904
2119
|
}
|
|
1905
2120
|
const fallback = await getFinalSummary(refinedPrompt, history);
|
|
@@ -1910,6 +2125,9 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1910
2125
|
error: `Reached maximum step limit (${String(maxSteps)}); still missing: ${judgment.missing}`,
|
|
1911
2126
|
duration_ms: Date.now() - startTime,
|
|
1912
2127
|
final_url: history.length > 0 ? history[history.length - 1].url : undefined,
|
|
2128
|
+
walls_hit: wallsHit,
|
|
2129
|
+
sites_visited: [...sitesVisited],
|
|
2130
|
+
records_extracted: recordsExtracted,
|
|
1913
2131
|
};
|
|
1914
2132
|
}
|
|
1915
2133
|
//# sourceMappingURL=agent-loop.js.map
|