Koog 1.0 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM
71
88%
Does it follow best practices?
Impact
—
No eval scenarios have been run
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:
@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/.
Sub-agent vs subgraph — pick the right primitive:
AIAgentService.fromAgent) — fully independent agent. Has its own context window; parent and child communicate only through the typed input/output of the tool call. Pick when the child should NOT see the parent's conversation history (isolation, separate scope, different system prompt that mustn't leak)subgraphWithTask / subgraphWithVerification, covered by Skill(skill: "author-strategy") and Skill(skill: "domain-model-subtask-pipeline")) — part of the SAME agent. All subgraphs share one message history, so each subgraph's LLM call sees what every prior subgraph saw and did. Pick when phases need shared context (typed-handoff pipelines, verify-and-fix loops, multi-stage classification)The default for "I want to break my agent into stages" is subgraph, not sub-agent. Reach for sub-agent only when isolation is the explicit requirement.
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.
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