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.
Default to read-only queries with a database principal granted SELECT only on the required tables. Inspect the user's actual schema before writing queries. Use typed lookup tools with prepared SQL and bounded results. Add each required analytics operation explicitly; do not expose unrestricted model-generated SQL.
agents-features-sql provides agent checkpoint storage. It has no installable
Sql feature, schemaScope configuration or automatically registered query tool.
Proceed immediately to Step 2.
For PostgreSQL, add implementation("org.postgresql:postgresql:42.7.10") beside
the existing ai.koog:koog-agents:1.3.0 dependency. Reuse the application's
DataSource or connection pool when available.
Proceed immediately to Step 3.
Write the tool class and updated agent construction to the user's source files.
The example assumes public.events(event_type, user_id, occurred_at) and
public.users(id). Adapt these names to the inspected schema.
import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.core.tools.annotations.Tool
import ai.koog.agents.core.tools.reflect.ToolSet
import ai.koog.agents.core.tools.reflect.asTools
import ai.koog.prompt.executor.clients.openai.OpenAIModels
import ai.koog.prompt.executor.model.PromptExecutor
import java.sql.Timestamp
import java.time.Instant
import javax.sql.DataSource
import org.postgresql.ds.PGSimpleDataSource
class AnalyticsQueries(private val dataSource: DataSource) : ToolSet {
@Tool
fun countEvents(startInclusive: String, endExclusive: String, limit: Int = 100): List<String> {
val start = Instant.parse(startInclusive)
val end = Instant.parse(endExclusive)
require(end > start) { "End must follow start; use ISO-8601 instants" }
val rowLimit = limit.coerceIn(1, 100)
return dataSource.connection.use { connection ->
connection.isReadOnly = true
connection.prepareStatement(
"""
SELECT e.event_type, COUNT(*) AS event_count
FROM public.events e JOIN public.users u ON u.id = e.user_id
WHERE e.occurred_at >= ? AND e.occurred_at < ?
GROUP BY e.event_type ORDER BY e.event_type LIMIT ?
""".trimIndent(),
).use { statement ->
statement.queryTimeout = 5
statement.maxRows = rowLimit
statement.setTimestamp(1, Timestamp.from(start))
statement.setTimestamp(2, Timestamp.from(end))
statement.setInt(3, rowLimit)
statement.executeQuery().use { results ->
buildList {
while (results.next()) {
add("${results.getString(1)}: ${results.getLong(2)}")
}
}
}
}
}
}
}
fun analyticsAgent(executor: PromptExecutor) = AIAgent(
promptExecutor = executor,
llmModel = OpenAIModels.Chat.GPT4o,
systemPrompt = """
Use countEvents for event totals in a requested interval.
Inputs are ISO-8601 instants; the start is inclusive and the end exclusive.
The query reads public.events(event_type, user_id, occurred_at) and
public.users(id). Results are grouped counts, capped at 100 groups.
""".trimIndent(),
toolRegistry = ToolRegistry {
val dataSource = PGSimpleDataSource().apply {
setURL(requireNotNull(System.getenv("ANALYTICS_DB_URL")) { "Set ANALYTICS_DB_URL" })
user = requireNotNull(System.getenv("ANALYTICS_DB_USER")) { "Set ANALYTICS_DB_USER" }
password = requireNotNull(System.getenv("ANALYTICS_DB_PASSWORD")) { "Set ANALYTICS_DB_PASSWORD" }
}
tools(AnalyticsQueries(dataSource).asTools())
},
)Add other approved lookups as typed methods with fixed table/column names and bound parameters. Register only the operations the user requested. For writes, use a separate, explicitly authorized tool with transaction and approval controls.
Proceed immediately to Step 4.
Run ./gradlew build. Against a development database with the intended principal:
countEventsA JDBC read-only flag is a driver hint, not a replacement for database grants. A row cap bounds returned data, not scan cost; configure server-side statement limits and indexes for the approved queries. Return only data permitted in the model's context.
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