export * as WebSearch from "./websearch" import { WebSearch } from "@opencode-ai/schema/websearch" import { Context, Effect, Layer, Schema } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Bus } from "./bus" import { KV } from "./kv" import { State } from "./state" export const ID = WebSearch.ID export type ID = WebSearch.ID export const Provider = WebSearch.Provider export type Provider = WebSearch.Provider export { Event } from "@opencode-ai/schema/websearch" export const Input = WebSearch.Input export type Input = WebSearch.Input export type ProviderInput = WebSearch.ProviderInput export const Result = WebSearch.Result export type Result = WebSearch.Result export const Response = WebSearch.Response export type Response = WebSearch.Response export interface ProviderImplementation extends Provider { readonly execute: (input: ProviderInput) => Effect.Effect } export class ProviderRequiredError extends Schema.TaggedErrorClass()( "WebSearch.ProviderRequired", {}, ) {} export class ProviderNotFoundError extends Schema.TaggedErrorClass()( "WebSearch.ProviderNotFound", { providerID: ID, }, ) {} export class DisabledError extends Schema.TaggedErrorClass()("WebSearch.Disabled", {}) {} export class RequestError extends Schema.TaggedErrorClass()("WebSearch.Request", { providerID: ID, cause: Schema.Defect(), }) {} export type Error = ProviderRequiredError | ProviderNotFoundError | DisabledError | RequestError export interface Interface extends State.Transformable { readonly providers: () => Effect.Effect readonly default: () => Effect.Effect readonly query: (input: Input) => Effect.Effect } export class Service extends Context.Service()("@opencode/WebSearch") {} type Data = { readonly providers: Map defaultProviderID?: ID } export type Draft = { add: (provider: ProviderImplementation) => void default: { get: () => ID | undefined set: (providerID: ID) => void } } const layer = Layer.effect( Service, Effect.gen(function* () { const bus = yield* Bus.Service const kv = yield* KV.Service const decodeResults = Schema.decodeUnknownEffect(Schema.Array(Result)) const state = State.create({ initial: () => ({ providers: new Map() }), draft: (draft) => ({ add: (provider) => draft.providers.set(provider.id, provider), default: { get: () => draft.defaultProviderID, set: (providerID) => (draft.defaultProviderID = providerID), }, }), finalize: () => bus.publish(WebSearch.Event.Updated, {}).pipe(Effect.asVoid), }) const requireProvider = (providers: Map, providerID: ID) => { const provider = providers.get(providerID) return provider ? Effect.succeed(provider) : Effect.fail(new ProviderNotFoundError({ providerID })) } const defaultProvider = Effect.fn("WebSearch.default")(function* () { const data = state.get() const configured = data.defaultProviderID ? data.providers.get(data.defaultProviderID) : undefined if (configured) return configured const stored = yield* kv.get("websearch:provider") if (stored === false) return yield* new DisabledError() if (typeof stored !== "string") return return data.providers.get(ID.make(stored)) }) const resolve = Effect.fn("WebSearch.resolve")(function* (input: Input) { const providers = state.get().providers if (input.providerID) return yield* requireProvider(providers, input.providerID) const provider = yield* defaultProvider() if (!provider) return yield* new ProviderRequiredError() return provider }) return Service.of({ transform: state.transform, reload: state.reload, providers: Effect.fn("WebSearch.providers")(function* () { return Array.from(state.get().providers.values(), (provider) => ({ id: provider.id, name: provider.name, })).toSorted((a, b) => a.name.localeCompare(b.name)) }), default: Effect.fn("WebSearch.defaultInfo")(function* () { const provider = yield* defaultProvider() return provider && { id: provider.id, name: provider.name } }), query: Effect.fn("WebSearch.query")(function* (input) { const provider = yield* resolve(input) const results = yield* provider.execute({ query: input.query }).pipe( Effect.flatMap(decodeResults), Effect.mapError((cause) => new RequestError({ providerID: provider.id, cause })), ) return new Response({ providerID: provider.id, results }) }), }) }), ) export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, KV.node], })