import { l as errorMessage } from "../errors-DkR6GPJw.js";
import { c as stripHtmlInline, o as isHostAllowed } from "../fetch-url-DPP6-Ruo.js";
//#region src/tools/web-search.ts
/**
* Web search tool with a runtime-resolved backend.
*
* Backend selection is keyless-fallback: the first backend whose env var
* is set wins, otherwise we fall through to DuckDuckGo's HTML endpoint
* (no key required, no auth, rate-limited and brittle to layout changes).
*
*   1. TAVILY_API_KEY        → https://api.tavily.com/search
*   2. BRAVE_SEARCH_API_KEY  → https://api.search.brave.com/res/v1/web/search
*   3. (fallback)            → https://html.duckduckgo.com/html/
*
* Selection happens per call (not at module init) so a key added
* mid-session via the chat layer's auth flow is picked up on the very next
* search without restarting the agent. The 5 ms env-var read is negligible
* compared to the network round-trip.
*
* Anthropic providers can take over this tool server-side via the
* `nativeWebSearch` capability — when set, the provider's `formatTools`
* drops this `ToolDef` from the function-tools list and emits the
* server-tool block instead, so this `execute` body is never invoked.
*/
const DEFAULT_MAX_RESULTS = 5;
const HARD_MAX_RESULTS = 20;
const HTTP_TIMEOUT_MS = 15e3;
const ERROR_BODY_MAX = 256;
/**
* Cap on the DuckDuckGo HTML we run the scrape regex over. `blockRe` chains
* lazy `[\s\S]*?` spans, which degrade badly (ReDoS-ish slow path) on
* multi-MB documents; a real DDG results page is well under 512K, so
* anything past the cap is not results markup we'd want anyway.
*/
const DDG_HTML_MAX_CHARS = 512 * 1024;
/**
* Current "Month YYYY" for the search description's recency hint. A model with
* an older training cutoff otherwise queries with a stale year (`React docs
* 2023`) and misses everything newer. Resolved per read (the `description`
* getter below) so a long-lived process picks up a month rollover; the value
* is stable within a session, so it doesn't thrash the provider prompt cache.
*/
function currentMonthYear() {
	return (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
		month: "long",
		year: "numeric"
	});
}
/** Egress host each backend talks to — gated against `fetchUrlAllowHosts`. */
const BACKEND_HOST = {
	tavily: "api.tavily.com",
	brave: "api.search.brave.com",
	duckduckgo: "html.duckduckgo.com"
};
function pickBackend() {
	if (process.env.TAVILY_API_KEY) return "tavily";
	if (process.env.BRAVE_SEARCH_API_KEY) return "brave";
	return "duckduckgo";
}
async function fetchWithTimeout(url, init, signal) {
	const ctrl = new AbortController();
	const onAbort = () => ctrl.abort();
	signal.addEventListener("abort", onAbort, { once: true });
	const timer = setTimeout(() => ctrl.abort(), HTTP_TIMEOUT_MS);
	try {
		return await fetch(url, {
			...init,
			signal: ctrl.signal
		});
	} finally {
		clearTimeout(timer);
		signal.removeEventListener("abort", onAbort);
	}
}
/**
* Truncate untrusted HTTP error bodies before embedding them in our own
* error string. Without this, a backend that returns an HTML 500 page
* leaks a kilobyte of unstructured chrome straight into the model's
* context window — which is both noisy and a small prompt-injection
* surface.
*/
async function readErrorBody(res) {
	const body = await res.text().catch(() => "");
	if (body.length <= ERROR_BODY_MAX) return body;
	return `${body.slice(0, ERROR_BODY_MAX)}… (truncated, ${body.length} bytes total)`;
}
async function searchTavily(query, max, signal) {
	const res = await fetchWithTimeout("https://api.tavily.com/search", {
		method: "POST",
		headers: { "content-type": "application/json" },
		body: JSON.stringify({
			api_key: process.env.TAVILY_API_KEY,
			query,
			max_results: max,
			search_depth: "basic"
		})
	}, signal);
	if (!res.ok) throw new Error(`Tavily HTTP ${res.status}: ${await readErrorBody(res)}`);
	return ((await res.json()).results ?? []).map((r) => ({
		title: r.title ?? "",
		url: r.url ?? "",
		snippet: r.content ?? ""
	}));
}
async function searchBrave(query, max, signal) {
	const res = await fetchWithTimeout(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${max}`, { headers: {
		"accept": "application/json",
		"x-subscription-token": process.env.BRAVE_SEARCH_API_KEY
	} }, signal);
	if (!res.ok) throw new Error(`Brave HTTP ${res.status}: ${await readErrorBody(res)}`);
	return ((await res.json()).web?.results ?? []).map((r) => ({
		title: r.title ?? "",
		url: r.url ?? "",
		snippet: r.description ?? ""
	}));
}
function unwrapDdgRedirect(href) {
	try {
		const u = new URL(href, "https://duckduckgo.com");
		if (u.pathname === "/l/" || u.pathname === "/l") {
			const target = u.searchParams.get("uddg");
			if (target) return decodeURIComponent(target);
		}
		return u.toString();
	} catch {
		return href;
	}
}
async function searchDuckDuckGo(query, max, signal) {
	const res = await fetchWithTimeout(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, { headers: {
		"user-agent": "Mozilla/5.0 (compatible; zidane/1.0)",
		"accept": "text/html"
	} }, signal);
	if (!res.ok) throw new Error(`DuckDuckGo HTTP ${res.status}: ${await readErrorBody(res)}`);
	const html = (await res.text()).slice(0, DDG_HTML_MAX_CHARS);
	const results = [];
	const blockRe = /<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/g;
	let m = blockRe.exec(html);
	while (m !== null) {
		results.push({
			url: unwrapDdgRedirect(m[1]),
			title: stripHtmlInline(m[2]),
			snippet: stripHtmlInline(m[3])
		});
		if (results.length >= max) break;
		m = blockRe.exec(html);
	}
	return results;
}
function formatResults(results, backend) {
	if (results.length === 0) {
		if (backend === "duckduckgo") return "(no results from the keyless DuckDuckGo fallback — it is rate-limited and brittle to layout changes. Set TAVILY_API_KEY or BRAVE_SEARCH_API_KEY for reliable search.)";
		return `(no results from ${backend})`;
	}
	return `${`# Web search results (${backend}, ${results.length} ${results.length === 1 ? "hit" : "hits"})\n`}\n${results.map((r, i) => `${i + 1}. ${r.title}\n   ${r.url}\n   ${r.snippet}`).join("\n\n")}`;
}
const webSearch = {
	isConcurrencySafe: true,
	spec: {
		name: "web_search",
		get description() {
			return `Search the public web and return ranked title / URL / snippet results. Use this to look up documentation, error messages, library versions, or recent events. If a \`fetch_url\` tool is also available, follow up with it to read a specific page in full.
IMPORTANT: the current month is ${currentMonthYear()}. When searching for recent information, docs, or current events, use the current year in the query — not a year from your training cutoff (e.g. search "React docs" with this year, not an older one).`;
		},
		inputSchema: {
			type: "object",
			properties: {
				query: {
					type: "string",
					description: "Search query. Plain natural-language or keyword form; quoting and site: operators are passed through verbatim."
				},
				max_results: {
					type: "number",
					description: `Maximum number of results to return. Default: ${DEFAULT_MAX_RESULTS}. Capped at ${HARD_MAX_RESULTS}.`
				}
			},
			required: ["query"]
		}
	},
	async execute({ query, max_results }, ctx) {
		const q = typeof query === "string" ? query.trim() : "";
		if (!q) return "web_search error: `query` is required";
		const max = Math.min(typeof max_results === "number" && max_results > 0 ? Math.floor(max_results) : DEFAULT_MAX_RESULTS, HARD_MAX_RESULTS);
		const backend = pickBackend();
		const allowHosts = ctx.behavior?.fetchUrlAllowHosts;
		if (!isHostAllowed(BACKEND_HOST[backend], allowHosts)) return `web_search error: the ${backend} backend (${BACKEND_HOST[backend]}) is not in the configured egress allowlist`;
		try {
			return formatResults(backend === "tavily" ? await searchTavily(q, max, ctx.signal) : backend === "brave" ? await searchBrave(q, max, ctx.signal) : await searchDuckDuckGo(q, max, ctx.signal), backend);
		} catch (err) {
			return `web_search error (${backend}): ${errorMessage(err)}`;
		}
	}
};
//#endregion
export { webSearch };

//# sourceMappingURL=web-search.js.map