import z from "zod" import { Effect } from "effect" import { HttpClient } from "effect/unstable/http" import * as Tool from "./tool" import * as McpExa from "./mcp-exa" import DESCRIPTION from "./websearch.txt" const Parameters = z.object({ query: z.string().describe("Websearch query"), numResults: z.number().optional().describe("Number of search results to return (default: 8)"), livecrawl: z .enum(["fallback", "preferred"]) .optional() .describe( "Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')", ), type: z .enum(["auto", "fast", "deep"]) .optional() .describe("Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search"), contextMaxCharacters: z .number() .optional() .describe("Maximum characters for context string optimized for LLMs (default: 10000)"), }) export const WebSearchTool = Tool.define( "websearch", Effect.gen(function* () { const http = yield* HttpClient.HttpClient return { get description() { return DESCRIPTION.replace("{{year}}", new Date().getFullYear().toString()) }, parameters: Parameters, execute: (params: z.infer, ctx: Tool.Context) => Effect.gen(function* () { yield* ctx.ask({ permission: "websearch", patterns: [params.query], always: ["*"], metadata: { query: params.query, numResults: params.numResults, livecrawl: params.livecrawl, type: params.type, contextMaxCharacters: params.contextMaxCharacters, }, }) const result = yield* McpExa.call( http, "web_search_exa", McpExa.SearchArgs, { query: params.query, type: params.type || "auto", numResults: params.numResults || 8, livecrawl: params.livecrawl || "fallback", contextMaxCharacters: params.contextMaxCharacters, }, "25 seconds", ) return { output: result ?? "No search results found. Please try a different query.", title: `Web search: ${params.query}`, metadata: {}, } }).pipe(Effect.orDie), } }), )