@quanta-intellect/vessel-browser 0.1.32 → 0.1.35
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/out/main/index.js
CHANGED
|
@@ -5127,6 +5127,7 @@ function makeImageResult(base64, description, mediaType = "image/png") {
|
|
|
5127
5127
|
return JSON.stringify(result);
|
|
5128
5128
|
}
|
|
5129
5129
|
class AnthropicProvider {
|
|
5130
|
+
agentToolProfile = "default";
|
|
5130
5131
|
client;
|
|
5131
5132
|
model;
|
|
5132
5133
|
abortController = null;
|
|
@@ -5424,6 +5425,66 @@ const PROVIDERS = {
|
|
|
5424
5425
|
apiKeyHint: "Optional — only if your endpoint requires authentication"
|
|
5425
5426
|
}
|
|
5426
5427
|
};
|
|
5428
|
+
const SAFE_TOOL_ALIASES = {
|
|
5429
|
+
goto_url: "navigate",
|
|
5430
|
+
go_to_url: "navigate",
|
|
5431
|
+
browser_goto: "navigate",
|
|
5432
|
+
browser_navigate: "navigate",
|
|
5433
|
+
open_url: "navigate",
|
|
5434
|
+
visit_url: "navigate",
|
|
5435
|
+
navigate_to: "navigate",
|
|
5436
|
+
open_page: "navigate",
|
|
5437
|
+
google_search: "search",
|
|
5438
|
+
site_search: "search",
|
|
5439
|
+
search_site: "search",
|
|
5440
|
+
page_search: "search",
|
|
5441
|
+
scroll_down: "scroll",
|
|
5442
|
+
scroll_up: "scroll",
|
|
5443
|
+
read: "read_page",
|
|
5444
|
+
read_current_page: "read_page",
|
|
5445
|
+
scan_page: "read_page"
|
|
5446
|
+
};
|
|
5447
|
+
function normalizeToolAlias(name) {
|
|
5448
|
+
const normalized = name.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
|
|
5449
|
+
return SAFE_TOOL_ALIASES[normalized] ?? name;
|
|
5450
|
+
}
|
|
5451
|
+
function parseModelSizeInBillions(model) {
|
|
5452
|
+
const match = model.toLowerCase().match(/(?:^|[:/_\-\s])(\d+(?:\.\d+)?)b(?:$|[:/_\-\s])/i);
|
|
5453
|
+
if (!match) return null;
|
|
5454
|
+
const parsed = Number(match[1]);
|
|
5455
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
5456
|
+
}
|
|
5457
|
+
function isLoopbackBaseUrl(baseUrl) {
|
|
5458
|
+
if (!baseUrl) return false;
|
|
5459
|
+
try {
|
|
5460
|
+
const url = new URL(baseUrl);
|
|
5461
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
5462
|
+
} catch {
|
|
5463
|
+
return false;
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
function resolveAgentToolProfile(config) {
|
|
5467
|
+
const providerId = config.id;
|
|
5468
|
+
const isLocalProvider = providerId === "ollama" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
|
|
5469
|
+
if (!isLocalProvider) return "default";
|
|
5470
|
+
const sizeInBillions = parseModelSizeInBillions(config.model);
|
|
5471
|
+
if (sizeInBillions === null) {
|
|
5472
|
+
return "compact";
|
|
5473
|
+
}
|
|
5474
|
+
return sizeInBillions <= 14 ? "compact" : "default";
|
|
5475
|
+
}
|
|
5476
|
+
function shouldDebugAgentLoop() {
|
|
5477
|
+
const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
|
|
5478
|
+
return value === "1" || value === "true";
|
|
5479
|
+
}
|
|
5480
|
+
function previewDebugValue(value, maxLength = 800) {
|
|
5481
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
5482
|
+
if (normalized.length <= maxLength) return normalized;
|
|
5483
|
+
return `${normalized.slice(0, maxLength)}…`;
|
|
5484
|
+
}
|
|
5485
|
+
function previewToolDebugContent(content) {
|
|
5486
|
+
return previewDebugValue(content, 500);
|
|
5487
|
+
}
|
|
5427
5488
|
function toOpenAITools(tools) {
|
|
5428
5489
|
return tools.map((t) => ({
|
|
5429
5490
|
type: "function",
|
|
@@ -5434,7 +5495,328 @@ function toOpenAITools(tools) {
|
|
|
5434
5495
|
}
|
|
5435
5496
|
}));
|
|
5436
5497
|
}
|
|
5498
|
+
function agentTemperatureForProfile(profile) {
|
|
5499
|
+
return profile === "compact" ? 0.2 : void 0;
|
|
5500
|
+
}
|
|
5501
|
+
function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
|
|
5502
|
+
if (profile !== "compact") return null;
|
|
5503
|
+
const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
|
|
5504
|
+
const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
|
|
5505
|
+
return {
|
|
5506
|
+
role: "system",
|
|
5507
|
+
content: `Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
|
|
5508
|
+
Do not ask the user what they want next unless the request is genuinely ambiguous or blocked. After navigation or page reads, keep executing the same task.` + (stateReminder ? `
|
|
5509
|
+
${stateReminder}` : "") + (phaseReminder ? `
|
|
5510
|
+
${phaseReminder}` : "")
|
|
5511
|
+
};
|
|
5512
|
+
}
|
|
5513
|
+
function extractSingleGoalDomain(goal) {
|
|
5514
|
+
const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
|
|
5515
|
+
if (!matches || matches.length !== 1) return null;
|
|
5516
|
+
return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
|
|
5517
|
+
}
|
|
5518
|
+
function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
|
|
5519
|
+
const phaseReminder = buildPhaseReminder(userMessage, assistantText);
|
|
5520
|
+
const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
|
|
5521
|
+
const goalDomain = extractSingleGoalDomain(userMessage);
|
|
5522
|
+
const latest = (latestToolResultPreview || "").toLowerCase();
|
|
5523
|
+
const assistant = assistantText.toLowerCase();
|
|
5524
|
+
const alreadyOnGoalSite = !!goalDomain && (latest.includes(goalDomain) || assistant.includes(`https://${goalDomain}`) || assistant.includes(`https://www.${goalDomain}`));
|
|
5525
|
+
const lines = [
|
|
5526
|
+
`The task is still in progress: ${userMessage}`,
|
|
5527
|
+
`Do not ask the user for permission to continue. Choose the next tool now unless the request is fully complete.`
|
|
5528
|
+
];
|
|
5529
|
+
if (alreadyOnGoalSite) {
|
|
5530
|
+
lines.push(
|
|
5531
|
+
`You are already on the requested site (${goalDomain}). Do not navigate to the homepage again and do not restart discovery from scratch.`
|
|
5532
|
+
);
|
|
5533
|
+
}
|
|
5534
|
+
if (stateReminder) {
|
|
5535
|
+
lines.push(stateReminder);
|
|
5536
|
+
}
|
|
5537
|
+
if (phaseReminder) {
|
|
5538
|
+
lines.push(phaseReminder);
|
|
5539
|
+
}
|
|
5540
|
+
return lines.join("\n");
|
|
5541
|
+
}
|
|
5542
|
+
function buildPhaseReminder(userMessage, assistantText) {
|
|
5543
|
+
const goal = userMessage.toLowerCase();
|
|
5544
|
+
const text = assistantText.toLowerCase();
|
|
5545
|
+
if (!goal || !text) return "";
|
|
5546
|
+
const wantsCart = /\b(cart|bag|basket|checkout)\b/.test(goal);
|
|
5547
|
+
const wantsExplanation = /\b(explain|reason|why)\b/.test(goal);
|
|
5548
|
+
const hasFiveItemList = /(?:^|\n)\s*1\./.test(assistantText) && /(?:^|\n)\s*2\./.test(assistantText) && /(?:^|\n)\s*3\./.test(assistantText) && /(?:^|\n)\s*4\./.test(assistantText) && /(?:^|\n)\s*5\./.test(assistantText);
|
|
5549
|
+
const selectedItems = hasFiveItemList || /i(?:'| a)?ve chosen/.test(text) || /i have chosen/.test(text) || /i selected/.test(text) || /here are the books/i.test(assistantText) || /here are the items/i.test(assistantText);
|
|
5550
|
+
const intendsCart = /next[, ]+i will add/.test(text) || /i(?:'| a)?ll start with the first/.test(text) || /proceed systematically/.test(text) || /add (these|the chosen|the selected).*(cart|bag|basket)/.test(text);
|
|
5551
|
+
const cartDone = /(added to cart|added them to the cart|cart confirmation|view cart|checkout)/.test(
|
|
5552
|
+
text
|
|
5553
|
+
);
|
|
5554
|
+
const explanationDone = /here is why i chose/.test(text) || /here are my reasons/.test(text) || /reason:/.test(text) || /reasons:/.test(text) || /why i chose/.test(text);
|
|
5555
|
+
if (wantsCart && selectedItems && (intendsCart || !cartDone)) {
|
|
5556
|
+
return `Progress reminder: You already selected the requested items. Do not restart browsing or searching unless a specific cart step fails. Continue adding the selected items to the cart one by one.`;
|
|
5557
|
+
}
|
|
5558
|
+
if (wantsCart && wantsExplanation && cartDone && !explanationDone) {
|
|
5559
|
+
return `Progress reminder: The cart step appears complete. Do not resume browsing. Finish by explaining why the chosen items were recommended.`;
|
|
5560
|
+
}
|
|
5561
|
+
return "";
|
|
5562
|
+
}
|
|
5563
|
+
function buildLatestStateReminder(toolResultPreview) {
|
|
5564
|
+
const text = toolResultPreview.trim();
|
|
5565
|
+
if (!text) return "";
|
|
5566
|
+
const stateMatch = text.match(
|
|
5567
|
+
/\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
|
|
5568
|
+
);
|
|
5569
|
+
if (stateMatch) {
|
|
5570
|
+
const url = stateMatch[1]?.trim();
|
|
5571
|
+
const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
|
|
5572
|
+
if (url) {
|
|
5573
|
+
return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
|
|
5577
|
+
const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
|
|
5578
|
+
if (structuredUrl) {
|
|
5579
|
+
return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
|
|
5580
|
+
}
|
|
5581
|
+
const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i)?.[1]?.trim();
|
|
5582
|
+
const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
|
|
5583
|
+
if (navigatedUrl) {
|
|
5584
|
+
return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
|
|
5585
|
+
}
|
|
5586
|
+
return "";
|
|
5587
|
+
}
|
|
5588
|
+
function shouldRecoverCompactStall(text, userMessage) {
|
|
5589
|
+
const trimmed = text.trim().toLowerCase();
|
|
5590
|
+
if (!trimmed) return true;
|
|
5591
|
+
if (trimmed.length <= 160 && trimmed.includes("?")) return true;
|
|
5592
|
+
if (userMessage && buildPhaseReminder(userMessage, text)) {
|
|
5593
|
+
return true;
|
|
5594
|
+
}
|
|
5595
|
+
const completionSignals = [
|
|
5596
|
+
"i found",
|
|
5597
|
+
"i chose",
|
|
5598
|
+
"i selected",
|
|
5599
|
+
"i added",
|
|
5600
|
+
"here are",
|
|
5601
|
+
"these are",
|
|
5602
|
+
"recommendations",
|
|
5603
|
+
"reasoning",
|
|
5604
|
+
"why i chose",
|
|
5605
|
+
"added them to the cart"
|
|
5606
|
+
];
|
|
5607
|
+
if (completionSignals.some((pattern) => trimmed.includes(pattern))) {
|
|
5608
|
+
return false;
|
|
5609
|
+
}
|
|
5610
|
+
return [
|
|
5611
|
+
"what are you hoping",
|
|
5612
|
+
"what would you like",
|
|
5613
|
+
"how can i help",
|
|
5614
|
+
"let me know",
|
|
5615
|
+
"are you looking for",
|
|
5616
|
+
"just browsing",
|
|
5617
|
+
"i need to",
|
|
5618
|
+
"i will",
|
|
5619
|
+
"i'll",
|
|
5620
|
+
"since i cannot see",
|
|
5621
|
+
"since i can't see",
|
|
5622
|
+
"cannot see the current page",
|
|
5623
|
+
"scroll down to",
|
|
5624
|
+
"load more results",
|
|
5625
|
+
"as placeholders",
|
|
5626
|
+
"would you like me to proceed",
|
|
5627
|
+
"action:",
|
|
5628
|
+
"one moment",
|
|
5629
|
+
"i will now navigate",
|
|
5630
|
+
"navigating to ",
|
|
5631
|
+
"this will take me",
|
|
5632
|
+
"i will use the browser"
|
|
5633
|
+
].some((pattern) => trimmed.includes(pattern));
|
|
5634
|
+
}
|
|
5635
|
+
function shouldRetryCompactToolLoop(profile, text, hasToolHistory, userMessage) {
|
|
5636
|
+
return profile === "compact" && hasToolHistory && shouldRecoverCompactStall(text, userMessage);
|
|
5637
|
+
}
|
|
5638
|
+
function stableToolSignature(name, args) {
|
|
5639
|
+
const canonicalArgs = canonicalizeArgsForTool(name, args);
|
|
5640
|
+
const sortedEntries = Object.entries(canonicalArgs).sort(
|
|
5641
|
+
([left], [right]) => left.localeCompare(right)
|
|
5642
|
+
);
|
|
5643
|
+
return JSON.stringify([name, sortedEntries]);
|
|
5644
|
+
}
|
|
5645
|
+
function hasRecentDuplicateToolCall(recentToolSignatures, signature) {
|
|
5646
|
+
return recentToolSignatures.includes(signature);
|
|
5647
|
+
}
|
|
5648
|
+
function normalizeToolToken(value) {
|
|
5649
|
+
return value.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
|
|
5650
|
+
}
|
|
5651
|
+
function canonicalizeUrlLike(value) {
|
|
5652
|
+
try {
|
|
5653
|
+
const url = new URL(value.trim());
|
|
5654
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
5655
|
+
url.hostname = url.hostname.replace(/^www\./, "");
|
|
5656
|
+
url.hash = "";
|
|
5657
|
+
if (url.pathname.endsWith("/") && url.pathname !== "/") {
|
|
5658
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
5659
|
+
}
|
|
5660
|
+
return url.toString();
|
|
5661
|
+
}
|
|
5662
|
+
} catch {
|
|
5663
|
+
}
|
|
5664
|
+
return value.trim();
|
|
5665
|
+
}
|
|
5666
|
+
function coerceToolArgsForExecution(name, args) {
|
|
5667
|
+
const coerced = { ...args };
|
|
5668
|
+
if (name === "search") {
|
|
5669
|
+
if (typeof coerced.query !== "string" || !coerced.query.trim()) {
|
|
5670
|
+
if (typeof coerced.text === "string" && coerced.text.trim()) {
|
|
5671
|
+
coerced.query = coerced.text.trim();
|
|
5672
|
+
} else if (typeof coerced.term === "string" && coerced.term.trim()) {
|
|
5673
|
+
coerced.query = coerced.term.trim();
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
if (name === "navigate") {
|
|
5678
|
+
if (typeof coerced.url !== "string" || !coerced.url.trim()) {
|
|
5679
|
+
if (typeof coerced.href === "string" && coerced.href.trim()) {
|
|
5680
|
+
coerced.url = coerced.href.trim();
|
|
5681
|
+
} else if (typeof coerced.link === "string" && coerced.link.trim()) {
|
|
5682
|
+
coerced.url = coerced.link.trim();
|
|
5683
|
+
} else if (typeof coerced.text === "string" && /^https?:\/\//i.test(coerced.text.trim())) {
|
|
5684
|
+
coerced.url = coerced.text.trim();
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
return coerced;
|
|
5689
|
+
}
|
|
5690
|
+
function canonicalizeArgsForTool(name, args) {
|
|
5691
|
+
const canonical = coerceToolArgsForExecution(name, args);
|
|
5692
|
+
if (typeof canonical.url === "string") {
|
|
5693
|
+
canonical.url = canonicalizeUrlLike(canonical.url);
|
|
5694
|
+
}
|
|
5695
|
+
if (typeof canonical.query === "string") {
|
|
5696
|
+
canonical.query = canonical.query.trim().replace(/\s+/g, " ").toLowerCase();
|
|
5697
|
+
delete canonical.text;
|
|
5698
|
+
}
|
|
5699
|
+
if (typeof canonical.text === "string") {
|
|
5700
|
+
canonical.text = canonical.text.trim().replace(/\s+/g, " ");
|
|
5701
|
+
}
|
|
5702
|
+
return canonical;
|
|
5703
|
+
}
|
|
5704
|
+
function resolveToolCallName(rawName, args, availableToolNames) {
|
|
5705
|
+
const aliased = normalizeToolAlias(rawName);
|
|
5706
|
+
if (availableToolNames.has(aliased)) return aliased;
|
|
5707
|
+
const normalized = normalizeToolToken(rawName);
|
|
5708
|
+
if (availableToolNames.has(normalized)) return normalized;
|
|
5709
|
+
const hasUrl = typeof args.url === "string" && args.url.trim().length > 0;
|
|
5710
|
+
if (availableToolNames.has("navigate") && (hasUrl || /goto|navigate|open|visit|browser|url|link/.test(normalized))) {
|
|
5711
|
+
return "navigate";
|
|
5712
|
+
}
|
|
5713
|
+
if (availableToolNames.has("search") && (/search|find|lookup|query/.test(normalized) || normalized === "google" || normalized.startsWith("google_"))) {
|
|
5714
|
+
return "search";
|
|
5715
|
+
}
|
|
5716
|
+
if (availableToolNames.has("scroll") && /scroll|page_?down|page_?up/.test(normalized)) {
|
|
5717
|
+
return "scroll";
|
|
5718
|
+
}
|
|
5719
|
+
if (availableToolNames.has("read_page") && /read|scan|inspect|analy[sz]e|summari[sz]e/.test(normalized)) {
|
|
5720
|
+
return "read_page";
|
|
5721
|
+
}
|
|
5722
|
+
return aliased;
|
|
5723
|
+
}
|
|
5724
|
+
function logAgentLoopDebug(payload) {
|
|
5725
|
+
if (!shouldDebugAgentLoop()) return;
|
|
5726
|
+
try {
|
|
5727
|
+
console.log(`[Vessel agent-debug] ${JSON.stringify(payload)}`);
|
|
5728
|
+
} catch (err) {
|
|
5729
|
+
console.warn("[Vessel agent-debug] Failed to serialize debug payload:", err);
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
function recoverTextEncodedToolCalls(text, availableToolNames) {
|
|
5733
|
+
const trimmed = text.trim();
|
|
5734
|
+
if (!trimmed) return [];
|
|
5735
|
+
const candidates = trimmed.match(
|
|
5736
|
+
/([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*?\})(?=\s*$|\n{2,}|[A-Za-z0-9._ -]+\s*\[ARGS\])/g
|
|
5737
|
+
);
|
|
5738
|
+
if (!candidates || candidates.length === 0) return [];
|
|
5739
|
+
const recovered = [];
|
|
5740
|
+
for (const candidate of candidates) {
|
|
5741
|
+
const match = candidate.match(
|
|
5742
|
+
/^\s*([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*\})\s*$/
|
|
5743
|
+
);
|
|
5744
|
+
if (!match) continue;
|
|
5745
|
+
const rawName = match[1] ?? "";
|
|
5746
|
+
const argsJson = match[2] ?? "{}";
|
|
5747
|
+
let parsedArgs = {};
|
|
5748
|
+
try {
|
|
5749
|
+
parsedArgs = JSON.parse(argsJson);
|
|
5750
|
+
} catch {
|
|
5751
|
+
continue;
|
|
5752
|
+
}
|
|
5753
|
+
const resolvedName = resolveToolCallName(
|
|
5754
|
+
rawName,
|
|
5755
|
+
parsedArgs,
|
|
5756
|
+
availableToolNames
|
|
5757
|
+
);
|
|
5758
|
+
recovered.push({
|
|
5759
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5760
|
+
name: resolvedName,
|
|
5761
|
+
argsJson
|
|
5762
|
+
});
|
|
5763
|
+
}
|
|
5764
|
+
return recovered;
|
|
5765
|
+
}
|
|
5766
|
+
function recoverNarratedActionToolCalls(text, availableToolNames) {
|
|
5767
|
+
const trimmed = text.trim();
|
|
5768
|
+
if (!trimmed) return [];
|
|
5769
|
+
const recovered = [];
|
|
5770
|
+
const actionLines = trimmed.match(/^action:\s+.+$/gim) ?? [];
|
|
5771
|
+
for (const rawLine of actionLines) {
|
|
5772
|
+
const line = rawLine.replace(/^action:\s*/i, "").trim();
|
|
5773
|
+
if (!line) continue;
|
|
5774
|
+
const quotedValue = line.match(/"([^"]+)"/)?.[1]?.trim() ?? line.match(/'([^']+)'/)?.[1]?.trim() ?? "";
|
|
5775
|
+
const navigateMatch = line.match(
|
|
5776
|
+
/\b(?:navigate|open|go)\b(?:\s+(?:to|the url))?\s+(https?:\/\/[^\s)]+)\.?/i
|
|
5777
|
+
);
|
|
5778
|
+
if (navigateMatch?.[1]) {
|
|
5779
|
+
const argsJson = JSON.stringify({ url: navigateMatch[1].replace(/\.$/, "") });
|
|
5780
|
+
recovered.push({
|
|
5781
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5782
|
+
name: resolveToolCallName("navigate", { url: navigateMatch[1] }, availableToolNames),
|
|
5783
|
+
argsJson
|
|
5784
|
+
});
|
|
5785
|
+
continue;
|
|
5786
|
+
}
|
|
5787
|
+
const isSearchAction = /\bsearch\b/i.test(line) || /\btype\b/i.test(line) && /\bsearch box\b/i.test(line);
|
|
5788
|
+
if (isSearchAction && quotedValue && availableToolNames.has("search")) {
|
|
5789
|
+
recovered.push({
|
|
5790
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5791
|
+
name: "search",
|
|
5792
|
+
argsJson: JSON.stringify({ query: quotedValue })
|
|
5793
|
+
});
|
|
5794
|
+
continue;
|
|
5795
|
+
}
|
|
5796
|
+
if (/\b(?:read|scan)\b.*\bpage\b/i.test(line) && availableToolNames.has("read_page")) {
|
|
5797
|
+
recovered.push({
|
|
5798
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5799
|
+
name: "read_page",
|
|
5800
|
+
argsJson: JSON.stringify({ mode: "visible_only" })
|
|
5801
|
+
});
|
|
5802
|
+
continue;
|
|
5803
|
+
}
|
|
5804
|
+
const toolRefMatch = line.match(
|
|
5805
|
+
/\b(?:use|call)\s+([a-z_][a-z0-9_]*)(?:\s+tool)?\b/i
|
|
5806
|
+
);
|
|
5807
|
+
if (toolRefMatch?.[1]) {
|
|
5808
|
+
const toolName = resolveToolCallName(toolRefMatch[1], {}, availableToolNames);
|
|
5809
|
+
recovered.push({
|
|
5810
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5811
|
+
name: toolName,
|
|
5812
|
+
argsJson: "{}"
|
|
5813
|
+
});
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5816
|
+
return recovered;
|
|
5817
|
+
}
|
|
5437
5818
|
class OpenAICompatProvider {
|
|
5819
|
+
agentToolProfile;
|
|
5438
5820
|
client;
|
|
5439
5821
|
model;
|
|
5440
5822
|
abortController = null;
|
|
@@ -5446,6 +5828,7 @@ class OpenAICompatProvider {
|
|
|
5446
5828
|
baseURL
|
|
5447
5829
|
});
|
|
5448
5830
|
this.model = config.model || meta?.defaultModel || "gpt-4o";
|
|
5831
|
+
this.agentToolProfile = resolveAgentToolProfile(config);
|
|
5449
5832
|
}
|
|
5450
5833
|
async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
|
|
5451
5834
|
this.abortController = new AbortController();
|
|
@@ -5483,6 +5866,7 @@ class OpenAICompatProvider {
|
|
|
5483
5866
|
async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
|
|
5484
5867
|
this.abortController = new AbortController();
|
|
5485
5868
|
const openAITools = toOpenAITools(tools);
|
|
5869
|
+
const availableToolNames = new Set(tools.map((tool) => tool.name));
|
|
5486
5870
|
const messages = [
|
|
5487
5871
|
{ role: "system", content: systemPrompt },
|
|
5488
5872
|
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
@@ -5491,18 +5875,28 @@ class OpenAICompatProvider {
|
|
|
5491
5875
|
try {
|
|
5492
5876
|
const maxIterations = getEffectiveMaxIterations();
|
|
5493
5877
|
let iterationsUsed = 0;
|
|
5878
|
+
let compactRecoveryCount = 0;
|
|
5879
|
+
let compactCorrectionCount = 0;
|
|
5880
|
+
const recentCompactToolSignatures = [];
|
|
5494
5881
|
for (let i = 0; i < maxIterations; i++) {
|
|
5495
5882
|
iterationsUsed = i + 1;
|
|
5496
5883
|
let textAccum = "";
|
|
5497
5884
|
const toolCallAccums = {};
|
|
5498
5885
|
let finishReason = null;
|
|
5886
|
+
const hasToolHistory = messages.some((message) => message.role === "tool");
|
|
5887
|
+
const priorToolMessages = messages.filter(
|
|
5888
|
+
(message) => message.role === "tool"
|
|
5889
|
+
);
|
|
5890
|
+
const latestToolMessage = priorToolMessages.length > 0 ? priorToolMessages[priorToolMessages.length - 1] : null;
|
|
5891
|
+
const debugRoundLabel = hasToolHistory ? "post_tool" : "initial";
|
|
5499
5892
|
const stream = await this.client.chat.completions.create(
|
|
5500
5893
|
{
|
|
5501
5894
|
model: this.model,
|
|
5502
5895
|
stream: true,
|
|
5503
5896
|
messages,
|
|
5504
5897
|
tools: openAITools,
|
|
5505
|
-
tool_choice: "auto"
|
|
5898
|
+
tool_choice: "auto",
|
|
5899
|
+
temperature: agentTemperatureForProfile(this.agentToolProfile)
|
|
5506
5900
|
},
|
|
5507
5901
|
{ signal: this.abortController.signal }
|
|
5508
5902
|
);
|
|
@@ -5527,10 +5921,50 @@ class OpenAICompatProvider {
|
|
|
5527
5921
|
}
|
|
5528
5922
|
}
|
|
5529
5923
|
}
|
|
5530
|
-
|
|
5924
|
+
let toolCalls = Object.values(toolCallAccums);
|
|
5531
5925
|
for (const tc of Object.values(toolCallAccums)) {
|
|
5532
5926
|
if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5927
|
+
let parsedArgs = {};
|
|
5928
|
+
try {
|
|
5929
|
+
parsedArgs = JSON.parse(tc.argsJson || "{}");
|
|
5930
|
+
} catch {
|
|
5931
|
+
parsedArgs = {};
|
|
5932
|
+
}
|
|
5933
|
+
tc.name = resolveToolCallName(tc.name, parsedArgs, availableToolNames);
|
|
5533
5934
|
}
|
|
5935
|
+
if (toolCalls.length === 0) {
|
|
5936
|
+
const recoveredToolCalls = recoverTextEncodedToolCalls(
|
|
5937
|
+
textAccum,
|
|
5938
|
+
availableToolNames
|
|
5939
|
+
);
|
|
5940
|
+
if (recoveredToolCalls.length > 0) {
|
|
5941
|
+
toolCalls = recoveredToolCalls;
|
|
5942
|
+
} else {
|
|
5943
|
+
const narratedToolCalls = recoverNarratedActionToolCalls(
|
|
5944
|
+
textAccum,
|
|
5945
|
+
availableToolNames
|
|
5946
|
+
);
|
|
5947
|
+
if (narratedToolCalls.length > 0) {
|
|
5948
|
+
toolCalls = narratedToolCalls;
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
logAgentLoopDebug({
|
|
5953
|
+
model: this.model,
|
|
5954
|
+
profile: this.agentToolProfile,
|
|
5955
|
+
iteration: i + 1,
|
|
5956
|
+
round: debugRoundLabel,
|
|
5957
|
+
priorToolCount: priorToolMessages.length,
|
|
5958
|
+
latestToolResultPreview: latestToolMessage ? previewToolDebugContent(String(latestToolMessage.content || "")) : null,
|
|
5959
|
+
finishReason,
|
|
5960
|
+
streamedText: previewDebugValue(textAccum),
|
|
5961
|
+
recoveredFromText: Object.keys(toolCallAccums).length === 0 && toolCalls.length > 0,
|
|
5962
|
+
toolCalls: toolCalls.map((tc) => ({
|
|
5963
|
+
id: tc.id,
|
|
5964
|
+
name: tc.name,
|
|
5965
|
+
argsJson: previewDebugValue(tc.argsJson || "{}", 300)
|
|
5966
|
+
}))
|
|
5967
|
+
});
|
|
5534
5968
|
const malformedToolCalls = /* @__PURE__ */ new Set();
|
|
5535
5969
|
for (const tc of toolCalls) {
|
|
5536
5970
|
try {
|
|
@@ -5552,7 +5986,28 @@ class OpenAICompatProvider {
|
|
|
5552
5986
|
}
|
|
5553
5987
|
};
|
|
5554
5988
|
messages.push(assistantMsg);
|
|
5555
|
-
if (toolCalls.length === 0)
|
|
5989
|
+
if (toolCalls.length === 0) {
|
|
5990
|
+
if (compactRecoveryCount < 2 && shouldRetryCompactToolLoop(
|
|
5991
|
+
this.agentToolProfile,
|
|
5992
|
+
textAccum,
|
|
5993
|
+
hasToolHistory,
|
|
5994
|
+
userMessage
|
|
5995
|
+
)) {
|
|
5996
|
+
compactRecoveryCount += 1;
|
|
5997
|
+
messages.push({
|
|
5998
|
+
role: "system",
|
|
5999
|
+
content: buildCompactRecoveryPrompt(
|
|
6000
|
+
userMessage,
|
|
6001
|
+
textAccum,
|
|
6002
|
+
latestToolMessage ? String(latestToolMessage.content || "") : null
|
|
6003
|
+
)
|
|
6004
|
+
});
|
|
6005
|
+
continue;
|
|
6006
|
+
}
|
|
6007
|
+
break;
|
|
6008
|
+
}
|
|
6009
|
+
compactRecoveryCount = 0;
|
|
6010
|
+
const iterationToolResultPreviews = [];
|
|
5556
6011
|
for (const tc of toolCalls) {
|
|
5557
6012
|
if (malformedToolCalls.has(tc.id)) {
|
|
5558
6013
|
onChunk(`
|
|
@@ -5579,7 +6034,48 @@ class OpenAICompatProvider {
|
|
|
5579
6034
|
});
|
|
5580
6035
|
continue;
|
|
5581
6036
|
}
|
|
5582
|
-
|
|
6037
|
+
args = coerceToolArgsForExecution(tc.name, args);
|
|
6038
|
+
if (!availableToolNames.has(tc.name)) {
|
|
6039
|
+
onChunk(`
|
|
6040
|
+
<<tool:unsupported_tool:⚠ unsupported>>
|
|
6041
|
+
`);
|
|
6042
|
+
messages.push({
|
|
6043
|
+
role: "tool",
|
|
6044
|
+
tool_call_id: tc.id,
|
|
6045
|
+
content: `Error: ${tc.name} is not a supported tool. Choose one of the available browser tools instead.`
|
|
6046
|
+
});
|
|
6047
|
+
compactCorrectionCount += 1;
|
|
6048
|
+
if (compactCorrectionCount >= 2) {
|
|
6049
|
+
messages.push({
|
|
6050
|
+
role: "system",
|
|
6051
|
+
content: `You are calling unsupported tools. Stop inventing tool names. Use the supported tools you were given and take the next concrete step.`
|
|
6052
|
+
});
|
|
6053
|
+
}
|
|
6054
|
+
continue;
|
|
6055
|
+
}
|
|
6056
|
+
const toolSignature = stableToolSignature(tc.name, args);
|
|
6057
|
+
if (this.agentToolProfile === "compact" && hasRecentDuplicateToolCall(
|
|
6058
|
+
recentCompactToolSignatures,
|
|
6059
|
+
toolSignature
|
|
6060
|
+
)) {
|
|
6061
|
+
onChunk(`
|
|
6062
|
+
<<tool:${tc.name}:↻ duplicate suppressed>>
|
|
6063
|
+
`);
|
|
6064
|
+
messages.push({
|
|
6065
|
+
role: "tool",
|
|
6066
|
+
tool_call_id: tc.id,
|
|
6067
|
+
content: `Error: Repeated the same tool call (${tc.name}) with the same arguments twice in a row. Do not repeat it. Continue with the next logical step for the original task.`
|
|
6068
|
+
});
|
|
6069
|
+
compactCorrectionCount += 1;
|
|
6070
|
+
if (compactCorrectionCount >= 2) {
|
|
6071
|
+
messages.push({
|
|
6072
|
+
role: "system",
|
|
6073
|
+
content: `You are stuck repeating the same action. Stop repeating navigate/search. Use a different supported tool that advances the task, such as click, read_page, or scroll.`
|
|
6074
|
+
});
|
|
6075
|
+
}
|
|
6076
|
+
continue;
|
|
6077
|
+
}
|
|
6078
|
+
const argSummary = args.url || args.query || args.text || args.direction || "";
|
|
5583
6079
|
onChunk(`
|
|
5584
6080
|
<<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
|
|
5585
6081
|
`);
|
|
@@ -5598,12 +6094,29 @@ class OpenAICompatProvider {
|
|
|
5598
6094
|
}
|
|
5599
6095
|
} catch {
|
|
5600
6096
|
}
|
|
6097
|
+
if (this.agentToolProfile === "compact") {
|
|
6098
|
+
recentCompactToolSignatures.push(toolSignature);
|
|
6099
|
+
if (recentCompactToolSignatures.length > 4) {
|
|
6100
|
+
recentCompactToolSignatures.shift();
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
compactCorrectionCount = 0;
|
|
6104
|
+
iterationToolResultPreviews.push(toolContent);
|
|
5601
6105
|
messages.push({
|
|
5602
6106
|
role: "tool",
|
|
5603
6107
|
tool_call_id: tc.id,
|
|
5604
6108
|
content: toolContent
|
|
5605
6109
|
});
|
|
5606
6110
|
}
|
|
6111
|
+
const followUpReminder = followUpReminderForProfile(
|
|
6112
|
+
this.agentToolProfile,
|
|
6113
|
+
userMessage,
|
|
6114
|
+
textAccum,
|
|
6115
|
+
iterationToolResultPreviews.length > 0 ? iterationToolResultPreviews[iterationToolResultPreviews.length - 1] : null
|
|
6116
|
+
);
|
|
6117
|
+
if (followUpReminder) {
|
|
6118
|
+
messages.push(followUpReminder);
|
|
6119
|
+
}
|
|
5607
6120
|
}
|
|
5608
6121
|
if (iterationsUsed >= maxIterations) {
|
|
5609
6122
|
onChunk(`
|
|
@@ -7239,6 +7752,103 @@ function buildGeneralPrompt(query) {
|
|
|
7239
7752
|
user: query
|
|
7240
7753
|
};
|
|
7241
7754
|
}
|
|
7755
|
+
const SHARED_CORE_INSTRUCTIONS = [
|
|
7756
|
+
"You can see the page the user is viewing. The content above is from the page.",
|
|
7757
|
+
"The structured page context always refers to the tab currently visible to the human unless a later tool call changes tabs.",
|
|
7758
|
+
"Use tools to interact with the page when asked to do something (navigate, click, type, select options, submit forms, press keys, scroll).",
|
|
7759
|
+
"Only say you completed an action after the corresponding tool succeeds. If no tool supports the request, say so plainly.",
|
|
7760
|
+
"Call one tool at a time unless you are certain your provider supports parallel tool calls. Sequential calls are more reliable.",
|
|
7761
|
+
"ACT, DON'T HEDGE: You have a full browser. If the user asks you to go somewhere and do something, start doing it immediately."
|
|
7762
|
+
];
|
|
7763
|
+
const SHARED_NAVIGATION_INSTRUCTIONS = [
|
|
7764
|
+
"Use current_tab when you only need to know what the human is currently looking at. Use list_tabs before switching context across multiple tabs.",
|
|
7765
|
+
"Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.",
|
|
7766
|
+
"After navigating to a new site, do not call read_page immediately unless you are genuinely stuck. Prefer the site's search box, known navigation patterns, or clicking a visible section first.",
|
|
7767
|
+
"On retail and marketplace sites, prefer the site's visible search box, filters, and result pages over direct product URLs.",
|
|
7768
|
+
"For broad discovery tasks, prefer direct sources and site-specific search over generic search engines."
|
|
7769
|
+
];
|
|
7770
|
+
const SHARED_READ_INSTRUCTIONS = [
|
|
7771
|
+
"The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.",
|
|
7772
|
+
"When you only need detail on one result, card, or form section, use inspect_element instead of reading the whole page.",
|
|
7773
|
+
'Escalate page reads progressively: read_page(mode="glance"), then visible_only/results_only/forms_only/summary/text_only as needed. Use read_page(mode="debug") only as a last resort.',
|
|
7774
|
+
'If read_page returns empty or times out, do not retry with the same mode. Switch to read_page(mode="glance") or use screenshot.',
|
|
7775
|
+
"Use screenshot when you need the exact rendered page or text extraction is failing.",
|
|
7776
|
+
"read_page inspects the page without moving the human-visible viewport. If you say you are going to scroll, call scroll or scroll_to_element so the user sees the page move too.",
|
|
7777
|
+
"After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode."
|
|
7778
|
+
];
|
|
7779
|
+
const DEFAULT_EXTRA_INSTRUCTIONS = [
|
|
7780
|
+
"Create a checkpoint before risky multi-step flows or before leaving an important state.",
|
|
7781
|
+
"Use save_session after completing a login flow you may need again later, and load_session to resume that authenticated state in future runs.",
|
|
7782
|
+
"If the user says they highlighted or selected text, use read_page before falling back to screenshots because it includes active selection and visible unsaved highlights.",
|
|
7783
|
+
"If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.",
|
|
7784
|
+
"If the page context reports a rate limit, human verification, or access warning, stop using that page and switch to a different source.",
|
|
7785
|
+
"Reference interactive elements by their index number (shown as [#N] in the listings above).",
|
|
7786
|
+
"Be concise. Explain what you're doing as you go.",
|
|
7787
|
+
"For simple questions about the page, just answer directly without using tools.",
|
|
7788
|
+
"VISUAL AWARENESS: The human is watching the browser alongside this chat. Use highlights proactively when you reference specific on-page findings or errors.",
|
|
7789
|
+
"After completing a task or answering a question, offer 1-2 brief, natural follow-up suggestions that make sense in context.",
|
|
7790
|
+
'MINIMIZE TOOL CALLS: Every tool call takes time and costs a round trip. Be efficient. The fastest path is usually: navigate -> search -> wait_for or read_page(mode="results_only") -> click.',
|
|
7791
|
+
"USE YOUR KNOWLEDGE: When the user asks for recommendations, make a clear recommendation, explain your reasoning briefly, and then execute.",
|
|
7792
|
+
"NEVER USE EMOJIS unless the user uses them first."
|
|
7793
|
+
];
|
|
7794
|
+
const COMPACT_FOCUS_INSTRUCTIONS = [
|
|
7795
|
+
"Trust the latest tool result over the initial page context. If a tool result shows a new URL/title/results page, that is the current truth.",
|
|
7796
|
+
"Do not ask the user for permission to continue a task they already requested.",
|
|
7797
|
+
"Stay on the current task until it is complete. Do not restart completed phases such as re-navigating to the same site or redoing discovery after you already have candidates.",
|
|
7798
|
+
"If you are already on the requested site, do not navigate to its homepage again unless the current page is clearly unusable.",
|
|
7799
|
+
"If search or read_page returns results on the target site, continue from those results. Do not assume the search failed unless the tool result says it failed.",
|
|
7800
|
+
"Use current_tab only if you are genuinely unsure of the current page after reading the latest tool result.",
|
|
7801
|
+
"On retail tasks, prefer this sequence: navigate -> site search or curated section -> inspect/read results -> click a product -> add to cart -> explain.",
|
|
7802
|
+
"Keep your reasoning short. Prefer taking the next tool action over writing a long plan."
|
|
7803
|
+
];
|
|
7804
|
+
function buildInstructionBlock(instructions) {
|
|
7805
|
+
return instructions.map((line) => `- ${line}`).join("\n");
|
|
7806
|
+
}
|
|
7807
|
+
function buildContextBlock(input) {
|
|
7808
|
+
return `You are Vessel, an AI agent embedded in a web browser. You can see the current page and interact with it using tools.
|
|
7809
|
+
|
|
7810
|
+
THE USER IS CURRENTLY LOOKING AT:
|
|
7811
|
+
Title: ${input.activeTabTitle}
|
|
7812
|
+
URL: ${input.activeTabUrl}${input.tabSummary || ""}
|
|
7813
|
+
|
|
7814
|
+
When the user says "this page", "this article", "this site", or asks about what they're viewing, they mean the page above. The context below is from that page.
|
|
7815
|
+
|
|
7816
|
+
Current page context:
|
|
7817
|
+
This brief is intentionally minimal and filtered for speed. It omits most page text and low-value chrome unless you explicitly ask for more.
|
|
7818
|
+
Default brief mode: ${input.defaultReadMode}
|
|
7819
|
+
Detected page type: ${input.pageType}
|
|
7820
|
+
|
|
7821
|
+
${input.structuredContext}
|
|
7822
|
+
|
|
7823
|
+
Supervisor state:
|
|
7824
|
+
- paused: ${input.supervisorPaused ? "yes" : "no"}
|
|
7825
|
+
- approval mode: ${input.approvalMode}
|
|
7826
|
+
- pending approvals: ${input.pendingApprovals}
|
|
7827
|
+
|
|
7828
|
+
Recent checkpoints:
|
|
7829
|
+
${input.recentCheckpoints || "- none"}
|
|
7830
|
+
|
|
7831
|
+
Task tracker:
|
|
7832
|
+
${input.taskTrackerContext || "- none"}`;
|
|
7833
|
+
}
|
|
7834
|
+
function buildAgentSystemPrompt(input) {
|
|
7835
|
+
const instructionBlocks = input.profile === "compact" ? [
|
|
7836
|
+
buildInstructionBlock(SHARED_CORE_INSTRUCTIONS),
|
|
7837
|
+
buildInstructionBlock(SHARED_NAVIGATION_INSTRUCTIONS),
|
|
7838
|
+
buildInstructionBlock(SHARED_READ_INSTRUCTIONS),
|
|
7839
|
+
buildInstructionBlock(COMPACT_FOCUS_INSTRUCTIONS)
|
|
7840
|
+
] : [
|
|
7841
|
+
buildInstructionBlock(SHARED_CORE_INSTRUCTIONS),
|
|
7842
|
+
buildInstructionBlock(SHARED_NAVIGATION_INSTRUCTIONS),
|
|
7843
|
+
buildInstructionBlock(SHARED_READ_INSTRUCTIONS),
|
|
7844
|
+
buildInstructionBlock(DEFAULT_EXTRA_INSTRUCTIONS)
|
|
7845
|
+
];
|
|
7846
|
+
return [
|
|
7847
|
+
buildContextBlock(input),
|
|
7848
|
+
"Instructions:",
|
|
7849
|
+
...instructionBlocks
|
|
7850
|
+
].join("\n\n");
|
|
7851
|
+
}
|
|
7242
7852
|
const WRAPPING_QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
|
|
7243
7853
|
function stripWrappingQuotes(value) {
|
|
7244
7854
|
const trimmed = value.trim();
|
|
@@ -7956,10 +8566,16 @@ const CONTEXT_HINTS = {
|
|
|
7956
8566
|
scroll: "💡 Long content — scroll to continue — "
|
|
7957
8567
|
}
|
|
7958
8568
|
};
|
|
7959
|
-
function scoreForContext(toolName, pageType) {
|
|
8569
|
+
function scoreForContext(toolName, pageType, intents) {
|
|
7960
8570
|
const def = defByName[toolName];
|
|
7961
8571
|
if (!def) return 500;
|
|
7962
8572
|
if (pageType === "SEARCH_READY") {
|
|
8573
|
+
if (intents.has("navigate")) {
|
|
8574
|
+
if (toolName === "navigate") return -30;
|
|
8575
|
+
if (toolName === "search") return 2;
|
|
8576
|
+
if (toolName === "type_text") return 5;
|
|
8577
|
+
if (toolName === "press_key") return 6;
|
|
8578
|
+
}
|
|
7963
8579
|
if (toolName === "search") return -20;
|
|
7964
8580
|
if (toolName === "type_text") return 5;
|
|
7965
8581
|
if (toolName === "press_key") return 6;
|
|
@@ -7993,10 +8609,53 @@ const ALWAYS_FAST_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
7993
8609
|
"screenshot",
|
|
7994
8610
|
"inspect_element"
|
|
7995
8611
|
]);
|
|
8612
|
+
const COMPACT_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
8613
|
+
"navigate",
|
|
8614
|
+
"go_back",
|
|
8615
|
+
"click",
|
|
8616
|
+
"type_text",
|
|
8617
|
+
"press_key",
|
|
8618
|
+
"scroll",
|
|
8619
|
+
"dismiss_popup",
|
|
8620
|
+
"clear_overlays",
|
|
8621
|
+
"accept_cookies",
|
|
8622
|
+
"read_page",
|
|
8623
|
+
"wait_for",
|
|
8624
|
+
"inspect_element",
|
|
8625
|
+
"search"
|
|
8626
|
+
]);
|
|
8627
|
+
const COMPACT_CONTEXTUAL_TOOL_NAMES = {
|
|
8628
|
+
LOGIN: ["fill_form", "submit_form", "login"],
|
|
8629
|
+
FORM: ["fill_form", "select_option", "submit_form"],
|
|
8630
|
+
SHOPPING: ["select_option", "fill_form", "submit_form"],
|
|
8631
|
+
SEARCH_RESULTS: ["paginate", "scroll_to_element"],
|
|
8632
|
+
PAGINATED_LIST: ["paginate", "scroll_to_element"]
|
|
8633
|
+
};
|
|
8634
|
+
const COMPACT_INTENT_TOOL_NAMES = {
|
|
8635
|
+
tabs: ["current_tab", "list_tabs", "switch_tab", "create_tab"],
|
|
8636
|
+
bookmarks: [
|
|
8637
|
+
"list_bookmarks",
|
|
8638
|
+
"search_bookmarks",
|
|
8639
|
+
"create_bookmark_folder",
|
|
8640
|
+
"save_bookmark",
|
|
8641
|
+
"organize_bookmark",
|
|
8642
|
+
"archive_bookmark",
|
|
8643
|
+
"open_bookmark"
|
|
8644
|
+
],
|
|
8645
|
+
sessions: ["login", "save_session", "load_session", "list_sessions", "delete_session"],
|
|
8646
|
+
workflow: ["create_checkpoint", "restore_checkpoint", "flow_start", "flow_advance", "flow_status", "flow_end"],
|
|
8647
|
+
metrics: ["metrics"],
|
|
8648
|
+
highlight: ["highlight", "clear_highlights"],
|
|
8649
|
+
table: ["extract_table"],
|
|
8650
|
+
debug: ["current_tab", "reload", "set_ad_blocking", "suggest", "screenshot"]
|
|
8651
|
+
};
|
|
7996
8652
|
function inferIntent(query) {
|
|
7997
8653
|
const lowered = query.toLowerCase();
|
|
7998
8654
|
const intents = /* @__PURE__ */ new Set();
|
|
7999
8655
|
if (/\b(tab|tabs|window|windows)\b/.test(lowered)) intents.add("tabs");
|
|
8656
|
+
if (/\b(go to|goto|open|visit|navigate to)\b/.test(lowered) || /\b[a-z0-9-]+\.(com|org|net|io|dev|app|ai|co|edu|gov)\b/.test(lowered) || /\bhttps?:\/\//.test(lowered)) {
|
|
8657
|
+
intents.add("navigate");
|
|
8658
|
+
}
|
|
8000
8659
|
if (/\b(bookmark|bookmarks|save this|folder)\b/.test(lowered)) {
|
|
8001
8660
|
intents.add("bookmarks");
|
|
8002
8661
|
}
|
|
@@ -8019,7 +8678,18 @@ function inferIntent(query) {
|
|
|
8019
8678
|
}
|
|
8020
8679
|
return intents;
|
|
8021
8680
|
}
|
|
8022
|
-
function shouldIncludeTool(toolName, pageType, intents) {
|
|
8681
|
+
function shouldIncludeTool(toolName, pageType, intents, profile) {
|
|
8682
|
+
if (profile === "compact") {
|
|
8683
|
+
if (COMPACT_CORE_TOOL_NAMES.has(toolName)) return true;
|
|
8684
|
+
const contextualTools = COMPACT_CONTEXTUAL_TOOL_NAMES[pageType] ?? [];
|
|
8685
|
+
if (contextualTools.includes(toolName)) return true;
|
|
8686
|
+
for (const intent of intents) {
|
|
8687
|
+
if ((COMPACT_INTENT_TOOL_NAMES[intent] ?? []).includes(toolName)) {
|
|
8688
|
+
return true;
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
return false;
|
|
8692
|
+
}
|
|
8023
8693
|
if (ALWAYS_FAST_TOOL_NAMES.has(toolName)) return true;
|
|
8024
8694
|
switch (toolName) {
|
|
8025
8695
|
case "select_option":
|
|
@@ -8076,13 +8746,14 @@ function shouldIncludeTool(toolName, pageType, intents) {
|
|
|
8076
8746
|
return !defByName[toolName]?.hiddenByDefault;
|
|
8077
8747
|
}
|
|
8078
8748
|
}
|
|
8079
|
-
function pruneToolsForContext(tools, pageType, query = "") {
|
|
8749
|
+
function pruneToolsForContext(tools, pageType, query = "", options = {}) {
|
|
8080
8750
|
const ctx = pageType ?? "GENERAL";
|
|
8081
8751
|
const hints = CONTEXT_HINTS[ctx] ?? {};
|
|
8082
8752
|
const intents = inferIntent(query);
|
|
8083
|
-
const
|
|
8753
|
+
const profile = options.profile ?? "default";
|
|
8754
|
+
const scored = tools.filter((tool) => shouldIncludeTool(tool.name, ctx, intents, profile)).map((tool) => ({
|
|
8084
8755
|
tool,
|
|
8085
|
-
score: scoreForContext(tool.name, ctx)
|
|
8756
|
+
score: scoreForContext(tool.name, ctx, intents)
|
|
8086
8757
|
}));
|
|
8087
8758
|
scored.sort((a, b) => a.score - b.score);
|
|
8088
8759
|
return scored.map(({ tool, score }) => {
|
|
@@ -8746,6 +9417,87 @@ async function captureScreenshot(wc) {
|
|
|
8746
9417
|
}
|
|
8747
9418
|
return { ok: false, error: "Page image was empty after 3 attempts" };
|
|
8748
9419
|
}
|
|
9420
|
+
function normalizeForComparison(value) {
|
|
9421
|
+
return value.toLowerCase().replace(/https?:\/\//g, "").replace(/www\./g, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
9422
|
+
}
|
|
9423
|
+
function canonicalizeUrlForComparison(value) {
|
|
9424
|
+
try {
|
|
9425
|
+
const url = new URL(value);
|
|
9426
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
9427
|
+
url.hostname = url.hostname.replace(/^www\./, "");
|
|
9428
|
+
url.hash = "";
|
|
9429
|
+
if (url.pathname.endsWith("/") && url.pathname !== "/") {
|
|
9430
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
9431
|
+
}
|
|
9432
|
+
return url.toString();
|
|
9433
|
+
} catch {
|
|
9434
|
+
return null;
|
|
9435
|
+
}
|
|
9436
|
+
}
|
|
9437
|
+
function isRedundantNavigateTarget(currentUrl, targetUrl) {
|
|
9438
|
+
const current = canonicalizeUrlForComparison(currentUrl);
|
|
9439
|
+
const target = canonicalizeUrlForComparison(targetUrl);
|
|
9440
|
+
return current !== null && target !== null && current === target;
|
|
9441
|
+
}
|
|
9442
|
+
function looksLikeCurrentSiteNameQuery(query, currentUrl, currentTitle) {
|
|
9443
|
+
const normalizedQuery = normalizeForComparison(query);
|
|
9444
|
+
if (!normalizedQuery) return false;
|
|
9445
|
+
let hostnameLabel = "";
|
|
9446
|
+
try {
|
|
9447
|
+
const url = new URL(currentUrl);
|
|
9448
|
+
hostnameLabel = url.hostname.replace(/^www\./, "").split(".")[0] || "";
|
|
9449
|
+
} catch {
|
|
9450
|
+
}
|
|
9451
|
+
const normalizedTitle = normalizeForComparison(currentTitle);
|
|
9452
|
+
const normalizedHost = normalizeForComparison(hostnameLabel);
|
|
9453
|
+
const normalizedTitlePrefix = normalizeForComparison(
|
|
9454
|
+
currentTitle.split("|")[0]?.split("—")[0]?.split("-")[0] || currentTitle
|
|
9455
|
+
);
|
|
9456
|
+
if (normalizedTitle && normalizedQuery === normalizedTitle) return true;
|
|
9457
|
+
if (normalizedTitlePrefix && normalizedQuery === normalizedTitlePrefix) {
|
|
9458
|
+
return true;
|
|
9459
|
+
}
|
|
9460
|
+
if (normalizedHost && normalizedQuery === normalizedHost) return true;
|
|
9461
|
+
const titleTokens = new Set(normalizedTitle.split(/\s+/).filter(Boolean));
|
|
9462
|
+
const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
|
|
9463
|
+
if (normalizedHost && queryTokens.includes(normalizedHost) && queryTokens.every((token) => titleTokens.has(token) || token === normalizedHost)) {
|
|
9464
|
+
return true;
|
|
9465
|
+
}
|
|
9466
|
+
return false;
|
|
9467
|
+
}
|
|
9468
|
+
function extractExplicitDomains(goal) {
|
|
9469
|
+
const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
|
|
9470
|
+
if (!matches) return [];
|
|
9471
|
+
const normalized = matches.map(
|
|
9472
|
+
(match) => match.replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase()
|
|
9473
|
+
);
|
|
9474
|
+
return [...new Set(normalized)];
|
|
9475
|
+
}
|
|
9476
|
+
function apexDomain(hostname) {
|
|
9477
|
+
const parts = hostname.replace(/^www\./, "").split(".").filter(Boolean);
|
|
9478
|
+
if (parts.length <= 2) return parts.join(".");
|
|
9479
|
+
return parts.slice(-2).join(".");
|
|
9480
|
+
}
|
|
9481
|
+
function shouldBlockOffGoalDomainNavigation(goal, targetUrl) {
|
|
9482
|
+
const explicitDomains = extractExplicitDomains(goal);
|
|
9483
|
+
if (explicitDomains.length !== 1) return null;
|
|
9484
|
+
let targetHost = "";
|
|
9485
|
+
try {
|
|
9486
|
+
const url = new URL(targetUrl);
|
|
9487
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
9488
|
+
targetHost = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
9489
|
+
} catch {
|
|
9490
|
+
return null;
|
|
9491
|
+
}
|
|
9492
|
+
const requestedDomain = explicitDomains[0];
|
|
9493
|
+
if (targetHost === requestedDomain || targetHost.endsWith(`.${requestedDomain}`) || apexDomain(targetHost) === apexDomain(requestedDomain)) {
|
|
9494
|
+
return null;
|
|
9495
|
+
}
|
|
9496
|
+
return {
|
|
9497
|
+
requestedDomain,
|
|
9498
|
+
targetDomain: targetHost
|
|
9499
|
+
};
|
|
9500
|
+
}
|
|
8749
9501
|
const SESSION_VERSION = 1;
|
|
8750
9502
|
function getSessionsDir() {
|
|
8751
9503
|
return path$1.join(electron.app.getPath("userData"), "named-sessions");
|
|
@@ -9358,6 +10110,28 @@ WARNING: Blocking overlay detected (${overlaySignal}). Call clear_overlays or ac
|
|
|
9358
10110
|
}
|
|
9359
10111
|
return titleLine;
|
|
9360
10112
|
}
|
|
10113
|
+
async function getPostSearchSummary(wc) {
|
|
10114
|
+
await waitForLoad$1(wc, 2e3);
|
|
10115
|
+
try {
|
|
10116
|
+
const content = await Promise.race([
|
|
10117
|
+
extractContent(wc),
|
|
10118
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 2500))
|
|
10119
|
+
]);
|
|
10120
|
+
if (content && content.content.length > 0) {
|
|
10121
|
+
const scoped = buildScopedContext(content, "results_only");
|
|
10122
|
+
const truncated = scoped.length > 2600 ? `${scoped.slice(0, 2600)}
|
|
10123
|
+
[Search results snapshot truncated...]` : scoped;
|
|
10124
|
+
return `
|
|
10125
|
+
Search results snapshot:
|
|
10126
|
+
${truncated}`;
|
|
10127
|
+
}
|
|
10128
|
+
} catch {
|
|
10129
|
+
}
|
|
10130
|
+
const fallback = await getPostNavSummary(wc);
|
|
10131
|
+
return fallback ? `${fallback}
|
|
10132
|
+
Search results snapshot unavailable. Use read_page(mode="results_only") if needed.` : `
|
|
10133
|
+
Search results snapshot unavailable. Use read_page(mode="results_only") if needed.`;
|
|
10134
|
+
}
|
|
9361
10135
|
async function scrollPage$1(wc, deltaY) {
|
|
9362
10136
|
const getScrollY = async () => {
|
|
9363
10137
|
const scrollY = await executePageScript(
|
|
@@ -12094,6 +12868,9 @@ async function searchPage(wc, args) {
|
|
|
12094
12868
|
if (buttonLikePatterns.some((p) => queryLower.includes(p))) {
|
|
12095
12869
|
return `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`;
|
|
12096
12870
|
}
|
|
12871
|
+
if (looksLikeCurrentSiteNameQuery(query, wc.getURL(), wc.getTitle() || "")) {
|
|
12872
|
+
return `Error: "${query}" looks like the current site's name, not a product query. You are already on ${wc.getURL()}. Open a section like staff picks/new releases or search for actual book titles, authors, or genres instead.`;
|
|
12873
|
+
}
|
|
12097
12874
|
if (typeof args.selector !== "string") {
|
|
12098
12875
|
const shortcut = buildSearchShortcut(wc.getURL(), query);
|
|
12099
12876
|
if (shortcut) {
|
|
@@ -12104,7 +12881,7 @@ async function searchPage(wc, args) {
|
|
|
12104
12881
|
const afterUrl2 = wc.getURL();
|
|
12105
12882
|
const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
|
|
12106
12883
|
const destination = shortcut.section ? ` ${shortcut.section}` : "";
|
|
12107
|
-
return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}`;
|
|
12884
|
+
return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}${await getPostSearchSummary(wc)}`;
|
|
12108
12885
|
}
|
|
12109
12886
|
}
|
|
12110
12887
|
const searchInfo = await locateSearchTarget(
|
|
@@ -12133,7 +12910,7 @@ async function searchPage(wc, args) {
|
|
|
12133
12910
|
await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
|
|
12134
12911
|
let afterUrl = wc.getURL();
|
|
12135
12912
|
if (afterUrl !== beforeUrl) {
|
|
12136
|
-
return `Searched "${query}" → ${afterUrl}`;
|
|
12913
|
+
return `Searched "${query}" → ${afterUrl}${await getPostSearchSummary(wc)}`;
|
|
12137
12914
|
}
|
|
12138
12915
|
if (searchInfo.submitSelector) {
|
|
12139
12916
|
const clickResult = await clickElementBySelector(wc, searchInfo.submitSelector);
|
|
@@ -12141,11 +12918,11 @@ async function searchPage(wc, args) {
|
|
|
12141
12918
|
await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
|
|
12142
12919
|
afterUrl = wc.getURL();
|
|
12143
12920
|
if (afterUrl !== beforeUrl) {
|
|
12144
|
-
return `Searched "${query}" (via search button) → ${afterUrl}`;
|
|
12921
|
+
return `Searched "${query}" (via search button) → ${afterUrl}${await getPostSearchSummary(wc)}`;
|
|
12145
12922
|
}
|
|
12146
12923
|
}
|
|
12147
12924
|
}
|
|
12148
|
-
return `Searched "${query}" (same page — results may have loaded dynamically
|
|
12925
|
+
return `Searched "${query}" (same page — results may have loaded dynamically)${await getPostSearchSummary(wc)}`;
|
|
12149
12926
|
}
|
|
12150
12927
|
async function pressKey$1(wc, args) {
|
|
12151
12928
|
const key = typeof args.key === "string" ? args.key.trim() : "";
|
|
@@ -12307,6 +13084,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
|
12307
13084
|
"wait_for_navigation"
|
|
12308
13085
|
]);
|
|
12309
13086
|
async function executeAction(name, args, ctx) {
|
|
13087
|
+
name = normalizeToolAlias(name);
|
|
12310
13088
|
if (!KNOWN_TOOLS.has(name)) {
|
|
12311
13089
|
for (const known of KNOWN_TOOLS) {
|
|
12312
13090
|
if (name.startsWith(known) && name.length > known.length) {
|
|
@@ -12423,6 +13201,19 @@ async function executeAction(name, args, ctx) {
|
|
|
12423
13201
|
}
|
|
12424
13202
|
case "navigate": {
|
|
12425
13203
|
if (!wc || !tabId) return "Error: No active tab";
|
|
13204
|
+
const taskGoal = ctx.runtime.getState().taskTracker?.goal;
|
|
13205
|
+
if (taskGoal && typeof args.url === "string") {
|
|
13206
|
+
const domainDrift = shouldBlockOffGoalDomainNavigation(
|
|
13207
|
+
taskGoal,
|
|
13208
|
+
args.url
|
|
13209
|
+
);
|
|
13210
|
+
if (domainDrift) {
|
|
13211
|
+
return `Navigation blocked: ${args.url} drifts away from the requested site ${domainDrift.requestedDomain}. Stay on the requested domain and continue the original task there.`;
|
|
13212
|
+
}
|
|
13213
|
+
}
|
|
13214
|
+
if (typeof args.url === "string" && !args.postBody && isRedundantNavigateTarget(wc.getURL(), args.url)) {
|
|
13215
|
+
return `Already on ${wc.getURL()}. Do not navigate to the same URL again. Use click, inspect_element, read_page, or search for actual book terms instead.`;
|
|
13216
|
+
}
|
|
12426
13217
|
const navValidation = await validateLinkDestination(args.url);
|
|
12427
13218
|
if (navValidation.status === "dead") {
|
|
12428
13219
|
return `Navigation blocked: ${args.url} returned ${navValidation.detail || "dead link"}. Try a different URL or go back and choose another link.`;
|
|
@@ -13358,81 +14149,44 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
|
|
|
13358
14149
|
const pageContent = await extractContent(activeWebContents);
|
|
13359
14150
|
const pageType = detectPageType(pageContent);
|
|
13360
14151
|
const defaultReadMode = chooseAgentReadMode(pageContent);
|
|
14152
|
+
if (provider.agentToolProfile === "compact") {
|
|
14153
|
+
runtime2.ensureTaskTracker(query, pageContent.url || activeWebContents.getURL());
|
|
14154
|
+
} else {
|
|
14155
|
+
runtime2.clearTaskTracker();
|
|
14156
|
+
}
|
|
13361
14157
|
const structuredContext = buildScopedContext(
|
|
13362
14158
|
pageContent,
|
|
13363
14159
|
defaultReadMode
|
|
13364
14160
|
);
|
|
13365
14161
|
const runtimeState = runtime2.getState();
|
|
13366
14162
|
const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
|
|
14163
|
+
const taskTrackerContext = runtime2.getTaskTrackerContext();
|
|
13367
14164
|
const activeTabTitle = pageContent.title || "(untitled)";
|
|
13368
14165
|
const activeTabUrl = pageContent.url || activeWebContents.getURL();
|
|
13369
14166
|
const allTabs = tabManager.getAllStates();
|
|
13370
14167
|
const activeTabId = tabManager.getActiveTabId();
|
|
13371
14168
|
const tabSummary = allTabs.length > 1 ? `
|
|
13372
14169
|
All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.title || "New Tab"} (${t.url})`).join(" | ")}` : "";
|
|
13373
|
-
const systemPrompt =
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
Supervisor state:
|
|
13389
|
-
- paused: ${runtimeState.supervisor.paused ? "yes" : "no"}
|
|
13390
|
-
- approval mode: ${runtimeState.supervisor.approvalMode}
|
|
13391
|
-
- pending approvals: ${runtimeState.supervisor.pendingApprovals.length}
|
|
13392
|
-
|
|
13393
|
-
Recent checkpoints:
|
|
13394
|
-
${recentCheckpoints || "- none"}
|
|
13395
|
-
|
|
13396
|
-
Instructions:
|
|
13397
|
-
- You can see the page the user is viewing. The content above is from the page.
|
|
13398
|
-
- The structured page context always refers to the tab currently visible to the human unless a later tool call changes tabs.
|
|
13399
|
-
- Use tools to interact with the page when asked to do something (navigate, click, type, select options, submit forms, press keys, scroll).
|
|
13400
|
-
- Only say you completed an action after the corresponding tool succeeds. If no tool supports the request, say so plainly.
|
|
13401
|
-
- Use current_tab when you only need to know what the human is currently looking at. Use list_tabs before switching context across multiple tabs.
|
|
13402
|
-
- Create a checkpoint before risky multi-step flows or before leaving an important state.
|
|
13403
|
-
- Use save_session after completing a login flow you may need again later, and load_session to resume that authenticated state in future runs.
|
|
13404
|
-
- Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.
|
|
13405
|
-
- After navigating to a new site, DO NOT call read_page immediately. Instead, act on what you already know: use the search tool to search the site, type_text to enter queries in search bars, or click on known navigation patterns. You know what major sites look like — use that knowledge. Only call read_page if you're genuinely stuck and need to discover unfamiliar page structure.
|
|
13406
|
-
- On retail and marketplace sites (like Newegg, Amazon, Walmart, Etsy, eBay), prefer the site's visible search box, filters, and result pages over direct product URLs. Only navigate directly to a product page if the user gave you that URL or the site's own search UI is clearly unavailable after a reasonable attempt.
|
|
13407
|
-
- The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
|
|
13408
|
-
- When you only need detail on one product/result/card/form section, use inspect_element instead of reading the page.
|
|
13409
|
-
- Escalate page reads progressively: read_page(mode="glance") for a fast viewport snapshot on heavy/slow pages, then read_page(mode="visible_only"), read_page(mode="results_only"), read_page(mode="forms_only"), read_page(mode="summary"), or read_page(mode="text_only") depending on what you need.
|
|
13410
|
-
- Use read_page(mode="glance") when a page is slow to load or extraction times out — it shows what's on screen (headings, links, buttons, inputs) without waiting for heavy JS. It's what a human would see by just looking at the page.
|
|
13411
|
-
- Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
|
|
13412
|
-
- If read_page returns empty or times out, do NOT retry with the same mode. Switch to read_page(mode="glance") or use screenshot to see the page visually.
|
|
13413
|
-
- Use screenshot when you need to see exactly what the user sees — visual layout, rendered content, images, or when text extraction is failing. The screenshot returns the actual rendered page image for visual analysis. It works even when the JS thread is completely blocked.
|
|
13414
|
-
- VIEWPORT SYNC: Treat scrolling as a real, user-visible browser action. If you say you are going to scroll, call scroll or scroll_to_element so the human sees the page move too.
|
|
13415
|
-
- read_page inspects the page without moving the human-visible viewport. Do not describe read_page as scrolling. If you want more context without changing the user's view, say you're reading the page; if you want the user to follow along lower on the page, actually scroll first.
|
|
13416
|
-
- After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. Do not jump straight to read_page(mode="debug").
|
|
13417
|
-
- If the user says they highlighted or selected text, use read_page before falling back to screenshots because it includes active selection and visible unsaved highlights.
|
|
13418
|
-
- If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.
|
|
13419
|
-
- For broad discovery tasks, prefer direct sources, official sites, venue directories, and site-specific search over generic search engines, which often rate-limit automated browser traffic.
|
|
13420
|
-
- If the page context reports a rate limit, human verification, or access warning, stop using that page and switch to a different source.
|
|
13421
|
-
- Reference interactive elements by their index number (shown as [#N] in the listings above).
|
|
13422
|
-
- Be concise. Explain what you're doing as you go.
|
|
13423
|
-
- For simple questions about the page, just answer directly without using tools.
|
|
13424
|
-
- VISUAL AWARENESS: The human is watching the browser alongside this chat. Highlights are your pointing finger — they show the user exactly what you're looking at on the page. Use them proactively: highlight key findings, important elements, errors, or anything you're referencing. Don't wait to be asked. If you mention something specific on the page, highlight it. Colors: yellow (default/attention), red (errors/warnings), green (success/good), blue (info/neutral), purple (important/notable), orange (caution). Clear highlights when moving to a new topic or page.
|
|
13425
|
-
- After completing a task or answering a question, offer 1-2 brief, natural follow-up suggestions that make sense in context (e.g. "Want me to highlight any of these?" or "I can save these to a bookmark folder if you'd like"). Keep suggestions short and conversational — don't list every possible action.
|
|
13426
|
-
- Call one tool at a time unless you are certain your provider supports parallel tool calls. Sequential calls are more reliable.
|
|
13427
|
-
- MINIMIZE TOOL CALLS: Every tool call takes time and costs a round trip. Be efficient. Don't use flow_start/flow_advance for simple multi-step tasks — just do the work. Don't call read_page after navigating — use search or type_text directly. Don't retry failed tools with slight variations — if search fails, go straight to type_text + press_key Enter, don't try read_page in between. The fastest path is usually: navigate → search → wait_for or read_page(mode="results_only") → click.
|
|
13428
|
-
- ACT, DON'T HEDGE: You have a full browser — you can navigate to any website, see live content, search, click, add to cart, fill forms, and interact with real pages in real time. Never claim you "don't have access" to a website's inventory, pricing, or content. If the user asks you to go somewhere and do something, start doing it immediately. Don't ask for permission to do what the user just asked you to do — that's redundant and frustrating. Jump straight into action.
|
|
13429
|
-
- USE YOUR KNOWLEDGE: You have broad, practical knowledge about technology, products, cooking, travel, finance, and countless other domains. When the user asks for recommendations, GIVE them — don't deflect to Reddit, YouTubers, or other sources. You know enough to recommend PC parts, suggest restaurants, pick a good laptop, or advise on most consumer decisions. Make a clear recommendation, explain your reasoning briefly, and then execute. If there's genuine ambiguity (e.g. AMD vs Intel is preference-dependent), state your pick and why, then ask only the questions that would actually change your recommendation. Never refuse a recommendation by claiming you're "not an expert" — the user chose to ask you, so help them.
|
|
13430
|
-
- NEVER USE EMOJIS unless the user uses them first.`;
|
|
14170
|
+
const systemPrompt = buildAgentSystemPrompt({
|
|
14171
|
+
profile: provider.agentToolProfile,
|
|
14172
|
+
activeTabTitle,
|
|
14173
|
+
activeTabUrl,
|
|
14174
|
+
tabSummary,
|
|
14175
|
+
defaultReadMode,
|
|
14176
|
+
pageType,
|
|
14177
|
+
structuredContext,
|
|
14178
|
+
supervisorPaused: runtimeState.supervisor.paused,
|
|
14179
|
+
approvalMode: runtimeState.supervisor.approvalMode,
|
|
14180
|
+
pendingApprovals: runtimeState.supervisor.pendingApprovals.length,
|
|
14181
|
+
recentCheckpoints: recentCheckpoints || "- none",
|
|
14182
|
+
taskTrackerContext: taskTrackerContext || "- none"
|
|
14183
|
+
});
|
|
13431
14184
|
const actionCtx = { tabManager, runtime: runtime2 };
|
|
13432
14185
|
const contextualTools = pruneToolsForContext(
|
|
13433
14186
|
AGENT_TOOLS,
|
|
13434
14187
|
pageType,
|
|
13435
|
-
query
|
|
14188
|
+
query,
|
|
14189
|
+
{ profile: provider.agentToolProfile }
|
|
13436
14190
|
);
|
|
13437
14191
|
const trace = createTraceSession(query, activeTabUrl, activeTabTitle);
|
|
13438
14192
|
let accumulatedResponse = "";
|
|
@@ -13450,6 +14204,14 @@ Instructions:
|
|
|
13450
14204
|
let isError = false;
|
|
13451
14205
|
try {
|
|
13452
14206
|
output = await executeAction(name, args, actionCtx);
|
|
14207
|
+
if (provider.agentToolProfile === "compact") {
|
|
14208
|
+
runtime2.updateTaskTracker(name, output);
|
|
14209
|
+
const trackerCtx = runtime2.getTaskTrackerContext();
|
|
14210
|
+
if (trackerCtx) {
|
|
14211
|
+
output = `${output}
|
|
14212
|
+
${trackerCtx}`;
|
|
14213
|
+
}
|
|
14214
|
+
}
|
|
13453
14215
|
} catch (err) {
|
|
13454
14216
|
isError = true;
|
|
13455
14217
|
output = err instanceof Error ? err.message : String(err);
|
|
@@ -19087,11 +19849,21 @@ function stopMcpServer() {
|
|
|
19087
19849
|
});
|
|
19088
19850
|
});
|
|
19089
19851
|
}
|
|
19852
|
+
const VALID_KIT_CATEGORIES = /* @__PURE__ */ new Set([
|
|
19853
|
+
"research",
|
|
19854
|
+
"shopping",
|
|
19855
|
+
"productivity",
|
|
19856
|
+
"forms"
|
|
19857
|
+
]);
|
|
19090
19858
|
const BUNDLED_KIT_IDS = /* @__PURE__ */ new Set([
|
|
19091
19859
|
"research-collect",
|
|
19092
19860
|
"price-scout",
|
|
19093
19861
|
"form-filler"
|
|
19094
19862
|
]);
|
|
19863
|
+
const KIT_ID_UNSAFE_CHAR_PATTERN = /[\/\\\0]/;
|
|
19864
|
+
function isSafeAutomationKitId(id) {
|
|
19865
|
+
return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
|
|
19866
|
+
}
|
|
19095
19867
|
function getUserKitsDir() {
|
|
19096
19868
|
return path$1.join(electron.app.getPath("userData"), "kits");
|
|
19097
19869
|
}
|
|
@@ -19101,10 +19873,16 @@ function ensureKitsDir() {
|
|
|
19101
19873
|
fs$1.mkdirSync(dir, { recursive: true });
|
|
19102
19874
|
}
|
|
19103
19875
|
}
|
|
19876
|
+
function getKitFilePath(id) {
|
|
19877
|
+
if (!isSafeAutomationKitId(id)) return null;
|
|
19878
|
+
const kitsDir = path$1.resolve(getUserKitsDir());
|
|
19879
|
+
const target = path$1.resolve(kitsDir, `${id}.kit.json`);
|
|
19880
|
+
return target.startsWith(`${kitsDir}${path$1.sep}`) ? target : null;
|
|
19881
|
+
}
|
|
19104
19882
|
function isValidKit(value) {
|
|
19105
19883
|
if (!value || typeof value !== "object") return false;
|
|
19106
19884
|
const k = value;
|
|
19107
|
-
return typeof k.id === "string" && k.id
|
|
19885
|
+
return typeof k.id === "string" && isSafeAutomationKitId(k.id) && typeof k.name === "string" && k.name.length > 0 && typeof k.description === "string" && typeof k.category === "string" && VALID_KIT_CATEGORIES.has(k.category) && typeof k.icon === "string" && typeof k.promptTemplate === "string" && k.promptTemplate.length > 0 && Array.isArray(k.inputs);
|
|
19108
19886
|
}
|
|
19109
19887
|
function getInstalledKits() {
|
|
19110
19888
|
ensureKitsDir();
|
|
@@ -19165,7 +19943,10 @@ async function installKitFromFile() {
|
|
|
19165
19943
|
};
|
|
19166
19944
|
}
|
|
19167
19945
|
ensureKitsDir();
|
|
19168
|
-
const dest =
|
|
19946
|
+
const dest = getKitFilePath(parsed.id);
|
|
19947
|
+
if (!dest) {
|
|
19948
|
+
return { ok: false, error: "Kit id contains unsupported characters." };
|
|
19949
|
+
}
|
|
19169
19950
|
try {
|
|
19170
19951
|
fs$1.writeFileSync(dest, JSON.stringify(parsed, null, 2), "utf-8");
|
|
19171
19952
|
} catch {
|
|
@@ -19173,12 +19954,21 @@ async function installKitFromFile() {
|
|
|
19173
19954
|
}
|
|
19174
19955
|
return { ok: true, kit: parsed };
|
|
19175
19956
|
}
|
|
19176
|
-
function uninstallKit(id) {
|
|
19957
|
+
function uninstallKit(id, scheduledKitIds) {
|
|
19177
19958
|
if (BUNDLED_KIT_IDS.has(id)) {
|
|
19178
19959
|
return { ok: false, error: "Built-in kits cannot be removed." };
|
|
19179
19960
|
}
|
|
19961
|
+
if (scheduledKitIds?.has(id)) {
|
|
19962
|
+
return {
|
|
19963
|
+
ok: false,
|
|
19964
|
+
error: "This kit has active scheduled jobs. Delete or reassign them first."
|
|
19965
|
+
};
|
|
19966
|
+
}
|
|
19180
19967
|
ensureKitsDir();
|
|
19181
|
-
const target =
|
|
19968
|
+
const target = getKitFilePath(id);
|
|
19969
|
+
if (!target) {
|
|
19970
|
+
return { ok: false, error: "Kit id contains unsupported characters." };
|
|
19971
|
+
}
|
|
19182
19972
|
if (!fs$1.existsSync(target)) {
|
|
19183
19973
|
return { ok: false, error: "Kit not found." };
|
|
19184
19974
|
}
|
|
@@ -19192,6 +19982,9 @@ function uninstallKit(id) {
|
|
|
19192
19982
|
let jobs = [];
|
|
19193
19983
|
let removeIdleListener = null;
|
|
19194
19984
|
let broadcastFn = null;
|
|
19985
|
+
function getScheduledKitIds() {
|
|
19986
|
+
return new Set(jobs.filter((j) => j.enabled).map((j) => j.kitId));
|
|
19987
|
+
}
|
|
19195
19988
|
function getJobsPath() {
|
|
19196
19989
|
return path$1.join(electron.app.getPath("userData"), "scheduled-jobs.json");
|
|
19197
19990
|
}
|
|
@@ -19361,29 +20154,40 @@ async function fireJob(job, windowState, runtime2) {
|
|
|
19361
20154
|
}
|
|
19362
20155
|
function tick(windowState, runtime2) {
|
|
19363
20156
|
if (isAIStreamActive()) return;
|
|
19364
|
-
const
|
|
19365
|
-
|
|
19366
|
-
|
|
19367
|
-
|
|
19368
|
-
|
|
19369
|
-
if (
|
|
19370
|
-
void fireJob(job, windowState, runtime2).finally(() => {
|
|
20157
|
+
const dueIds = jobs.filter((job) => job.enabled && /* @__PURE__ */ new Date() >= new Date(job.nextRunAt)).map((job) => job.id);
|
|
20158
|
+
if (dueIds.length === 0) return;
|
|
20159
|
+
if (!tryBeginAIStream("scheduled")) return;
|
|
20160
|
+
let idx = 0;
|
|
20161
|
+
const fireNext = () => {
|
|
20162
|
+
if (idx >= dueIds.length) {
|
|
19371
20163
|
endAIStream("scheduled");
|
|
19372
20164
|
queueMicrotask(() => tick(windowState, runtime2));
|
|
19373
|
-
|
|
19374
|
-
|
|
20165
|
+
return;
|
|
20166
|
+
}
|
|
20167
|
+
const jobId = dueIds[idx++];
|
|
20168
|
+
const job = jobs.find((candidate) => candidate.id === jobId);
|
|
20169
|
+
if (!job || !job.enabled) {
|
|
20170
|
+
fireNext();
|
|
20171
|
+
return;
|
|
20172
|
+
}
|
|
20173
|
+
const firedAt = /* @__PURE__ */ new Date();
|
|
20174
|
+
if (firedAt < new Date(job.nextRunAt)) {
|
|
20175
|
+
fireNext();
|
|
20176
|
+
return;
|
|
20177
|
+
}
|
|
20178
|
+
job.lastRunAt = firedAt.toISOString();
|
|
19375
20179
|
if (job.schedule.type === "once") {
|
|
19376
20180
|
job.enabled = false;
|
|
19377
20181
|
} else {
|
|
19378
|
-
job.nextRunAt = computeNextRun(job.schedule,
|
|
20182
|
+
job.nextRunAt = computeNextRun(job.schedule, firedAt).toISOString();
|
|
19379
20183
|
}
|
|
19380
|
-
changed = true;
|
|
19381
|
-
break;
|
|
19382
|
-
}
|
|
19383
|
-
if (changed) {
|
|
19384
20184
|
saveJobs();
|
|
19385
20185
|
broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
|
|
19386
|
-
|
|
20186
|
+
void fireJob(job, windowState, runtime2).catch((err) => {
|
|
20187
|
+
console.warn("[scheduler] Unexpected error firing job:", err);
|
|
20188
|
+
}).finally(fireNext);
|
|
20189
|
+
};
|
|
20190
|
+
fireNext();
|
|
19387
20191
|
}
|
|
19388
20192
|
function registerScheduleHandlers(windowState, runtime2, sendToAll) {
|
|
19389
20193
|
broadcastFn = sendToAll;
|
|
@@ -19646,7 +20450,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
19646
20450
|
Channels.AI_STREAM_CHUNK,
|
|
19647
20451
|
"Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
|
|
19648
20452
|
);
|
|
19649
|
-
sendToRendererViews(Channels.AI_STREAM_END);
|
|
20453
|
+
sendToRendererViews(Channels.AI_STREAM_END, "failed");
|
|
19650
20454
|
return { accepted: true };
|
|
19651
20455
|
}
|
|
19652
20456
|
if (!tryBeginAIStream("manual")) {
|
|
@@ -19663,7 +20467,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
19663
20467
|
activeChatProvider,
|
|
19664
20468
|
activeTab?.view.webContents,
|
|
19665
20469
|
(chunk) => sendToRendererViews(Channels.AI_STREAM_CHUNK, chunk),
|
|
19666
|
-
() => sendToRendererViews(Channels.AI_STREAM_END),
|
|
20470
|
+
() => sendToRendererViews(Channels.AI_STREAM_END, "completed"),
|
|
19667
20471
|
tabManager,
|
|
19668
20472
|
runtime2,
|
|
19669
20473
|
history
|
|
@@ -19672,7 +20476,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
19672
20476
|
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
19673
20477
|
sendToRendererViews(Channels.AI_STREAM_CHUNK, `
|
|
19674
20478
|
[Error: ${msg}]`);
|
|
19675
|
-
sendToRendererViews(Channels.AI_STREAM_END);
|
|
20479
|
+
sendToRendererViews(Channels.AI_STREAM_END, "failed");
|
|
19676
20480
|
} finally {
|
|
19677
20481
|
activeChatProvider = null;
|
|
19678
20482
|
endAIStream("manual");
|
|
@@ -20094,10 +20898,175 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
20094
20898
|
});
|
|
20095
20899
|
electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (_event, id) => {
|
|
20096
20900
|
assertString(id, "id");
|
|
20097
|
-
return uninstallKit(id);
|
|
20901
|
+
return uninstallKit(id, getScheduledKitIds());
|
|
20098
20902
|
});
|
|
20099
20903
|
registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
|
|
20100
20904
|
}
|
|
20905
|
+
function makeStep(label, status = "pending") {
|
|
20906
|
+
return { label, status };
|
|
20907
|
+
}
|
|
20908
|
+
function extractRequestedCount(goal) {
|
|
20909
|
+
const digitMatch = goal.match(/\b(\d+)\b/);
|
|
20910
|
+
if (digitMatch) return Number(digitMatch[1]);
|
|
20911
|
+
const wordMap = {
|
|
20912
|
+
one: 1,
|
|
20913
|
+
two: 2,
|
|
20914
|
+
three: 3,
|
|
20915
|
+
four: 4,
|
|
20916
|
+
five: 5,
|
|
20917
|
+
six: 6,
|
|
20918
|
+
seven: 7,
|
|
20919
|
+
eight: 8,
|
|
20920
|
+
nine: 9,
|
|
20921
|
+
ten: 10
|
|
20922
|
+
};
|
|
20923
|
+
for (const [word, count] of Object.entries(wordMap)) {
|
|
20924
|
+
if (new RegExp(`\\b${word}\\b`, "i").test(goal)) return count;
|
|
20925
|
+
}
|
|
20926
|
+
return null;
|
|
20927
|
+
}
|
|
20928
|
+
function buildInitialSteps(goal) {
|
|
20929
|
+
const lowered = goal.toLowerCase();
|
|
20930
|
+
const requestedCount = extractRequestedCount(goal);
|
|
20931
|
+
const itemLabel = /\b(book|books)\b/.test(lowered) ? "books" : "items";
|
|
20932
|
+
const countLabel = requestedCount ? `${requestedCount} ${itemLabel}` : itemLabel;
|
|
20933
|
+
const steps = [
|
|
20934
|
+
makeStep("Navigate to the requested site", "active")
|
|
20935
|
+
];
|
|
20936
|
+
if (/\b(find|browse|look|discover|select|recommend|interesting)\b/.test(lowered)) {
|
|
20937
|
+
steps.push(makeStep(`Browse or search for relevant ${itemLabel}`));
|
|
20938
|
+
}
|
|
20939
|
+
steps.push(makeStep(`Pick the requested ${countLabel}`));
|
|
20940
|
+
if (/\b(cart|checkout|bag)\b/.test(lowered)) {
|
|
20941
|
+
steps.push(makeStep(`Add the chosen ${itemLabel} to the cart`));
|
|
20942
|
+
}
|
|
20943
|
+
if (/\b(explain|reason|why)\b/.test(lowered)) {
|
|
20944
|
+
steps.push(makeStep("Explain the recommendations"));
|
|
20945
|
+
}
|
|
20946
|
+
return steps;
|
|
20947
|
+
}
|
|
20948
|
+
function setActiveStep(steps, currentStepIndex) {
|
|
20949
|
+
return steps.map((step, index) => {
|
|
20950
|
+
if (step.status === "done" || step.status === "failed") return step;
|
|
20951
|
+
return {
|
|
20952
|
+
...step,
|
|
20953
|
+
status: index === currentStepIndex ? "active" : "pending"
|
|
20954
|
+
};
|
|
20955
|
+
});
|
|
20956
|
+
}
|
|
20957
|
+
function completeStep(state2, detail) {
|
|
20958
|
+
const steps = state2.steps.map((step) => ({ ...step }));
|
|
20959
|
+
const current = steps[state2.currentStepIndex];
|
|
20960
|
+
if (current) {
|
|
20961
|
+
current.status = "done";
|
|
20962
|
+
current.detail = detail || current.detail;
|
|
20963
|
+
}
|
|
20964
|
+
const nextIndex = Math.min(state2.currentStepIndex + 1, steps.length - 1);
|
|
20965
|
+
const normalizedSteps = setActiveStep(
|
|
20966
|
+
steps,
|
|
20967
|
+
current ? nextIndex : state2.currentStepIndex
|
|
20968
|
+
);
|
|
20969
|
+
return {
|
|
20970
|
+
...state2,
|
|
20971
|
+
steps: normalizedSteps,
|
|
20972
|
+
currentStepIndex: current ? nextIndex : state2.currentStepIndex,
|
|
20973
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20974
|
+
};
|
|
20975
|
+
}
|
|
20976
|
+
function setNextHint(state2, nextHint) {
|
|
20977
|
+
return {
|
|
20978
|
+
...state2,
|
|
20979
|
+
nextHint,
|
|
20980
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20981
|
+
};
|
|
20982
|
+
}
|
|
20983
|
+
function normalizeResult(result) {
|
|
20984
|
+
return result.toLowerCase();
|
|
20985
|
+
}
|
|
20986
|
+
function createTaskTracker(goal, startUrl) {
|
|
20987
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20988
|
+
return {
|
|
20989
|
+
goal,
|
|
20990
|
+
startedAt: now,
|
|
20991
|
+
updatedAt: now,
|
|
20992
|
+
startUrl,
|
|
20993
|
+
currentStepIndex: 0,
|
|
20994
|
+
steps: buildInitialSteps(goal),
|
|
20995
|
+
lastAction: void 0,
|
|
20996
|
+
nextHint: "Open a relevant section or search path and keep working the same request."
|
|
20997
|
+
};
|
|
20998
|
+
}
|
|
20999
|
+
function updateTaskTracker(state2, actionName, result) {
|
|
21000
|
+
const loweredResult = normalizeResult(result);
|
|
21001
|
+
let nextState = {
|
|
21002
|
+
...state2,
|
|
21003
|
+
lastAction: actionName,
|
|
21004
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21005
|
+
};
|
|
21006
|
+
const currentLabel = nextState.steps[nextState.currentStepIndex]?.label.toLowerCase() ?? "";
|
|
21007
|
+
if (actionName === "navigate") {
|
|
21008
|
+
nextState = completeStep(nextState, "Reached the requested site.");
|
|
21009
|
+
return setNextHint(
|
|
21010
|
+
nextState,
|
|
21011
|
+
"Open a curated section or use the site's book search to find promising titles."
|
|
21012
|
+
);
|
|
21013
|
+
}
|
|
21014
|
+
const isDiscoveryAction = [
|
|
21015
|
+
"read_page",
|
|
21016
|
+
"search",
|
|
21017
|
+
"click",
|
|
21018
|
+
"inspect_element",
|
|
21019
|
+
"scroll"
|
|
21020
|
+
].includes(actionName);
|
|
21021
|
+
if (isDiscoveryAction && /browse or search/.test(currentLabel)) {
|
|
21022
|
+
nextState = completeStep(nextState, "Found a starting point on the site.");
|
|
21023
|
+
return setNextHint(
|
|
21024
|
+
nextState,
|
|
21025
|
+
"Inspect individual books and keep track of the best candidates until you have the full set."
|
|
21026
|
+
);
|
|
21027
|
+
}
|
|
21028
|
+
if (/pick the requested/.test(currentLabel) && isDiscoveryAction) {
|
|
21029
|
+
return setNextHint(
|
|
21030
|
+
nextState,
|
|
21031
|
+
"Keep selecting candidate books. After you have the requested set, add each one to the cart."
|
|
21032
|
+
);
|
|
21033
|
+
}
|
|
21034
|
+
if (/add the chosen .* to the cart/.test(currentLabel) && /(added to cart|cart confirmation|shopping cart|view cart|checkout)/.test(
|
|
21035
|
+
loweredResult
|
|
21036
|
+
)) {
|
|
21037
|
+
nextState = completeStep(nextState, "Cart interaction succeeded.");
|
|
21038
|
+
return setNextHint(
|
|
21039
|
+
nextState,
|
|
21040
|
+
"Summarize the chosen books and explain why they were recommended."
|
|
21041
|
+
);
|
|
21042
|
+
}
|
|
21043
|
+
if (/explain the recommendations/.test(currentLabel)) {
|
|
21044
|
+
return setNextHint(
|
|
21045
|
+
nextState,
|
|
21046
|
+
"Finish by naming the chosen books and giving concise reasons for each."
|
|
21047
|
+
);
|
|
21048
|
+
}
|
|
21049
|
+
return nextState;
|
|
21050
|
+
}
|
|
21051
|
+
function formatTaskTracker(state2) {
|
|
21052
|
+
if (!state2) return "";
|
|
21053
|
+
const completed = state2.steps.filter((step) => step.status === "done").map((step) => step.label);
|
|
21054
|
+
const current = state2.steps[state2.currentStepIndex]?.label ?? "Task in progress";
|
|
21055
|
+
const remaining = state2.steps.filter((step, index) => step.status !== "done" && index !== state2.currentStepIndex).map((step) => step.label);
|
|
21056
|
+
const lines = [
|
|
21057
|
+
"--- Task Tracker ---",
|
|
21058
|
+
`Goal: ${state2.goal}`,
|
|
21059
|
+
`Completed: ${completed.length > 0 ? completed.join("; ") : "none yet"}`,
|
|
21060
|
+
`Current: ${current}`,
|
|
21061
|
+
`Remaining: ${remaining.length > 0 ? remaining.join("; ") : "wrap up the response"}`
|
|
21062
|
+
];
|
|
21063
|
+
if (state2.nextHint) {
|
|
21064
|
+
lines.push(`Next: ${state2.nextHint}`);
|
|
21065
|
+
}
|
|
21066
|
+
return `
|
|
21067
|
+
${lines.join("\n")}
|
|
21068
|
+
---`;
|
|
21069
|
+
}
|
|
20101
21070
|
const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
|
|
20102
21071
|
const PERSIST_DEBOUNCE_MS = 500;
|
|
20103
21072
|
function clone(value) {
|
|
@@ -20133,7 +21102,8 @@ function sanitizePersistence(persisted) {
|
|
|
20133
21102
|
checkpoints: Array.isArray(persisted?.checkpoints) ? persisted.checkpoints.slice(-20) : [],
|
|
20134
21103
|
transcript: [],
|
|
20135
21104
|
mcpStatus: "stopped",
|
|
20136
|
-
flowState: null
|
|
21105
|
+
flowState: null,
|
|
21106
|
+
taskTracker: null
|
|
20137
21107
|
};
|
|
20138
21108
|
}
|
|
20139
21109
|
class AgentRuntime {
|
|
@@ -20252,6 +21222,32 @@ class AgentRuntime {
|
|
|
20252
21222
|
this.emit();
|
|
20253
21223
|
return this.getState();
|
|
20254
21224
|
}
|
|
21225
|
+
ensureTaskTracker(goal, startUrl) {
|
|
21226
|
+
const trimmedGoal = goal.trim();
|
|
21227
|
+
if (this.state.taskTracker && this.state.taskTracker.goal.trim() === trimmedGoal) {
|
|
21228
|
+
return clone(this.state.taskTracker);
|
|
21229
|
+
}
|
|
21230
|
+
this.state.taskTracker = createTaskTracker(trimmedGoal, startUrl);
|
|
21231
|
+
this.emit();
|
|
21232
|
+
return clone(this.state.taskTracker);
|
|
21233
|
+
}
|
|
21234
|
+
updateTaskTracker(actionName, result) {
|
|
21235
|
+
if (!this.state.taskTracker) return null;
|
|
21236
|
+
this.state.taskTracker = updateTaskTracker(
|
|
21237
|
+
this.state.taskTracker,
|
|
21238
|
+
actionName,
|
|
21239
|
+
result
|
|
21240
|
+
);
|
|
21241
|
+
this.emit();
|
|
21242
|
+
return clone(this.state.taskTracker);
|
|
21243
|
+
}
|
|
21244
|
+
clearTaskTracker() {
|
|
21245
|
+
this.state.taskTracker = null;
|
|
21246
|
+
this.emit();
|
|
21247
|
+
}
|
|
21248
|
+
getTaskTrackerContext() {
|
|
21249
|
+
return formatTaskTracker(this.state.taskTracker);
|
|
21250
|
+
}
|
|
20255
21251
|
// --- Speedee Flow State ---
|
|
20256
21252
|
startFlow(goal, steps, startUrl) {
|
|
20257
21253
|
const flow = {
|