Tools are the hands of your AI agent - they allow the agent to interact with external systems, perform computations, and retrieve information. In Koog, tools are type-safe, serializable functions that can be invoked by the LLM during agent execution.
public abstract class Tool<TArgs, TResult>( public val argsSerializer: KSerializer<TArgs>, public val resultSerializer: KSerializer<TResult>, public val descriptor: ToolDescriptor, public val metadata: Map<String, String> = emptyMap()) { public val name: String get() = descriptor.name public abstract suspend fun execute(args: TArgs): TResult}
The simplest way to create tools is with the @Tool annotation:
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., 'Paris', 'London')") location: String, @LLMDescription("ISO 3166-1 alpha-2 country code (e.g., 'FR', 'GB')") countryCodeISO2: String, @LLMDescription("Start date in ISO format (e.g., '2024-06-01')") startDate: String, @LLMDescription("End date in ISO format (e.g., '2024-06-07')") endDate: String, @LLMDescription("Granularity of forecast: DAILY or HOURLY") granularity: ForecastGranularity ): String { val forecast = apiClient.getWeatherForecast( location = location, countryCode = countryCodeISO2, startDate = LocalDate.parse(startDate), endDate = LocalDate.parse(endDate), granularity = granularity ) return formatForecast(forecast) }}
The @LLMDescription annotation provides context to the LLM about what the tool does and what each parameter means. Clear descriptions improve tool selection and usage.
@Tool@LLMDescription("Add days to a date")fun addDate( @LLMDescription("Base date in ISO format (e.g., '2024-06-01')") date: String, @LLMDescription("Number of days to add") days: Int): String { val baseDate = LocalDate.parse(date) val newDate = baseDate.plus(DatePeriod(days = days)) return newDate.toString()}// Register in tool registryval toolRegistry = ToolRegistry { tool(::addDate)}
@Serializabledata class EmailArgs( val to: String, val subject: String, val body: String, val attachments: List<String> = emptyList(), val priority: Priority = Priority.NORMAL)@Serializableenum class Priority { LOW, NORMAL, HIGH, URGENT }@Tool@LLMDescription("Send an email")suspend fun sendEmail(args: EmailArgs): String { // Send email return "Email sent to ${args.to}"}
class UserTools( private val showMessage: suspend (String) -> String) : ToolSet { @Tool @LLMDescription("Show a message to the user and get their response") suspend fun askUser( @LLMDescription("The message to show") message: String ): String { return showMessage(message) } @Tool @LLMDescription("Confirm an action with the user") suspend fun confirmAction( @LLMDescription("The action to confirm") action: String ): Boolean { val response = showMessage("Confirm: $action (yes/no)") return response.lowercase() == "yes" }}// Register all tools from the setval toolRegistry = ToolRegistry { tools(userTools) // Automatically discovers @Tool annotated methods}
class CriticalTool : Tool<Args, Result>(...) { override suspend fun execute(args: Args): Result { try { return performCriticalOperation(args) } catch (e: Exception) { // This will be logged and may terminate the agent throw ToolExecutionException( "Critical operation failed: ${e.message}", cause = e ) } }}
@Tool@LLMDescription("Read a file")suspend fun readFile( @LLMDescription("Path to the file") path: String): String { val file = File(path) require(file.exists()) { "File not found: $path" } require(file.length() < 1_000_000) { "File too large" } return file.readText()}