Migrating to 0.3
0.3 is mostly additive (new embedding subsystem, new loopWithState,
tool requirements lifted onto the R channel) but contains a handful
of mechanical renames and one removal. This page is the consolidated
“old → new” view; the changelog
covers the why.
At a glance
| Before | After | Kind |
|---|---|---|
Loop.streamUntilComplete(fn) | Loop.onTurnComplete(fn) | rename |
Toolkit.nextStateFrom(events, fn) | events.pipe(Toolkit.continueWith(fn)) | reshape |
import { matchType } from ".../Match" | Match.discriminators("type") from effect | removed |
Tool.AnyKindTool (implied R = never) | Tool.AnyKindTool<R> (defaults to any) | widened |
_tag literals on ToolResult / ToolEvent | Data.TaggedEnum constructors + .$is / .$match | reshape (back-compat) |
Renames
Loop.streamUntilComplete → Loop.onTurnComplete
Pure rename. Same signature, same semantics. The new name is honest about what the callback fires on (turn completion).
// Beforeimport { loop, stop, streamUntilComplete } from "@effect-uai/core/Loop"stream.pipe(streamUntilComplete<State, ToolEvent>((turn) => ...))
// Afterimport { loop, stop, onTurnComplete } from "@effect-uai/core/Loop"stream.pipe(onTurnComplete<State, ToolEvent>((turn) => ...))Toolkit.nextStateFrom → Toolkit.continueWith (pipe-friendly)
continueWith is curried via Function.dual, so the typical call
site becomes a .pipe(...) rather than a positional call. Both forms
type-check, but the pipe form composes better.
// Beforeconst events = Toolkit.executeAll(tools, calls)return Toolkit.nextStateFrom(events, (results) => Turn.appendTurn(state, turn, results.map(toFunctionCallOutput)),)
// After (recommended — pipe form)return Toolkit.executeAll(tools, calls).pipe( Toolkit.continueWith((results) => Turn.appendTurn(state, turn, results.map(toFunctionCallOutput)), ),)
// Also valid (data-first, mirrors the old shape)const events = Toolkit.executeAll(tools, calls)return Toolkit.continueWith(events, (results) => ...)Keep the intermediate const events = only when the stream is
assembled from multiple sources (e.g. the queue-based approval flow
that merges executeAll, decisions, and announce). Otherwise
inline the pipe.
Removed
@effect-uai/core/Match and matchType
Removed in favour of Match from effect. The replacement is more
flexible and is the idiomatic Effect approach.
// Beforeimport { matchType } from "@effect-uai/core/Match"const handler = matchType<ToolEvent>()({ Intermediate: (e) => ..., Output: (e) => ..., ApprovalRequested: (e) => ...,})
// Afterimport { Match } from "effect"const handler = Match.discriminators("_tag")({ Intermediate: (e) => ..., Output: (e) => ..., ApprovalRequested: (e) => ...,})// or `discriminatorsExhaustive` if you want exhaustiveness checkedBehavior changes
Tool requirements flow through R
Tool.AnyKindTool no longer pins R to never. A tool whose run
yields WeatherApiKey now declares that on its type, and
Toolkit.executeAll surfaces the union for the caller to provide via
Layer. See Tools and toolkits → Tools with service requirements.
This is widening — code that previously compiled still compiles. The
only practical change: the resulting Stream<ToolEvent, never, R>
may now have a non-never R you must satisfy with Stream.provide.
ToolResult, ToolEvent, Image*Source are Data.TaggedEnum
You now get constructors and matchers without writing them by hand:
import * as ToolResult from "@effect-uai/core/Outcome"
// Construction (preferred)ToolResult.Failure({ call_id, tool, kind: "denied", reason: "..." })
// Pattern-matchingToolResult.$match({ Value: ({ value }) => ..., Failure: ({ kind, reason }) => ...,})(result)
// PredicatesToolResult.$is("Failure")(result)The _tag wire shape and existing is* predicates (isValue,
isFailure, isOutput, etc.) are preserved, so existing pattern-
matching keeps working. Only adopt the new shape if you want it.
New capabilities (additive — no migration needed)
Loop.loopWithState— likeloop, but returns{ stream, state: SubscriptionRef<S> }so callers can read state after the loop ends or observe changes live viaSubscriptionRef.changes(state).@effect-uai/core/EmbeddingModel— parallel ofLanguageModelfor vectorization. Provider modules:OpenAIEmbedding,GeminiEmbedding,JinaEmbedding.@effect-uai/core/Vector—cosine,dot,l2Norm, sparse variants,maxSimfor multivector.- Barrel re-exports from
@effect-uai/core:Outcome,ToolEvent,Resolvers,HistoryCheck.
Recommended order
- Mechanical renames (
streamUntilComplete,nextStateFrom) — Find & Replace gets you 99% of the way. - Drop the
@effect-uai/core/Matchimport; switch toMatchfromeffect. - (Optional) Switch
nextStateFromcall sites to the pipe form. - (Optional) Adopt
Data.TaggedEnumconstructors. - (Optional) Use
Loop.loopWithStateif you want post-loop state readout.
Using Claude to migrate
The effect-uai-migrate skill encodes these rules. From Claude Code:
/skill effect-uai-migrateOr just point Claude at this page — the rewrites are mechanical enough that any tool-using LLM can apply them.