Koog 1.2 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM
71
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Process steps in order. Do not skip ahead.
Worked Kotlin example excerpts (illustrative, with ... placeholders for tool
bodies and prompt-executor wiring; not a standalone buildable file):
skills/domain-model-subtask-pipeline/references/banking-example.mdThe pattern earns its complexity when the work has distinct phases that produce structurally different intermediate artifacts. Examples: issue triage (raw text → classified issue → fix proposal → verified fix); customer support (question → identified problem → applied resolution → verified resolution); code review (PR diff → categorized changes → review comments → applied edits).
singleRunStrategy()Skill(skill: "use-planner")Proceed immediately to Step 2.
The slicing axis is access, not feature. Group tools into separate ToolSet
classes by what they can do:
Communication tools — ask the user, send email, post comments, request approval
Read tools — query, list, search, get details (no side effects)
Write tools — mutate, create, delete, transfer (real consequences)
Constructor parameters are dependency injection. They are NOT visible to the LLM
userId, sessionId, DB connections, HTTP clients go through the constructor
The LLM only sees @Tool-annotated methods
Keeps authorization context out of the LLM-controlled surface
For tool registration mechanics, invoke Skill(skill: "add-tool"). See the
reference file for a worked three-ToolSet example.
Proceed immediately to Step 3.
Each subtask's input and output is a @Serializable Kotlin data class with
@LLMDescription on the class and on every property.
@LLMDescription describes the contract@LLMDescription describes each fieldFor top-level structured output (no subtask pipeline), invoke
Skill(skill: "add-structured-output"). See the reference file for worked
handoff classes.
Proceed immediately to Step 4.
subgraphWithTask<In, Out>Each subtask is a subgraphWithTask (or subgraphWithVerification, see
Step 5) typed end-to-end. Pass the subset of tools the subtask needs plus the
model that fits the subtask's complexity:
import ai.koog.agents.ext.agent.subgraphWithTask
import ai.koog.agents.ext.agent.subgraphWithVerification
import ai.koog.agents.ext.agent.CriticResult
val identifyProblem by subgraphWithTask<String, AccountIssueSummary>(
tools = communicationTools.asTools() + readTools.asTools(),
llmModel = OpenAIModels.Chat.GPT5_2,
) { input -> "Identify the problem:\n$input" }Match the model to the subtask:
subgraphWithTask, subgraphWithVerification, and CriticResult live in
package ai.koog.agents.ext.agent but ship inside the agents-core
artifact, which the koog-agents umbrella already pulls. No extra
dependency needed — the standalone ai.koog:agents-ext artifact is a
separate beta-line module and is NOT required for these APIs.
If you type-annotate the returned strategy, the strategy type lives in
ai.koog.agents.core.agent.entity.AIAgentGraphStrategy (not bare
ai.koog.agents.core.agent).
For graph DSL mechanics (edges, node naming, the member-vs-extension
import table), invoke Skill(skill: "author-strategy").
Proceed immediately to Step 5.
subgraphWithVerificationsubgraphWithVerification<T> produces a CriticResult<T> with
.successful: Boolean, .feedback: String?, .input: T. Branch on it:
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)transformed { it.input } pulls the verified payload out of CriticResult<T>transformed { it.feedback.orEmpty() } coerces nullable feedback to non-null
for subgraphWithTask<String, _> inputThe adjust subgraph re-runs the action. Grant it read + write ToolSets.
Communication-only adjustment cannot apply the corrected fix.
Cap the loop. A stubborn critic / adjuster can ping-pong forever within
maxIterations.
AIAgentStorage (see Skill(skill: "manage-state"))adjustSolution to nodeFinish when attempts exceed a
sensible boundProceed immediately to Step 6.
Koog shares the message history across subtasks automatically, even when each
subtask uses a different model. You do NOT thread the history manually between
subgraphs. Tool calls from identifyProblem are visible to fixProblem's
LLM; fixProblem's actions are visible to verifySolution.
Subgraphs are different from independent agents:
subgraphWithTask / subgraphWithVerification) is part of one agent. All subgraphs read the same shared message historyAIAgentService.fromAgent, covered by Skill(skill: "add-tool") Step 3) is a separate agent. The parent and child communicate only through the typed input/output of the tool callIf you actually want isolation (a phase that should NOT see prior context),
use a sub-agent (Skill(skill: "add-tool") Step 3), not a subgraph.
If the chain is long, history grows. Compress at deliberate boundaries (end of a phase, start of the next), not at every node:
llm.writeSession {
replaceHistoryWithTLDR()
}Pass a HistoryCompressionStrategy variant when the default TL;DR shape
doesn't fit. For the full set of variants, invoke
Skill(skill: "manage-state").
Proceed immediately to Step 7.
AIAgentPass the strategy explicitly as strategy =. It is not the default.
Register every tool at the agent level via toolRegistry
Each subgraph selects its subset via subgraphWithTask's tools = parameter
The agent's registry is one flat namespace. Don't register tools per-subgraph
Set maxIterations higher than the default 50. Pipelines chain LLM calls
and the default runs out fast
When per-subgraph llmModel values span providers (e.g., OpenAIModels.Chat.O3 for the verifier, AnthropicModels.Sonnet_4_5 for the deployer), the agent needs a MultiLLMPromptExecutor with one client per provider. simpleOpenAIExecutor / simpleAnthropicExecutor only know one provider each and will fail at runtime when a subgraph asks for the other:
import ai.koog.prompt.executor.clients.anthropic.AnthropicLLMClient
import ai.koog.prompt.executor.clients.openai.OpenAILLMClient
import ai.koog.prompt.executor.llms.MultiLLMPromptExecutor
val executor = MultiLLMPromptExecutor(
OpenAILLMClient(System.getenv("OPENAI_API_KEY")),
AnthropicLLMClient(System.getenv("ANTHROPIC_API_KEY")),
)Run ./gradlew build. The most common failure is a type mismatch on an edge
predicate. The chain only compiles when every subgraph's output type matches
the next subgraph's input type. Go back to Step 3 if types don't line up.
Full agent-construction snippet:
skills/domain-model-subtask-pipeline/references/banking-example.mdFinish here.
.tessl-plugin
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-agent-skills
use-attachments
use-cli-agents
use-functional-agent
use-llm-node-variants
use-planner
wire-a2a
wire-acp-server
wire-ktor-server
wire-mcp-server
wire-spring-boot