chenbhao commited on
Commit
76e6c36
·
1 Parent(s): eb3a949

fix: WebSearch API error

Browse files
bun.lock CHANGED
@@ -54,6 +54,7 @@
54
  "cli-highlight": "^2.1.11",
55
  "code-excerpt": "^4.0.0",
56
  "diff": "^8.0.4",
 
57
  "emoji-regex": "^10.6.0",
58
  "env-paths": "^4.0.0",
59
  "execa": "^9.6.1",
@@ -618,6 +619,8 @@
618
 
619
  "dom-mutator": ["dom-mutator@0.6.0", "", {}, "sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg=="],
620
 
 
 
621
  "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
622
 
623
  "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
@@ -742,6 +745,8 @@
742
 
743
  "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
744
 
 
 
745
  "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
746
 
747
  "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
@@ -862,6 +867,8 @@
862
 
863
  "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
864
 
 
 
865
  "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
866
 
867
  "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
@@ -966,6 +973,8 @@
966
 
967
  "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
968
 
 
 
969
  "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
970
 
971
  "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -1426,6 +1435,8 @@
1426
 
1427
  "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1428
 
 
 
1429
  "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
1430
 
1431
  "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
 
54
  "cli-highlight": "^2.1.11",
55
  "code-excerpt": "^4.0.0",
56
  "diff": "^8.0.4",
57
+ "duck-duck-scrape": "^2.2.7",
58
  "emoji-regex": "^10.6.0",
59
  "env-paths": "^4.0.0",
60
  "execa": "^9.6.1",
 
619
 
620
  "dom-mutator": ["dom-mutator@0.6.0", "", {}, "sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg=="],
621
 
622
+ "duck-duck-scrape": ["duck-duck-scrape@2.2.7", "", { "dependencies": { "html-entities": "^2.3.3", "needle": "^3.2.0" } }, "sha512-BEcglwnfx5puJl90KQfX+Q2q5vCguqyMpZcSRPBWk8OY55qWwV93+E+7DbIkrGDW4qkqPfUvtOUdi0lXz6lEMQ=="],
623
+
624
  "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
625
 
626
  "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
 
745
 
746
  "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
747
 
748
+ "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
749
+
750
  "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
751
 
752
  "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
 
867
 
868
  "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
869
 
870
+ "needle": ["needle@3.5.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, "bin": { "needle": "bin/needle" } }, "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w=="],
871
+
872
  "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
873
 
874
  "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
 
973
 
974
  "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
975
 
976
+ "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
977
+
978
  "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
979
 
980
  "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
 
1435
 
1436
  "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1437
 
1438
+ "needle/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
1439
+
1440
  "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
1441
 
1442
  "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
package.json CHANGED
@@ -69,6 +69,7 @@
69
  "cli-highlight": "^2.1.11",
70
  "code-excerpt": "^4.0.0",
71
  "diff": "^8.0.4",
 
72
  "emoji-regex": "^10.6.0",
73
  "env-paths": "^4.0.0",
74
  "execa": "^9.6.1",
 
69
  "cli-highlight": "^2.1.11",
70
  "code-excerpt": "^4.0.0",
71
  "diff": "^8.0.4",
72
+ "duck-duck-scrape": "^2.2.7",
73
  "emoji-regex": "^10.6.0",
74
  "env-paths": "^4.0.0",
75
  "execa": "^9.6.1",
src/tools/WebSearchTool/WebSearchTool.ts CHANGED
@@ -1,19 +1,10 @@
1
- import type {
2
- BetaContentBlock,
3
- BetaWebSearchTool20250305,
4
- } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
5
- import { getAPIProvider } from 'src/utils/model/providers.js'
6
  import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
7
  import { z } from 'zod/v4'
8
- import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
9
- import { queryModelWithStreaming } from '../../services/api/claude.js'
10
  import { buildTool, type ToolDef } from '../../Tool.js'
11
  import { lazySchema } from '../../utils/lazySchema.js'
12
  import { logError } from '../../utils/log.js'
13
- import { createUserMessage } from '../../utils/messages.js'
14
- import { getMainLoopModel, getSmallFastModel } from '../../utils/model/model.js'
15
- import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
16
- import { asSystemPrompt } from '../../utils/systemPromptType.js'
17
  import { getWebSearchPrompt, WEB_SEARCH_TOOL_NAME } from './prompt.js'
18
  import {
19
  getToolUseSummary,
@@ -73,123 +64,50 @@ export type { WebSearchProgress } from '../../types/tools.js'
73
 
74
  import type { WebSearchProgress } from '../../types/tools.js'
75
 
76
- function makeToolSchema(input: Input): BetaWebSearchTool20250305 {
77
- return {
78
- type: 'web_search_20250305',
79
- name: 'web_search',
80
- allowed_domains: input.allowed_domains,
81
- blocked_domains: input.blocked_domains,
82
- max_uses: 8, // Hardcoded to 8 searches maximum
83
- }
84
- }
85
-
86
- function makeOutputFromSearchResponse(
87
- result: BetaContentBlock[],
88
- query: string,
89
- durationSeconds: number,
90
- ): Output {
91
- // The result is a sequence of these blocks:
92
- // - text to start -- always?
93
- // [
94
- // - server_tool_use
95
- // - web_search_tool_result
96
- // - text and citation blocks intermingled
97
- // ]+ (this block repeated for each search)
98
-
99
- const results: (SearchResult | string)[] = []
100
- let textAcc = ''
101
- let inText = true
102
-
103
- for (const block of result) {
104
- if (block.type === 'server_tool_use') {
105
- if (inText) {
106
- inText = false
107
- if (textAcc.trim().length > 0) {
108
- results.push(textAcc.trim())
109
- }
110
- textAcc = ''
111
  }
112
- continue
113
- }
114
 
115
- if (block.type === 'web_search_tool_result') {
116
- // Handle error case - content is a WebSearchToolResultError
117
- if (!Array.isArray(block.content)) {
118
- const errorMessage = `Web search error: ${block.content.error_code}`
119
- logError(new Error(errorMessage))
120
- results.push(errorMessage)
121
- continue
122
  }
123
- // Success case - add results to our collection
124
- const hits = block.content.map(r => ({ title: r.title, url: r.url }))
125
- results.push({
126
- tool_use_id: block.tool_use_id,
127
- content: hits,
128
- })
129
- }
130
 
131
- if (block.type === 'text') {
132
- if (inText) {
133
- textAcc += block.text
134
- } else {
135
- inText = true
136
- textAcc = block.text
137
- }
138
  }
139
- }
140
-
141
- if (textAcc.length) {
142
- results.push(textAcc.trim())
143
- }
144
-
145
- return {
146
- query,
147
- results,
148
- durationSeconds,
149
- }
150
  }
151
 
152
- export const WebSearchTool = buildTool({
153
  name: WEB_SEARCH_TOOL_NAME,
154
- searchHint: 'search the web for current information',
155
- maxResultSizeChars: 100_000,
156
- shouldDefer: true,
157
- async description(input) {
158
- return `Claude wants to search the web for: ${input.query}`
159
- },
160
- userFacingName() {
161
- return 'Web Search'
162
- },
163
  getToolUseSummary,
164
  getActivityDescription(input) {
165
  const summary = getToolUseSummary(input)
166
  return summary ? `Searching for ${summary}` : 'Searching the web'
167
  },
168
  isEnabled() {
169
- const provider = getAPIProvider()
170
- const model = getMainLoopModel()
171
-
172
- // Enable for firstParty
173
- if (provider === 'firstParty') {
174
- return true
175
- }
176
-
177
- // Enable for Vertex AI with supported models (Claude 4.0+)
178
- if (provider === 'vertex') {
179
- const supportsWebSearch =
180
- model.includes('claude-opus-4') ||
181
- model.includes('claude-sonnet-4') ||
182
- model.includes('claude-haiku-4')
183
-
184
- return supportsWebSearch
185
- }
186
-
187
- // Foundry only ships models that already support Web Search
188
- if (provider === 'foundry') {
189
- return true
190
- }
191
-
192
- return false
193
  },
194
  get inputSchema(): InputSchema {
195
  return inputSchema()
@@ -253,150 +171,79 @@ export const WebSearchTool = buildTool({
253
  },
254
  async call(input, context, _canUseTool, _parentMessage, onProgress) {
255
  const startTime = performance.now()
256
- const { query } = input
257
- const userMessage = createUserMessage({
258
- content: 'Perform a web search for the query: ' + query,
259
- })
260
- const toolSchema = makeToolSchema(input)
261
-
262
- const useHaiku = getFeatureValue_CACHED_MAY_BE_STALE(
263
- 'tengu_plum_vx3',
264
- false,
265
- )
266
 
267
- const appState = context.getAppState()
268
- const queryStream = queryModelWithStreaming({
269
- messages: [userMessage],
270
- systemPrompt: asSystemPrompt([
271
- 'You are an assistant for performing a web search tool use',
272
- ]),
273
- thinkingConfig: useHaiku
274
- ? { type: 'disabled' as const }
275
- : context.options.thinkingConfig,
276
- tools: [],
277
- signal: context.abortController.signal,
278
- options: {
279
- getToolPermissionContext: async () => appState.toolPermissionContext,
280
- model: useHaiku ? getSmallFastModel() : context.options.mainLoopModel,
281
- toolChoice: useHaiku ? { type: 'tool', name: 'web_search' } : undefined,
282
- isNonInteractiveSession: context.options.isNonInteractiveSession,
283
- hasAppendSystemPrompt: !!context.options.appendSystemPrompt,
284
- extraToolSchemas: [toolSchema],
285
- querySource: 'web_search_tool',
286
- agents: context.options.agentDefinitions.activeAgents,
287
- mcpTools: [],
288
- agentId: context.agentId,
289
- effortValue: appState.effortValue,
290
- },
291
- })
292
 
293
- const allContentBlocks: BetaContentBlock[] = []
294
- let currentToolUseId = null
295
- let currentToolUseJson = ''
296
- let progressCounter = 0
297
- const toolUseQueries = new Map() // Map of tool_use_id to query
298
 
299
- for await (const event of queryStream) {
300
- if (event.type === 'assistant') {
301
- allContentBlocks.push(...event.message.content)
302
- continue
 
 
 
 
303
  }
304
 
305
- // Track tool use ID when server_tool_use starts
306
- if (
307
- event.type === 'stream_event' &&
308
- event.event?.type === 'content_block_start'
309
- ) {
310
- const contentBlock = event.event.content_block
311
- if (contentBlock && contentBlock.type === 'server_tool_use') {
312
- currentToolUseId = contentBlock.id
313
- currentToolUseJson = ''
314
- // Note: The ServerToolUseBlock doesn't contain input.query
315
- // The actual query comes through input_json_delta events
316
- continue
317
- }
318
  }
319
 
320
- // Accumulate JSON for current tool use
321
- if (
322
- currentToolUseId &&
323
- event.type === 'stream_event' &&
324
- event.event?.type === 'content_block_delta'
325
- ) {
326
- const delta = event.event.delta
327
- if (delta?.type === 'input_json_delta' && delta.partial_json) {
328
- currentToolUseJson += delta.partial_json
 
 
 
 
 
329
 
330
- // Try to extract query from partial JSON for progress updates
331
- try {
332
- // Look for a complete query field
333
- const queryMatch = currentToolUseJson.match(
334
- /"query"\s*:\s*"((?:[^"\\]|\\.)*)"/,
335
- )
336
- if (queryMatch && queryMatch[1]) {
337
- // The regex properly handles escaped characters
338
- const query = jsonParse('"' + queryMatch[1] + '"')
339
 
340
- if (
341
- !toolUseQueries.has(currentToolUseId) ||
342
- toolUseQueries.get(currentToolUseId) !== query
343
- ) {
344
- toolUseQueries.set(currentToolUseId, query)
345
- progressCounter++
346
- if (onProgress) {
347
- onProgress({
348
- toolUseID: `search-progress-${progressCounter}`,
349
- data: {
350
- type: 'query_update',
351
- query,
352
- },
353
- })
354
- }
355
- }
356
- }
357
- } catch {
358
- // Ignore parsing errors for partial JSON
359
- }
360
- }
361
  }
 
 
 
 
 
362
 
363
- // Yield progress when search results come in
364
- if (
365
- event.type === 'stream_event' &&
366
- event.event?.type === 'content_block_start'
367
- ) {
368
- const contentBlock = event.event.content_block
369
- if (contentBlock && contentBlock.type === 'web_search_tool_result') {
370
- // Get the actual query that was used for this search
371
- const toolUseId = contentBlock.tool_use_id
372
- const actualQuery = toolUseQueries.get(toolUseId) || query
373
- const content = contentBlock.content
374
-
375
- progressCounter++
376
- if (onProgress) {
377
- onProgress({
378
- toolUseID: toolUseId || `search-progress-${progressCounter}`,
379
- data: {
380
- type: 'search_results_received',
381
- resultCount: Array.isArray(content) ? content.length : 0,
382
- query: actualQuery,
383
- },
384
- })
385
- }
386
- }
387
  }
388
  }
389
-
390
- // Process the final result
391
- const endTime = performance.now()
392
- const durationSeconds = (endTime - startTime) / 1000
393
-
394
- const data = makeOutputFromSearchResponse(
395
- allContentBlocks,
396
- query,
397
- durationSeconds,
398
- )
399
- return { data }
400
  },
401
  mapToolResultToToolResultBlockParam(output, toolUseID) {
402
  const { query, results } = output
@@ -432,4 +279,4 @@ export const WebSearchTool = buildTool({
432
  content: formattedOutput.trim(),
433
  }
434
  },
435
- } satisfies ToolDef<InputSchema, Output, WebSearchProgress>)
 
1
+ import { search } from 'duck-duck-scrape'
 
 
 
 
2
  import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
3
  import { z } from 'zod/v4'
 
 
4
  import { buildTool, type ToolDef } from '../../Tool.js'
5
  import { lazySchema } from '../../utils/lazySchema.js'
6
  import { logError } from '../../utils/log.js'
7
+ import { jsonStringify } from '../../utils/slowOperations.js'
 
 
 
8
  import { getWebSearchPrompt, WEB_SEARCH_TOOL_NAME } from './prompt.js'
9
  import {
10
  getToolUseSummary,
 
64
 
65
  import type { WebSearchProgress } from '../../types/tools.js'
66
 
67
+ /**
68
+ * Filter search results by domain
69
+ */
70
+ function filterDomains(
71
+ results: Array<{ url: string }>,
72
+ allowedDomains?: string[],
73
+ blockedDomains?: string[]
74
+ ): Array<{ url: string }> {
75
+ return results.filter(result => {
76
+ try {
77
+ const url = new URL(result.url)
78
+ const domain = url.hostname
79
+
80
+ if (allowedDomains?.length > 0) {
81
+ return allowedDomains.some(allowed =>
82
+ domain === allowed || domain.endsWith(`.${allowed}`)
83
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
 
 
85
 
86
+ if (blockedDomains?.length > 0) {
87
+ return !blockedDomains.some(blocked =>
88
+ domain === blocked || domain.endsWith(`.${blocked}`)
89
+ )
 
 
 
90
  }
 
 
 
 
 
 
 
91
 
92
+ return true
93
+ } catch {
94
+ // Invalid URL, filter it out
95
+ return false
 
 
 
96
  }
97
+ })
 
 
 
 
 
 
 
 
 
 
98
  }
99
 
100
+ export const WebSearchTool = buildTool<InputSchema, Output, WebSearchTool>({
101
  name: WEB_SEARCH_TOOL_NAME,
102
+ description: 'Search the web and return search results with titles, URLs, and snippets.',
 
 
 
 
 
 
 
 
103
  getToolUseSummary,
104
  getActivityDescription(input) {
105
  const summary = getToolUseSummary(input)
106
  return summary ? `Searching for ${summary}` : 'Searching the web'
107
  },
108
  isEnabled() {
109
+ // DuckDuckGo works with all providers, including local models
110
+ return true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  },
112
  get inputSchema(): InputSchema {
113
  return inputSchema()
 
171
  },
172
  async call(input, context, _canUseTool, _parentMessage, onProgress) {
173
  const startTime = performance.now()
174
+ const { query, allowed_domains, blocked_domains } = input
 
 
 
 
 
 
 
 
 
175
 
176
+ // Progress update: starting search
177
+ if (onProgress) {
178
+ onProgress({
179
+ toolUseID: 'search-progress-1',
180
+ data: { type: 'query_update', query },
181
+ })
182
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ try {
185
+ // Call DuckDuckGo search
186
+ const response = await search(query, {
187
+ safeSearch: 'moderate',
188
+ })
189
 
190
+ // Filter results by domain if specified
191
+ let filteredResults = response.results
192
+ if (allowed_domains || blocked_domains) {
193
+ filteredResults = filterDomains(
194
+ response.results,
195
+ allowed_domains,
196
+ blocked_domains
197
+ )
198
  }
199
 
200
+ // Progress update: results received
201
+ if (onProgress) {
202
+ onProgress({
203
+ toolUseID: 'search-progress-2',
204
+ data: {
205
+ type: 'search_results_received',
206
+ resultCount: filteredResults.length,
207
+ query,
208
+ },
209
+ })
 
 
 
210
  }
211
 
212
+ // Convert to output format
213
+ const results: (SearchResult | string)[] = []
214
+
215
+ if (filteredResults.length === 0) {
216
+ results.push(`No results for: ${query}`)
217
+ } else {
218
+ results.push({
219
+ tool_use_id: 'search-1',
220
+ content: filteredResults.map(r => ({
221
+ title: r.title,
222
+ url: r.url,
223
+ }))
224
+ })
225
+ }
226
 
227
+ const endTime = performance.now()
228
+ const durationSeconds = (endTime - startTime) / 1000
 
 
 
 
 
 
 
229
 
230
+ return {
231
+ query,
232
+ results,
233
+ durationSeconds,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  }
235
+ } catch (error) {
236
+ logError(error)
237
+
238
+ const endTime = performance.now()
239
+ const durationSeconds = (endTime - startTime) / 1000
240
 
241
+ return {
242
+ query,
243
+ results: [`Error: ${error instanceof Error ? error.message : String(error)}`],
244
+ durationSeconds,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  }
246
  }
 
 
 
 
 
 
 
 
 
 
 
247
  },
248
  mapToolResultToToolResultBlockParam(output, toolUseID) {
249
  const { query, results } = output
 
279
  content: formattedOutput.trim(),
280
  }
281
  },
282
+ }) satisfies ToolDef<InputSchema, Output, WebSearchProgress>
src/tools/WebSearchTool/prompt.ts CHANGED
@@ -5,11 +5,11 @@ export const WEB_SEARCH_TOOL_NAME = 'WebSearch'
5
  export function getWebSearchPrompt(): string {
6
  const currentMonthYear = getLocalMonthYear()
7
  return `
8
- - Allows Claude to search the web and use the results to inform responses
9
  - Provides up-to-date information for current events and recent data
10
  - Returns search result information formatted as search result blocks, including links as markdown hyperlinks
11
  - Use this tool for accessing information beyond Claude's knowledge cutoff
12
- - Searches are performed automatically within a single API call
13
 
14
  CRITICAL REQUIREMENT - You MUST follow this:
15
  - After answering the user's question, you MUST include a "Sources:" section at the end of your response
@@ -25,7 +25,8 @@ CRITICAL REQUIREMENT - You MUST follow this:
25
 
26
  Usage notes:
27
  - Domain filtering is supported to include or block specific websites
28
- - Web search is only available in the US
 
29
 
30
  IMPORTANT - Use the correct year in search queries:
31
  - The current month is ${currentMonthYear}. You MUST use this year when searching for recent information, documentation, or current events.
 
5
  export function getWebSearchPrompt(): string {
6
  const currentMonthYear = getLocalMonthYear()
7
  return `
8
+ - Allows Claude to search the web using DuckDuckGo and use the results to inform responses
9
  - Provides up-to-date information for current events and recent data
10
  - Returns search result information formatted as search result blocks, including links as markdown hyperlinks
11
  - Use this tool for accessing information beyond Claude's knowledge cutoff
12
+ - Works with all AI providers including local models
13
 
14
  CRITICAL REQUIREMENT - You MUST follow this:
15
  - After answering the user's question, you MUST include a "Sources:" section at the end of your response
 
25
 
26
  Usage notes:
27
  - Domain filtering is supported to include or block specific websites
28
+ - DuckDuckGo search is available globally
29
+ - Rate limit: approximately 30 requests per minute
30
 
31
  IMPORTANT - Use the correct year in search queries:
32
  - The current month is ${currentMonthYear}. You MUST use this year when searching for recent information, documentation, or current events.