Files
locode/src/tools/webSearch.ts
T
kim 6fe98887d5 v0.6.0: complete all 12 local-model upgrades + raise maxIterations to 100
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)
2026-08-20 16:52:03 +09:00

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();
}
},
};