baikai-effectful-0.3.0.0: src/Baikai/Effectful.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeFamilies #-}
-- | A thin, policy-free @effectful@ binding over baikai's transport.
--
-- This module exposes a single dynamic effect, 'Baikai', whose operations mirror
-- baikai's transport functions, plus two interpreters that run those operations
-- against a real (global) or isolated provider registry. It carries NO policy:
-- failures propagate exactly as baikai produces them — the blocking path
-- ('complete') returns an error-shaped 'Response' whose
-- 'Baikai.responseError' is populated; the streaming paths
-- ('streamCollect', 'streamEach') surface baikai's terminal @EventError@ in-band.
-- Retries, caching, budgets, rate limiting, and error remapping belong one layer
-- up, in terms of this effect.
module Baikai.Effectful
( -- * The effect
Baikai (..),
-- * Operations
complete,
streamCollect,
streamEach,
-- * Interpreters
runBaikai,
runBaikaiWith,
-- * Re-exports of the baikai request/response vocabulary
Model,
Context,
Options,
Response,
AssistantMessageEvent,
)
where
import Baikai (AssistantMessageEvent, Context, Model, Options, Response)
import Baikai qualified
import Baikai.Provider.Registry (ProviderRegistry)
import Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, IOE, liftIO, (:>))
import Effectful.Dispatch.Dynamic (interpret, localSeqUnliftIO, send)
import Streamly.Data.Fold qualified as Fold
import Streamly.Data.Stream qualified as Stream
-- | The provider-neutral baikai transport effect. Operations mirror baikai's
-- transport functions; the interpreter selects the provider registry.
data Baikai :: Effect where
-- | A blocking completion. Mirrors 'Baikai.completeRequest' — failures
-- are returned in-band as an error-shaped 'Response'.
Complete :: Model -> Context -> Options -> Baikai m Response
-- | A streaming completion materialized into the full event list. Mirrors
-- 'Baikai.streamRequest' drained to a list; a terminal @EventError@ appears
-- as the last element on failure (baikai does not throw here).
StreamCollect :: Model -> Context -> Options -> Baikai m [AssistantMessageEvent]
-- | A streaming completion where each event is handed, in order, to a
-- caller-supplied callback that runs inside the same effect context.
StreamEach :: Model -> Context -> Options -> (AssistantMessageEvent -> m ()) -> Baikai m ()
type instance DispatchOf Baikai = 'Dynamic
-- | Issue a blocking completion. Provider failures are returned in-band
-- as an error-shaped 'Response', just like 'Baikai.completeRequest'.
complete :: (Baikai :> es) => Model -> Context -> Options -> Eff es Response
complete m c o = send (Complete m c o)
-- | Materialize a streaming completion into the full event list. Convenient when
-- you do not need incremental deltas. Does not throw on provider failure; a
-- terminal 'AssistantMessageEvent' @EventError@ appears as the last element.
streamCollect :: (Baikai :> es) => Model -> Context -> Options -> Eff es [AssistantMessageEvent]
streamCollect m c o = send (StreamCollect m c o)
-- | Stream a completion, invoking the callback on each event in order, inside
-- 'Eff'. Preserves incrementality: the callback sees events as they arrive, in
-- the same effect context as the caller.
streamEach ::
(Baikai :> es) =>
Model ->
Context ->
Options ->
(AssistantMessageEvent -> Eff es ()) ->
Eff es ()
streamEach m c o k = send (StreamEach m c o k)
-- | Interpret 'Baikai' against an explicit provider registry. Requires 'IOE'
-- because the operations are ultimately baikai 'IO' calls. No retries, caching,
-- or error remapping.
runBaikaiWith :: (IOE :> es) => ProviderRegistry -> Eff (Baikai : es) a -> Eff es a
runBaikaiWith reg = interpret $ \env -> \case
Complete m c o ->
liftIO (Baikai.completeRequestWith reg m c o)
StreamCollect m c o ->
liftIO (Stream.toList (Baikai.streamRequestWith reg m c o))
StreamEach m c o k ->
-- @k@ is the caller's @Eff@ action (a higher-order argument). @localSeqUnliftIO@
-- gives a function @unlift :: forall r. Eff localEs r -> IO r@ valid for the
-- duration of the block, so we run @k event@ as IO inside a streamly fold that
-- drains the stream — one callback invocation per event, in order.
localSeqUnliftIO env $ \unlift ->
Stream.fold
(Fold.drainMapM (\event -> unlift (k event)))
(Baikai.streamRequestWith reg m c o)
-- | Interpret 'Baikai' against baikai's process-global provider registry.
runBaikai :: (IOE :> es) => Eff (Baikai : es) a -> Eff es a
runBaikai = runBaikaiWith Baikai.globalProviderRegistry