Buckets:
| export * as WebSearchTool from "./websearch" | |
| import { ToolFailure } from "@opencode-ai/llm" | |
| import { Context, Duration, Effect, Layer, Schema } from "effect" | |
| import { HttpClient, HttpClientRequest } from "effect/unstable/http" | |
| import { truthy } from "../flag/flag" | |
| import { InstallationVersion } from "../installation/version" | |
| import { PositiveInt } from "../schema" | |
| import { PermissionV2 } from "../permission" | |
| import { Tool } from "./tool" | |
| import { Tools } from "./tools" | |
| import { checksum } from "../util/encode" | |
| export const name = "websearch" | |
| export const NO_RESULTS = "No search results found. Please try a different query." | |
| export const EXA_URL = "https://mcp.exa.ai/mcp" | |
| export const PARALLEL_URL = "https://search.parallel.ai/mcp" | |
| export const MAX_NUM_RESULTS = 20 | |
| export const MAX_CONTEXT_CHARACTERS = 50_000 | |
| export const MAX_RESPONSE_BYTES = 256 * 1024 | |
| /** | |
| * Provider-independent local web search retained in V2 core for launch parity. | |
| * This invokes the legacy Exa/Parallel product backends itself. It is distinct | |
| * from provider-hosted web search tools, which remain route-owned and execute | |
| * at the model provider. Ownership of this compromise can be revisited later. | |
| */ | |
| export const description = `Search the web using the session's local web search provider. Use this for current information beyond knowledge cutoff. | |
| This is a provider-independent local tool backed by Exa or Parallel. Provider-hosted web search tools are separate and execute at the model provider. | |
| Optional controls support result count, live crawling ('fallback' or 'preferred'), search type ('auto', 'fast', or 'deep'), and maximum context characters. | |
| The current year is ${new Date().getFullYear()}. Use this year when searching for recent information or current events.` | |
| export const Input = Schema.Struct({ | |
| query: Schema.String.annotate({ description: "Websearch query" }), | |
| numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({ | |
| description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})`, | |
| }), | |
| livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({ | |
| description: | |
| "Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling (default: 'fallback')", | |
| }), | |
| 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(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate( | |
| { | |
| description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`, | |
| }, | |
| ), | |
| }) | |
| export const Provider = Schema.Literals(["exa", "parallel"]) | |
| export type Provider = typeof Provider.Type | |
| export interface Config { | |
| readonly provider?: Provider | |
| readonly enableExa: boolean | |
| readonly enableParallel: boolean | |
| readonly exaApiKey?: string | |
| readonly parallelApiKey?: string | |
| } | |
| export class ConfigService extends Context.Service<ConfigService, Config>()("@opencode/v2/WebSearchConfig") {} | |
| /** Isolates the retained product environment contract from the generic tool implementation. */ | |
| export const defaultConfigLayer = Layer.sync(ConfigService, () => | |
| ConfigService.of({ | |
| provider: | |
| process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel" | |
| ? process.env.OPENCODE_WEBSEARCH_PROVIDER | |
| : undefined, | |
| enableExa: truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA"), | |
| enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"), | |
| exaApiKey: process.env.EXA_API_KEY, | |
| parallelApiKey: process.env.PARALLEL_API_KEY, | |
| }), | |
| ) | |
| export function selectProvider( | |
| sessionID: string, | |
| flags: Pick<Config, "enableExa" | "enableParallel"> = { enableExa: false, enableParallel: false }, | |
| override?: Provider, | |
| ): Provider { | |
| if (override) return override | |
| if (flags.enableParallel) return "parallel" | |
| if (flags.enableExa) return "exa" | |
| return Number.parseInt(checksum(sessionID) ?? "0", 36) % 2 === 0 ? "exa" : "parallel" | |
| } | |
| const McpResult = Schema.Struct({ | |
| result: Schema.Struct({ | |
| content: Schema.Array(Schema.Struct({ type: Schema.String, text: Schema.String })), | |
| }), | |
| }) | |
| const decodeMcpResult = Schema.decodeUnknownEffect(Schema.fromJsonString(McpResult)) | |
| const parsePayload = (payload: string) => | |
| Effect.gen(function* () { | |
| const trimmed = payload.trim() | |
| if (!trimmed.startsWith("{")) return undefined | |
| return (yield* decodeMcpResult(trimmed)).result.content.find((item) => item.text)?.text | |
| }) | |
| export const parseResponse = Effect.fn("WebSearchTool.parseResponse")(function* (body: string) { | |
| const trimmed = body.trim() | |
| const direct = trimmed ? yield* parsePayload(trimmed) : undefined | |
| if (direct) return direct | |
| for (const line of body.split("\n")) { | |
| if (!line.startsWith("data: ")) continue | |
| const data = yield* parsePayload(line.substring(6)) | |
| if (data) return data | |
| } | |
| return undefined | |
| }) | |
| const ExaArgs = Schema.Struct({ | |
| query: Schema.String, | |
| type: Schema.String, | |
| numResults: Schema.Number, | |
| livecrawl: Schema.String, | |
| contextMaxCharacters: Schema.optional(Schema.Number), | |
| }) | |
| const ParallelArgs = Schema.Struct({ | |
| objective: Schema.String, | |
| search_queries: Schema.Array(Schema.String), | |
| session_id: Schema.String, | |
| }) | |
| const McpRequest = <F extends Schema.Struct.Fields>(args: Schema.Struct<F>) => | |
| Schema.Struct({ | |
| jsonrpc: Schema.Literal("2.0"), | |
| id: Schema.Literal(1), | |
| method: Schema.Literal("tools/call"), | |
| params: Schema.Struct({ name: Schema.String, arguments: args }), | |
| }) | |
| const exaUrl = (apiKey: string | undefined) => { | |
| if (!apiKey) return EXA_URL | |
| const url = new URL(EXA_URL) | |
| url.searchParams.set("exaApiKey", apiKey) | |
| return url.toString() | |
| } | |
| const callMcp = <F extends Schema.Struct.Fields>( | |
| http: HttpClient.HttpClient, | |
| url: string, | |
| tool: string, | |
| args: Schema.Struct<F>, | |
| value: Schema.Struct.Type<F>, | |
| headers: Record<string, string> = {}, | |
| ) => | |
| Effect.gen(function* () { | |
| const request = yield* HttpClientRequest.post(url).pipe( | |
| HttpClientRequest.accept("application/json, text/event-stream"), | |
| HttpClientRequest.setHeaders(headers), | |
| HttpClientRequest.schemaBodyJson(McpRequest(args))({ | |
| jsonrpc: "2.0" as const, | |
| id: 1 as const, | |
| method: "tools/call" as const, | |
| params: { name: tool, arguments: value }, | |
| }), | |
| ) | |
| return yield* Effect.gen(function* () { | |
| const response = yield* HttpClient.filterStatusOk(http).execute(request) | |
| const body = yield* response.text | |
| if (Buffer.byteLength(body, "utf8") > MAX_RESPONSE_BYTES) | |
| return yield* Effect.fail(new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`)) | |
| return yield* parseResponse(body) | |
| }).pipe( | |
| Effect.timeoutOrElse({ | |
| duration: Duration.seconds(25), | |
| orElse: () => Effect.fail(new Error(`${tool} request timed out`)), | |
| }), | |
| ) | |
| }) | |
| const Output = Schema.Struct({ | |
| provider: Provider, | |
| text: Schema.String, | |
| }) | |
| export const layer = Layer.effectDiscard( | |
| Effect.gen(function* () { | |
| const tools = yield* Tools.Service | |
| const http = yield* HttpClient.HttpClient | |
| const config = yield* ConfigService | |
| const permission = yield* PermissionV2.Service | |
| yield* tools | |
| .register({ | |
| [name]: Tool.make({ | |
| description, | |
| input: Input, | |
| output: Output, | |
| toModelOutput: ({ output }) => [{ type: "text", text: output.text }], | |
| execute: (input, context) => { | |
| const provider = selectProvider(context.sessionID, config, config.provider) | |
| return Effect.gen(function* () { | |
| yield* permission.assert({ | |
| action: name, | |
| resources: [input.query], | |
| save: ["*"], | |
| metadata: { ...input, provider }, | |
| sessionID: context.sessionID, | |
| agent: context.agent, | |
| source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, | |
| }) | |
| const text = | |
| provider === "exa" | |
| ? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, { | |
| query: input.query, | |
| type: input.type || "auto", | |
| numResults: input.numResults || 8, | |
| livecrawl: input.livecrawl || "fallback", | |
| contextMaxCharacters: input.contextMaxCharacters, | |
| }) | |
| : yield* callMcp( | |
| http, | |
| PARALLEL_URL, | |
| "web_search", | |
| ParallelArgs, | |
| { | |
| objective: input.query, | |
| search_queries: [input.query], | |
| session_id: context.sessionID, | |
| // V2 invocation context does not safely expose the model yet. | |
| }, | |
| { | |
| "User-Agent": `opencode/${InstallationVersion}`, | |
| ...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}), | |
| }, | |
| ) | |
| return { | |
| provider, | |
| text: text ?? NO_RESULTS, | |
| } | |
| }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to search the web for ${input.query}` }))) | |
| }, | |
| }), | |
| }) | |
| .pipe(Effect.orDie) | |
| }), | |
| ) | |
Xet Storage Details
- Size:
- 9.83 kB
- Xet hash:
- f952adc637cd9ebae463c47139969ff638cb92104358b3f9762e8704a760fff8
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.