import { Context, Effect, Layer } from "effect" import type { DecisionAnswer, DecisionModel, DecisionOptions, DecisionQuestion, DecisionQuestions, DecisionRequest, DecisionResponseFor, } from "./decision.js" import { RequestExecutor } from "./route/executor.js" import { AIError, InvalidProviderOutputError } from "./schema/index.js" export type Execute = RequestExecutor.Interface["execute"] export interface Interface { readonly generate: ( request: DecisionRequest, Questions>, ) => Effect.Effect, AIError> } export class Service extends Context.Service()("@opencode/AI/DecisionClient") {} export const generate = ( request: DecisionRequest, Questions>, ): Effect.Effect, AIError, Service> => Effect.flatMap(Service, (client) => client.generate(request)) export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { const executor = yield* RequestExecutor.Service return Service.of({ generate: Effect.fnUntraced(function* (request) { const route = request.model.route const response = yield* route.generate(request, executor.execute) if (answersMatch(request.questions, response.answers)) return response as DecisionResponseFor return yield* new AIError({ reason: new InvalidProviderOutputError({ route: route.id, message: "Decision answers do not match the requested questions", cause: response.answers, }), }) }), }) }), ) export const fetchLayer = layer.pipe(Layer.provideMerge(RequestExecutor.fetchLayer)) /** Host bugs (a missing or extra answer, an option that wasn't offered, a score off the scale) fail the whole call. */ const answersMatch = (questions: ReadonlyArray, answers: ReadonlyArray) => questions.length === answers.length && questions.every((question, index) => { const answer = answers[index] if (answer.name !== question.name) return false if (answer.type === "error") return true if (question.type === "predicate") return answer.type === "predicate" if (question.type === "choice") { if (answer.type !== "choice") return false const values = question.choices.map((choice) => choice.value) return ( values.includes(answer.choice) && sameMembers( values, answer.probabilities.map((item) => item.value), ) ) } if (answer.type !== "score" || answer.score < 0 || answer.score > question.levels.length - 1) return false return sameMembers( question.levels.map((_, level) => level), answer.probabilities.map((item) => item.value), ) }) const sameMembers = (expected: ReadonlyArray, received: ReadonlyArray) => expected.length === received.length && new Set(received).size === received.length && received.every((item) => expected.includes(item)) export const DecisionClient = { Service, layer, fetchLayer, generate, } as const