Koog 1.0 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM
89
89%
Does it follow best practices?
Impact
89%
1.78xAverage score across 47 eval scenarios
Advisory
Suggest reviewing before use
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:
nodeLLMRequestStreaming)nodeLLMRequestMultipleChoices)nodeLLMModerateText / nodeLLMModerateMessage)nodeLLMRequestForceOneTool)Use when the caller needs to see partial output as it arrives (Ktor server endpoint, CLI with live print, conference demo with text appearing live).
Write the strategy and the modified agent construction to disk with explicit Path: labels (same convention as scaffold-agent):
Path: src/main/kotlin/com/example/Strategy.kt — strategy with the streaming nodePath: src/main/kotlin/com/example/Main.kt — modified agent construction passing strategy = ...Create files if they don't exist. Do not respond with prose only.
val strategy = strategy<String, String>("stream-reply") {
val streamReply by nodeLLMRequestStreaming { chunk ->
// chunk-handler — runs on every streamed chunk
print(chunk.text)
}
edge(nodeStart forwardTo streamReply)
edge(streamReply forwardTo nodeFinish)
}The chunk-handler lambda runs synchronously on every streamed delta. Don't block inside it — the LLM's stream stalls if the handler is slow. For network I/O (writing chunks to a Ktor response), use a channel/flow downstream rather than calling respond* directly inside the handler.
Streaming doesn't compose with tool calls in the same node — for "stream the reply text but still allow tool calls", use a singleRunStrategy shape and stream from nodeLLMRequestStreaming only on the text-reply branch.
Finish here.
Use when you want multiple completions for the same prompt and downstream code (or a critic node) picks the best.
Write the strategy and modified agent construction to disk via Path::
Path: src/main/kotlin/com/example/Strategy.kt — strategy with the sampling + picker nodesPath: src/main/kotlin/com/example/Main.kt — modified agent constructionCreate files if missing. Do not respond with prose only.
val strategy = strategy<String, String>("sample-and-pick") {
val sample by nodeLLMRequestMultipleChoices(numChoices = 3)
val pick by node<List<Message.User>, Message.User>("pick-best") { choices ->
// pick logic — e.g., longest reply, or run a critic LLM
choices.maxBy { it.content.length }
}
edge(nodeStart forwardTo sample)
edge(sample forwardTo pick)
edge(pick forwardTo nodeFinish transformed { it.content })
}Each choice costs one LLM completion at full token rates — sample 3 for 3× the cost. Use when the picker's signal is cheaper or more reliable than the per-completion quality.
Finish here.
Two variants:
nodeLLMModerateText() — String inputnodeLLMModerateMessage() — Message.User inputBoth call a moderation classifier (provider-dependent) and produce a moderation result that downstream edges branch on.
Write the strategy and modified agent construction to disk via Path::
Path: src/main/kotlin/com/example/Strategy.kt — moderated strategyPath: src/main/kotlin/com/example/Main.kt — modified agent constructionCreate files if missing. Do not respond with prose only.
val strategy = strategy<String, String>("moderated-chat") {
val moderate by nodeLLMModerateText()
val safeReply by nodeLLMRequest()
edge(nodeStart forwardTo moderate)
edge(moderate forwardTo nodeFinish onCondition { it.flagged } transformed { "I can't help with that request." })
edge(moderate forwardTo safeReply onCondition { !it.flagged })
edge(safeReply forwardTo nodeFinish onTextMessage { true })
}The moderation node returns a typed result with a flagged boolean and category breakdown — branch on flagged to short-circuit, and on categories if you need finer-grained handling.
Finish here.
Use when the next step must invoke a specific tool — no LLM choice, no fallback to text. Common case: a "router" agent where the first step always calls classify_intent.
Write the strategy and modified agent construction to disk via Path::
Path: src/main/kotlin/com/example/Strategy.kt — strategy with the forced-tool nodePath: src/main/kotlin/com/example/Main.kt — modified agent constructionCreate files if missing. Do not respond with prose only.
val classifyTool = ClassifyIntentTool()
val strategy = strategy<String, String>("force-router") {
val forceClassify by nodeLLMRequestForceOneTool(classifyTool)
val execute by nodeExecuteTools()
val followUp by nodeLLMSendToolResults()
edge(nodeStart forwardTo forceClassify)
edge(forceClassify forwardTo execute)
edge(execute forwardTo followUp)
edge(followUp forwardTo nodeFinish onTextMessage { true })
}The forced tool must already be in the agent's ToolRegistry. Forcing a tool not in the registry is a runtime error.
Finish here.
.gemini
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
scenario-46
scenario-47
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