import { Cause, Effect, Exit, Layer, Option, Result } from "effect" import { isAppError, OperationAborted, toToolResult } from "./errors.ts" import type { ToolResult } from "./result.ts" /** * Per-call runner (no toolContent wrap): provide `layer`, map AppError → * ToolResult, defects → bug:. No module-level ManagedRuntime. */ export async function runToolResult( effect: Effect.Effect, // Required: a defaulted `Layer.empty as Layer.Layer` erases R, so calling // without the layer would type-check and then die as a service-not-found // defect on every invocation. Pass `Layer.empty` explicitly when R is never. layer: Layer.Layer, hooks: { signal?: AbortSignal operation?: string abortDetails?: Record } = {}, ): Promise { const provided = Effect.provide(effect, layer) as Effect.Effect const exit = await Effect.runPromiseExit(provided, { signal: hooks.signal }) if (Exit.isSuccess(exit)) { return exit.value } const error = Exit.findErrorOption(exit) if ( hooks.signal?.aborted || Result.isSuccess(Cause.findInterrupt(exit.cause)) ) { return toToolResult( new OperationAborted({ operation: hooks.operation ?? "operation", details: hooks.abortDetails, }), ) } if (Option.isSome(error) && isAppError(error.value)) { return toToolResult(error.value) } const defect = Cause.findDefect(exit.cause) const msg = Result.isSuccess(defect) ? defectMessage(defect.success) : defectMessage(Cause.squash(exit.cause)) return { ok: false, error: `bug: ${msg}` } } function defectMessage(defect: unknown): string { if (defect instanceof Error) return defect.message || defect.name if (typeof defect === "string") return defect try { return JSON.stringify(defect) } catch { return String(defect) } }