Skip to content

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

BeforeAfterKind
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 effectremoved
Tool.AnyKindTool (implied R = never)Tool.AnyKindTool<R> (defaults to any)widened
_tag literals on ToolResult / ToolEventData.TaggedEnum constructors + .$is / .$matchreshape (back-compat)

Renames

Loop.streamUntilCompleteLoop.onTurnComplete

Pure rename. Same signature, same semantics. The new name is honest about what the callback fires on (turn completion).

// Before
import { loop, stop, streamUntilComplete } from "@effect-uai/core/Loop"
stream.pipe(streamUntilComplete<State, ToolEvent>((turn) => ...))
// After
import { loop, stop, onTurnComplete } from "@effect-uai/core/Loop"
stream.pipe(onTurnComplete<State, ToolEvent>((turn) => ...))

Toolkit.nextStateFromToolkit.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.

// Before
const 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.

// Before
import { matchType } from "@effect-uai/core/Match"
const handler = matchType<ToolEvent>()({
Intermediate: (e) => ...,
Output: (e) => ...,
ApprovalRequested: (e) => ...,
})
// After
import { Match } from "effect"
const handler = Match.discriminators("_tag")({
Intermediate: (e) => ...,
Output: (e) => ...,
ApprovalRequested: (e) => ...,
})
// or `discriminatorsExhaustive` if you want exhaustiveness checked

Behavior 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-matching
ToolResult.$match({
Value: ({ value }) => ...,
Failure: ({ kind, reason }) => ...,
})(result)
// Predicates
ToolResult.$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 — like loop, but returns { stream, state: SubscriptionRef<S> } so callers can read state after the loop ends or observe changes live via SubscriptionRef.changes(state).
  • @effect-uai/core/EmbeddingModel — parallel of LanguageModel for vectorization. Provider modules: OpenAIEmbedding, GeminiEmbedding, JinaEmbedding.
  • @effect-uai/core/Vectorcosine, dot, l2Norm, sparse variants, maxSim for multivector.
  • Barrel re-exports from @effect-uai/core: Outcome, ToolEvent, Resolvers, HistoryCheck.
  1. Mechanical renames (streamUntilComplete, nextStateFrom) — Find & Replace gets you 99% of the way.
  2. Drop the @effect-uai/core/Match import; switch to Match from effect.
  3. (Optional) Switch nextStateFrom call sites to the pipe form.
  4. (Optional) Adopt Data.TaggedEnum constructors.
  5. (Optional) Use Loop.loopWithState if you want post-loop state readout.

Using Claude to migrate

The effect-uai-migrate skill encodes these rules. From Claude Code:

/skill effect-uai-migrate

Or just point Claude at this page — the rewrites are mechanical enough that any tool-using LLM can apply them.