Koog 1.0 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM
88
88%
Does it follow best practices?
Impact
88%
1.95xAverage score across 43 eval scenarios
Passed
No known issues
This skill is an action router — pick the step that matches the user's intent and execute only that step. Do not run other steps; do not parallelize.
Available actions:
@Tool on a ToolSet class): the right default for tools that take JSON-shaped args and return a stringifiable resultTool<TArgs, TResult>): when you need typed arguments with custom serialization or programmatic schema controlAIAgent as a tool): when the step is itself agent-shaped — its own tools, its own model, its own strategyIf the user is ambiguous, default to Step 1 (annotated). Escalate to Step 2 (typed Tool<TArgs,TResult>) when any of these is true:
data class, not flat primitivesdata class or other typed resultEscalate to Step 3 only when the new "tool" is itself agent-shaped.
Locate the agent's ToolRegistry (typically in the file that constructs AIAgent(...)). If the project has no ToolSet class yet, create one in the same package:
import ai.koog.agents.core.tools.annotations.LLMDescription
import ai.koog.agents.core.tools.annotations.Tool
import ai.koog.agents.core.tools.reflect.ToolSet
import ai.koog.agents.core.tools.reflect.asTools
@LLMDescription("Tools for <one-line purpose of the group>")
class <Name>Tools : ToolSet {
@Tool
@LLMDescription("<one-line what this tool does, written for the LLM>")
fun <toolName>(
@LLMDescription("<arg purpose>") arg1: <Type>,
@LLMDescription("<arg purpose>") arg2: <Type>,
): String {
// implementation
return "<result the LLM will see>"
}
}Then register in the agent construction:
val agent = AIAgent(
promptExecutor = ...,
llmModel = ...,
toolRegistry = ToolRegistry { tools(<Name>Tools().asTools()) },
systemPrompt = ...,
)If the Kotlin function name doesn't read well to the LLM, add @Tool(customName = "list_prs"). As of Koog 1.0 this override is honored by asTools().
Tools that return non-String types: convert at the boundary to a String (toString(), a JSON-encoded shape, etc.). The LLM only sees the returned string.
Finish here.
Tool<TArgs, TResult>)Use when annotations don't fit — typed arguments with custom serialization, programmatic schema control, registration based on runtime data, or any case where the user's existing function takes a typed data class parameter.
Write the tool class and the modified agent construction to disk with explicit Path: labels (same convention as scaffold-agent):
Path: src/main/kotlin/com/example/AccountLookupTool.kt — typed Tool<TArgs,TResult> subclass (rename to match the tool's actual name; do not write <Name>Tool.kt literally)Path: src/main/kotlin/com/example/Main.kt — modified agent construction registering the toolPath: build.gradle.kts — any new dependency linesCreate files if they don't exist. Do not respond with prose only. Replace placeholder names in the path with the user's actual tool name.
import ai.koog.agents.core.tools.Tool
import ai.koog.agents.core.tools.ToolDescriptor
import ai.koog.agents.core.tools.TypeToken
import kotlinx.serialization.Serializable
@Serializable
data class <Name>Args(val field1: String, val field2: Int)
@Serializable
data class <Name>Result(val output: String)
class <Name>Tool : Tool<<Name>Args, <Name>Result>(
argsType = TypeToken.of(<Name>Args::class),
resultType = TypeToken.of(<Name>Result::class),
descriptor = ToolDescriptor(
name = "<tool_name>",
description = "<what the tool does>",
// parameter descriptions if needed
),
) {
override suspend fun execute(args: <Name>Args): <Name>Result {
return <Name>Result(output = "...")
}
}Register:
toolRegistry = ToolRegistry { tool(<Name>Tool()) }If you need raw ToolCallMetadata (per-call trace IDs, live AIAgentContext), subclass ToolBase directly rather than Tool. Reach for ToolBase only when Tool cannot express the dependency.
Finish here.
Use when the new "tool" is itself agent-shaped: its own tools, model, strategy. Reference: examples/code-agent/step-04-add-subagent/.
Construct the child agent first (full AIAgent(...) factory call). Then wrap and register:
import ai.koog.agents.core.agent.AIAgentService
val childAgent: GraphAIAgent<String, String> = AIAgent(
promptExecutor = ...,
llmModel = ...,
toolRegistry = ToolRegistry { tools(<ChildTools>().asTools()) },
systemPrompt = "<child's focused system prompt>",
)
val childAsTool = AIAgentService
.fromAgent(childAgent)
.createAgentTool<String, String>(
agentName = "__<child_name>_agent__",
agentDescription = "<one-line what the child does, written for the parent's LLM>",
inputDescription = "<one-line what input the parent should pass>",
)
val parent = AIAgent(
promptExecutor = ...,
llmModel = ...,
toolRegistry = ToolRegistry { tool(childAsTool) },
systemPrompt = ...,
)Name convention: prefix child agent tool names with __ (double underscore) per the in-repo example — this keeps them visually distinct from regular tools in logs.
Each child agent invocation is a full agent run with its own LLM round-trips. Budget maxIterations on the parent accordingly — agent-tools are not free.
Finish here.
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
skills
add-observability
add-persistence
add-rag
add-structured-output
add-token-budgeting
add-tool
cache-llm-calls
define-prompt
domain-model-subtask-pipeline
references
enable-prompt-caching
handle-agent-events
manage-state
migrate-from-0-x
model-planner-subtasks
persist-chat-history
query-sql-from-agent
scaffold-agent
snapshot-and-restore
test-koog-agents
trace-agent-internals
use-attachments
use-functional-agent
use-llm-node-variants
use-planner
wire-a2a
wire-acp-server
wire-ktor-server
wire-mcp-server
wire-spring-boot