blob: c994766555c2c1860a2157847804dd3b057d0da7 [file]
import { z } from 'zod';
import { resolveHostedWebSearchCapability } from '@maka/core';
import type { HostedWebSearchAdapter } from '@maka/core';
import type { RuntimeExecutionConnection } from '@maka/core';
import type { WebSearchSettings } from '@maka/core';
import type { MakaTool } from './tool-runtime.js';
export const NATIVE_WEB_SEARCH_TOOL_NAME = 'WebSearch';
/**
* Provider-executed search descriptor. AI SDK compiles this into the selected
* provider's native tool; the local implementation is an invariant guard only.
*/
export function buildNativeWebSearchTool(input?: {
readonly adapter?: Extract<HostedWebSearchAdapter, 'openai-responses' | 'anthropic-messages'>;
readonly searchContextSize?: 'low' | 'medium' | 'high';
readonly maxUses?: number;
}): MakaTool {
const adapter = input?.adapter ?? 'openai-responses';
return {
name: NATIVE_WEB_SEARCH_TOOL_NAME,
displayName: 'Web search',
activityKind: 'websearch',
categoryHint: 'web_read',
description:
'Search and read the live web through the current model provider. Use it for current external information and source-backed answers.',
parameters: z.object({}).strict(),
providerTool: {
...(adapter === 'anthropic-messages'
? {
kind: 'anthropic-web-search-20250305' as const,
maxUses: input?.maxUses ?? 8,
}
: {
kind: 'openai-web-search' as const,
searchContextSize: input?.searchContextSize ?? 'medium',
}),
},
impl: () => {
throw new Error('Provider-native WebSearch must not execute through ToolRuntime');
},
};
}
/** Freezes one unambiguous WebSearch tool meaning for the selected model turn. */
export function routeWebSearchTools(input: {
readonly tools: readonly MakaTool[];
readonly settings: Pick<WebSearchSettings, 'enabled' | 'defaultProvider'>;
readonly connection: RuntimeExecutionConnection;
readonly model: string;
readonly privacy?: { readonly incognitoActive: boolean };
/** Root surfaces may add native search even when no client WebSearch exists. */
readonly allowAddNative?: boolean;
}): MakaTool[] {
const firstSearchIndex = input.tools.findIndex(
(tool) => tool.name === NATIVE_WEB_SEARCH_TOOL_NAME,
);
const withoutWebSearch = input.tools.filter((tool) => tool.name !== NATIVE_WEB_SEARCH_TOOL_NAME);
if (!input.settings.enabled || input.privacy?.incognitoActive === true) return withoutWebSearch;
let selected: MakaTool | undefined;
if (input.settings.defaultProvider === 'tavily') {
selected = input.tools.find((tool) => tool.name === NATIVE_WEB_SEARCH_TOOL_NAME);
} else {
const capability = resolveHostedWebSearchCapability(
input.connection.providerType,
input.connection.models,
input.model,
);
if (
(firstSearchIndex >= 0 || input.allowAddNative === true) &&
capability?.implemented === true &&
capability.adapter === 'openai-responses'
) {
selected = buildNativeWebSearchTool({ adapter: 'openai-responses' });
} else if (
(firstSearchIndex >= 0 || input.allowAddNative === true) &&
capability?.implemented === true &&
capability.adapter === 'anthropic-messages'
) {
selected = buildNativeWebSearchTool({ adapter: 'anthropic-messages' });
}
}
if (!selected) return withoutWebSearch;
const insertionIndex =
firstSearchIndex < 0
? withoutWebSearch.length
: input.tools
.slice(0, firstSearchIndex)
.filter((tool) => tool.name !== NATIVE_WEB_SEARCH_TOOL_NAME).length;
return [
...withoutWebSearch.slice(0, insertionIndex),
selected,
...withoutWebSearch.slice(insertionIndex),
];
}