WebSearchTool.ts
tools/WebSearchTool/WebSearchTool.ts
383
Lines
12363
Bytes
3
Exports
17
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is part of the tool layer, which means it describes actions the system can perform for the user or model.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with tool-system. It contains 383 lines, 17 detected imports, and 3 detected exports.
Important relationships
Detected exports
SearchResultOutputWebSearchTool
Keywords
querysearchresultsinputeventresultcontentblockdescribemodel
Detected imports
@anthropic-ai/sdk/resources/beta/messages/messages.mjs../../utils/model/providers.js../../utils/permissions/PermissionResult.jszod/v4../../services/analytics/growthbook.js../../services/api/claude.js../../Tool.js../../utils/lazySchema.js../../utils/log.js../../utils/messages.js../../utils/model/model.js../../utils/slowOperations.js../../utils/systemPromptType.js./prompt.js./UI.js../../types/tools.js../../types/tools.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import type { BetaContentBlock, BetaWebSearchTool20250305 } from "@anthropic-ai/sdk/resources/beta/messages/messages.mjs";
import { getAPIProvider } from "../../utils/model/providers.js";
import type { PermissionResult } from "../../utils/permissions/PermissionResult.js";
import { z } from "zod/v4";
import { getFeatureValue_CACHED_MAY_BE_STALE } from "../../services/analytics/growthbook.js";
import { queryModelWithStreaming } from "../../services/api/claude.js";
import { buildTool, type ToolDef } from "../../Tool.js";
import { lazySchema } from "../../utils/lazySchema.js";
import { logError } from "../../utils/log.js";
import { createUserMessage } from "../../utils/messages.js";
import { getMainLoopModel, getSmallFastModel } from "../../utils/model/model.js";
import { jsonParse, jsonStringify } from "../../utils/slowOperations.js";
import { asSystemPrompt } from "../../utils/systemPromptType.js";
import { getWebSearchPrompt, WEB_SEARCH_TOOL_NAME } from "./prompt.js";
import { getToolUseSummary, renderToolResultMessage, renderToolUseMessage, renderToolUseProgressMessage } from "./UI.js";
const inputSchema = lazySchema(() =>
z.strictObject({
query: z.string().min(2).describe("The search query to use"),
allowed_domains: z.array(z.string()).optional().describe("Only include search results from these domains"),
blocked_domains: z.array(z.string()).optional().describe("Never include search results from these domains"),
})
);
type InputSchema = ReturnType<typeof inputSchema>;
type Input = z.infer<InputSchema>;
const searchResultSchema = lazySchema(() => {
const searchHitSchema = z.object({
title: z.string().describe("The title of the search result"),
url: z.string().describe("The URL of the search result"),
});
return z.object({
tool_use_id: z.string().describe("ID of the tool use"),
content: z.array(searchHitSchema).describe("Array of search hits"),
});
});
export type SearchResult = z.infer<ReturnType<typeof searchResultSchema>>;
const outputSchema = lazySchema(() =>
z.object({
query: z.string().describe("The search query that was executed"),
results: z.array(z.union([searchResultSchema(), z.string()])).describe("Search results and/or text commentary from the model"),
durationSeconds: z.number().describe("Time taken to complete the search operation"),
})
);
type OutputSchema = ReturnType<typeof outputSchema>;
export type Output = z.infer<OutputSchema>;
// Re-export WebSearchProgress from centralized types to break import cycles
export type { WebSearchProgress } from "../../types/tools.js";
import type { WebSearchProgress } from "../../types/tools.js";
function makeToolSchema(input: Input): BetaWebSearchTool20250305 {
return {
type: "web_search_20250305",
name: "web_search",
allowed_domains: input.allowed_domains,
blocked_domains: input.blocked_domains,
max_uses: 8, // Hardcoded to 8 searches maximum
};
}
function makeOutputFromSearchResponse(result: BetaContentBlock[], query: string, durationSeconds: number): Output {
// The result is a sequence of these blocks:
// - text to start -- always?
// [
// - server_tool_use
// - web_search_tool_result
// - text and citation blocks intermingled
// ]+ (this block repeated for each search)
const results: (SearchResult | string)[] = [];
let textAcc = "";
let inText = true;
for (const block of result) {
if (block.type === "server_tool_use") {
if (inText) {
inText = false;
if (textAcc.trim().length > 0) {
results.push(textAcc.trim());
}
textAcc = "";
}
continue;
}
if (block.type === "web_search_tool_result") {
// Handle error case - content is a WebSearchToolResultError
if (!Array.isArray(block.content)) {
const errorMessage = `Web search error: ${block.content.error_code}`;
logError(new Error(errorMessage));
results.push(errorMessage);
continue;
}
// Success case - add results to our collection
const hits = block.content.map((r) => ({ title: r.title, url: r.url }));
results.push({
tool_use_id: block.tool_use_id,
content: hits,
});
}
if (block.type === "text") {
if (inText) {
textAcc += block.text;
} else {
inText = true;
textAcc = block.text;
}
}
}
if (textAcc.length) {
results.push(textAcc.trim());
}
return {
query,
results,
durationSeconds,
};
}
export const WebSearchTool = buildTool({
name: WEB_SEARCH_TOOL_NAME,
searchHint: "search the web for current information",
maxResultSizeChars: 100_000,
shouldDefer: true,
async description(input) {
return `Claude wants to search the web for: ${input.query}`;
},
userFacingName() {
return "Web Search";
},
getToolUseSummary,
getActivityDescription(input) {
const summary = getToolUseSummary(input);
return summary ? `Searching for ${summary}` : "Searching the web";
},
isEnabled() {
const provider = getAPIProvider();
const model = getMainLoopModel();
// Enable for firstParty
if (provider === "firstParty") {
return true;
}
// Enable for Vertex AI with supported models (Claude 4.0+)
if (provider === "vertex") {
const supportsWebSearch = model.includes("claude-opus-4") || model.includes("claude-sonnet-4") || model.includes("claude-haiku-4");
return supportsWebSearch;
}
// Foundry only ships models that already support Web Search
if (provider === "foundry") {
return true;
}
return false;
},
get inputSchema(): InputSchema {
return inputSchema();
},
get outputSchema(): OutputSchema {
return outputSchema();
},
isConcurrencySafe() {
return true;
},
isReadOnly() {
return true;
},
toAutoClassifierInput(input) {
return input.query;
},
async checkPermissions(_input): Promise<PermissionResult> {
return {
behavior: "passthrough",
message: "WebSearchTool requires permission.",
suggestions: [
{
type: "addRules",
rules: [{ toolName: WEB_SEARCH_TOOL_NAME }],
behavior: "allow",
destination: "localSettings",
},
],
};
},
async prompt() {
return getWebSearchPrompt();
},
renderToolUseMessage,
renderToolUseProgressMessage,
renderToolResultMessage,
extractSearchText() {
// renderToolResultMessage shows only "Did N searches in Xs" chrome —
// the results[] content never appears on screen. Heuristic would index
// string entries in results[] (phantom match). Nothing to search.
return "";
},
async validateInput(input) {
const { query, allowed_domains, blocked_domains } = input;
if (!query.length) {
return {
result: false,
message: "Error: Missing query",
errorCode: 1,
};
}
if (allowed_domains?.length && blocked_domains?.length) {
return {
result: false,
message: "Error: Cannot specify both allowed_domains and blocked_domains in the same request",
errorCode: 2,
};
}
return { result: true };
},
async call(input, context, _canUseTool, _parentMessage, onProgress) {
const startTime = performance.now();
const { query } = input;
const userMessage = createUserMessage({
content: "Perform a web search for the query: " + query,
});
const toolSchema = makeToolSchema(input);
const useHaiku = getFeatureValue_CACHED_MAY_BE_STALE("tengu_plum_vx3", false);
const appState = context.getAppState();
const queryStream = queryModelWithStreaming({
messages: [userMessage],
systemPrompt: asSystemPrompt(["You are an assistant for performing a web search tool use"]),
thinkingConfig: useHaiku ? { type: "disabled" as const } : context.options.thinkingConfig,
tools: [],
signal: context.abortController.signal,
options: {
getToolPermissionContext: async () => appState.toolPermissionContext,
model: useHaiku ? getSmallFastModel() : context.options.mainLoopModel,
toolChoice: useHaiku ? { type: "tool", name: "web_search" } : undefined,
isNonInteractiveSession: context.options.isNonInteractiveSession,
hasAppendSystemPrompt: !!context.options.appendSystemPrompt,
extraToolSchemas: [toolSchema],
querySource: "web_search_tool",
agents: context.options.agentDefinitions.activeAgents,
mcpTools: [],
agentId: context.agentId,
effortValue: appState.effortValue,
},
});
const allContentBlocks: BetaContentBlock[] = [];
let currentToolUseId = null;
let currentToolUseJson = "";
let progressCounter = 0;
const toolUseQueries = new Map(); // Map of tool_use_id to query
for await (const event of queryStream) {
if (event.type === "assistant") {
allContentBlocks.push(...event.message.content);
continue;
}
// Track tool use ID when server_tool_use starts
if (event.type === "stream_event" && event.event?.type === "content_block_start") {
const contentBlock = event.event.content_block;
if (contentBlock && contentBlock.type === "server_tool_use") {
currentToolUseId = contentBlock.id;
currentToolUseJson = "";
// Note: The ServerToolUseBlock doesn't contain input.query
// The actual query comes through input_json_delta events
continue;
}
}
// Accumulate JSON for current tool use
if (currentToolUseId && event.type === "stream_event" && event.event?.type === "content_block_delta") {
const delta = event.event.delta;
if (delta?.type === "input_json_delta" && delta.partial_json) {
currentToolUseJson += delta.partial_json;
// Try to extract query from partial JSON for progress updates
try {
// Look for a complete query field
const queryMatch = currentToolUseJson.match(/"query"\s*:\s*"((?:[^"\\]|\\.)*)"/);
if (queryMatch && queryMatch[1]) {
// The regex properly handles escaped characters
const query = jsonParse('"' + queryMatch[1] + '"');
if (!toolUseQueries.has(currentToolUseId) || toolUseQueries.get(currentToolUseId) !== query) {
toolUseQueries.set(currentToolUseId, query);
progressCounter++;
if (onProgress) {
onProgress({
toolUseID: `search-progress-${progressCounter}`,
data: {
type: "query_update",
query,
},
});
}
}
}
} catch {
// Ignore parsing errors for partial JSON
}
}
}
// Yield progress when search results come in
if (event.type === "stream_event" && event.event?.type === "content_block_start") {
const contentBlock = event.event.content_block;
if (contentBlock && contentBlock.type === "web_search_tool_result") {
// Get the actual query that was used for this search
const toolUseId = contentBlock.tool_use_id;
const actualQuery = toolUseQueries.get(toolUseId) || query;
const content = contentBlock.content;
progressCounter++;
if (onProgress) {
onProgress({
toolUseID: toolUseId || `search-progress-${progressCounter}`,
data: {
type: "search_results_received",
resultCount: Array.isArray(content) ? content.length : 0,
query: actualQuery,
},
});
}
}
}
}
// Process the final result
const endTime = performance.now();
const durationSeconds = (endTime - startTime) / 1000;
const data = makeOutputFromSearchResponse(allContentBlocks, query, durationSeconds);
return { data };
},
mapToolResultToToolResultBlockParam(output, toolUseID) {
const { query, results } = output;
let formattedOutput = `Web search results for query: "${query}"\n\n`;
// Process the results array - it can contain both string summaries and search result objects.
// Guard against null/undefined entries that can appear after JSON round-tripping
// (e.g., from compaction or transcript deserialization).
(results ?? []).forEach((result) => {
if (result == null) {
return;
}
if (typeof result === "string") {
// Text summary
formattedOutput += result + "\n\n";
} else {
// Search result with links
if (result.content?.length > 0) {
formattedOutput += `Links: ${jsonStringify(result.content)}\n\n`;
} else {
formattedOutput += "No links found.\n\n";
}
}
});
formattedOutput += "\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.";
return {
tool_use_id: toolUseID,
type: "tool_result",
content: formattedOutput.trim(),
};
},
} satisfies ToolDef<InputSchema, Output, WebSearchProgress>);