@quanta-intellect/vessel-browser 0.1.33 → 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 +3161 -412
- package/out/preload/content-script.js +21 -2
- package/out/renderer/assets/{index-BFdOm6Op.js → index-BC3sJqLj.js} +173 -136
- 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,214 +5126,6 @@ function makeImageResult(base64, description, mediaType = "image/png") {
|
|
|
5126
5126
|
};
|
|
5127
5127
|
return JSON.stringify(result);
|
|
5128
5128
|
}
|
|
5129
|
-
class AnthropicProvider {
|
|
5130
|
-
client;
|
|
5131
|
-
model;
|
|
5132
|
-
abortController = null;
|
|
5133
|
-
constructor(apiKey, model) {
|
|
5134
|
-
this.client = new Anthropic({ apiKey });
|
|
5135
|
-
this.model = model || "claude-sonnet-4-20250514";
|
|
5136
|
-
}
|
|
5137
|
-
async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
|
|
5138
|
-
this.abortController = new AbortController();
|
|
5139
|
-
const messages = [
|
|
5140
|
-
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
5141
|
-
{ role: "user", content: userMessage }
|
|
5142
|
-
];
|
|
5143
|
-
try {
|
|
5144
|
-
const stream = this.client.messages.stream(
|
|
5145
|
-
{
|
|
5146
|
-
model: this.model,
|
|
5147
|
-
max_tokens: 4096,
|
|
5148
|
-
system: systemPrompt,
|
|
5149
|
-
messages
|
|
5150
|
-
},
|
|
5151
|
-
{ signal: this.abortController.signal }
|
|
5152
|
-
);
|
|
5153
|
-
for await (const event of stream) {
|
|
5154
|
-
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
5155
|
-
onChunk(event.delta.text);
|
|
5156
|
-
}
|
|
5157
|
-
}
|
|
5158
|
-
} catch (err) {
|
|
5159
|
-
if (err instanceof Error && err.name !== "AbortError") {
|
|
5160
|
-
onChunk(`
|
|
5161
|
-
|
|
5162
|
-
[Error: ${err.message}]`);
|
|
5163
|
-
}
|
|
5164
|
-
} finally {
|
|
5165
|
-
this.abortController = null;
|
|
5166
|
-
onEnd();
|
|
5167
|
-
}
|
|
5168
|
-
}
|
|
5169
|
-
async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
|
|
5170
|
-
this.abortController = new AbortController();
|
|
5171
|
-
const messages = [
|
|
5172
|
-
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
5173
|
-
{ role: "user", content: userMessage }
|
|
5174
|
-
];
|
|
5175
|
-
try {
|
|
5176
|
-
const maxIterations = getEffectiveMaxIterations();
|
|
5177
|
-
let iterationsUsed = 0;
|
|
5178
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
5179
|
-
iterationsUsed = i + 1;
|
|
5180
|
-
const stream = this.client.messages.stream(
|
|
5181
|
-
{
|
|
5182
|
-
model: this.model,
|
|
5183
|
-
max_tokens: 4096,
|
|
5184
|
-
system: systemPrompt,
|
|
5185
|
-
messages,
|
|
5186
|
-
tools
|
|
5187
|
-
},
|
|
5188
|
-
{ signal: this.abortController.signal }
|
|
5189
|
-
);
|
|
5190
|
-
let textContent = "";
|
|
5191
|
-
const toolUseBlocks = [];
|
|
5192
|
-
let currentToolUse = null;
|
|
5193
|
-
const STREAM_IDLE_TIMEOUT_MS = 3e4;
|
|
5194
|
-
let idleTimer = null;
|
|
5195
|
-
const resetIdleTimer = () => {
|
|
5196
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
5197
|
-
idleTimer = setTimeout(() => {
|
|
5198
|
-
this.abortController?.abort();
|
|
5199
|
-
}, STREAM_IDLE_TIMEOUT_MS);
|
|
5200
|
-
};
|
|
5201
|
-
resetIdleTimer();
|
|
5202
|
-
try {
|
|
5203
|
-
for await (const event of stream) {
|
|
5204
|
-
resetIdleTimer();
|
|
5205
|
-
if (event.type === "content_block_start") {
|
|
5206
|
-
if (event.content_block.type === "tool_use") {
|
|
5207
|
-
currentToolUse = {
|
|
5208
|
-
id: event.content_block.id,
|
|
5209
|
-
name: event.content_block.name,
|
|
5210
|
-
inputJson: ""
|
|
5211
|
-
};
|
|
5212
|
-
}
|
|
5213
|
-
} else if (event.type === "content_block_delta") {
|
|
5214
|
-
if (event.delta.type === "text_delta") {
|
|
5215
|
-
textContent += event.delta.text;
|
|
5216
|
-
onChunk(event.delta.text);
|
|
5217
|
-
} else if (event.delta.type === "input_json_delta" && currentToolUse) {
|
|
5218
|
-
currentToolUse.inputJson += event.delta.partial_json;
|
|
5219
|
-
}
|
|
5220
|
-
} else if (event.type === "content_block_stop" && currentToolUse) {
|
|
5221
|
-
try {
|
|
5222
|
-
toolUseBlocks.push({
|
|
5223
|
-
id: currentToolUse.id,
|
|
5224
|
-
name: currentToolUse.name,
|
|
5225
|
-
input: JSON.parse(currentToolUse.inputJson || "{}")
|
|
5226
|
-
});
|
|
5227
|
-
} catch {
|
|
5228
|
-
toolUseBlocks.push({
|
|
5229
|
-
id: currentToolUse.id,
|
|
5230
|
-
name: currentToolUse.name,
|
|
5231
|
-
input: {},
|
|
5232
|
-
_malformedArgs: currentToolUse.inputJson
|
|
5233
|
-
});
|
|
5234
|
-
}
|
|
5235
|
-
currentToolUse = null;
|
|
5236
|
-
}
|
|
5237
|
-
}
|
|
5238
|
-
} finally {
|
|
5239
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
5240
|
-
}
|
|
5241
|
-
const finalMessage = await stream.finalMessage();
|
|
5242
|
-
const assistantContent = [];
|
|
5243
|
-
if (textContent) {
|
|
5244
|
-
assistantContent.push({ type: "text", text: textContent });
|
|
5245
|
-
}
|
|
5246
|
-
for (const tb of toolUseBlocks) {
|
|
5247
|
-
assistantContent.push({
|
|
5248
|
-
type: "tool_use",
|
|
5249
|
-
id: tb.id,
|
|
5250
|
-
name: tb.name,
|
|
5251
|
-
input: tb.input
|
|
5252
|
-
});
|
|
5253
|
-
}
|
|
5254
|
-
messages.push({ role: "assistant", content: assistantContent });
|
|
5255
|
-
if (toolUseBlocks.length === 0) {
|
|
5256
|
-
break;
|
|
5257
|
-
}
|
|
5258
|
-
const toolResults = [];
|
|
5259
|
-
for (const tb of toolUseBlocks) {
|
|
5260
|
-
if (tb._malformedArgs !== void 0) {
|
|
5261
|
-
onChunk(`
|
|
5262
|
-
<<tool:${tb.name}:⚠ invalid args>>
|
|
5263
|
-
`);
|
|
5264
|
-
toolResults.push({
|
|
5265
|
-
type: "tool_result",
|
|
5266
|
-
tool_use_id: tb.id,
|
|
5267
|
-
content: `Error: Invalid JSON in tool arguments — could not parse. Please retry with valid JSON. Raw input: ${tb._malformedArgs.slice(0, 200)}`,
|
|
5268
|
-
is_error: true
|
|
5269
|
-
});
|
|
5270
|
-
continue;
|
|
5271
|
-
}
|
|
5272
|
-
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
5273
|
-
onChunk(`
|
|
5274
|
-
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
5275
|
-
`);
|
|
5276
|
-
let result;
|
|
5277
|
-
try {
|
|
5278
|
-
result = await onToolCall(tb.name, tb.input);
|
|
5279
|
-
} catch (toolErr) {
|
|
5280
|
-
const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
5281
|
-
result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
|
|
5282
|
-
}
|
|
5283
|
-
let parsedRich = null;
|
|
5284
|
-
try {
|
|
5285
|
-
const parsed = JSON.parse(result);
|
|
5286
|
-
if (isRichToolResult(parsed)) parsedRich = parsed;
|
|
5287
|
-
} catch {
|
|
5288
|
-
}
|
|
5289
|
-
if (parsedRich) {
|
|
5290
|
-
toolResults.push({
|
|
5291
|
-
type: "tool_result",
|
|
5292
|
-
tool_use_id: tb.id,
|
|
5293
|
-
content: parsedRich.content.map((block) => {
|
|
5294
|
-
if (block.type === "image") {
|
|
5295
|
-
return {
|
|
5296
|
-
type: "image",
|
|
5297
|
-
source: {
|
|
5298
|
-
type: "base64",
|
|
5299
|
-
media_type: block.mediaType,
|
|
5300
|
-
data: block.base64
|
|
5301
|
-
}
|
|
5302
|
-
};
|
|
5303
|
-
}
|
|
5304
|
-
return { type: "text", text: block.text };
|
|
5305
|
-
})
|
|
5306
|
-
});
|
|
5307
|
-
} else {
|
|
5308
|
-
toolResults.push({
|
|
5309
|
-
type: "tool_result",
|
|
5310
|
-
tool_use_id: tb.id,
|
|
5311
|
-
content: result
|
|
5312
|
-
});
|
|
5313
|
-
}
|
|
5314
|
-
}
|
|
5315
|
-
messages.push({ role: "user", content: toolResults });
|
|
5316
|
-
}
|
|
5317
|
-
if (iterationsUsed >= maxIterations) {
|
|
5318
|
-
onChunk(`
|
|
5319
|
-
|
|
5320
|
-
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
5321
|
-
}
|
|
5322
|
-
} catch (err) {
|
|
5323
|
-
if (err instanceof Error && err.name !== "AbortError") {
|
|
5324
|
-
onChunk(`
|
|
5325
|
-
|
|
5326
|
-
[Error: ${err.message}]`);
|
|
5327
|
-
}
|
|
5328
|
-
} finally {
|
|
5329
|
-
this.abortController = null;
|
|
5330
|
-
onEnd();
|
|
5331
|
-
}
|
|
5332
|
-
}
|
|
5333
|
-
cancel() {
|
|
5334
|
-
this.abortController?.abort();
|
|
5335
|
-
}
|
|
5336
|
-
}
|
|
5337
5129
|
const PROVIDERS = {
|
|
5338
5130
|
anthropic: {
|
|
5339
5131
|
id: "anthropic",
|
|
@@ -5383,6 +5175,16 @@ const PROVIDERS = {
|
|
|
5383
5175
|
apiKeyPlaceholder: "",
|
|
5384
5176
|
apiKeyHint: "No API key needed — runs locally via Ollama"
|
|
5385
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
|
+
},
|
|
5386
5188
|
mistral: {
|
|
5387
5189
|
id: "mistral",
|
|
5388
5190
|
name: "Mistral AI",
|
|
@@ -5423,20 +5225,609 @@ const PROVIDERS = {
|
|
|
5423
5225
|
apiKeyPlaceholder: "Bearer token or API key",
|
|
5424
5226
|
apiKeyHint: "Optional — only if your endpoint requires authentication"
|
|
5425
5227
|
}
|
|
5426
|
-
};
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5228
|
+
};
|
|
5229
|
+
const SAFE_TOOL_ALIASES = {
|
|
5230
|
+
goto_url: "navigate",
|
|
5231
|
+
go_to_url: "navigate",
|
|
5232
|
+
browser_goto: "navigate",
|
|
5233
|
+
browser_navigate: "navigate",
|
|
5234
|
+
open_url: "navigate",
|
|
5235
|
+
visit_url: "navigate",
|
|
5236
|
+
navigate_to: "navigate",
|
|
5237
|
+
open_page: "navigate",
|
|
5238
|
+
google_search: "search",
|
|
5239
|
+
site_search: "search",
|
|
5240
|
+
search_site: "search",
|
|
5241
|
+
page_search: "search",
|
|
5242
|
+
scroll_down: "scroll",
|
|
5243
|
+
scroll_up: "scroll",
|
|
5244
|
+
read: "read_page",
|
|
5245
|
+
read_current_page: "read_page",
|
|
5246
|
+
scan_page: "read_page"
|
|
5247
|
+
};
|
|
5248
|
+
function normalizeToolAlias(name) {
|
|
5249
|
+
const normalized = name.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
|
|
5250
|
+
return SAFE_TOOL_ALIASES[normalized] ?? name;
|
|
5251
|
+
}
|
|
5252
|
+
function parseModelSizeInBillions(model) {
|
|
5253
|
+
const match = model.toLowerCase().match(/(?:^|[:/_\-\s])(\d+(?:\.\d+)?)b(?:$|[:/_\-\s])/i);
|
|
5254
|
+
if (!match) return null;
|
|
5255
|
+
const parsed = Number(match[1]);
|
|
5256
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
5257
|
+
}
|
|
5258
|
+
function isLoopbackBaseUrl(baseUrl) {
|
|
5259
|
+
if (!baseUrl) return false;
|
|
5260
|
+
try {
|
|
5261
|
+
const url = new URL(baseUrl);
|
|
5262
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
5263
|
+
} catch {
|
|
5264
|
+
return false;
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
function resolveAgentToolProfile(config) {
|
|
5268
|
+
const providerId = config.id;
|
|
5269
|
+
const isLocalProvider = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
|
|
5270
|
+
if (!isLocalProvider) return "default";
|
|
5271
|
+
const sizeInBillions = parseModelSizeInBillions(config.model);
|
|
5272
|
+
if (sizeInBillions === null) {
|
|
5273
|
+
return "compact";
|
|
5274
|
+
}
|
|
5275
|
+
return sizeInBillions <= 14 ? "compact" : "default";
|
|
5276
|
+
}
|
|
5277
|
+
const LLAMA_CPP_MIN_CTX_TOKENS$1 = 16384;
|
|
5278
|
+
const LLAMA_CPP_RECOMMENDED_CTX_TOKENS$1 = 32768;
|
|
5279
|
+
function shouldDebugAgentLoop() {
|
|
5280
|
+
const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
|
|
5281
|
+
return value === "1" || value === "true";
|
|
5282
|
+
}
|
|
5283
|
+
function previewDebugValue(value, maxLength = 800) {
|
|
5284
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
5285
|
+
if (normalized.length <= maxLength) return normalized;
|
|
5286
|
+
return `${normalized.slice(0, maxLength)}…`;
|
|
5287
|
+
}
|
|
5288
|
+
function previewToolDebugContent(content) {
|
|
5289
|
+
return previewDebugValue(content, 500);
|
|
5290
|
+
}
|
|
5291
|
+
function toOpenAITools(tools) {
|
|
5292
|
+
return tools.map((t) => ({
|
|
5293
|
+
type: "function",
|
|
5294
|
+
function: {
|
|
5295
|
+
name: t.name,
|
|
5296
|
+
description: t.description ?? "",
|
|
5297
|
+
parameters: t.input_schema
|
|
5298
|
+
}
|
|
5299
|
+
}));
|
|
5300
|
+
}
|
|
5301
|
+
function agentTemperatureForProfile(profile) {
|
|
5302
|
+
return profile === "compact" ? 0.2 : void 0;
|
|
5303
|
+
}
|
|
5304
|
+
function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
|
|
5305
|
+
if (profile !== "compact") return null;
|
|
5306
|
+
const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
|
|
5307
|
+
const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
|
|
5308
|
+
return {
|
|
5309
|
+
role: "user",
|
|
5310
|
+
content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
|
|
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 ? `
|
|
5312
|
+
${stateReminder}` : "") + (phaseReminder ? `
|
|
5313
|
+
${phaseReminder}` : "")
|
|
5314
|
+
};
|
|
5315
|
+
}
|
|
5316
|
+
function extractSingleGoalDomain(goal) {
|
|
5317
|
+
const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
|
|
5318
|
+
if (!matches || matches.length !== 1) return null;
|
|
5319
|
+
return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
|
|
5320
|
+
}
|
|
5321
|
+
function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
|
|
5322
|
+
const phaseReminder = buildPhaseReminder(userMessage, assistantText);
|
|
5323
|
+
const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
|
|
5324
|
+
const goalDomain = extractSingleGoalDomain(userMessage);
|
|
5325
|
+
const latest = (latestToolResultPreview || "").toLowerCase();
|
|
5326
|
+
const assistant = assistantText.toLowerCase();
|
|
5327
|
+
const alreadyOnGoalSite = !!goalDomain && (latest.includes(goalDomain) || assistant.includes(`https://${goalDomain}`) || assistant.includes(`https://www.${goalDomain}`));
|
|
5328
|
+
const lines = [
|
|
5329
|
+
`The task is still in progress: ${userMessage}`,
|
|
5330
|
+
`Do not ask the user for permission to continue. Choose the next tool now unless the request is fully complete.`
|
|
5331
|
+
];
|
|
5332
|
+
if (alreadyOnGoalSite) {
|
|
5333
|
+
lines.push(
|
|
5334
|
+
`You are already on the requested site (${goalDomain}). Do not navigate to the homepage again and do not restart discovery from scratch.`
|
|
5335
|
+
);
|
|
5336
|
+
}
|
|
5337
|
+
if (stateReminder) {
|
|
5338
|
+
lines.push(stateReminder);
|
|
5339
|
+
}
|
|
5340
|
+
if (phaseReminder) {
|
|
5341
|
+
lines.push(phaseReminder);
|
|
5342
|
+
}
|
|
5343
|
+
return lines.join("\n");
|
|
5344
|
+
}
|
|
5345
|
+
function buildPhaseReminder(userMessage, assistantText) {
|
|
5346
|
+
const goal = userMessage.toLowerCase();
|
|
5347
|
+
const text = assistantText.toLowerCase();
|
|
5348
|
+
if (!goal || !text) return "";
|
|
5349
|
+
const wantsCart = /\b(cart|bag|basket|checkout)\b/.test(goal);
|
|
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
|
+
);
|
|
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);
|
|
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);
|
|
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);
|
|
5357
|
+
const cartDone = /(added to cart|added them to the cart|cart confirmation|view cart|checkout)/.test(
|
|
5358
|
+
text
|
|
5359
|
+
);
|
|
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
|
+
}
|
|
5412
|
+
if (wantsCart && selectedItems && (intendsCart || !cartDone)) {
|
|
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.`;
|
|
5414
|
+
}
|
|
5415
|
+
if (wantsCart && wantsExplanation && cartDone && !explanationDone) {
|
|
5416
|
+
return `Progress reminder: The cart step appears complete. Do not resume browsing. Finish by explaining why the chosen items were recommended.`;
|
|
5417
|
+
}
|
|
5418
|
+
return "";
|
|
5419
|
+
}
|
|
5420
|
+
function buildLatestStateReminder(toolResultPreview) {
|
|
5421
|
+
const text = toolResultPreview.trim();
|
|
5422
|
+
if (!text) return "";
|
|
5423
|
+
const stateMatch = text.match(
|
|
5424
|
+
/\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
|
|
5425
|
+
);
|
|
5426
|
+
if (stateMatch) {
|
|
5427
|
+
const url = stateMatch[1]?.trim();
|
|
5428
|
+
const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
|
|
5429
|
+
if (url) {
|
|
5430
|
+
return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
|
|
5434
|
+
const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
|
|
5435
|
+
if (structuredUrl) {
|
|
5436
|
+
return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
|
|
5437
|
+
}
|
|
5438
|
+
const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i)?.[1]?.trim();
|
|
5439
|
+
const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
|
|
5440
|
+
if (navigatedUrl) {
|
|
5441
|
+
return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
|
|
5442
|
+
}
|
|
5443
|
+
return "";
|
|
5444
|
+
}
|
|
5445
|
+
function shouldRecoverCompactStall(text, userMessage) {
|
|
5446
|
+
const trimmed = text.trim().toLowerCase();
|
|
5447
|
+
if (!trimmed) return true;
|
|
5448
|
+
if (trimmed.length <= 160 && trimmed.includes("?")) return true;
|
|
5449
|
+
if (userMessage && buildPhaseReminder(userMessage, text)) {
|
|
5450
|
+
return true;
|
|
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
|
+
}
|
|
5476
|
+
const completionSignals = [
|
|
5477
|
+
"i found",
|
|
5478
|
+
"i chose",
|
|
5479
|
+
"i selected",
|
|
5480
|
+
"i added",
|
|
5481
|
+
"here are",
|
|
5482
|
+
"these are",
|
|
5483
|
+
"recommendations",
|
|
5484
|
+
"reasoning",
|
|
5485
|
+
"why i chose",
|
|
5486
|
+
"added them to the cart"
|
|
5487
|
+
];
|
|
5488
|
+
if (completionSignals.some((pattern) => trimmed.includes(pattern))) {
|
|
5489
|
+
return false;
|
|
5490
|
+
}
|
|
5491
|
+
return [
|
|
5492
|
+
"what are you hoping",
|
|
5493
|
+
"what would you like",
|
|
5494
|
+
"how can i help",
|
|
5495
|
+
"let me know",
|
|
5496
|
+
"are you looking for",
|
|
5497
|
+
"just browsing",
|
|
5498
|
+
"i need to",
|
|
5499
|
+
"i will",
|
|
5500
|
+
"i'll",
|
|
5501
|
+
"since i cannot see",
|
|
5502
|
+
"since i can't see",
|
|
5503
|
+
"cannot see the current page",
|
|
5504
|
+
"scroll down to",
|
|
5505
|
+
"load more results",
|
|
5506
|
+
"as placeholders",
|
|
5507
|
+
"would you like me to proceed",
|
|
5508
|
+
"action:",
|
|
5509
|
+
"one moment",
|
|
5510
|
+
"i will now navigate",
|
|
5511
|
+
"navigating to ",
|
|
5512
|
+
"this will take me",
|
|
5513
|
+
"i will use the browser"
|
|
5514
|
+
].some((pattern) => trimmed.includes(pattern));
|
|
5515
|
+
}
|
|
5516
|
+
function shouldRetryCompactToolLoop(profile, text, hasToolHistory, userMessage) {
|
|
5517
|
+
return profile === "compact" && hasToolHistory && shouldRecoverCompactStall(text, userMessage);
|
|
5518
|
+
}
|
|
5519
|
+
function stableToolSignature(name, args) {
|
|
5520
|
+
const canonicalArgs = canonicalizeArgsForTool(name, args);
|
|
5521
|
+
const sortedEntries = Object.entries(canonicalArgs).sort(
|
|
5522
|
+
([left], [right]) => left.localeCompare(right)
|
|
5523
|
+
);
|
|
5524
|
+
return JSON.stringify([name, sortedEntries]);
|
|
5525
|
+
}
|
|
5526
|
+
function hasRecentDuplicateToolCall(recentToolSignatures, signature) {
|
|
5527
|
+
return recentToolSignatures.includes(signature);
|
|
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
|
+
}
|
|
5540
|
+
function normalizeToolToken(value) {
|
|
5541
|
+
return value.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
|
|
5542
|
+
}
|
|
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 };
|
|
5612
|
+
try {
|
|
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;
|
|
5620
|
+
}
|
|
5621
|
+
return null;
|
|
5622
|
+
} catch {
|
|
5623
|
+
}
|
|
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;
|
|
5637
|
+
}
|
|
5638
|
+
function coerceToolArgsForExecution(name, args) {
|
|
5639
|
+
const coerced = { ...args };
|
|
5640
|
+
if (name === "search") {
|
|
5641
|
+
if (typeof coerced.query !== "string" || !coerced.query.trim()) {
|
|
5642
|
+
if (typeof coerced.text === "string" && coerced.text.trim()) {
|
|
5643
|
+
coerced.query = coerced.text.trim();
|
|
5644
|
+
} else if (typeof coerced.term === "string" && coerced.term.trim()) {
|
|
5645
|
+
coerced.query = coerced.term.trim();
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
if (name === "navigate") {
|
|
5650
|
+
if (typeof coerced.url !== "string" || !coerced.url.trim()) {
|
|
5651
|
+
if (typeof coerced.href === "string" && coerced.href.trim()) {
|
|
5652
|
+
coerced.url = coerced.href.trim();
|
|
5653
|
+
} else if (typeof coerced.link === "string" && coerced.link.trim()) {
|
|
5654
|
+
coerced.url = coerced.link.trim();
|
|
5655
|
+
} else if (typeof coerced.text === "string" && /^https?:\/\//i.test(coerced.text.trim())) {
|
|
5656
|
+
coerced.url = coerced.text.trim();
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
return coerced;
|
|
5661
|
+
}
|
|
5662
|
+
function canonicalizeArgsForTool(name, args) {
|
|
5663
|
+
const canonical = coerceToolArgsForExecution(name, args);
|
|
5664
|
+
if (typeof canonical.url === "string") {
|
|
5665
|
+
canonical.url = canonicalizeUrlLike(canonical.url);
|
|
5666
|
+
}
|
|
5667
|
+
if (typeof canonical.query === "string") {
|
|
5668
|
+
canonical.query = canonical.query.trim().replace(/\s+/g, " ").toLowerCase();
|
|
5669
|
+
delete canonical.text;
|
|
5670
|
+
}
|
|
5671
|
+
if (typeof canonical.text === "string") {
|
|
5672
|
+
canonical.text = canonical.text.trim().replace(/\s+/g, " ");
|
|
5673
|
+
}
|
|
5674
|
+
return canonical;
|
|
5675
|
+
}
|
|
5676
|
+
function resolveToolCallName(rawName, args, availableToolNames) {
|
|
5677
|
+
const aliased = normalizeToolAlias(rawName);
|
|
5678
|
+
if (availableToolNames.has(aliased)) return aliased;
|
|
5679
|
+
const normalized = normalizeToolToken(rawName);
|
|
5680
|
+
if (availableToolNames.has(normalized)) return normalized;
|
|
5681
|
+
const hasUrl = typeof args.url === "string" && args.url.trim().length > 0;
|
|
5682
|
+
if (availableToolNames.has("navigate") && (hasUrl || /goto|navigate|open|visit|browser|url|link/.test(normalized))) {
|
|
5683
|
+
return "navigate";
|
|
5684
|
+
}
|
|
5685
|
+
if (availableToolNames.has("search") && (/search|find|lookup|query/.test(normalized) || normalized === "google" || normalized.startsWith("google_"))) {
|
|
5686
|
+
return "search";
|
|
5687
|
+
}
|
|
5688
|
+
if (availableToolNames.has("scroll") && /scroll|page_?down|page_?up/.test(normalized)) {
|
|
5689
|
+
return "scroll";
|
|
5690
|
+
}
|
|
5691
|
+
if (availableToolNames.has("read_page") && /read|scan|inspect|analy[sz]e|summari[sz]e/.test(normalized)) {
|
|
5692
|
+
return "read_page";
|
|
5693
|
+
}
|
|
5694
|
+
return aliased;
|
|
5695
|
+
}
|
|
5696
|
+
function logAgentLoopDebug(payload) {
|
|
5697
|
+
if (!shouldDebugAgentLoop()) return;
|
|
5698
|
+
try {
|
|
5699
|
+
console.log(`[Vessel agent-debug] ${JSON.stringify(payload)}`);
|
|
5700
|
+
} catch (err) {
|
|
5701
|
+
console.warn("[Vessel agent-debug] Failed to serialize debug payload:", err);
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
function recoverTextEncodedToolCalls(text, availableToolNames) {
|
|
5705
|
+
const trimmed = text.trim();
|
|
5706
|
+
if (!trimmed) return [];
|
|
5707
|
+
const candidates = trimmed.match(
|
|
5708
|
+
/([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*?\})(?=\s*$|\n{2,}|[A-Za-z0-9._ -]+\s*\[ARGS\])/g
|
|
5709
|
+
);
|
|
5710
|
+
if (!candidates || candidates.length === 0) return [];
|
|
5711
|
+
const recovered = [];
|
|
5712
|
+
for (const candidate of candidates) {
|
|
5713
|
+
const match = candidate.match(
|
|
5714
|
+
/^\s*([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*\})\s*$/
|
|
5715
|
+
);
|
|
5716
|
+
if (!match) continue;
|
|
5717
|
+
const rawName = match[1] ?? "";
|
|
5718
|
+
const argsJson = match[2] ?? "{}";
|
|
5719
|
+
let parsedArgs = {};
|
|
5720
|
+
try {
|
|
5721
|
+
parsedArgs = JSON.parse(argsJson);
|
|
5722
|
+
} catch {
|
|
5723
|
+
continue;
|
|
5434
5724
|
}
|
|
5435
|
-
|
|
5725
|
+
const resolvedName = resolveToolCallName(
|
|
5726
|
+
rawName,
|
|
5727
|
+
parsedArgs,
|
|
5728
|
+
availableToolNames
|
|
5729
|
+
);
|
|
5730
|
+
recovered.push({
|
|
5731
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5732
|
+
name: resolvedName,
|
|
5733
|
+
argsJson
|
|
5734
|
+
});
|
|
5735
|
+
}
|
|
5736
|
+
return recovered;
|
|
5737
|
+
}
|
|
5738
|
+
function recoverNarratedActionToolCalls(text, availableToolNames) {
|
|
5739
|
+
const trimmed = text.trim();
|
|
5740
|
+
if (!trimmed) return [];
|
|
5741
|
+
const recovered = [];
|
|
5742
|
+
const actionLines = trimmed.match(/^action:\s+.+$/gim) ?? [];
|
|
5743
|
+
for (const rawLine of actionLines) {
|
|
5744
|
+
const line = rawLine.replace(/^action:\s*/i, "").trim();
|
|
5745
|
+
if (!line) continue;
|
|
5746
|
+
const quotedValue = line.match(/"([^"]+)"/)?.[1]?.trim() ?? line.match(/'([^']+)'/)?.[1]?.trim() ?? "";
|
|
5747
|
+
const navigateMatch = line.match(
|
|
5748
|
+
/\b(?:navigate|open|go)\b(?:\s+(?:to|the url))?\s+(https?:\/\/[^\s)]+)\.?/i
|
|
5749
|
+
);
|
|
5750
|
+
if (navigateMatch?.[1]) {
|
|
5751
|
+
const argsJson = JSON.stringify({ url: navigateMatch[1].replace(/\.$/, "") });
|
|
5752
|
+
recovered.push({
|
|
5753
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5754
|
+
name: resolveToolCallName("navigate", { url: navigateMatch[1] }, availableToolNames),
|
|
5755
|
+
argsJson
|
|
5756
|
+
});
|
|
5757
|
+
continue;
|
|
5758
|
+
}
|
|
5759
|
+
const isSearchAction = /\bsearch\b/i.test(line) || /\btype\b/i.test(line) && /\bsearch box\b/i.test(line);
|
|
5760
|
+
if (isSearchAction && quotedValue && availableToolNames.has("search")) {
|
|
5761
|
+
recovered.push({
|
|
5762
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5763
|
+
name: "search",
|
|
5764
|
+
argsJson: JSON.stringify({ query: quotedValue })
|
|
5765
|
+
});
|
|
5766
|
+
continue;
|
|
5767
|
+
}
|
|
5768
|
+
if (/\b(?:read|scan)\b.*\bpage\b/i.test(line) && availableToolNames.has("read_page")) {
|
|
5769
|
+
recovered.push({
|
|
5770
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5771
|
+
name: "read_page",
|
|
5772
|
+
argsJson: JSON.stringify({ mode: "visible_only" })
|
|
5773
|
+
});
|
|
5774
|
+
continue;
|
|
5775
|
+
}
|
|
5776
|
+
const toolRefMatch = line.match(
|
|
5777
|
+
/\b(?:use|call)\s+([a-z_][a-z0-9_]*)(?:\s+tool)?\b/i
|
|
5778
|
+
);
|
|
5779
|
+
if (toolRefMatch?.[1]) {
|
|
5780
|
+
const toolName = resolveToolCallName(toolRefMatch[1], {}, availableToolNames);
|
|
5781
|
+
recovered.push({
|
|
5782
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5783
|
+
name: toolName,
|
|
5784
|
+
argsJson: "{}"
|
|
5785
|
+
});
|
|
5786
|
+
}
|
|
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
|
+
return recovered;
|
|
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;
|
|
5436
5825
|
}
|
|
5437
5826
|
class OpenAICompatProvider {
|
|
5827
|
+
agentToolProfile;
|
|
5438
5828
|
client;
|
|
5439
5829
|
model;
|
|
5830
|
+
providerId;
|
|
5440
5831
|
abortController = null;
|
|
5441
5832
|
constructor(config) {
|
|
5442
5833
|
const meta = PROVIDERS[config.id];
|
|
@@ -5445,7 +5836,9 @@ class OpenAICompatProvider {
|
|
|
5445
5836
|
apiKey: config.apiKey || "ollama",
|
|
5446
5837
|
baseURL
|
|
5447
5838
|
});
|
|
5839
|
+
this.providerId = config.id;
|
|
5448
5840
|
this.model = config.model || meta?.defaultModel || "gpt-4o";
|
|
5841
|
+
this.agentToolProfile = resolveAgentToolProfile(config);
|
|
5449
5842
|
}
|
|
5450
5843
|
async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
|
|
5451
5844
|
this.abortController = new AbortController();
|
|
@@ -5458,22 +5851,31 @@ class OpenAICompatProvider {
|
|
|
5458
5851
|
const stream = await this.client.chat.completions.create(
|
|
5459
5852
|
{
|
|
5460
5853
|
model: this.model,
|
|
5854
|
+
max_tokens: 4096,
|
|
5461
5855
|
stream: true,
|
|
5462
5856
|
messages
|
|
5463
5857
|
},
|
|
5464
5858
|
{ signal: this.abortController.signal }
|
|
5465
5859
|
);
|
|
5466
5860
|
for await (const chunk of stream) {
|
|
5467
|
-
const
|
|
5468
|
-
if (
|
|
5469
|
-
|
|
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);
|
|
5470
5870
|
}
|
|
5471
5871
|
}
|
|
5472
5872
|
} catch (err) {
|
|
5473
5873
|
if (err instanceof Error && err.name !== "AbortError") {
|
|
5474
|
-
onChunk(
|
|
5874
|
+
onChunk(
|
|
5875
|
+
`
|
|
5475
5876
|
|
|
5476
|
-
[Error: ${err.message}]`
|
|
5877
|
+
[Error: ${formatOpenAICompatErrorMessage(this.providerId, err.message)}]`
|
|
5878
|
+
);
|
|
5477
5879
|
}
|
|
5478
5880
|
} finally {
|
|
5479
5881
|
this.abortController = null;
|
|
@@ -5483,6 +5885,7 @@ class OpenAICompatProvider {
|
|
|
5483
5885
|
async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
|
|
5484
5886
|
this.abortController = new AbortController();
|
|
5485
5887
|
const openAITools = toOpenAITools(tools);
|
|
5888
|
+
const availableToolNames = new Set(tools.map((tool) => tool.name));
|
|
5486
5889
|
const messages = [
|
|
5487
5890
|
{ role: "system", content: systemPrompt },
|
|
5488
5891
|
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
@@ -5491,18 +5894,31 @@ class OpenAICompatProvider {
|
|
|
5491
5894
|
try {
|
|
5492
5895
|
const maxIterations = getEffectiveMaxIterations();
|
|
5493
5896
|
let iterationsUsed = 0;
|
|
5897
|
+
let compactRecoveryCount = 0;
|
|
5898
|
+
let compactCorrectionCount = 0;
|
|
5899
|
+
const recentCompactToolSignatures = [];
|
|
5900
|
+
const recentToolNames = [];
|
|
5901
|
+
let clickReadLoopNudged = false;
|
|
5494
5902
|
for (let i = 0; i < maxIterations; i++) {
|
|
5495
5903
|
iterationsUsed = i + 1;
|
|
5496
5904
|
let textAccum = "";
|
|
5497
5905
|
const toolCallAccums = {};
|
|
5498
5906
|
let finishReason = null;
|
|
5907
|
+
const hasToolHistory = messages.some((message) => message.role === "tool");
|
|
5908
|
+
const priorToolMessages = messages.filter(
|
|
5909
|
+
(message) => message.role === "tool"
|
|
5910
|
+
);
|
|
5911
|
+
const latestToolMessage = priorToolMessages.length > 0 ? priorToolMessages[priorToolMessages.length - 1] : null;
|
|
5912
|
+
const debugRoundLabel = hasToolHistory ? "post_tool" : "initial";
|
|
5499
5913
|
const stream = await this.client.chat.completions.create(
|
|
5500
5914
|
{
|
|
5501
5915
|
model: this.model,
|
|
5916
|
+
max_tokens: 4096,
|
|
5502
5917
|
stream: true,
|
|
5503
5918
|
messages,
|
|
5504
5919
|
tools: openAITools,
|
|
5505
|
-
tool_choice: "auto"
|
|
5920
|
+
tool_choice: "auto",
|
|
5921
|
+
temperature: agentTemperatureForProfile(this.agentToolProfile)
|
|
5506
5922
|
},
|
|
5507
5923
|
{ signal: this.abortController.signal }
|
|
5508
5924
|
);
|
|
@@ -5511,6 +5927,10 @@ class OpenAICompatProvider {
|
|
|
5511
5927
|
if (!choice) continue;
|
|
5512
5928
|
const delta = choice.delta;
|
|
5513
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
|
+
}
|
|
5514
5934
|
if (delta.content) {
|
|
5515
5935
|
textAccum += delta.content;
|
|
5516
5936
|
onChunk(delta.content);
|
|
@@ -5527,17 +5947,62 @@ class OpenAICompatProvider {
|
|
|
5527
5947
|
}
|
|
5528
5948
|
}
|
|
5529
5949
|
}
|
|
5530
|
-
|
|
5950
|
+
let toolCalls = Object.values(toolCallAccums);
|
|
5531
5951
|
for (const tc of Object.values(toolCallAccums)) {
|
|
5532
5952
|
if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5953
|
+
let parsedArgs = {};
|
|
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
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
tc.name = resolveToolCallName(tc.name, parsedArgs, availableToolNames);
|
|
5962
|
+
}
|
|
5963
|
+
if (toolCalls.length === 0) {
|
|
5964
|
+
const recoveredToolCalls = recoverTextEncodedToolCalls(
|
|
5965
|
+
textAccum,
|
|
5966
|
+
availableToolNames
|
|
5967
|
+
);
|
|
5968
|
+
if (recoveredToolCalls.length > 0) {
|
|
5969
|
+
toolCalls = recoveredToolCalls;
|
|
5970
|
+
} else {
|
|
5971
|
+
const narratedToolCalls = recoverNarratedActionToolCalls(
|
|
5972
|
+
textAccum,
|
|
5973
|
+
availableToolNames
|
|
5974
|
+
);
|
|
5975
|
+
if (narratedToolCalls.length > 0) {
|
|
5976
|
+
toolCalls = narratedToolCalls;
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5533
5979
|
}
|
|
5980
|
+
logAgentLoopDebug({
|
|
5981
|
+
model: this.model,
|
|
5982
|
+
profile: this.agentToolProfile,
|
|
5983
|
+
iteration: i + 1,
|
|
5984
|
+
round: debugRoundLabel,
|
|
5985
|
+
priorToolCount: priorToolMessages.length,
|
|
5986
|
+
latestToolResultPreview: latestToolMessage ? previewToolDebugContent(String(latestToolMessage.content || "")) : null,
|
|
5987
|
+
finishReason,
|
|
5988
|
+
streamedText: previewDebugValue(textAccum),
|
|
5989
|
+
recoveredFromText: Object.keys(toolCallAccums).length === 0 && toolCalls.length > 0,
|
|
5990
|
+
toolCalls: toolCalls.map((tc) => ({
|
|
5991
|
+
id: tc.id,
|
|
5992
|
+
name: tc.name,
|
|
5993
|
+
argsJson: previewDebugValue(tc.argsJson || "{}", 300)
|
|
5994
|
+
}))
|
|
5995
|
+
});
|
|
5534
5996
|
const malformedToolCalls = /* @__PURE__ */ new Set();
|
|
5535
5997
|
for (const tc of toolCalls) {
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
} catch {
|
|
5998
|
+
const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
|
|
5999
|
+
if (!repairedArgs) {
|
|
5539
6000
|
malformedToolCalls.add(tc.id);
|
|
5540
6001
|
tc.argsJson = "{}";
|
|
6002
|
+
continue;
|
|
6003
|
+
}
|
|
6004
|
+
if (repairedArgs.repaired) {
|
|
6005
|
+
tc.argsJson = JSON.stringify(repairedArgs.args);
|
|
5541
6006
|
}
|
|
5542
6007
|
}
|
|
5543
6008
|
const assistantMsg = {
|
|
@@ -5552,7 +6017,28 @@ class OpenAICompatProvider {
|
|
|
5552
6017
|
}
|
|
5553
6018
|
};
|
|
5554
6019
|
messages.push(assistantMsg);
|
|
5555
|
-
if (toolCalls.length === 0)
|
|
6020
|
+
if (toolCalls.length === 0) {
|
|
6021
|
+
if (compactRecoveryCount < 2 && shouldRetryCompactToolLoop(
|
|
6022
|
+
this.agentToolProfile,
|
|
6023
|
+
textAccum,
|
|
6024
|
+
hasToolHistory,
|
|
6025
|
+
userMessage
|
|
6026
|
+
)) {
|
|
6027
|
+
compactRecoveryCount += 1;
|
|
6028
|
+
messages.push({
|
|
6029
|
+
role: "user",
|
|
6030
|
+
content: `[System] ${buildCompactRecoveryPrompt(
|
|
6031
|
+
userMessage,
|
|
6032
|
+
textAccum,
|
|
6033
|
+
latestToolMessage ? String(latestToolMessage.content || "") : null
|
|
6034
|
+
)}`
|
|
6035
|
+
});
|
|
6036
|
+
continue;
|
|
6037
|
+
}
|
|
6038
|
+
break;
|
|
6039
|
+
}
|
|
6040
|
+
compactRecoveryCount = 0;
|
|
6041
|
+
const iterationToolResultPreviews = [];
|
|
5556
6042
|
for (const tc of toolCalls) {
|
|
5557
6043
|
if (malformedToolCalls.has(tc.id)) {
|
|
5558
6044
|
onChunk(`
|
|
@@ -5565,43 +6051,341 @@ class OpenAICompatProvider {
|
|
|
5565
6051
|
});
|
|
5566
6052
|
continue;
|
|
5567
6053
|
}
|
|
5568
|
-
let args = {};
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
6054
|
+
let args = {};
|
|
6055
|
+
const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
|
|
6056
|
+
if (!repairedArgs) {
|
|
6057
|
+
onChunk(`
|
|
6058
|
+
<<tool:${tc.name}:⚠ invalid args>>
|
|
6059
|
+
`);
|
|
6060
|
+
messages.push({
|
|
6061
|
+
role: "tool",
|
|
6062
|
+
tool_call_id: tc.id,
|
|
6063
|
+
content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON.`
|
|
6064
|
+
});
|
|
6065
|
+
continue;
|
|
6066
|
+
}
|
|
6067
|
+
args = repairedArgs.args;
|
|
6068
|
+
args = coerceToolArgsForExecution(tc.name, args);
|
|
6069
|
+
if (!availableToolNames.has(tc.name)) {
|
|
6070
|
+
onChunk(`
|
|
6071
|
+
<<tool:unsupported_tool:⚠ unsupported>>
|
|
6072
|
+
`);
|
|
6073
|
+
messages.push({
|
|
6074
|
+
role: "tool",
|
|
6075
|
+
tool_call_id: tc.id,
|
|
6076
|
+
content: `Error: ${tc.name} is not a supported tool. Choose one of the available browser tools instead.`
|
|
6077
|
+
});
|
|
6078
|
+
compactCorrectionCount += 1;
|
|
6079
|
+
if (compactCorrectionCount >= 2) {
|
|
6080
|
+
messages.push({
|
|
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.`
|
|
6083
|
+
});
|
|
6084
|
+
}
|
|
6085
|
+
continue;
|
|
6086
|
+
}
|
|
6087
|
+
const toolSignature = stableToolSignature(tc.name, args);
|
|
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(
|
|
6098
|
+
recentCompactToolSignatures,
|
|
6099
|
+
toolSignature
|
|
6100
|
+
)) {
|
|
6101
|
+
onChunk(`
|
|
6102
|
+
<<tool:${tc.name}:↻ duplicate suppressed>>
|
|
6103
|
+
`);
|
|
6104
|
+
messages.push({
|
|
6105
|
+
role: "tool",
|
|
6106
|
+
tool_call_id: tc.id,
|
|
6107
|
+
content: `Error: Repeated the same tool call (${tc.name}) with the same arguments twice in a row. Do not repeat it. Continue with the next logical step for the original task.`
|
|
6108
|
+
});
|
|
6109
|
+
compactCorrectionCount += 1;
|
|
6110
|
+
if (compactCorrectionCount >= 2) {
|
|
6111
|
+
messages.push({
|
|
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.`
|
|
6114
|
+
});
|
|
6115
|
+
}
|
|
6116
|
+
continue;
|
|
6117
|
+
}
|
|
6118
|
+
const argSummary = args.url || args.query || args.text || args.direction || "";
|
|
6119
|
+
onChunk(`
|
|
6120
|
+
<<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
|
|
6121
|
+
`);
|
|
6122
|
+
let result;
|
|
6123
|
+
try {
|
|
6124
|
+
result = await onToolCall(tc.name, args);
|
|
6125
|
+
} catch (toolErr) {
|
|
6126
|
+
const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
6127
|
+
result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
|
|
6128
|
+
}
|
|
6129
|
+
let toolContent = result;
|
|
6130
|
+
try {
|
|
6131
|
+
const parsed = JSON.parse(result);
|
|
6132
|
+
if (isRichToolResult(parsed)) {
|
|
6133
|
+
toolContent = parsed.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
6134
|
+
}
|
|
6135
|
+
} catch {
|
|
6136
|
+
}
|
|
6137
|
+
if (this.agentToolProfile === "compact") {
|
|
6138
|
+
recentCompactToolSignatures.push(toolSignature);
|
|
6139
|
+
if (recentCompactToolSignatures.length > 4) {
|
|
6140
|
+
recentCompactToolSignatures.shift();
|
|
6141
|
+
}
|
|
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
|
+
}
|
|
6152
|
+
compactCorrectionCount = 0;
|
|
6153
|
+
iterationToolResultPreviews.push(toolContent);
|
|
6154
|
+
messages.push({
|
|
6155
|
+
role: "tool",
|
|
6156
|
+
tool_call_id: tc.id,
|
|
6157
|
+
content: toolContent
|
|
6158
|
+
});
|
|
6159
|
+
}
|
|
6160
|
+
const followUpReminder = followUpReminderForProfile(
|
|
6161
|
+
this.agentToolProfile,
|
|
6162
|
+
userMessage,
|
|
6163
|
+
textAccum,
|
|
6164
|
+
iterationToolResultPreviews.length > 0 ? iterationToolResultPreviews[iterationToolResultPreviews.length - 1] : null
|
|
6165
|
+
);
|
|
6166
|
+
if (followUpReminder) {
|
|
6167
|
+
messages.push(followUpReminder);
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
if (iterationsUsed >= maxIterations) {
|
|
6171
|
+
onChunk(`
|
|
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) {
|
|
5572
6327
|
onChunk(`
|
|
5573
|
-
<<tool:${
|
|
6328
|
+
<<tool:${tb.name}:⚠ invalid args>>
|
|
5574
6329
|
`);
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON
|
|
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
|
|
5579
6335
|
});
|
|
5580
6336
|
continue;
|
|
5581
6337
|
}
|
|
5582
|
-
const argSummary =
|
|
6338
|
+
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
5583
6339
|
onChunk(`
|
|
5584
|
-
<<tool:${
|
|
6340
|
+
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
5585
6341
|
`);
|
|
5586
6342
|
let result;
|
|
5587
6343
|
try {
|
|
5588
|
-
result = await onToolCall(
|
|
6344
|
+
result = await onToolCall(tb.name, tb.input);
|
|
5589
6345
|
} catch (toolErr) {
|
|
5590
6346
|
const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
5591
6347
|
result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
|
|
5592
6348
|
}
|
|
5593
|
-
let
|
|
6349
|
+
let parsedRich = null;
|
|
5594
6350
|
try {
|
|
5595
6351
|
const parsed = JSON.parse(result);
|
|
5596
|
-
if (isRichToolResult(parsed))
|
|
5597
|
-
toolContent = parsed.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
5598
|
-
}
|
|
6352
|
+
if (isRichToolResult(parsed)) parsedRich = parsed;
|
|
5599
6353
|
} catch {
|
|
5600
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;
|
|
5601
6386
|
messages.push({
|
|
5602
|
-
role: "
|
|
5603
|
-
|
|
5604
|
-
content: toolContent
|
|
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.`
|
|
5605
6389
|
});
|
|
5606
6390
|
}
|
|
5607
6391
|
}
|
|
@@ -5653,6 +6437,55 @@ function validateProviderConnection(config, options = { requireModel: true }) {
|
|
|
5653
6437
|
}
|
|
5654
6438
|
return null;
|
|
5655
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
|
+
}
|
|
5656
6489
|
async function fetchProviderModels(config) {
|
|
5657
6490
|
const normalized = sanitizeProviderConfig(config);
|
|
5658
6491
|
const error = validateProviderConnection(normalized, { requireModel: false });
|
|
@@ -5662,7 +6495,7 @@ async function fetchProviderModels(config) {
|
|
|
5662
6495
|
if (normalized.id === "anthropic") {
|
|
5663
6496
|
const client2 = new Anthropic({ apiKey: normalized.apiKey });
|
|
5664
6497
|
const page2 = await client2.models.list();
|
|
5665
|
-
return page2.data.map((model) => model.id);
|
|
6498
|
+
return { ok: true, models: page2.data.map((model) => model.id) };
|
|
5666
6499
|
}
|
|
5667
6500
|
const meta = PROVIDERS[normalized.id];
|
|
5668
6501
|
const baseURL = normalized.baseUrl || meta?.defaultBaseUrl || "https://api.openai.com/v1";
|
|
@@ -5671,7 +6504,13 @@ async function fetchProviderModels(config) {
|
|
|
5671
6504
|
baseURL
|
|
5672
6505
|
});
|
|
5673
6506
|
const page = await client.models.list();
|
|
5674
|
-
|
|
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
|
+
};
|
|
5675
6514
|
}
|
|
5676
6515
|
function createProvider(config) {
|
|
5677
6516
|
const normalized = sanitizeProviderConfig(config);
|
|
@@ -5728,7 +6567,7 @@ function createTraceSession(query, url, title) {
|
|
|
5728
6567
|
}
|
|
5729
6568
|
const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
|
|
5730
6569
|
const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
|
|
5731
|
-
function elementLabel(el) {
|
|
6570
|
+
function elementLabel$1(el) {
|
|
5732
6571
|
return el.text?.trim() || el.label?.trim() || el.value?.trim() || el.placeholder?.trim() || void 0;
|
|
5733
6572
|
}
|
|
5734
6573
|
function isOverlayAction(el) {
|
|
@@ -5744,11 +6583,11 @@ function isRadioOption(el) {
|
|
|
5744
6583
|
function normalizeAction(el) {
|
|
5745
6584
|
return {
|
|
5746
6585
|
index: el.index,
|
|
5747
|
-
label: elementLabel(el),
|
|
6586
|
+
label: elementLabel$1(el),
|
|
5748
6587
|
selector: el.selector,
|
|
5749
6588
|
role: el.role,
|
|
5750
6589
|
labelSource: el.labelSource,
|
|
5751
|
-
looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel(el))
|
|
6590
|
+
looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel$1(el))
|
|
5752
6591
|
};
|
|
5753
6592
|
}
|
|
5754
6593
|
function normalizeStoredAction(action) {
|
|
@@ -6044,7 +6883,7 @@ function getCartItemLinks(page) {
|
|
|
6044
6883
|
return false;
|
|
6045
6884
|
}
|
|
6046
6885
|
if (blockedText.test(text) || blockedHref.test(href)) return false;
|
|
6047
|
-
const key = `${normalizeComparable(text)}|${normalizeUrlForMatch(href) || href}`;
|
|
6886
|
+
const key = `${normalizeComparable$1(text)}|${normalizeUrlForMatch$1(href) || href}`;
|
|
6048
6887
|
if (seen.has(key)) return false;
|
|
6049
6888
|
seen.add(key);
|
|
6050
6889
|
return true;
|
|
@@ -6113,6 +6952,67 @@ function formatCartSnapshot(page) {
|
|
|
6113
6952
|
function isVisibleToUser(el) {
|
|
6114
6953
|
return el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true;
|
|
6115
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
|
+
}
|
|
6116
7016
|
function getDialogFocusedElements(page) {
|
|
6117
7017
|
return page.interactiveElements.filter(
|
|
6118
7018
|
(el) => isVisibleToUser(el) && el.context === "dialog"
|
|
@@ -6166,6 +7066,10 @@ function formatInteractiveElements(elements) {
|
|
|
6166
7066
|
const scoreEl = (el) => {
|
|
6167
7067
|
let s = 0;
|
|
6168
7068
|
if (el.context === "dialog") s -= 40;
|
|
7069
|
+
const purchasePriority = purchaseActionPriority(el);
|
|
7070
|
+
if (Number.isFinite(purchasePriority)) {
|
|
7071
|
+
s -= 25 - purchasePriority * 5;
|
|
7072
|
+
}
|
|
6169
7073
|
if (el.visible === false) s += 100;
|
|
6170
7074
|
if (el.inViewport === false) s += 50;
|
|
6171
7075
|
if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
|
|
@@ -6525,10 +7429,10 @@ function chooseAgentReadMode(page) {
|
|
|
6525
7429
|
return "visible_only";
|
|
6526
7430
|
}
|
|
6527
7431
|
}
|
|
6528
|
-
function normalizeComparable(value) {
|
|
7432
|
+
function normalizeComparable$1(value) {
|
|
6529
7433
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
|
|
6530
7434
|
}
|
|
6531
|
-
function normalizeUrlForMatch(value) {
|
|
7435
|
+
function normalizeUrlForMatch$1(value) {
|
|
6532
7436
|
if (!value) return null;
|
|
6533
7437
|
try {
|
|
6534
7438
|
const url = new URL(value);
|
|
@@ -6538,7 +7442,7 @@ function normalizeUrlForMatch(value) {
|
|
|
6538
7442
|
return value.trim().replace(/\/+$/, "").toLowerCase() || null;
|
|
6539
7443
|
}
|
|
6540
7444
|
}
|
|
6541
|
-
function getUrlPathSegments(value) {
|
|
7445
|
+
function getUrlPathSegments$1(value) {
|
|
6542
7446
|
if (!value) return [];
|
|
6543
7447
|
try {
|
|
6544
7448
|
return new URL(value).pathname.split("/").filter(Boolean);
|
|
@@ -6547,7 +7451,7 @@ function getUrlPathSegments(value) {
|
|
|
6547
7451
|
}
|
|
6548
7452
|
}
|
|
6549
7453
|
function isSearchOrListingPage(page) {
|
|
6550
|
-
const haystack = normalizeComparable(
|
|
7454
|
+
const haystack = normalizeComparable$1(
|
|
6551
7455
|
[
|
|
6552
7456
|
page.url,
|
|
6553
7457
|
page.title,
|
|
@@ -6587,30 +7491,30 @@ function getResultCandidates(page) {
|
|
|
6587
7491
|
const entityItems = collectJsonLdEntityItems(page.jsonLd ?? []);
|
|
6588
7492
|
const entityNames = new Set(
|
|
6589
7493
|
entityItems.map(
|
|
6590
|
-
(item) => typeof item.name === "string" ? normalizeComparable(item.name) : ""
|
|
7494
|
+
(item) => typeof item.name === "string" ? normalizeComparable$1(item.name) : ""
|
|
6591
7495
|
).filter(Boolean)
|
|
6592
7496
|
);
|
|
6593
7497
|
const entityUrls = new Set(
|
|
6594
7498
|
entityItems.map(
|
|
6595
|
-
(item) => typeof item.url === "string" ? normalizeUrlForMatch(item.url) : null
|
|
7499
|
+
(item) => typeof item.url === "string" ? normalizeUrlForMatch$1(item.url) : null
|
|
6596
7500
|
).filter((value) => Boolean(value))
|
|
6597
7501
|
);
|
|
6598
|
-
const pageHost = normalizeUrlForMatch(page.url);
|
|
7502
|
+
const pageHost = normalizeUrlForMatch$1(page.url);
|
|
6599
7503
|
const searchOrListingPage = isSearchOrListingPage(page);
|
|
6600
7504
|
const scored = page.interactiveElements.filter(
|
|
6601
7505
|
(element) => element.type === "link" && element.text?.trim() && element.href
|
|
6602
7506
|
).map((element) => {
|
|
6603
7507
|
const text = element.text?.trim() || "";
|
|
6604
|
-
const comparableText = normalizeComparable(text);
|
|
6605
|
-
const href = normalizeUrlForMatch(element.href);
|
|
6606
|
-
const haystack = normalizeComparable(
|
|
7508
|
+
const comparableText = normalizeComparable$1(text);
|
|
7509
|
+
const href = normalizeUrlForMatch$1(element.href);
|
|
7510
|
+
const haystack = normalizeComparable$1(
|
|
6607
7511
|
[element.text, element.description, element.selector, element.href].filter(Boolean).join(" ")
|
|
6608
7512
|
);
|
|
6609
7513
|
let score = 0;
|
|
6610
7514
|
if (entityNames.has(comparableText)) score += 6;
|
|
6611
7515
|
if (href && entityUrls.has(href)) score += 6;
|
|
6612
7516
|
if (entityItems.some((item) => {
|
|
6613
|
-
const name = typeof item.name === "string" ? normalizeComparable(item.name) : "";
|
|
7517
|
+
const name = typeof item.name === "string" ? normalizeComparable$1(item.name) : "";
|
|
6614
7518
|
return Boolean(name) && (name.includes(comparableText) || comparableText.includes(name));
|
|
6615
7519
|
})) {
|
|
6616
7520
|
score += 4;
|
|
@@ -6624,7 +7528,7 @@ function getResultCandidates(page) {
|
|
|
6624
7528
|
} catch {
|
|
6625
7529
|
}
|
|
6626
7530
|
}
|
|
6627
|
-
const hrefSegments = getUrlPathSegments(element.href);
|
|
7531
|
+
const hrefSegments = getUrlPathSegments$1(element.href);
|
|
6628
7532
|
if (hrefSegments.length >= 2) score += 1;
|
|
6629
7533
|
if (text.includes("/")) score += 1;
|
|
6630
7534
|
if (searchOrListingPage && (element.context === "article" || element.context === "main" || element.context === "content")) {
|
|
@@ -6657,7 +7561,7 @@ function getResultCandidates(page) {
|
|
|
6657
7561
|
);
|
|
6658
7562
|
const seen = /* @__PURE__ */ new Set();
|
|
6659
7563
|
return scored.map(({ element }) => element).filter((element) => {
|
|
6660
|
-
const key = `${normalizeComparable(element.text || "")}|${normalizeUrlForMatch(element.href) || ""}`;
|
|
7564
|
+
const key = `${normalizeComparable$1(element.text || "")}|${normalizeUrlForMatch$1(element.href) || ""}`;
|
|
6661
7565
|
if (seen.has(key)) return false;
|
|
6662
7566
|
seen.add(key);
|
|
6663
7567
|
return true;
|
|
@@ -6893,6 +7797,10 @@ function buildScopedContext(page, mode) {
|
|
|
6893
7797
|
})).filter((form) => form.fields.length > 0)
|
|
6894
7798
|
};
|
|
6895
7799
|
const quantityElements = getQuantityElements(visiblePage);
|
|
7800
|
+
const purchaseActions = getPurchaseActionElements(visiblePage, {
|
|
7801
|
+
visibleOnly: true
|
|
7802
|
+
});
|
|
7803
|
+
const offscreenPurchaseActions = getOffscreenPurchaseActionElements(page);
|
|
6896
7804
|
const cartSnapshot = formatCartSnapshot(visiblePage);
|
|
6897
7805
|
const visibleForms = visiblePage.forms;
|
|
6898
7806
|
const dialogFocus = formatDialogFocus(page);
|
|
@@ -6948,6 +7856,19 @@ function buildScopedContext(page, mode) {
|
|
|
6948
7856
|
sections.push(formatQuantityElements(quantityElements));
|
|
6949
7857
|
sections.push("");
|
|
6950
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
|
+
}
|
|
6951
7872
|
if (visiblePage.interactiveElements.length > 0) {
|
|
6952
7873
|
sections.push(
|
|
6953
7874
|
`### Visible In-Viewport Interactive Elements (${visiblePage.interactiveElements.length})`
|
|
@@ -7216,28 +8137,466 @@ ${truncateContent(page.content)}
|
|
|
7216
8137
|
**Task:** Summarize this web page based on the structure and content above. Identify the main purpose, key sections, and important interactive elements.`
|
|
7217
8138
|
};
|
|
7218
8139
|
}
|
|
7219
|
-
function buildQuestionPrompt(page, question) {
|
|
7220
|
-
const structuredContext = buildStructuredContext(page);
|
|
7221
|
-
return {
|
|
7222
|
-
system: "You are Vessel, an AI browsing assistant. Use the provided page structure and content to answer questions accurately. You can reference specific elements by their labels or positions.",
|
|
7223
|
-
user: `${structuredContext}
|
|
7224
|
-
|
|
7225
|
-
## PAGE CONTENT
|
|
7226
|
-
|
|
7227
|
-
${truncateContent(page.content)}
|
|
7228
|
-
|
|
7229
|
-
---
|
|
7230
|
-
|
|
7231
|
-
**Question:** ${question}
|
|
7232
|
-
|
|
7233
|
-
**Instructions:** Answer based on the page structure and content above. If the question asks about interactive elements, forms, or navigation, use the structured context to provide specific details.`
|
|
7234
|
-
};
|
|
8140
|
+
function buildQuestionPrompt(page, question) {
|
|
8141
|
+
const structuredContext = buildStructuredContext(page);
|
|
8142
|
+
return {
|
|
8143
|
+
system: "You are Vessel, an AI browsing assistant. Use the provided page structure and content to answer questions accurately. You can reference specific elements by their labels or positions.",
|
|
8144
|
+
user: `${structuredContext}
|
|
8145
|
+
|
|
8146
|
+
## PAGE CONTENT
|
|
8147
|
+
|
|
8148
|
+
${truncateContent(page.content)}
|
|
8149
|
+
|
|
8150
|
+
---
|
|
8151
|
+
|
|
8152
|
+
**Question:** ${question}
|
|
8153
|
+
|
|
8154
|
+
**Instructions:** Answer based on the page structure and content above. If the question asks about interactive elements, forms, or navigation, use the structured context to provide specific details.`
|
|
8155
|
+
};
|
|
8156
|
+
}
|
|
8157
|
+
function buildGeneralPrompt(query) {
|
|
8158
|
+
return {
|
|
8159
|
+
system: "You are Vessel, an AI assistant embedded in a web browser. You can normally see the content of the page the user is viewing, but no page is currently active. Help the user with their browsing needs. Be concise and helpful.",
|
|
8160
|
+
user: query
|
|
8161
|
+
};
|
|
8162
|
+
}
|
|
8163
|
+
const SHARED_CORE_INSTRUCTIONS = [
|
|
8164
|
+
"You can see the page the user is viewing. The content above is from the page.",
|
|
8165
|
+
"The structured page context always refers to the tab currently visible to the human unless a later tool call changes tabs.",
|
|
8166
|
+
"Use tools to interact with the page when asked to do something (navigate, click, type, select options, submit forms, press keys, scroll).",
|
|
8167
|
+
"Only say you completed an action after the corresponding tool succeeds. If no tool supports the request, say so plainly.",
|
|
8168
|
+
"Call one tool at a time unless you are certain your provider supports parallel tool calls. Sequential calls are more reliable.",
|
|
8169
|
+
"ACT, DON'T HEDGE: You have a full browser. If the user asks you to go somewhere and do something, start doing it immediately."
|
|
8170
|
+
];
|
|
8171
|
+
const SHARED_NAVIGATION_INSTRUCTIONS = [
|
|
8172
|
+
"Use current_tab when you only need to know what the human is currently looking at. Use list_tabs before switching context across multiple tabs.",
|
|
8173
|
+
"Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.",
|
|
8174
|
+
"After navigating to a new site, do not call read_page immediately unless you are genuinely stuck. Prefer the site's search box, known navigation patterns, or clicking a visible section first.",
|
|
8175
|
+
"On retail and marketplace sites, prefer the site's visible search box, filters, and result pages over direct product URLs.",
|
|
8176
|
+
"For broad discovery tasks, prefer direct sources and site-specific search over generic search engines."
|
|
8177
|
+
];
|
|
8178
|
+
const SHARED_READ_INSTRUCTIONS = [
|
|
8179
|
+
"The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.",
|
|
8180
|
+
"When you only need detail on one result, card, or form section, use inspect_element instead of reading the whole page.",
|
|
8181
|
+
'Escalate page reads progressively: read_page(mode="glance"), then visible_only/results_only/forms_only/summary/text_only as needed. Use read_page(mode="debug") only as a last resort.',
|
|
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.',
|
|
8183
|
+
"Use screenshot when you need the exact rendered page or text extraction is failing.",
|
|
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.",
|
|
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."
|
|
8186
|
+
];
|
|
8187
|
+
const DEFAULT_EXTRA_INSTRUCTIONS = [
|
|
8188
|
+
"Create a checkpoint before risky multi-step flows or before leaving an important state.",
|
|
8189
|
+
"Use save_session after completing a login flow you may need again later, and load_session to resume that authenticated state in future runs.",
|
|
8190
|
+
"If the user says they highlighted or selected text, use read_page before falling back to screenshots because it includes active selection and visible unsaved highlights.",
|
|
8191
|
+
"If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.",
|
|
8192
|
+
"If the page context reports a rate limit, human verification, or access warning, stop using that page and switch to a different source.",
|
|
8193
|
+
"Reference interactive elements by their index number (shown as [#N] in the listings above).",
|
|
8194
|
+
"Be concise. Explain what you're doing as you go.",
|
|
8195
|
+
"For simple questions about the page, just answer directly without using tools.",
|
|
8196
|
+
"VISUAL AWARENESS: The human is watching the browser alongside this chat. Use highlights proactively when you reference specific on-page findings or errors.",
|
|
8197
|
+
"After completing a task or answering a question, offer 1-2 brief, natural follow-up suggestions that make sense in context.",
|
|
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.',
|
|
8199
|
+
"USE YOUR KNOWLEDGE: When the user asks for recommendations, make a clear recommendation, explain your reasoning briefly, and then execute.",
|
|
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."
|
|
8202
|
+
];
|
|
8203
|
+
const COMPACT_FOCUS_INSTRUCTIONS = [
|
|
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.",
|
|
8205
|
+
"Do not ask the user for permission to continue a task they already requested.",
|
|
8206
|
+
"Stay on the current task until it is complete. Do not restart completed phases such as re-navigating to the same site or redoing discovery after you already have candidates.",
|
|
8207
|
+
"If you are already on the requested site, do not navigate to its homepage again unless the current page is clearly unusable.",
|
|
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.",
|
|
8209
|
+
"Use current_tab only if you are genuinely unsure of the current page after reading the latest tool result.",
|
|
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.",
|
|
8215
|
+
"Keep your reasoning short. Prefer taking the next tool action over writing a long plan."
|
|
8216
|
+
];
|
|
8217
|
+
function buildInstructionBlock(instructions) {
|
|
8218
|
+
return instructions.map((line) => `- ${line}`).join("\n");
|
|
8219
|
+
}
|
|
8220
|
+
function buildContextBlock(input) {
|
|
8221
|
+
return `You are Vessel, an AI agent embedded in a web browser. You can see the current page and interact with it using tools.
|
|
8222
|
+
|
|
8223
|
+
THE USER IS CURRENTLY LOOKING AT:
|
|
8224
|
+
Title: ${input.activeTabTitle}
|
|
8225
|
+
URL: ${input.activeTabUrl}${input.tabSummary || ""}
|
|
8226
|
+
|
|
8227
|
+
When the user says "this page", "this article", "this site", or asks about what they're viewing, they mean the page above. The context below is from that page.
|
|
8228
|
+
|
|
8229
|
+
Current page context:
|
|
8230
|
+
This brief is intentionally minimal and filtered for speed. It omits most page text and low-value chrome unless you explicitly ask for more.
|
|
8231
|
+
Default brief mode: ${input.defaultReadMode}
|
|
8232
|
+
Detected page type: ${input.pageType}
|
|
8233
|
+
|
|
8234
|
+
${input.structuredContext}
|
|
8235
|
+
|
|
8236
|
+
Supervisor state:
|
|
8237
|
+
- paused: ${input.supervisorPaused ? "yes" : "no"}
|
|
8238
|
+
- approval mode: ${input.approvalMode}
|
|
8239
|
+
- pending approvals: ${input.pendingApprovals}
|
|
8240
|
+
|
|
8241
|
+
Recent checkpoints:
|
|
8242
|
+
${input.recentCheckpoints || "- none"}
|
|
8243
|
+
|
|
8244
|
+
Task tracker:
|
|
8245
|
+
${input.taskTrackerContext || "- none"}`;
|
|
8246
|
+
}
|
|
8247
|
+
function buildAgentSystemPrompt(input) {
|
|
8248
|
+
const instructionBlocks = input.profile === "compact" ? [
|
|
8249
|
+
buildInstructionBlock(SHARED_CORE_INSTRUCTIONS),
|
|
8250
|
+
buildInstructionBlock(SHARED_NAVIGATION_INSTRUCTIONS),
|
|
8251
|
+
buildInstructionBlock(SHARED_READ_INSTRUCTIONS),
|
|
8252
|
+
buildInstructionBlock(COMPACT_FOCUS_INSTRUCTIONS)
|
|
8253
|
+
] : [
|
|
8254
|
+
buildInstructionBlock(SHARED_CORE_INSTRUCTIONS),
|
|
8255
|
+
buildInstructionBlock(SHARED_NAVIGATION_INSTRUCTIONS),
|
|
8256
|
+
buildInstructionBlock(SHARED_READ_INSTRUCTIONS),
|
|
8257
|
+
buildInstructionBlock(DEFAULT_EXTRA_INSTRUCTIONS)
|
|
8258
|
+
];
|
|
8259
|
+
return [
|
|
8260
|
+
buildContextBlock(input),
|
|
8261
|
+
"Instructions:",
|
|
8262
|
+
...instructionBlocks
|
|
8263
|
+
].join("\n\n");
|
|
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
|
+
);
|
|
7235
8481
|
}
|
|
7236
|
-
function
|
|
7237
|
-
return
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
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");
|
|
7241
8600
|
}
|
|
7242
8601
|
const WRAPPING_QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
|
|
7243
8602
|
function stripWrappingQuotes(value) {
|
|
@@ -7384,10 +8743,11 @@ const TOOL_DEFINITIONS = [
|
|
|
7384
8743
|
{
|
|
7385
8744
|
name: "click",
|
|
7386
8745
|
title: "Click Element",
|
|
7387
|
-
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.",
|
|
7388
8747
|
inputSchema: {
|
|
7389
8748
|
index: zod.z.number().optional().describe("Element index from the page content listing"),
|
|
7390
|
-
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")
|
|
7391
8751
|
},
|
|
7392
8752
|
tier: 0
|
|
7393
8753
|
},
|
|
@@ -7508,10 +8868,11 @@ const TOOL_DEFINITIONS = [
|
|
|
7508
8868
|
{
|
|
7509
8869
|
name: "inspect_element",
|
|
7510
8870
|
title: "Inspect Element",
|
|
7511
|
-
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.",
|
|
7512
8872
|
inputSchema: {
|
|
7513
8873
|
index: zod.z.number().optional().describe("Element index to inspect"),
|
|
7514
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"),
|
|
7515
8876
|
limit: zod.z.number().optional().describe("Maximum nearby controls to include (default 8)")
|
|
7516
8877
|
},
|
|
7517
8878
|
tier: 1,
|
|
@@ -7863,10 +9224,11 @@ const TOOL_DEFINITIONS = [
|
|
|
7863
9224
|
{
|
|
7864
9225
|
name: "scroll_to_element",
|
|
7865
9226
|
title: "Scroll To Element",
|
|
7866
|
-
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.",
|
|
7867
9228
|
inputSchema: {
|
|
7868
9229
|
index: zod.z.number().optional().describe("Element index to scroll to"),
|
|
7869
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"),
|
|
7870
9232
|
position: zod.z.enum(["center", "top", "bottom"]).optional().describe(
|
|
7871
9233
|
"Where to position the element in the viewport (default center)"
|
|
7872
9234
|
)
|
|
@@ -7956,10 +9318,16 @@ const CONTEXT_HINTS = {
|
|
|
7956
9318
|
scroll: "💡 Long content — scroll to continue — "
|
|
7957
9319
|
}
|
|
7958
9320
|
};
|
|
7959
|
-
function scoreForContext(toolName, pageType) {
|
|
9321
|
+
function scoreForContext(toolName, pageType, intents) {
|
|
7960
9322
|
const def = defByName[toolName];
|
|
7961
9323
|
if (!def) return 500;
|
|
7962
9324
|
if (pageType === "SEARCH_READY") {
|
|
9325
|
+
if (intents.has("navigate")) {
|
|
9326
|
+
if (toolName === "navigate") return -30;
|
|
9327
|
+
if (toolName === "search") return 2;
|
|
9328
|
+
if (toolName === "type_text") return 5;
|
|
9329
|
+
if (toolName === "press_key") return 6;
|
|
9330
|
+
}
|
|
7963
9331
|
if (toolName === "search") return -20;
|
|
7964
9332
|
if (toolName === "type_text") return 5;
|
|
7965
9333
|
if (toolName === "press_key") return 6;
|
|
@@ -7993,10 +9361,53 @@ const ALWAYS_FAST_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
7993
9361
|
"screenshot",
|
|
7994
9362
|
"inspect_element"
|
|
7995
9363
|
]);
|
|
9364
|
+
const COMPACT_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
9365
|
+
"navigate",
|
|
9366
|
+
"go_back",
|
|
9367
|
+
"click",
|
|
9368
|
+
"type_text",
|
|
9369
|
+
"press_key",
|
|
9370
|
+
"scroll",
|
|
9371
|
+
"dismiss_popup",
|
|
9372
|
+
"clear_overlays",
|
|
9373
|
+
"accept_cookies",
|
|
9374
|
+
"read_page",
|
|
9375
|
+
"wait_for",
|
|
9376
|
+
"inspect_element",
|
|
9377
|
+
"search"
|
|
9378
|
+
]);
|
|
9379
|
+
const COMPACT_CONTEXTUAL_TOOL_NAMES = {
|
|
9380
|
+
LOGIN: ["fill_form", "submit_form", "login"],
|
|
9381
|
+
FORM: ["fill_form", "select_option", "submit_form"],
|
|
9382
|
+
SHOPPING: ["select_option", "fill_form", "submit_form"],
|
|
9383
|
+
SEARCH_RESULTS: ["paginate", "scroll_to_element"],
|
|
9384
|
+
PAGINATED_LIST: ["paginate", "scroll_to_element"]
|
|
9385
|
+
};
|
|
9386
|
+
const COMPACT_INTENT_TOOL_NAMES = {
|
|
9387
|
+
tabs: ["current_tab", "list_tabs", "switch_tab", "create_tab"],
|
|
9388
|
+
bookmarks: [
|
|
9389
|
+
"list_bookmarks",
|
|
9390
|
+
"search_bookmarks",
|
|
9391
|
+
"create_bookmark_folder",
|
|
9392
|
+
"save_bookmark",
|
|
9393
|
+
"organize_bookmark",
|
|
9394
|
+
"archive_bookmark",
|
|
9395
|
+
"open_bookmark"
|
|
9396
|
+
],
|
|
9397
|
+
sessions: ["login", "save_session", "load_session", "list_sessions", "delete_session"],
|
|
9398
|
+
workflow: ["create_checkpoint", "restore_checkpoint", "flow_start", "flow_advance", "flow_status", "flow_end"],
|
|
9399
|
+
metrics: ["metrics"],
|
|
9400
|
+
highlight: ["highlight", "clear_highlights"],
|
|
9401
|
+
table: ["extract_table"],
|
|
9402
|
+
debug: ["current_tab", "reload", "set_ad_blocking", "suggest", "screenshot"]
|
|
9403
|
+
};
|
|
7996
9404
|
function inferIntent(query) {
|
|
7997
9405
|
const lowered = query.toLowerCase();
|
|
7998
9406
|
const intents = /* @__PURE__ */ new Set();
|
|
7999
9407
|
if (/\b(tab|tabs|window|windows)\b/.test(lowered)) intents.add("tabs");
|
|
9408
|
+
if (/\b(go to|goto|open|visit|navigate to)\b/.test(lowered) || /\b[a-z0-9-]+\.(com|org|net|io|dev|app|ai|co|edu|gov)\b/.test(lowered) || /\bhttps?:\/\//.test(lowered)) {
|
|
9409
|
+
intents.add("navigate");
|
|
9410
|
+
}
|
|
8000
9411
|
if (/\b(bookmark|bookmarks|save this|folder)\b/.test(lowered)) {
|
|
8001
9412
|
intents.add("bookmarks");
|
|
8002
9413
|
}
|
|
@@ -8019,7 +9430,18 @@ function inferIntent(query) {
|
|
|
8019
9430
|
}
|
|
8020
9431
|
return intents;
|
|
8021
9432
|
}
|
|
8022
|
-
function shouldIncludeTool(toolName, pageType, intents) {
|
|
9433
|
+
function shouldIncludeTool(toolName, pageType, intents, profile) {
|
|
9434
|
+
if (profile === "compact") {
|
|
9435
|
+
if (COMPACT_CORE_TOOL_NAMES.has(toolName)) return true;
|
|
9436
|
+
const contextualTools = COMPACT_CONTEXTUAL_TOOL_NAMES[pageType] ?? [];
|
|
9437
|
+
if (contextualTools.includes(toolName)) return true;
|
|
9438
|
+
for (const intent of intents) {
|
|
9439
|
+
if ((COMPACT_INTENT_TOOL_NAMES[intent] ?? []).includes(toolName)) {
|
|
9440
|
+
return true;
|
|
9441
|
+
}
|
|
9442
|
+
}
|
|
9443
|
+
return false;
|
|
9444
|
+
}
|
|
8023
9445
|
if (ALWAYS_FAST_TOOL_NAMES.has(toolName)) return true;
|
|
8024
9446
|
switch (toolName) {
|
|
8025
9447
|
case "select_option":
|
|
@@ -8076,13 +9498,14 @@ function shouldIncludeTool(toolName, pageType, intents) {
|
|
|
8076
9498
|
return !defByName[toolName]?.hiddenByDefault;
|
|
8077
9499
|
}
|
|
8078
9500
|
}
|
|
8079
|
-
function pruneToolsForContext(tools, pageType, query = "") {
|
|
9501
|
+
function pruneToolsForContext(tools, pageType, query = "", options = {}) {
|
|
8080
9502
|
const ctx = pageType ?? "GENERAL";
|
|
8081
9503
|
const hints = CONTEXT_HINTS[ctx] ?? {};
|
|
8082
9504
|
const intents = inferIntent(query);
|
|
8083
|
-
const
|
|
9505
|
+
const profile = options.profile ?? "default";
|
|
9506
|
+
const scored = tools.filter((tool) => shouldIncludeTool(tool.name, ctx, intents, profile)).map((tool) => ({
|
|
8084
9507
|
tool,
|
|
8085
|
-
score: scoreForContext(tool.name, ctx)
|
|
9508
|
+
score: scoreForContext(tool.name, ctx, intents)
|
|
8086
9509
|
}));
|
|
8087
9510
|
scored.sort((a, b) => a.score - b.score);
|
|
8088
9511
|
return scored.map(({ tool, score }) => {
|
|
@@ -8746,6 +10169,87 @@ async function captureScreenshot(wc) {
|
|
|
8746
10169
|
}
|
|
8747
10170
|
return { ok: false, error: "Page image was empty after 3 attempts" };
|
|
8748
10171
|
}
|
|
10172
|
+
function normalizeForComparison(value) {
|
|
10173
|
+
return value.toLowerCase().replace(/https?:\/\//g, "").replace(/www\./g, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
10174
|
+
}
|
|
10175
|
+
function canonicalizeUrlForComparison(value) {
|
|
10176
|
+
try {
|
|
10177
|
+
const url = new URL(value);
|
|
10178
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
10179
|
+
url.hostname = url.hostname.replace(/^www\./, "");
|
|
10180
|
+
url.hash = "";
|
|
10181
|
+
if (url.pathname.endsWith("/") && url.pathname !== "/") {
|
|
10182
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
10183
|
+
}
|
|
10184
|
+
return url.toString();
|
|
10185
|
+
} catch {
|
|
10186
|
+
return null;
|
|
10187
|
+
}
|
|
10188
|
+
}
|
|
10189
|
+
function isRedundantNavigateTarget(currentUrl, targetUrl) {
|
|
10190
|
+
const current = canonicalizeUrlForComparison(currentUrl);
|
|
10191
|
+
const target = canonicalizeUrlForComparison(targetUrl);
|
|
10192
|
+
return current !== null && target !== null && current === target;
|
|
10193
|
+
}
|
|
10194
|
+
function looksLikeCurrentSiteNameQuery(query, currentUrl, currentTitle) {
|
|
10195
|
+
const normalizedQuery = normalizeForComparison(query);
|
|
10196
|
+
if (!normalizedQuery) return false;
|
|
10197
|
+
let hostnameLabel = "";
|
|
10198
|
+
try {
|
|
10199
|
+
const url = new URL(currentUrl);
|
|
10200
|
+
hostnameLabel = url.hostname.replace(/^www\./, "").split(".")[0] || "";
|
|
10201
|
+
} catch {
|
|
10202
|
+
}
|
|
10203
|
+
const normalizedTitle = normalizeForComparison(currentTitle);
|
|
10204
|
+
const normalizedHost = normalizeForComparison(hostnameLabel);
|
|
10205
|
+
const normalizedTitlePrefix = normalizeForComparison(
|
|
10206
|
+
currentTitle.split("|")[0]?.split("—")[0]?.split("-")[0] || currentTitle
|
|
10207
|
+
);
|
|
10208
|
+
if (normalizedTitle && normalizedQuery === normalizedTitle) return true;
|
|
10209
|
+
if (normalizedTitlePrefix && normalizedQuery === normalizedTitlePrefix) {
|
|
10210
|
+
return true;
|
|
10211
|
+
}
|
|
10212
|
+
if (normalizedHost && normalizedQuery === normalizedHost) return true;
|
|
10213
|
+
const titleTokens = new Set(normalizedTitle.split(/\s+/).filter(Boolean));
|
|
10214
|
+
const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
|
|
10215
|
+
if (normalizedHost && queryTokens.includes(normalizedHost) && queryTokens.every((token) => titleTokens.has(token) || token === normalizedHost)) {
|
|
10216
|
+
return true;
|
|
10217
|
+
}
|
|
10218
|
+
return false;
|
|
10219
|
+
}
|
|
10220
|
+
function extractExplicitDomains(goal) {
|
|
10221
|
+
const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
|
|
10222
|
+
if (!matches) return [];
|
|
10223
|
+
const normalized = matches.map(
|
|
10224
|
+
(match) => match.replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase()
|
|
10225
|
+
);
|
|
10226
|
+
return [...new Set(normalized)];
|
|
10227
|
+
}
|
|
10228
|
+
function apexDomain(hostname) {
|
|
10229
|
+
const parts = hostname.replace(/^www\./, "").split(".").filter(Boolean);
|
|
10230
|
+
if (parts.length <= 2) return parts.join(".");
|
|
10231
|
+
return parts.slice(-2).join(".");
|
|
10232
|
+
}
|
|
10233
|
+
function shouldBlockOffGoalDomainNavigation(goal, targetUrl) {
|
|
10234
|
+
const explicitDomains = extractExplicitDomains(goal);
|
|
10235
|
+
if (explicitDomains.length !== 1) return null;
|
|
10236
|
+
let targetHost = "";
|
|
10237
|
+
try {
|
|
10238
|
+
const url = new URL(targetUrl);
|
|
10239
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
10240
|
+
targetHost = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
10241
|
+
} catch {
|
|
10242
|
+
return null;
|
|
10243
|
+
}
|
|
10244
|
+
const requestedDomain = explicitDomains[0];
|
|
10245
|
+
if (targetHost === requestedDomain || targetHost.endsWith(`.${requestedDomain}`) || apexDomain(targetHost) === apexDomain(requestedDomain)) {
|
|
10246
|
+
return null;
|
|
10247
|
+
}
|
|
10248
|
+
return {
|
|
10249
|
+
requestedDomain,
|
|
10250
|
+
targetDomain: targetHost
|
|
10251
|
+
};
|
|
10252
|
+
}
|
|
8749
10253
|
const SESSION_VERSION = 1;
|
|
8750
10254
|
function getSessionsDir() {
|
|
8751
10255
|
return path$1.join(electron.app.getPath("userData"), "named-sessions");
|
|
@@ -9024,6 +10528,340 @@ function deleteNamedSession(name) {
|
|
|
9024
10528
|
fs$1.unlinkSync(filePath);
|
|
9025
10529
|
return true;
|
|
9026
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
|
+
}
|
|
9027
10865
|
const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS = 1500;
|
|
9028
10866
|
const QUIET_NAVIGATION_WINDOW_MS = 1200;
|
|
9029
10867
|
const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
|
|
@@ -9358,6 +11196,47 @@ WARNING: Blocking overlay detected (${overlaySignal}). Call clear_overlays or ac
|
|
|
9358
11196
|
}
|
|
9359
11197
|
return titleLine;
|
|
9360
11198
|
}
|
|
11199
|
+
async function getPostSearchSummary(wc) {
|
|
11200
|
+
await waitForLoad$1(wc, 2e3);
|
|
11201
|
+
try {
|
|
11202
|
+
const content = await Promise.race([
|
|
11203
|
+
extractContent(wc),
|
|
11204
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 2500))
|
|
11205
|
+
]);
|
|
11206
|
+
if (content && content.content.length > 0) {
|
|
11207
|
+
const scoped = buildScopedContext(content, "results_only");
|
|
11208
|
+
const truncated = scoped.length > 2600 ? `${scoped.slice(0, 2600)}
|
|
11209
|
+
[Search results snapshot truncated...]` : scoped;
|
|
11210
|
+
return `
|
|
11211
|
+
Search results snapshot:
|
|
11212
|
+
${truncated}`;
|
|
11213
|
+
}
|
|
11214
|
+
} catch {
|
|
11215
|
+
}
|
|
11216
|
+
const fallback = await getPostNavSummary(wc);
|
|
11217
|
+
return fallback ? `${fallback}
|
|
11218
|
+
Search results snapshot unavailable. Use read_page(mode="results_only") if needed.` : `
|
|
11219
|
+
Search results snapshot unavailable. Use read_page(mode="results_only") if needed.`;
|
|
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
|
+
}
|
|
9361
11240
|
async function scrollPage$1(wc, deltaY) {
|
|
9362
11241
|
const getScrollY = async () => {
|
|
9363
11242
|
const scrollY = await executePageScript(
|
|
@@ -9550,9 +11429,17 @@ async function describeElementForClick$1(wc, selector) {
|
|
|
9550
11429
|
if (!el) return { error: "Element not found" };
|
|
9551
11430
|
const anchor = el instanceof HTMLAnchorElement ? el : el.closest("a[href]");
|
|
9552
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;
|
|
9553
11437
|
return {
|
|
9554
11438
|
text: text || "Element",
|
|
9555
11439
|
href: anchor instanceof HTMLAnchorElement ? anchor.href : undefined,
|
|
11440
|
+
target: anchor instanceof HTMLAnchorElement ? (anchor.getAttribute("target") || "") : undefined,
|
|
11441
|
+
tag,
|
|
11442
|
+
isInteractive,
|
|
9556
11443
|
};
|
|
9557
11444
|
})()
|
|
9558
11445
|
`,
|
|
@@ -9571,7 +11458,10 @@ async function describeElementForClick$1(wc, selector) {
|
|
|
9571
11458
|
}
|
|
9572
11459
|
return {
|
|
9573
11460
|
text: "text" in result && typeof result.text === "string" ? result.text : "Element",
|
|
9574
|
-
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
|
|
9575
11465
|
};
|
|
9576
11466
|
}
|
|
9577
11467
|
async function inspectElement(wc, selector, limit = 8) {
|
|
@@ -9655,6 +11545,18 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
9655
11545
|
) || "element";
|
|
9656
11546
|
}
|
|
9657
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
|
+
|
|
9658
11560
|
function chooseRegion(target) {
|
|
9659
11561
|
const preferred = target.closest(
|
|
9660
11562
|
"[data-testid], article, [role='article'], [role='listitem'], li, tr, form, section, aside, dialog, [role='dialog']"
|
|
@@ -9680,19 +11582,69 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
9680
11582
|
const region = chooseRegion(target);
|
|
9681
11583
|
const nearby = [];
|
|
9682
11584
|
const seen = new Set();
|
|
11585
|
+
const purchaseActions = [];
|
|
11586
|
+
const purchaseSeen = new Set();
|
|
9683
11587
|
region.querySelectorAll("a[href], button, input:not([type='hidden']), select, textarea").forEach((el) => {
|
|
9684
11588
|
if (!(el instanceof HTMLElement) || !isVisible(el)) return;
|
|
9685
11589
|
const candidateSelector = selectorFor(el);
|
|
9686
11590
|
if (!candidateSelector || seen.has(candidateSelector)) return;
|
|
9687
11591
|
seen.add(candidateSelector);
|
|
11592
|
+
const candidateLabel = labelFor(el).slice(0, 100);
|
|
11593
|
+
const candidateHref = el instanceof HTMLAnchorElement ? text(el.href) : undefined;
|
|
9688
11594
|
nearby.push({
|
|
9689
|
-
|
|
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,
|
|
9690
11634
|
type: el.tagName.toLowerCase(),
|
|
9691
11635
|
selector: candidateSelector,
|
|
9692
|
-
href:
|
|
11636
|
+
href: candidateHref,
|
|
11637
|
+
source: "page",
|
|
11638
|
+
rank: purchaseRank,
|
|
9693
11639
|
});
|
|
9694
11640
|
});
|
|
9695
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
|
+
|
|
9696
11648
|
return {
|
|
9697
11649
|
target: {
|
|
9698
11650
|
label: labelFor(target).slice(0, 120),
|
|
@@ -9709,6 +11661,14 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
9709
11661
|
text: text(region.textContent)?.slice(0, 400),
|
|
9710
11662
|
},
|
|
9711
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
|
+
})),
|
|
9712
11672
|
};
|
|
9713
11673
|
})()
|
|
9714
11674
|
`,
|
|
@@ -9741,10 +11701,25 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
9741
11701
|
lines.push("Nearby controls:");
|
|
9742
11702
|
for (const item of result.nearby) {
|
|
9743
11703
|
const hrefSuffix = item.href ? ` -> ${item.href}` : "";
|
|
11704
|
+
const indexPrefix = typeof item.index === "number" ? `[#${item.index}] ` : "";
|
|
11705
|
+
lines.push(
|
|
11706
|
+
`- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
|
|
11707
|
+
);
|
|
11708
|
+
}
|
|
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}] ` : "";
|
|
9744
11716
|
lines.push(
|
|
9745
|
-
`- ${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
|
|
11717
|
+
`- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}${sourceSuffix}`
|
|
9746
11718
|
);
|
|
9747
11719
|
}
|
|
11720
|
+
lines.push(
|
|
11721
|
+
"When an index is available, prefer click(index=N) over selector-based clicks because it is more stable."
|
|
11722
|
+
);
|
|
9748
11723
|
}
|
|
9749
11724
|
return lines.join("\n");
|
|
9750
11725
|
}
|
|
@@ -9833,6 +11808,11 @@ const ADD_TO_CART_PATTERNS = [
|
|
|
9833
11808
|
];
|
|
9834
11809
|
const recentCartClicks = /* @__PURE__ */ new Map();
|
|
9835
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;
|
|
9836
11816
|
function isAddToCartText(text) {
|
|
9837
11817
|
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
9838
11818
|
return ADD_TO_CART_PATTERNS.some((p) => normalized.includes(p));
|
|
@@ -9852,20 +11832,115 @@ function isDuplicateCartClick(url, text) {
|
|
|
9852
11832
|
recentCartClicks.delete(url);
|
|
9853
11833
|
return false;
|
|
9854
11834
|
}
|
|
9855
|
-
return isAddToCartText(text);
|
|
11835
|
+
return isAddToCartText(text);
|
|
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}`;
|
|
9856
11925
|
}
|
|
9857
11926
|
async function clickResolvedSelector$1(wc, selector) {
|
|
9858
11927
|
if (selector.startsWith("__vessel_idx:")) {
|
|
9859
11928
|
const idx = Number(selector.slice("__vessel_idx:".length));
|
|
9860
11929
|
const beforeUrl2 = wc.getURL();
|
|
11930
|
+
let idxCartMatch = false;
|
|
9861
11931
|
const idxLabel = await executePageScript(
|
|
9862
11932
|
wc,
|
|
9863
11933
|
`window.__vessel?.getElementText?.(${idx}) || ""`,
|
|
9864
11934
|
{ label: "shadow element text" }
|
|
9865
11935
|
);
|
|
9866
|
-
if (typeof idxLabel === "string" && isAddToCartText(idxLabel) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
|
|
11936
|
+
if (typeof idxLabel === "string" && (idxCartMatch = isAddToCartText(idxLabel)) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
|
|
9867
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).`;
|
|
9868
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
|
+
}
|
|
9869
11944
|
const result = await executePageScript(
|
|
9870
11945
|
wc,
|
|
9871
11946
|
`window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`,
|
|
@@ -9875,18 +11950,40 @@ async function clickResolvedSelector$1(wc, selector) {
|
|
|
9875
11950
|
);
|
|
9876
11951
|
if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
|
|
9877
11952
|
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
9878
|
-
if (
|
|
11953
|
+
if (idxCartMatch) {
|
|
9879
11954
|
recordCartClick(beforeUrl2, idxLabel);
|
|
9880
11955
|
}
|
|
9881
11956
|
await waitForPotentialNavigation$1(wc, beforeUrl2);
|
|
9882
11957
|
const afterUrl2 = wc.getURL();
|
|
9883
11958
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
9884
|
-
|
|
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
|
+
}
|
|
9885
11980
|
return idxOverlay ? `${result}
|
|
9886
|
-
${idxOverlay}` : result
|
|
11981
|
+
${idxOverlay}` : `${result}
|
|
11982
|
+
Note: Page did not change after click.`;
|
|
9887
11983
|
}
|
|
9888
11984
|
if (selector.includes(" >>> ")) {
|
|
9889
11985
|
const beforeUrl2 = wc.getURL();
|
|
11986
|
+
let shadowCartMatch = false;
|
|
9890
11987
|
const shadowLabel = await executePageScript(
|
|
9891
11988
|
wc,
|
|
9892
11989
|
`(function() {
|
|
@@ -9895,17 +11992,24 @@ ${idxOverlay}` : result;
|
|
|
9895
11992
|
})()`,
|
|
9896
11993
|
{ label: "shadow element text" }
|
|
9897
11994
|
);
|
|
9898
|
-
if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
|
|
11995
|
+
if (typeof shadowLabel === "string" && (shadowCartMatch = isAddToCartText(shadowLabel)) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
|
|
9899
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).`;
|
|
9900
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
|
+
}
|
|
9901
12003
|
const result = await executePageScript(
|
|
9902
12004
|
wc,
|
|
9903
12005
|
`
|
|
9904
12006
|
(function() {
|
|
9905
12007
|
var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
|
|
9906
|
-
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.";
|
|
9907
12009
|
if (el instanceof HTMLElement) { el.focus(); el.click(); }
|
|
9908
|
-
|
|
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 : "");
|
|
9909
12013
|
})()
|
|
9910
12014
|
`,
|
|
9911
12015
|
{
|
|
@@ -9914,15 +12018,36 @@ ${idxOverlay}` : result;
|
|
|
9914
12018
|
);
|
|
9915
12019
|
if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
|
|
9916
12020
|
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
9917
|
-
if (
|
|
12021
|
+
if (shadowCartMatch) {
|
|
9918
12022
|
recordCartClick(beforeUrl2, shadowLabel);
|
|
9919
12023
|
}
|
|
9920
12024
|
await waitForPotentialNavigation$1(wc, beforeUrl2);
|
|
9921
12025
|
const afterUrl2 = wc.getURL();
|
|
9922
12026
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
9923
|
-
|
|
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
|
+
}
|
|
9924
12048
|
return shadowOverlay ? `${result}
|
|
9925
|
-
${shadowOverlay}` : result
|
|
12049
|
+
${shadowOverlay}` : `${result}
|
|
12050
|
+
Note: Page did not change after click.`;
|
|
9926
12051
|
}
|
|
9927
12052
|
const beforeUrl = wc.getURL();
|
|
9928
12053
|
const elInfo = await describeElementForClick$1(wc, selector);
|
|
@@ -9945,10 +12070,16 @@ Click one of these dialog actions instead.`;
|
|
|
9945
12070
|
return formatDeadLinkMessage(elInfo.text, validation);
|
|
9946
12071
|
}
|
|
9947
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
|
+
}
|
|
9948
12078
|
if (cartMatch) {
|
|
9949
12079
|
recordCartClick(beforeUrl, elInfo.text);
|
|
9950
12080
|
}
|
|
9951
|
-
const
|
|
12081
|
+
const tagLabel = elInfo.tag && elInfo.tag !== "a" && elInfo.tag !== "button" ? ` <${elInfo.tag}>` : "";
|
|
12082
|
+
const clickText = `Clicked: ${elInfo.text}${tagLabel}`;
|
|
9952
12083
|
const clickResult = await clickElement$1(wc, selector);
|
|
9953
12084
|
if (clickResult.startsWith("Error:")) return clickResult;
|
|
9954
12085
|
await waitForPotentialNavigation$1(wc, beforeUrl);
|
|
@@ -9958,15 +12089,30 @@ Click one of these dialog actions instead.`;
|
|
|
9958
12089
|
}
|
|
9959
12090
|
const overlayHint = await detectPostClickOverlay$1(wc);
|
|
9960
12091
|
if (overlayHint) {
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
12092
|
+
if (cartMatch) {
|
|
12093
|
+
return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
|
|
12094
|
+
wc,
|
|
12095
|
+
beforeUrl,
|
|
12096
|
+
overlayHint
|
|
12097
|
+
)}`;
|
|
12098
|
+
}
|
|
9965
12099
|
return `${clickText} (${clickResult})
|
|
9966
|
-
${overlayHint}
|
|
12100
|
+
${overlayHint}`;
|
|
9967
12101
|
}
|
|
9968
12102
|
if (cartMatch) {
|
|
9969
|
-
|
|
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
|
+
)}`;
|
|
9970
12116
|
}
|
|
9971
12117
|
const activationResult = await activateElement$1(wc, selector);
|
|
9972
12118
|
if (!activationResult.startsWith("Error:")) {
|
|
@@ -9981,7 +12127,65 @@ ${overlayHint}${actionsSuffix}`;
|
|
|
9981
12127
|
return `${clickText} (${clickResult})
|
|
9982
12128
|
${postActivationOverlayHint}`;
|
|
9983
12129
|
}
|
|
9984
|
-
|
|
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;
|
|
9985
12189
|
}
|
|
9986
12190
|
async function getCartDialogActions$1(wc) {
|
|
9987
12191
|
const result = await executePageScript(
|
|
@@ -10787,6 +12991,24 @@ async function resolveSelector$1(wc, index, selector) {
|
|
|
10787
12991
|
if (extractedSelector) return extractedSelector;
|
|
10788
12992
|
return null;
|
|
10789
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
|
+
}
|
|
10790
13012
|
function normalizeFieldToken(value) {
|
|
10791
13013
|
return typeof value === "string" ? value.trim() : "";
|
|
10792
13014
|
}
|
|
@@ -12094,6 +14316,9 @@ async function searchPage(wc, args) {
|
|
|
12094
14316
|
if (buttonLikePatterns.some((p) => queryLower.includes(p))) {
|
|
12095
14317
|
return `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`;
|
|
12096
14318
|
}
|
|
14319
|
+
if (looksLikeCurrentSiteNameQuery(query, wc.getURL(), wc.getTitle() || "")) {
|
|
14320
|
+
return `Error: "${query}" looks like the current site's name, not a product query. You are already on ${wc.getURL()}. Open a section like staff picks/new releases or search for actual book titles, authors, or genres instead.`;
|
|
14321
|
+
}
|
|
12097
14322
|
if (typeof args.selector !== "string") {
|
|
12098
14323
|
const shortcut = buildSearchShortcut(wc.getURL(), query);
|
|
12099
14324
|
if (shortcut) {
|
|
@@ -12104,7 +14329,7 @@ async function searchPage(wc, args) {
|
|
|
12104
14329
|
const afterUrl2 = wc.getURL();
|
|
12105
14330
|
const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
|
|
12106
14331
|
const destination = shortcut.section ? ` ${shortcut.section}` : "";
|
|
12107
|
-
return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}`;
|
|
14332
|
+
return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}${await getPostSearchSummary(wc)}`;
|
|
12108
14333
|
}
|
|
12109
14334
|
}
|
|
12110
14335
|
const searchInfo = await locateSearchTarget(
|
|
@@ -12133,7 +14358,7 @@ async function searchPage(wc, args) {
|
|
|
12133
14358
|
await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
|
|
12134
14359
|
let afterUrl = wc.getURL();
|
|
12135
14360
|
if (afterUrl !== beforeUrl) {
|
|
12136
|
-
return `Searched "${query}" → ${afterUrl}`;
|
|
14361
|
+
return `Searched "${query}" → ${afterUrl}${await getPostSearchSummary(wc)}`;
|
|
12137
14362
|
}
|
|
12138
14363
|
if (searchInfo.submitSelector) {
|
|
12139
14364
|
const clickResult = await clickElementBySelector(wc, searchInfo.submitSelector);
|
|
@@ -12141,11 +14366,11 @@ async function searchPage(wc, args) {
|
|
|
12141
14366
|
await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
|
|
12142
14367
|
afterUrl = wc.getURL();
|
|
12143
14368
|
if (afterUrl !== beforeUrl) {
|
|
12144
|
-
return `Searched "${query}" (via search button) → ${afterUrl}`;
|
|
14369
|
+
return `Searched "${query}" (via search button) → ${afterUrl}${await getPostSearchSummary(wc)}`;
|
|
12145
14370
|
}
|
|
12146
14371
|
}
|
|
12147
14372
|
}
|
|
12148
|
-
return `Searched "${query}" (same page — results may have loaded dynamically
|
|
14373
|
+
return `Searched "${query}" (same page — results may have loaded dynamically)${await getPostSearchSummary(wc)}`;
|
|
12149
14374
|
}
|
|
12150
14375
|
async function pressKey$1(wc, args) {
|
|
12151
14376
|
const key = typeof args.key === "string" ? args.key.trim() : "";
|
|
@@ -12234,8 +14459,49 @@ async function getPostActionState$1(ctx, name) {
|
|
|
12234
14459
|
if (wc.isLoading()) {
|
|
12235
14460
|
await waitForLoad$1(wc);
|
|
12236
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
|
+
}
|
|
12237
14484
|
return `
|
|
12238
|
-
[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
|
+
}
|
|
12239
14505
|
}
|
|
12240
14506
|
if (interactActions.includes(name)) {
|
|
12241
14507
|
return `
|
|
@@ -12307,6 +14573,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
|
12307
14573
|
"wait_for_navigation"
|
|
12308
14574
|
]);
|
|
12309
14575
|
async function executeAction(name, args, ctx) {
|
|
14576
|
+
name = normalizeToolAlias(name);
|
|
12310
14577
|
if (!KNOWN_TOOLS.has(name)) {
|
|
12311
14578
|
for (const known of KNOWN_TOOLS) {
|
|
12312
14579
|
if (name.startsWith(known) && name.length > known.length) {
|
|
@@ -12423,6 +14690,19 @@ async function executeAction(name, args, ctx) {
|
|
|
12423
14690
|
}
|
|
12424
14691
|
case "navigate": {
|
|
12425
14692
|
if (!wc || !tabId) return "Error: No active tab";
|
|
14693
|
+
const taskGoal = ctx.runtime.getState().taskTracker?.goal;
|
|
14694
|
+
if (taskGoal && typeof args.url === "string") {
|
|
14695
|
+
const domainDrift = shouldBlockOffGoalDomainNavigation(
|
|
14696
|
+
taskGoal,
|
|
14697
|
+
args.url
|
|
14698
|
+
);
|
|
14699
|
+
if (domainDrift) {
|
|
14700
|
+
return `Navigation blocked: ${args.url} drifts away from the requested site ${domainDrift.requestedDomain}. Stay on the requested domain and continue the original task there.`;
|
|
14701
|
+
}
|
|
14702
|
+
}
|
|
14703
|
+
if (typeof args.url === "string" && !args.postBody && isRedundantNavigateTarget(wc.getURL(), args.url)) {
|
|
14704
|
+
return `Already on ${wc.getURL()}. Do not navigate to the same URL again. Use click, inspect_element, read_page, or search for actual book terms instead.`;
|
|
14705
|
+
}
|
|
12426
14706
|
const navValidation = await validateLinkDestination(args.url);
|
|
12427
14707
|
if (navValidation.status === "dead") {
|
|
12428
14708
|
return `Navigation blocked: ${args.url} returned ${navValidation.detail || "dead link"}. Try a different URL or go back and choose another link.`;
|
|
@@ -12462,14 +14742,48 @@ async function executeAction(name, args, ctx) {
|
|
|
12462
14742
|
}
|
|
12463
14743
|
case "click": {
|
|
12464
14744
|
if (!wc) return "Error: No active tab";
|
|
12465
|
-
|
|
12466
|
-
|
|
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
|
+
}
|
|
12467
14767
|
return clickResolvedSelector$1(wc, selector);
|
|
12468
14768
|
}
|
|
12469
14769
|
case "inspect_element": {
|
|
12470
14770
|
if (!wc) return "Error: No active tab";
|
|
12471
|
-
|
|
12472
|
-
|
|
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
|
+
}
|
|
12473
14787
|
return inspectElement(
|
|
12474
14788
|
wc,
|
|
12475
14789
|
selector,
|
|
@@ -12618,7 +14932,8 @@ async function executeAction(name, args, ctx) {
|
|
|
12618
14932
|
const livePrefix = liveSelectionSection ? `${liveSelectionSection}
|
|
12619
14933
|
|
|
12620
14934
|
` : "";
|
|
12621
|
-
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;
|
|
12622
14937
|
if (requestedMode === "debug" || requestedMode === "full") {
|
|
12623
14938
|
const structured = buildStructuredContext(content);
|
|
12624
14939
|
const truncated = content.content.length > 2e4 ? content.content.slice(0, 2e4) + "\n[Content truncated...]" : content.content;
|
|
@@ -12630,7 +14945,7 @@ ${structured}
|
|
|
12630
14945
|
|
|
12631
14946
|
${truncated}`;
|
|
12632
14947
|
}
|
|
12633
|
-
const scoped = buildScopedContext(content, requestedMode);
|
|
14948
|
+
const scoped = ctx.toolProfile === "compact" ? buildCompactScopedContext(content, requestedMode) : buildScopedContext(content, requestedMode);
|
|
12634
14949
|
return [
|
|
12635
14950
|
livePrefix ? livePrefix.trimEnd() : "",
|
|
12636
14951
|
`[read_page mode=${requestedMode}]`,
|
|
@@ -13307,9 +15622,20 @@ ${JSON.stringify(tableJson, null, 2)}`;
|
|
|
13307
15622
|
}
|
|
13308
15623
|
case "scroll_to_element": {
|
|
13309
15624
|
if (!wc) return "Error: No active tab";
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
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
|
+
}
|
|
13313
15639
|
const block = args.position === "top" ? "start" : args.position === "bottom" ? "end" : "center";
|
|
13314
15640
|
if (sel.startsWith("__vessel_idx:")) {
|
|
13315
15641
|
const idx = Number(sel.slice("__vessel_idx:".length));
|
|
@@ -13347,8 +15673,36 @@ ${JSON.stringify(tableJson, null, 2)}`;
|
|
|
13347
15673
|
}
|
|
13348
15674
|
}
|
|
13349
15675
|
});
|
|
15676
|
+
const formattedResult = ctx.toolProfile === "compact" ? formatCompactToolResult(name, result) : result;
|
|
13350
15677
|
const flowCtx = ctx.runtime.getFlowContext();
|
|
13351
|
-
|
|
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;
|
|
13352
15706
|
}
|
|
13353
15707
|
async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history) {
|
|
13354
15708
|
const lowerQuery = query.toLowerCase().trim();
|
|
@@ -13358,81 +15712,49 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
|
|
|
13358
15712
|
const pageContent = await extractContent(activeWebContents);
|
|
13359
15713
|
const pageType = detectPageType(pageContent);
|
|
13360
15714
|
const defaultReadMode = chooseAgentReadMode(pageContent);
|
|
13361
|
-
|
|
15715
|
+
if (provider.agentToolProfile === "compact") {
|
|
15716
|
+
runtime2.ensureTaskTracker(query, pageContent.url || activeWebContents.getURL());
|
|
15717
|
+
} else {
|
|
15718
|
+
runtime2.clearTaskTracker();
|
|
15719
|
+
}
|
|
15720
|
+
const structuredContext = provider.agentToolProfile === "compact" ? buildCompactScopedContext(
|
|
13362
15721
|
pageContent,
|
|
13363
|
-
defaultReadMode
|
|
13364
|
-
|
|
15722
|
+
defaultReadMode,
|
|
15723
|
+
pageType
|
|
15724
|
+
) : buildScopedContext(pageContent, defaultReadMode);
|
|
13365
15725
|
const runtimeState = runtime2.getState();
|
|
13366
15726
|
const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
|
|
15727
|
+
const taskTrackerContext = runtime2.getTaskTrackerContext();
|
|
13367
15728
|
const activeTabTitle = pageContent.title || "(untitled)";
|
|
13368
15729
|
const activeTabUrl = pageContent.url || activeWebContents.getURL();
|
|
13369
15730
|
const allTabs = tabManager.getAllStates();
|
|
13370
15731
|
const activeTabId = tabManager.getActiveTabId();
|
|
13371
15732
|
const tabSummary = allTabs.length > 1 ? `
|
|
13372
15733
|
All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.title || "New Tab"} (${t.url})`).join(" | ")}` : "";
|
|
13373
|
-
const systemPrompt =
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
Recent checkpoints:
|
|
13394
|
-
${recentCheckpoints || "- none"}
|
|
13395
|
-
|
|
13396
|
-
Instructions:
|
|
13397
|
-
- You can see the page the user is viewing. The content above is from the page.
|
|
13398
|
-
- The structured page context always refers to the tab currently visible to the human unless a later tool call changes tabs.
|
|
13399
|
-
- Use tools to interact with the page when asked to do something (navigate, click, type, select options, submit forms, press keys, scroll).
|
|
13400
|
-
- Only say you completed an action after the corresponding tool succeeds. If no tool supports the request, say so plainly.
|
|
13401
|
-
- Use current_tab when you only need to know what the human is currently looking at. Use list_tabs before switching context across multiple tabs.
|
|
13402
|
-
- Create a checkpoint before risky multi-step flows or before leaving an important state.
|
|
13403
|
-
- Use save_session after completing a login flow you may need again later, and load_session to resume that authenticated state in future runs.
|
|
13404
|
-
- Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.
|
|
13405
|
-
- After navigating to a new site, DO NOT call read_page immediately. Instead, act on what you already know: use the search tool to search the site, type_text to enter queries in search bars, or click on known navigation patterns. You know what major sites look like — use that knowledge. Only call read_page if you're genuinely stuck and need to discover unfamiliar page structure.
|
|
13406
|
-
- On retail and marketplace sites (like Newegg, Amazon, Walmart, Etsy, eBay), prefer the site's visible search box, filters, and result pages over direct product URLs. Only navigate directly to a product page if the user gave you that URL or the site's own search UI is clearly unavailable after a reasonable attempt.
|
|
13407
|
-
- The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
|
|
13408
|
-
- When you only need detail on one product/result/card/form section, use inspect_element instead of reading the page.
|
|
13409
|
-
- Escalate page reads progressively: read_page(mode="glance") for a fast viewport snapshot on heavy/slow pages, then read_page(mode="visible_only"), read_page(mode="results_only"), read_page(mode="forms_only"), read_page(mode="summary"), or read_page(mode="text_only") depending on what you need.
|
|
13410
|
-
- Use read_page(mode="glance") when a page is slow to load or extraction times out — it shows what's on screen (headings, links, buttons, inputs) without waiting for heavy JS. It's what a human would see by just looking at the page.
|
|
13411
|
-
- Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
|
|
13412
|
-
- If read_page returns empty or times out, do NOT retry with the same mode. Switch to read_page(mode="glance") or use screenshot to see the page visually.
|
|
13413
|
-
- Use screenshot when you need to see exactly what the user sees — visual layout, rendered content, images, or when text extraction is failing. The screenshot returns the actual rendered page image for visual analysis. It works even when the JS thread is completely blocked.
|
|
13414
|
-
- VIEWPORT SYNC: Treat scrolling as a real, user-visible browser action. If you say you are going to scroll, call scroll or scroll_to_element so the human sees the page move too.
|
|
13415
|
-
- read_page inspects the page without moving the human-visible viewport. Do not describe read_page as scrolling. If you want more context without changing the user's view, say you're reading the page; if you want the user to follow along lower on the page, actually scroll first.
|
|
13416
|
-
- After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. Do not jump straight to read_page(mode="debug").
|
|
13417
|
-
- If the user says they highlighted or selected text, use read_page before falling back to screenshots because it includes active selection and visible unsaved highlights.
|
|
13418
|
-
- If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.
|
|
13419
|
-
- For broad discovery tasks, prefer direct sources, official sites, venue directories, and site-specific search over generic search engines, which often rate-limit automated browser traffic.
|
|
13420
|
-
- If the page context reports a rate limit, human verification, or access warning, stop using that page and switch to a different source.
|
|
13421
|
-
- Reference interactive elements by their index number (shown as [#N] in the listings above).
|
|
13422
|
-
- Be concise. Explain what you're doing as you go.
|
|
13423
|
-
- For simple questions about the page, just answer directly without using tools.
|
|
13424
|
-
- VISUAL AWARENESS: The human is watching the browser alongside this chat. Highlights are your pointing finger — they show the user exactly what you're looking at on the page. Use them proactively: highlight key findings, important elements, errors, or anything you're referencing. Don't wait to be asked. If you mention something specific on the page, highlight it. Colors: yellow (default/attention), red (errors/warnings), green (success/good), blue (info/neutral), purple (important/notable), orange (caution). Clear highlights when moving to a new topic or page.
|
|
13425
|
-
- After completing a task or answering a question, offer 1-2 brief, natural follow-up suggestions that make sense in context (e.g. "Want me to highlight any of these?" or "I can save these to a bookmark folder if you'd like"). Keep suggestions short and conversational — don't list every possible action.
|
|
13426
|
-
- Call one tool at a time unless you are certain your provider supports parallel tool calls. Sequential calls are more reliable.
|
|
13427
|
-
- MINIMIZE TOOL CALLS: Every tool call takes time and costs a round trip. Be efficient. Don't use flow_start/flow_advance for simple multi-step tasks — just do the work. Don't call read_page after navigating — use search or type_text directly. Don't retry failed tools with slight variations — if search fails, go straight to type_text + press_key Enter, don't try read_page in between. The fastest path is usually: navigate → search → wait_for or read_page(mode="results_only") → click.
|
|
13428
|
-
- ACT, DON'T HEDGE: You have a full browser — you can navigate to any website, see live content, search, click, add to cart, fill forms, and interact with real pages in real time. Never claim you "don't have access" to a website's inventory, pricing, or content. If the user asks you to go somewhere and do something, start doing it immediately. Don't ask for permission to do what the user just asked you to do — that's redundant and frustrating. Jump straight into action.
|
|
13429
|
-
- USE YOUR KNOWLEDGE: You have broad, practical knowledge about technology, products, cooking, travel, finance, and countless other domains. When the user asks for recommendations, GIVE them — don't deflect to Reddit, YouTubers, or other sources. You know enough to recommend PC parts, suggest restaurants, pick a good laptop, or advise on most consumer decisions. Make a clear recommendation, explain your reasoning briefly, and then execute. If there's genuine ambiguity (e.g. AMD vs Intel is preference-dependent), state your pick and why, then ask only the questions that would actually change your recommendation. Never refuse a recommendation by claiming you're "not an expert" — the user chose to ask you, so help them.
|
|
13430
|
-
- NEVER USE EMOJIS unless the user uses them first.`;
|
|
13431
|
-
const actionCtx = { tabManager, runtime: runtime2 };
|
|
15734
|
+
const systemPrompt = buildAgentSystemPrompt({
|
|
15735
|
+
profile: provider.agentToolProfile,
|
|
15736
|
+
activeTabTitle,
|
|
15737
|
+
activeTabUrl,
|
|
15738
|
+
tabSummary,
|
|
15739
|
+
defaultReadMode,
|
|
15740
|
+
pageType,
|
|
15741
|
+
structuredContext,
|
|
15742
|
+
supervisorPaused: runtimeState.supervisor.paused,
|
|
15743
|
+
approvalMode: runtimeState.supervisor.approvalMode,
|
|
15744
|
+
pendingApprovals: runtimeState.supervisor.pendingApprovals.length,
|
|
15745
|
+
recentCheckpoints: recentCheckpoints || "- none",
|
|
15746
|
+
taskTrackerContext: taskTrackerContext || "- none"
|
|
15747
|
+
});
|
|
15748
|
+
const actionCtx = {
|
|
15749
|
+
tabManager,
|
|
15750
|
+
runtime: runtime2,
|
|
15751
|
+
toolProfile: provider.agentToolProfile
|
|
15752
|
+
};
|
|
13432
15753
|
const contextualTools = pruneToolsForContext(
|
|
13433
15754
|
AGENT_TOOLS,
|
|
13434
15755
|
pageType,
|
|
13435
|
-
query
|
|
15756
|
+
query,
|
|
15757
|
+
{ profile: provider.agentToolProfile }
|
|
13436
15758
|
);
|
|
13437
15759
|
const trace = createTraceSession(query, activeTabUrl, activeTabTitle);
|
|
13438
15760
|
let accumulatedResponse = "";
|
|
@@ -13450,6 +15772,14 @@ Instructions:
|
|
|
13450
15772
|
let isError = false;
|
|
13451
15773
|
try {
|
|
13452
15774
|
output = await executeAction(name, args, actionCtx);
|
|
15775
|
+
if (provider.agentToolProfile === "compact") {
|
|
15776
|
+
runtime2.updateTaskTracker(name, output);
|
|
15777
|
+
const trackerCtx = runtime2.getTaskTrackerContext();
|
|
15778
|
+
if (trackerCtx) {
|
|
15779
|
+
output = `${output}
|
|
15780
|
+
${trackerCtx}`;
|
|
15781
|
+
}
|
|
15782
|
+
}
|
|
13453
15783
|
} catch (err) {
|
|
13454
15784
|
isError = true;
|
|
13455
15785
|
output = err instanceof Error ? err.message : String(err);
|
|
@@ -14719,8 +17049,24 @@ async function clickResolvedSelector(wc, selector) {
|
|
|
14719
17049
|
await waitForPotentialNavigation(wc, beforeUrl2);
|
|
14720
17050
|
const afterUrl2 = wc.getURL();
|
|
14721
17051
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
14722
|
-
|
|
14723
|
-
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
|
+
}
|
|
14724
17070
|
const dialogActions = typeof idxLabel === "string" && isAddToCartText(idxLabel) ? await getCartDialogActions(wc) : null;
|
|
14725
17071
|
const actionsSuffix = dialogActions ? `
|
|
14726
17072
|
${dialogActions}
|
|
@@ -14742,9 +17088,11 @@ ${overlayHint2}${actionsSuffix}`;
|
|
|
14742
17088
|
const result = await wc.executeJavaScript(`
|
|
14743
17089
|
(function() {
|
|
14744
17090
|
var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
|
|
14745
|
-
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.";
|
|
14746
17092
|
if (el instanceof HTMLElement) { el.focus(); el.click(); }
|
|
14747
|
-
|
|
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 : "");
|
|
14748
17096
|
})()
|
|
14749
17097
|
`);
|
|
14750
17098
|
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
@@ -14754,14 +17102,30 @@ ${overlayHint2}${actionsSuffix}`;
|
|
|
14754
17102
|
await waitForPotentialNavigation(wc, beforeUrl2);
|
|
14755
17103
|
const afterUrl2 = wc.getURL();
|
|
14756
17104
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
14757
|
-
|
|
14758
|
-
if (!overlayHint2)
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
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}
|
|
14762
17126
|
Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
14763
17127
|
return `${result}
|
|
14764
|
-
${overlayHint2}${
|
|
17128
|
+
${overlayHint2}${actionsSuffix2}`;
|
|
14765
17129
|
}
|
|
14766
17130
|
const beforeUrl = wc.getURL();
|
|
14767
17131
|
const elInfo = await describeElementForClick(wc, selector);
|
|
@@ -14805,6 +17169,16 @@ Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
|
14805
17169
|
${overlayHint}${actionsSuffix}`;
|
|
14806
17170
|
}
|
|
14807
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
|
+
}
|
|
14808
17182
|
return `${clickText} (${clickResult})`;
|
|
14809
17183
|
}
|
|
14810
17184
|
const activationResult = await activateElement(wc, selector);
|
|
@@ -19730,8 +22104,9 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
19730
22104
|
if (!config || typeof config !== "object" || !("id" in config)) {
|
|
19731
22105
|
return { ok: false, models: [], error: "Invalid provider configuration" };
|
|
19732
22106
|
}
|
|
19733
|
-
|
|
19734
|
-
|
|
22107
|
+
return await fetchProviderModels(
|
|
22108
|
+
config
|
|
22109
|
+
);
|
|
19735
22110
|
} catch (err) {
|
|
19736
22111
|
return { ok: false, models: [], error: err instanceof Error ? err.message : "Unknown error" };
|
|
19737
22112
|
}
|
|
@@ -20140,6 +22515,353 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
20140
22515
|
});
|
|
20141
22516
|
registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
|
|
20142
22517
|
}
|
|
22518
|
+
function makeStep(label, status = "pending") {
|
|
22519
|
+
return { label, status };
|
|
22520
|
+
}
|
|
22521
|
+
function extractRequestedCount(goal) {
|
|
22522
|
+
const digitMatch = goal.match(/\b(\d+)\b/);
|
|
22523
|
+
if (digitMatch) return Number(digitMatch[1]);
|
|
22524
|
+
const wordMap = {
|
|
22525
|
+
one: 1,
|
|
22526
|
+
two: 2,
|
|
22527
|
+
three: 3,
|
|
22528
|
+
four: 4,
|
|
22529
|
+
five: 5,
|
|
22530
|
+
six: 6,
|
|
22531
|
+
seven: 7,
|
|
22532
|
+
eight: 8,
|
|
22533
|
+
nine: 9,
|
|
22534
|
+
ten: 10
|
|
22535
|
+
};
|
|
22536
|
+
for (const [word, count] of Object.entries(wordMap)) {
|
|
22537
|
+
if (new RegExp(`\\b${word}\\b`, "i").test(goal)) return count;
|
|
22538
|
+
}
|
|
22539
|
+
return null;
|
|
22540
|
+
}
|
|
22541
|
+
function buildInitialSteps(goal) {
|
|
22542
|
+
const lowered = goal.toLowerCase();
|
|
22543
|
+
const requestedCount = extractRequestedCount(goal);
|
|
22544
|
+
const itemLabel = /\b(book|books)\b/.test(lowered) ? "books" : "items";
|
|
22545
|
+
const countLabel = requestedCount ? `${requestedCount} ${itemLabel}` : itemLabel;
|
|
22546
|
+
const steps = [
|
|
22547
|
+
makeStep("Navigate to the requested site", "active")
|
|
22548
|
+
];
|
|
22549
|
+
if (/\b(find|browse|look|discover|select|recommend|interesting)\b/.test(lowered)) {
|
|
22550
|
+
steps.push(makeStep(`Browse or search for relevant ${itemLabel}`));
|
|
22551
|
+
}
|
|
22552
|
+
steps.push(makeStep(`Pick the requested ${countLabel}`));
|
|
22553
|
+
if (/\b(cart|checkout|bag)\b/.test(lowered)) {
|
|
22554
|
+
steps.push(makeStep(`Add the chosen ${itemLabel} to the cart`));
|
|
22555
|
+
}
|
|
22556
|
+
if (/\b(explain|reason|why)\b/.test(lowered)) {
|
|
22557
|
+
steps.push(makeStep("Explain the recommendations"));
|
|
22558
|
+
}
|
|
22559
|
+
return steps;
|
|
22560
|
+
}
|
|
22561
|
+
function setActiveStep(steps, currentStepIndex) {
|
|
22562
|
+
return steps.map((step, index) => {
|
|
22563
|
+
if (step.status === "done" || step.status === "failed") return step;
|
|
22564
|
+
return {
|
|
22565
|
+
...step,
|
|
22566
|
+
status: index === currentStepIndex ? "active" : "pending"
|
|
22567
|
+
};
|
|
22568
|
+
});
|
|
22569
|
+
}
|
|
22570
|
+
function completeStep(state2, detail) {
|
|
22571
|
+
const steps = state2.steps.map((step) => ({ ...step }));
|
|
22572
|
+
const current = steps[state2.currentStepIndex];
|
|
22573
|
+
if (current) {
|
|
22574
|
+
current.status = "done";
|
|
22575
|
+
current.detail = detail || current.detail;
|
|
22576
|
+
}
|
|
22577
|
+
const nextIndex = Math.min(state2.currentStepIndex + 1, steps.length - 1);
|
|
22578
|
+
const normalizedSteps = setActiveStep(
|
|
22579
|
+
steps,
|
|
22580
|
+
current ? nextIndex : state2.currentStepIndex
|
|
22581
|
+
);
|
|
22582
|
+
return {
|
|
22583
|
+
...state2,
|
|
22584
|
+
steps: normalizedSteps,
|
|
22585
|
+
currentStepIndex: current ? nextIndex : state2.currentStepIndex,
|
|
22586
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22587
|
+
};
|
|
22588
|
+
}
|
|
22589
|
+
function setNextHint(state2, nextHint) {
|
|
22590
|
+
return {
|
|
22591
|
+
...state2,
|
|
22592
|
+
nextHint,
|
|
22593
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22594
|
+
};
|
|
22595
|
+
}
|
|
22596
|
+
function normalizeResult(result) {
|
|
22597
|
+
return result.toLowerCase();
|
|
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
|
+
}
|
|
22675
|
+
function createTaskTracker(goal, startUrl) {
|
|
22676
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22677
|
+
return {
|
|
22678
|
+
goal,
|
|
22679
|
+
startedAt: now,
|
|
22680
|
+
updatedAt: now,
|
|
22681
|
+
startUrl,
|
|
22682
|
+
currentStepIndex: 0,
|
|
22683
|
+
steps: buildInitialSteps(goal),
|
|
22684
|
+
lastAction: void 0,
|
|
22685
|
+
nextHint: "Use the site's search box or a strong curated section immediately. Avoid rereading the homepage unless search or navigation is hidden."
|
|
22686
|
+
};
|
|
22687
|
+
}
|
|
22688
|
+
function updateTaskTracker(state2, actionName, result) {
|
|
22689
|
+
const requestedCount = state2.requestedCount ?? extractRequestedCount(state2.goal) ?? null;
|
|
22690
|
+
let cartCount = state2.cartCount ?? 0;
|
|
22691
|
+
const cartVisible = state2.cartVisible || looksLikeCartPage(result);
|
|
22692
|
+
let nextState = {
|
|
22693
|
+
...state2,
|
|
22694
|
+
lastAction: actionName,
|
|
22695
|
+
requestedCount,
|
|
22696
|
+
cartCount,
|
|
22697
|
+
cartVisible,
|
|
22698
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22699
|
+
};
|
|
22700
|
+
const currentLabel = nextState.steps[nextState.currentStepIndex]?.label.toLowerCase() ?? "";
|
|
22701
|
+
if (actionName === "navigate") {
|
|
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
|
+
}
|
|
22738
|
+
}
|
|
22739
|
+
const isDiscoveryAction = [
|
|
22740
|
+
"read_page",
|
|
22741
|
+
"search",
|
|
22742
|
+
"click",
|
|
22743
|
+
"inspect_element",
|
|
22744
|
+
"scroll"
|
|
22745
|
+
].includes(actionName);
|
|
22746
|
+
if (isDiscoveryAction && /browse or search/.test(currentLabel)) {
|
|
22747
|
+
const surfacedResults = countSurfacedResults(result);
|
|
22748
|
+
nextState = completeStep(nextState, "Found a starting point on the site.");
|
|
22749
|
+
return setNextHint(
|
|
22750
|
+
nextState,
|
|
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."
|
|
22752
|
+
);
|
|
22753
|
+
}
|
|
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
|
+
}
|
|
22814
|
+
return setNextHint(
|
|
22815
|
+
nextState,
|
|
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."
|
|
22817
|
+
);
|
|
22818
|
+
}
|
|
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);
|
|
22833
|
+
return setNextHint(
|
|
22834
|
+
nextState,
|
|
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."
|
|
22836
|
+
);
|
|
22837
|
+
}
|
|
22838
|
+
if (/explain the recommendations/.test(currentLabel)) {
|
|
22839
|
+
return setNextHint(
|
|
22840
|
+
nextState,
|
|
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."
|
|
22842
|
+
);
|
|
22843
|
+
}
|
|
22844
|
+
return nextState;
|
|
22845
|
+
}
|
|
22846
|
+
function formatTaskTracker(state2) {
|
|
22847
|
+
if (!state2) return "";
|
|
22848
|
+
const completed = state2.steps.filter((step) => step.status === "done").map((step) => step.label);
|
|
22849
|
+
const current = state2.steps[state2.currentStepIndex]?.label ?? "Task in progress";
|
|
22850
|
+
const remaining = state2.steps.filter((step, index) => step.status !== "done" && index !== state2.currentStepIndex).map((step) => step.label);
|
|
22851
|
+
const lines = [
|
|
22852
|
+
"--- Task Tracker ---",
|
|
22853
|
+
`Goal: ${state2.goal}`,
|
|
22854
|
+
`Completed: ${completed.length > 0 ? completed.join("; ") : "none yet"}`,
|
|
22855
|
+
`Current: ${current}`,
|
|
22856
|
+
`Remaining: ${remaining.length > 0 ? remaining.join("; ") : "wrap up the response"}`
|
|
22857
|
+
];
|
|
22858
|
+
if (state2.nextHint) {
|
|
22859
|
+
lines.push(`Next: ${state2.nextHint}`);
|
|
22860
|
+
}
|
|
22861
|
+
return `
|
|
22862
|
+
${lines.join("\n")}
|
|
22863
|
+
---`;
|
|
22864
|
+
}
|
|
20143
22865
|
const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
|
|
20144
22866
|
const PERSIST_DEBOUNCE_MS = 500;
|
|
20145
22867
|
function clone(value) {
|
|
@@ -20175,7 +22897,8 @@ function sanitizePersistence(persisted) {
|
|
|
20175
22897
|
checkpoints: Array.isArray(persisted?.checkpoints) ? persisted.checkpoints.slice(-20) : [],
|
|
20176
22898
|
transcript: [],
|
|
20177
22899
|
mcpStatus: "stopped",
|
|
20178
|
-
flowState: null
|
|
22900
|
+
flowState: null,
|
|
22901
|
+
taskTracker: null
|
|
20179
22902
|
};
|
|
20180
22903
|
}
|
|
20181
22904
|
class AgentRuntime {
|
|
@@ -20294,6 +23017,32 @@ class AgentRuntime {
|
|
|
20294
23017
|
this.emit();
|
|
20295
23018
|
return this.getState();
|
|
20296
23019
|
}
|
|
23020
|
+
ensureTaskTracker(goal, startUrl) {
|
|
23021
|
+
const trimmedGoal = goal.trim();
|
|
23022
|
+
if (this.state.taskTracker && this.state.taskTracker.goal.trim() === trimmedGoal) {
|
|
23023
|
+
return clone(this.state.taskTracker);
|
|
23024
|
+
}
|
|
23025
|
+
this.state.taskTracker = createTaskTracker(trimmedGoal, startUrl);
|
|
23026
|
+
this.emit();
|
|
23027
|
+
return clone(this.state.taskTracker);
|
|
23028
|
+
}
|
|
23029
|
+
updateTaskTracker(actionName, result) {
|
|
23030
|
+
if (!this.state.taskTracker) return null;
|
|
23031
|
+
this.state.taskTracker = updateTaskTracker(
|
|
23032
|
+
this.state.taskTracker,
|
|
23033
|
+
actionName,
|
|
23034
|
+
result
|
|
23035
|
+
);
|
|
23036
|
+
this.emit();
|
|
23037
|
+
return clone(this.state.taskTracker);
|
|
23038
|
+
}
|
|
23039
|
+
clearTaskTracker() {
|
|
23040
|
+
this.state.taskTracker = null;
|
|
23041
|
+
this.emit();
|
|
23042
|
+
}
|
|
23043
|
+
getTaskTrackerContext() {
|
|
23044
|
+
return formatTaskTracker(this.state.taskTracker);
|
|
23045
|
+
}
|
|
20297
23046
|
// --- Speedee Flow State ---
|
|
20298
23047
|
startFlow(goal, steps, startUrl) {
|
|
20299
23048
|
const flow = {
|