Upgrade candidates (all 12 done): - #1 parallel read-only tool execution (runToolBatch, 4 loop sites) - #2 configurable retry policy (maxRetries + exponential backoff via SDK) - #3 script-aware token estimation (CJK/symbol/structure-aware heuristic) - #4 head+tail output capping (truncate.ts), auto-applies to bash/git/etc - #5 partial-history compaction (preserve recent tail, summarize older prefix) - #6 dynamic max_tokens (resolveMaxTokens) - #7 richer tool descriptions with "use when" guidance - #8 MCP reconnect retry + /mcp reconnect command + session toolset refresh - #9 context-window cache TTL (cachedAt timestamp, default 7 days) - #10 edit_file similar-match suggestion on old_string miss (bounded Levenshtein) - #11 git_status output head+tail (resolved via #4) - #12 auto-accept now approves all mutating tools; auto-edit stays edit-only Other: - DEFAULT_MAX_ITERATIONS 50 -> 100 (local models issue one tool call per step) - MaxIterationsError message guides resume + config override - plus prior known-issues work (grep -e/--, session id sanitization, MCP content types, 12 hook events, plugin collisions, skill references, git ops expansion, configurable autoCompactThreshold, bashGuard, pathGuard, FilePanel, replay)
73 lines
2.2 KiB
TypeScript
73 lines
2.2 KiB
TypeScript
import { z } from "zod";
|
|
import { stripTags, USER_AGENT, withFetchTimeout } from "../utils/html.js";
|
|
import type { ToolDef } from "./types.js";
|
|
|
|
const schema = z.object({
|
|
query: z.string().describe("Search query."),
|
|
max_results: z.number().int().min(1).max(20).optional(),
|
|
});
|
|
|
|
interface SearchResult {
|
|
title: string;
|
|
url: string;
|
|
snippet: string;
|
|
}
|
|
|
|
function resolveResultUrl(href: string): string {
|
|
const absolute = href.startsWith("//") ? `https:${href}` : href;
|
|
try {
|
|
const url = new URL(absolute);
|
|
const uddg = url.searchParams.get("uddg");
|
|
return uddg ? uddg : absolute;
|
|
} catch {
|
|
return absolute;
|
|
}
|
|
}
|
|
|
|
function parseResults(html: string, limit: number): SearchResult[] {
|
|
const results: SearchResult[] = [];
|
|
const resultRegex =
|
|
/<a rel="nofollow" class="result__a" href="([^"]+)">([\s\S]*?)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
|
|
|
|
let match: RegExpExecArray | null;
|
|
while ((match = resultRegex.exec(html)) && results.length < limit) {
|
|
const [, href, titleHtml, snippetHtml] = match;
|
|
if (!href || !titleHtml || !snippetHtml) continue;
|
|
results.push({
|
|
title: stripTags(titleHtml),
|
|
url: resolveResultUrl(href),
|
|
snippet: stripTags(snippetHtml),
|
|
});
|
|
}
|
|
return results;
|
|
}
|
|
|
|
export const webSearchTool: ToolDef<z.infer<typeof schema>> = {
|
|
name: "web_search",
|
|
description:
|
|
"Search the web via DuckDuckGo. Returns title, url, snippet. Use for info not in the local codebase — " +
|
|
"e.g. an unfamiliar API, library docs, or an error message. Follow up with web_fetch on a specific result for full page text.",
|
|
schema,
|
|
mutating: false,
|
|
handler: async ({ query, max_results }) => {
|
|
const limit = max_results ?? 5;
|
|
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
|
|
const { signal, clear } = withFetchTimeout(10_000);
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: { "User-Agent": USER_AGENT },
|
|
signal,
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Search request failed with status ${response.status}`);
|
|
}
|
|
const html = await response.text();
|
|
const results = parseResults(html, limit);
|
|
return { query, results };
|
|
} finally {
|
|
clear();
|
|
}
|
|
},
|
|
};
|