blob: e798affa6dfc97ee9867b0a1c59df7ccbd12116f [file]
import { z } from 'zod';
import {
WEB_SEARCH_DEFAULT_LIMIT,
WEB_SEARCH_MAX_LIMIT,
normalizeWebSearchLimit,
normalizeWebSearchQuery,
type WebSearchErrorReason,
type WebSearchResponse,
} from '@maka/core';
import type { MakaTool } from './tool-runtime.js';
const WEB_SEARCH_TOOL_NAME = 'WebSearch';
interface WebSearchExecutor {
search(input: {
readonly query: string;
readonly limit: number;
readonly sessionId: string;
readonly abortSignal?: AbortSignal;
}): Promise<WebSearchResponse>;
}
/** Builds the canonical model tool while leaving policy and transport ownership to its executor. */
export function buildWebSearchTool(executor: WebSearchExecutor): MakaTool {
return {
name: WEB_SEARCH_TOOL_NAME,
categoryHint: 'web_read',
displayName: 'Web search',
description:
'Query the live web through the configured search provider. Returns bounded source rows. Use it only when the task requires current external information.',
parameters: z
.object({
query: z.string().min(1).max(200).describe('Search query, max 200 characters.'),
limit: z
.number()
.int()
.min(1)
.max(WEB_SEARCH_MAX_LIMIT)
.optional()
.describe(`Maximum result count; defaults to ${WEB_SEARCH_DEFAULT_LIMIT}.`),
})
.strict(),
impl: async ({ query, limit }, context) => {
const normalizedQuery = normalizeWebSearchQuery(query);
if (!normalizedQuery) {
return webSearchError('invalid_query', 'Web search requires a valid query.');
}
const response = await executor.search({
query: normalizedQuery,
limit: normalizeWebSearchLimit(limit),
sessionId: context.sessionId,
...(context.abortSignal ? { abortSignal: context.abortSignal } : {}),
});
if (!response.ok) return webSearchError(response.reason, response.message, normalizedQuery);
return {
kind: 'web_search' as const,
provider: response.provider ?? response.results[0]?.provider ?? ('tavily' as const),
query: normalizedQuery,
rows: response.results.map((row) => ({
title: row.title,
url: row.url,
snippet: row.snippet,
source: row.source,
})),
};
},
};
}
function webSearchError(reason: WebSearchErrorReason, message: string, query?: string) {
return {
kind: 'web_search_error' as const,
ok: false as const,
provider: 'tavily' as const,
...(query ? { query } : {}),
reason,
message,
};
}