Skip to content

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

BeforeAfter
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 R
const lookup: Tool.Tool<"lookup", Args, never, string, WebSearch> = ...
// After: E is fifth, R is sixth
const 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 string failure, or Tool.fail(message, { kind? }) (which fails with the ToolFailed sentinel), is absorbed into a ToolResult.Failure the 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: catchTag a 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:

  1. Fail from inside run with a plain string or Tool.fail(...). Both are model-visible by design.
  2. 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 text
const 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.fromQueue takes an optional timeout: an unanswered gated call resolves as cancelled instead 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 a ToolResult.Failure of kind input_validation_error carrying 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

  1. If you hand-wrote a Tool<...> type, insert E in the fifth slot and shift R to sixth. Tools built with Tool.make need no change.
  2. For any tool whose failure the model should see, fail with a string or Tool.fail(...), or wrap the toolkit in Toolkit.describeFailures(...).
  3. 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.