Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197export * as WebSearchTool from "./websearch.js"
import type { Context } from "@opencode/plugin/effect/plugin"import type { SessionHooks } from "@opencode/plugin/effect/session"import { ToolFailure } from "@opencode/ai"import { Effect, Schema, Semaphore } from "effect"import { HttpClientError } from "effect/unstable/http"import { Form } from "../../form.js"import { Permission } from "../../permission.js"import { WebSearch } from "../../websearch.js"
export const name = "websearch"export const NO_RESULTS = "No search results found. Please try a different query."const providerSelectionLock = Semaphore.makeUnsafe(1)const httpErrors = new Map([ [429, "Web search rate limited (HTTP 429)"], [401, "Web search authentication failed (HTTP 401)"],])
export const description = `Search the web using the user's selected search integration. Use this for current information beyond knowledge cutoff.
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" }),})
const Output = Schema.Struct({ provider: WebSearch.ID, results: Schema.Array(WebSearch.Result),})export const Plugin = { id: "opencode.tool.websearch", effect: Effect.fn("WebSearchTool.Plugin")(function* (ctx: Context) { const permission = yield* Permission.Service const forms = yield* Form.Service const websearch = yield* WebSearch.Service
yield* ctx.tool .transform((editor) => editor.add({ name, options: { codemode: false }, description, input: Input, output: Output, execute: (input, context) => Effect.gen(function* () { yield* permission.assert({ action: name, resources: [input.query], save: ["*"], metadata: input, sessionID: context.sessionID, agent: context.agent, source: { type: "tool", messageID: context.messageID, id: context.id }, }) const search = (providerID?: WebSearch.ID) => websearch.query( { ...input, providerID }, { sessionID: context.sessionID, onProvider: (provider) => context.progress({ provider: provider.id }), }, ) const result = yield* search().pipe( Effect.catchTag("WebSearch.ProviderRequired", () => { return providerSelectionLock .withPermit( Effect.gen(function* () { if (yield* websearch.default()) return const providers = (yield* ctx.websearch.providers()).data const defaultProvider = providers[0] if (!defaultProvider) return yield* new WebSearch.ProviderRequiredError() const response = yield* forms.ask({ sessionID: context.sessionID, title: "Web Search", metadata: { kind: "websearch.provider" }, fields: [ { key: "choice", description: "Allow OpenCode to search the web for up-to-date information?", type: "string", required: true, custom: false, options: [ { value: "allow", label: `Allow search via ${providers.map((provider) => provider.name).join(", ")}`, }, { value: "choose", label: "Choose another provider", }, { value: "disable", label: "Disable web search" }, ], }, ], }) if (response.status === "cancelled") return yield* Effect.fail(new Error("Web search cancelled")) if (response.answer.choice === "disable") { yield* websearch.select(false) return yield* new WebSearch.DisabledError() } const selection = response.answer.choice === "choose" ? yield* forms.ask({ sessionID: context.sessionID, title: "Choose a web search provider", metadata: { kind: "websearch.provider" }, fields: [ { key: "provider", description: "Choose a provider for web search.", type: "string", required: true, custom: false, options: providers.map((provider) => ({ value: provider.id, label: provider.name, })), }, ], }) : undefined if (selection?.status === "cancelled") return yield* Effect.fail(new Error("Web search cancelled")) const providerID = selection?.answer.provider ?? "random" if (providerID === "random") { yield* websearch.select("random") return } const provider = providers.find((provider) => provider.id === providerID) if (!provider) return yield* new WebSearch.ProviderRequiredError() yield* websearch.select(provider.id) return provider.id }), ) .pipe( Effect.timeoutOrElse({ duration: "1 minute", orElse: () => Effect.fail(new Error("Web search cancelled")), }), Effect.flatMap(search), ) }), ) const output = { provider: result.providerID, results: result.results, } const content = output.results.length ? output.results .map((result) => { const title = result.title ?? result.url const published = result.time.published ? `\nPublished: ${new Date(result.time.published).toISOString()}` : "" return `## [${title}](${result.url})${published}${result.content ? `\n\n${result.content}` : ""}` }) .join("\n\n") : NO_RESULTS return { output, content, metadata: { provider: output.provider } } }).pipe( Effect.mapError((error) => { const fallback = `Unable to search the web for ${input.query}` if (!Schema.is(WebSearch.RequestError)(error)) return new ToolFailure({ message: fallback, error }) const status = HttpClientError.isHttpClientError(error.cause) ? error.cause.response?.status : undefined return new ToolFailure({ message: status === undefined ? fallback : (httpErrors.get(status) ?? `Web search request failed (HTTP ${status})`), error, metadata: { provider: error.providerID }, }) }), ), }), ) .pipe(Effect.orDie)
const hook = (event: SessionHooks["context"]) => Effect.gen(function* () { const disabled = yield* websearch.default().pipe( Effect.as(false), Effect.catchTag("WebSearch.Disabled", () => Effect.succeed(true)), ) if (disabled) delete event.tools[name] }) yield* ctx.session.hook("context", hook) yield* ctx.session.hook("compaction", hook) yield* ctx.session.hook("generate", hook) }),}