Phase 2.2: Async Tool Execution
Phase 2.2: Async Tool Execution
Status: Complete Last Updated: 2025-11-26 Related: Agent Framework Roadmap
Executive Summary
Phase 2.2 adds asynchronous tool execution to the agent framework, enabling:
- Parallel execution of independent tools
- Non-blocking tool operations (HTTP calls, database queries)
- Better resource utilization for multi-tool agents
- Configurable execution strategies (parallel vs sequential)
Motivation
Current Limitations
The current tool execution is synchronous and sequential:
// Current: Each tool blocks until complete
val toolMessages = toolCalls.map { toolCall =>
val result = toolRegistry.execute(request) // BLOCKS
ToolMessage(result, toolCall.id)
}
Problems:
- Tool 2 waits for Tool 1 even if theyβre independent
- HTTP/IO-bound tools block the entire agent
- Multi-tool queries are slow (latency adds up)
- No parallelization even when safe
Benefits of Async Execution
- Parallel Execution: Independent tools run simultaneously
- Non-blocking: Agent can progress while tools execute
- Better Latency: Multi-tool queries complete faster
- Resource Efficiency: IO-bound tools donβt waste CPU
Architecture
Design Principles
- Backward Compatible: Existing sync tools continue to work
- Opt-in Async: New async tools via separate trait
- Configurable Strategy: Choose parallel or sequential
- Type Safe: Use existing
AsyncResult[A]type
Execution Strategies
sealed trait ToolExecutionStrategy
object ToolExecutionStrategy {
/** Execute tools one at a time (current behavior) */
case object Sequential extends ToolExecutionStrategy
/** Execute all tools in parallel */
case object Parallel extends ToolExecutionStrategy
/** Execute tools in parallel with concurrency limit */
case class ParallelWithLimit(maxConcurrency: Int) extends ToolExecutionStrategy
}
AsyncToolFunction
New trait for tools that can execute asynchronously:
trait AsyncToolFunction[T, R] {
def name: String
def description: String
def schema: SchemaDefinition[T]
/** Execute asynchronously, returning Future[Result[R]] */
def executeAsync(args: ujson.Value)(implicit ec: ExecutionContext): AsyncResult[ujson.Value]
}
Unified Tool Execution
The ToolRegistry provides unified execution:
class ToolRegistry(tools: Seq[ToolFunction[_, _]]) {
/** Synchronous execution (existing) */
def execute(request: ToolCallRequest): Either[ToolCallError, ujson.Value]
/** Async execution - wraps sync tools if needed */
def executeAsync(request: ToolCallRequest)(implicit ec: ExecutionContext): Future[Either[ToolCallError, ujson.Value]]
/** Execute multiple tools with strategy */
def executeAll(
requests: Seq[ToolCallRequest],
strategy: ToolExecutionStrategy = ToolExecutionStrategy.Parallel
)(implicit ec: ExecutionContext): Future[Seq[Either[ToolCallError, ujson.Value]]]
}
Agent Integration
New method in Agent for async tool processing:
class Agent(client: LLMClient) {
/** Run with configurable tool execution strategy */
def runWithStrategy(
query: String,
tools: ToolRegistry,
toolExecutionStrategy: ToolExecutionStrategy = ToolExecutionStrategy.Sequential,
// ... other params
): Result[AgentState]
/** Process tool calls asynchronously */
private def processToolCallsAsync(
state: AgentState,
toolCalls: Seq[ToolCall],
strategy: ToolExecutionStrategy,
debug: Boolean
)(implicit ec: ExecutionContext): Future[AgentState]
}
Implementation Plan
Phase 1: Core Infrastructure
- Add
ToolExecutionStrategyenum - Add
executeAsynctoToolRegistry - Add
executeAllfor batch execution
Phase 2: Agent Integration
- Add
processToolCallsAsyncmethod - Add
runWithStrategymethod - Update streaming methods for async
Phase 3: Async Tool Definition
- Create
AsyncToolFunctiontrait - Add
AsyncToolBuilderfor easy creation - Update
ToolRegistryto handle both types
Usage Examples
Parallel Tool Execution
val result = agent.runWithStrategy(
query = "Get weather in London, Paris, and Tokyo",
tools = weatherTools,
toolExecutionStrategy = ToolExecutionStrategy.Parallel
)
// All 3 weather calls execute simultaneously!
Creating Async Tools
val asyncWeatherTool = AsyncToolBuilder[WeatherInput, WeatherOutput](
name = "get_weather_async",
description = "Get weather asynchronously",
schema = weatherSchema
).withAsyncHandler { extractor =>
extractor.getString("city").fold(
error => Future.successful(Left(error)),
city => weatherApi.getWeather(city) // Returns Future[Result[WeatherOutput]]
)
}.build()
Mixed Sync/Async Registry
val registry = new ToolRegistry(
syncTools = Seq(calculatorTool, dateTool),
asyncTools = Seq(asyncWeatherTool, asyncSearchTool)
)
// executeAsync works for both - sync tools are wrapped
registry.executeAsync(request)
Backward Compatibility
- Existing
Agent.run()continues to work unchanged - Existing sync tools work with no modifications
ToolRegistry.execute()remains synchronous- New async features are opt-in via
runWithStrategy()
Testing Strategy
- Unit Tests: Async execution, strategy selection
- Integration Tests: Mixed sync/async tool execution
- Performance Tests: Parallel vs sequential timing
- Concurrency Tests: Thread safety, race conditions
File Structure
modules/core/src/main/scala/org/llm4s/
βββ toolapi/
β βββ ToolExecutionStrategy.scala # NEW: Execution strategies (Sequential, Parallel, ParallelWithLimit)
β βββ ToolRegistry.scala # Updated: Add executeAsync(), executeAll()
βββ agent/
βββ Agent.scala # Updated: Add runWithStrategy(), continueConversationWithStrategy()
Implementation Notes
What Was Implemented
- ToolExecutionStrategy - Sealed trait with three strategies:
Sequential- Execute one at a time (default, safest)Parallel- Execute all simultaneously (fastest)ParallelWithLimit(n)- Max n concurrent executions
- ToolRegistry Enhancements:
executeAsync()- Single tool async executionexecuteAll()- Batch execution with configurable strategy- Private helpers:
executeSequential(),executeParallel(),executeWithLimit()
- Agent Methods:
runWithStrategy()- Run agent with parallel tool executioncontinueConversationWithStrategy()- Continue conversation with strategyprocessToolCallsAsync()- Internal async tool processing
What Was Deferred
The following features from the original design were deferred for future work:
AsyncToolFunctiontrait for natively async toolsAsyncToolBuilderfor creating async tools- Mixed sync/async tool registry
These werenβt needed for the core use case of parallelizing existing synchronous tools.
Samples
- ToolRegistry example:
samples/runMain org.llm4s.samples.toolapi.ParallelToolExecutionExample - Agent example:
samples/runMain org.llm4s.samples.agent.AsyncToolAgentExample
Tests
core/testOnly org.llm4s.toolapi.AsyncToolExecutionSpec- 11 tests covering all strategies