Koog 1.0 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM
87
88%
Does it follow best practices?
Impact
87%
1.85xAverage score across 45 eval scenarios
Advisory
Suggest reviewing before use
Illustrative Kotlin snippets matching the seven-step skill. Contains ...
placeholders for tool bodies and prompt-executor wiring; not a standalone
buildable file. Adapted from the JetBrains KotlinConf 2026 banking demo —
four typed phases (identifyProblem → fixProblem → verifySolution ⇄
adjustSolution), tools sliced by access into three ToolSets, mixed
per-phase models.
import ai.koog.agents.core.tools.annotations.Tool
import ai.koog.agents.core.tools.annotations.LLMDescription
import ai.koog.agents.core.tools.reflect.ToolSet
import ai.koog.agents.core.tools.reflect.asTools
class CommunicationTools(private val sessionId: String) : ToolSet {
@Tool
@LLMDescription("Ask the user a clarifying question and wait for the reply")
fun askUser(@LLMDescription("Question text") question: String): String { ... }
}
class AccountReadTools(private val userId: String) : ToolSet {
@Tool
@LLMDescription("Get account balance (in USD) for the current user")
fun getAccountBalance(): String { ... } // stringify at the boundary per add-tool
@Tool
@LLMDescription("Returns a list of transactions for the current user")
fun getLatestTransactions(startDate: Instant?, status: Transaction.Status?): String { ... } // JSON-encoded list
}
class AccountWriteTools(private val userId: String) : ToolSet {
@Tool
@LLMDescription("Transfers money to the recipient")
fun transferMoney(
@LLMDescription("ID of the recipient") recipientId: String,
@LLMDescription("Amount in USD to be transferred") amount: Int,
): String { ... } // JSON-encoded result
}@LLMDescription("Full info about the user's issue with the bank account")
@Serializable
data class AccountIssueSummary(
@property:LLMDescription("Account number of the user in the database")
val accountNumber: String,
@property:LLMDescription("Username of the account holder")
val username: String,
@property:LLMDescription("Current account balance in US dollars")
val currentBalance: Int,
@property:LLMDescription("ID of the transaction related to this issue, if applicable")
val relatedTransactionId: String?,
@property:LLMDescription("What exactly is the user's issue with their account or transaction")
val problem: String,
@property:LLMDescription("Was the issue already resolved?")
val resolved: Boolean,
)
@LLMDescription("Summary about what was done to resolve the issue")
@Serializable
data class AccountIssueSolution(
@property:LLMDescription("Account number that was affected") val accountNumber: String,
@property:LLMDescription("Brief summary of the actions taken to resolve the issue") val actionsTaken: String,
)import ai.koog.agents.core.dsl.builder.strategy
import ai.koog.agents.ext.agent.subgraphWithTask
import ai.koog.agents.ext.agent.subgraphWithVerification
import ai.koog.agents.ext.agent.CriticResult
// forwardTo / onCondition / transformed are infix members — no imports needed.
// onToolCalls, onTextMessage, onIsInstance, etc. are extensions in
// ai.koog.agents.core.dsl.extension.* and would each need an import here if used.
val triageStrategy = strategy<String, AccountIssueSolution>("issue-triage") {
val identifyProblem by subgraphWithTask<String, AccountIssueSummary>(
tools = communicationTools.asTools() + readTools.asTools(),
llmModel = OpenAIModels.Chat.GPT5_2, // cheap classification
) { input -> "Identify the problem, formulate a problem description:\n$input" }
val fixProblem by subgraphWithTask<AccountIssueSummary, AccountIssueSolution>(
tools = readTools.asTools() + writeTools.asTools(),
llmModel = AnthropicModels.Sonnet_4, // smart action
) { description -> "Now solve the user's problem:\n$description" }
val verifySolution by subgraphWithVerification<AccountIssueSolution>(
tools = communicationTools.asTools() + readTools.asTools(),
llmModel = OpenAIModels.Chat.O3, // reasoning verification
) { solution -> "Now verify that the problem is actually solved:\n$solution" }
val adjustSolution by subgraphWithTask<String, AccountIssueSolution>(
tools = readTools.asTools() + writeTools.asTools(), // re-runs the action — needs write
llmModel = AnthropicModels.Sonnet_4,
) { feedback -> "Adjust the solution using this feedback:\n$feedback" }
edge(nodeStart forwardTo identifyProblem)
edge(identifyProblem forwardTo fixProblem)
edge(fixProblem forwardTo verifySolution)
edge(verifySolution forwardTo nodeFinish onCondition { it.successful } transformed { it.input })
edge(verifySolution forwardTo adjustSolution onCondition { !it.successful } transformed { it.feedback.orEmpty() })
edge(adjustSolution forwardTo verifySolution)
}The transformed { it.input } pulls the verified payload out of
CriticResult<T> on the success edge. The failure edge coerces
it.feedback (nullable String?) to non-null via .orEmpty() so it matches
adjustSolution's subgraphWithTask<String, _> input type.
val agent = AIAgent(
promptExecutor = ...,
llmModel = OpenAIModels.Chat.GPT5_2, // default model — subtasks override
toolRegistry = ToolRegistry {
tools(communicationTools.asTools())
tools(readTools.asTools())
tools(writeTools.asTools())
},
systemPrompt = "...",
strategy = triageStrategy,
maxIterations = 200, // pipelines chain LLM calls — default 50 is too low
)import ai.koog.agents.core.dsl.extension.HistoryCompressionStrategy
// inside a node body, between phases
llm.writeSession {
replaceHistoryWithTLDR() // collapses everything into a single TL;DR summary
}Invoke at the end of a phase or the start of the next, not at every node; see
Skill(skill: "manage-state") for HistoryCompressionStrategy variants
(Chunked, FromLastNMessages, FactRetrieval, etc.).
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
scenario-44
scenario-45
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