Migrating to 0.10
0.10 adds two capabilities and makes one breaking change to the tool layer.
The breaking change is typed-only and small: a Tool now carries its error
type, and Toolkit.run is deliberate about which failures the model sees.
Everything else is additive: a WebRead capability with four providers, and
a Browser capability driven over CDP.
If you hit a 0.10 compile error that looks like an older rename (tool
arrays, Tool.streaming, durationSeconds, executeAll, Outcome), you
are crossing an earlier breaking release. Apply the 0.9
page and the ones before it first.
Required rewrites
Tools carry a typed error
The Tool type gains an error parameter, E, inserted before R, and
run returns Effect.Effect<Output, E, R> instead of
Effect.Effect<Output, unknown, R>.
| Before | After |
|---|---|
Tool<Name, Input, Event, Output, R> | Tool<Name, Input, Event, Output, E, R> |
Tool.make infers E from run, so tools you build with it need no
change. The only thing that breaks is a hand-written full type
annotation: if you spelled your requirement in the fifth slot, it now
lands in the error slot. Add unknown (or the real error) as the fifth
argument and shift R to sixth.
// Before: fifth slot was Rconst lookup: Tool.Tool<"lookup", Args, never, string, WebSearch> = ...
// After: E is fifth, R is sixthconst lookup: Tool.Tool<"lookup", Args, never, string, AiError, WebSearch> = ...There is also a new Tool.ToolE<T> type extractor, alongside the existing
Tool.ToolR<T>.
Failed tools propagate; opt them back into the model
In 0.9 a tool’s error channel was unknown, and a tool that failed had that
error folded into a ToolResult.Failure the model read. In 0.10 the channel
is typed and Toolkit.run splits failures into two paths:
- A
stringfailure, orTool.fail(message, { kind? })(which fails with theToolFailedsentinel), is absorbed into aToolResult.Failurethe model reads and adapts to. This is the path for “tell the model it went wrong.” - Every other tool error stays typed on
Toolkit.run’s channel and propagates (Exclude<..., string | ToolFailed>). Your loop handles it like any Effect failure:catchTaga specific one, fall back, or let it fail the run. Defects still die.
So if you relied on an arbitrary tool failure surfacing to the model, pick one:
- Fail from inside
runwith a plainstringorTool.fail(...). Both are model-visible by design. - Keep the typed failure and opt the whole toolkit into visibility with
Toolkit.describeFailures(describe), which maps its failures to strings the model reads.
// Show this toolkit's failures to the model, as textconst visible = Toolkit.describeFailures(AiError.describe)(toolkit)Toolkit.describeFailures is built on Toolkit.wrap (0.9); reach for wrap
directly for other cross-cutting behavior (logging, retry, auth).
Behavior changes (no rewrite)
Approval.fromQueuetakes an optionaltimeout: an unanswered gated call resolves ascancelledinstead of blocking forever, and the approval router retires once a round is fully resolved rather than running for the life of the process.- Tool input decoding is hardened: empty arguments normalize to
{}, a throwing validator is caught, and unparseable or invalid arguments come back as aToolResult.Failureof kindinput_validation_errorcarrying the issue detail.
What’s new: WebRead (additive)
0.10 adds a WebRead capability: turn a URL into clean markdown (or HTML),
then extract typed data from it. It mirrors WebSearch: one generic tag,
several provider layers, and a ready-made tool.
import { read } from "@effect-uai/core/WebRead"import { layer as firecrawl } from "@effect-uai/firecrawl/FirecrawlRead"
const program = read({ url }).pipe( Effect.provide(firecrawl({ apiKey: Redacted.make(process.env.FIRECRAWL_API_KEY!) })),)Four providers register the generic WebRead tag, swappable as a Layer:
@effect-uai/firecrawl(new package): JS-rendered pages to markdown or HTML.@effect-uai/exa/ExaContents: Exa/contents.@effect-uai/tavily/TavilyRead: Tavily/extract.@effect-uai/jina/JinaReader: Jina Reader.
webReadTool (from @effect-uai/core/WebReadTool) hands the capability to a
model as a tool, the same way webSearchTool does for search. See
Web reading and the Market intel
recipe. Nothing in existing code changes; provide a
layer and your capability-tag code resolves.
What’s new: Browser (additive)
0.10 adds a Browser capability: drive a real browser over the Chrome
DevTools Protocol, navigate/click/fill/press/scroll, and read a page as
markdown with its interactive elements labeled.
import { layer as cdp } from "@effect-uai/browser/Connect"
const chromium = cdp({ endpoint: "http://127.0.0.1:9222" })@effect-uai/core/BrowserTool exports verb tools (gotoTool, clickTool,
fillTool, pressTool, scrollTool) and a browserToolkit(session) that
bundles them, for handing the browser to an agent loop. The
@effect-uai/browser adapter covers the whole CDP field with one endpoint:
a headless Chromium container, a local Chrome or Edge, a from-scratch engine
like obscura, or a hosted browser cloud. See Browser,
Agent usability testing, and
Dashboard briefing.
Migration order
- If you hand-wrote a
Tool<...>type, insertEin the fifth slot and shiftRto sixth. Tools built withTool.makeneed no change. - For any tool whose failure the model should see, fail with a
stringorTool.fail(...), or wrap the toolkit inToolkit.describeFailures(...). - Run
pnpm typecheck.
Everything under “What’s new” is additive: add the layer and your capability-tag code resolves.
See the Tools and toolkits concept page for the current tool-layer picture.