feat(tools): extend cancellation to browser, web, and process tools

This commit is contained in:
William Valentin
2026-02-15 22:12:03 -08:00
parent 7877a1bcc9
commit b4006e91ff
10 changed files with 228 additions and 32 deletions
+24 -6
View File
@@ -1,4 +1,4 @@
import type { Tool, ToolResult } from '../types.js';
import type { Tool, ToolExecutionContext, ToolResult } from '../types.js';
/** Configuration for the web search tool. */
export interface WebSearchConfig {
@@ -49,16 +49,22 @@ async function searchBrave(
query: string,
count: number,
apiKey: string,
signal?: AbortSignal,
): Promise<SearchResult[]> {
const params = new URLSearchParams({
q: query,
count: String(count),
});
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
const fetchSignal = signal
? AbortSignal.any([signal, timeoutSignal])
: timeoutSignal;
const response = await fetch(
`https://api.search.brave.com/res/v1/web/search?${params.toString()}`,
{
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
signal: fetchSignal,
headers: {
'Accept': 'application/json',
'X-Subscription-Token': apiKey,
@@ -90,6 +96,7 @@ async function searchSearxng(
query: string,
count: number,
endpoint: string,
signal?: AbortSignal,
): Promise<SearchResult[]> {
// Strip trailing slash from endpoint
const baseUrl = endpoint.replace(/\/+$/, '');
@@ -99,8 +106,13 @@ async function searchSearxng(
categories: 'general',
});
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
const fetchSignal = signal
? AbortSignal.any([signal, timeoutSignal])
: timeoutSignal;
const response = await fetch(`${baseUrl}/search?${params.toString()}`, {
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
signal: fetchSignal,
headers: {
'Accept': 'application/json',
},
@@ -147,8 +159,11 @@ export function createWebSearchTool(config: WebSearchConfig): Tool {
},
required: ['query'],
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
const args = rawArgs as WebSearchArgs;
if (context?.signal?.aborted) {
return { success: false, output: '', error: 'Operation aborted' };
}
// Clamp count: use provided value (capped at MAX_RESULTS), or fall back to default
const count = Math.min(args.count ?? defaultCount, MAX_RESULTS);
@@ -163,7 +178,7 @@ export function createWebSearchTool(config: WebSearchConfig): Tool {
error: 'Brave Search API key not configured',
};
}
results = await searchBrave(args.query, count, config.apiKey);
results = await searchBrave(args.query, count, config.apiKey, context?.signal);
} else {
// SearXNG provider
if (!config.endpoint) {
@@ -173,7 +188,7 @@ export function createWebSearchTool(config: WebSearchConfig): Tool {
error: 'SearXNG endpoint not configured',
};
}
results = await searchSearxng(args.query, count, config.endpoint);
results = await searchSearxng(args.query, count, config.endpoint, context?.signal);
}
if (results.length === 0) {
@@ -185,6 +200,9 @@ export function createWebSearchTool(config: WebSearchConfig): Tool {
return { success: true, output: formatResults(results) };
} catch (error) {
if (error instanceof Error && (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted'))) {
return { success: false, output: '', error: 'Operation aborted' };
}
return {
success: false,
output: '',