mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 09:28:27 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4af6f98d82 | |||
| 87e8690090 |
@@ -55,7 +55,7 @@ export const Plugin = {
|
|||||||
const search = (): Effect.Effect<Effect.Success<ReturnType<typeof ctx.websearch.query>>, unknown> =>
|
const search = (): Effect.Effect<Effect.Success<ReturnType<typeof ctx.websearch.query>>, unknown> =>
|
||||||
websearch.default().pipe(
|
websearch.default().pipe(
|
||||||
Effect.flatMap((provider) => {
|
Effect.flatMap((provider) => {
|
||||||
if (!provider) return ctx.websearch.query(input)
|
if (!provider || provider === WebSearch.AUTO) return ctx.websearch.query(input)
|
||||||
return context
|
return context
|
||||||
.progress({ provider: provider.id })
|
.progress({ provider: provider.id })
|
||||||
.pipe(Effect.andThen(ctx.websearch.query({ ...input, providerID: provider.id })))
|
.pipe(Effect.andThen(ctx.websearch.query({ ...input, providerID: provider.id })))
|
||||||
@@ -67,8 +67,7 @@ export const Plugin = {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (yield* websearch.default()) return yield* Effect.void
|
if (yield* websearch.default()) return yield* Effect.void
|
||||||
const providers = (yield* ctx.websearch.providers()).data
|
const providers = (yield* ctx.websearch.providers()).data
|
||||||
const defaultProvider = providers[0]
|
if (!providers.length) return yield* new WebSearch.ProviderRequiredError()
|
||||||
if (!defaultProvider) return yield* new WebSearch.ProviderRequiredError()
|
|
||||||
const response = yield* forms.ask({
|
const response = yield* forms.ask({
|
||||||
sessionID: context.sessionID,
|
sessionID: context.sessionID,
|
||||||
title: "Web Search",
|
title: "Web Search",
|
||||||
@@ -83,11 +82,11 @@ export const Plugin = {
|
|||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: "allow",
|
value: "allow",
|
||||||
label: `Allow web search via ${defaultProvider.name}`,
|
label: "Allow web search",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "choose",
|
value: "choose",
|
||||||
label: "Choose another provider",
|
label: "Choose a specific provider",
|
||||||
},
|
},
|
||||||
{ value: "disable", label: "Disable web search" },
|
{ value: "disable", label: "Disable web search" },
|
||||||
],
|
],
|
||||||
@@ -123,10 +122,10 @@ export const Plugin = {
|
|||||||
: undefined
|
: undefined
|
||||||
if (selection?.status === "cancelled")
|
if (selection?.status === "cancelled")
|
||||||
return yield* Effect.fail(new Error("Web search cancelled"))
|
return yield* Effect.fail(new Error("Web search cancelled"))
|
||||||
const providerID = selection?.answer.provider ?? defaultProvider.id
|
const providerID = selection?.answer.provider ?? WebSearch.AUTO
|
||||||
if (
|
if (
|
||||||
typeof providerID !== "string" ||
|
typeof providerID !== "string" ||
|
||||||
!providers.some((provider) => provider.id === providerID)
|
(providerID !== WebSearch.AUTO && !providers.some((provider) => provider.id === providerID))
|
||||||
)
|
)
|
||||||
return yield* new WebSearch.ProviderRequiredError()
|
return yield* new WebSearch.ProviderRequiredError()
|
||||||
return yield* kv.set("websearch:provider", providerID)
|
return yield* kv.set("websearch:provider", providerID)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as WebSearch from "./websearch.js"
|
export * as WebSearch from "./websearch.js"
|
||||||
|
|
||||||
import { WebSearch } from "@opencode-ai/schema/websearch"
|
import { WebSearch } from "@opencode-ai/schema/websearch"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Random, Schema } from "effect"
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
import { Bus } from "./bus.js"
|
import { Bus } from "./bus.js"
|
||||||
import { KV } from "./kv.js"
|
import { KV } from "./kv.js"
|
||||||
@@ -10,6 +10,8 @@ import { State } from "./state.js"
|
|||||||
export const ID = WebSearch.ID
|
export const ID = WebSearch.ID
|
||||||
export type ID = WebSearch.ID
|
export type ID = WebSearch.ID
|
||||||
|
|
||||||
|
export const AUTO = WebSearch.AUTO
|
||||||
|
|
||||||
export const Provider = WebSearch.Provider
|
export const Provider = WebSearch.Provider
|
||||||
export type Provider = WebSearch.Provider
|
export type Provider = WebSearch.Provider
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ export type Error = ProviderRequiredError | ProviderNotFoundError | DisabledErro
|
|||||||
|
|
||||||
export interface Interface extends State.Transformable<Draft> {
|
export interface Interface extends State.Transformable<Draft> {
|
||||||
readonly providers: () => Effect.Effect<readonly Provider[]>
|
readonly providers: () => Effect.Effect<readonly Provider[]>
|
||||||
readonly default: () => Effect.Effect<Provider | undefined, DisabledError>
|
readonly default: () => Effect.Effect<Provider | typeof AUTO | undefined, DisabledError>
|
||||||
readonly query: (input: Input) => Effect.Effect<Response, Error>
|
readonly query: (input: Input) => Effect.Effect<Response, Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +62,14 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/We
|
|||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
readonly providers: Map<ID, ProviderImplementation>
|
readonly providers: Map<ID, ProviderImplementation>
|
||||||
defaultProviderID?: ID
|
defaultProviderID?: WebSearch.Selection
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Draft = {
|
export type Draft = {
|
||||||
add: (provider: ProviderImplementation) => void
|
add: (provider: ProviderImplementation) => void
|
||||||
default: {
|
default: {
|
||||||
get: () => ID | undefined
|
get: () => WebSearch.Selection | undefined
|
||||||
set: (providerID: ID) => void
|
set: (providerID: WebSearch.Selection) => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +98,13 @@ const layer = Layer.effect(
|
|||||||
|
|
||||||
const defaultProvider = Effect.fn("WebSearch.default")(function* () {
|
const defaultProvider = Effect.fn("WebSearch.default")(function* () {
|
||||||
const data = state.get()
|
const data = state.get()
|
||||||
|
if (data.defaultProviderID === AUTO) return AUTO
|
||||||
const configured = data.defaultProviderID ? data.providers.get(data.defaultProviderID) : undefined
|
const configured = data.defaultProviderID ? data.providers.get(data.defaultProviderID) : undefined
|
||||||
if (configured) return configured
|
if (configured) return configured
|
||||||
const stored = yield* kv.get("websearch:provider")
|
const stored = yield* kv.get("websearch:provider")
|
||||||
if (stored === false) return yield* new DisabledError()
|
if (stored === false) return yield* new DisabledError()
|
||||||
if (typeof stored !== "string") return
|
if (typeof stored !== "string") return
|
||||||
|
if (stored === AUTO) return AUTO
|
||||||
return data.providers.get(ID.make(stored))
|
return data.providers.get(ID.make(stored))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -109,9 +113,21 @@ const layer = Layer.effect(
|
|||||||
if (input.providerID) return yield* requireProvider(providers, input.providerID)
|
if (input.providerID) return yield* requireProvider(providers, input.providerID)
|
||||||
const provider = yield* defaultProvider()
|
const provider = yield* defaultProvider()
|
||||||
if (!provider) return yield* new ProviderRequiredError()
|
if (!provider) return yield* new ProviderRequiredError()
|
||||||
|
if (provider === AUTO) {
|
||||||
|
if (!providers.size) return yield* new ProviderRequiredError()
|
||||||
|
return yield* Random.shuffle(providers.values())
|
||||||
|
}
|
||||||
return provider
|
return provider
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const execute = Effect.fn("WebSearch.execute")(function* (provider: ProviderImplementation, input: 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 })
|
||||||
|
})
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
reload: state.reload,
|
reload: state.reload,
|
||||||
@@ -123,15 +139,13 @@ const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
default: Effect.fn("WebSearch.defaultInfo")(function* () {
|
default: Effect.fn("WebSearch.defaultInfo")(function* () {
|
||||||
const provider = yield* defaultProvider()
|
const provider = yield* defaultProvider()
|
||||||
|
if (provider === AUTO) return AUTO
|
||||||
return provider && { id: provider.id, name: provider.name }
|
return provider && { id: provider.id, name: provider.name }
|
||||||
}),
|
}),
|
||||||
query: Effect.fn("WebSearch.query")(function* (input) {
|
query: Effect.fn("WebSearch.query")(function* (input) {
|
||||||
const provider = yield* resolve(input)
|
const route = yield* resolve(input)
|
||||||
const results = yield* provider.execute({ query: input.query }).pipe(
|
if (Array.isArray(route)) return yield* Effect.firstSuccessOf(route.map((provider) => execute(provider, input)))
|
||||||
Effect.flatMap(decodeResults),
|
return yield* execute(route, input)
|
||||||
Effect.mapError((cause) => new RequestError({ providerID: provider.id, cause })),
|
|
||||||
)
|
|
||||||
return new Response({ providerID: provider.id, results })
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const websearch = Layer.succeed(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const stored = values.get("websearch:provider")
|
const stored = values.get("websearch:provider")
|
||||||
if (stored === false) return yield* new WebSearch.DisabledError()
|
if (stored === false) return yield* new WebSearch.DisabledError()
|
||||||
|
if (stored === WebSearch.AUTO) return WebSearch.AUTO
|
||||||
return typeof stored === "string" ? providers.find((provider) => provider.id === stored) : undefined
|
return typeof stored === "string" ? providers.find((provider) => provider.id === stored) : undefined
|
||||||
}),
|
}),
|
||||||
query: (input) =>
|
query: (input) =>
|
||||||
@@ -93,6 +94,7 @@ const websearch = Layer.succeed(
|
|||||||
}
|
}
|
||||||
if (queryError) return yield* queryError
|
if (queryError) return yield* queryError
|
||||||
if (providerRequired && typeof stored !== "string") return yield* new WebSearch.ProviderRequiredError()
|
if (providerRequired && typeof stored !== "string") return yield* new WebSearch.ProviderRequiredError()
|
||||||
|
if (stored === WebSearch.AUTO) return result
|
||||||
if (typeof stored === "string")
|
if (typeof stored === "string")
|
||||||
return new WebSearch.Response({ providerID: WebSearch.ID.make(stored), results: result.results })
|
return new WebSearch.Response({ providerID: WebSearch.ID.make(stored), results: result.results })
|
||||||
return result
|
return result
|
||||||
@@ -234,7 +236,7 @@ describe("WebSearchTool registration", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("asks once and uses the default provider when web search is first enabled", () =>
|
it.effect("asks once and enables automatic provider selection", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
providerRequired = true
|
providerRequired = true
|
||||||
formResponse = { status: "answered", answer: { choice: "allow" } }
|
formResponse = { status: "answered", answer: { choice: "allow" } }
|
||||||
@@ -247,7 +249,7 @@ describe("WebSearchTool registration", () => {
|
|||||||
call: { type: "tool-call", id: "call-enable", name: "websearch", input: { query: "effect" } },
|
call: { type: "tool-call", id: "call-enable", name: "websearch", input: { query: "effect" } },
|
||||||
}),
|
}),
|
||||||
).toMatchObject({ status: "completed", metadata: { provider: "exa" } })
|
).toMatchObject({ status: "completed", metadata: { provider: "exa" } })
|
||||||
expect(values.get("websearch:provider")).toBe("exa")
|
expect(values.get("websearch:provider")).toBe(WebSearch.AUTO)
|
||||||
expect(queries).toHaveLength(2)
|
expect(queries).toHaveLength(2)
|
||||||
expect(formRequests).toEqual([
|
expect(formRequests).toEqual([
|
||||||
{
|
{
|
||||||
@@ -264,11 +266,11 @@ describe("WebSearchTool registration", () => {
|
|||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: "allow",
|
value: "allow",
|
||||||
label: "Allow web search via Exa",
|
label: "Allow web search",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "choose",
|
value: "choose",
|
||||||
label: "Choose another provider",
|
label: "Choose a specific provider",
|
||||||
},
|
},
|
||||||
{ value: "disable", label: "Disable web search" },
|
{ value: "disable", label: "Disable web search" },
|
||||||
],
|
],
|
||||||
@@ -353,7 +355,7 @@ describe("WebSearchTool registration", () => {
|
|||||||
|
|
||||||
expect(results.every((item) => item.status === "completed")).toBe(true)
|
expect(results.every((item) => item.status === "completed")).toBe(true)
|
||||||
expect(formRequests).toHaveLength(1)
|
expect(formRequests).toHaveLength(1)
|
||||||
expect(values.get("websearch:provider")).toBe("exa")
|
expect(values.get("websearch:provider")).toBe(WebSearch.AUTO)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Exit, Scope } from "effect"
|
import { Effect, Exit, Random, Scope } from "effect"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||||
import { Bus } from "@opencode-ai/core/bus"
|
import { Bus } from "@opencode-ai/core/bus"
|
||||||
@@ -9,7 +9,7 @@ import { testEffect } from "./lib/effect"
|
|||||||
|
|
||||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([WebSearch.node, Bus.node, KV.node])))
|
const it = testEffect(AppNodeBuilder.build(LayerNode.group([WebSearch.node, Bus.node, KV.node])))
|
||||||
|
|
||||||
const register = (id: string) =>
|
const register = (id: string, behavior: "results" | "empty" | "fail" = "results") =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const websearch = yield* WebSearch.Service
|
const websearch = yield* WebSearch.Service
|
||||||
const providerID = WebSearch.ID.make(id)
|
const providerID = WebSearch.ID.make(id)
|
||||||
@@ -19,17 +19,24 @@ const register = (id: string) =>
|
|||||||
id: providerID,
|
id: providerID,
|
||||||
name: id.toUpperCase(),
|
name: id.toUpperCase(),
|
||||||
execute: (input) =>
|
execute: (input) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => calls.push(input)).pipe(
|
||||||
calls.push(input)
|
Effect.andThen(
|
||||||
return [
|
behavior === "fail"
|
||||||
{
|
? Effect.fail(new Error(`${id} failed`))
|
||||||
url: `https://${id}.example.com`,
|
: Effect.succeed(
|
||||||
title: input.query,
|
behavior === "empty"
|
||||||
content: `${id}: ${input.query}`,
|
? []
|
||||||
time: {},
|
: [
|
||||||
},
|
{
|
||||||
]
|
url: `https://${id}.example.com`,
|
||||||
}),
|
title: input.query,
|
||||||
|
content: `${id}: ${input.query}`,
|
||||||
|
time: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return { providerID, calls }
|
return { providerID, calls }
|
||||||
@@ -60,6 +67,21 @@ describe("WebSearch", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("keeps explicit providers strict when automatic selection is enabled", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const exa = yield* register("exa", "fail")
|
||||||
|
const parallel = yield* register("parallel")
|
||||||
|
const websearch = yield* WebSearch.Service
|
||||||
|
yield* websearch.transform((draft) => draft.default.set(WebSearch.AUTO))
|
||||||
|
|
||||||
|
const error = yield* websearch.query({ query: "strict", providerID: exa.providerID }).pipe(Effect.flip)
|
||||||
|
|
||||||
|
expect(error).toMatchObject({ _tag: "WebSearch.Request", providerID: exa.providerID })
|
||||||
|
expect(exa.calls).toEqual([{ query: "strict" }])
|
||||||
|
expect(parallel.calls).toEqual([])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("requires a provider when no default is set", () =>
|
it.effect("requires a provider when no default is set", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* register("exa")
|
yield* register("exa")
|
||||||
@@ -81,6 +103,20 @@ describe("WebSearch", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("keeps fixed configured providers strict", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const exa = yield* register("exa", "fail")
|
||||||
|
const parallel = yield* register("parallel")
|
||||||
|
const websearch = yield* WebSearch.Service
|
||||||
|
yield* websearch.transform((draft) => draft.default.set(exa.providerID))
|
||||||
|
|
||||||
|
const error = yield* websearch.query({ query: "configured" }).pipe(Effect.flip)
|
||||||
|
|
||||||
|
expect(error).toMatchObject({ _tag: "WebSearch.Request", providerID: exa.providerID })
|
||||||
|
expect(parallel.calls).toEqual([])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("uses the provider stored in KV", () =>
|
it.effect("uses the provider stored in KV", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* register("exa")
|
yield* register("exa")
|
||||||
@@ -94,6 +130,118 @@ describe("WebSearch", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("keeps fixed KV providers strict", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const exa = yield* register("exa", "fail")
|
||||||
|
const parallel = yield* register("parallel")
|
||||||
|
const websearch = yield* WebSearch.Service
|
||||||
|
const kv = yield* KV.Service
|
||||||
|
yield* kv.set("websearch:provider", exa.providerID)
|
||||||
|
|
||||||
|
const error = yield* websearch.query({ query: "fixed" }).pipe(Effect.flip)
|
||||||
|
|
||||||
|
expect(error).toMatchObject({ _tag: "WebSearch.Request", providerID: exa.providerID })
|
||||||
|
expect(parallel.calls).toEqual([])
|
||||||
|
yield* kv.remove("websearch:provider")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("automatically tries each provider at most once until one succeeds", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const order = yield* Random.shuffle(["exa", "parallel", "firecrawl"]).pipe(Random.withSeed("fallback"))
|
||||||
|
const registered = yield* Effect.forEach(["exa", "parallel", "firecrawl"], (id) =>
|
||||||
|
register(id, id === order.at(-1) ? "results" : "fail"),
|
||||||
|
)
|
||||||
|
const websearch = yield* WebSearch.Service
|
||||||
|
const kv = yield* KV.Service
|
||||||
|
yield* kv.set("websearch:provider", WebSearch.AUTO)
|
||||||
|
|
||||||
|
const response = yield* websearch.query({ query: "automatic" }).pipe(Random.withSeed("fallback"))
|
||||||
|
|
||||||
|
expect(response.providerID).toBe(WebSearch.ID.make(order.at(-1)!))
|
||||||
|
expect(registered.flatMap((provider) => provider.calls)).toHaveLength(3)
|
||||||
|
expect(registered.every((provider) => provider.calls.length === 1)).toBe(true)
|
||||||
|
yield* kv.remove("websearch:provider")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("stops automatic fallback on empty results", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const empty = yield* register("empty", "empty")
|
||||||
|
const fallback = yield* register("fallback")
|
||||||
|
const websearch = yield* WebSearch.Service
|
||||||
|
yield* websearch.transform((draft) => draft.default.set(WebSearch.AUTO))
|
||||||
|
|
||||||
|
const response = yield* websearch.query({ query: "empty" }).pipe(Random.withSeed("empty-first"))
|
||||||
|
|
||||||
|
expect(response.results).toEqual([])
|
||||||
|
expect(empty.calls).toHaveLength(1)
|
||||||
|
expect(fallback.calls).toHaveLength(0)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("returns the final request error when every automatic provider fails", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const exa = yield* register("exa", "fail")
|
||||||
|
const parallel = yield* register("parallel", "fail")
|
||||||
|
const websearch = yield* WebSearch.Service
|
||||||
|
yield* websearch.transform((draft) => draft.default.set(WebSearch.AUTO))
|
||||||
|
const expected = yield* Random.shuffle([exa.providerID, parallel.providerID]).pipe(Random.withSeed("all-fail"))
|
||||||
|
|
||||||
|
const error = yield* websearch.query({ query: "failure" }).pipe(Random.withSeed("all-fail"), Effect.flip)
|
||||||
|
|
||||||
|
expect(error).toMatchObject({ _tag: "WebSearch.Request", providerID: expected.at(-1) })
|
||||||
|
expect(exa.calls).toHaveLength(1)
|
||||||
|
expect(parallel.calls).toHaveLength(1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("supports zero and one registered provider in automatic mode", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const websearch = yield* WebSearch.Service
|
||||||
|
yield* websearch.transform((draft) => draft.default.set(WebSearch.AUTO))
|
||||||
|
expect((yield* websearch.query({ query: "zero" }).pipe(Effect.flip))._tag).toBe("WebSearch.ProviderRequired")
|
||||||
|
|
||||||
|
const only = yield* register("only")
|
||||||
|
expect((yield* websearch.query({ query: "one" })).providerID).toBe(only.providerID)
|
||||||
|
expect(only.calls).toHaveLength(1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("lets an automatic configured default override a fixed KV provider", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fixed = yield* register("fixed", "fail")
|
||||||
|
const fallback = yield* register("fallback")
|
||||||
|
const websearch = yield* WebSearch.Service
|
||||||
|
const kv = yield* KV.Service
|
||||||
|
yield* kv.set("websearch:provider", fixed.providerID)
|
||||||
|
yield* websearch.transform((draft) => draft.default.set(WebSearch.AUTO))
|
||||||
|
|
||||||
|
expect((yield* websearch.query({ query: "config" })).providerID).toBe(fallback.providerID)
|
||||||
|
expect(fixed.calls.length).toBeLessThanOrEqual(1)
|
||||||
|
expect(fallback.calls).toHaveLength(1)
|
||||||
|
yield* kv.remove("websearch:provider")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("can start automatic selection with any registered provider", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const exa = yield* register("exa")
|
||||||
|
const parallel = yield* register("parallel")
|
||||||
|
const websearch = yield* WebSearch.Service
|
||||||
|
yield* websearch.transform((draft) => draft.default.set(WebSearch.AUTO))
|
||||||
|
|
||||||
|
const selected = yield* Effect.forEach(["a", "b", "c", "d", "e", "f"], (seed) =>
|
||||||
|
websearch.query({ query: seed }).pipe(
|
||||||
|
Random.withSeed(seed),
|
||||||
|
Effect.map((response) => response.providerID),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(new Set(selected)).toEqual(new Set([exa.providerID, parallel.providerID]))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("fails when web search is explicitly disabled", () =>
|
it.effect("fails when web search is explicitly disabled", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* register("exa")
|
yield* register("exa")
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ import { Schema } from "effect"
|
|||||||
import { WebSearch } from "../websearch.js"
|
import { WebSearch } from "../websearch.js"
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("ConfigWebSearch.Info")({
|
export class Info extends Schema.Class<Info>("ConfigWebSearch.Info")({
|
||||||
provider: WebSearch.ID,
|
provider: WebSearch.Selection,
|
||||||
}) {}
|
}) {}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { optional } from "./schema.js"
|
|||||||
export const ID = Schema.String.pipe(Schema.brand("WebSearch.ID"))
|
export const ID = Schema.String.pipe(Schema.brand("WebSearch.ID"))
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
|
export const AUTO = "auto" as const
|
||||||
|
|
||||||
|
export const Selection = Schema.Union([Schema.Literal(AUTO), ID])
|
||||||
|
export type Selection = typeof Selection.Type
|
||||||
|
|
||||||
export interface Provider extends Schema.Schema.Type<typeof Provider> {}
|
export interface Provider extends Schema.Schema.Type<typeof Provider> {}
|
||||||
export const Provider = Schema.Struct({
|
export const Provider = Schema.Struct({
|
||||||
id: ID,
|
id: ID,
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ import { ConfigMCP } from "../src/config/mcp.js"
|
|||||||
import { ConfigProvider } from "../src/config/provider.js"
|
import { ConfigProvider } from "../src/config/provider.js"
|
||||||
import { Mcp } from "../src/mcp.js"
|
import { Mcp } from "../src/mcp.js"
|
||||||
import { AbsolutePath } from "../src/schema.js"
|
import { AbsolutePath } from "../src/schema.js"
|
||||||
|
import { WebSearch } from "../src/websearch.js"
|
||||||
|
|
||||||
describe("Config.Entry", () => {
|
describe("Config.Entry", () => {
|
||||||
|
test("accepts automatic web search provider selection", () => {
|
||||||
|
const config = new Config.Info({ websearch: { provider: WebSearch.AUTO } })
|
||||||
|
expect(config.websearch?.provider).toBe(WebSearch.AUTO)
|
||||||
|
})
|
||||||
|
|
||||||
test("round-trips every configuration entry type", () => {
|
test("round-trips every configuration entry type", () => {
|
||||||
const entries = [
|
const entries = [
|
||||||
new Config.Document({
|
new Config.Document({
|
||||||
|
|||||||
Reference in New Issue
Block a user