Skip to content

Migrating to 0.6

0.6 bundles one large-but-mechanical naming sweep with a set of additive speech features. The breaking part is the naming sweep: the public API settles on “tool call” terminology everywhere (no more “function call” in type or helper names), two modules move to clearer names, and the Loop helper surface is trimmed to value / next / stop. The wire format is unchangedfunction_call / function_call_output still go out on the wire, so this is a source-level rename with no behavior change. The additive part (multi-speaker dialogue, custom pronunciations, and a handful of new Loop / Toolkit helpers) needs no migration — see New capabilities at the end. This page is the consolidated “old → new” view; the changelog covers the why.

Almost every breaking rewrite is find-and-replace. The one spot that needs thought is the Loop helper trim (nextAfter / stopAfter and friends removed) — covered in its own section below.

At a glance

BeforeAfterKind
import … from "@effect-uai/core/Outcome"import … from "@effect-uai/core/ToolResult"rename
import … from "@effect-uai/core/Resolvers"import … from "@effect-uai/core/Approval"rename
ItemHistoryItemrename
FunctionCall / FunctionCallOutputToolCall / ToolCallOutputrename
Items.functionCallOutput(…)Items.toolCallOutput(…)rename
Items.isFunctionCall / Items.isFunctionCallOutputItems.isToolCall / Items.isToolCallOutputrename
Turn.functionCalls(turn)Turn.getToolCalls(turn)rename
Turn.appendTurn(…)Turn.appendToHistory(…)rename
Turn.toStructured(…)Turn.decodeStructured(…)rename
ToolResult.Value / isValueToolResult.Ok / isOkrename
rejected(call, kind, reason)failed(call, kind, reason)rename
toFunctionCallOutput(result)toToolCallOutput(result)rename
Toolkit.executeAll(tools, calls)Toolkit.run(tools, calls)rename
Toolkit.continueWith(build)Toolkit.continueWithResults(build)rename
Toolkit.make([…]) + Toolkit.toDescriptors(kit)Tool.toDescriptors([…])removed
Tool.AnyKindToolTool.AnyToolrename
ToolEvent.Intermediate / isIntermediateToolEvent.Progress / isProgressrename
ToolCallDecisionApprovalDecisionrename
fromApprovalMap(…) / fromVerdictQueue(…)fromMap(…) / fromQueue(…)rename
fromVerdictQueue(…).announcefromQueue(…).approvalRequestsrename
Loop.loopFrom(…)Loop.loopOver(…)rename
Loop.Event<A, S>Loop.Step<A, S>rename
return stopreturn stop()reshape
Loop.stopWith(state)Loop.stop(state)rename
Loop.nextAfter(stream, s)stream.pipe(Stream.map(value), Stream.concat(next(s)))removed
Loop.stopAfter / stopWithAfter / nextAfterFoldcompose with Stream.concat(stop()) / Stream.concat(stop(s))removed

”Function call” → “tool call” terminology

The biggest sweep, but the most mechanical: every public name that said “function call” now says “tool call”. The wire literals are untouched — items still serialize with type: "function_call" / type: "function_call_output", so matching on the wire string keeps working and no provider payloads change.

ItemHistoryItem

// Before
import type { Item } from "@effect-uai/core/Items"
const history: ReadonlyArray<Item> = [...]
// After
import type { HistoryItem } from "@effect-uai/core/Items"
const history: ReadonlyArray<HistoryItem> = [...]

FunctionCall / FunctionCallOutputToolCall / ToolCallOutput

// Before
type Item = Message | FunctionCall | FunctionCallOutput | Reasoning
import { functionCallOutput, isFunctionCall, isFunctionCallOutput } from "@effect-uai/core/Items"
const out = functionCallOutput({ call_id, output })
items.filter(isFunctionCall)
// After
type HistoryItem = Message | ToolCall | ToolCallOutput | Reasoning
import { toolCallOutput, isToolCall, isToolCallOutput } from "@effect-uai/core/Items"
const out = toolCallOutput({ call_id, output })
items.filter(isToolCall)

toolCallOutput(...) still produces an item whose wire type is "function_call_output". If you pattern-match on item.type === "function_call", leave that string as-is.

Turn helpers

// Before
const calls = Turn.functionCalls(turn) // FunctionCall[]
const next = Turn.appendTurn(state, turn, outputs)
const parsed = yield * Turn.toStructured(turn, format)
// After
const calls = Turn.getToolCalls(turn) // ToolCall[]
const next = Turn.appendToHistory(state, turn, outputs)
const parsed = yield * Turn.decodeStructured(turn, format)

Module: OutcomeToolResult

The module that synthesizes structured tool results is now named for what it produces. The import path and a couple of member names change.

// Before
import { toFunctionCallOutput, rejected } from "@effect-uai/core/Outcome"
import * as ToolResult from "@effect-uai/core/Outcome"
ToolResult.Value({ call_id, tool, value }) // success variant
ToolResult.isValue(result)
const denied = rejected(call, "denied", "policy blocked it")
outputs.push(toFunctionCallOutput(result))
// After
import { toToolCallOutput, failed } from "@effect-uai/core/ToolResult"
import * as ToolResult from "@effect-uai/core/ToolResult"
ToolResult.Ok({ call_id, tool, value }) // success variant
ToolResult.isOk(result)
const denied = failed(call, "denied", "policy blocked it")
outputs.push(toToolCallOutput(result))

The Failure variant, the denied / cancelled / executionError synthesizers, and the wire shape are unchanged — only the success variant (ValueOk, isValueisOk), the custom-kind synthesizer (rejectedfailed), and the wire converter (toFunctionCallOutputtoToolCallOutput) move.

Module: ResolversApproval

The approval-gating module is renamed to match the domain it models.

// Before
import { fromApprovalMap, fromVerdictQueue, ToolCallDecision } from "@effect-uai/core/Resolvers"
const plan = fromApprovalMap(isSensitive, approvals)(calls)
const { approved, decisions, announce } = yield * fromVerdictQueue(isSensitive, verdicts)(calls)
const d: ToolCallDecision = ToolCallDecision.Approved({ call })
// After
import { fromMap, fromQueue, ApprovalDecision } from "@effect-uai/core/Approval"
const plan = fromMap(isSensitive, approvals)(calls)
const { approved, decisions, approvalRequests } = yield * fromQueue(isSensitive, verdicts)(calls)
const d: ApprovalDecision = ApprovalDecision.Approved({ call })

Note the renamed field on the queue helper’s result: announceapprovalRequests. The approve / reject sugar and the approved / decisions fields keep their names.

Toolkit: executeAllrun, continueWithcontinueWithResults

// Before
import * as Toolkit from "@effect-uai/core/Toolkit"
return Toolkit.executeAll(tools, calls).pipe(
Toolkit.continueWith((results) =>
Turn.appendTurn(state, turn, results.map(toFunctionCallOutput)),
),
)
// After
import * as Toolkit from "@effect-uai/core/Toolkit"
return Toolkit.run(tools, calls).pipe(
Toolkit.continueWithResults((results) =>
Turn.appendToHistory(state, turn, results.map(toToolCallOutput)),
),
)

Removed: Toolkit.make / Toolkit.toDescriptors

The homogeneous-toolkit wrapper is gone. Build a flat array of tools (plain, streaming, or mixed) and render it with Tool.toDescriptors.

// Before
import * as Toolkit from "@effect-uai/core/Toolkit"
const toolkit = Toolkit.make([getTime, lookupWeather])
const descriptors = Toolkit.toDescriptors(toolkit)
// After
import * as Tool from "@effect-uai/core/Tool"
const descriptors = Tool.toDescriptors([getTime, lookupWeather])

Tool: AnyKindToolAnyTool

// Before
const tools: ReadonlyArray<Tool.AnyKindTool> = [getTime, askSubagent]
// After
const tools: ReadonlyArray<Tool.AnyTool> = [getTime, askSubagent]

ToolEvent: IntermediateProgress

The streaming-tool progress variant is renamed; Output and ApprovalRequested are unchanged.

// Before
Match.value(event).pipe(
Match.discriminators("_tag")({
Intermediate: (e) => renderProgress(e),
Output: (e) => commit(e.result),
ApprovalRequested: (e) => prompt(e),
}),
)
ToolEvent.isIntermediate(event)
// After
Match.value(event).pipe(
Match.discriminators("_tag")({
Progress: (e) => renderProgress(e),
Output: (e) => commit(e.result),
ApprovalRequested: (e) => prompt(e),
}),
)
ToolEvent.isProgress(event)

Loop: loopFromloopOver, EventStep, and a trimmed helper set

Two renames and one real cleanup.

Renames. The input-driven loop is now loopOver, and the body’s emit type is Step (was Event):

// Before
import { loopFrom } from "@effect-uai/core/Loop"
const body = (state: S, item: I): Stream<Loop.Event<A, S>> => ...
input.pipe(loopFrom(initial, body))
// After
import { loopOver } from "@effect-uai/core/Loop"
const body = (state: S, item: I): Stream<Loop.Step<A, S>> => ...
input.pipe(loopOver(initial, body))

next and stop are single-element streams; the *After helpers are gone. Previously nextAfter / stopAfter / stopWithAfter bundled “emit these values, then continue/stop” into one call. Now next(state), stop(), and stop(state) each emit a single terminal step, and you concatenate your values in front of them with ordinary Stream combinators. stopWith(state) collapses into stop(state).

BeforeAfter
return stopreturn stop()
return stopWith(state)return stop(state)
return nextAfter(stream, s)return stream.pipe(Stream.map(value), Stream.concat(next(s)))
return stopAfter(stream)return stream.pipe(Stream.map(value), Stream.concat(stop()))
return stopWithAfter(stream, s)return stream.pipe(Stream.map(value), Stream.concat(stop(s)))
// Before
import { value, next, stop, stopWith, nextAfter } from "@effect-uai/core/Loop"
return nextAfter(deltaStream, nextState) // emit deltas, then continue
// After
import { value, next, stop } from "@effect-uai/core/Loop"
return deltaStream.pipe(Stream.map(value), Stream.concat(next(nextState)))
// …or just `next(nextState)` when there were no values to emit first

stopEvent and nextAfterFold are removed with no direct replacement — build the step stream you want from value / next / stop and standard Stream combinators.

The canonical loop body, before and after

The most common shape — drain a turn, run the requested tools, fold the outputs back into history — changes name-for-name but keeps its structure:

// Before
onTurnComplete<State, ToolEvent>((turn) => {
const calls = Turn.functionCalls(turn)
if (calls.length === 0) return stop
return Toolkit.executeAll(tools, calls).pipe(
Toolkit.continueWith((results) =>
Turn.appendTurn(state, turn, results.map(toFunctionCallOutput)),
),
)
})
// After
onTurnComplete<State, ToolEvent>((turn) => {
const calls = Turn.getToolCalls(turn)
if (calls.length === 0) return stop()
return Toolkit.run(tools, calls).pipe(
Toolkit.continueWithResults((results) =>
Turn.appendToHistory(state, turn, results.map(toToolCallOutput)),
),
)
})

New capabilities (additive — no migration needed)

0.6 also ships features that don’t touch existing call sites. Adopt them when you need them; nothing breaks if you ignore them.

Multi-speaker dialogue + custom pronunciations on SpeechSynthesizer

  • synthesizeDialogue / streamSynthesizeDialogue on SpeechSynthesizerService, taking a CommonSynthesizeDialogueRequest ({ model, turns, outputFormat?, languageCode?, pronunciations? }) where each DialogueTurn is { voiceId, text, styleDescription?, speed? }.
  • A new MultiSpeakerTts capability marker — shipped only by provider layers with native dialogue support. The top-level synthesizeDialogue / streamSynthesizeDialogue helpers require it in R, so a dialogue-less provider fails at compile time (mirrors the existing TtsIncrementalText marker).
  • Optional pronunciations?: ReadonlyArray<CustomPronunciation> on CommonSynthesizeRequest, with CustomPronunciation ({ phrase, pronunciation, encoding }) and PhoneticEncoding ("ipa" | "x-sampa" | "cmu-arpabet"). Adapters that can’t honor an entry drop it silently and still render with the default pronunciation.
  • MockSpeechSynthesizer gains dialogueBlobs / streamSynthesizeDialogueChunks script fields and a layerWithoutMultiSpeaker variant for testing the marker.

New Toolkit history helpers

Toolkit.appendToolResults(state, turn) is shorthand for the common continueWithResults body — it folds the collected results into history for you, so you rarely hand-write the .map(toToolCallOutput):

return Toolkit.run(tools, calls).pipe(
Toolkit.continueWithResults(Toolkit.appendToolResults(state, turn)),
)

Toolkit.collectResults is the lower-level counterpart: it drains a Stream<ToolEvent> to its ToolResults without advancing the loop.

Sandboxes (@effect-uai/core/Sandbox + two adapters)

A new Sandbox capability lets agents run untrusted code, commands, or LLM-generated scripts inside an isolated microVM. Two provider packages implement the same SandboxService (apart from the provider-specific create request shape):

  • @effect-uai/microsandbox — local Firecracker microVMs via microsandbox. Best for local dev, integration tests, and self-hosted setups.
  • @effect-uai/deno — hosted Firecracker microVMs on Deno Deploy. No infra to run; good for production agents that need on-demand sandboxing.

Both packages cover the full surface — create / exec / execStream / volumes / snapshots / network policies / bound secrets / OCI images. Swap providers with a Layer change:

import * as Sandbox from "@effect-uai/core/Sandbox"
import * as Image from "@effect-uai/core/SandboxImage"
import * as Network from "@effect-uai/core/SandboxNetwork"
const program = Effect.gen(function* () {
const sb = yield* Sandbox.create({
image: Image.registry("python:3.12-slim"),
network: Network.blocked,
})
const r = yield* sb.exec({ cmd: ["python3", "-c", "print(2 + 2)"] })
console.log(r.stdout) // "4"
}).pipe(Effect.scoped) // destroys the sandbox when scope closes

See the effect-uai-sandbox-basics skill, the recipes-extras/sandbox-code-interpreter recipe, and the Sandboxes docs section for the full surface.

New recipes

Two additional recipes ship in 0.6:

  • sleeper-agent: long-lived background agent that wakes on scheduled triggers, performs a task, and goes back to sleep.
  • sandbox-code-interpreter (in recipes-extras/): the “run, fix, repeat” pattern. The model writes Python, the sandbox runs it, stderr feeds back into the next turn until the code works. Lives in recipes-extras/ because it needs a sandbox provider running.
  1. Swap the two module import paths: @effect-uai/core/Outcome@effect-uai/core/ToolResult and @effect-uai/core/Resolvers@effect-uai/core/Approval.
  2. Find-and-replace the flat renames: functionCallsgetToolCalls, appendTurnappendToHistory, toStructureddecodeStructured, executeAllrun, continueWithcontinueWithResults, AnyKindToolAnyTool, IntermediateProgress, loopFromloopOver, Loop.EventLoop.Step, toFunctionCallOutputtoToolCallOutput.
  3. Rename the type/constructor families: ItemHistoryItem, FunctionCall(Output)ToolCall(Output), ToolResult.ValueToolResult.Ok, rejectedfailed, fromApprovalMapfromMap, fromVerdictQueuefromQueue, ToolCallDecisionApprovalDecision, the announce field → approvalRequests.
  4. Replace Toolkit.make(...) + Toolkit.toDescriptors(...) with Tool.toDescriptors([...]).
  5. Fix the Loop helper trim: stopstop(), stopWith(s)stop(s), and rewrite each nextAfter / stopAfter / stopWithAfter to the Stream.concat form above.
  6. Leave wire literals ("function_call", "function_call_output") untouched — they are unchanged.
  7. Run typecheck, then tests.

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.