Kastrax AI Agent Tools System ✅
Tools are a fundamental component of the Kastrax AI Agent framework that extend agents’ capabilities beyond conversation. They allow agents to perform specific actions, access external systems, process data, and interact with the world. In Kastrax, tools are implemented as strongly-typed Kotlin functions with well-defined input/output schemas, execution logic, and error handling.
Tool Architecture in Kastrax ✅
Kastrax implements a robust tool system with several key components:
- Tool Definition: A strongly-typed interface that defines the tool’s inputs, outputs, and metadata
- Tool Implementation: The actual execution logic that performs the tool’s function
- Tool Registry: A central registry that makes tools available to agents
- Tool Invocation: The mechanism by which agents call tools during execution
This architecture ensures type safety, proper error handling, and seamless integration with the agent system.
Creating Basic Tools ✅
Let’s create a simple weather information tool that demonstrates the basic structure of Kastrax tools:
import ai.kastrax.core.tool.Tool
import ai.kastrax.core.tool.ToolDefinition
import ai.kastrax.core.tool.ToolResult
import kotlinx.serialization.Serializable
// Define the input parameters for the tool
@Serializable
data class WeatherToolInput(
val city: String,
val units: String = "metric" // Optional parameter with default value
)
// Define the output structure
@Serializable
data class WeatherToolOutput(
val temperature: Double,
val conditions: String,
val humidity: Int,
val windSpeed: Double
)
// Implement the tool
class WeatherTool : Tool<WeatherToolInput, WeatherToolOutput> {
// Define tool metadata
override val definition = ToolDefinition(
name = "get_weather",
description = "Fetches current weather information for a specified city",
inputType = WeatherToolInput::class,
outputType = WeatherToolOutput::class
)
// Implement the execution logic
override suspend fun execute(input: WeatherToolInput): ToolResult<WeatherToolOutput> {
return try {
// In a real implementation, this would call a weather API
val weatherData = fetchWeatherData(input.city, input.units)
ToolResult.Success(
WeatherToolOutput(
temperature = weatherData.temperature,
conditions = weatherData.conditions,
humidity = weatherData.humidity,
windSpeed = weatherData.windSpeed
)
)
} catch (e: Exception) {
ToolResult.Error("Failed to fetch weather data: ${e.message}")
}
}
// Helper method to fetch weather data (would be implemented with actual API calls)
private suspend fun fetchWeatherData(city: String, units: String): WeatherData {
// This would be an actual API call in a real implementation
// For demonstration, we're returning mock data
return WeatherData(22.5, "Sunny", 65, 10.2)
}
// Internal data class for the API response
private data class WeatherData(
val temperature: Double,
val conditions: String,
val humidity: Int,
val windSpeed: Double
)
}
Adding Tools to an Agent ✅
Once you’ve created your tools, you need to register them with your agent. In Kastrax, this is done by adding the tools to the agent’s configuration:
import ai.kastrax.core.agent.Agent
import ai.kastrax.core.agent.AgentConfig
import ai.kastrax.core.llm.DeepSeekProvider
import ai.kastrax.tools.WeatherTool
class WeatherAgent {
// Create an instance of the weather tool
private val weatherTool = WeatherTool()
// Create the agent with the tool
val agent = Agent(
config = AgentConfig(
name = "WeatherAssistant",
description = "An assistant that can provide weather information for cities around the world",
instructions = """
You are a helpful weather assistant that provides accurate weather information.
When asked about the weather in a specific location, use the get_weather tool to fetch current data.
Present the information in a clear, concise format including temperature, conditions, humidity, and wind speed.
Always specify whether temperatures are in Celsius (metric) or Fahrenheit (imperial) based on the units used.
""".trimIndent(),
llmProvider = DeepSeekProvider(apiKey = "your-deepseek-api-key"),
tools = listOf(weatherTool)
)
)
}
Registering Tools in the Application ✅
In a larger application, you might want to register your tools centrally so they can be used by multiple agents:
import ai.kastrax.core.KastraxSystem
import ai.kastrax.tools.WeatherTool
import ai.kastrax.tools.CalculatorTool
import ai.kastrax.tools.SearchTool
import ai.kastrax.agents.WeatherAgent
class Application {
// Initialize the Kastrax system
private val system = KastraxSystem()
// Register tools centrally
init {
// Register individual tools
system.registerTool(WeatherTool())
system.registerTool(CalculatorTool())
system.registerTool(SearchTool())
// Register agents
system.registerAgent(WeatherAgent().agent)
}
fun start() {
// Start the application
system.start()
}
}
This approach allows you to maintain a central registry of tools that can be shared across different agents in your application.
Handling Cancellation ✅
Kastrax supports cancellation of tool execution through Kotlin’s built-in cancellation mechanism. This allows you to gracefully handle situations where a tool execution needs to be stopped:
import ai.kastrax.core.tool.Tool
import ai.kastrax.core.tool.ToolResult
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.Dispatchers
class LongRunningTool : Tool<LongRunningToolInput, LongRunningToolOutput> {
// Tool definition omitted for brevity
override suspend fun execute(input: LongRunningToolInput): ToolResult<LongRunningToolOutput> {
return try {
// Set a timeout for the operation
withTimeout(30000) { // 30 seconds timeout
withContext(Dispatchers.IO) {
// Check for cancellation periodically
kotlinx.coroutines.ensureActive()
// Perform long-running operation
val result = performLongRunningTask(input)
// Check again before returning
kotlinx.coroutines.ensureActive()
ToolResult.Success(result)
}
}
} catch (e: CancellationException) {
// Handle cancellation gracefully
ToolResult.Error("Operation was cancelled")
} catch (e: Exception) {
ToolResult.Error("Operation failed: ${e.message}")
}
}
}
When using the agent, you can provide a cancellation handler:
val job = kotlinx.coroutines.launch {
agent.generate(
input = "Perform a long-running analysis",
sessionId = "user-123",
conversationId = "analysis-session"
)
}
// Cancel the operation after some condition
if (userRequestedCancellation) {
job.cancel()
}
Context and Configuration Injection ✅
Kastrax allows you to inject context and configuration into tools, making them more flexible and adaptable to different scenarios:
import ai.kastrax.core.tool.Tool
import ai.kastrax.core.tool.ToolContext
import ai.kastrax.core.tool.ToolResult
class ConfigurableWeatherTool : Tool<WeatherToolInput, WeatherToolOutput> {
// Tool definition omitted for brevity
override suspend fun execute(
input: WeatherToolInput,
context: ToolContext = ToolContext()
): ToolResult<WeatherToolOutput> {
// Get user preferences from context
val temperatureUnit = context.getPreference("temperature_unit") ?: "celsius"
val language = context.getPreference("language") ?: "en"
// Get API configuration from context
val apiKey = context.getConfig("weather_api_key")
?: return ToolResult.Error("Weather API key not configured")
return try {
// Use the context information in the API call
val weatherData = fetchWeatherData(
city = input.city,
units = if (temperatureUnit == "celsius") "metric" else "imperial",
language = language,
apiKey = apiKey
)
ToolResult.Success(weatherData.toOutput())
} catch (e: Exception) {
ToolResult.Error("Failed to fetch weather data: ${e.message}")
}
}
}
When using the tool, you can provide the context:
// Create a tool context with user preferences and configuration
val toolContext = ToolContext().apply {
// User preferences
setPreference("temperature_unit", "fahrenheit")
setPreference("language", "en-US")
// System configuration
setConfig("weather_api_key", "your-api-key-here")
}
// Use the context when generating a response
val response = agent.generate(
input = "What's the weather in Tokyo?",
sessionId = "user-456",
conversationId = "weather-session",
toolContext = toolContext
)
Testing and Debugging Tools ✅
Kastrax provides robust support for testing and debugging tools. Here’s how to write tests for your tools using Kotlin’s testing frameworks:
import ai.kastrax.core.tool.ToolResult
import ai.kastrax.tools.WeatherTool
import ai.kastrax.tools.WeatherToolInput
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
class WeatherToolTest {
private val weatherTool = WeatherTool()
@Test
fun `test successful weather retrieval`() = runBlocking {
// Arrange
val input = WeatherToolInput(city = "Tokyo", units = "metric")
// Act
val result = weatherTool.execute(input)
// Assert
assertTrue(result is ToolResult.Success)
if (result is ToolResult.Success) {
val output = result.value
assertEquals("Sunny", output.conditions)
// Add more assertions as needed
}
}
@Test
fun `test error handling for invalid city`() = runBlocking {
// Arrange
val input = WeatherToolInput(city = "", units = "metric")
// Act
val result = weatherTool.execute(input)
// Assert
assertTrue(result is ToolResult.Error)
if (result is ToolResult.Error) {
assertTrue(result.message.contains("city"))
}
}
}
Using Tools in Practice ✅
Let’s see a complete example of how to use a tool with an agent in a real application:
import ai.kastrax.agents.WeatherAgent
import ai.kastrax.core.tool.ToolContext
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
// Create the weather agent
val weatherAgent = WeatherAgent().agent
// Create a tool context with necessary configuration
val toolContext = ToolContext().apply {
setConfig("weather_api_key", System.getenv("WEATHER_API_KEY") ?: "demo-key")
setPreference("temperature_unit", "celsius")
}
// Generate a response using the agent and its tools
val response = weatherAgent.generate(
input = "What's the current weather in Paris, France?",
sessionId = "user-789",
conversationId = "weather-inquiry",
toolContext = toolContext
)
// Print the response
println("Agent response: ${response.text}")
// The agent will automatically use the WeatherTool to fetch and include
// current weather information for Paris in its response
}
The agent will use the WeatherTool
to fetch the current weather in Paris and incorporate that information into its response.
Integration with External Systems ✅
Kastrax tools can integrate with various external systems and APIs. Here’s an example of a tool that integrates with a database:
import ai.kastrax.core.tool.Tool
import ai.kastrax.core.tool.ToolDefinition
import ai.kastrax.core.tool.ToolResult
import kotlinx.serialization.Serializable
import java.sql.Connection
import java.sql.DriverManager
@Serializable
data class QueryInput(
val query: String,
val limit: Int = 10
)
@Serializable
data class QueryResult(
val columns: List<String>,
val rows: List<List<String>>
)
class DatabaseTool : Tool<QueryInput, QueryResult> {
override val definition = ToolDefinition(
name = "database_query",
description = "Execute a SQL query against the database to retrieve information",
inputType = QueryInput::class,
outputType = QueryResult::class
)
private var connection: Connection? = null
private fun getConnection(context: ToolContext): Connection {
if (connection == null || connection?.isClosed == true) {
val jdbcUrl = context.getConfig("jdbc_url")
?: throw IllegalStateException("Database URL not configured")
val username = context.getConfig("db_username") ?: ""
val password = context.getConfig("db_password") ?: ""
connection = DriverManager.getConnection(jdbcUrl, username, password)
}
return connection!!
}
override suspend fun execute(input: QueryInput, context: ToolContext): ToolResult<QueryResult> {
return try {
// Sanitize and validate the query for security
val sanitizedQuery = sanitizeQuery(input.query, input.limit)
// Execute the query
val conn = getConnection(context)
val statement = conn.createStatement()
val resultSet = statement.executeQuery(sanitizedQuery)
// Process the results
val metadata = resultSet.metaData
val columnCount = metadata.columnCount
// Extract column names
val columns = (1..columnCount).map { metadata.getColumnName(it) }
// Extract rows
val rows = mutableListOf<List<String>>()
while (resultSet.next() && rows.size < input.limit) {
val row = (1..columnCount).map {
resultSet.getString(it) ?: "null"
}
rows.add(row)
}
ToolResult.Success(QueryResult(columns, rows))
} catch (e: Exception) {
ToolResult.Error("Database query failed: ${e.message}")
}
}
private fun sanitizeQuery(query: String, limit: Int): String {
// Implement query sanitization and validation
// This is a simplified example - in production, use prepared statements
// and more sophisticated validation
if (!query.lowercase().startsWith("select")) {
throw IllegalArgumentException("Only SELECT queries are allowed")
}
return if (query.lowercase().contains(" limit ")) {
query
} else {
"$query LIMIT $limit"
}
}
}
Tool Design Best Practices ✅
When creating tools for your Kastrax agents, follow these guidelines to ensure reliable and intuitive tool usage:
Tool Descriptions
Your tool’s description should focus on its purpose and value:
- Keep descriptions simple and focused on what the tool does
- Emphasize the tool’s primary use case
- Avoid implementation details in the main description
- Focus on helping the agent understand when to use the tool
ToolDefinition(
name = "document_search",
description = "Search the knowledge base to find information needed to answer user questions",
inputType = SearchInput::class,
outputType = SearchResult::class
)
Input and Output Types
Design clear, well-structured input and output types:
- Make parameters self-documenting with clear names
- Include default values for optional parameters
- Use appropriate types for each parameter
- Document the purpose of each field
@Serializable
data class SearchInput(
val query: String, // The search query to find relevant information
val limit: Int = 10, // Number of results to return
val filters: Map<String, String> = mapOf() // Optional filters to apply
)
Error Handling
Implement robust error handling in your tools:
- Return clear error messages that explain what went wrong
- Handle expected failure cases gracefully
- Provide guidance on how to fix the issue when possible
- Log detailed error information for debugging
Security Considerations
Ensure your tools are secure:
- Validate and sanitize all inputs
- Use prepared statements for database queries
- Implement proper authentication and authorization
- Limit access to sensitive operations
- Don’t expose sensitive information in error messages
Advanced Tool Features ✅
Kastrax supports several advanced tool features:
Tool Composition
You can compose multiple tools together to create more complex functionality:
class CompositeSearchTool(
private val webSearchTool: WebSearchTool,
private val databaseTool: DatabaseTool,
private val documentTool: DocumentTool
) : Tool<CompositeSearchInput, CompositeSearchResult> {
// Implementation that uses multiple tools together
}
Tool Versioning
Implement versioning for your tools to maintain backward compatibility:
class WeatherToolV2 : Tool<WeatherToolInputV2, WeatherToolOutputV2> {
override val definition = ToolDefinition(
name = "get_weather_v2",
description = "Enhanced weather information tool with additional data",
inputType = WeatherToolInputV2::class,
outputType = WeatherToolOutputV2::class,
version = "2.0"
)
// Implementation
}
Tool Metrics and Monitoring
Implement metrics collection for your tools:
class MonitoredTool<I : Any, O : Any>(private val delegate: Tool<I, O>) : Tool<I, O> {
override val definition = delegate.definition
override suspend fun execute(input: I, context: ToolContext): ToolResult<O> {
val startTime = System.currentTimeMillis()
try {
val result = delegate.execute(input, context)
val duration = System.currentTimeMillis() - startTime
recordMetrics(definition.name, duration, result is ToolResult.Success)
return result
} catch (e: Exception) {
val duration = System.currentTimeMillis() - startTime
recordMetrics(definition.name, duration, false)
throw e
}
}
private fun recordMetrics(toolName: String, durationMs: Long, success: Boolean) {
// Record metrics to your monitoring system
}
}
Integration with Actor Model ✅
One of Kastrax’s unique features is the integration of tools with the actor model, allowing for distributed tool execution:
import ai.kastrax.actor.ActorRef
import ai.kastrax.actor.ActorSystem
import ai.kastrax.tools.WeatherTool
class ToolActorSystem(private val system: ActorSystem) {
// Create actor references for tools
val weatherToolActor: ActorRef = system.actorOf(WeatherToolActor.props(WeatherTool()))
val databaseToolActor: ActorRef = system.actorOf(DatabaseToolActor.props(DatabaseTool()))
// Tool actor message types
data class ToolRequest<I>(val input: I, val replyTo: ActorRef)
data class ToolResponse<O>(val result: ToolResult<O>)
// Example of using a tool through the actor system
suspend fun getWeather(city: String): ToolResult<WeatherToolOutput> {
val input = WeatherToolInput(city = city)
val response = system.ask<ToolResponse<WeatherToolOutput>>(
weatherToolActor,
ToolRequest(input, system.selfRef())
)
return response.result
}
}
This integration enables building sophisticated multi-agent systems where tools can be distributed across different nodes and executed concurrently.