Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
14 kB · 435 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436import type { BetaContentBlock, BetaWebSearchTool20250305,} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'import { getAPIProvider } from 'src/utils/model/providers.js'import type { PermissionResult } from 'src/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 cyclesexport 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>)