|
|
@@ -2,7 +2,7 @@ export * as WebSearchTool from "./websearch"
|
|
|
|
|
|
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
|
|
import { ToolFailure } from "@opencode-ai/ai"
|
|
|
-import { Effect, Schema } from "effect"
|
|
|
+import { Effect, Schema, Semaphore } from "effect"
|
|
|
import { Form } from "../../form"
|
|
|
import { KV } from "../../kv"
|
|
|
import { Permission } from "../../permission"
|
|
|
@@ -10,6 +10,7 @@ import { WebSearch } from "../../websearch"
|
|
|
|
|
|
export const name = "websearch"
|
|
|
export const NO_RESULTS = "No search results found. Please try a different query."
|
|
|
+const providerSelectionLock = Semaphore.makeUnsafe(1)
|
|
|
|
|
|
export const description = `Search the web using the user's selected search integration. Use this for current information beyond knowledge cutoff.
|
|
|
|
|
|
@@ -29,6 +30,7 @@ export const Plugin = {
|
|
|
const permission = yield* Permission.Service
|
|
|
const forms = yield* Form.Service
|
|
|
const kv = yield* KV.Service
|
|
|
+ const websearch = yield* WebSearch.Service
|
|
|
|
|
|
yield* ctx.tool
|
|
|
.transform((draft) =>
|
|
|
@@ -49,70 +51,90 @@ export const Plugin = {
|
|
|
agent: context.agent,
|
|
|
source: { type: "tool", messageID: context.messageID, id: context.id },
|
|
|
})
|
|
|
- const result = yield* ctx.websearch.query(input).pipe(
|
|
|
- Effect.catch((error) => {
|
|
|
- if (!Schema.is(WebSearch.ProviderRequiredError)(error)) return Effect.fail(error)
|
|
|
- return Effect.gen(function* () {
|
|
|
- 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 web search via ${defaultProvider.name}`,
|
|
|
- },
|
|
|
- {
|
|
|
- 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* kv.set("websearch:provider", false)
|
|
|
- return yield* new WebSearch.DisabledError()
|
|
|
- }
|
|
|
- const selection =
|
|
|
- response.answer.choice === "choose"
|
|
|
- ? yield* forms.ask({
|
|
|
+ const search = (): Effect.Effect<Effect.Success<ReturnType<typeof ctx.websearch.query>>, unknown> =>
|
|
|
+ ctx.websearch.query(input).pipe(
|
|
|
+ Effect.catch((error) => {
|
|
|
+ if (!Schema.is(WebSearch.ProviderRequiredError)(error)) return Effect.fail(error)
|
|
|
+ return providerSelectionLock
|
|
|
+ .withPermit(
|
|
|
+ Effect.gen(function* () {
|
|
|
+ if (yield* websearch.default()) return yield* Effect.void
|
|
|
+ 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: "Choose a web search provider",
|
|
|
+ title: "Web Search",
|
|
|
metadata: { kind: "websearch.provider" },
|
|
|
fields: [
|
|
|
{
|
|
|
- key: "provider",
|
|
|
- description: "Choose a provider for web search.",
|
|
|
+ key: "choice",
|
|
|
+ description: "Allow OpenCode to search the web for up-to-date information?",
|
|
|
type: "string",
|
|
|
required: true,
|
|
|
custom: false,
|
|
|
- options: providers.map((provider) => ({ value: provider.id, label: provider.name })),
|
|
|
+ options: [
|
|
|
+ {
|
|
|
+ value: "allow",
|
|
|
+ label: `Allow web search via ${defaultProvider.name}`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "choose",
|
|
|
+ label: "Choose another provider",
|
|
|
+ },
|
|
|
+ { value: "disable", label: "Disable web search" },
|
|
|
+ ],
|
|
|
},
|
|
|
],
|
|
|
})
|
|
|
- : undefined
|
|
|
- if (selection?.status === "cancelled") return yield* Effect.fail(new Error("Web search cancelled"))
|
|
|
- const providerID = selection?.answer.provider ?? defaultProvider.id
|
|
|
- if (typeof providerID !== "string" || !providers.some((provider) => provider.id === providerID))
|
|
|
- return yield* new WebSearch.ProviderRequiredError()
|
|
|
- yield* kv.set("websearch:provider", providerID)
|
|
|
- return yield* ctx.websearch.query(input)
|
|
|
- })
|
|
|
- }),
|
|
|
- )
|
|
|
+ if (response.status === "cancelled")
|
|
|
+ return yield* Effect.fail(new Error("Web search cancelled"))
|
|
|
+ if (response.answer.choice === "disable") {
|
|
|
+ yield* kv.set("websearch:provider", 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 ?? defaultProvider.id
|
|
|
+ if (
|
|
|
+ typeof providerID !== "string" ||
|
|
|
+ !providers.some((provider) => provider.id === providerID)
|
|
|
+ )
|
|
|
+ return yield* new WebSearch.ProviderRequiredError()
|
|
|
+ return yield* kv.set("websearch:provider", providerID)
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ .pipe(
|
|
|
+ Effect.timeoutOrElse({
|
|
|
+ duration: "1 minute",
|
|
|
+ orElse: () => Effect.fail(new Error("Web search cancelled")),
|
|
|
+ }),
|
|
|
+ Effect.andThen(Effect.suspend(search)),
|
|
|
+ )
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ const result = yield* search()
|
|
|
const output = {
|
|
|
provider: result.data.providerID,
|
|
|
results: result.data.results,
|