ToolExecutor
ToolExecutor is the interface for implementing custom tools that agents can call during conversations. Tools execute client-side, providing secure access to local device capabilities and custom business logic.
Interface Definition
interface ToolExecutor {
val tool: Tool
suspend fun execute(context: ToolExecutionContext): ToolExecutionResult
fun validate(toolCall: ToolCall): ToolValidationResult
fun canExecute(toolCall: ToolCall): Boolean
fun getMaxExecutionTimeMs(): Long?
}Core Components
Tool Definition
Every executor must define its tool:
override val tool = Tool(
name = "calculator",
description = "Perform mathematical calculations",
parameters = buildJsonObject {
put("type", "object")
put("properties", buildJsonObject {
put("expression", buildJsonObject {
put("type", "string")
put("description", "Mathematical expression to evaluate")
})
})
},
required = listOf("expression")
)Execution Context
Tools receive context during execution:
data class ToolExecutionContext(
val toolCall: ToolCall,
val threadId: String? = null,
val runId: String? = null,
val metadata: Map<String, Any> = emptyMap()
)Execution Result
Tools return structured results:
data class ToolExecutionResult(
val success: Boolean,
val result: JsonElement? = null,
val message: String? = null
)
// Convenience methods
ToolExecutionResult.success(result = JsonPrimitive("42"))
ToolExecutionResult.failure("Invalid expression")Implementation
Basic Implementation
class CalculatorToolExecutor : ToolExecutor {
override val tool = Tool(
name = "calculator",
description = "Perform basic calculations",
parameters = buildJsonObject {
put("type", "object")
put("properties", buildJsonObject {
put("expression", buildJsonObject {
put("type", "string")
})
})
},
required = listOf("expression")
)
override suspend fun execute(context: ToolExecutionContext): ToolExecutionResult {
val args = context.toolCall.function.arguments.jsonObject
val expression = args["expression"]?.jsonPrimitive?.content
?: return ToolExecutionResult.failure("Missing expression")
return try {
val result = evaluateExpression(expression)
ToolExecutionResult.success(
result = JsonPrimitive(result),
message = "$expression = $result"
)
} catch (e: Exception) {
ToolExecutionResult.failure("Calculation error: ${e.message}")
}
}
private fun evaluateExpression(expression: String): Double {
// Implementation details...
return 42.0
}
}Using AbstractToolExecutor
For common error handling patterns:
class WeatherToolExecutor : AbstractToolExecutor(weatherTool) {
override suspend fun executeInternal(context: ToolExecutionContext): ToolExecutionResult {
val location = extractLocation(context.toolCall)
val weather = fetchWeather(location)
return ToolExecutionResult.success(
result = buildJsonObject {
put("temperature", weather.temperature)
put("condition", weather.condition)
}
)
}
override fun validate(toolCall: ToolCall): ToolValidationResult {
val args = toolCall.function.arguments.jsonObject
if (!args.containsKey("location")) {
return ToolValidationResult.failure("Missing required parameter: location")
}
val location = args["location"]?.jsonPrimitive?.content
if (location.isNullOrBlank()) {
return ToolValidationResult.failure("Location cannot be empty")
}
return ToolValidationResult.success()
}
}Methods
execute
Execute the tool with given context:
override suspend fun execute(context: ToolExecutionContext): ToolExecutionResult {
// Access tool call arguments
val args = context.toolCall.function.arguments.jsonObject
// Perform tool logic
val result = performOperation(args)
// Return structured result
return ToolExecutionResult.success(result)
}validate
Validate tool call arguments:
override fun validate(toolCall: ToolCall): ToolValidationResult {
val args = toolCall.function.arguments.jsonObject
val errors = mutableListOf<String>()
// Check required parameters
if (!args.containsKey("required_param")) {
errors.add("Missing required parameter: required_param")
}
// Validate parameter values
val value = args["number"]?.jsonPrimitive?.doubleOrNull
if (value != null && value < 0) {
errors.add("Number must be non-negative")
}
return if (errors.isEmpty()) {
ToolValidationResult.success()
} else {
ToolValidationResult.failure(errors)
}
}canExecute
Check if executor can handle a tool call:
override fun canExecute(toolCall: ToolCall): Boolean {
// Default implementation matches by name
return toolCall.function.name == tool.name
// Custom logic example:
// return toolCall.function.name == tool.name &&
// hasRequiredPermissions()
}getMaxExecutionTimeMs
Set execution timeout:
override fun getMaxExecutionTimeMs(): Long? {
return 30_000 // 30 seconds
// Or no timeout:
// return null
}Error Handling
Validation Errors
Return validation failures for invalid arguments:
override fun validate(toolCall: ToolCall): ToolValidationResult {
val email = getEmailParameter(toolCall)
if (!email.contains("@")) {
return ToolValidationResult.failure("Invalid email format")
}
return ToolValidationResult.success()
}Execution Errors
Handle different error types during execution:
override suspend fun execute(context: ToolExecutionContext): ToolExecutionResult {
return try {
val result = performRiskyOperation()
ToolExecutionResult.success(result)
} catch (e: IllegalArgumentException) {
// Expected validation error
ToolExecutionResult.failure("Invalid input: ${e.message}")
} catch (e: IOException) {
// Network/IO error
ToolExecutionResult.failure("Network error: ${e.message}")
} catch (e: SecurityException) {
// Permission error
ToolExecutionResult.failure("Permission denied: ${e.message}")
} catch (e: Exception) {
// Unrecoverable error - let framework handle
throw ToolExecutionException(
message = "Tool execution failed: ${e.message}",
cause = e,
toolName = tool.name,
toolCallId = context.toolCall.id
)
}
}Best Practices
Parameter Extraction
Create helper methods for common parameters:
class LocationToolExecutor : ToolExecutor {
private fun extractLocation(toolCall: ToolCall): String {
return toolCall.function.arguments.jsonObject["location"]
?.jsonPrimitive?.content
?: throw IllegalArgumentException("Missing location parameter")
}
private fun extractUnits(toolCall: ToolCall): String {
return toolCall.function.arguments.jsonObject["units"]
?.jsonPrimitive?.content
?: "metric" // Default value
}
}Resource Management
Always clean up resources:
class FileToolExecutor : ToolExecutor {
override suspend fun execute(context: ToolExecutionContext): ToolExecutionResult {
var inputStream: InputStream? = null
return try {
val filename = extractFilename(context.toolCall)
inputStream = File(filename).inputStream()
val content = inputStream.readText()
ToolExecutionResult.success(JsonPrimitive(content))
} catch (e: IOException) {
ToolExecutionResult.failure("File error: ${e.message}")
} finally {
inputStream?.close()
}
}
}Async Operations
Use proper coroutine patterns:
class FileToolExecutor : ToolExecutor {
override suspend fun execute(context: ToolExecutionContext): ToolExecutionResult {
return try {
// Use suspend functions for file I/O
val filename = extractFilename(context.toolCall)
val content = File(filename).readText()
ToolExecutionResult.success(JsonPrimitive(content))
} catch (e: IOException) {
ToolExecutionResult.failure("File error: ${e.message}")
}
}
override fun getMaxExecutionTimeMs(): Long = 10_000 // 10 seconds for large files
}Platform-Specific Implementation
Handle platform differences:
expect class PlatformLocationProvider {
suspend fun getCurrentLocation(): Location
}
class LocationToolExecutor : ToolExecutor {
private val locationProvider = PlatformLocationProvider()
override suspend fun execute(context: ToolExecutionContext): ToolExecutionResult {
return try {
val location = locationProvider.getCurrentLocation()
ToolExecutionResult.success(buildJsonObject {
put("latitude", location.latitude)
put("longitude", location.longitude)
})
} catch (e: SecurityException) {
ToolExecutionResult.failure("Location permission required")
}
}
}Tool Definition Builder
Use builders for complex tool definitions:
private val complexTool = Tool(
name = "data_query",
description = "Query data with filters and sorting",
parameters = buildJsonObject {
put("type", "object")
put("properties", buildJsonObject {
put("query", buildJsonObject {
put("type", "string")
put("description", "Search query")
})
put("filters", buildJsonObject {
put("type", "object")
put("properties", buildJsonObject {
put("category", buildJsonObject {
put("type", "string")
put("enum", buildJsonArray {
add("books")
add("movies")
add("music")
})
})
put("minRating", buildJsonObject {
put("type", "number")
put("minimum", 0)
put("maximum", 5)
})
})
})
put("sortBy", buildJsonObject {
put("type", "string")
put("enum", buildJsonArray {
add("name")
add("rating")
add("date")
})
})
})
},
required = listOf("query")
)Testing
Write comprehensive tests for tools:
class CalculatorToolExecutorTest {
private val calculator = CalculatorToolExecutor()
@Test
fun testBasicAddition() = runTest {
val toolCall = ToolCall(
id = "test-1",
function = ToolCall.Function(
name = "calculator",
arguments = buildJsonObject {
put("expression", "2 + 3")
}
)
)
val context = ToolExecutionContext(toolCall)
val result = calculator.execute(context)
assertTrue(result.success)
assertEquals(5.0, result.result?.jsonPrimitive?.double)
}
@Test
fun testInvalidExpression() = runTest {
val toolCall = ToolCall(
id = "test-2",
function = ToolCall.Function(
name = "calculator",
arguments = buildJsonObject {
put("expression", "invalid")
}
)
)
val context = ToolExecutionContext(toolCall)
val result = calculator.execute(context)
assertFalse(result.success)
assertNotNull(result.message)
}
}