Annotation-Based Tools
Annotation-based tools are the simplest way to create tools in Koog. Use @Tool and @LLMDescription annotations on functions, and Koog handles the rest.
Quick Example
import ai.koog.agents.core.tools.annotations.Tool
import ai.koog.agents.core.tools.annotations.LLMDescription
@Tool
@LLMDescription("Get the current weather for a city")
suspend fun getWeather(
@LLMDescription("The city name") city: String
): String {
return "The weather in $city is 22°C and sunny"
}
The @Tool Annotation
Mark a function as a tool with @Tool:
@Tool
suspend fun myTool(): String {
return "Tool result"
}
The @Tool annotation:
- Registers the function as a callable tool
- Uses the function name as the tool name (or a custom name via
customNameparameter) - The function should return
String(suspend is supported but not required)
Custom Tool Name
@Tool(customName = "web_search")
suspend fun searchTheWeb(@LLMDescription("Query") query: String): String {
return "Results for: $query"
}
The @LLMDescription Annotation
Use @LLMDescription to provide descriptions that help the LLM understand when and how to use the tool:
On Functions
@Tool
@LLMDescription("""
Search the web for current information.
Use this when you need up-to-date facts, news, or data
that may not be in your training data.
""")
suspend fun webSearch(
@LLMDescription("The search query") query: String
): String {
return performSearch(query)
}
On Parameters
@Tool
@LLMDescription("Send an email to a recipient")
suspend fun sendEmail(
@LLMDescription("The recipient's email address") to: String,
@LLMDescription("The email subject line") subject: String,
@LLMDescription("The email body content") body: String,
@LLMDescription("Whether to send as HTML (true) or plain text (false)") isHtml: Boolean = false
): String {
return sendEmail(to, subject, body, isHtml)
}
Parameter Types
Annotation-based tools support these parameter types:
| Kotlin Type | Java Type | Example |
|---|---|---|
String |
String |
@LLMDescription("...") name: String |
Int |
int / Integer |
@LLMDescription("...") count: Int |
Long |
long / Long |
@LLMDescription("...") id: Long |
Float |
float / Float |
@LLMDescription("...") amount: Float |
Double |
double / Double |
@LLMDescription("...") price: Double |
Boolean |
boolean / Boolean |
@LLMDescription("...") enabled: Boolean |
Default Values
Parameters can have default values:
@Tool
@LLMDescription("Search with pagination")
suspend fun search(
@LLMDescription("The search query") query: String,
@LLMDescription("Page number (1-based)") page: Int = 1,
@LLMDescription("Results per page") pageSize: Int = 10,
@LLMDescription("Sort order: relevance or date") sortBy: String = "relevance"
): String {
return performSearch(query, page, pageSize, sortBy)
}
When the LLM doesn't provide a value for a parameter with a default, the default is used.
Kotlin Examples
Simple Calculator
@Tool
@LLMDescription("Perform basic arithmetic calculations")
suspend fun calculate(
@LLMDescription("The arithmetic expression (e.g., '2 + 3', '10 * 5')") expression: String
): String {
return try {
evaluateExpression(expression).toString()
} catch (e: Exception) {
"Error: Invalid expression '${expression}'. ${e.message}"
}
}
Database Query
@Tool
@LLMDescription("Query the user database for information")
suspend fun queryUsers(
@LLMDescription("SQL WHERE clause (without the WHERE keyword)") filter: String,
@LLMDescription("Maximum number of results") limit: Int = 10
): String {
val query = "SELECT * FROM users WHERE $filter LIMIT $limit"
val results = database.query(query)
return results.joinToString("\n") { "${it.name} (${it.email})" }
}
File Operations
@Tool
@LLMDescription("Read the contents of a file")
suspend fun readFile(
@LLMDescription("The file path relative to the project root") path: String
): String {
return try {
File(path).readText()
} catch (e: FileNotFoundException) {
"Error: File not found at '$path'"
}
}
@Tool
@LLMDescription("Write content to a file")
suspend fun writeFile(
@LLMDescription("The file path relative to the project root") path: String,
@LLMDescription("The content to write") content: String,
@LLMDescription("Whether to append (true) or overwrite (false)") append: Boolean = false
): String {
val file = File(path)
file.parentFile?.mkdirs()
file.writeText(content)
return "Successfully wrote ${content.length} characters to '$path'"
}
HTTP Client
@Tool
@LLMDescription("Make an HTTP GET request to a URL")
suspend fun httpGet(
@LLMDescription("The URL to fetch") url: String,
@LLMDescription("Request timeout in seconds") timeout: Int = 30
): String {
return try {
val response = httpClient.get(url) {
timeout {
requestTimeoutMillis = timeout * 1000L
}
}
response.bodyAsText()
} catch (e: Exception) {
"Error fetching $url: ${e.message}"
}
}
ToolSet Interface
Group related tools into a ToolSet for organized registration:
import ai.koog.agents.core.tools.ToolSet
import ai.koog.agents.core.tools.annotations.Tool
import ai.koog.agents.core.tools.annotations.LLMDescription
class WeatherTools : ToolSet {
@Tool
@LLMDescription("Get current weather for a city")
suspend fun getCurrentWeather(
@LLMDescription("City name") city: String
): String {
return "Current weather in $city: 22°C, sunny"
}
@Tool
@LLMDescription("Get weather forecast for the next 5 days")
suspend fun getForecast(
@LLMDescription("City name") city: String,
@LLMDescription("Number of days (1-5)") days: Int = 3
): String {
return "Forecast for $city for $days days: ..."
}
@Tool
@LLMDescription("Get weather alerts for a region")
suspend fun getAlerts(
@LLMDescription("Region code (e.g., US-CA)") region: String
): String {
return "No active alerts for $region"
}
}
// Register all tools at once
val toolRegistry = ToolRegistry {
toolSet(WeatherTools())
}
Multiple ToolSets
class EmailTools : ToolSet {
@Tool
@LLMDescription("Send an email")
suspend fun sendEmail(
@LLMDescription("Recipient") to: String,
@LLMDescription("Subject") subject: String,
@LLMDescription("Body") body: String
): String = "Email sent to $to"
@Tool
@LLMDescription("List recent emails")
suspend fun listEmails(
@LLMDescription("Number of emails") count: Int = 10
): String = "Last $count emails..."
}
// Combine multiple tool sets
val toolRegistry = ToolRegistry {
toolSet(WeatherTools())
toolSet(EmailTools())
toolSet(CalculatorTools())
}
Java Examples
Basic Tool
import ai.koog.agents.core.tools.annotations.Tool;
import ai.koog.agents.core.tools.annotations.LLMDescription;
public class MyTools {
@Tool
@LLMDescription("Convert temperature between Celsius and Fahrenheit")
public static String convertTemperature(
@LLMDescription("The temperature value") double value,
@LLMDescription("Source unit: celsius or fahrenheit") String from,
@LLMDescription("Target unit: celsius or fahrenheit") String to
) {
if (from.equalsIgnoreCase("celsius") && to.equalsIgnoreCase("fahrenheit")) {
return value * 9/5 + 32 + "°F";
} else if (from.equalsIgnoreCase("fahrenheit") && to.equalsIgnoreCase("celsius")) {
return (value - 32) * 5/9 + "°C";
}
return "Invalid units. Use 'celsius' or 'fahrenheit'.";
}
}
Java ToolSet
import ai.koog.agents.core.tools.ToolSet;
import ai.koog.agents.core.tools.Tool;
import ai.koog.agents.core.tools.annotations.LLMDescription;
public class MathToolSet implements ToolSet {
@Tool
@LLMDescription("Add two numbers")
public String add(
@LLMDescription("First number") double a,
@LLMDescription("Second number") double b
) {
return String.valueOf(a + b);
}
@Tool
@LLMDescription("Multiply two numbers")
public String multiply(
@LLMDescription("First number") double a,
@LLMDescription("Second number") double b
) {
return String.valueOf(a * b);
}
@Override
public List<Tool<?>> getTools() {
return ToolSet.fromAnnotations(this);
}
}
// Register
var registry = ToolRegistry.builder()
.toolSet(new MathToolSet())
.build();
Instance vs Static Methods
// Static methods (recommended for stateless tools)
@Tool
@LLMDescription("Static tool example")
public static String staticTool(@LLMDescription("Input") String input) {
return "Result: " + input;
}
// Instance methods (for stateful tools)
public class StatefulTools {
private final Database db;
public StatefulTools(Database db) {
this.db = db;
}
@Tool
@LLMDescription("Query the database")
public String query(@LLMDescription("SQL query") String sql) {
return db.execute(sql).toString();
}
}
// Register instance tools
var statefulTools = new StatefulTools(database);
var registry = ToolRegistry.builder()
.tool(statefulTools)
.build();
Best Practices
- Write clear descriptions — The LLM relies on
@LLMDescriptionto decide when to use a tool - Describe every parameter — Each parameter should have a description explaining its purpose
- Use descriptive names — Function names become tool names; make them self-explanatory
- Provide defaults — Use default values for optional parameters
- Validate inputs — Check parameter values before executing
- Return useful results — Help the LLM understand the outcome
- Handle errors gracefully — Return error messages, don't throw exceptions
- Keep tools focused — One tool, one responsibility
Common Pitfalls
| Pitfall | Solution |
|---|---|
Missing @LLMDescription |
Always describe the tool and its parameters |
| Non-suspend functions | Tool functions must be suspend in Kotlin |
| Complex parameter types | Use simple types (String, Int, Boolean, etc.) |
| Throwing exceptions | Return error strings instead |
| Ambiguous descriptions | Be specific about what the tool does and when to use it |