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
Process steps in order. Do not skip ahead.
testImplementation("ai.koog:agents-test:1.0.0")This is a testImplementation — it should never appear in production classpath.
Proceed immediately to Step 2.
Real LLM calls in tests are slow, expensive, and non-deterministic. Replace simpleOpenAIExecutor(...) (or any other provider executor) with a scripted mock:
import ai.koog.agents.test.TestPromptExecutor
import ai.koog.prompt.model.Message
val executor = TestPromptExecutor.scripted(
// First LLM call: agent picks a tool
response = Message.Assistant.toolCalls(
toolCalls = listOf(ToolCall(name = "lookup_account", args = mapOf("id" to "acc-42")))
),
// Second LLM call (after tool result returns): agent replies with text
response = Message.Assistant.text("Account acc-42 is on the Pro tier."),
)
val agent = AIAgent(
promptExecutor = executor,
llmModel = OpenAIModels.Chat.GPT4o,
toolRegistry = ToolRegistry { tools(MyTools().asTools()) },
systemPrompt = "...",
)Each scripted response is consumed in order — the Nth LLM call gets the Nth response. The test fails if the agent makes more calls than scripted (good — surfaces unexpected loops).
Proceed immediately to Step 3.
If the agent reads the current time (logging timestamps, prompt augmenters that include "now"), inject a deterministic KoogClock:
import ai.koog.agents.core.clock.KoogClock
import kotlin.time.Instant
val fixedTime = Instant.parse("2026-05-25T12:00:00Z")
val fakeClock = KoogClock.fixed(fixedTime)
val agent = AIAgent(
promptExecutor = executor,
llmModel = ...,
systemPrompt = "...",
clock = fakeClock,
)KoogClock replaced kotlin.time.Clock as the parameter type in 1.0 (#1925). Test doubles need to use KoogClock factories, not the generic Clock interface.
Proceed immediately to Step 4.
For verifying the agent made the right tool calls, attach a recording event handler in the test:
import ai.koog.agents.features.eventhandler.handleEvents
val toolCalls = mutableListOf<String>()
val agent = AIAgent(
promptExecutor = executor,
llmModel = ...,
toolRegistry = ...,
systemPrompt = "...",
) {
handleEvents {
onToolCallStarting { ctx -> toolCalls.add(ctx.toolName) }
}
}
agent.run("Look up account acc-42")
assertEquals(listOf("lookup_account"), toolCalls)This pattern keeps assertions on observable behavior (which tools, in which order) rather than internal state — which makes the tests robust to refactors of the strategy.
Proceed immediately to Step 5.
Tools should usually run their real implementations in tests — that's part of what's under test. Mock the LLM (Step 2), not the tools. If a tool itself depends on something expensive (a real database, network call), mock at that boundary inside the tool — keep the tool's interface to the agent unchanged.
The exception: tools that have external side effects (send email, post to Slack). For those, mock the underlying client and assert the tool was called with the right args via the event handler in Step 4.
Reference: agents-test ships further utilities — recorder executors, deterministic schedulers — that are useful for richer scenarios. The four primitives above (scripted executor, fake clock, event-handler recorder, real-tool defaults) cover most agent tests.
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
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