@quanta-intellect/vessel-browser 0.1.33 → 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
- const toolCalls = Object.values(toolCallAccums);
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) break;
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
- const argSummary = args.url || args.text || args.direction || "";
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 scored = tools.filter((tool) => shouldIncludeTool(tool.name, ctx, intents)).map((tool) => ({
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; inspect with read_page(mode="results_only") or read_page(mode="visible_only") before navigating directly elsewhere)`;
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 = `You are Vessel, an AI agent embedded in a web browser. You can see the current page and interact with it using tools.
13374
-
13375
- THE USER IS CURRENTLY LOOKING AT:
13376
- Title: ${activeTabTitle}
13377
- URL: ${activeTabUrl}${tabSummary}
13378
-
13379
- 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 — answer directly without needing to call read_page or current_tab first.
13380
-
13381
- Current page context:
13382
- This brief is intentionally minimal and filtered for speed. It omits most page text and low-value chrome unless you explicitly ask for more.
13383
- Default brief mode: ${defaultReadMode}
13384
- Detected page type: ${pageType}
13385
-
13386
- ${structuredContext}
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);
@@ -20140,6 +20902,171 @@ function registerIpcHandlers(windowState, runtime2) {
20140
20902
  });
20141
20903
  registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
20142
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
+ }
20143
21070
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
20144
21071
  const PERSIST_DEBOUNCE_MS = 500;
20145
21072
  function clone(value) {
@@ -20175,7 +21102,8 @@ function sanitizePersistence(persisted) {
20175
21102
  checkpoints: Array.isArray(persisted?.checkpoints) ? persisted.checkpoints.slice(-20) : [],
20176
21103
  transcript: [],
20177
21104
  mcpStatus: "stopped",
20178
- flowState: null
21105
+ flowState: null,
21106
+ taskTracker: null
20179
21107
  };
20180
21108
  }
20181
21109
  class AgentRuntime {
@@ -20294,6 +21222,32 @@ class AgentRuntime {
20294
21222
  this.emit();
20295
21223
  return this.getState();
20296
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
+ }
20297
21251
  // --- Speedee Flow State ---
20298
21252
  startFlow(goal, steps, startUrl) {
20299
21253
  const flow = {