Migrating from v0.2.9 to v0.3.0

This guide covers breaking changes and deprecations introduced in v0.3.0. The primary change is replacing exception-throwing APIs with type-safe Result[T] alternatives.

Table of contents

  1. Overview
  2. Quick Reference
    1. ToolBuilder
    2. Individual Tools
    3. Package-level Collections
    4. BuiltinTools
    5. Agent
  3. Migration Patterns
    1. Handling Result[T]
    2. Single Tool
    3. Multiple Tools with for-comprehension
    4. BuiltinTools
    5. Custom Tool with ToolBuilder
    6. Agent Initialization
    7. Chaining with the LLM for-comprehension
  4. Why lazy val for Deprecated Wrappers
  5. Compiler Warnings

Overview

In v0.2.x, many APIs returned values directly but threw IllegalStateException on failure:

1
2
3
4
// v0.2.x — throws on failure
val tool: ToolFunction[_, _] = DateTimeTool.tool
val tools: Seq[ToolFunction[_, _]] = BuiltinTools.core
val state: AgentState = agent.initialize("query", registry)

In v0.3.x, these APIs return Result[T] (Either[LLMError, T]) so failures are explicit and handled at compile time:

1
2
3
4
// v0.3.x — safe, no exceptions
val tool: Result[ToolFunction[_, _]] = DateTimeTool.toolSafe
val tools: Result[Seq[ToolFunction[_, _]]] = BuiltinTools.coreSafe
val state: Result[AgentState] = agent.initializeSafe("query", registry)

The old methods are still present and compile, but are marked @deprecated and will be removed in a future release.


Quick Reference

ToolBuilder

v0.2.x (deprecated) v0.3.x (safe)
builder.build() builder.buildSafe()

Individual Tools

v0.2.x (deprecated) v0.3.x (safe)
DateTimeTool.tool DateTimeTool.toolSafe
CalculatorTool.tool CalculatorTool.toolSafe
UUIDTool.tool UUIDTool.toolSafe
JSONTool.tool JSONTool.toolSafe
ReadFileTool.tool ReadFileTool.toolSafe
ReadFileTool.create(config) ReadFileTool.createSafe(config)
ListDirectoryTool.tool ListDirectoryTool.toolSafe
ListDirectoryTool.create(config) ListDirectoryTool.createSafe(config)
FileInfoTool.tool FileInfoTool.toolSafe
FileInfoTool.create(config) FileInfoTool.createSafe(config)
WriteFileTool.create(config) WriteFileTool.createSafe(config)
HTTPTool.tool HTTPTool.toolSafe
HTTPTool.create(config) HTTPTool.createSafe(config)
ShellTool.create(config) ShellTool.createSafe(config)
WeatherTool.tool WeatherTool.toolSafe

Package-level Collections

v0.2.x (deprecated) v0.3.x (safe)
core.allTools core.allToolsSafe
filesystem.readOnlyTools filesystem.readOnlyToolsSafe
http.allTools http.allToolsSafe

BuiltinTools

v0.2.x (deprecated) v0.3.x (safe)
BuiltinTools.core BuiltinTools.coreSafe
BuiltinTools.safe(httpConfig) BuiltinTools.withHttpSafe(httpConfig)
BuiltinTools.withFiles(...) BuiltinTools.withFilesSafe(...)
BuiltinTools.development(...) BuiltinTools.developmentSafe(...)
BuiltinTools.custom(...) BuiltinTools.customSafe(...)

Agent

v0.2.x (deprecated) v0.3.x (safe)
agent.initialize(query, tools, ...) agent.initializeSafe(query, tools, ...)

Migration Patterns

Handling Result[T]

Result[T] is a type alias for Either[LLMError, T]. Pattern match explicitly:

1
2
3
4
DateTimeTool.toolSafe match {
  case Right(tool) => // use tool
  case Left(err)   => println(s"Failed: ${err.formatted}")
}

Single Tool

1
2
3
4
5
6
7
8
// Before
val tool = DateTimeTool.tool  // throws IllegalStateException on failure

// After
val tool = DateTimeTool.toolSafe match {
  case Right(t) => t
  case Left(e)  => ??? // handle gracefully
}

Multiple Tools with for-comprehension

1
2
3
4
5
6
7
8
9
10
11
12
13
// Before
val tools = new ToolRegistry(Seq(
  DateTimeTool.tool,
  CalculatorTool.tool,
  UUIDTool.tool
))

// After
val registryResult: Result[ToolRegistry] = for {
  dateTime   <- DateTimeTool.toolSafe
  calculator <- CalculatorTool.toolSafe
  uuid       <- UUIDTool.toolSafe
} yield new ToolRegistry(Seq(dateTime, calculator, uuid))

BuiltinTools

1
2
3
4
5
6
7
8
// Before
val tools = BuiltinTools.core  // throws on failure

// After
BuiltinTools.coreSafe match {
  case Right(tools) => new ToolRegistry(tools)
  case Left(err)    => ??? // handle gracefully
}

Custom Tool with ToolBuilder

1
2
3
4
5
6
7
8
9
// Before
val tool = ToolBuilder[Map[String, Any], MyResult]("my-tool", "desc", schema)
  .withHandler { extractor => ??? }
  .build()  // throws if handler missing

// After
val toolResult = ToolBuilder[Map[String, Any], MyResult]("my-tool", "desc", schema)
  .withHandler { extractor => ??? }
  .buildSafe()  // returns Result[ToolFunction]

Agent Initialization

1
2
3
4
5
6
7
8
// Before
val state = agent.initialize(query, tools)  // throws on failure

// After
agent.initializeSafe(query, tools) match {
  case Right(state) => // continue
  case Left(err)    => println(s"Init failed: ${err.formatted}")
}

Chaining with the LLM for-comprehension

The new safe APIs compose naturally in for-comprehensions alongside other Result-returning code:

1
2
3
4
5
6
7
8
for {
  providerConfig <- Llm4sConfig.provider()
  client         <- LLMConnect.getClient(providerConfig)
  tools          <- BuiltinTools.coreSafe
  agent           = new Agent(client)
  registry        = new ToolRegistry(tools)
  state          <- agent.run("What time is it?", registry)
} yield state

Why lazy val for Deprecated Wrappers

The deprecated tool vals (DateTimeTool.tool, etc.) are lazy val rather than val intentionally.

A regular val is evaluated eagerly when the enclosing object is first loaded. This means accessing DateTimeTool.toolSafe (the safe API) would also trigger evaluation of DateTimeTool.tool (the deprecated one), potentially throwing before you ever called the deprecated method.

lazy val defers evaluation until the member is explicitly accessed, so only callers of the deprecated API see the exception. Users of toolSafe are unaffected.


Compiler Warnings

After migrating, enable fatal deprecation warnings to catch any remaining uses:

1
2
// build.sbt
scalacOptions += "-Xfatal-warnings"

Or check for remaining deprecated usages without failing the build:

1
grep -r "\.tool\b\|\.build()\|BuiltinTools\.core\b\|BuiltinTools\.safe\b" modules/