import * as Data from "effect/Data"; import * as Result from "effect/Result"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import type { WebErrorCode, WebResult } from "../shared/web"; const messages: Record = { invalid_selector: "Use a valid CSS selector, or omit the selector to read the loaded page.", invalid_url: "Use an absolute HTTP or HTTPS URL without credentials.", blocked_url: "Private or local URLs and redirects are not allowed.", timeout: "The research service did not respond within the time limit. Try again or use another source.", cancelled: "Web research was cancelled.", http_error: "The website returned an unsuccessful HTTP response. Try another source.", unsupported_content: "This reader supports HTML, Markdown and plain text. Use a text version of this source.", request_failed: "The website could not be read. Check the URL or try another source.", search_blocked: "The search service blocked or rate-limited this request. Try again later or read a known URL.", search_billing: "Web search requires Cloudflare AI Gateway credits. Add credits or check the gateway's billing configuration.", search_authentication: "Web search access was rejected. Check Cloudflare AI Gateway and OpenAI model access.", search_rate_limited: "Web search is rate-limited. Try again later or read a known URL.", search_unavailable: "Web search is unavailable. Check AI Gateway model access or read a known URL.", invalid_search_response: "The search provider did not return a completed search with usable source metadata. Try again or read a known URL.", browser_unavailable: "The browser service is unavailable. Try again later or read a known URL.", cleanup_failed: "The browser session could not be confirmed closed. It has a short inactivity expiry; try again later.", }; export class WebFailure extends Data.TaggedError("WebFailure")<{ code: WebErrorCode; status?: number; }> { constructor(code: WebErrorCode, status?: number) { super({ code, ...(status === undefined ? {} : { status }) }); } override get message() { return messages[this.code]; } } export function webValidation(read: () => A) { return Effect.try({ try: read, catch: (error) => error instanceof WebFailure ? error : new WebFailure("request_failed"), }); } // The native tool protocol carries safe results, including explicit cancellation. export async function runWeb( program: Effect.Effect, signal?: AbortSignal, ): Promise { if (signal?.aborted) return webError(undefined, signal); const exit = await Effect.runPromiseExit(program, { signal }); if (Exit.isSuccess(exit)) return exit.value; const failure = Exit.findErrorOption(exit); const defect = Exit.findDefect(exit); return webError( Option.isSome(failure) ? failure.value : Result.isSuccess(defect) && defect.success instanceof WebFailure ? defect.success : undefined, signal, ); } export function webFailure(code: WebErrorCode, status?: number): WebResult { return { ok: false, code, message: messages[code], ...(status === undefined ? {} : { status }), }; } export function webError(error: unknown, signal?: AbortSignal): WebResult { if (signal?.aborted) return webFailure( signal.reason instanceof WebFailure ? signal.reason.code : "cancelled", ); if (error instanceof WebFailure) return webFailure(error.code, error.status); return webFailure("request_failed"); }