Skip to main content
Tools are the actions your AI agent can perform. Koog provides built-in tools and makes it easy to create custom ones.

Tool Basics

A tool is a Kotlin class that implements Tool<TArgs, TResult> with:
  • Arguments (TArgs): Parameters the LLM provides when calling the tool
  • Result (TResult): What the tool returns to the LLM
  • Annotations: Descriptions that help the LLM understand when and how to use the tool

Creating Custom Tools

Using ToolSet and Annotations

The easiest way to create tools is with the @Tool annotation and ToolSet:
WeatherTools.kt
import ai.koog.agents.core.tools.annotations.Tool
import ai.koog.agents.core.tools.annotations.LLMDescription
import ai.koog.agents.core.tools.reflect.ToolSet

class WeatherTools(private val apiClient: OpenMeteoClient) : ToolSet {
    @Tool
    @LLMDescription("Get weather forecast for a location and date range")
    suspend fun getWeatherForecast(
        @LLMDescription("Location name (e.g., 'New York', 'London', 'Paris')")
        location: String,
        
        @LLMDescription("ISO 3166-1 alpha-2 country code (e.g., 'US', 'GB', 'FR')")
        countryCodeISO2: String,
        
        @LLMDescription("Start date in ISO format (e.g., '2023-05-20')")
        startDate: String,
        
        @LLMDescription("End date in ISO format (e.g., '2023-05-20')")
        endDate: String,
        
        @LLMDescription("Forecast granularity")
        granularity: ForecastGranularity
    ): String {
        val location = apiClient.searchLocation(location)
            .find { it.countryCode == countryCodeISO2 }
            ?: return "Location not found"
        
        val forecast = apiClient.getWeatherForecast(
            latitude = location.latitude,
            longitude = location.longitude,
            startDate = LocalDate.parse(startDate),
            endDate = LocalDate.parse(endDate),
            granularity = granularity
        )
        
        return formatForecast(forecast)
    }
}

enum class ForecastGranularity {
    DAILY,
    HOURLY
}

Registering Tools from ToolSet

Convert your ToolSet to tools and register them:
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.core.tools.reflect.asTools

val apiClient = OpenMeteoClient()

val toolRegistry = ToolRegistry {
    tools(WeatherTools(apiClient).asTools())
}

Built-in Tools

Koog provides ready-to-use tools for common operations:

File Tools

import ai.koog.agents.ext.tool.file.*
import ai.koog.rag.base.files.JVMFileSystemProvider

val toolRegistry = ToolRegistry {
    tool(ListDirectoryTool(JVMFileSystemProvider.ReadOnly))
    tool(ReadFileTool(JVMFileSystemProvider.ReadOnly))
    tool(EditFileTool(JVMFileSystemProvider.ReadWrite))
}

Shell Tools

import ai.koog.agents.ext.tool.shell.*

val toolRegistry = ToolRegistry {
    tool(ExecuteShellCommandTool(
        executor = JvmShellCommandExecutor(),
        confirmationHandler = PrintShellCommandConfirmationHandler()
    ))
}

User Interaction Tools

import ai.koog.agents.ext.tool.SayToUser

val toolRegistry = ToolRegistry {
    tool(SayToUser)
}

Advanced Tool Implementation

Manual Tool Implementation

For more control, implement the Tool interface directly:
import ai.koog.agents.core.tools.Tool
import ai.koog.agents.core.tools.ToolResult
import kotlinx.serialization.Serializable

object CalculatorTool : Tool<CalculatorTool.Args, ToolResult.Text> {
    @Serializable
    data class Args(
        val operation: String,
        val a: Double,
        val b: Double
    )
    
    override val name = "calculator"
    override val description = "Perform basic math operations"
    override val argsType = typeOf<Args>()
    override val resultType = typeOf<ToolResult.Text>()
    
    override suspend fun execute(args: Args): ToolResult.Text {
        val result = when (args.operation) {
            "add" -> args.a + args.b
            "subtract" -> args.a - args.b
            "multiply" -> args.a * args.b
            "divide" -> args.a / args.b
            else -> throw IllegalArgumentException("Unknown operation")
        }
        return ToolResult.Text("Result: $result")
    }
}

Stateful Tools

Tools can maintain state for tracking operations:
class OrderTools(private val orderStore: OrderStore) : ToolSet {
    @Tool
    @LLMDescription("Create a new order")
    suspend fun createOrder(
        @LLMDescription("Customer ID")
        customerId: String,
        
        @LLMDescription("List of items")
        items: List<String>
    ): String {
        val order = Order(
            id = UUID.randomUUID().toString(),
            customerId = customerId,
            items = items,
            status = OrderStatus.PENDING
        )
        
        orderStore.save(order)
        return "Order ${order.id} created successfully"
    }
    
    @Tool
    @LLMDescription("Get order status")
    suspend fun getOrderStatus(
        @LLMDescription("Order ID")
        orderId: String
    ): String {
        val order = orderStore.get(orderId)
            ?: return "Order not found"
        
        return "Order ${order.id} status: ${order.status}"
    }
}

Tool Best Practices

Clear Descriptions

Provide detailed descriptions to help the LLM understand when to use each tool:
@Tool
@LLMDescription(
    "Search for products in the inventory by name, category, or SKU. " +
    "Returns up to 10 matching products with pricing and availability."
)
suspend fun searchProducts(
    @LLMDescription("Search query (product name, category, or SKU)")
    query: String,
    
    @LLMDescription("Maximum number of results (1-50, default 10)")
    limit: Int = 10
): List<Product>

Error Handling

Handle errors gracefully and return meaningful messages:
@Tool
@LLMDescription("Fetch user account details")
suspend fun getUserAccount(
    @LLMDescription("User ID")
    userId: String
): String {
    return try {
        val account = accountService.getAccount(userId)
        "Account found: ${account.email}, Balance: ${account.balance}"
    } catch (e: NotFoundException) {
        "User account not found for ID: $userId"
    } catch (e: Exception) {
        "Error fetching account: ${e.message}"
    }
}

Type Safety

Use serializable data classes for complex arguments:
@Serializable
data class PaymentRequest(
    val amount: Double,
    val currency: String,
    val method: PaymentMethod
)

@Serializable
enum class PaymentMethod {
    CREDIT_CARD,
    DEBIT_CARD,
    PAYPAL,
    BANK_TRANSFER
}

@Tool
@LLMDescription("Process a payment")
suspend fun processPayment(
    @LLMDescription("Payment details")
    request: PaymentRequest
): String {
    // Process payment...
}

Structured Outputs

Return structured data when useful:
@Serializable
data class WeatherForecast(
    val location: String,
    val temperature: Double,
    val condition: String,
    val humidity: Int
)

@Tool
@LLMDescription("Get current weather for a location")
suspend fun getCurrentWeather(
    @LLMDescription("City name")
    city: String
): WeatherForecast {
    val data = weatherApi.getCurrentWeather(city)
    return WeatherForecast(
        location = data.name,
        temperature = data.main.temp,
        condition = data.weather.first().description,
        humidity = data.main.humidity
    )
}

Merging Tool Registries

Combine multiple tool registries:
val fileTools = ToolRegistry {
    tool(ReadFileTool(JVMFileSystemProvider.ReadOnly))
    tool(EditFileTool(JVMFileSystemProvider.ReadWrite))
}

val weatherTools = ToolRegistry {
    tools(WeatherTools(apiClient).asTools())
}

// Merge registries
val allTools = fileTools + weatherTools

Next Steps

Real-World Example

Here’s a complete example with multiple tools:
class SwitchTools(private val switch: Switch) : ToolSet {
    @Tool
    @LLMDescription("Turn the switch on")
    suspend fun turnOn(): String {
        switch.turnOn()
        return "Switch turned on"
    }
    
    @Tool
    @LLMDescription("Turn the switch off")
    suspend fun turnOff(): String {
        switch.turnOff()
        return "Switch turned off"
    }
    
    @Tool
    @LLMDescription("Get current switch state")
    suspend fun getState(): String {
        return "Switch is ${if (switch.isOn) "on" else "off"}"
    }
}

// Usage
val switch = Switch()
val toolRegistry = ToolRegistry {
    tools(SwitchTools(switch).asTools())
}

val agent = AIAgent(
    promptExecutor = executor,
    llmModel = OpenAIModels.Chat.GPT4oMini,
    systemPrompt = "You control a switch. Help users operate it.",
    toolRegistry = toolRegistry
)

agent.run("Is the switch on? If not, turn it on.")

Build docs developers (and LLMs) love