Koog 1.3 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
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) — concrete providers such as PostgresJdbcChatHistoryProvider and MySQLJdbcChatHistoryProvider. Default when the user names Postgres / MySQL / a JDBC URLchat-history-aws) — AgentcoreChatHistoryProvider for Bedrock AgentCore short-term memorychat-memory-sql) — Exposed-backed providers such as PostgresChatHistoryProviderThese modules supply providers for ChatMemory; they are not installable features.
Proceed immediately to Step 3.
implementation("ai.koog:agents-features-chat-history-jdbc:1.3.0")
// or:
// implementation("ai.koog:agents-features-chat-history-aws:1.3.0-beta")
// or:
// implementation("ai.koog:agents-features-chat-memory-sql:1.3.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.chatMemory.feature.ChatMemory
import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.features.chathistory.jdbc.PostgresJdbcChatHistoryProvider
import ai.koog.prompt.executor.clients.openai.OpenAIModels
import ai.koog.prompt.executor.model.PromptExecutor
import org.postgresql.ds.PGSimpleDataSource
suspend fun chatAgent(executor: PromptExecutor): AIAgent<String, String> {
val dataSource = PGSimpleDataSource().apply {
setURL(requireNotNull(System.getenv("DB_URL")) { "Set DB_URL" })
user = requireNotNull(System.getenv("DB_USER")) { "Set DB_USER" }
password = requireNotNull(System.getenv("DB_PASSWORD")) { "Set DB_PASSWORD" }
}
val historyProvider = PostgresJdbcChatHistoryProvider(dataSource)
historyProvider.migrate()
return AIAgent(
promptExecutor = executor,
llmModel = OpenAIModels.Chat.GPT4o,
systemPrompt = "Help the user resume their conversation.",
) {
install(ChatMemory) {
chatHistoryProvider = historyProvider
windowSize(50)
}
}
}Include the PostgreSQL JDBC driver for PGSimpleDataSource. Migrate the provider's
schema before serving requests. Read credentials from environment variables.
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. ChatMemory loads and stores history through its
configured provider. Keep the session ID in the application's user/session layer.
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.
.tessl-plugin
skills
add-observability
add-persistence
references
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
references
test-koog-agents
trace-agent-internals
use-agent-skills
references
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