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.
Three Koog persistence concepts — easy to confuse:
chat-history-jdbc, chat-history-aws, chat-memory-sql. Stores the conversation messages keyed by session ID. Used to resume a chat where the user left offSkill(skill: "add-persistence")Skill(skill: "manage-state")If the user wants "user picks up the conversation tomorrow", this skill. If "agent resumes a crashed run from where it stopped", invoke Skill(skill: "add-persistence"). If "agent remembers things across sessions for retrieval", invoke Skill(skill: "manage-state").
Proceed immediately to Step 2.
Match what the user has available without blocking on a clarifying question:
chat-history-jdbc) — any JDBC-compatible database (Postgres, MySQL, etc.). Default when the user names Postgres / MySQL / a JDBC URL / env vars like DB_URLchat-history-aws) — DynamoDB or AWS-hosted alternatives. Pick when the user names DynamoDB / AWS / S3chat-memory-sql) — SQL backend with stronger typing over chat messages. Pick when the user explicitly asks for typed chat memoryProceed immediately to Step 3.
implementation("ai.koog:agents-features-chat-history-jdbc:1.0.0")
// or:
// implementation("ai.koog:agents-features-chat-history-aws:1.0.0")
// or:
// implementation("ai.koog:agents-features-chat-memory-sql:1.0.0")For JDBC, also include the driver for the actual database (Postgres, etc.) — that's not provided by Koog.
Proceed immediately to Step 4.
Write the modified agent construction, the dependency, and any handler updates to disk with explicit Path: labels (same convention as scaffold-agent):
Path: src/main/kotlin/com/example/Main.kt — modified agent construction (or the file containing the AIAgent(...) call)Path: build.gradle.kts — appended dependency lineRoutes.kt, ChatController.kt), write the updated handler under a concrete path using that filename — for example Path: src/main/kotlin/com/example/Routes.kt. Never emit a literal angle-bracket placeholder filenameCreate files if they don't exist. Do not respond with prose only.
Install in the AIAgent(...) trailing lambda. JDBC example:
import ai.koog.agents.features.chathistory.jdbc.JdbcChatHistory
val agent = AIAgent(
promptExecutor = ...,
llmModel = ...,
systemPrompt = "...",
) {
install(JdbcChatHistory) {
jdbcUrl = System.getenv("DB_URL")
username = System.getenv("DB_USER")
password = System.getenv("DB_PASSWORD")
// table name + schema — defaults are sensible, override if you must
}
}Always read credentials from environment variables, never inline the values.
Proceed immediately to Step 5.
agent.run(input, sessionId = "...") accepts a session ID. With chat-history persistence installed:
// Day 1
val sessionId = UUID.randomUUID().toString()
agent.run("Hello, I need help triaging a bug", sessionId = sessionId)
// store sessionId somewhere persistent (database, cookie, etc.)
// Day 2
val resumedSessionId = loadSessionIdForUser(...)
agent.run("Following up on the bug from yesterday", sessionId = resumedSessionId)
// agent sees both messagesDon't store the session ID inside the agent or in AIAgentStorage — that storage is run-scoped. The session ID is the user's identity in the chat history backend; persist it in your application's user/session layer.
Multi-turn within one process: the same agent instance can call agent.run(input, sessionId = ...) repeatedly. The installed chat-history backend (JdbcChatHistory / ChatHistoryAws / ChatMemorySql) accumulates the message log on each call, so a while (true) { agent.run(submissions.receive(), sessionId = userId) } loop maintains context across user turns without reconstructing the agent.
Proceed immediately to Step 6.
The chat-history feature is for real conversation turns between the user and the agent. Do not synthesise fake Message.User / Message.Assistant entries to inject domain facts the agent "should remember" — even if it's tempting to implement ChatHistoryProvider with a hand-rolled list of pseudo-turns.
Symptoms that you've gone down this wrong path:
[2025-06-19] to each pseudo-turn to stop the model from treating the latest entry as "today's" task. The data isn't conversation-shaped; the model is correctly confusedMessage.Assistant content makes claims about actions the agent never actually tookThe right Koog primitive depends on the data's shape:
LongTermMemory (see Skill(skill: "manage-state") Step 3) — designed for stored facts with explicit SearchQueryProvider retrieval@Tool (see Skill(skill: "add-tool")) — getRecentlyUsedFlavors(organizer), getPriorDeclines(eventId), etc.systemPromptstorage.set(key, value) (see Skill(skill: "manage-state") Step 1)A custom ChatHistoryProvider is legitimate when the source IS conversation messages — replaying a stored chat from an external system, mirroring a Slack thread, etc. It is not legitimate as a backdoor for facts.
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