baikai-effectful
A thin, policy-free effectful
binding over baikai's transport. It exposes one dynamic effect, Baikai,
whose operations mirror baikai's transport functions, plus interpreters that run those
operations against a real or isolated provider registry.
It is the seam, not the framework: it adds no retries, backoff, rate limiting,
budgets, caching, or error remapping. Failures propagate exactly as baikai produces them:
blocking completions return the same error-shaped Response as completeRequest, and
streaming paths surface baikai's terminal EventError event in-band. Policy belongs one
layer up, written in terms of this effect.
The effect
data Baikai :: Effect where
Complete :: Model -> Context -> Options -> Baikai m Response
StreamCollect :: Model -> Context -> Options -> Baikai m [AssistantMessageEvent]
StreamCollectTraced :: TraceSink -> Model -> Context -> Options -> Baikai m [AssistantMessageEvent]
StreamEach :: Model -> Context -> Options -> (AssistantMessageEvent -> m ()) -> Baikai m ()
Operations
| Operation |
Meaning |
complete m c o |
Blocking completion → Response, including error-shaped responses. |
streamCollect m c o |
Drain the stream into the full [AssistantMessageEvent]. |
streamCollectTraced sink m c o |
Collect events with tracing and bounded cleanup on cancellation. |
streamEach m c o k |
Run callback k once per event, in order, inside Eff. |
streamEach preserves incrementality (the callback sees events as they arrive); a terminal
EventError appears in-band rather than as a thrown exception, matching baikai.
Evidence on cancellation
streamCollectTraced delegates to core's withTraceListWith, which owns the
whole fold lifetime. Supply a TraceSink and opt in through Options.evidence.
On success or provider failure the sink's single CallEvidence carries exactly
the terminal's evidence record, including its callId. A strict pre-dispatch
refusal retains core's failed evidence and never invokes the provider. Opt-out
produces no evidence; providers that omit successful evidence retain core's
strict missing-record failure.
For cancellation before terminal commitment, use a short, nonblocking sink that stores the complete
ModelCallEvidence in a caller-owned cell before acknowledging it. Read that
cell immediately in the caller's exception handler. With a healthy sink the
CallAborted record is already present and the original exception is unchanged.
The sink fold's end action also completes before propagation. Cancellation after
terminal commitment retains that terminal's evidence and adds no abort.
The sink drain budget is one second in total. A blocked or throwing sink still
allows cancellation to unwind and reports its failure on stderr; an empty cell
means delivery was unconfirmed. A worker abandoned at the bound can deliver
later. Exactly-once means Baikai submits one evidence event, not that a failing
external destination durably stores it. Keep slow export behind a nonblocking
queue.
Core's withTraceFold, withTraceList, withTrace, and their With variants
own the same lifetime. Arbitrary abandonment of withTraceStream between stream
steps still relies on garbage collection. The existing streamCollect and
streamEach operations keep their behavior.
Interpreters
runBaikai :: (IOE :> es) => Eff (Baikai : es) a -> Eff es a -- global registry
runBaikaiWith :: (IOE :> es) => ProviderRegistry -> Eff (Baikai : es) a -> Eff es a -- explicit registry
The operations are registry-agnostic; the interpreter picks the registry, mirroring
baikai's own completeRequest vs completeRequestWith split. Because Baikai is a dynamic
effect, you can re-interpret the same operations — stub them in tests, record/replay,
intercept, or (one layer up) wrap them with retries/caching/tracing — without touching any
call site.
Example
import Baikai.Effectful
import Effectful (Eff, runEff)
describe :: (Baikai :> es) => Model -> Context -> Options -> Eff es Response
describe = complete
main :: IO ()
main = do
-- against baikai's process-global registry (providers registered elsewhere):
r <- runEff . runBaikai $ describe model ctx opts
print r
For a hermetic test, register a stub provider in an isolated registry with
Baikai.Provider.Registry.newProviderRegistry / registerApiProviderWith and drive the
operations through runEff . runBaikaiWith reg. See test/ in this package.
Live demo
The test suite includes a live, network-touching demo gated on an environment variable, so
the default run stays hermetic:
# hermetic (default): no network, no key — the live case prints a skip line and passes
cabal test baikai-effectful-test
# live: registers the OpenAI provider and makes one real call
BAIKAI_EFFECTFUL_LIVE=1 OPENAI_API_KEY=sk-... cabal test baikai-effectful-test
Why a separate package
The core baikai package deliberately carries no effectful dependency, and most baikai
users do not use effectful. Shipping the binding as its own artifact keeps the core
dependency-light while giving any effectful program a reusable baikai↔effectful seam — the
same way baikai-claude / baikai-openai layer on baikai.