2026-04-23 20:09:34 +00:00
import { Effect , Schema } from "effect"
2026-04-10 15:30:38 +00:00
import { HttpClient } from "effect/unstable/http"
2026-04-16 03:56:54 +00:00
import * as Tool from "./tool"
2026-05-08 08:49:36 +00:00
import * as McpWebSearch from "./mcp-websearch"
2025-11-10 21:39:40 +00:00
import DESCRIPTION from "./websearch.txt"
2026-05-08 08:49:36 +00:00
import { checksum } from "@opencode-ai/core/util/encode"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
2026-05-13 01:45:53 +00:00
import { RuntimeFlags } from "@/effect/runtime-flags"
2025-11-10 21:39:40 +00:00
2026-04-23 20:09:34 +00:00
export const Parameters = Schema . Struct ( {
query : Schema.String.annotate ( { description : "Websearch query" } ) ,
numResults : Schema.optional ( Schema . Number ) . annotate ( {
description : "Number of search results to return (default: 8)" ,
} ) ,
livecrawl : Schema.optional ( Schema . Literals ( [ "fallback" , "preferred" ] ) ) . annotate ( {
description :
2026-04-07 23:48:12 +00:00
"Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')" ,
2026-04-23 20:09:34 +00:00
} ) ,
type : Schema . optional ( Schema . Literals ( [ "auto" , "fast" , "deep" ] ) ) . annotate ( {
description : "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search" ,
} ) ,
contextMaxCharacters : Schema.optional ( Schema . Number ) . annotate ( {
description : "Maximum characters for context string optimized for LLMs (default: 10000)" ,
} ) ,
2026-04-07 23:48:12 +00:00
} )
2026-05-08 08:49:36 +00:00
const WebSearchProviderSchema = Schema . Literals ( [ "exa" , "parallel" ] )
export type WebSearchProvider = Schema . Schema . Type < typeof WebSearchProviderSchema >
2026-05-13 01:45:53 +00:00
export function selectWebSearchProvider ( sessionID : string , flags = { exa : false , parallel : false } ) : WebSearchProvider {
2026-05-08 08:49:36 +00:00
const override = process . env . OPENCODE_WEBSEARCH_PROVIDER
if ( override === "exa" || override === "parallel" ) return override
if ( flags . parallel ) return "parallel"
if ( flags . exa ) return "exa"
return Number . parseInt ( checksum ( sessionID ) ? ? "0" , 36 ) % 2 === 0 ? "exa" : "parallel"
}
export function webSearchProviderLabel ( provider : unknown ) {
if ( provider === "parallel" ) return "Parallel Web Search"
if ( provider === "exa" ) return "Exa Web Search"
return "Web Search"
}
export function webSearchModelName ( extra : Tool.Context [ "extra" ] ) {
const model = extra ? . model
if ( ! model || typeof model !== "object" ) return undefined
const api = "api" in model && model . api && typeof model . api === "object" ? model.api : undefined
const apiID = api && "id" in api && typeof api . id === "string" ? api.id : undefined
const id = "id" in model && typeof model . id === "string" ? model.id : undefined
return ( apiID ? ? id ) ? . slice ( 0 , 100 )
}
function parallelAuthHeaders() {
const headers = { "User-Agent" : ` opencode/ ${ InstallationVersion } ` }
if ( ! process . env . PARALLEL_API_KEY ) return headers
return { . . . headers , Authorization : ` Bearer ${ process . env . PARALLEL_API_KEY } ` }
}
function callProvider (
http : HttpClient.HttpClient ,
provider : WebSearchProvider ,
params : Schema.Schema.Type < typeof Parameters > ,
ctx : Tool.Context ,
) {
if ( provider === "parallel" ) {
return McpWebSearch . call (
http ,
McpWebSearch . PARALLEL_URL ,
"web_search" ,
McpWebSearch . ParallelSearchArgs ,
{
objective : params.query ,
search_queries : [ params . query ] ,
session_id : ctx.sessionID ,
model_name : webSearchModelName ( ctx . extra ) ,
} ,
"25 seconds" ,
parallelAuthHeaders ( ) ,
)
}
return McpWebSearch . call (
http ,
McpWebSearch . EXA_URL ,
"web_search_exa" ,
McpWebSearch . SearchArgs ,
{
query : params.query ,
type : params . type || "auto" ,
numResults : params.numResults || 8 ,
livecrawl : params.livecrawl || "fallback" ,
contextMaxCharacters : params.contextMaxCharacters ,
} ,
"25 seconds" ,
)
}
2026-04-11 02:36:02 +00:00
export const WebSearchTool = Tool . define (
2026-04-10 15:30:38 +00:00
"websearch" ,
Effect . gen ( function * ( ) {
const http = yield * HttpClient . HttpClient
2026-05-13 01:45:53 +00:00
const flags = yield * RuntimeFlags . Service
2026-04-10 15:30:38 +00:00
return {
get description() {
return DESCRIPTION . replace ( "{{year}}" , new Date ( ) . getFullYear ( ) . toString ( ) )
} ,
parameters : Parameters ,
2026-04-23 20:09:34 +00:00
execute : ( params : Schema.Schema.Type < typeof Parameters > , ctx : Tool.Context ) = >
2026-04-10 15:30:38 +00:00
Effect . gen ( function * ( ) {
2026-05-13 01:45:53 +00:00
const provider = selectWebSearchProvider ( ctx . sessionID , {
exa : flags.enableExa ,
parallel : flags.enableParallel ,
} )
2026-05-08 08:49:36 +00:00
const title = webSearchProviderLabel ( provider )
yield * ctx . metadata ( { title : ` ${ title } " ${ params . query } " ` , metadata : { provider } } )
2026-04-11 02:36:02 +00:00
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 ,
2026-05-08 08:49:36 +00:00
provider ,
2026-04-11 02:36:02 +00:00
} ,
} )
2026-04-10 15:30:38 +00:00
2026-05-08 08:49:36 +00:00
const result = yield * callProvider ( http , provider , params , ctx )
2026-04-10 15:30:38 +00:00
return {
output : result ? ? "No search results found. Please try a different query." ,
2026-05-08 08:49:36 +00:00
title : ` ${ title } : ${ params . query } ` ,
metadata : { provider } ,
2025-11-10 21:39:40 +00:00
}
2026-04-11 02:36:02 +00:00
} ) . pipe ( Effect . orDie ) ,
2026-04-10 15:30:38 +00:00
}
} ) ,
)