@quanta-intellect/vessel-browser 0.1.35 → 0.1.44
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/README.md +27 -2
- package/out/main/index.js +2132 -337
- package/out/preload/content-script.js +21 -2
- package/out/renderer/assets/{index-IYqP5OYH.js → index-BC3sJqLj.js} +170 -135
- package/out/renderer/index.html +1 -1
- package/package.json +8 -7
package/out/main/index.js
CHANGED
|
@@ -2067,8 +2067,8 @@ class TabManager {
|
|
|
2067
2067
|
window;
|
|
2068
2068
|
onStateChange;
|
|
2069
2069
|
highlightCaptureCallback = null;
|
|
2070
|
-
constructor(
|
|
2071
|
-
this.window =
|
|
2070
|
+
constructor(window2, onStateChange) {
|
|
2071
|
+
this.window = window2;
|
|
2072
2072
|
this.onStateChange = onStateChange;
|
|
2073
2073
|
}
|
|
2074
2074
|
createTab(url = "about:blank", options) {
|
|
@@ -5126,215 +5126,6 @@ function makeImageResult(base64, description, mediaType = "image/png") {
|
|
|
5126
5126
|
};
|
|
5127
5127
|
return JSON.stringify(result);
|
|
5128
5128
|
}
|
|
5129
|
-
class AnthropicProvider {
|
|
5130
|
-
agentToolProfile = "default";
|
|
5131
|
-
client;
|
|
5132
|
-
model;
|
|
5133
|
-
abortController = null;
|
|
5134
|
-
constructor(apiKey, model) {
|
|
5135
|
-
this.client = new Anthropic({ apiKey });
|
|
5136
|
-
this.model = model || "claude-sonnet-4-20250514";
|
|
5137
|
-
}
|
|
5138
|
-
async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
|
|
5139
|
-
this.abortController = new AbortController();
|
|
5140
|
-
const messages = [
|
|
5141
|
-
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
5142
|
-
{ role: "user", content: userMessage }
|
|
5143
|
-
];
|
|
5144
|
-
try {
|
|
5145
|
-
const stream = this.client.messages.stream(
|
|
5146
|
-
{
|
|
5147
|
-
model: this.model,
|
|
5148
|
-
max_tokens: 4096,
|
|
5149
|
-
system: systemPrompt,
|
|
5150
|
-
messages
|
|
5151
|
-
},
|
|
5152
|
-
{ signal: this.abortController.signal }
|
|
5153
|
-
);
|
|
5154
|
-
for await (const event of stream) {
|
|
5155
|
-
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
5156
|
-
onChunk(event.delta.text);
|
|
5157
|
-
}
|
|
5158
|
-
}
|
|
5159
|
-
} catch (err) {
|
|
5160
|
-
if (err instanceof Error && err.name !== "AbortError") {
|
|
5161
|
-
onChunk(`
|
|
5162
|
-
|
|
5163
|
-
[Error: ${err.message}]`);
|
|
5164
|
-
}
|
|
5165
|
-
} finally {
|
|
5166
|
-
this.abortController = null;
|
|
5167
|
-
onEnd();
|
|
5168
|
-
}
|
|
5169
|
-
}
|
|
5170
|
-
async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
|
|
5171
|
-
this.abortController = new AbortController();
|
|
5172
|
-
const messages = [
|
|
5173
|
-
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
5174
|
-
{ role: "user", content: userMessage }
|
|
5175
|
-
];
|
|
5176
|
-
try {
|
|
5177
|
-
const maxIterations = getEffectiveMaxIterations();
|
|
5178
|
-
let iterationsUsed = 0;
|
|
5179
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
5180
|
-
iterationsUsed = i + 1;
|
|
5181
|
-
const stream = this.client.messages.stream(
|
|
5182
|
-
{
|
|
5183
|
-
model: this.model,
|
|
5184
|
-
max_tokens: 4096,
|
|
5185
|
-
system: systemPrompt,
|
|
5186
|
-
messages,
|
|
5187
|
-
tools
|
|
5188
|
-
},
|
|
5189
|
-
{ signal: this.abortController.signal }
|
|
5190
|
-
);
|
|
5191
|
-
let textContent = "";
|
|
5192
|
-
const toolUseBlocks = [];
|
|
5193
|
-
let currentToolUse = null;
|
|
5194
|
-
const STREAM_IDLE_TIMEOUT_MS = 3e4;
|
|
5195
|
-
let idleTimer = null;
|
|
5196
|
-
const resetIdleTimer = () => {
|
|
5197
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
5198
|
-
idleTimer = setTimeout(() => {
|
|
5199
|
-
this.abortController?.abort();
|
|
5200
|
-
}, STREAM_IDLE_TIMEOUT_MS);
|
|
5201
|
-
};
|
|
5202
|
-
resetIdleTimer();
|
|
5203
|
-
try {
|
|
5204
|
-
for await (const event of stream) {
|
|
5205
|
-
resetIdleTimer();
|
|
5206
|
-
if (event.type === "content_block_start") {
|
|
5207
|
-
if (event.content_block.type === "tool_use") {
|
|
5208
|
-
currentToolUse = {
|
|
5209
|
-
id: event.content_block.id,
|
|
5210
|
-
name: event.content_block.name,
|
|
5211
|
-
inputJson: ""
|
|
5212
|
-
};
|
|
5213
|
-
}
|
|
5214
|
-
} else if (event.type === "content_block_delta") {
|
|
5215
|
-
if (event.delta.type === "text_delta") {
|
|
5216
|
-
textContent += event.delta.text;
|
|
5217
|
-
onChunk(event.delta.text);
|
|
5218
|
-
} else if (event.delta.type === "input_json_delta" && currentToolUse) {
|
|
5219
|
-
currentToolUse.inputJson += event.delta.partial_json;
|
|
5220
|
-
}
|
|
5221
|
-
} else if (event.type === "content_block_stop" && currentToolUse) {
|
|
5222
|
-
try {
|
|
5223
|
-
toolUseBlocks.push({
|
|
5224
|
-
id: currentToolUse.id,
|
|
5225
|
-
name: currentToolUse.name,
|
|
5226
|
-
input: JSON.parse(currentToolUse.inputJson || "{}")
|
|
5227
|
-
});
|
|
5228
|
-
} catch {
|
|
5229
|
-
toolUseBlocks.push({
|
|
5230
|
-
id: currentToolUse.id,
|
|
5231
|
-
name: currentToolUse.name,
|
|
5232
|
-
input: {},
|
|
5233
|
-
_malformedArgs: currentToolUse.inputJson
|
|
5234
|
-
});
|
|
5235
|
-
}
|
|
5236
|
-
currentToolUse = null;
|
|
5237
|
-
}
|
|
5238
|
-
}
|
|
5239
|
-
} finally {
|
|
5240
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
5241
|
-
}
|
|
5242
|
-
const finalMessage = await stream.finalMessage();
|
|
5243
|
-
const assistantContent = [];
|
|
5244
|
-
if (textContent) {
|
|
5245
|
-
assistantContent.push({ type: "text", text: textContent });
|
|
5246
|
-
}
|
|
5247
|
-
for (const tb of toolUseBlocks) {
|
|
5248
|
-
assistantContent.push({
|
|
5249
|
-
type: "tool_use",
|
|
5250
|
-
id: tb.id,
|
|
5251
|
-
name: tb.name,
|
|
5252
|
-
input: tb.input
|
|
5253
|
-
});
|
|
5254
|
-
}
|
|
5255
|
-
messages.push({ role: "assistant", content: assistantContent });
|
|
5256
|
-
if (toolUseBlocks.length === 0) {
|
|
5257
|
-
break;
|
|
5258
|
-
}
|
|
5259
|
-
const toolResults = [];
|
|
5260
|
-
for (const tb of toolUseBlocks) {
|
|
5261
|
-
if (tb._malformedArgs !== void 0) {
|
|
5262
|
-
onChunk(`
|
|
5263
|
-
<<tool:${tb.name}:⚠ invalid args>>
|
|
5264
|
-
`);
|
|
5265
|
-
toolResults.push({
|
|
5266
|
-
type: "tool_result",
|
|
5267
|
-
tool_use_id: tb.id,
|
|
5268
|
-
content: `Error: Invalid JSON in tool arguments — could not parse. Please retry with valid JSON. Raw input: ${tb._malformedArgs.slice(0, 200)}`,
|
|
5269
|
-
is_error: true
|
|
5270
|
-
});
|
|
5271
|
-
continue;
|
|
5272
|
-
}
|
|
5273
|
-
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
5274
|
-
onChunk(`
|
|
5275
|
-
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
5276
|
-
`);
|
|
5277
|
-
let result;
|
|
5278
|
-
try {
|
|
5279
|
-
result = await onToolCall(tb.name, tb.input);
|
|
5280
|
-
} catch (toolErr) {
|
|
5281
|
-
const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
5282
|
-
result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
|
|
5283
|
-
}
|
|
5284
|
-
let parsedRich = null;
|
|
5285
|
-
try {
|
|
5286
|
-
const parsed = JSON.parse(result);
|
|
5287
|
-
if (isRichToolResult(parsed)) parsedRich = parsed;
|
|
5288
|
-
} catch {
|
|
5289
|
-
}
|
|
5290
|
-
if (parsedRich) {
|
|
5291
|
-
toolResults.push({
|
|
5292
|
-
type: "tool_result",
|
|
5293
|
-
tool_use_id: tb.id,
|
|
5294
|
-
content: parsedRich.content.map((block) => {
|
|
5295
|
-
if (block.type === "image") {
|
|
5296
|
-
return {
|
|
5297
|
-
type: "image",
|
|
5298
|
-
source: {
|
|
5299
|
-
type: "base64",
|
|
5300
|
-
media_type: block.mediaType,
|
|
5301
|
-
data: block.base64
|
|
5302
|
-
}
|
|
5303
|
-
};
|
|
5304
|
-
}
|
|
5305
|
-
return { type: "text", text: block.text };
|
|
5306
|
-
})
|
|
5307
|
-
});
|
|
5308
|
-
} else {
|
|
5309
|
-
toolResults.push({
|
|
5310
|
-
type: "tool_result",
|
|
5311
|
-
tool_use_id: tb.id,
|
|
5312
|
-
content: result
|
|
5313
|
-
});
|
|
5314
|
-
}
|
|
5315
|
-
}
|
|
5316
|
-
messages.push({ role: "user", content: toolResults });
|
|
5317
|
-
}
|
|
5318
|
-
if (iterationsUsed >= maxIterations) {
|
|
5319
|
-
onChunk(`
|
|
5320
|
-
|
|
5321
|
-
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
5322
|
-
}
|
|
5323
|
-
} catch (err) {
|
|
5324
|
-
if (err instanceof Error && err.name !== "AbortError") {
|
|
5325
|
-
onChunk(`
|
|
5326
|
-
|
|
5327
|
-
[Error: ${err.message}]`);
|
|
5328
|
-
}
|
|
5329
|
-
} finally {
|
|
5330
|
-
this.abortController = null;
|
|
5331
|
-
onEnd();
|
|
5332
|
-
}
|
|
5333
|
-
}
|
|
5334
|
-
cancel() {
|
|
5335
|
-
this.abortController?.abort();
|
|
5336
|
-
}
|
|
5337
|
-
}
|
|
5338
5129
|
const PROVIDERS = {
|
|
5339
5130
|
anthropic: {
|
|
5340
5131
|
id: "anthropic",
|
|
@@ -5384,6 +5175,16 @@ const PROVIDERS = {
|
|
|
5384
5175
|
apiKeyPlaceholder: "",
|
|
5385
5176
|
apiKeyHint: "No API key needed — runs locally via Ollama"
|
|
5386
5177
|
},
|
|
5178
|
+
llama_cpp: {
|
|
5179
|
+
id: "llama_cpp",
|
|
5180
|
+
name: "llama.cpp (Local)",
|
|
5181
|
+
defaultModel: "",
|
|
5182
|
+
models: [],
|
|
5183
|
+
requiresApiKey: false,
|
|
5184
|
+
defaultBaseUrl: "http://localhost:8080/v1",
|
|
5185
|
+
apiKeyPlaceholder: "",
|
|
5186
|
+
apiKeyHint: "No API key needed — scans the active llama-server model on localhost:8080"
|
|
5187
|
+
},
|
|
5387
5188
|
mistral: {
|
|
5388
5189
|
id: "mistral",
|
|
5389
5190
|
name: "Mistral AI",
|
|
@@ -5465,7 +5266,7 @@ function isLoopbackBaseUrl(baseUrl) {
|
|
|
5465
5266
|
}
|
|
5466
5267
|
function resolveAgentToolProfile(config) {
|
|
5467
5268
|
const providerId = config.id;
|
|
5468
|
-
const isLocalProvider = providerId === "ollama" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
|
|
5269
|
+
const isLocalProvider = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
|
|
5469
5270
|
if (!isLocalProvider) return "default";
|
|
5470
5271
|
const sizeInBillions = parseModelSizeInBillions(config.model);
|
|
5471
5272
|
if (sizeInBillions === null) {
|
|
@@ -5473,6 +5274,8 @@ function resolveAgentToolProfile(config) {
|
|
|
5473
5274
|
}
|
|
5474
5275
|
return sizeInBillions <= 14 ? "compact" : "default";
|
|
5475
5276
|
}
|
|
5277
|
+
const LLAMA_CPP_MIN_CTX_TOKENS$1 = 16384;
|
|
5278
|
+
const LLAMA_CPP_RECOMMENDED_CTX_TOKENS$1 = 32768;
|
|
5476
5279
|
function shouldDebugAgentLoop() {
|
|
5477
5280
|
const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
|
|
5478
5281
|
return value === "1" || value === "true";
|
|
@@ -5503,8 +5306,8 @@ function followUpReminderForProfile(profile, userMessage, assistantText, latestT
|
|
|
5503
5306
|
const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
|
|
5504
5307
|
const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
|
|
5505
5308
|
return {
|
|
5506
|
-
role: "
|
|
5507
|
-
content: `Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
|
|
5309
|
+
role: "user",
|
|
5310
|
+
content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
|
|
5508
5311
|
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
5312
|
${stateReminder}` : "") + (phaseReminder ? `
|
|
5510
5313
|
${phaseReminder}` : "")
|
|
@@ -5545,6 +5348,9 @@ function buildPhaseReminder(userMessage, assistantText) {
|
|
|
5545
5348
|
if (!goal || !text) return "";
|
|
5546
5349
|
const wantsCart = /\b(cart|bag|basket|checkout)\b/.test(goal);
|
|
5547
5350
|
const wantsExplanation = /\b(explain|reason|why)\b/.test(goal);
|
|
5351
|
+
const wantsBookRecommendations = /\b(book|books|recommend|recommended|interesting|novel|fiction|nonfiction)\b/.test(
|
|
5352
|
+
goal
|
|
5353
|
+
);
|
|
5548
5354
|
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
5355
|
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
5356
|
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);
|
|
@@ -5552,8 +5358,59 @@ function buildPhaseReminder(userMessage, assistantText) {
|
|
|
5552
5358
|
text
|
|
5553
5359
|
);
|
|
5554
5360
|
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);
|
|
5361
|
+
const listingLoopSignals = /page contains a list of books|book listings|book cards|visible book|load more results|scroll further|scroll down|inspect the visible|focus on the book listings|targeting the book images|limited to interactive elements|identify the book cards|click one of the visible book/.test(
|
|
5362
|
+
text
|
|
5363
|
+
);
|
|
5364
|
+
const missedResultsSignals = /visible_only mode did not return specific book titles|did not yield a book title link|did not yield specific book titles|navigation links rather than book titles|inspect elements did not yield|inspect the page to find a specific book title|inspect the page to locate a book title|book title link from the search results/.test(
|
|
5365
|
+
text
|
|
5366
|
+
);
|
|
5367
|
+
const falseCartSuccessSignals = /added\s+to\s+the\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+to\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+["“”'a-z0-9 ,:&-]+\s+to the cart|added\s+["“”'a-z0-9 ,:&-]+\s+to cart|added\s+.*\s+by\s+.*\s+to the cart/.test(
|
|
5368
|
+
text
|
|
5369
|
+
) && !/(cart confirmation|view cart|shopping cart|checkout|continue shopping)/.test(
|
|
5370
|
+
text
|
|
5371
|
+
);
|
|
5372
|
+
const skippedSingleResultSignals = /did not yield a direct match|no direct match|no matches|unavailable on powell|out of stock or unavailable/.test(
|
|
5373
|
+
text
|
|
5374
|
+
) && /proceed to (?:add|search for) the next book|move on to the next book|next book from my list/.test(
|
|
5375
|
+
text
|
|
5376
|
+
);
|
|
5377
|
+
const selectedItemsRestartSignals = /navigate back to the search results page|search for ".*" directly in the search box|search for .* directly|page structure has shifted|refresh the page|restart search/.test(
|
|
5378
|
+
text
|
|
5379
|
+
);
|
|
5380
|
+
const multiClickSelectionSignals = /i(?:'| a)?ll start by clicking on the following books|i will start by clicking on the following books|i will click on the following books|clicked on five different book titles|clicked on \d+ different book titles|clicking through the selected titles|click each of the selected titles/.test(
|
|
5381
|
+
text
|
|
5382
|
+
);
|
|
5383
|
+
const staleSelectionSignals = /cannot locate the elements to click|page structure is not being reliably captured|specific titles failed|page may have changed|stale-index/.test(
|
|
5384
|
+
text
|
|
5385
|
+
);
|
|
5386
|
+
const intermediateCartDialogSignals = /(added to cart|has been added to the cart|cart confirmation)/.test(text) && /(continue shopping|search results page|return to the search results page|back button|go back)/.test(
|
|
5387
|
+
text
|
|
5388
|
+
) && !/(all requested books are now in the cart|all 5 books are now in the cart|5 of 5 requested books are now in the cart)/.test(
|
|
5389
|
+
text
|
|
5390
|
+
);
|
|
5391
|
+
if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && listingLoopSignals) {
|
|
5392
|
+
return `Progress reminder: If product results or primary results are already visible, do not keep rereading or rescrolling the same listing page. Open one promising result now. On the detail page, add that item to the cart before returning for the next unseen result.`;
|
|
5393
|
+
}
|
|
5394
|
+
if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && missedResultsSignals) {
|
|
5395
|
+
return `Progress reminder: On a results page, do not use visible_only or generic inspect_element to hunt product results. Call read_page(mode="results_only") once. If Primary Results are shown, click a listed result directly.`;
|
|
5396
|
+
}
|
|
5397
|
+
if (wantsCart && falseCartSuccessSignals && !selectedItems && !explanationDone) {
|
|
5398
|
+
return `Progress reminder: Do not assume an item was added just because its product page is open or you inspected it. Only treat the cart step as complete after a successful Add to Cart click followed by cart confirmation, View Cart, Continue Shopping, or the cart page itself.`;
|
|
5399
|
+
}
|
|
5400
|
+
if (wantsCart && skippedSingleResultSignals && !selectedItems) {
|
|
5401
|
+
return `Progress reminder: Do not skip to a new query just because the match is not exact. If the results page shows even one plausible product result, inspect or click that result before concluding there is no match.`;
|
|
5402
|
+
}
|
|
5403
|
+
if (wantsCart && intermediateCartDialogSignals && !explanationDone) {
|
|
5404
|
+
return `Progress reminder: After an Add to Cart success, prefer the cart-confirmation dialog action Continue Shopping while more items remain. Do not click View Cart or Go to Basket yet, and do not use the browser back button while the dialog is still open.`;
|
|
5405
|
+
}
|
|
5406
|
+
if (wantsCart && selectedItems && !cartDone && selectedItemsRestartSignals) {
|
|
5407
|
+
return `Progress reminder: The chosen items are already decided. Do not restart search, refresh the results page, or navigate back to browse again unless a specific saved link fails. Use the current results page or the chosen result links you already have: open one chosen result, add it to the cart, confirm success, then continue to the next chosen result.`;
|
|
5408
|
+
}
|
|
5409
|
+
if (wantsCart && wantsBookRecommendations && !cartDone && (multiClickSelectionSignals || staleSelectionSignals)) {
|
|
5410
|
+
return `Progress reminder: Do not batch-click multiple results from a listing or category page. Open exactly one visible result, finish that item's Add to Cart flow, confirm success, then use Continue Shopping or go back once to choose the next unseen result. If a remembered label or index fails, trust the latest page state and refresh it with one read_page call before continuing.`;
|
|
5411
|
+
}
|
|
5555
5412
|
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.`;
|
|
5413
|
+
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. Use the chosen result links you already have, add one selected item to the cart, confirm success, then continue to the next one. Do not click multiple chosen results in a row from the same listing page.`;
|
|
5557
5414
|
}
|
|
5558
5415
|
if (wantsCart && wantsExplanation && cartDone && !explanationDone) {
|
|
5559
5416
|
return `Progress reminder: The cart step appears complete. Do not resume browsing. Finish by explaining why the chosen items were recommended.`;
|
|
@@ -5592,6 +5449,30 @@ function shouldRecoverCompactStall(text, userMessage) {
|
|
|
5592
5449
|
if (userMessage && buildPhaseReminder(userMessage, text)) {
|
|
5593
5450
|
return true;
|
|
5594
5451
|
}
|
|
5452
|
+
const repetitivePlanningSignals = [
|
|
5453
|
+
"next step:",
|
|
5454
|
+
"i will now inspect",
|
|
5455
|
+
"i will now read",
|
|
5456
|
+
"i will now click",
|
|
5457
|
+
"i'll use readpage",
|
|
5458
|
+
"i'll use read_page",
|
|
5459
|
+
"i'll start by clicking",
|
|
5460
|
+
"i have clicked on five different book titles",
|
|
5461
|
+
"clicked on five different book titles",
|
|
5462
|
+
"i'll begin with",
|
|
5463
|
+
"if the selection is unclear"
|
|
5464
|
+
];
|
|
5465
|
+
if (repetitivePlanningSignals.some((pattern) => trimmed.includes(pattern))) {
|
|
5466
|
+
return true;
|
|
5467
|
+
}
|
|
5468
|
+
const falseCartSuccessWithoutConfirmation = /added\s+to\s+the\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+to\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+["“”'a-z0-9 ,:&-]+\s+to the cart|added\s+["“”'a-z0-9 ,:&-]+\s+to cart|added\s+.*\s+by\s+.*\s+to the cart/.test(
|
|
5469
|
+
trimmed
|
|
5470
|
+
) && !/(cart confirmation|view cart|continue shopping|shopping cart|checkout|why i chose|here is why i chose|here are my reasons)/.test(
|
|
5471
|
+
trimmed
|
|
5472
|
+
);
|
|
5473
|
+
if (falseCartSuccessWithoutConfirmation) {
|
|
5474
|
+
return true;
|
|
5475
|
+
}
|
|
5595
5476
|
const completionSignals = [
|
|
5596
5477
|
"i found",
|
|
5597
5478
|
"i chose",
|
|
@@ -5645,23 +5526,114 @@ function stableToolSignature(name, args) {
|
|
|
5645
5526
|
function hasRecentDuplicateToolCall(recentToolSignatures, signature) {
|
|
5646
5527
|
return recentToolSignatures.includes(signature);
|
|
5647
5528
|
}
|
|
5529
|
+
function isClickReadLoop(names) {
|
|
5530
|
+
if (names.length < 6) return false;
|
|
5531
|
+
const tail = names.slice(-6);
|
|
5532
|
+
let clickReadPairs = 0;
|
|
5533
|
+
for (let i = 0; i < tail.length - 1; i++) {
|
|
5534
|
+
if (tail[i] === "click" && tail[i + 1] === "read_page") {
|
|
5535
|
+
clickReadPairs++;
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
return clickReadPairs >= 2;
|
|
5539
|
+
}
|
|
5648
5540
|
function normalizeToolToken(value) {
|
|
5649
5541
|
return value.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
|
|
5650
5542
|
}
|
|
5651
|
-
function canonicalizeUrlLike(value) {
|
|
5543
|
+
function canonicalizeUrlLike(value) {
|
|
5544
|
+
try {
|
|
5545
|
+
const url = new URL(value.trim());
|
|
5546
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
5547
|
+
url.hostname = url.hostname.replace(/^www\./, "");
|
|
5548
|
+
url.hash = "";
|
|
5549
|
+
if (url.pathname.endsWith("/") && url.pathname !== "/") {
|
|
5550
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
5551
|
+
}
|
|
5552
|
+
return url.toString();
|
|
5553
|
+
}
|
|
5554
|
+
} catch {
|
|
5555
|
+
}
|
|
5556
|
+
return value.trim();
|
|
5557
|
+
}
|
|
5558
|
+
function toLikelyUrl(value) {
|
|
5559
|
+
const trimmed = value.trim().replace(/^["']|["']$/g, "");
|
|
5560
|
+
if (!trimmed) return null;
|
|
5561
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
5562
|
+
if (/^[a-z0-9-]+\.(com|org|net|io|dev|app|ai|co|edu|gov)(\/\S*)?$/i.test(trimmed)) {
|
|
5563
|
+
return `https://${trimmed}`;
|
|
5564
|
+
}
|
|
5565
|
+
return null;
|
|
5566
|
+
}
|
|
5567
|
+
function scalarArgsForTool(name, scalar) {
|
|
5568
|
+
const trimmed = scalar.trim();
|
|
5569
|
+
if (!trimmed) return null;
|
|
5570
|
+
if (name === "navigate") {
|
|
5571
|
+
const url = toLikelyUrl(trimmed);
|
|
5572
|
+
return url ? { url } : null;
|
|
5573
|
+
}
|
|
5574
|
+
if (name === "search") {
|
|
5575
|
+
return { query: trimmed.replace(/^["']|["']$/g, "") };
|
|
5576
|
+
}
|
|
5577
|
+
if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
|
|
5578
|
+
return { text: trimmed.replace(/^["']|["']$/g, "") };
|
|
5579
|
+
}
|
|
5580
|
+
if (name === "read_page") {
|
|
5581
|
+
const mode = trimmed.replace(/^["']|["']$/g, "").toLowerCase();
|
|
5582
|
+
if (mode) return { mode };
|
|
5583
|
+
}
|
|
5584
|
+
return null;
|
|
5585
|
+
}
|
|
5586
|
+
function tryParseJsonWithCommonRepairs(raw) {
|
|
5587
|
+
const trimmed = raw.trim();
|
|
5588
|
+
if (!trimmed) return {};
|
|
5589
|
+
const candidates = /* @__PURE__ */ new Set([trimmed]);
|
|
5590
|
+
const objectMatch = trimmed.match(/\{[\s\S]*\}/);
|
|
5591
|
+
if (objectMatch?.[0]) candidates.add(objectMatch[0]);
|
|
5592
|
+
if (!trimmed.startsWith("{") && trimmed.includes(":")) {
|
|
5593
|
+
candidates.add(`{${trimmed}}`);
|
|
5594
|
+
}
|
|
5595
|
+
for (const candidate of candidates) {
|
|
5596
|
+
const normalized = candidate.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
5597
|
+
if (!normalized) continue;
|
|
5598
|
+
const repaired = normalized.replace(/([{,]\s*)([A-Za-z_][A-Za-z0-9_-]*)(\s*:)/g, '$1"$2"$3').replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3').replace(
|
|
5599
|
+
/:\s*'([^'\\]*(?:\\.[^'\\]*)*)'/g,
|
|
5600
|
+
(_match, value) => `: ${JSON.stringify(value)}`
|
|
5601
|
+
).replace(/,\s*([}\]])/g, "$1");
|
|
5602
|
+
try {
|
|
5603
|
+
return JSON.parse(repaired);
|
|
5604
|
+
} catch {
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
throw new Error("invalid-json");
|
|
5608
|
+
}
|
|
5609
|
+
function parseToolArgsWithRepair(name, argsJson) {
|
|
5610
|
+
const trimmed = (argsJson || "").trim();
|
|
5611
|
+
if (!trimmed) return { args: {}, repaired: false };
|
|
5652
5612
|
try {
|
|
5653
|
-
const
|
|
5654
|
-
if (
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
}
|
|
5660
|
-
return url.toString();
|
|
5613
|
+
const parsed = JSON.parse(trimmed);
|
|
5614
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
5615
|
+
return { args: parsed, repaired: false };
|
|
5616
|
+
}
|
|
5617
|
+
if (typeof parsed === "string") {
|
|
5618
|
+
const scalarArgs2 = scalarArgsForTool(name, parsed);
|
|
5619
|
+
return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
|
|
5661
5620
|
}
|
|
5621
|
+
return null;
|
|
5662
5622
|
} catch {
|
|
5663
5623
|
}
|
|
5664
|
-
|
|
5624
|
+
try {
|
|
5625
|
+
const repaired = tryParseJsonWithCommonRepairs(trimmed);
|
|
5626
|
+
if (repaired && typeof repaired === "object" && !Array.isArray(repaired)) {
|
|
5627
|
+
return { args: repaired, repaired: true };
|
|
5628
|
+
}
|
|
5629
|
+
if (typeof repaired === "string") {
|
|
5630
|
+
const scalarArgs2 = scalarArgsForTool(name, repaired);
|
|
5631
|
+
return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
|
|
5632
|
+
}
|
|
5633
|
+
} catch {
|
|
5634
|
+
}
|
|
5635
|
+
const scalarArgs = scalarArgsForTool(name, trimmed);
|
|
5636
|
+
return scalarArgs ? { args: scalarArgs, repaired: true } : null;
|
|
5665
5637
|
}
|
|
5666
5638
|
function coerceToolArgsForExecution(name, args) {
|
|
5667
5639
|
const coerced = { ...args };
|
|
@@ -5813,12 +5785,49 @@ function recoverNarratedActionToolCalls(text, availableToolNames) {
|
|
|
5813
5785
|
});
|
|
5814
5786
|
}
|
|
5815
5787
|
}
|
|
5788
|
+
const inlineReadMatch = trimmed.match(
|
|
5789
|
+
/\bread_?page\b\s*\(\s*mode\s*=\s*["']?([a-z_]+)["']?\s*\)/i
|
|
5790
|
+
) ?? trimmed.match(
|
|
5791
|
+
/\breadpage\b\s*\(\s*mode\s*=\s*["']?([a-z_]+)["']?\s*\)/i
|
|
5792
|
+
);
|
|
5793
|
+
if (inlineReadMatch && availableToolNames.has("read_page")) {
|
|
5794
|
+
const rawMode = (inlineReadMatch[1] || "").trim().toLowerCase();
|
|
5795
|
+
const normalizedMode = rawMode === "visibleonly" ? "visible_only" : rawMode === "resultsonly" ? "results_only" : rawMode;
|
|
5796
|
+
if (normalizedMode) {
|
|
5797
|
+
recovered.push({
|
|
5798
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5799
|
+
name: "read_page",
|
|
5800
|
+
argsJson: JSON.stringify({ mode: normalizedMode })
|
|
5801
|
+
});
|
|
5802
|
+
return recovered;
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
const inlineInspectMatch = trimmed.match(
|
|
5806
|
+
/\binspect_?element\b(?:\s+tool)?\b/i
|
|
5807
|
+
) ?? trimmed.match(/\binspectelement\b\b/i);
|
|
5808
|
+
if (inlineInspectMatch && availableToolNames.has("inspect_element")) {
|
|
5809
|
+
recovered.push({
|
|
5810
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5811
|
+
name: "inspect_element",
|
|
5812
|
+
argsJson: "{}"
|
|
5813
|
+
});
|
|
5814
|
+
return recovered;
|
|
5815
|
+
}
|
|
5816
5816
|
return recovered;
|
|
5817
5817
|
}
|
|
5818
|
+
function formatOpenAICompatErrorMessage(providerId, message) {
|
|
5819
|
+
if (providerId === "llama_cpp" && /(available context size|context size exceeded|exceeds the available context size|try increasing it)/i.test(
|
|
5820
|
+
message
|
|
5821
|
+
)) {
|
|
5822
|
+
return `${message} llama.cpp sets context size at server startup, not per request. Vessel's agent prompt plus tool schema is about 6.5k tokens before browsing history, so run llama-server with --ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS$1} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS$1} recommended).`;
|
|
5823
|
+
}
|
|
5824
|
+
return message;
|
|
5825
|
+
}
|
|
5818
5826
|
class OpenAICompatProvider {
|
|
5819
5827
|
agentToolProfile;
|
|
5820
5828
|
client;
|
|
5821
5829
|
model;
|
|
5830
|
+
providerId;
|
|
5822
5831
|
abortController = null;
|
|
5823
5832
|
constructor(config) {
|
|
5824
5833
|
const meta = PROVIDERS[config.id];
|
|
@@ -5827,6 +5836,7 @@ class OpenAICompatProvider {
|
|
|
5827
5836
|
apiKey: config.apiKey || "ollama",
|
|
5828
5837
|
baseURL
|
|
5829
5838
|
});
|
|
5839
|
+
this.providerId = config.id;
|
|
5830
5840
|
this.model = config.model || meta?.defaultModel || "gpt-4o";
|
|
5831
5841
|
this.agentToolProfile = resolveAgentToolProfile(config);
|
|
5832
5842
|
}
|
|
@@ -5841,22 +5851,31 @@ class OpenAICompatProvider {
|
|
|
5841
5851
|
const stream = await this.client.chat.completions.create(
|
|
5842
5852
|
{
|
|
5843
5853
|
model: this.model,
|
|
5854
|
+
max_tokens: 4096,
|
|
5844
5855
|
stream: true,
|
|
5845
5856
|
messages
|
|
5846
5857
|
},
|
|
5847
5858
|
{ signal: this.abortController.signal }
|
|
5848
5859
|
);
|
|
5849
5860
|
for await (const chunk of stream) {
|
|
5850
|
-
const
|
|
5851
|
-
if (
|
|
5852
|
-
|
|
5861
|
+
const choice = chunk.choices[0];
|
|
5862
|
+
if (!choice) continue;
|
|
5863
|
+
const delta = choice.delta;
|
|
5864
|
+
const reasoning = delta?.reasoning_content;
|
|
5865
|
+
if (typeof reasoning === "string" && reasoning.length > 0) {
|
|
5866
|
+
onChunk(reasoning);
|
|
5867
|
+
}
|
|
5868
|
+
if (delta.content) {
|
|
5869
|
+
onChunk(delta.content);
|
|
5853
5870
|
}
|
|
5854
5871
|
}
|
|
5855
5872
|
} catch (err) {
|
|
5856
5873
|
if (err instanceof Error && err.name !== "AbortError") {
|
|
5857
|
-
onChunk(
|
|
5874
|
+
onChunk(
|
|
5875
|
+
`
|
|
5858
5876
|
|
|
5859
|
-
[Error: ${err.message}]`
|
|
5877
|
+
[Error: ${formatOpenAICompatErrorMessage(this.providerId, err.message)}]`
|
|
5878
|
+
);
|
|
5860
5879
|
}
|
|
5861
5880
|
} finally {
|
|
5862
5881
|
this.abortController = null;
|
|
@@ -5878,6 +5897,8 @@ class OpenAICompatProvider {
|
|
|
5878
5897
|
let compactRecoveryCount = 0;
|
|
5879
5898
|
let compactCorrectionCount = 0;
|
|
5880
5899
|
const recentCompactToolSignatures = [];
|
|
5900
|
+
const recentToolNames = [];
|
|
5901
|
+
let clickReadLoopNudged = false;
|
|
5881
5902
|
for (let i = 0; i < maxIterations; i++) {
|
|
5882
5903
|
iterationsUsed = i + 1;
|
|
5883
5904
|
let textAccum = "";
|
|
@@ -5892,6 +5913,7 @@ class OpenAICompatProvider {
|
|
|
5892
5913
|
const stream = await this.client.chat.completions.create(
|
|
5893
5914
|
{
|
|
5894
5915
|
model: this.model,
|
|
5916
|
+
max_tokens: 4096,
|
|
5895
5917
|
stream: true,
|
|
5896
5918
|
messages,
|
|
5897
5919
|
tools: openAITools,
|
|
@@ -5905,6 +5927,10 @@ class OpenAICompatProvider {
|
|
|
5905
5927
|
if (!choice) continue;
|
|
5906
5928
|
const delta = choice.delta;
|
|
5907
5929
|
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
5930
|
+
const reasoning = delta?.reasoning_content;
|
|
5931
|
+
if (typeof reasoning === "string" && reasoning.length > 0) {
|
|
5932
|
+
onChunk(reasoning);
|
|
5933
|
+
}
|
|
5908
5934
|
if (delta.content) {
|
|
5909
5935
|
textAccum += delta.content;
|
|
5910
5936
|
onChunk(delta.content);
|
|
@@ -5925,10 +5951,12 @@ class OpenAICompatProvider {
|
|
|
5925
5951
|
for (const tc of Object.values(toolCallAccums)) {
|
|
5926
5952
|
if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5927
5953
|
let parsedArgs = {};
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5954
|
+
const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
|
|
5955
|
+
if (repairedArgs) {
|
|
5956
|
+
parsedArgs = repairedArgs.args;
|
|
5957
|
+
if (repairedArgs.repaired) {
|
|
5958
|
+
tc.argsJson = JSON.stringify(parsedArgs);
|
|
5959
|
+
}
|
|
5932
5960
|
}
|
|
5933
5961
|
tc.name = resolveToolCallName(tc.name, parsedArgs, availableToolNames);
|
|
5934
5962
|
}
|
|
@@ -5967,11 +5995,14 @@ class OpenAICompatProvider {
|
|
|
5967
5995
|
});
|
|
5968
5996
|
const malformedToolCalls = /* @__PURE__ */ new Set();
|
|
5969
5997
|
for (const tc of toolCalls) {
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
} catch {
|
|
5998
|
+
const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
|
|
5999
|
+
if (!repairedArgs) {
|
|
5973
6000
|
malformedToolCalls.add(tc.id);
|
|
5974
6001
|
tc.argsJson = "{}";
|
|
6002
|
+
continue;
|
|
6003
|
+
}
|
|
6004
|
+
if (repairedArgs.repaired) {
|
|
6005
|
+
tc.argsJson = JSON.stringify(repairedArgs.args);
|
|
5975
6006
|
}
|
|
5976
6007
|
}
|
|
5977
6008
|
const assistantMsg = {
|
|
@@ -5995,12 +6026,12 @@ class OpenAICompatProvider {
|
|
|
5995
6026
|
)) {
|
|
5996
6027
|
compactRecoveryCount += 1;
|
|
5997
6028
|
messages.push({
|
|
5998
|
-
role: "
|
|
5999
|
-
content: buildCompactRecoveryPrompt(
|
|
6029
|
+
role: "user",
|
|
6030
|
+
content: `[System] ${buildCompactRecoveryPrompt(
|
|
6000
6031
|
userMessage,
|
|
6001
6032
|
textAccum,
|
|
6002
6033
|
latestToolMessage ? String(latestToolMessage.content || "") : null
|
|
6003
|
-
)
|
|
6034
|
+
)}`
|
|
6004
6035
|
});
|
|
6005
6036
|
continue;
|
|
6006
6037
|
}
|
|
@@ -6021,9 +6052,8 @@ class OpenAICompatProvider {
|
|
|
6021
6052
|
continue;
|
|
6022
6053
|
}
|
|
6023
6054
|
let args = {};
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
} catch {
|
|
6055
|
+
const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
|
|
6056
|
+
if (!repairedArgs) {
|
|
6027
6057
|
onChunk(`
|
|
6028
6058
|
<<tool:${tc.name}:⚠ invalid args>>
|
|
6029
6059
|
`);
|
|
@@ -6034,6 +6064,7 @@ class OpenAICompatProvider {
|
|
|
6034
6064
|
});
|
|
6035
6065
|
continue;
|
|
6036
6066
|
}
|
|
6067
|
+
args = repairedArgs.args;
|
|
6037
6068
|
args = coerceToolArgsForExecution(tc.name, args);
|
|
6038
6069
|
if (!availableToolNames.has(tc.name)) {
|
|
6039
6070
|
onChunk(`
|
|
@@ -6047,14 +6078,23 @@ class OpenAICompatProvider {
|
|
|
6047
6078
|
compactCorrectionCount += 1;
|
|
6048
6079
|
if (compactCorrectionCount >= 2) {
|
|
6049
6080
|
messages.push({
|
|
6050
|
-
role: "
|
|
6051
|
-
content: `You are calling unsupported tools. Stop inventing tool names. Use the supported tools you were given and take the next concrete step.`
|
|
6081
|
+
role: "user",
|
|
6082
|
+
content: `[System] You are calling unsupported tools. Stop inventing tool names. Use the supported tools you were given and take the next concrete step.`
|
|
6052
6083
|
});
|
|
6053
6084
|
}
|
|
6054
6085
|
continue;
|
|
6055
6086
|
}
|
|
6056
6087
|
const toolSignature = stableToolSignature(tc.name, args);
|
|
6057
|
-
|
|
6088
|
+
const neverSuppressDuplicate = [
|
|
6089
|
+
"read_page",
|
|
6090
|
+
"current_tab",
|
|
6091
|
+
"inspect_element",
|
|
6092
|
+
"screenshot",
|
|
6093
|
+
"go_back",
|
|
6094
|
+
"go_forward",
|
|
6095
|
+
"click"
|
|
6096
|
+
].includes(tc.name);
|
|
6097
|
+
if (this.agentToolProfile === "compact" && !neverSuppressDuplicate && hasRecentDuplicateToolCall(
|
|
6058
6098
|
recentCompactToolSignatures,
|
|
6059
6099
|
toolSignature
|
|
6060
6100
|
)) {
|
|
@@ -6069,8 +6109,8 @@ class OpenAICompatProvider {
|
|
|
6069
6109
|
compactCorrectionCount += 1;
|
|
6070
6110
|
if (compactCorrectionCount >= 2) {
|
|
6071
6111
|
messages.push({
|
|
6072
|
-
role: "
|
|
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.`
|
|
6112
|
+
role: "user",
|
|
6113
|
+
content: `[System] 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
6114
|
});
|
|
6075
6115
|
}
|
|
6076
6116
|
continue;
|
|
@@ -6100,6 +6140,15 @@ class OpenAICompatProvider {
|
|
|
6100
6140
|
recentCompactToolSignatures.shift();
|
|
6101
6141
|
}
|
|
6102
6142
|
}
|
|
6143
|
+
recentToolNames.push(tc.name);
|
|
6144
|
+
if (recentToolNames.length > 8) recentToolNames.shift();
|
|
6145
|
+
if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
|
|
6146
|
+
clickReadLoopNudged = true;
|
|
6147
|
+
messages.push({
|
|
6148
|
+
role: "user",
|
|
6149
|
+
content: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
|
|
6150
|
+
});
|
|
6151
|
+
}
|
|
6103
6152
|
compactCorrectionCount = 0;
|
|
6104
6153
|
iterationToolResultPreviews.push(toolContent);
|
|
6105
6154
|
messages.push({
|
|
@@ -6121,6 +6170,228 @@ class OpenAICompatProvider {
|
|
|
6121
6170
|
if (iterationsUsed >= maxIterations) {
|
|
6122
6171
|
onChunk(`
|
|
6123
6172
|
|
|
6173
|
+
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
6174
|
+
}
|
|
6175
|
+
} catch (err) {
|
|
6176
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
6177
|
+
onChunk(
|
|
6178
|
+
`
|
|
6179
|
+
|
|
6180
|
+
[Error: ${formatOpenAICompatErrorMessage(this.providerId, err.message)}]`
|
|
6181
|
+
);
|
|
6182
|
+
}
|
|
6183
|
+
} finally {
|
|
6184
|
+
this.abortController = null;
|
|
6185
|
+
onEnd();
|
|
6186
|
+
}
|
|
6187
|
+
}
|
|
6188
|
+
cancel() {
|
|
6189
|
+
this.abortController?.abort();
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
class AnthropicProvider {
|
|
6193
|
+
agentToolProfile = "default";
|
|
6194
|
+
client;
|
|
6195
|
+
model;
|
|
6196
|
+
abortController = null;
|
|
6197
|
+
constructor(apiKey, model) {
|
|
6198
|
+
this.client = new Anthropic({ apiKey });
|
|
6199
|
+
this.model = model || "claude-sonnet-4-20250514";
|
|
6200
|
+
}
|
|
6201
|
+
async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
|
|
6202
|
+
this.abortController = new AbortController();
|
|
6203
|
+
const messages = [
|
|
6204
|
+
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
6205
|
+
{ role: "user", content: userMessage }
|
|
6206
|
+
];
|
|
6207
|
+
try {
|
|
6208
|
+
const stream = this.client.messages.stream(
|
|
6209
|
+
{
|
|
6210
|
+
model: this.model,
|
|
6211
|
+
max_tokens: 4096,
|
|
6212
|
+
system: systemPrompt,
|
|
6213
|
+
messages
|
|
6214
|
+
},
|
|
6215
|
+
{ signal: this.abortController.signal }
|
|
6216
|
+
);
|
|
6217
|
+
for await (const event of stream) {
|
|
6218
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
6219
|
+
onChunk(event.delta.text);
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
} catch (err) {
|
|
6223
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
6224
|
+
onChunk(`
|
|
6225
|
+
|
|
6226
|
+
[Error: ${err.message}]`);
|
|
6227
|
+
}
|
|
6228
|
+
} finally {
|
|
6229
|
+
this.abortController = null;
|
|
6230
|
+
onEnd();
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
|
|
6234
|
+
this.abortController = new AbortController();
|
|
6235
|
+
const messages = [
|
|
6236
|
+
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
6237
|
+
{ role: "user", content: userMessage }
|
|
6238
|
+
];
|
|
6239
|
+
try {
|
|
6240
|
+
const maxIterations = getEffectiveMaxIterations();
|
|
6241
|
+
let iterationsUsed = 0;
|
|
6242
|
+
const recentToolNames = [];
|
|
6243
|
+
let clickReadLoopNudged = false;
|
|
6244
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
6245
|
+
iterationsUsed = i + 1;
|
|
6246
|
+
const stream = this.client.messages.stream(
|
|
6247
|
+
{
|
|
6248
|
+
model: this.model,
|
|
6249
|
+
max_tokens: 4096,
|
|
6250
|
+
system: systemPrompt,
|
|
6251
|
+
messages,
|
|
6252
|
+
tools
|
|
6253
|
+
},
|
|
6254
|
+
{ signal: this.abortController.signal }
|
|
6255
|
+
);
|
|
6256
|
+
let textContent = "";
|
|
6257
|
+
const toolUseBlocks = [];
|
|
6258
|
+
let currentToolUse = null;
|
|
6259
|
+
const STREAM_IDLE_TIMEOUT_MS = 3e4;
|
|
6260
|
+
let idleTimer = null;
|
|
6261
|
+
const resetIdleTimer = () => {
|
|
6262
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
6263
|
+
idleTimer = setTimeout(() => {
|
|
6264
|
+
this.abortController?.abort();
|
|
6265
|
+
}, STREAM_IDLE_TIMEOUT_MS);
|
|
6266
|
+
};
|
|
6267
|
+
resetIdleTimer();
|
|
6268
|
+
try {
|
|
6269
|
+
for await (const event of stream) {
|
|
6270
|
+
resetIdleTimer();
|
|
6271
|
+
if (event.type === "content_block_start") {
|
|
6272
|
+
if (event.content_block.type === "tool_use") {
|
|
6273
|
+
currentToolUse = {
|
|
6274
|
+
id: event.content_block.id,
|
|
6275
|
+
name: event.content_block.name,
|
|
6276
|
+
inputJson: ""
|
|
6277
|
+
};
|
|
6278
|
+
}
|
|
6279
|
+
} else if (event.type === "content_block_delta") {
|
|
6280
|
+
if (event.delta.type === "text_delta") {
|
|
6281
|
+
textContent += event.delta.text;
|
|
6282
|
+
onChunk(event.delta.text);
|
|
6283
|
+
} else if (event.delta.type === "input_json_delta" && currentToolUse) {
|
|
6284
|
+
currentToolUse.inputJson += event.delta.partial_json;
|
|
6285
|
+
}
|
|
6286
|
+
} else if (event.type === "content_block_stop" && currentToolUse) {
|
|
6287
|
+
try {
|
|
6288
|
+
toolUseBlocks.push({
|
|
6289
|
+
id: currentToolUse.id,
|
|
6290
|
+
name: currentToolUse.name,
|
|
6291
|
+
input: JSON.parse(currentToolUse.inputJson || "{}")
|
|
6292
|
+
});
|
|
6293
|
+
} catch {
|
|
6294
|
+
toolUseBlocks.push({
|
|
6295
|
+
id: currentToolUse.id,
|
|
6296
|
+
name: currentToolUse.name,
|
|
6297
|
+
input: {},
|
|
6298
|
+
_malformedArgs: currentToolUse.inputJson
|
|
6299
|
+
});
|
|
6300
|
+
}
|
|
6301
|
+
currentToolUse = null;
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
} finally {
|
|
6305
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
6306
|
+
}
|
|
6307
|
+
const finalMessage = await stream.finalMessage();
|
|
6308
|
+
const assistantContent = [];
|
|
6309
|
+
if (textContent) {
|
|
6310
|
+
assistantContent.push({ type: "text", text: textContent });
|
|
6311
|
+
}
|
|
6312
|
+
for (const tb of toolUseBlocks) {
|
|
6313
|
+
assistantContent.push({
|
|
6314
|
+
type: "tool_use",
|
|
6315
|
+
id: tb.id,
|
|
6316
|
+
name: tb.name,
|
|
6317
|
+
input: tb.input
|
|
6318
|
+
});
|
|
6319
|
+
}
|
|
6320
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
6321
|
+
if (toolUseBlocks.length === 0) {
|
|
6322
|
+
break;
|
|
6323
|
+
}
|
|
6324
|
+
const toolResults = [];
|
|
6325
|
+
for (const tb of toolUseBlocks) {
|
|
6326
|
+
if (tb._malformedArgs !== void 0) {
|
|
6327
|
+
onChunk(`
|
|
6328
|
+
<<tool:${tb.name}:⚠ invalid args>>
|
|
6329
|
+
`);
|
|
6330
|
+
toolResults.push({
|
|
6331
|
+
type: "tool_result",
|
|
6332
|
+
tool_use_id: tb.id,
|
|
6333
|
+
content: `Error: Invalid JSON in tool arguments — could not parse. Please retry with valid JSON. Raw input: ${tb._malformedArgs.slice(0, 200)}`,
|
|
6334
|
+
is_error: true
|
|
6335
|
+
});
|
|
6336
|
+
continue;
|
|
6337
|
+
}
|
|
6338
|
+
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
6339
|
+
onChunk(`
|
|
6340
|
+
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
6341
|
+
`);
|
|
6342
|
+
let result;
|
|
6343
|
+
try {
|
|
6344
|
+
result = await onToolCall(tb.name, tb.input);
|
|
6345
|
+
} catch (toolErr) {
|
|
6346
|
+
const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
6347
|
+
result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
|
|
6348
|
+
}
|
|
6349
|
+
let parsedRich = null;
|
|
6350
|
+
try {
|
|
6351
|
+
const parsed = JSON.parse(result);
|
|
6352
|
+
if (isRichToolResult(parsed)) parsedRich = parsed;
|
|
6353
|
+
} catch {
|
|
6354
|
+
}
|
|
6355
|
+
if (parsedRich) {
|
|
6356
|
+
toolResults.push({
|
|
6357
|
+
type: "tool_result",
|
|
6358
|
+
tool_use_id: tb.id,
|
|
6359
|
+
content: parsedRich.content.map((block) => {
|
|
6360
|
+
if (block.type === "image") {
|
|
6361
|
+
return {
|
|
6362
|
+
type: "image",
|
|
6363
|
+
source: {
|
|
6364
|
+
type: "base64",
|
|
6365
|
+
media_type: block.mediaType,
|
|
6366
|
+
data: block.base64
|
|
6367
|
+
}
|
|
6368
|
+
};
|
|
6369
|
+
}
|
|
6370
|
+
return { type: "text", text: block.text };
|
|
6371
|
+
})
|
|
6372
|
+
});
|
|
6373
|
+
} else {
|
|
6374
|
+
toolResults.push({
|
|
6375
|
+
type: "tool_result",
|
|
6376
|
+
tool_use_id: tb.id,
|
|
6377
|
+
content: result
|
|
6378
|
+
});
|
|
6379
|
+
}
|
|
6380
|
+
recentToolNames.push(tb.name);
|
|
6381
|
+
if (recentToolNames.length > 8) recentToolNames.shift();
|
|
6382
|
+
}
|
|
6383
|
+
messages.push({ role: "user", content: toolResults });
|
|
6384
|
+
if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
|
|
6385
|
+
clickReadLoopNudged = true;
|
|
6386
|
+
messages.push({
|
|
6387
|
+
role: "user",
|
|
6388
|
+
content: `You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
|
|
6389
|
+
});
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
if (iterationsUsed >= maxIterations) {
|
|
6393
|
+
onChunk(`
|
|
6394
|
+
|
|
6124
6395
|
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
6125
6396
|
}
|
|
6126
6397
|
} catch (err) {
|
|
@@ -6166,6 +6437,55 @@ function validateProviderConnection(config, options = { requireModel: true }) {
|
|
|
6166
6437
|
}
|
|
6167
6438
|
return null;
|
|
6168
6439
|
}
|
|
6440
|
+
const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
|
|
6441
|
+
const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
|
|
6442
|
+
function extractLlamaCppCtxSize(payload) {
|
|
6443
|
+
if (!payload || typeof payload !== "object") return null;
|
|
6444
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6445
|
+
const queue = [payload];
|
|
6446
|
+
while (queue.length > 0) {
|
|
6447
|
+
const current = queue.shift();
|
|
6448
|
+
if (!current || typeof current !== "object" || visited.has(current)) continue;
|
|
6449
|
+
visited.add(current);
|
|
6450
|
+
for (const [key, value] of Object.entries(current)) {
|
|
6451
|
+
if (typeof value === "number" && Number.isFinite(value) && /^(n_ctx|ctx_size|context_size)$/i.test(key)) {
|
|
6452
|
+
return value;
|
|
6453
|
+
}
|
|
6454
|
+
if (value && typeof value === "object") {
|
|
6455
|
+
queue.push(value);
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
return null;
|
|
6460
|
+
}
|
|
6461
|
+
function buildLlamaCppCtxWarning(ctxSize) {
|
|
6462
|
+
if (ctxSize == null) {
|
|
6463
|
+
return `Could not detect llama-server context size. Run llama-server with --ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} recommended for Vessel agent loops).`;
|
|
6464
|
+
}
|
|
6465
|
+
if (ctxSize < LLAMA_CPP_MIN_CTX_TOKENS) {
|
|
6466
|
+
return `Detected llama-server ctx-size ${ctxSize}, which is too small for reliable Vessel agent loops. Run llama-server with --ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} recommended).`;
|
|
6467
|
+
}
|
|
6468
|
+
if (ctxSize < LLAMA_CPP_RECOMMENDED_CTX_TOKENS) {
|
|
6469
|
+
return `Detected llama-server ctx-size ${ctxSize}. This should work, but ${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} is recommended for longer Vessel agent runs.`;
|
|
6470
|
+
}
|
|
6471
|
+
return void 0;
|
|
6472
|
+
}
|
|
6473
|
+
async function probeLlamaCppCtxWarning(baseURL) {
|
|
6474
|
+
try {
|
|
6475
|
+
const root = new URL(baseURL);
|
|
6476
|
+
root.pathname = "/props";
|
|
6477
|
+
root.search = "";
|
|
6478
|
+
root.hash = "";
|
|
6479
|
+
const response = await fetch(root.toString());
|
|
6480
|
+
if (!response.ok) {
|
|
6481
|
+
return buildLlamaCppCtxWarning(null);
|
|
6482
|
+
}
|
|
6483
|
+
const payload = await response.json();
|
|
6484
|
+
return buildLlamaCppCtxWarning(extractLlamaCppCtxSize(payload));
|
|
6485
|
+
} catch {
|
|
6486
|
+
return buildLlamaCppCtxWarning(null);
|
|
6487
|
+
}
|
|
6488
|
+
}
|
|
6169
6489
|
async function fetchProviderModels(config) {
|
|
6170
6490
|
const normalized = sanitizeProviderConfig(config);
|
|
6171
6491
|
const error = validateProviderConnection(normalized, { requireModel: false });
|
|
@@ -6175,7 +6495,7 @@ async function fetchProviderModels(config) {
|
|
|
6175
6495
|
if (normalized.id === "anthropic") {
|
|
6176
6496
|
const client2 = new Anthropic({ apiKey: normalized.apiKey });
|
|
6177
6497
|
const page2 = await client2.models.list();
|
|
6178
|
-
return page2.data.map((model) => model.id);
|
|
6498
|
+
return { ok: true, models: page2.data.map((model) => model.id) };
|
|
6179
6499
|
}
|
|
6180
6500
|
const meta = PROVIDERS[normalized.id];
|
|
6181
6501
|
const baseURL = normalized.baseUrl || meta?.defaultBaseUrl || "https://api.openai.com/v1";
|
|
@@ -6184,7 +6504,13 @@ async function fetchProviderModels(config) {
|
|
|
6184
6504
|
baseURL
|
|
6185
6505
|
});
|
|
6186
6506
|
const page = await client.models.list();
|
|
6187
|
-
|
|
6507
|
+
const models = page.data.map((model) => model.id);
|
|
6508
|
+
const warning = normalized.id === "llama_cpp" ? await probeLlamaCppCtxWarning(baseURL) : void 0;
|
|
6509
|
+
return {
|
|
6510
|
+
ok: true,
|
|
6511
|
+
models,
|
|
6512
|
+
...warning ? { warning } : {}
|
|
6513
|
+
};
|
|
6188
6514
|
}
|
|
6189
6515
|
function createProvider(config) {
|
|
6190
6516
|
const normalized = sanitizeProviderConfig(config);
|
|
@@ -6241,7 +6567,7 @@ function createTraceSession(query, url, title) {
|
|
|
6241
6567
|
}
|
|
6242
6568
|
const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
|
|
6243
6569
|
const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
|
|
6244
|
-
function elementLabel(el) {
|
|
6570
|
+
function elementLabel$1(el) {
|
|
6245
6571
|
return el.text?.trim() || el.label?.trim() || el.value?.trim() || el.placeholder?.trim() || void 0;
|
|
6246
6572
|
}
|
|
6247
6573
|
function isOverlayAction(el) {
|
|
@@ -6257,11 +6583,11 @@ function isRadioOption(el) {
|
|
|
6257
6583
|
function normalizeAction(el) {
|
|
6258
6584
|
return {
|
|
6259
6585
|
index: el.index,
|
|
6260
|
-
label: elementLabel(el),
|
|
6586
|
+
label: elementLabel$1(el),
|
|
6261
6587
|
selector: el.selector,
|
|
6262
6588
|
role: el.role,
|
|
6263
6589
|
labelSource: el.labelSource,
|
|
6264
|
-
looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel(el))
|
|
6590
|
+
looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel$1(el))
|
|
6265
6591
|
};
|
|
6266
6592
|
}
|
|
6267
6593
|
function normalizeStoredAction(action) {
|
|
@@ -6557,7 +6883,7 @@ function getCartItemLinks(page) {
|
|
|
6557
6883
|
return false;
|
|
6558
6884
|
}
|
|
6559
6885
|
if (blockedText.test(text) || blockedHref.test(href)) return false;
|
|
6560
|
-
const key = `${normalizeComparable(text)}|${normalizeUrlForMatch(href) || href}`;
|
|
6886
|
+
const key = `${normalizeComparable$1(text)}|${normalizeUrlForMatch$1(href) || href}`;
|
|
6561
6887
|
if (seen.has(key)) return false;
|
|
6562
6888
|
seen.add(key);
|
|
6563
6889
|
return true;
|
|
@@ -6626,6 +6952,67 @@ function formatCartSnapshot(page) {
|
|
|
6626
6952
|
function isVisibleToUser(el) {
|
|
6627
6953
|
return el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true;
|
|
6628
6954
|
}
|
|
6955
|
+
function purchaseActionPriority(el) {
|
|
6956
|
+
const haystack = normalizeComparable$1(
|
|
6957
|
+
[
|
|
6958
|
+
el.text,
|
|
6959
|
+
el.label,
|
|
6960
|
+
el.name,
|
|
6961
|
+
el.placeholder,
|
|
6962
|
+
el.description,
|
|
6963
|
+
el.href
|
|
6964
|
+
].filter(Boolean).join(" ")
|
|
6965
|
+
);
|
|
6966
|
+
if (!haystack) return Number.POSITIVE_INFINITY;
|
|
6967
|
+
if (/\badd(?: item)? to (?:cart|bag|basket)\b/.test(haystack)) return 0;
|
|
6968
|
+
if (/\b(?:buy now|preorder|pre-order|reserve now|shop now)\b/.test(haystack)) {
|
|
6969
|
+
return 1;
|
|
6970
|
+
}
|
|
6971
|
+
if (/\b(?:checkout|view cart|view basket|go to cart|view bag)\b/.test(haystack)) {
|
|
6972
|
+
return 2;
|
|
6973
|
+
}
|
|
6974
|
+
return Number.POSITIVE_INFINITY;
|
|
6975
|
+
}
|
|
6976
|
+
function isPurchaseActionElement(el) {
|
|
6977
|
+
if (el.type !== "button" && el.type !== "link" && !(el.type === "input" && (el.inputType === "submit" || el.inputType === "button"))) {
|
|
6978
|
+
return false;
|
|
6979
|
+
}
|
|
6980
|
+
return Number.isFinite(purchaseActionPriority(el));
|
|
6981
|
+
}
|
|
6982
|
+
function getPurchaseActionElements(page, options) {
|
|
6983
|
+
const visibleOnly = options?.visibleOnly !== false;
|
|
6984
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6985
|
+
return page.interactiveElements.filter((el) => {
|
|
6986
|
+
if (!isPurchaseActionElement(el)) return false;
|
|
6987
|
+
if (visibleOnly && !isVisibleToUser(el)) return false;
|
|
6988
|
+
if (el.blockedByOverlay) return false;
|
|
6989
|
+
const key = String(
|
|
6990
|
+
el.index ?? el.selector ?? `${el.type}|${el.text || ""}|${el.label || ""}|${el.href || ""}`
|
|
6991
|
+
);
|
|
6992
|
+
if (seen.has(key)) return false;
|
|
6993
|
+
seen.add(key);
|
|
6994
|
+
return true;
|
|
6995
|
+
}).sort((a, b) => {
|
|
6996
|
+
const delta = purchaseActionPriority(a) - purchaseActionPriority(b);
|
|
6997
|
+
if (delta !== 0) return delta;
|
|
6998
|
+
return (a.index ?? Number.MAX_SAFE_INTEGER) - (b.index ?? Number.MAX_SAFE_INTEGER);
|
|
6999
|
+
}).slice(0, 8);
|
|
7000
|
+
}
|
|
7001
|
+
function getOffscreenPurchaseActionElements(page) {
|
|
7002
|
+
const visibleKeys = new Set(
|
|
7003
|
+
getPurchaseActionElements(page, { visibleOnly: true }).map(
|
|
7004
|
+
(el) => String(
|
|
7005
|
+
el.index ?? el.selector ?? `${el.type}|${el.text || ""}|${el.label || ""}|${el.href || ""}`
|
|
7006
|
+
)
|
|
7007
|
+
)
|
|
7008
|
+
);
|
|
7009
|
+
return getPurchaseActionElements(page, { visibleOnly: false }).filter((el) => {
|
|
7010
|
+
const key = String(
|
|
7011
|
+
el.index ?? el.selector ?? `${el.type}|${el.text || ""}|${el.label || ""}|${el.href || ""}`
|
|
7012
|
+
);
|
|
7013
|
+
return !visibleKeys.has(key) && el.visible !== false;
|
|
7014
|
+
});
|
|
7015
|
+
}
|
|
6629
7016
|
function getDialogFocusedElements(page) {
|
|
6630
7017
|
return page.interactiveElements.filter(
|
|
6631
7018
|
(el) => isVisibleToUser(el) && el.context === "dialog"
|
|
@@ -6679,6 +7066,10 @@ function formatInteractiveElements(elements) {
|
|
|
6679
7066
|
const scoreEl = (el) => {
|
|
6680
7067
|
let s = 0;
|
|
6681
7068
|
if (el.context === "dialog") s -= 40;
|
|
7069
|
+
const purchasePriority = purchaseActionPriority(el);
|
|
7070
|
+
if (Number.isFinite(purchasePriority)) {
|
|
7071
|
+
s -= 25 - purchasePriority * 5;
|
|
7072
|
+
}
|
|
6682
7073
|
if (el.visible === false) s += 100;
|
|
6683
7074
|
if (el.inViewport === false) s += 50;
|
|
6684
7075
|
if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
|
|
@@ -7038,10 +7429,10 @@ function chooseAgentReadMode(page) {
|
|
|
7038
7429
|
return "visible_only";
|
|
7039
7430
|
}
|
|
7040
7431
|
}
|
|
7041
|
-
function normalizeComparable(value) {
|
|
7432
|
+
function normalizeComparable$1(value) {
|
|
7042
7433
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
|
|
7043
7434
|
}
|
|
7044
|
-
function normalizeUrlForMatch(value) {
|
|
7435
|
+
function normalizeUrlForMatch$1(value) {
|
|
7045
7436
|
if (!value) return null;
|
|
7046
7437
|
try {
|
|
7047
7438
|
const url = new URL(value);
|
|
@@ -7051,7 +7442,7 @@ function normalizeUrlForMatch(value) {
|
|
|
7051
7442
|
return value.trim().replace(/\/+$/, "").toLowerCase() || null;
|
|
7052
7443
|
}
|
|
7053
7444
|
}
|
|
7054
|
-
function getUrlPathSegments(value) {
|
|
7445
|
+
function getUrlPathSegments$1(value) {
|
|
7055
7446
|
if (!value) return [];
|
|
7056
7447
|
try {
|
|
7057
7448
|
return new URL(value).pathname.split("/").filter(Boolean);
|
|
@@ -7060,7 +7451,7 @@ function getUrlPathSegments(value) {
|
|
|
7060
7451
|
}
|
|
7061
7452
|
}
|
|
7062
7453
|
function isSearchOrListingPage(page) {
|
|
7063
|
-
const haystack = normalizeComparable(
|
|
7454
|
+
const haystack = normalizeComparable$1(
|
|
7064
7455
|
[
|
|
7065
7456
|
page.url,
|
|
7066
7457
|
page.title,
|
|
@@ -7100,30 +7491,30 @@ function getResultCandidates(page) {
|
|
|
7100
7491
|
const entityItems = collectJsonLdEntityItems(page.jsonLd ?? []);
|
|
7101
7492
|
const entityNames = new Set(
|
|
7102
7493
|
entityItems.map(
|
|
7103
|
-
(item) => typeof item.name === "string" ? normalizeComparable(item.name) : ""
|
|
7494
|
+
(item) => typeof item.name === "string" ? normalizeComparable$1(item.name) : ""
|
|
7104
7495
|
).filter(Boolean)
|
|
7105
7496
|
);
|
|
7106
7497
|
const entityUrls = new Set(
|
|
7107
7498
|
entityItems.map(
|
|
7108
|
-
(item) => typeof item.url === "string" ? normalizeUrlForMatch(item.url) : null
|
|
7499
|
+
(item) => typeof item.url === "string" ? normalizeUrlForMatch$1(item.url) : null
|
|
7109
7500
|
).filter((value) => Boolean(value))
|
|
7110
7501
|
);
|
|
7111
|
-
const pageHost = normalizeUrlForMatch(page.url);
|
|
7502
|
+
const pageHost = normalizeUrlForMatch$1(page.url);
|
|
7112
7503
|
const searchOrListingPage = isSearchOrListingPage(page);
|
|
7113
7504
|
const scored = page.interactiveElements.filter(
|
|
7114
7505
|
(element) => element.type === "link" && element.text?.trim() && element.href
|
|
7115
7506
|
).map((element) => {
|
|
7116
7507
|
const text = element.text?.trim() || "";
|
|
7117
|
-
const comparableText = normalizeComparable(text);
|
|
7118
|
-
const href = normalizeUrlForMatch(element.href);
|
|
7119
|
-
const haystack = normalizeComparable(
|
|
7508
|
+
const comparableText = normalizeComparable$1(text);
|
|
7509
|
+
const href = normalizeUrlForMatch$1(element.href);
|
|
7510
|
+
const haystack = normalizeComparable$1(
|
|
7120
7511
|
[element.text, element.description, element.selector, element.href].filter(Boolean).join(" ")
|
|
7121
7512
|
);
|
|
7122
7513
|
let score = 0;
|
|
7123
7514
|
if (entityNames.has(comparableText)) score += 6;
|
|
7124
7515
|
if (href && entityUrls.has(href)) score += 6;
|
|
7125
7516
|
if (entityItems.some((item) => {
|
|
7126
|
-
const name = typeof item.name === "string" ? normalizeComparable(item.name) : "";
|
|
7517
|
+
const name = typeof item.name === "string" ? normalizeComparable$1(item.name) : "";
|
|
7127
7518
|
return Boolean(name) && (name.includes(comparableText) || comparableText.includes(name));
|
|
7128
7519
|
})) {
|
|
7129
7520
|
score += 4;
|
|
@@ -7137,7 +7528,7 @@ function getResultCandidates(page) {
|
|
|
7137
7528
|
} catch {
|
|
7138
7529
|
}
|
|
7139
7530
|
}
|
|
7140
|
-
const hrefSegments = getUrlPathSegments(element.href);
|
|
7531
|
+
const hrefSegments = getUrlPathSegments$1(element.href);
|
|
7141
7532
|
if (hrefSegments.length >= 2) score += 1;
|
|
7142
7533
|
if (text.includes("/")) score += 1;
|
|
7143
7534
|
if (searchOrListingPage && (element.context === "article" || element.context === "main" || element.context === "content")) {
|
|
@@ -7170,7 +7561,7 @@ function getResultCandidates(page) {
|
|
|
7170
7561
|
);
|
|
7171
7562
|
const seen = /* @__PURE__ */ new Set();
|
|
7172
7563
|
return scored.map(({ element }) => element).filter((element) => {
|
|
7173
|
-
const key = `${normalizeComparable(element.text || "")}|${normalizeUrlForMatch(element.href) || ""}`;
|
|
7564
|
+
const key = `${normalizeComparable$1(element.text || "")}|${normalizeUrlForMatch$1(element.href) || ""}`;
|
|
7174
7565
|
if (seen.has(key)) return false;
|
|
7175
7566
|
seen.add(key);
|
|
7176
7567
|
return true;
|
|
@@ -7406,6 +7797,10 @@ function buildScopedContext(page, mode) {
|
|
|
7406
7797
|
})).filter((form) => form.fields.length > 0)
|
|
7407
7798
|
};
|
|
7408
7799
|
const quantityElements = getQuantityElements(visiblePage);
|
|
7800
|
+
const purchaseActions = getPurchaseActionElements(visiblePage, {
|
|
7801
|
+
visibleOnly: true
|
|
7802
|
+
});
|
|
7803
|
+
const offscreenPurchaseActions = getOffscreenPurchaseActionElements(page);
|
|
7409
7804
|
const cartSnapshot = formatCartSnapshot(visiblePage);
|
|
7410
7805
|
const visibleForms = visiblePage.forms;
|
|
7411
7806
|
const dialogFocus = formatDialogFocus(page);
|
|
@@ -7461,6 +7856,19 @@ function buildScopedContext(page, mode) {
|
|
|
7461
7856
|
sections.push(formatQuantityElements(quantityElements));
|
|
7462
7857
|
sections.push("");
|
|
7463
7858
|
}
|
|
7859
|
+
if (purchaseActions.length > 0) {
|
|
7860
|
+
sections.push("### Primary Purchase Actions");
|
|
7861
|
+
sections.push(formatInteractiveElements(purchaseActions));
|
|
7862
|
+
sections.push("");
|
|
7863
|
+
}
|
|
7864
|
+
if (offscreenPurchaseActions.length > 0) {
|
|
7865
|
+
sections.push("### Offscreen Purchase Actions");
|
|
7866
|
+
sections.push(
|
|
7867
|
+
"These purchase controls are present on the page but currently outside the viewport. You can scroll to reveal them or click them by index."
|
|
7868
|
+
);
|
|
7869
|
+
sections.push(formatInteractiveElements(offscreenPurchaseActions));
|
|
7870
|
+
sections.push("");
|
|
7871
|
+
}
|
|
7464
7872
|
if (visiblePage.interactiveElements.length > 0) {
|
|
7465
7873
|
sections.push(
|
|
7466
7874
|
`### Visible In-Viewport Interactive Elements (${visiblePage.interactiveElements.length})`
|
|
@@ -7774,7 +8182,7 @@ const SHARED_READ_INSTRUCTIONS = [
|
|
|
7774
8182
|
'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
8183
|
"Use screenshot when you need the exact rendered page or text extraction is failing.",
|
|
7776
8184
|
"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."
|
|
8185
|
+
"After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. When a click navigates to a new page, the click result includes a page snapshot — you only need read_page if you need details beyond what the snapshot shows."
|
|
7778
8186
|
];
|
|
7779
8187
|
const DEFAULT_EXTRA_INSTRUCTIONS = [
|
|
7780
8188
|
"Create a checkpoint before risky multi-step flows or before leaving an important state.",
|
|
@@ -7789,7 +8197,8 @@ const DEFAULT_EXTRA_INSTRUCTIONS = [
|
|
|
7789
8197
|
"After completing a task or answering a question, offer 1-2 brief, natural follow-up suggestions that make sense in context.",
|
|
7790
8198
|
'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
8199
|
"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."
|
|
8200
|
+
"NEVER USE EMOJIS unless the user uses them first.",
|
|
8201
|
+
"When adding multiple items to a cart, track which products you've already added. After adding an item, go back and select a DIFFERENT product. The system blocks duplicate cart additions and shows 'Already in cart' warnings."
|
|
7793
8202
|
];
|
|
7794
8203
|
const COMPACT_FOCUS_INSTRUCTIONS = [
|
|
7795
8204
|
"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.",
|
|
@@ -7799,6 +8208,10 @@ const COMPACT_FOCUS_INSTRUCTIONS = [
|
|
|
7799
8208
|
"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
8209
|
"Use current_tab only if you are genuinely unsure of the current page after reading the latest tool result.",
|
|
7801
8210
|
"On retail tasks, prefer this sequence: navigate -> site search or curated section -> inspect/read results -> click a product -> add to cart -> explain.",
|
|
8211
|
+
'On product/detail pages, prefer read_page(mode="visible_only") to find indexed purchase controls like Add to Cart, quantity, and checkout actions.',
|
|
8212
|
+
"When read_page or inspect_element gives you an element index, prefer click(index=N) over selector-based clicks.",
|
|
8213
|
+
'If a product page has no visible purchase control, scroll and call read_page(mode="visible_only") again. Do not loop on generic inspect_element calls against navigation or unrelated regions.',
|
|
8214
|
+
"After adding an item to cart and going back, ALWAYS call read_page to see the current results. The system shows which products are already in your cart — do NOT click those again. Pick a DIFFERENT product from the list. If all visible results are already in cart, scroll down for more.",
|
|
7802
8215
|
"Keep your reasoning short. Prefer taking the next tool action over writing a long plan."
|
|
7803
8216
|
];
|
|
7804
8217
|
function buildInstructionBlock(instructions) {
|
|
@@ -7849,6 +8262,342 @@ function buildAgentSystemPrompt(input) {
|
|
|
7849
8262
|
...instructionBlocks
|
|
7850
8263
|
].join("\n\n");
|
|
7851
8264
|
}
|
|
8265
|
+
function normalizeComparable(value) {
|
|
8266
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
|
|
8267
|
+
}
|
|
8268
|
+
function normalizeUrlForMatch(value) {
|
|
8269
|
+
if (!value) return null;
|
|
8270
|
+
try {
|
|
8271
|
+
const url = new URL(value);
|
|
8272
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
8273
|
+
return `${url.origin}${pathname}`.toLowerCase();
|
|
8274
|
+
} catch {
|
|
8275
|
+
return value.trim().replace(/\/+$/, "").toLowerCase() || null;
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
function getUrlPathSegments(value) {
|
|
8279
|
+
if (!value) return [];
|
|
8280
|
+
try {
|
|
8281
|
+
return new URL(value).pathname.split("/").filter(Boolean);
|
|
8282
|
+
} catch {
|
|
8283
|
+
return value.split("?")[0].split("#")[0].split("/").filter(Boolean);
|
|
8284
|
+
}
|
|
8285
|
+
}
|
|
8286
|
+
function isVisible(element) {
|
|
8287
|
+
return element.visible !== false && element.obscured !== true && element.blockedByOverlay !== true;
|
|
8288
|
+
}
|
|
8289
|
+
function isInViewport(element) {
|
|
8290
|
+
return element.inViewport !== false;
|
|
8291
|
+
}
|
|
8292
|
+
function looksLikeListingPage(page) {
|
|
8293
|
+
const haystack = normalizeComparable(
|
|
8294
|
+
[
|
|
8295
|
+
page.url,
|
|
8296
|
+
page.title,
|
|
8297
|
+
page.excerpt,
|
|
8298
|
+
page.content.slice(0, 2e3),
|
|
8299
|
+
page.headings.map((heading) => heading.text).join(" ")
|
|
8300
|
+
].filter(Boolean).join(" ")
|
|
8301
|
+
);
|
|
8302
|
+
return /\b(search|results|browse|discover|arrivals|new arrivals|staff picks|picks of the month|monthly picks|featured|bestsellers|best sellers|category|categories|books|book list|fiction|nonfiction|poetry|history|science|children)\b/.test(
|
|
8303
|
+
haystack
|
|
8304
|
+
);
|
|
8305
|
+
}
|
|
8306
|
+
function isBlockedLabel(label) {
|
|
8307
|
+
return /\b(home|menu|about|contact|privacy|terms|login|sign in|sign up|subscribe|newsletter|facebook|instagram|pinterest|share|print|next|previous|prev|sort|filter|wishlist|account|cart|checkout|view all|see all|refine|narrow|clear all|remove filter)\b/.test(
|
|
8308
|
+
label
|
|
8309
|
+
);
|
|
8310
|
+
}
|
|
8311
|
+
function resultScore(page, element, listingLike) {
|
|
8312
|
+
if (element.type !== "link" || !element.href || !element.text?.trim()) return -1;
|
|
8313
|
+
const text = element.text.trim();
|
|
8314
|
+
const comparableText = normalizeComparable(text);
|
|
8315
|
+
const href = normalizeUrlForMatch(element.href);
|
|
8316
|
+
const pageUrl = normalizeUrlForMatch(page.url);
|
|
8317
|
+
const hrefSegments = getUrlPathSegments(element.href);
|
|
8318
|
+
const haystack = normalizeComparable(
|
|
8319
|
+
[text, element.description, element.selector, element.href, element.context].filter(Boolean).join(" ")
|
|
8320
|
+
);
|
|
8321
|
+
if (!comparableText || isBlockedLabel(comparableText)) return -1;
|
|
8322
|
+
let score = 0;
|
|
8323
|
+
if (element.context === "article") score += 5;
|
|
8324
|
+
if (element.context === "main" || element.context === "content") score += 4;
|
|
8325
|
+
if (!element.context) score += 1;
|
|
8326
|
+
if (isVisible(element)) score += 2;
|
|
8327
|
+
if (isInViewport(element)) score += 3;
|
|
8328
|
+
if (text.length >= 8 && text.length <= 140) score += 2;
|
|
8329
|
+
if (text.split(/\s+/).length >= 2) score += 2;
|
|
8330
|
+
if (text.split(/\s+/).length >= 4) score += 1;
|
|
8331
|
+
if (hrefSegments.length >= 2) score += 1;
|
|
8332
|
+
if (hrefSegments.some((segment) => /book|books|item|product|title|catalog/.test(segment))) {
|
|
8333
|
+
score += 3;
|
|
8334
|
+
}
|
|
8335
|
+
if (href && pageUrl) {
|
|
8336
|
+
try {
|
|
8337
|
+
if (new URL(href).origin === new URL(pageUrl).origin) score += 2;
|
|
8338
|
+
} catch {
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
if (listingLike) score += 2;
|
|
8342
|
+
if (/\b(book|novel|story|poems|poetry|essays|memoir|history|science|fiction)\b/.test(haystack)) {
|
|
8343
|
+
score += 2;
|
|
8344
|
+
}
|
|
8345
|
+
if (/\b(author|hardcover|paperback|preorder|pre-order|signed edition)\b/.test(haystack)) {
|
|
8346
|
+
score += 1;
|
|
8347
|
+
}
|
|
8348
|
+
if (element.context === "nav" || element.context === "header" || element.context === "footer" || element.context === "sidebar" || element.context === "dialog") {
|
|
8349
|
+
score -= 6;
|
|
8350
|
+
}
|
|
8351
|
+
if (/\b(filter|sort|format|price|signed|staff picks|more results|view all)\b/.test(comparableText)) {
|
|
8352
|
+
score -= 3;
|
|
8353
|
+
}
|
|
8354
|
+
try {
|
|
8355
|
+
const linkUrl = new URL(element.href);
|
|
8356
|
+
const filterParams = ["condition", "binding", "format", "availability", "sort", "filter", "price", "category_id", "view"];
|
|
8357
|
+
if (filterParams.some((p) => linkUrl.searchParams.has(p))) {
|
|
8358
|
+
score -= 10;
|
|
8359
|
+
}
|
|
8360
|
+
const filterPathSegments = ["format", "condition", "binding", "availability", "sort"];
|
|
8361
|
+
const hasFilterPath = filterPathSegments.some(
|
|
8362
|
+
(seg) => hrefSegments.some((s) => s.toLowerCase() === seg)
|
|
8363
|
+
);
|
|
8364
|
+
if (hasFilterPath) {
|
|
8365
|
+
score -= 10;
|
|
8366
|
+
}
|
|
8367
|
+
} catch {
|
|
8368
|
+
}
|
|
8369
|
+
return score;
|
|
8370
|
+
}
|
|
8371
|
+
function getCompactPrimaryResultLinks(page, options) {
|
|
8372
|
+
const listingLike = looksLikeListingPage(page);
|
|
8373
|
+
const max = options?.max ?? 8;
|
|
8374
|
+
const visibleOnly = options?.visibleOnly === true;
|
|
8375
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8376
|
+
return page.interactiveElements.filter((element) => !visibleOnly || isVisible(element)).map((element) => ({
|
|
8377
|
+
element,
|
|
8378
|
+
score: resultScore(page, element, listingLike)
|
|
8379
|
+
})).filter(({ score }) => score >= (listingLike ? 5 : 7)).sort(
|
|
8380
|
+
(a, b) => b.score - a.score || (a.element.index ?? Number.MAX_SAFE_INTEGER) - (b.element.index ?? Number.MAX_SAFE_INTEGER)
|
|
8381
|
+
).map(({ element }) => element).filter((element) => {
|
|
8382
|
+
const key = `${normalizeComparable(element.text)}|${normalizeUrlForMatch(element.href) || ""}`;
|
|
8383
|
+
if (seen.has(key)) return false;
|
|
8384
|
+
seen.add(key);
|
|
8385
|
+
return true;
|
|
8386
|
+
}).slice(0, max);
|
|
8387
|
+
}
|
|
8388
|
+
function looksLikeProductPage(page) {
|
|
8389
|
+
const url = (page.url || "").toLowerCase();
|
|
8390
|
+
return /\/(book|product|item|detail|dp|gp\/product)\//i.test(url) || /\b(add to cart|add to bag|add to basket|buy now)\b/i.test(
|
|
8391
|
+
page.content.slice(0, 3e3)
|
|
8392
|
+
);
|
|
8393
|
+
}
|
|
8394
|
+
function chooseCompactReadMode(page, fallbackMode) {
|
|
8395
|
+
if (fallbackMode === "results_only") return fallbackMode;
|
|
8396
|
+
if (looksLikeProductPage(page)) return fallbackMode;
|
|
8397
|
+
const candidates = getCompactPrimaryResultLinks(page, { max: 6 });
|
|
8398
|
+
if (candidates.length >= 4) return "results_only";
|
|
8399
|
+
if (candidates.length >= 2 && looksLikeListingPage(page)) return "results_only";
|
|
8400
|
+
return fallbackMode;
|
|
8401
|
+
}
|
|
8402
|
+
const MAX_RESULTS = 6;
|
|
8403
|
+
const MAX_CONTROLS = 8;
|
|
8404
|
+
const MAX_FIELDS = 8;
|
|
8405
|
+
const MAX_HEADINGS = 5;
|
|
8406
|
+
const MAX_TEXT_CHARS = 420;
|
|
8407
|
+
function compactText(value, max = MAX_TEXT_CHARS) {
|
|
8408
|
+
const text = (value || "").replace(/\s+/g, " ").trim();
|
|
8409
|
+
if (!text) return "";
|
|
8410
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
8411
|
+
}
|
|
8412
|
+
function isVisibleElement(element) {
|
|
8413
|
+
return element.visible !== false && element.inViewport !== false && element.blockedByOverlay !== true && element.obscured !== true;
|
|
8414
|
+
}
|
|
8415
|
+
function elementLabel(element) {
|
|
8416
|
+
return compactText(
|
|
8417
|
+
element.text || element.label || element.placeholder || element.name || element.href || element.description,
|
|
8418
|
+
96
|
|
8419
|
+
) || "Element";
|
|
8420
|
+
}
|
|
8421
|
+
function formatElement(element) {
|
|
8422
|
+
const prefix = element.index != null ? `[#${element.index}] ` : "";
|
|
8423
|
+
const kind = element.type === "input" ? `${element.inputType || "text"} input` : element.type === "select" ? "select" : element.type;
|
|
8424
|
+
const href = element.type === "link" && element.href ? ` -> ${element.href}` : "";
|
|
8425
|
+
return `${prefix}${elementLabel(element)} (${kind})${href}`;
|
|
8426
|
+
}
|
|
8427
|
+
function uniqueElements(elements) {
|
|
8428
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8429
|
+
return elements.filter((element) => {
|
|
8430
|
+
const key = `${element.index ?? ""}|${element.type}|${elementLabel(element)}|${element.href ?? ""}`;
|
|
8431
|
+
if (seen.has(key)) return false;
|
|
8432
|
+
seen.add(key);
|
|
8433
|
+
return true;
|
|
8434
|
+
});
|
|
8435
|
+
}
|
|
8436
|
+
function isPaginationLike(element) {
|
|
8437
|
+
const text = `${element.text || ""} ${element.label || ""}`.toLowerCase();
|
|
8438
|
+
return /\b(next|prev|previous|load more|more results)\b/.test(text);
|
|
8439
|
+
}
|
|
8440
|
+
function getPrimaryResultLinks(page) {
|
|
8441
|
+
return uniqueElements(
|
|
8442
|
+
getCompactPrimaryResultLinks(page, {
|
|
8443
|
+
visibleOnly: true,
|
|
8444
|
+
max: MAX_RESULTS
|
|
8445
|
+
}).filter((element) => !isPaginationLike(element))
|
|
8446
|
+
);
|
|
8447
|
+
}
|
|
8448
|
+
function isPurchaseControl(element) {
|
|
8449
|
+
const text = `${element.text || ""} ${element.label || ""}`.toLowerCase();
|
|
8450
|
+
return /\b(add to cart|add to bag|add to basket|buy now|checkout|view cart)\b/.test(
|
|
8451
|
+
text
|
|
8452
|
+
);
|
|
8453
|
+
}
|
|
8454
|
+
function isAddToCartControl(element) {
|
|
8455
|
+
const text = `${element.text || ""} ${element.label || ""}`.toLowerCase();
|
|
8456
|
+
return /\badd to cart|add to bag|add to basket\b/.test(text);
|
|
8457
|
+
}
|
|
8458
|
+
function looksLikeProductDetailPage(page) {
|
|
8459
|
+
return /\/book\//i.test(page.url) || /\bbook\b/i.test(page.title);
|
|
8460
|
+
}
|
|
8461
|
+
function hasCartConfirmationState(page) {
|
|
8462
|
+
const haystack = compactText(
|
|
8463
|
+
[
|
|
8464
|
+
page.url,
|
|
8465
|
+
page.title,
|
|
8466
|
+
page.excerpt,
|
|
8467
|
+
page.content.slice(0, 1200),
|
|
8468
|
+
page.overlays.map((overlay) => overlay.label || overlay.message || overlay.text || overlay.kind || "").join(" ")
|
|
8469
|
+
].filter(Boolean).join(" "),
|
|
8470
|
+
1600
|
|
8471
|
+
).toLowerCase();
|
|
8472
|
+
return /\/cart\b/.test(page.url.toLowerCase()) || /\b(cart confirmation|added to cart|added to bag|added to basket|continue shopping|shopping cart|view cart|checkout)\b/.test(
|
|
8473
|
+
haystack
|
|
8474
|
+
);
|
|
8475
|
+
}
|
|
8476
|
+
function getVisibleControls(page) {
|
|
8477
|
+
return uniqueElements(page.interactiveElements.filter(isVisibleElement)).slice(
|
|
8478
|
+
0,
|
|
8479
|
+
MAX_CONTROLS
|
|
8480
|
+
);
|
|
8481
|
+
}
|
|
8482
|
+
function getVisiblePurchaseControls(page) {
|
|
8483
|
+
return uniqueElements(
|
|
8484
|
+
page.interactiveElements.filter(isVisibleElement).filter(isPurchaseControl).sort((a, b) => (a.index ?? Number.MAX_SAFE_INTEGER) - (b.index ?? Number.MAX_SAFE_INTEGER))
|
|
8485
|
+
).slice(0, 6);
|
|
8486
|
+
}
|
|
8487
|
+
function getOffscreenPurchaseControls(page) {
|
|
8488
|
+
const visibleKeys = new Set(getVisiblePurchaseControls(page).map(controlKey));
|
|
8489
|
+
return uniqueElements(
|
|
8490
|
+
page.interactiveElements.filter((element) => isPurchaseControl(element)).filter((element) => element.blockedByOverlay !== true).filter((element) => element.visible !== false).filter((element) => !visibleKeys.has(controlKey(element))).sort((a, b) => (a.index ?? Number.MAX_SAFE_INTEGER) - (b.index ?? Number.MAX_SAFE_INTEGER))
|
|
8491
|
+
).slice(0, 4);
|
|
8492
|
+
}
|
|
8493
|
+
function controlKey(element) {
|
|
8494
|
+
return `${element.index ?? ""}|${element.type}|${elementLabel(element)}|${element.href ?? ""}`;
|
|
8495
|
+
}
|
|
8496
|
+
function isLowValueListingControl(element) {
|
|
8497
|
+
const label = elementLabel(element).toLowerCase();
|
|
8498
|
+
return /\b(filter|sort|format|price|availability|signed edition|binding|language|refine|clear all|remove filter)\b/.test(
|
|
8499
|
+
label
|
|
8500
|
+
);
|
|
8501
|
+
}
|
|
8502
|
+
function getVisibleFormFields(page) {
|
|
8503
|
+
return uniqueElements(
|
|
8504
|
+
page.forms.flatMap((form) => form.fields).filter(isVisibleElement)
|
|
8505
|
+
).slice(0, MAX_FIELDS);
|
|
8506
|
+
}
|
|
8507
|
+
function pushSection(lines, title, items) {
|
|
8508
|
+
if (items.length === 0) return;
|
|
8509
|
+
lines.push("");
|
|
8510
|
+
lines.push(title);
|
|
8511
|
+
lines.push(...items.map((item) => `- ${item}`));
|
|
8512
|
+
}
|
|
8513
|
+
function buildTextSnapshot(page) {
|
|
8514
|
+
const excerpt = compactText(page.excerpt);
|
|
8515
|
+
if (excerpt) return [excerpt];
|
|
8516
|
+
const content = compactText(page.content);
|
|
8517
|
+
return content ? [content] : [];
|
|
8518
|
+
}
|
|
8519
|
+
function buildCompactScopedContext(page, mode, pageType = detectPageType(page)) {
|
|
8520
|
+
const lines = [
|
|
8521
|
+
`**URL:** ${page.url}`,
|
|
8522
|
+
`**Title:** ${page.title}`,
|
|
8523
|
+
`**Page Type:** ${pageType}`,
|
|
8524
|
+
`**Mode:** ${mode}`
|
|
8525
|
+
];
|
|
8526
|
+
if (page.byline) {
|
|
8527
|
+
lines.push(`**Author:** ${compactText(page.byline, 120)}`);
|
|
8528
|
+
}
|
|
8529
|
+
const warnings = (page.pageIssues || []).slice(0, 3).map((issue) => compactText(issue.summary, 140));
|
|
8530
|
+
pushSection(lines, "### Access Warnings", warnings);
|
|
8531
|
+
const blockingOverlays = page.overlays.filter((overlay) => overlay.blocksInteraction).slice(0, 3).map(
|
|
8532
|
+
(overlay) => compactText(
|
|
8533
|
+
overlay.label || overlay.message || overlay.text || overlay.kind || overlay.type,
|
|
8534
|
+
140
|
|
8535
|
+
)
|
|
8536
|
+
);
|
|
8537
|
+
pushSection(lines, "### Immediate Blockers", blockingOverlays);
|
|
8538
|
+
const visiblePurchaseControls = getVisiblePurchaseControls(page);
|
|
8539
|
+
const offscreenPurchaseControls = getOffscreenPurchaseControls(page);
|
|
8540
|
+
const purchaseControls = visiblePurchaseControls.map(formatElement);
|
|
8541
|
+
const addToCartVisible = visiblePurchaseControls.some(isAddToCartControl);
|
|
8542
|
+
const addToCartOffscreen = offscreenPurchaseControls.some(isAddToCartControl);
|
|
8543
|
+
if (looksLikeProductDetailPage(page) && !hasCartConfirmationState(page)) {
|
|
8544
|
+
if (addToCartVisible) {
|
|
8545
|
+
pushSection(lines, "### Action Status", [
|
|
8546
|
+
"Product detail page open. This item is not in the cart yet.",
|
|
8547
|
+
"Click Add to Cart and wait for cart confirmation before moving on."
|
|
8548
|
+
]);
|
|
8549
|
+
} else if (addToCartOffscreen) {
|
|
8550
|
+
pushSection(lines, "### Action Status", [
|
|
8551
|
+
"Product detail page open. This item is not in the cart yet.",
|
|
8552
|
+
"Add to Cart is present but outside the current viewport.",
|
|
8553
|
+
"Scroll once or use the offscreen purchase control below, then wait for cart confirmation."
|
|
8554
|
+
]);
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
pushSection(lines, "### Visible Purchase Controls", purchaseControls);
|
|
8558
|
+
pushSection(
|
|
8559
|
+
lines,
|
|
8560
|
+
"### Offscreen Purchase Actions",
|
|
8561
|
+
offscreenPurchaseControls.map(formatElement)
|
|
8562
|
+
);
|
|
8563
|
+
const primaryResultElements = getPrimaryResultLinks(page);
|
|
8564
|
+
const primaryResults = primaryResultElements.map(formatElement);
|
|
8565
|
+
if (primaryResults.length > 0) {
|
|
8566
|
+
pushSection(
|
|
8567
|
+
lines,
|
|
8568
|
+
"### Primary Results",
|
|
8569
|
+
primaryResults
|
|
8570
|
+
);
|
|
8571
|
+
}
|
|
8572
|
+
if (pageType === "FORM" || pageType === "LOGIN" || mode === "forms_only") {
|
|
8573
|
+
pushSection(
|
|
8574
|
+
lines,
|
|
8575
|
+
"### Form Fields",
|
|
8576
|
+
getVisibleFormFields(page).map(formatElement)
|
|
8577
|
+
);
|
|
8578
|
+
}
|
|
8579
|
+
if (mode === "visible_only" || mode === "interactives_only" || pageType === "SEARCH_READY" || pageType === "GENERAL") {
|
|
8580
|
+
const primaryResultKeys = new Set(primaryResultElements.map(controlKey));
|
|
8581
|
+
const visibleControls = getVisibleControls(page).filter((element) => !primaryResultKeys.has(controlKey(element))).filter(
|
|
8582
|
+
(element) => primaryResultElements.length > 0 ? !isLowValueListingControl(element) : true
|
|
8583
|
+
).map(formatElement);
|
|
8584
|
+
pushSection(
|
|
8585
|
+
lines,
|
|
8586
|
+
"### Visible Controls",
|
|
8587
|
+
visibleControls
|
|
8588
|
+
);
|
|
8589
|
+
}
|
|
8590
|
+
const headingItems = page.headings.slice(0, MAX_HEADINGS).map((heading) => `H${heading.level}: ${compactText(heading.text, 100)}`);
|
|
8591
|
+
pushSection(lines, "### Top Headings", headingItems);
|
|
8592
|
+
if (mode === "summary" || mode === "text_only" || lines.length <= 6) {
|
|
8593
|
+
pushSection(lines, "### Text Snapshot", buildTextSnapshot(page));
|
|
8594
|
+
}
|
|
8595
|
+
lines.push("");
|
|
8596
|
+
lines.push(
|
|
8597
|
+
`Stats: ${page.interactiveElements.length} interactives, ${page.forms.length} forms, ${page.navigation.length} nav links, ${page.headings.length} headings`
|
|
8598
|
+
);
|
|
8599
|
+
return lines.join("\n");
|
|
8600
|
+
}
|
|
7852
8601
|
const WRAPPING_QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
|
|
7853
8602
|
function stripWrappingQuotes(value) {
|
|
7854
8603
|
const trimmed = value.trim();
|
|
@@ -7994,10 +8743,11 @@ const TOOL_DEFINITIONS = [
|
|
|
7994
8743
|
{
|
|
7995
8744
|
name: "click",
|
|
7996
8745
|
title: "Click Element",
|
|
7997
|
-
description: "Click an element on the page by its index number or
|
|
8746
|
+
description: "Click an element on the page by its index number, CSS selector, or visible text/section name. If you know the label on the page but not the index yet, pass text instead of guessing a selector.",
|
|
7998
8747
|
inputSchema: {
|
|
7999
8748
|
index: zod.z.number().optional().describe("Element index from the page content listing"),
|
|
8000
|
-
selector: zod.z.string().optional().describe("CSS selector as fallback")
|
|
8749
|
+
selector: zod.z.string().optional().describe("CSS selector as fallback"),
|
|
8750
|
+
text: zod.z.string().optional().describe("Visible label, link text, button text, or section name to match")
|
|
8001
8751
|
},
|
|
8002
8752
|
tier: 0
|
|
8003
8753
|
},
|
|
@@ -8118,10 +8868,11 @@ const TOOL_DEFINITIONS = [
|
|
|
8118
8868
|
{
|
|
8119
8869
|
name: "inspect_element",
|
|
8120
8870
|
title: "Inspect Element",
|
|
8121
|
-
description: "Inspect one element and its nearest local UI region such as a product card, result row, form section, or modal.
|
|
8871
|
+
description: "Inspect one element and its nearest local UI region such as a product card, result row, form section, or modal. You can target it by index, selector, or visible text/section name when you know what it says but not where it is.",
|
|
8122
8872
|
inputSchema: {
|
|
8123
8873
|
index: zod.z.number().optional().describe("Element index to inspect"),
|
|
8124
8874
|
selector: zod.z.string().optional().describe("CSS selector to inspect"),
|
|
8875
|
+
text: zod.z.string().optional().describe("Visible label or section text to locate before inspecting"),
|
|
8125
8876
|
limit: zod.z.number().optional().describe("Maximum nearby controls to include (default 8)")
|
|
8126
8877
|
},
|
|
8127
8878
|
tier: 1,
|
|
@@ -8473,10 +9224,11 @@ const TOOL_DEFINITIONS = [
|
|
|
8473
9224
|
{
|
|
8474
9225
|
name: "scroll_to_element",
|
|
8475
9226
|
title: "Scroll To Element",
|
|
8476
|
-
description: "Scroll a specific element into view by index or
|
|
9227
|
+
description: "Scroll a specific element into view by index, selector, or visible text/section name. Useful for navigating to off-screen content when you know the heading or label you want.",
|
|
8477
9228
|
inputSchema: {
|
|
8478
9229
|
index: zod.z.number().optional().describe("Element index to scroll to"),
|
|
8479
9230
|
selector: zod.z.string().optional().describe("CSS selector to scroll to"),
|
|
9231
|
+
text: zod.z.string().optional().describe("Visible label or section text to scroll into view"),
|
|
8480
9232
|
position: zod.z.enum(["center", "top", "bottom"]).optional().describe(
|
|
8481
9233
|
"Where to position the element in the viewport (default center)"
|
|
8482
9234
|
)
|
|
@@ -9776,6 +10528,340 @@ function deleteNamedSession(name) {
|
|
|
9776
10528
|
fs$1.unlinkSync(filePath);
|
|
9777
10529
|
return true;
|
|
9778
10530
|
}
|
|
10531
|
+
function isInvalidTextTargetQuery(rawQuery) {
|
|
10532
|
+
const trimmed = String(rawQuery || "").trim();
|
|
10533
|
+
if (!trimmed) return true;
|
|
10534
|
+
if (/<\/?[a-z][^>]*>/i.test(trimmed)) return true;
|
|
10535
|
+
if (/^<\/?[a-z][^&]*>$/i.test(trimmed)) return true;
|
|
10536
|
+
if (/^<\/?[a-z][a-z0-9:-]*>$/i.test(trimmed)) return true;
|
|
10537
|
+
return false;
|
|
10538
|
+
}
|
|
10539
|
+
function resolveTextTargetInDocument(doc, rawQuery, mode) {
|
|
10540
|
+
function normalize(value) {
|
|
10541
|
+
return String(value || "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
10542
|
+
}
|
|
10543
|
+
function text(value) {
|
|
10544
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
10545
|
+
}
|
|
10546
|
+
function escapeSelectorValue(value) {
|
|
10547
|
+
const cssObject = typeof CSS !== "undefined" ? CSS : void 0;
|
|
10548
|
+
if (cssObject && typeof cssObject.escape === "function") {
|
|
10549
|
+
return cssObject.escape(value);
|
|
10550
|
+
}
|
|
10551
|
+
return String(value).replace(/["\\]/g, "\\$&");
|
|
10552
|
+
}
|
|
10553
|
+
function uniqueSelector(candidate) {
|
|
10554
|
+
if (!candidate) return null;
|
|
10555
|
+
try {
|
|
10556
|
+
return doc.querySelectorAll(candidate).length === 1 ? candidate : null;
|
|
10557
|
+
} catch {
|
|
10558
|
+
return null;
|
|
10559
|
+
}
|
|
10560
|
+
}
|
|
10561
|
+
function uniqueAttributeSelector(el, attribute) {
|
|
10562
|
+
const value = text(el.getAttribute(attribute));
|
|
10563
|
+
if (!value) return null;
|
|
10564
|
+
const candidate = `${el.tagName.toLowerCase()}[${attribute}="${escapeSelectorValue(value)}"]`;
|
|
10565
|
+
return uniqueSelector(candidate);
|
|
10566
|
+
}
|
|
10567
|
+
function selectorFor(el) {
|
|
10568
|
+
if (!el) return null;
|
|
10569
|
+
const htmlEl = el;
|
|
10570
|
+
if (htmlEl.id) return `#${escapeSelectorValue(htmlEl.id)}`;
|
|
10571
|
+
for (const attribute of [
|
|
10572
|
+
"data-testid",
|
|
10573
|
+
"name",
|
|
10574
|
+
"aria-label",
|
|
10575
|
+
"title",
|
|
10576
|
+
"href"
|
|
10577
|
+
]) {
|
|
10578
|
+
const candidate2 = uniqueAttributeSelector(el, attribute);
|
|
10579
|
+
if (candidate2) return candidate2;
|
|
10580
|
+
}
|
|
10581
|
+
const parts = [];
|
|
10582
|
+
let current = el;
|
|
10583
|
+
while (current) {
|
|
10584
|
+
const currentEl = current;
|
|
10585
|
+
if (currentEl.id) {
|
|
10586
|
+
parts.unshift(`#${escapeSelectorValue(currentEl.id)}`);
|
|
10587
|
+
break;
|
|
10588
|
+
}
|
|
10589
|
+
const parent = current.parentElement;
|
|
10590
|
+
const tag = current.tagName.toLowerCase();
|
|
10591
|
+
if (!parent) {
|
|
10592
|
+
parts.unshift(tag);
|
|
10593
|
+
break;
|
|
10594
|
+
}
|
|
10595
|
+
const siblings = Array.from(parent.children).filter(
|
|
10596
|
+
(child) => child.tagName === current.tagName
|
|
10597
|
+
);
|
|
10598
|
+
const index = siblings.indexOf(current) + 1;
|
|
10599
|
+
parts.unshift(
|
|
10600
|
+
siblings.length > 1 ? `${tag}:nth-of-type(${index})` : tag
|
|
10601
|
+
);
|
|
10602
|
+
current = parent;
|
|
10603
|
+
}
|
|
10604
|
+
const candidate = parts.join(" > ");
|
|
10605
|
+
return uniqueSelector(candidate) || candidate;
|
|
10606
|
+
}
|
|
10607
|
+
function isVisible2(el) {
|
|
10608
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
10609
|
+
if (el.hidden || el.getAttribute("aria-hidden") === "true") return false;
|
|
10610
|
+
const style = typeof getComputedStyle === "function" ? getComputedStyle(el) : null;
|
|
10611
|
+
if (style && (style.display === "none" || style.visibility === "hidden" || style.opacity === "0")) {
|
|
10612
|
+
return false;
|
|
10613
|
+
}
|
|
10614
|
+
return true;
|
|
10615
|
+
}
|
|
10616
|
+
function inViewport(el) {
|
|
10617
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
10618
|
+
if (typeof el.getBoundingClientRect !== "function") return true;
|
|
10619
|
+
const rect = el.getBoundingClientRect();
|
|
10620
|
+
if (rect.width === 0 && rect.height === 0) return true;
|
|
10621
|
+
const vw = window.innerWidth || doc.documentElement?.clientWidth || 0;
|
|
10622
|
+
const vh = window.innerHeight || doc.documentElement?.clientHeight || 0;
|
|
10623
|
+
return rect.bottom > 0 && rect.right > 0 && rect.top < vh && rect.left < vw;
|
|
10624
|
+
}
|
|
10625
|
+
function labelFor(el) {
|
|
10626
|
+
const htmlEl = el;
|
|
10627
|
+
return text(
|
|
10628
|
+
htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || htmlEl.getAttribute("name") || htmlEl.getAttribute("placeholder") || ("value" in htmlEl ? String(htmlEl.value || "") : "") || htmlEl.textContent
|
|
10629
|
+
) || el.tagName.toLowerCase();
|
|
10630
|
+
}
|
|
10631
|
+
function contentFor(el) {
|
|
10632
|
+
const ariaLabel = text(el.getAttribute?.("aria-label"));
|
|
10633
|
+
const title = text(el.getAttribute?.("title"));
|
|
10634
|
+
const ownText = text(el.textContent).slice(0, 300);
|
|
10635
|
+
return [ariaLabel, title, ownText].filter(Boolean).join(" ");
|
|
10636
|
+
}
|
|
10637
|
+
function scoreText(query2, candidate) {
|
|
10638
|
+
const normalizedCandidate = normalize(candidate);
|
|
10639
|
+
if (!normalizedCandidate) return -1;
|
|
10640
|
+
if (normalizedCandidate === query2) return 180;
|
|
10641
|
+
if (normalizedCandidate.startsWith(query2)) return 150;
|
|
10642
|
+
if (normalizedCandidate.includes(query2)) {
|
|
10643
|
+
return 130 - Math.min(30, normalizedCandidate.length - query2.length);
|
|
10644
|
+
}
|
|
10645
|
+
const words = query2.split(" ").filter((word) => word.length >= 3);
|
|
10646
|
+
if (words.length === 0) return -1;
|
|
10647
|
+
const overlap = words.filter((word) => normalizedCandidate.includes(word));
|
|
10648
|
+
if (overlap.length === 0) return -1;
|
|
10649
|
+
return overlap.length * 18;
|
|
10650
|
+
}
|
|
10651
|
+
function interactiveBonus(el) {
|
|
10652
|
+
const htmlEl = el;
|
|
10653
|
+
const tag = el.tagName.toLowerCase();
|
|
10654
|
+
const label = normalize(labelFor(el));
|
|
10655
|
+
let score = 0;
|
|
10656
|
+
if (tag === "button") score += 40;
|
|
10657
|
+
if (tag === "a") score += 35;
|
|
10658
|
+
if (tag === "input") score += 20;
|
|
10659
|
+
if (htmlEl.getAttribute("role") === "button") score += 25;
|
|
10660
|
+
if (/\b(shop|view|see|explore|browse|open|details|collection|discover)\b/.test(label)) {
|
|
10661
|
+
score += 30;
|
|
10662
|
+
}
|
|
10663
|
+
if (inViewport(el)) score += 25;
|
|
10664
|
+
return score;
|
|
10665
|
+
}
|
|
10666
|
+
function regionBonus(el) {
|
|
10667
|
+
const tag = el.tagName.toLowerCase();
|
|
10668
|
+
let score = 0;
|
|
10669
|
+
if (/^h[1-4]$/.test(tag)) score += 45;
|
|
10670
|
+
if (tag === "section" || tag === "article" || tag === "main") score += 30;
|
|
10671
|
+
if (el.getAttribute("role") === "heading") score += 35;
|
|
10672
|
+
if (inViewport(el)) score += 20;
|
|
10673
|
+
return score;
|
|
10674
|
+
}
|
|
10675
|
+
function consider(best, el, score, matchedText) {
|
|
10676
|
+
if (!Number.isFinite(score) || score < 0) return best;
|
|
10677
|
+
if (!best || score > best.score) return { el, score, matchedText };
|
|
10678
|
+
return best;
|
|
10679
|
+
}
|
|
10680
|
+
if (isInvalidTextTargetQuery(rawQuery)) return null;
|
|
10681
|
+
const query = normalize(rawQuery);
|
|
10682
|
+
if (!query) return null;
|
|
10683
|
+
let bestInteractive = null;
|
|
10684
|
+
const interactiveSelector = "a[href], button, [role='button'], input[type='submit'], input[type='button'], input[type='radio'], input[type='checkbox'], select, textarea";
|
|
10685
|
+
doc.querySelectorAll(interactiveSelector).forEach((el) => {
|
|
10686
|
+
if (!isVisible2(el)) return;
|
|
10687
|
+
const matchedText = labelFor(el);
|
|
10688
|
+
const score = scoreText(query, matchedText) + scoreText(query, contentFor(el)) + interactiveBonus(el);
|
|
10689
|
+
bestInteractive = consider(bestInteractive, el, score, matchedText);
|
|
10690
|
+
});
|
|
10691
|
+
if (mode === "interactive" && bestInteractive && bestInteractive.score >= 120) {
|
|
10692
|
+
const selector2 = selectorFor(bestInteractive.el);
|
|
10693
|
+
if (selector2) {
|
|
10694
|
+
return {
|
|
10695
|
+
selector: selector2,
|
|
10696
|
+
label: labelFor(bestInteractive.el),
|
|
10697
|
+
kind: bestInteractive.el.tagName.toLowerCase(),
|
|
10698
|
+
matchedText: bestInteractive.matchedText
|
|
10699
|
+
};
|
|
10700
|
+
}
|
|
10701
|
+
}
|
|
10702
|
+
let bestRegion = null;
|
|
10703
|
+
const regionSelector = "h1, h2, h3, h4, [role='heading'], section, article, main, aside, li, [data-testid], div";
|
|
10704
|
+
let seenRegions = 0;
|
|
10705
|
+
doc.querySelectorAll(regionSelector).forEach((el) => {
|
|
10706
|
+
if (seenRegions >= 400 || !isVisible2(el)) return;
|
|
10707
|
+
seenRegions += 1;
|
|
10708
|
+
const matchedText = contentFor(el);
|
|
10709
|
+
const score = scoreText(query, matchedText) + regionBonus(el);
|
|
10710
|
+
bestRegion = consider(bestRegion, el, score, matchedText);
|
|
10711
|
+
});
|
|
10712
|
+
if (!bestRegion || bestRegion.score < 80) {
|
|
10713
|
+
if (mode === "interactive" && bestInteractive) {
|
|
10714
|
+
const selector2 = selectorFor(bestInteractive.el);
|
|
10715
|
+
if (!selector2) return null;
|
|
10716
|
+
return {
|
|
10717
|
+
selector: selector2,
|
|
10718
|
+
label: labelFor(bestInteractive.el),
|
|
10719
|
+
kind: bestInteractive.el.tagName.toLowerCase(),
|
|
10720
|
+
matchedText: bestInteractive.matchedText
|
|
10721
|
+
};
|
|
10722
|
+
}
|
|
10723
|
+
return null;
|
|
10724
|
+
}
|
|
10725
|
+
if (mode === "context") {
|
|
10726
|
+
const selector2 = selectorFor(bestRegion.el);
|
|
10727
|
+
if (!selector2) return null;
|
|
10728
|
+
return {
|
|
10729
|
+
selector: selector2,
|
|
10730
|
+
label: labelFor(bestRegion.el),
|
|
10731
|
+
kind: bestRegion.el.tagName.toLowerCase(),
|
|
10732
|
+
matchedText: bestRegion.matchedText
|
|
10733
|
+
};
|
|
10734
|
+
}
|
|
10735
|
+
let regionAction = null;
|
|
10736
|
+
bestRegion.el.querySelectorAll(interactiveSelector).forEach((el) => {
|
|
10737
|
+
if (!isVisible2(el)) return;
|
|
10738
|
+
const matchedText = labelFor(el);
|
|
10739
|
+
const score = scoreText(query, matchedText) + interactiveBonus(el) + (bestRegion ? Math.floor(bestRegion.score / 4) : 0);
|
|
10740
|
+
regionAction = consider(regionAction, el, score, matchedText);
|
|
10741
|
+
});
|
|
10742
|
+
const chosen = regionAction || bestInteractive;
|
|
10743
|
+
if (!chosen) return null;
|
|
10744
|
+
const selector = selectorFor(chosen.el);
|
|
10745
|
+
if (!selector) return null;
|
|
10746
|
+
return {
|
|
10747
|
+
selector,
|
|
10748
|
+
label: labelFor(chosen.el),
|
|
10749
|
+
kind: chosen.el.tagName.toLowerCase(),
|
|
10750
|
+
matchedText: chosen.matchedText
|
|
10751
|
+
};
|
|
10752
|
+
}
|
|
10753
|
+
function normalizeWhitespace(text) {
|
|
10754
|
+
return text.replace(/\r/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
10755
|
+
}
|
|
10756
|
+
function limitText(text, maxLines, maxChars) {
|
|
10757
|
+
const normalized = normalizeWhitespace(text);
|
|
10758
|
+
const lines = normalized.split("\n");
|
|
10759
|
+
const trimmedLines = lines.slice(0, maxLines);
|
|
10760
|
+
let limited = trimmedLines.join("\n");
|
|
10761
|
+
if (limited.length > maxChars) {
|
|
10762
|
+
limited = `${limited.slice(0, maxChars - 1)}…`;
|
|
10763
|
+
} else if (lines.length > maxLines) {
|
|
10764
|
+
limited += "\n…";
|
|
10765
|
+
}
|
|
10766
|
+
return limited;
|
|
10767
|
+
}
|
|
10768
|
+
function extractSection(text, heading) {
|
|
10769
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10770
|
+
const match = text.match(
|
|
10771
|
+
new RegExp(`${escaped}\\n([\\s\\S]*?)(?=\\n### |\\n## |$)`)
|
|
10772
|
+
);
|
|
10773
|
+
if (!match) return null;
|
|
10774
|
+
return `${heading}
|
|
10775
|
+
${match[1].trim()}`;
|
|
10776
|
+
}
|
|
10777
|
+
function compactReadPageResult(text) {
|
|
10778
|
+
const cleaned = normalizeWhitespace(
|
|
10779
|
+
text.replace(
|
|
10780
|
+
/\n+Need more detail\? Escalate with read_page\(mode="debug"\) only if the narrow modes are insufficient\.\s*$/s,
|
|
10781
|
+
""
|
|
10782
|
+
)
|
|
10783
|
+
);
|
|
10784
|
+
const readHeader = cleaned.match(/^\[read_page mode=[^\]]+\]/m)?.[0];
|
|
10785
|
+
const mandatoryHeadings = [
|
|
10786
|
+
"### Action Status",
|
|
10787
|
+
"### Visible Purchase Controls",
|
|
10788
|
+
"### Offscreen Purchase Actions"
|
|
10789
|
+
];
|
|
10790
|
+
const optionalHeadings = [
|
|
10791
|
+
"### Access Warnings",
|
|
10792
|
+
"### Immediate Blockers",
|
|
10793
|
+
"### Likely Search Results",
|
|
10794
|
+
"### Primary Results",
|
|
10795
|
+
"### Form Fields",
|
|
10796
|
+
"### Visible Controls",
|
|
10797
|
+
"### Top Headings",
|
|
10798
|
+
"### Text Snapshot"
|
|
10799
|
+
];
|
|
10800
|
+
const mandatorySections = mandatoryHeadings.map((heading) => extractSection(cleaned, heading)).filter((value) => Boolean(value));
|
|
10801
|
+
const optionalSections = optionalHeadings.map((heading) => extractSection(cleaned, heading)).filter((value) => Boolean(value));
|
|
10802
|
+
const allSections = [...mandatorySections, ...optionalSections];
|
|
10803
|
+
if (allSections.length === 0) {
|
|
10804
|
+
return limitText(cleaned, 22, 1800);
|
|
10805
|
+
}
|
|
10806
|
+
const metaLines = cleaned.split("\n").filter((line) => /^\*\*(URL|Title|Page Type|Mode|Author):\*\*/.test(line)).slice(0, 5);
|
|
10807
|
+
const maxOptional = Math.max(0, 5 - mandatorySections.length);
|
|
10808
|
+
const keptSections = [
|
|
10809
|
+
...mandatorySections,
|
|
10810
|
+
...optionalSections.slice(0, maxOptional)
|
|
10811
|
+
];
|
|
10812
|
+
return [
|
|
10813
|
+
readHeader,
|
|
10814
|
+
metaLines.join("\n"),
|
|
10815
|
+
keptSections.join("\n\n")
|
|
10816
|
+
].filter(Boolean).join("\n\n");
|
|
10817
|
+
}
|
|
10818
|
+
function compactSearchLikeResult(text) {
|
|
10819
|
+
const cleaned = normalizeWhitespace(text);
|
|
10820
|
+
const marker = "\nSearch results snapshot:\n";
|
|
10821
|
+
const markerIndex = cleaned.indexOf(marker);
|
|
10822
|
+
if (markerIndex === -1) {
|
|
10823
|
+
return limitText(cleaned, 16, 1400);
|
|
10824
|
+
}
|
|
10825
|
+
const summary = cleaned.slice(0, markerIndex).trim();
|
|
10826
|
+
const snapshot = cleaned.slice(markerIndex + marker.length).trim();
|
|
10827
|
+
return [summary, compactReadPageResult(snapshot)].filter(Boolean).join("\n\n");
|
|
10828
|
+
}
|
|
10829
|
+
function compactCurrentTabResult(text) {
|
|
10830
|
+
try {
|
|
10831
|
+
const parsed = JSON.parse(text);
|
|
10832
|
+
if (!parsed || typeof parsed !== "object") return limitText(text, 8, 500);
|
|
10833
|
+
return [
|
|
10834
|
+
`Current tab: ${parsed.title || "(untitled)"}`,
|
|
10835
|
+
parsed.url ? `URL: ${parsed.url}` : "",
|
|
10836
|
+
`State: loading=${parsed.isLoading ? "yes" : "no"}, back=${parsed.canGoBack ? "yes" : "no"}, forward=${parsed.canGoForward ? "yes" : "no"}`
|
|
10837
|
+
].filter(Boolean).join("\n");
|
|
10838
|
+
} catch {
|
|
10839
|
+
return limitText(text, 8, 500);
|
|
10840
|
+
}
|
|
10841
|
+
}
|
|
10842
|
+
function looksLikeRichToolResult(text) {
|
|
10843
|
+
return text.startsWith("{") && text.includes('"__richResult":true');
|
|
10844
|
+
}
|
|
10845
|
+
function formatCompactToolResult(name, result) {
|
|
10846
|
+
if (!result || looksLikeRichToolResult(result)) return result;
|
|
10847
|
+
switch (name) {
|
|
10848
|
+
case "current_tab":
|
|
10849
|
+
return compactCurrentTabResult(result);
|
|
10850
|
+
case "read_page":
|
|
10851
|
+
return compactReadPageResult(result);
|
|
10852
|
+
case "search":
|
|
10853
|
+
case "navigate":
|
|
10854
|
+
case "go_back":
|
|
10855
|
+
case "go_forward":
|
|
10856
|
+
case "paginate":
|
|
10857
|
+
case "wait_for_navigation":
|
|
10858
|
+
return compactSearchLikeResult(result);
|
|
10859
|
+
case "list_tabs":
|
|
10860
|
+
return limitText(result, 10, 900);
|
|
10861
|
+
default:
|
|
10862
|
+
return limitText(result, 18, 1400);
|
|
10863
|
+
}
|
|
10864
|
+
}
|
|
9779
10865
|
const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS = 1500;
|
|
9780
10866
|
const QUIET_NAVIGATION_WINDOW_MS = 1200;
|
|
9781
10867
|
const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
|
|
@@ -10132,6 +11218,25 @@ ${truncated}`;
|
|
|
10132
11218
|
Search results snapshot unavailable. Use read_page(mode="results_only") if needed.` : `
|
|
10133
11219
|
Search results snapshot unavailable. Use read_page(mode="results_only") if needed.`;
|
|
10134
11220
|
}
|
|
11221
|
+
async function getPostClickNavSummary(wc, toolProfile) {
|
|
11222
|
+
try {
|
|
11223
|
+
const content = await Promise.race([
|
|
11224
|
+
extractContent(wc),
|
|
11225
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
|
|
11226
|
+
]);
|
|
11227
|
+
if (content && content.content.length > 0) {
|
|
11228
|
+
const scoped = toolProfile === "compact" ? buildCompactScopedContext(content, "visible_only") : buildScopedContext(content, "visible_only");
|
|
11229
|
+
const maxLen = toolProfile === "compact" ? 1800 : 3e3;
|
|
11230
|
+
const truncated = scoped.length > maxLen ? `${scoped.slice(0, maxLen)}
|
|
11231
|
+
[Page snapshot truncated. Use read_page for full details.]` : scoped;
|
|
11232
|
+
return `
|
|
11233
|
+
Page snapshot after navigation:
|
|
11234
|
+
${truncated}`;
|
|
11235
|
+
}
|
|
11236
|
+
} catch {
|
|
11237
|
+
}
|
|
11238
|
+
return "";
|
|
11239
|
+
}
|
|
10135
11240
|
async function scrollPage$1(wc, deltaY) {
|
|
10136
11241
|
const getScrollY = async () => {
|
|
10137
11242
|
const scrollY = await executePageScript(
|
|
@@ -10324,9 +11429,17 @@ async function describeElementForClick$1(wc, selector) {
|
|
|
10324
11429
|
if (!el) return { error: "Element not found" };
|
|
10325
11430
|
const anchor = el instanceof HTMLAnchorElement ? el : el.closest("a[href]");
|
|
10326
11431
|
const text = (el.textContent || el.tagName || "Element").trim().slice(0, 100);
|
|
11432
|
+
const tag = el.tagName.toLowerCase();
|
|
11433
|
+
const interactiveTags = new Set(["a","button","input","select","textarea","summary","details","option"]);
|
|
11434
|
+
const hasRole = el.getAttribute("role") === "button" || el.getAttribute("role") === "link" || el.getAttribute("role") === "tab";
|
|
11435
|
+
const hasClickListener = el.onclick != null || el.getAttribute("onclick") != null;
|
|
11436
|
+
const isInteractive = interactiveTags.has(tag) || hasRole || hasClickListener || !!anchor;
|
|
10327
11437
|
return {
|
|
10328
11438
|
text: text || "Element",
|
|
10329
11439
|
href: anchor instanceof HTMLAnchorElement ? anchor.href : undefined,
|
|
11440
|
+
target: anchor instanceof HTMLAnchorElement ? (anchor.getAttribute("target") || "") : undefined,
|
|
11441
|
+
tag,
|
|
11442
|
+
isInteractive,
|
|
10330
11443
|
};
|
|
10331
11444
|
})()
|
|
10332
11445
|
`,
|
|
@@ -10345,7 +11458,10 @@ async function describeElementForClick$1(wc, selector) {
|
|
|
10345
11458
|
}
|
|
10346
11459
|
return {
|
|
10347
11460
|
text: "text" in result && typeof result.text === "string" ? result.text : "Element",
|
|
10348
|
-
href: "href" in result && typeof result.href === "string" ? result.href : void 0
|
|
11461
|
+
href: "href" in result && typeof result.href === "string" ? result.href : void 0,
|
|
11462
|
+
target: "target" in result && typeof result.target === "string" ? result.target : void 0,
|
|
11463
|
+
tag: "tag" in result && typeof result.tag === "string" ? result.tag : void 0,
|
|
11464
|
+
isInteractive: "isInteractive" in result && typeof result.isInteractive === "boolean" ? result.isInteractive : void 0
|
|
10349
11465
|
};
|
|
10350
11466
|
}
|
|
10351
11467
|
async function inspectElement(wc, selector, limit = 8) {
|
|
@@ -10429,6 +11545,18 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
10429
11545
|
) || "element";
|
|
10430
11546
|
}
|
|
10431
11547
|
|
|
11548
|
+
function purchasePriority(label, href) {
|
|
11549
|
+
const haystack = ((label || "") + " " + (href || ""))
|
|
11550
|
+
.toLowerCase()
|
|
11551
|
+
.replace(/s+/g, " ")
|
|
11552
|
+
.trim();
|
|
11553
|
+
if (!haystack) return null;
|
|
11554
|
+
if (/\badd(?: item)? to (?:cart|bag|basket)\b/.test(haystack)) return 0;
|
|
11555
|
+
if (/\b(?:buy now|preorder|pre-order|reserve now|shop now)\b/.test(haystack)) return 1;
|
|
11556
|
+
if (/\b(?:checkout|view cart|view basket|go to cart|view bag)\b/.test(haystack)) return 2;
|
|
11557
|
+
return null;
|
|
11558
|
+
}
|
|
11559
|
+
|
|
10432
11560
|
function chooseRegion(target) {
|
|
10433
11561
|
const preferred = target.closest(
|
|
10434
11562
|
"[data-testid], article, [role='article'], [role='listitem'], li, tr, form, section, aside, dialog, [role='dialog']"
|
|
@@ -10454,19 +11582,69 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
10454
11582
|
const region = chooseRegion(target);
|
|
10455
11583
|
const nearby = [];
|
|
10456
11584
|
const seen = new Set();
|
|
11585
|
+
const purchaseActions = [];
|
|
11586
|
+
const purchaseSeen = new Set();
|
|
10457
11587
|
region.querySelectorAll("a[href], button, input:not([type='hidden']), select, textarea").forEach((el) => {
|
|
10458
11588
|
if (!(el instanceof HTMLElement) || !isVisible(el)) return;
|
|
10459
11589
|
const candidateSelector = selectorFor(el);
|
|
10460
11590
|
if (!candidateSelector || seen.has(candidateSelector)) return;
|
|
10461
11591
|
seen.add(candidateSelector);
|
|
11592
|
+
const candidateLabel = labelFor(el).slice(0, 100);
|
|
11593
|
+
const candidateHref = el instanceof HTMLAnchorElement ? text(el.href) : undefined;
|
|
10462
11594
|
nearby.push({
|
|
10463
|
-
|
|
11595
|
+
index: typeof window.__vessel?.getElementIndexBySelector === "function"
|
|
11596
|
+
? window.__vessel.getElementIndexBySelector(candidateSelector) ?? undefined
|
|
11597
|
+
: undefined,
|
|
11598
|
+
label: candidateLabel,
|
|
11599
|
+
type: el.tagName.toLowerCase(),
|
|
11600
|
+
selector: candidateSelector,
|
|
11601
|
+
href: candidateHref,
|
|
11602
|
+
});
|
|
11603
|
+
const purchaseRank = purchasePriority(candidateLabel, candidateHref);
|
|
11604
|
+
if (purchaseRank !== null && !purchaseSeen.has(candidateSelector)) {
|
|
11605
|
+
purchaseSeen.add(candidateSelector);
|
|
11606
|
+
purchaseActions.push({
|
|
11607
|
+
index: typeof window.__vessel?.getElementIndexBySelector === "function"
|
|
11608
|
+
? window.__vessel.getElementIndexBySelector(candidateSelector) ?? undefined
|
|
11609
|
+
: undefined,
|
|
11610
|
+
label: candidateLabel,
|
|
11611
|
+
type: el.tagName.toLowerCase(),
|
|
11612
|
+
selector: candidateSelector,
|
|
11613
|
+
href: candidateHref,
|
|
11614
|
+
source: "nearby",
|
|
11615
|
+
rank: purchaseRank,
|
|
11616
|
+
});
|
|
11617
|
+
}
|
|
11618
|
+
});
|
|
11619
|
+
|
|
11620
|
+
document.querySelectorAll("button, a[href], input[type='submit'], input[type='button']").forEach((el) => {
|
|
11621
|
+
if (!(el instanceof HTMLElement) || !isVisible(el)) return;
|
|
11622
|
+
const candidateSelector = selectorFor(el);
|
|
11623
|
+
if (!candidateSelector || purchaseSeen.has(candidateSelector)) return;
|
|
11624
|
+
const candidateLabel = labelFor(el).slice(0, 100);
|
|
11625
|
+
const candidateHref = el instanceof HTMLAnchorElement ? text(el.href) : undefined;
|
|
11626
|
+
const purchaseRank = purchasePriority(candidateLabel, candidateHref);
|
|
11627
|
+
if (purchaseRank === null) return;
|
|
11628
|
+
purchaseSeen.add(candidateSelector);
|
|
11629
|
+
purchaseActions.push({
|
|
11630
|
+
index: typeof window.__vessel?.getElementIndexBySelector === "function"
|
|
11631
|
+
? window.__vessel.getElementIndexBySelector(candidateSelector) ?? undefined
|
|
11632
|
+
: undefined,
|
|
11633
|
+
label: candidateLabel,
|
|
10464
11634
|
type: el.tagName.toLowerCase(),
|
|
10465
11635
|
selector: candidateSelector,
|
|
10466
|
-
href:
|
|
11636
|
+
href: candidateHref,
|
|
11637
|
+
source: "page",
|
|
11638
|
+
rank: purchaseRank,
|
|
10467
11639
|
});
|
|
10468
11640
|
});
|
|
10469
11641
|
|
|
11642
|
+
purchaseActions.sort((a, b) => {
|
|
11643
|
+
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
11644
|
+
if (a.source !== b.source) return a.source === "nearby" ? -1 : 1;
|
|
11645
|
+
return a.label.localeCompare(b.label);
|
|
11646
|
+
});
|
|
11647
|
+
|
|
10470
11648
|
return {
|
|
10471
11649
|
target: {
|
|
10472
11650
|
label: labelFor(target).slice(0, 120),
|
|
@@ -10483,6 +11661,14 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
10483
11661
|
text: text(region.textContent)?.slice(0, 400),
|
|
10484
11662
|
},
|
|
10485
11663
|
nearby: nearby.slice(0, ${Math.max(1, Math.min(20, limit))}),
|
|
11664
|
+
purchaseActions: purchaseActions.slice(0, 8).map((item) => ({
|
|
11665
|
+
index: item.index,
|
|
11666
|
+
label: item.label,
|
|
11667
|
+
type: item.type,
|
|
11668
|
+
selector: item.selector,
|
|
11669
|
+
href: item.href,
|
|
11670
|
+
source: item.source,
|
|
11671
|
+
})),
|
|
10486
11672
|
};
|
|
10487
11673
|
})()
|
|
10488
11674
|
`,
|
|
@@ -10515,11 +11701,26 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
10515
11701
|
lines.push("Nearby controls:");
|
|
10516
11702
|
for (const item of result.nearby) {
|
|
10517
11703
|
const hrefSuffix = item.href ? ` -> ${item.href}` : "";
|
|
11704
|
+
const indexPrefix = typeof item.index === "number" ? `[#${item.index}] ` : "";
|
|
10518
11705
|
lines.push(
|
|
10519
|
-
`- ${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
|
|
11706
|
+
`- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
|
|
10520
11707
|
);
|
|
10521
11708
|
}
|
|
10522
11709
|
}
|
|
11710
|
+
if (Array.isArray(result.purchaseActions) && result.purchaseActions.length > 0) {
|
|
11711
|
+
lines.push("Likely purchase actions:");
|
|
11712
|
+
for (const item of result.purchaseActions) {
|
|
11713
|
+
const hrefSuffix = item.href ? ` -> ${item.href}` : "";
|
|
11714
|
+
const sourceSuffix = item.source === "nearby" ? " (same region)" : " (elsewhere on page)";
|
|
11715
|
+
const indexPrefix = typeof item.index === "number" ? `[#${item.index}] ` : "";
|
|
11716
|
+
lines.push(
|
|
11717
|
+
`- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}${sourceSuffix}`
|
|
11718
|
+
);
|
|
11719
|
+
}
|
|
11720
|
+
lines.push(
|
|
11721
|
+
"When an index is available, prefer click(index=N) over selector-based clicks because it is more stable."
|
|
11722
|
+
);
|
|
11723
|
+
}
|
|
10523
11724
|
return lines.join("\n");
|
|
10524
11725
|
}
|
|
10525
11726
|
async function getLocaleSnapshot(wc) {
|
|
@@ -10607,6 +11808,11 @@ const ADD_TO_CART_PATTERNS = [
|
|
|
10607
11808
|
];
|
|
10608
11809
|
const recentCartClicks = /* @__PURE__ */ new Map();
|
|
10609
11810
|
const CART_CLICK_COOLDOWN_MS = 15e3;
|
|
11811
|
+
const CART_ADDED_TTL_MS = 30 * 6e4;
|
|
11812
|
+
const cartAddedProducts = /* @__PURE__ */ new Map();
|
|
11813
|
+
let clickStreakUrl = null;
|
|
11814
|
+
let clickStreakCount = 0;
|
|
11815
|
+
const CLICK_STREAK_THRESHOLD = 3;
|
|
10610
11816
|
function isAddToCartText(text) {
|
|
10611
11817
|
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
10612
11818
|
return ADD_TO_CART_PATTERNS.some((p) => normalized.includes(p));
|
|
@@ -10628,18 +11834,113 @@ function isDuplicateCartClick(url, text) {
|
|
|
10628
11834
|
}
|
|
10629
11835
|
return isAddToCartText(text);
|
|
10630
11836
|
}
|
|
11837
|
+
async function getProductPageTitle(wc) {
|
|
11838
|
+
try {
|
|
11839
|
+
const heading = await executePageScript(
|
|
11840
|
+
wc,
|
|
11841
|
+
`(function() {
|
|
11842
|
+
var h1 = document.querySelector('h1');
|
|
11843
|
+
if (h1 && h1.textContent.trim().length > 3 && h1.textContent.trim().length < 200) {
|
|
11844
|
+
return h1.textContent.trim();
|
|
11845
|
+
}
|
|
11846
|
+
var meta = document.querySelector('meta[property="og:title"]');
|
|
11847
|
+
if (meta && meta.content && meta.content.trim().length > 3) {
|
|
11848
|
+
return meta.content.trim();
|
|
11849
|
+
}
|
|
11850
|
+
return '';
|
|
11851
|
+
})()`,
|
|
11852
|
+
{ timeoutMs: 800, label: "get product title" }
|
|
11853
|
+
);
|
|
11854
|
+
if (heading && heading !== PAGE_SCRIPT_TIMEOUT && typeof heading === "string" && heading.length > 0) {
|
|
11855
|
+
return heading;
|
|
11856
|
+
}
|
|
11857
|
+
} catch {
|
|
11858
|
+
}
|
|
11859
|
+
return wc.getTitle() || "";
|
|
11860
|
+
}
|
|
11861
|
+
function normalizeCartProductKey(url) {
|
|
11862
|
+
try {
|
|
11863
|
+
const parsed = new URL(url);
|
|
11864
|
+
const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
11865
|
+
return `${parsed.origin}${pathname}`;
|
|
11866
|
+
} catch {
|
|
11867
|
+
return url;
|
|
11868
|
+
}
|
|
11869
|
+
}
|
|
11870
|
+
function pruneCartAddedProducts(now = Date.now()) {
|
|
11871
|
+
for (const [key, entry] of cartAddedProducts) {
|
|
11872
|
+
if (now - entry.ts > CART_ADDED_TTL_MS) {
|
|
11873
|
+
cartAddedProducts.delete(key);
|
|
11874
|
+
}
|
|
11875
|
+
}
|
|
11876
|
+
}
|
|
11877
|
+
function cartOrigin(url) {
|
|
11878
|
+
if (!url) return null;
|
|
11879
|
+
try {
|
|
11880
|
+
return new URL(url).origin;
|
|
11881
|
+
} catch {
|
|
11882
|
+
return null;
|
|
11883
|
+
}
|
|
11884
|
+
}
|
|
11885
|
+
function recordProductAddedToCart(url, productName) {
|
|
11886
|
+
pruneCartAddedProducts();
|
|
11887
|
+
cartAddedProducts.set(normalizeCartProductKey(url), {
|
|
11888
|
+
title: productName || url,
|
|
11889
|
+
ts: Date.now()
|
|
11890
|
+
});
|
|
11891
|
+
}
|
|
11892
|
+
function isProductAlreadyInCart(url) {
|
|
11893
|
+
pruneCartAddedProducts();
|
|
11894
|
+
return cartAddedProducts.has(normalizeCartProductKey(url));
|
|
11895
|
+
}
|
|
11896
|
+
function getCartAddedSummary(url) {
|
|
11897
|
+
pruneCartAddedProducts();
|
|
11898
|
+
const origin = cartOrigin(url);
|
|
11899
|
+
const items = Array.from(cartAddedProducts.entries()).filter(([key]) => !origin || key.startsWith(`${origin}/`)).map(([_path, info]) => `- ${info.title}`).join("\n");
|
|
11900
|
+
if (!items) return "";
|
|
11901
|
+
const count = items.split("\n").length;
|
|
11902
|
+
return `
|
|
11903
|
+
Already in cart (${count} items):
|
|
11904
|
+
${items}`;
|
|
11905
|
+
}
|
|
11906
|
+
async function buildCartSuccessSuffix(wc, productUrl, overlayHint) {
|
|
11907
|
+
const productTitle = await getProductPageTitle(wc);
|
|
11908
|
+
recordProductAddedToCart(productUrl, productTitle);
|
|
11909
|
+
const cartSummary = getCartAddedSummary(productUrl);
|
|
11910
|
+
const dismissResult = await tryAutoDismissCartDialog(wc);
|
|
11911
|
+
if (dismissResult) {
|
|
11912
|
+
return `
|
|
11913
|
+
Item added to cart. ${dismissResult}${cartSummary}
|
|
11914
|
+
Go back to search results to select the next product.`;
|
|
11915
|
+
}
|
|
11916
|
+
if (!overlayHint) {
|
|
11917
|
+
return cartSummary;
|
|
11918
|
+
}
|
|
11919
|
+
const dialogActions = await getCartDialogActions$1(wc);
|
|
11920
|
+
const actionsSuffix = dialogActions ? `
|
|
11921
|
+
${dialogActions}
|
|
11922
|
+
Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
11923
|
+
return `
|
|
11924
|
+
${overlayHint}${actionsSuffix}${cartSummary}`;
|
|
11925
|
+
}
|
|
10631
11926
|
async function clickResolvedSelector$1(wc, selector) {
|
|
10632
11927
|
if (selector.startsWith("__vessel_idx:")) {
|
|
10633
11928
|
const idx = Number(selector.slice("__vessel_idx:".length));
|
|
10634
11929
|
const beforeUrl2 = wc.getURL();
|
|
11930
|
+
let idxCartMatch = false;
|
|
10635
11931
|
const idxLabel = await executePageScript(
|
|
10636
11932
|
wc,
|
|
10637
11933
|
`window.__vessel?.getElementText?.(${idx}) || ""`,
|
|
10638
11934
|
{ label: "shadow element text" }
|
|
10639
11935
|
);
|
|
10640
|
-
if (typeof idxLabel === "string" && isAddToCartText(idxLabel) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
|
|
11936
|
+
if (typeof idxLabel === "string" && (idxCartMatch = isAddToCartText(idxLabel)) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
|
|
10641
11937
|
return `Blocked: "${idxLabel}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
|
|
10642
11938
|
}
|
|
11939
|
+
if (idxCartMatch && isProductAlreadyInCart(beforeUrl2)) {
|
|
11940
|
+
const summary = getCartAddedSummary(beforeUrl2);
|
|
11941
|
+
return `Blocked: This product was already added to the cart.${summary}
|
|
11942
|
+
Go back and select a different product.`;
|
|
11943
|
+
}
|
|
10643
11944
|
const result = await executePageScript(
|
|
10644
11945
|
wc,
|
|
10645
11946
|
`window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`,
|
|
@@ -10649,18 +11950,40 @@ async function clickResolvedSelector$1(wc, selector) {
|
|
|
10649
11950
|
);
|
|
10650
11951
|
if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
|
|
10651
11952
|
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
10652
|
-
if (
|
|
11953
|
+
if (idxCartMatch) {
|
|
10653
11954
|
recordCartClick(beforeUrl2, idxLabel);
|
|
10654
11955
|
}
|
|
10655
11956
|
await waitForPotentialNavigation$1(wc, beforeUrl2);
|
|
10656
11957
|
const afterUrl2 = wc.getURL();
|
|
10657
11958
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
10658
|
-
|
|
11959
|
+
let idxOverlay = await detectPostClickOverlay$1(wc);
|
|
11960
|
+
if (!idxOverlay && idxCartMatch) {
|
|
11961
|
+
await sleep$1(1200);
|
|
11962
|
+
idxOverlay = await detectPostClickOverlay$1(wc);
|
|
11963
|
+
}
|
|
11964
|
+
if (idxCartMatch) {
|
|
11965
|
+
return `${result}${await buildCartSuccessSuffix(wc, beforeUrl2, idxOverlay)}`;
|
|
11966
|
+
}
|
|
11967
|
+
if (!idxOverlay) {
|
|
11968
|
+
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
11969
|
+
if (hrefMatch) {
|
|
11970
|
+
try {
|
|
11971
|
+
assertSafeURL(hrefMatch[1]);
|
|
11972
|
+
await wc.loadURL(hrefMatch[1]);
|
|
11973
|
+
await waitForLoad$1(wc, 8e3);
|
|
11974
|
+
const hrefUrl = wc.getURL();
|
|
11975
|
+
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
11976
|
+
} catch {
|
|
11977
|
+
}
|
|
11978
|
+
}
|
|
11979
|
+
}
|
|
10659
11980
|
return idxOverlay ? `${result}
|
|
10660
|
-
${idxOverlay}` : result
|
|
11981
|
+
${idxOverlay}` : `${result}
|
|
11982
|
+
Note: Page did not change after click.`;
|
|
10661
11983
|
}
|
|
10662
11984
|
if (selector.includes(" >>> ")) {
|
|
10663
11985
|
const beforeUrl2 = wc.getURL();
|
|
11986
|
+
let shadowCartMatch = false;
|
|
10664
11987
|
const shadowLabel = await executePageScript(
|
|
10665
11988
|
wc,
|
|
10666
11989
|
`(function() {
|
|
@@ -10669,17 +11992,24 @@ ${idxOverlay}` : result;
|
|
|
10669
11992
|
})()`,
|
|
10670
11993
|
{ label: "shadow element text" }
|
|
10671
11994
|
);
|
|
10672
|
-
if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
|
|
11995
|
+
if (typeof shadowLabel === "string" && (shadowCartMatch = isAddToCartText(shadowLabel)) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
|
|
10673
11996
|
return `Blocked: "${shadowLabel}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
|
|
10674
11997
|
}
|
|
11998
|
+
if (shadowCartMatch && isProductAlreadyInCart(beforeUrl2)) {
|
|
11999
|
+
const summary = getCartAddedSummary(beforeUrl2);
|
|
12000
|
+
return `Blocked: This product was already added to the cart.${summary}
|
|
12001
|
+
Go back and select a different product.`;
|
|
12002
|
+
}
|
|
10675
12003
|
const result = await executePageScript(
|
|
10676
12004
|
wc,
|
|
10677
12005
|
`
|
|
10678
12006
|
(function() {
|
|
10679
12007
|
var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
|
|
10680
|
-
if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
12008
|
+
if (!el || !document.contains(el)) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
10681
12009
|
if (el instanceof HTMLElement) { el.focus(); el.click(); }
|
|
10682
|
-
|
|
12010
|
+
var anchor = el instanceof HTMLAnchorElement ? el : el.closest('a[href]');
|
|
12011
|
+
var href = anchor instanceof HTMLAnchorElement ? anchor.href : null;
|
|
12012
|
+
return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase()) + (href ? "\\nhref: " + href : "");
|
|
10683
12013
|
})()
|
|
10684
12014
|
`,
|
|
10685
12015
|
{
|
|
@@ -10688,15 +12018,36 @@ ${idxOverlay}` : result;
|
|
|
10688
12018
|
);
|
|
10689
12019
|
if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
|
|
10690
12020
|
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
10691
|
-
if (
|
|
12021
|
+
if (shadowCartMatch) {
|
|
10692
12022
|
recordCartClick(beforeUrl2, shadowLabel);
|
|
10693
12023
|
}
|
|
10694
12024
|
await waitForPotentialNavigation$1(wc, beforeUrl2);
|
|
10695
12025
|
const afterUrl2 = wc.getURL();
|
|
10696
12026
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
10697
|
-
|
|
12027
|
+
let shadowOverlay = await detectPostClickOverlay$1(wc);
|
|
12028
|
+
if (!shadowOverlay && shadowCartMatch) {
|
|
12029
|
+
await sleep$1(1200);
|
|
12030
|
+
shadowOverlay = await detectPostClickOverlay$1(wc);
|
|
12031
|
+
}
|
|
12032
|
+
if (shadowCartMatch) {
|
|
12033
|
+
return `${result}${await buildCartSuccessSuffix(wc, beforeUrl2, shadowOverlay)}`;
|
|
12034
|
+
}
|
|
12035
|
+
if (!shadowOverlay) {
|
|
12036
|
+
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
12037
|
+
if (hrefMatch) {
|
|
12038
|
+
try {
|
|
12039
|
+
assertSafeURL(hrefMatch[1]);
|
|
12040
|
+
await wc.loadURL(hrefMatch[1]);
|
|
12041
|
+
await waitForLoad$1(wc, 8e3);
|
|
12042
|
+
const hrefUrl = wc.getURL();
|
|
12043
|
+
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
12044
|
+
} catch {
|
|
12045
|
+
}
|
|
12046
|
+
}
|
|
12047
|
+
}
|
|
10698
12048
|
return shadowOverlay ? `${result}
|
|
10699
|
-
${shadowOverlay}` : result
|
|
12049
|
+
${shadowOverlay}` : `${result}
|
|
12050
|
+
Note: Page did not change after click.`;
|
|
10700
12051
|
}
|
|
10701
12052
|
const beforeUrl = wc.getURL();
|
|
10702
12053
|
const elInfo = await describeElementForClick$1(wc, selector);
|
|
@@ -10719,10 +12070,16 @@ Click one of these dialog actions instead.`;
|
|
|
10719
12070
|
return formatDeadLinkMessage(elInfo.text, validation);
|
|
10720
12071
|
}
|
|
10721
12072
|
}
|
|
12073
|
+
if (cartMatch && isProductAlreadyInCart(beforeUrl)) {
|
|
12074
|
+
const summary = getCartAddedSummary(beforeUrl);
|
|
12075
|
+
return `Blocked: This product was already added to the cart.${summary}
|
|
12076
|
+
Go back and select a different product.`;
|
|
12077
|
+
}
|
|
10722
12078
|
if (cartMatch) {
|
|
10723
12079
|
recordCartClick(beforeUrl, elInfo.text);
|
|
10724
12080
|
}
|
|
10725
|
-
const
|
|
12081
|
+
const tagLabel = elInfo.tag && elInfo.tag !== "a" && elInfo.tag !== "button" ? ` <${elInfo.tag}>` : "";
|
|
12082
|
+
const clickText = `Clicked: ${elInfo.text}${tagLabel}`;
|
|
10726
12083
|
const clickResult = await clickElement$1(wc, selector);
|
|
10727
12084
|
if (clickResult.startsWith("Error:")) return clickResult;
|
|
10728
12085
|
await waitForPotentialNavigation$1(wc, beforeUrl);
|
|
@@ -10732,15 +12089,30 @@ Click one of these dialog actions instead.`;
|
|
|
10732
12089
|
}
|
|
10733
12090
|
const overlayHint = await detectPostClickOverlay$1(wc);
|
|
10734
12091
|
if (overlayHint) {
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
12092
|
+
if (cartMatch) {
|
|
12093
|
+
return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
|
|
12094
|
+
wc,
|
|
12095
|
+
beforeUrl,
|
|
12096
|
+
overlayHint
|
|
12097
|
+
)}`;
|
|
12098
|
+
}
|
|
10739
12099
|
return `${clickText} (${clickResult})
|
|
10740
|
-
${overlayHint}
|
|
12100
|
+
${overlayHint}`;
|
|
10741
12101
|
}
|
|
10742
12102
|
if (cartMatch) {
|
|
10743
|
-
|
|
12103
|
+
await sleep$1(1200);
|
|
12104
|
+
const delayedOverlayHint = await detectPostClickOverlay$1(wc);
|
|
12105
|
+
if (delayedOverlayHint) {
|
|
12106
|
+
return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
|
|
12107
|
+
wc,
|
|
12108
|
+
beforeUrl,
|
|
12109
|
+
delayedOverlayHint
|
|
12110
|
+
)}`;
|
|
12111
|
+
}
|
|
12112
|
+
return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
|
|
12113
|
+
wc,
|
|
12114
|
+
beforeUrl
|
|
12115
|
+
)}`;
|
|
10744
12116
|
}
|
|
10745
12117
|
const activationResult = await activateElement$1(wc, selector);
|
|
10746
12118
|
if (!activationResult.startsWith("Error:")) {
|
|
@@ -10755,7 +12127,65 @@ ${overlayHint}${actionsSuffix}`;
|
|
|
10755
12127
|
return `${clickText} (${clickResult})
|
|
10756
12128
|
${postActivationOverlayHint}`;
|
|
10757
12129
|
}
|
|
10758
|
-
|
|
12130
|
+
const sameTabLinkTarget = typeof elInfo.href === "string" && elInfo.href.trim().length > 0 && (!elInfo.target || !/^_blank$/i.test(elInfo.target.trim()));
|
|
12131
|
+
if (sameTabLinkTarget) {
|
|
12132
|
+
const validation = await validateLinkDestination(elInfo.href);
|
|
12133
|
+
if (validation.status !== "dead") {
|
|
12134
|
+
try {
|
|
12135
|
+
assertSafeURL(elInfo.href);
|
|
12136
|
+
await wc.loadURL(elInfo.href);
|
|
12137
|
+
await waitForLoad$1(wc, 8e3);
|
|
12138
|
+
const hrefFallbackUrl = wc.getURL();
|
|
12139
|
+
if (hrefFallbackUrl !== beforeUrl) {
|
|
12140
|
+
return `${clickText} -> ${hrefFallbackUrl} (recovered via href fallback)`;
|
|
12141
|
+
}
|
|
12142
|
+
} catch {
|
|
12143
|
+
}
|
|
12144
|
+
}
|
|
12145
|
+
}
|
|
12146
|
+
const nonInteractiveWarning = elInfo.isInteractive === false && !elInfo.href ? `
|
|
12147
|
+
Note: The clicked element (<${elInfo.tag || "unknown"}>) is not a link or button. Nothing happened. Try clicking the actual link element nearby or use read_page to find the correct interactive element.` : `
|
|
12148
|
+
Note: Page did not change after click. The element may need a different interaction method. Consider read_page or inspect_element.`;
|
|
12149
|
+
return `${clickText} (${clickResult})${nonInteractiveWarning}`;
|
|
12150
|
+
}
|
|
12151
|
+
async function tryAutoDismissCartDialog(wc) {
|
|
12152
|
+
try {
|
|
12153
|
+
const result = await executePageScript(
|
|
12154
|
+
wc,
|
|
12155
|
+
`
|
|
12156
|
+
(function() {
|
|
12157
|
+
var dialog = document.querySelector('[role="dialog"], dialog[open], [role="alertdialog"], [aria-modal="true"]');
|
|
12158
|
+
if (!dialog) return null;
|
|
12159
|
+
var cs = getComputedStyle(dialog);
|
|
12160
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') return null;
|
|
12161
|
+
var buttons = dialog.querySelectorAll('button, a[href], [role="button"]');
|
|
12162
|
+
var continueBtn = null;
|
|
12163
|
+
var closeBtn = null;
|
|
12164
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
12165
|
+
var label = (buttons[i].getAttribute('aria-label') || buttons[i].textContent || '').trim().toLowerCase();
|
|
12166
|
+
if (/continue shopping|keep shopping|back to shopping/.test(label)) { continueBtn = buttons[i]; break; }
|
|
12167
|
+
if (/close|dismiss|×/.test(label) && !closeBtn) { closeBtn = buttons[i]; }
|
|
12168
|
+
}
|
|
12169
|
+
var target = continueBtn || closeBtn;
|
|
12170
|
+
if (!target) return null;
|
|
12171
|
+
var actionLabel = (target.getAttribute('aria-label') || target.textContent || '').trim();
|
|
12172
|
+
if (target.tagName === 'A' && target.href) {
|
|
12173
|
+
window.location.href = target.href;
|
|
12174
|
+
return "Navigated via: " + actionLabel;
|
|
12175
|
+
}
|
|
12176
|
+
target.click();
|
|
12177
|
+
return "Dismissed dialog via: " + actionLabel;
|
|
12178
|
+
})()
|
|
12179
|
+
`,
|
|
12180
|
+
{ timeoutMs: 1500, label: "auto dismiss cart dialog" }
|
|
12181
|
+
);
|
|
12182
|
+
if (result && result !== PAGE_SCRIPT_TIMEOUT && typeof result === "string") {
|
|
12183
|
+
await sleep$1(500);
|
|
12184
|
+
return result;
|
|
12185
|
+
}
|
|
12186
|
+
} catch {
|
|
12187
|
+
}
|
|
12188
|
+
return null;
|
|
10759
12189
|
}
|
|
10760
12190
|
async function getCartDialogActions$1(wc) {
|
|
10761
12191
|
const result = await executePageScript(
|
|
@@ -11561,6 +12991,24 @@ async function resolveSelector$1(wc, index, selector) {
|
|
|
11561
12991
|
if (extractedSelector) return extractedSelector;
|
|
11562
12992
|
return null;
|
|
11563
12993
|
}
|
|
12994
|
+
async function resolveTargetByText(wc, query, mode) {
|
|
12995
|
+
const trimmed = query.trim();
|
|
12996
|
+
if (!trimmed) return null;
|
|
12997
|
+
if (isInvalidTextTargetQuery(trimmed)) return null;
|
|
12998
|
+
const result = await executePageScript(
|
|
12999
|
+
wc,
|
|
13000
|
+
`(${resolveTextTargetInDocument.toString()})(document, ${JSON.stringify(trimmed)}, ${JSON.stringify(mode)})`,
|
|
13001
|
+
{
|
|
13002
|
+
timeoutMs: 2200,
|
|
13003
|
+
label: `resolve ${mode} target by text`
|
|
13004
|
+
}
|
|
13005
|
+
);
|
|
13006
|
+
if (result === PAGE_SCRIPT_TIMEOUT) return PAGE_SCRIPT_TIMEOUT;
|
|
13007
|
+
if (!result || typeof result.selector !== "string" || !result.selector) {
|
|
13008
|
+
return null;
|
|
13009
|
+
}
|
|
13010
|
+
return result.selector;
|
|
13011
|
+
}
|
|
11564
13012
|
function normalizeFieldToken(value) {
|
|
11565
13013
|
return typeof value === "string" ? value.trim() : "";
|
|
11566
13014
|
}
|
|
@@ -13011,8 +14459,49 @@ async function getPostActionState$1(ctx, name) {
|
|
|
13011
14459
|
if (wc.isLoading()) {
|
|
13012
14460
|
await waitForLoad$1(wc);
|
|
13013
14461
|
}
|
|
14462
|
+
const currentUrl = wc.getURL();
|
|
14463
|
+
let warnings = "";
|
|
14464
|
+
if (isProductAlreadyInCart(currentUrl)) {
|
|
14465
|
+
warnings += `
|
|
14466
|
+
WARNING: This product is already in your cart.${getCartAddedSummary(currentUrl)}
|
|
14467
|
+
Go back and select a different product.`;
|
|
14468
|
+
}
|
|
14469
|
+
const taskGoal = ctx.runtime.getState().taskTracker?.goal;
|
|
14470
|
+
if (taskGoal && name === "click") {
|
|
14471
|
+
const drift = shouldBlockOffGoalDomainNavigation(taskGoal, currentUrl);
|
|
14472
|
+
if (drift) {
|
|
14473
|
+
warnings += `
|
|
14474
|
+
WARNING: You drifted to ${drift.targetDomain} but the task requires staying on ${drift.requestedDomain}. Call go_back immediately to return to the previous page.`;
|
|
14475
|
+
}
|
|
14476
|
+
}
|
|
14477
|
+
if (name === "go_back" || name === "search") {
|
|
14478
|
+
const cartSummary = getCartAddedSummary(currentUrl);
|
|
14479
|
+
if (cartSummary) {
|
|
14480
|
+
warnings += `${cartSummary}
|
|
14481
|
+
Select a DIFFERENT product that is not in the cart. Call read_page if needed to see available results.`;
|
|
14482
|
+
}
|
|
14483
|
+
}
|
|
13014
14484
|
return `
|
|
13015
|
-
[state: url=${
|
|
14485
|
+
[state: url=${currentUrl}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]${warnings}`;
|
|
14486
|
+
}
|
|
14487
|
+
if (name === "click" && !wc.isLoading()) {
|
|
14488
|
+
try {
|
|
14489
|
+
const emptyPage = await executePageScript(
|
|
14490
|
+
wc,
|
|
14491
|
+
`(function() {
|
|
14492
|
+
var body = (document.body.textContent || '').toLowerCase();
|
|
14493
|
+
return /\b(no results|no items found|nothing matched|0 results|zero results|no products|your search.*did not match|no books found)\b/.test(body)
|
|
14494
|
+
&& body.length < 8000;
|
|
14495
|
+
})()`,
|
|
14496
|
+
{ timeoutMs: 1e3, label: "empty page check" }
|
|
14497
|
+
);
|
|
14498
|
+
if (emptyPage && emptyPage !== PAGE_SCRIPT_TIMEOUT) {
|
|
14499
|
+
return `
|
|
14500
|
+
[state: url=${wc.getURL()}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=false]
|
|
14501
|
+
WARNING: This page shows no results. You likely clicked a filter or category link instead of a product. Call go_back to return to the search results.`;
|
|
14502
|
+
}
|
|
14503
|
+
} catch {
|
|
14504
|
+
}
|
|
13016
14505
|
}
|
|
13017
14506
|
if (interactActions.includes(name)) {
|
|
13018
14507
|
return `
|
|
@@ -13253,14 +14742,48 @@ async function executeAction(name, args, ctx) {
|
|
|
13253
14742
|
}
|
|
13254
14743
|
case "click": {
|
|
13255
14744
|
if (!wc) return "Error: No active tab";
|
|
13256
|
-
|
|
13257
|
-
|
|
14745
|
+
let selector = null;
|
|
14746
|
+
const textTarget = typeof args.text === "string" && args.text.trim() ? args.text.trim() : "";
|
|
14747
|
+
if (typeof args.selector === "string" && args.selector.trim()) {
|
|
14748
|
+
selector = await resolveSelector$1(wc, void 0, args.selector);
|
|
14749
|
+
} else if (textTarget) {
|
|
14750
|
+
if (isInvalidTextTargetQuery(textTarget)) {
|
|
14751
|
+
return `Error: "${textTarget}" looks like HTML or markup, not a visible page label. Use a book title, button text, or element index instead.`;
|
|
14752
|
+
}
|
|
14753
|
+
selector = await resolveTargetByText(wc, textTarget, "interactive");
|
|
14754
|
+
if (!selector && typeof args.index === "number") {
|
|
14755
|
+
selector = `__vessel_idx:${args.index}`;
|
|
14756
|
+
}
|
|
14757
|
+
} else if (typeof args.index === "number") {
|
|
14758
|
+
selector = await resolveSelector$1(wc, args.index);
|
|
14759
|
+
if (!selector) selector = `__vessel_idx:${args.index}`;
|
|
14760
|
+
} else {
|
|
14761
|
+
selector = await resolveSelector$1(wc, args.index, args.selector);
|
|
14762
|
+
}
|
|
14763
|
+
if (selector === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
|
|
14764
|
+
if (!selector) {
|
|
14765
|
+
return "Error: No element index, selector, or visible text provided";
|
|
14766
|
+
}
|
|
13258
14767
|
return clickResolvedSelector$1(wc, selector);
|
|
13259
14768
|
}
|
|
13260
14769
|
case "inspect_element": {
|
|
13261
14770
|
if (!wc) return "Error: No active tab";
|
|
13262
|
-
|
|
13263
|
-
|
|
14771
|
+
let selector = null;
|
|
14772
|
+
const textTarget = typeof args.text === "string" && args.text.trim() ? args.text.trim() : "";
|
|
14773
|
+
if (textTarget) {
|
|
14774
|
+
if (isInvalidTextTargetQuery(textTarget)) {
|
|
14775
|
+
return `Error: "${textTarget}" looks like HTML or markup, not visible page text. Use a section title, book title, or element index instead.`;
|
|
14776
|
+
}
|
|
14777
|
+
selector = await resolveTargetByText(wc, textTarget, "context");
|
|
14778
|
+
} else {
|
|
14779
|
+
selector = await resolveSelector$1(wc, args.index, args.selector);
|
|
14780
|
+
}
|
|
14781
|
+
if (selector === PAGE_SCRIPT_TIMEOUT) {
|
|
14782
|
+
return pageBusyError("inspect_element");
|
|
14783
|
+
}
|
|
14784
|
+
if (!selector) {
|
|
14785
|
+
return "Error: No element index, selector, or visible text provided";
|
|
14786
|
+
}
|
|
13264
14787
|
return inspectElement(
|
|
13265
14788
|
wc,
|
|
13266
14789
|
selector,
|
|
@@ -13409,7 +14932,8 @@ async function executeAction(name, args, ctx) {
|
|
|
13409
14932
|
const livePrefix = liveSelectionSection ? `${liveSelectionSection}
|
|
13410
14933
|
|
|
13411
14934
|
` : "";
|
|
13412
|
-
const
|
|
14935
|
+
const baseMode = normalizeReadPageMode(args.mode, content);
|
|
14936
|
+
const requestedMode = ctx.toolProfile === "compact" && (args.mode == null || typeof args.mode === "string" && !args.mode.trim()) ? chooseCompactReadMode(content, baseMode) : baseMode;
|
|
13413
14937
|
if (requestedMode === "debug" || requestedMode === "full") {
|
|
13414
14938
|
const structured = buildStructuredContext(content);
|
|
13415
14939
|
const truncated = content.content.length > 2e4 ? content.content.slice(0, 2e4) + "\n[Content truncated...]" : content.content;
|
|
@@ -13421,7 +14945,7 @@ ${structured}
|
|
|
13421
14945
|
|
|
13422
14946
|
${truncated}`;
|
|
13423
14947
|
}
|
|
13424
|
-
const scoped = buildScopedContext(content, requestedMode);
|
|
14948
|
+
const scoped = ctx.toolProfile === "compact" ? buildCompactScopedContext(content, requestedMode) : buildScopedContext(content, requestedMode);
|
|
13425
14949
|
return [
|
|
13426
14950
|
livePrefix ? livePrefix.trimEnd() : "",
|
|
13427
14951
|
`[read_page mode=${requestedMode}]`,
|
|
@@ -14098,9 +15622,20 @@ ${JSON.stringify(tableJson, null, 2)}`;
|
|
|
14098
15622
|
}
|
|
14099
15623
|
case "scroll_to_element": {
|
|
14100
15624
|
if (!wc) return "Error: No active tab";
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
15625
|
+
let sel = null;
|
|
15626
|
+
const textTarget = typeof args.text === "string" && args.text.trim() ? args.text.trim() : "";
|
|
15627
|
+
if (textTarget) {
|
|
15628
|
+
if (isInvalidTextTargetQuery(textTarget)) {
|
|
15629
|
+
return `Error: "${textTarget}" looks like HTML or markup, not visible page text. Use a section title or element index instead.`;
|
|
15630
|
+
}
|
|
15631
|
+
sel = await resolveTargetByText(wc, textTarget, "context");
|
|
15632
|
+
} else {
|
|
15633
|
+
sel = await resolveSelector$1(wc, args.index, args.selector);
|
|
15634
|
+
}
|
|
15635
|
+
if (sel === PAGE_SCRIPT_TIMEOUT) return pageBusyError("scroll_to_element");
|
|
15636
|
+
if (!sel) {
|
|
15637
|
+
return "Error: Provide an index, selector, or visible text for the element to scroll to.";
|
|
15638
|
+
}
|
|
14104
15639
|
const block = args.position === "top" ? "start" : args.position === "bottom" ? "end" : "center";
|
|
14105
15640
|
if (sel.startsWith("__vessel_idx:")) {
|
|
14106
15641
|
const idx = Number(sel.slice("__vessel_idx:".length));
|
|
@@ -14138,8 +15673,36 @@ ${JSON.stringify(tableJson, null, 2)}`;
|
|
|
14138
15673
|
}
|
|
14139
15674
|
}
|
|
14140
15675
|
});
|
|
15676
|
+
const formattedResult = ctx.toolProfile === "compact" ? formatCompactToolResult(name, result) : result;
|
|
14141
15677
|
const flowCtx = ctx.runtime.getFlowContext();
|
|
14142
|
-
|
|
15678
|
+
let clickNavSummary = "";
|
|
15679
|
+
if (name === "click" && !result.startsWith("Error") && !result.startsWith("Blocked") && result.includes(" -> ")) {
|
|
15680
|
+
const summaryWc = ctx.tabManager.getActiveTab()?.view.webContents;
|
|
15681
|
+
if (summaryWc) {
|
|
15682
|
+
clickNavSummary = await getPostClickNavSummary(
|
|
15683
|
+
summaryWc,
|
|
15684
|
+
ctx.toolProfile
|
|
15685
|
+
);
|
|
15686
|
+
}
|
|
15687
|
+
}
|
|
15688
|
+
let streakWarning = "";
|
|
15689
|
+
if (name === "click" && !result.startsWith("Error") && !result.startsWith("Blocked")) {
|
|
15690
|
+
const currentUrl = ctx.tabManager.getActiveTab()?.view.webContents.getURL() ?? "";
|
|
15691
|
+
if (currentUrl === clickStreakUrl) {
|
|
15692
|
+
clickStreakCount++;
|
|
15693
|
+
} else {
|
|
15694
|
+
clickStreakUrl = currentUrl;
|
|
15695
|
+
clickStreakCount = 1;
|
|
15696
|
+
}
|
|
15697
|
+
if (clickStreakCount >= CLICK_STREAK_THRESHOLD) {
|
|
15698
|
+
streakWarning = `
|
|
15699
|
+
WARNING: You have clicked ${clickStreakCount} elements on this page without verifying the result. Call read_page or inspect_element to check the current page state before clicking again. If clicks are having no effect, the elements may not be interactive — try different element indices or read the page to find clickable links.`;
|
|
15700
|
+
}
|
|
15701
|
+
} else if (["read_page", "inspect_element", "screenshot", "wait_for"].includes(name)) {
|
|
15702
|
+
clickStreakCount = 0;
|
|
15703
|
+
clickStreakUrl = null;
|
|
15704
|
+
}
|
|
15705
|
+
return formattedResult + await getPostActionState$1(ctx, name) + clickNavSummary + streakWarning + flowCtx;
|
|
14143
15706
|
}
|
|
14144
15707
|
async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history) {
|
|
14145
15708
|
const lowerQuery = query.toLowerCase().trim();
|
|
@@ -14154,10 +15717,11 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
|
|
|
14154
15717
|
} else {
|
|
14155
15718
|
runtime2.clearTaskTracker();
|
|
14156
15719
|
}
|
|
14157
|
-
const structuredContext =
|
|
15720
|
+
const structuredContext = provider.agentToolProfile === "compact" ? buildCompactScopedContext(
|
|
14158
15721
|
pageContent,
|
|
14159
|
-
defaultReadMode
|
|
14160
|
-
|
|
15722
|
+
defaultReadMode,
|
|
15723
|
+
pageType
|
|
15724
|
+
) : buildScopedContext(pageContent, defaultReadMode);
|
|
14161
15725
|
const runtimeState = runtime2.getState();
|
|
14162
15726
|
const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
|
|
14163
15727
|
const taskTrackerContext = runtime2.getTaskTrackerContext();
|
|
@@ -14181,7 +15745,11 @@ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.ti
|
|
|
14181
15745
|
recentCheckpoints: recentCheckpoints || "- none",
|
|
14182
15746
|
taskTrackerContext: taskTrackerContext || "- none"
|
|
14183
15747
|
});
|
|
14184
|
-
const actionCtx = {
|
|
15748
|
+
const actionCtx = {
|
|
15749
|
+
tabManager,
|
|
15750
|
+
runtime: runtime2,
|
|
15751
|
+
toolProfile: provider.agentToolProfile
|
|
15752
|
+
};
|
|
14185
15753
|
const contextualTools = pruneToolsForContext(
|
|
14186
15754
|
AGENT_TOOLS,
|
|
14187
15755
|
pageType,
|
|
@@ -15481,8 +17049,24 @@ async function clickResolvedSelector(wc, selector) {
|
|
|
15481
17049
|
await waitForPotentialNavigation(wc, beforeUrl2);
|
|
15482
17050
|
const afterUrl2 = wc.getURL();
|
|
15483
17051
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
15484
|
-
|
|
15485
|
-
if (!overlayHint2)
|
|
17052
|
+
let overlayHint2 = await detectPostClickOverlay(wc);
|
|
17053
|
+
if (!overlayHint2 && typeof idxLabel === "string" && isAddToCartText(idxLabel)) {
|
|
17054
|
+
await sleep(1200);
|
|
17055
|
+
overlayHint2 = await detectPostClickOverlay(wc);
|
|
17056
|
+
}
|
|
17057
|
+
if (!overlayHint2) {
|
|
17058
|
+
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
17059
|
+
if (hrefMatch) {
|
|
17060
|
+
try {
|
|
17061
|
+
await wc.loadURL(hrefMatch[1]);
|
|
17062
|
+
await waitForLoad(wc, 8e3);
|
|
17063
|
+
const hrefUrl = wc.getURL();
|
|
17064
|
+
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
17065
|
+
} catch {
|
|
17066
|
+
}
|
|
17067
|
+
}
|
|
17068
|
+
return result;
|
|
17069
|
+
}
|
|
15486
17070
|
const dialogActions = typeof idxLabel === "string" && isAddToCartText(idxLabel) ? await getCartDialogActions(wc) : null;
|
|
15487
17071
|
const actionsSuffix = dialogActions ? `
|
|
15488
17072
|
${dialogActions}
|
|
@@ -15504,9 +17088,11 @@ ${overlayHint2}${actionsSuffix}`;
|
|
|
15504
17088
|
const result = await wc.executeJavaScript(`
|
|
15505
17089
|
(function() {
|
|
15506
17090
|
var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
|
|
15507
|
-
if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
17091
|
+
if (!el || !document.contains(el)) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
15508
17092
|
if (el instanceof HTMLElement) { el.focus(); el.click(); }
|
|
15509
|
-
|
|
17093
|
+
var anchor = el instanceof HTMLAnchorElement ? el : el.closest('a[href]');
|
|
17094
|
+
var href = anchor instanceof HTMLAnchorElement ? anchor.href : null;
|
|
17095
|
+
return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase()) + (href ? "\\nhref: " + href : "");
|
|
15510
17096
|
})()
|
|
15511
17097
|
`);
|
|
15512
17098
|
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
@@ -15516,14 +17102,30 @@ ${overlayHint2}${actionsSuffix}`;
|
|
|
15516
17102
|
await waitForPotentialNavigation(wc, beforeUrl2);
|
|
15517
17103
|
const afterUrl2 = wc.getURL();
|
|
15518
17104
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
15519
|
-
|
|
15520
|
-
if (!overlayHint2)
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
17105
|
+
let overlayHint2 = await detectPostClickOverlay(wc);
|
|
17106
|
+
if (!overlayHint2 && typeof shadowLabel === "string" && isAddToCartText(shadowLabel)) {
|
|
17107
|
+
await sleep(1200);
|
|
17108
|
+
overlayHint2 = await detectPostClickOverlay(wc);
|
|
17109
|
+
}
|
|
17110
|
+
if (!overlayHint2) {
|
|
17111
|
+
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
17112
|
+
if (hrefMatch) {
|
|
17113
|
+
try {
|
|
17114
|
+
await wc.loadURL(hrefMatch[1]);
|
|
17115
|
+
await waitForLoad(wc, 8e3);
|
|
17116
|
+
const hrefUrl = wc.getURL();
|
|
17117
|
+
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
17118
|
+
} catch {
|
|
17119
|
+
}
|
|
17120
|
+
}
|
|
17121
|
+
return result;
|
|
17122
|
+
}
|
|
17123
|
+
const dialogActions2 = typeof shadowLabel === "string" && isAddToCartText(shadowLabel) ? await getCartDialogActions(wc) : null;
|
|
17124
|
+
const actionsSuffix2 = dialogActions2 ? `
|
|
17125
|
+
${dialogActions2}
|
|
15524
17126
|
Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
15525
17127
|
return `${result}
|
|
15526
|
-
${overlayHint2}${
|
|
17128
|
+
${overlayHint2}${actionsSuffix2}`;
|
|
15527
17129
|
}
|
|
15528
17130
|
const beforeUrl = wc.getURL();
|
|
15529
17131
|
const elInfo = await describeElementForClick(wc, selector);
|
|
@@ -15567,6 +17169,16 @@ Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
|
15567
17169
|
${overlayHint}${actionsSuffix}`;
|
|
15568
17170
|
}
|
|
15569
17171
|
if (cartMatch) {
|
|
17172
|
+
await sleep(1200);
|
|
17173
|
+
const delayedOverlayHint = await detectPostClickOverlay(wc);
|
|
17174
|
+
if (delayedOverlayHint) {
|
|
17175
|
+
const dialogActions = await getCartDialogActions(wc);
|
|
17176
|
+
const actionsSuffix = dialogActions ? `
|
|
17177
|
+
${dialogActions}
|
|
17178
|
+
Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
17179
|
+
return `${clickText} (${clickResult})
|
|
17180
|
+
${delayedOverlayHint}${actionsSuffix}`;
|
|
17181
|
+
}
|
|
15570
17182
|
return `${clickText} (${clickResult})`;
|
|
15571
17183
|
}
|
|
15572
17184
|
const activationResult = await activateElement(wc, selector);
|
|
@@ -20492,8 +22104,9 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
20492
22104
|
if (!config || typeof config !== "object" || !("id" in config)) {
|
|
20493
22105
|
return { ok: false, models: [], error: "Invalid provider configuration" };
|
|
20494
22106
|
}
|
|
20495
|
-
|
|
20496
|
-
|
|
22107
|
+
return await fetchProviderModels(
|
|
22108
|
+
config
|
|
22109
|
+
);
|
|
20497
22110
|
} catch (err) {
|
|
20498
22111
|
return { ok: false, models: [], error: err instanceof Error ? err.message : "Unknown error" };
|
|
20499
22112
|
}
|
|
@@ -20983,6 +22596,82 @@ function setNextHint(state2, nextHint) {
|
|
|
20983
22596
|
function normalizeResult(result) {
|
|
20984
22597
|
return result.toLowerCase();
|
|
20985
22598
|
}
|
|
22599
|
+
function looksLikeListingResult(result) {
|
|
22600
|
+
const lowered = normalizeResult(result);
|
|
22601
|
+
return lowered.includes("### primary results") || lowered.includes("### likely search results") || lowered.includes("[read_page mode=results_only]");
|
|
22602
|
+
}
|
|
22603
|
+
function countSurfacedResults(result) {
|
|
22604
|
+
const matches = result.match(/^\s*-\s+\[#\d+\]/gm);
|
|
22605
|
+
return matches?.length ?? 0;
|
|
22606
|
+
}
|
|
22607
|
+
function looksLikeSearchResultsPage(result) {
|
|
22608
|
+
const lowered = normalizeResult(result);
|
|
22609
|
+
return lowered.includes("/searchresults") || lowered.includes("/books/search") || lowered.includes("search results") || lowered.includes("bestsellers") || lowered.includes("best sellers");
|
|
22610
|
+
}
|
|
22611
|
+
function looksLikeProductDetailResult(result) {
|
|
22612
|
+
const lowered = normalizeResult(result);
|
|
22613
|
+
return lowered.includes("### visible purchase controls") || /\badd(?: item)? to (?:cart|bag|basket)\b/.test(lowered) || lowered.includes("buy now") || /https?:\/\/[^\s)]+\/book\//i.test(result);
|
|
22614
|
+
}
|
|
22615
|
+
function looksLikeCartConfirmation(result) {
|
|
22616
|
+
return /(added to cart|cart confirmation|view cart|continue shopping|shopping cart|checkout)/.test(
|
|
22617
|
+
normalizeResult(result)
|
|
22618
|
+
);
|
|
22619
|
+
}
|
|
22620
|
+
function looksLikeCartPage(result) {
|
|
22621
|
+
const lowered = normalizeResult(result);
|
|
22622
|
+
return /\*\*url:\*\*\s*https?:\/\/[^\s]+\/cart\b/.test(lowered) || /\b(?:navigated to|went back to|went forward to)\s+https?:\/\/[^\s]+\/cart\b/.test(
|
|
22623
|
+
lowered
|
|
22624
|
+
) || /\*\*title:\*\*.*\b(cart|checkout)\b/.test(lowered) || /\b(shopping cart|cart subtotal|cart total)\b/.test(lowered);
|
|
22625
|
+
}
|
|
22626
|
+
function looksLikeStaleElementError(result) {
|
|
22627
|
+
const lowered = normalizeResult(result);
|
|
22628
|
+
return lowered.includes("error[stale-index]") || lowered.includes("element not found — the page may have changed") || lowered.includes("shadow dom element not found") || lowered.includes("call read_page to refresh") || lowered.includes("cannot locate the elements to click") || lowered.includes("page structure is not being reliably captured");
|
|
22629
|
+
}
|
|
22630
|
+
function extractStructuredUrl(result) {
|
|
22631
|
+
return result.match(/\*\*url:\*\*\s*([^\n]+)/i)?.[1]?.trim() ?? extractNavigatedUrl(result);
|
|
22632
|
+
}
|
|
22633
|
+
function isAddToCartSuccess(actionName, result) {
|
|
22634
|
+
const lowered = normalizeResult(result);
|
|
22635
|
+
if (actionName !== "click") return false;
|
|
22636
|
+
if (lowered.startsWith("blocked:")) return false;
|
|
22637
|
+
const clickedAddToCart = /clicked:.*add(?: item)? to (?:cart|bag|basket)/.test(lowered);
|
|
22638
|
+
return clickedAddToCart && looksLikeCartConfirmation(result);
|
|
22639
|
+
}
|
|
22640
|
+
function extractNavigatedUrl(result) {
|
|
22641
|
+
return result.match(
|
|
22642
|
+
/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i
|
|
22643
|
+
)?.[1]?.trim() ?? null;
|
|
22644
|
+
}
|
|
22645
|
+
function stepIndexMatching(steps, pattern) {
|
|
22646
|
+
return steps.findIndex((step) => pattern.test(step.label.toLowerCase()));
|
|
22647
|
+
}
|
|
22648
|
+
function finalizeShoppingTracker(state2, detail) {
|
|
22649
|
+
const steps = state2.steps.map((step) => ({ ...step }));
|
|
22650
|
+
const pickIndex = stepIndexMatching(steps, /^pick the requested/);
|
|
22651
|
+
const cartIndex = stepIndexMatching(steps, /^add the chosen .* to the cart$/);
|
|
22652
|
+
const explainIndex = stepIndexMatching(steps, /^explain the recommendations$/);
|
|
22653
|
+
if (pickIndex >= 0) {
|
|
22654
|
+
steps[pickIndex] = {
|
|
22655
|
+
...steps[pickIndex],
|
|
22656
|
+
status: "done",
|
|
22657
|
+
detail
|
|
22658
|
+
};
|
|
22659
|
+
}
|
|
22660
|
+
if (cartIndex >= 0) {
|
|
22661
|
+
steps[cartIndex] = {
|
|
22662
|
+
...steps[cartIndex],
|
|
22663
|
+
status: "done",
|
|
22664
|
+
detail
|
|
22665
|
+
};
|
|
22666
|
+
}
|
|
22667
|
+
const activeIndex = explainIndex >= 0 ? explainIndex : state2.currentStepIndex;
|
|
22668
|
+
return {
|
|
22669
|
+
...state2,
|
|
22670
|
+
steps: setActiveStep(steps, activeIndex),
|
|
22671
|
+
currentStepIndex: activeIndex,
|
|
22672
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22673
|
+
};
|
|
22674
|
+
}
|
|
20986
22675
|
function createTaskTracker(goal, startUrl) {
|
|
20987
22676
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20988
22677
|
return {
|
|
@@ -20993,23 +22682,59 @@ function createTaskTracker(goal, startUrl) {
|
|
|
20993
22682
|
currentStepIndex: 0,
|
|
20994
22683
|
steps: buildInitialSteps(goal),
|
|
20995
22684
|
lastAction: void 0,
|
|
20996
|
-
nextHint: "
|
|
22685
|
+
nextHint: "Use the site's search box or a strong curated section immediately. Avoid rereading the homepage unless search or navigation is hidden."
|
|
20997
22686
|
};
|
|
20998
22687
|
}
|
|
20999
22688
|
function updateTaskTracker(state2, actionName, result) {
|
|
21000
|
-
const
|
|
22689
|
+
const requestedCount = state2.requestedCount ?? extractRequestedCount(state2.goal) ?? null;
|
|
22690
|
+
let cartCount = state2.cartCount ?? 0;
|
|
22691
|
+
const cartVisible = state2.cartVisible || looksLikeCartPage(result);
|
|
21001
22692
|
let nextState = {
|
|
21002
22693
|
...state2,
|
|
21003
22694
|
lastAction: actionName,
|
|
22695
|
+
requestedCount,
|
|
22696
|
+
cartCount,
|
|
22697
|
+
cartVisible,
|
|
21004
22698
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21005
22699
|
};
|
|
21006
22700
|
const currentLabel = nextState.steps[nextState.currentStepIndex]?.label.toLowerCase() ?? "";
|
|
21007
22701
|
if (actionName === "navigate") {
|
|
21008
|
-
|
|
21009
|
-
|
|
21010
|
-
|
|
21011
|
-
|
|
21012
|
-
|
|
22702
|
+
if (/navigate to the requested site/.test(currentLabel)) {
|
|
22703
|
+
nextState = completeStep(nextState, "Reached the requested site.");
|
|
22704
|
+
return setNextHint(
|
|
22705
|
+
nextState,
|
|
22706
|
+
"Use the site's search box or a curated section to expose product results you can click directly. Avoid a full-page read unless the path is unclear."
|
|
22707
|
+
);
|
|
22708
|
+
}
|
|
22709
|
+
const navigatedUrl = extractNavigatedUrl(result) || "";
|
|
22710
|
+
if (/pick the requested/.test(currentLabel)) {
|
|
22711
|
+
if (/\/book\//i.test(navigatedUrl)) {
|
|
22712
|
+
return setNextHint(
|
|
22713
|
+
nextState,
|
|
22714
|
+
"You opened a chosen product detail page. Do not restart search. Click Add to Cart here, then wait for cart confirmation before moving on."
|
|
22715
|
+
);
|
|
22716
|
+
}
|
|
22717
|
+
if (looksLikeSearchResultsPage(result)) {
|
|
22718
|
+
return setNextHint(
|
|
22719
|
+
nextState,
|
|
22720
|
+
"You are back on a results page while the chosen items are already decided. Do not restart search or browse new categories. Open one of the chosen result links and continue the add-to-cart flow."
|
|
22721
|
+
);
|
|
22722
|
+
}
|
|
22723
|
+
}
|
|
22724
|
+
if (/add the chosen .* to the cart/.test(currentLabel)) {
|
|
22725
|
+
if (/\/book\//i.test(navigatedUrl)) {
|
|
22726
|
+
return setNextHint(
|
|
22727
|
+
nextState,
|
|
22728
|
+
"Stay on this product detail page and add the current chosen item to the cart. Do not go back to search unless this specific cart step fails."
|
|
22729
|
+
);
|
|
22730
|
+
}
|
|
22731
|
+
if (looksLikeSearchResultsPage(result)) {
|
|
22732
|
+
return setNextHint(
|
|
22733
|
+
nextState,
|
|
22734
|
+
"The chosen items are already decided. Do not restart the search flow here. Open the next chosen result from the current page and add it to the cart."
|
|
22735
|
+
);
|
|
22736
|
+
}
|
|
22737
|
+
}
|
|
21013
22738
|
}
|
|
21014
22739
|
const isDiscoveryAction = [
|
|
21015
22740
|
"read_page",
|
|
@@ -21019,31 +22744,101 @@ function updateTaskTracker(state2, actionName, result) {
|
|
|
21019
22744
|
"scroll"
|
|
21020
22745
|
].includes(actionName);
|
|
21021
22746
|
if (isDiscoveryAction && /browse or search/.test(currentLabel)) {
|
|
22747
|
+
const surfacedResults = countSurfacedResults(result);
|
|
21022
22748
|
nextState = completeStep(nextState, "Found a starting point on the site.");
|
|
21023
22749
|
return setNextHint(
|
|
21024
22750
|
nextState,
|
|
21025
|
-
"Inspect
|
|
22751
|
+
looksLikeListingResult(result) ? surfacedResults === 1 ? "One likely result is visible. Inspect or click that result before deciding there is no match. Do not skip to a new search yet." : "Product results are already visible. Open exactly one unseen result now, add that item before choosing another, and do not click multiple results in a row from the same listing page." : looksLikeSearchResultsPage(result) ? 'You are on a results page. Call read_page(mode="results_only") now to surface product results. Do not use visible_only or generic inspect_element to hunt result links.' : "Expose product results you can click directly, then inspect individual items until you have the full set."
|
|
21026
22752
|
);
|
|
21027
22753
|
}
|
|
21028
22754
|
if (/pick the requested/.test(currentLabel) && isDiscoveryAction) {
|
|
22755
|
+
if (isAddToCartSuccess(actionName, result)) {
|
|
22756
|
+
cartCount += 1;
|
|
22757
|
+
nextState = {
|
|
22758
|
+
...nextState,
|
|
22759
|
+
cartCount
|
|
22760
|
+
};
|
|
22761
|
+
if (requestedCount && cartCount >= requestedCount) {
|
|
22762
|
+
nextState = finalizeShoppingTracker(
|
|
22763
|
+
nextState,
|
|
22764
|
+
`Added ${cartCount} of ${requestedCount} requested items to the cart.`
|
|
22765
|
+
);
|
|
22766
|
+
return setNextHint(
|
|
22767
|
+
nextState,
|
|
22768
|
+
cartVisible ? "All requested items are now in the cart and the cart is visible. Explain your reasoning in chat now and stop using tools." : "All requested items are now in the cart. Open the cart so the user can see it, then explain your reasoning in chat and stop using tools."
|
|
22769
|
+
);
|
|
22770
|
+
}
|
|
22771
|
+
return setNextHint(
|
|
22772
|
+
nextState,
|
|
22773
|
+
requestedCount ? `${cartCount} of ${requestedCount} requested items are now in the cart. If the cart confirmation dialog is open, click Continue Shopping there. Do not click View Cart or Go to Basket until all requested items are added. Only use go_back if no dialog action is available. Then open the next unseen result.` : "This item is now in the cart. If the cart confirmation dialog is open, click Continue Shopping there. Do not click View Cart or Go to Basket yet. Only use go_back if no dialog action is available. Then open the next unseen result."
|
|
22774
|
+
);
|
|
22775
|
+
}
|
|
22776
|
+
if (looksLikeCartConfirmation(result)) {
|
|
22777
|
+
return setNextHint(
|
|
22778
|
+
nextState,
|
|
22779
|
+
"This item is already in the cart. If the cart confirmation dialog is still open, click Continue Shopping there. Do not click View Cart or Go to Basket yet. Only use go_back if no dialog action is available. Then open the next unseen result."
|
|
22780
|
+
);
|
|
22781
|
+
}
|
|
22782
|
+
const structuredUrl = extractStructuredUrl(result) || "";
|
|
22783
|
+
if (actionName === "read_page" && cartCount > 0 && /\/book\//i.test(structuredUrl) && looksLikeProductDetailResult(result)) {
|
|
22784
|
+
return setNextHint(
|
|
22785
|
+
nextState,
|
|
22786
|
+
"This detail page may already be for an item you just added. Do not click Add to Cart again on the same page. If the cart confirmation dialog is still open, click Continue Shopping there. Otherwise go back once and open the next chosen result. Do not click View Cart or Go to Basket yet."
|
|
22787
|
+
);
|
|
22788
|
+
}
|
|
22789
|
+
if (looksLikeProductDetailResult(result)) {
|
|
22790
|
+
return setNextHint(
|
|
22791
|
+
nextState,
|
|
22792
|
+
'You are on a product detail page. Opening this page did not add the item to the cart. Click Add to Cart now, then wait for cart confirmation before moving on. Use read_page(mode="visible_only") once only if you need the Add to Cart index.'
|
|
22793
|
+
);
|
|
22794
|
+
}
|
|
22795
|
+
if (looksLikeSearchResultsPage(result) && !looksLikeListingResult(result)) {
|
|
22796
|
+
return setNextHint(
|
|
22797
|
+
nextState,
|
|
22798
|
+
'This is still a results page. Call read_page(mode="results_only") now and click a surfaced result. Do not loop on visible_only or generic inspect_element here.'
|
|
22799
|
+
);
|
|
22800
|
+
}
|
|
22801
|
+
if (looksLikeListingResult(result)) {
|
|
22802
|
+
const surfacedResults = countSurfacedResults(result);
|
|
22803
|
+
return setNextHint(
|
|
22804
|
+
nextState,
|
|
22805
|
+
surfacedResults === 1 ? "There is one likely result visible. Inspect or click that result before declaring no match or moving to a different query." : "A product listing is already visible. Open exactly one unseen result now, add that item to the cart from its detail page, then return for the next result. Do not click multiple remembered results in a row from the listing page."
|
|
22806
|
+
);
|
|
22807
|
+
}
|
|
22808
|
+
if (actionName === "click" && looksLikeStaleElementError(result)) {
|
|
22809
|
+
return setNextHint(
|
|
22810
|
+
nextState,
|
|
22811
|
+
"The last remembered click target is stale. Trust the latest page state, not older labels or indexes. Call read_page once on the current page. If you are on a detail page, add the current item now. If you are still on a listing page, open exactly one visible unseen result. Do not click multiple saved results in sequence."
|
|
22812
|
+
);
|
|
22813
|
+
}
|
|
21029
22814
|
return setNextHint(
|
|
21030
22815
|
nextState,
|
|
21031
|
-
"
|
|
22816
|
+
"Pick one promising unseen item at a time. As soon as a detail page opens, add that item to the cart before selecting another result."
|
|
21032
22817
|
);
|
|
21033
22818
|
}
|
|
21034
|
-
if (/
|
|
21035
|
-
|
|
21036
|
-
|
|
21037
|
-
|
|
22819
|
+
if (/pick the requested/.test(currentLabel) && actionName === "go_back") {
|
|
22820
|
+
return setNextHint(
|
|
22821
|
+
nextState,
|
|
22822
|
+
"You are back on the listing flow. Open exactly one next chosen or unseen result now instead of rereading the whole page, restarting search, or clicking multiple results in sequence."
|
|
22823
|
+
);
|
|
22824
|
+
}
|
|
22825
|
+
if (/add the chosen .* to the cart/.test(currentLabel) && isAddToCartSuccess(actionName, result)) {
|
|
22826
|
+
cartCount += 1;
|
|
22827
|
+
nextState = {
|
|
22828
|
+
...nextState,
|
|
22829
|
+
cartCount
|
|
22830
|
+
};
|
|
22831
|
+
const detail = requestedCount ? `Added ${cartCount} of ${requestedCount} requested items to the cart.` : "Cart interaction succeeded.";
|
|
22832
|
+
nextState = completeStep(nextState, detail);
|
|
21038
22833
|
return setNextHint(
|
|
21039
22834
|
nextState,
|
|
21040
|
-
"Summarize the chosen books and explain why they were recommended."
|
|
22835
|
+
requestedCount && cartCount >= requestedCount ? cartVisible ? "All requested books are now in the cart and the cart is visible. Explain your reasoning in chat now and stop using tools." : "All requested books are now in the cart. Open the cart so the user can see it, then explain your reasoning in chat and stop using tools." : requestedCount ? `${cartCount} of ${requestedCount} requested books are now in the cart. Continue adding the remaining selected books.` : "Summarize the chosen books and explain why they were recommended."
|
|
21041
22836
|
);
|
|
21042
22837
|
}
|
|
21043
22838
|
if (/explain the recommendations/.test(currentLabel)) {
|
|
21044
22839
|
return setNextHint(
|
|
21045
22840
|
nextState,
|
|
21046
|
-
"Finish by naming the chosen books and giving concise reasons for each."
|
|
22841
|
+
cartVisible ? "The cart is visible. Explain your reasoning in chat now, mention the chosen books, and stop using tools." : "Finish by naming the chosen books and giving concise reasons for each. If the cart is not visible yet, show it first."
|
|
21047
22842
|
);
|
|
21048
22843
|
}
|
|
21049
22844
|
return nextState;
|