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
Process steps in order. Do not skip ahead.
use-planner covers picking the planner type and basic factory wiring. This skill is for projects that need to model the plan itself — the tree of subtasks the planner builds, how its nodes compose, how to access in-flight subtasks during a run, how to retry failures gracefully.
If the user just needs "an LLM-based planner with default behavior", invoke Skill(skill: "use-planner"). If they describe a domain in terms of "parallel and sequential subtasks", "retrying a failed step", "inspecting the plan tree", continue here.
Proceed immediately to Step 2.
PlannerNode TreeThe planner represents a plan as a tree of PlannerNodes:
Building blocks (from agents-planner):
import ai.koog.agents.planner.PlannerNode
val plan = PlannerNode.sequential(
PlannerNode.leaf("classify", description = "..."),
PlannerNode.parallel(
PlannerNode.leaf("lookup_user", description = "..."),
PlannerNode.leaf("lookup_account", description = "..."),
),
PlannerNode.leaf("respond", description = "..."),
)For LLM-based planners (Planners.llmBased), the tree is generated by the LLM from the goal — your code doesn't build it explicitly. For static planners or when you want to pre-seed structure, build it directly and pass to the strategy.
Proceed immediately to Step 3.
Use AIAgentStorage to track in-flight planner subtasks across nodes. The pattern from the in-repo example:
import ai.koog.agents.core.agent.context.createStorageKey
val unfinishedNodesKey = createStorageKey<MutableList<PlannerNode.Builder.Reference>>("unfinishedNodes")
val currentNodeKey = createStorageKey<PlannerNode.Builder.Reference>("currentNode")
// inside a strategy node body, while iterating subtasks:
val unfinished = storage.get(unfinishedNodesKey) ?: mutableListOf()
val next = unfinished.removeFirstOrNull() ?: return null
storage.set(currentNodeKey, next)
storage.set(unfinishedNodesKey, unfinished)PlannerNode.Builder.Reference is the planner's identifier for a subtask in the tree. Keep it in storage to maintain "where am I in the plan" across nodes.
In 1.0 storage is serialized into checkpoints — references survive a restart if checkpointing is on (add-persistence).
Proceed immediately to Step 4.
When a subtask requires the LLM to produce structured output (a JSON plan revision, a typed result), parse failures should retry the subtask, not abort the whole plan:
val executeSubtask by nodeLLMRequest()
val parseResult by node<Message.User, ParsedSubtaskResult?>("parse") { msg ->
runCatching { parseStructured<ParsedSubtaskResult>(msg.content) }.getOrNull()
}
edge(executeSubtask forwardTo parseResult)
edge(parseResult forwardTo continueWithResult onCondition { it != null })
edge(parseResult forwardTo executeSubtask onCondition { it == null } transformed { "Re-emit the plan revision as valid JSON." })The retry edge feeds a corrective prompt back into the same subtask node. Cap retries — usually 2–3 — by counting attempts in storage; uncapped retries on a misformatted LLM response burn budget fast.
Proceed immediately to Step 5.
Long planner runs accumulate enormous history (every subtask's LLM round-trip lands in the conversation). Without compression you hit context limits halfway through. Compress at phase boundaries — between "planning" and "execution", or every N completed subtasks:
val phaseBoundary by node<Unit, Unit>("phase-boundary") {
llm.writeSession {
replaceHistoryWithTLDR()
}
}Pick the compression points by looking at the plan tree — boundaries at the end of a parallel block or between sequential phases. See manage-state for the broader compression vocabulary.
Proceed immediately to Step 6.
For debugging and observability, expose the in-flight plan as a side channel:
AIAgentStorage at every node entryhandle-agent-events's onNodeEntry (or equivalent) callback can dump the plan state to stdout for live demostrace-agent-internals) for post-mortem analysisFor production, surface key counters (subtasks completed, subtasks failed, retries used) as OpenTelemetry custom metrics on top of the built-in gen_ai.* set.
Reference: examples/simple-examples/.../planner/PlannerAgentExample.kt — the richest demonstration of all six steps in working code. The example's inline parse is a TODO() skeleton; the rest of the plumbing (storage keys, retry edges, history compression) is real.
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