Building AI Agents in Kotlin • Anton Arhipov • YOW! 2025
Anton Arhipov from JetBrains introduces Koog, a lightweight, Kotlin-native framework for building tool-using LLM agents. This session covers the rationale for using Kotlin in AI, the architecture of Koog agents, and how its graph-based DSL enables the creation of structured, type-safe, and reproducible agent workflows, moving beyond simple prompt-chaining to sophisticated orchestration.
The Case for AI Agents and Kotlin
When tackling problems with a vast, vaguely defined solution space, such as code generation, AI agents offer a more flexible approach than deterministic software. While Python has been the traditional choice for AI development, building agents on the JVM using a language like Kotlin presents significant advantages, especially when integrating with existing enterprise applications.
The decision by JetBrains to build the Koog framework in Kotlin was driven by practical needs, not just language preference. Key reasons include:
- JVM Integration: Agents need to run on the JVM to seamlessly interact with the domain models and business logic of products written in Java and Kotlin. This avoids the complexities of cross-language communication and keeps the software self-sufficient.
- Orchestration Needs: Building an agent is more than creating a client for an LLM. It involves complex orchestration, internal loops, and interaction with domain-specific objects, which benefits from a robust, statically-typed language.
- Language Capabilities: Kotlin’s features, such as type safety, coroutines for asynchronous operations, and powerful DSL (Domain-Specific Language) capabilities, are exceptionally well-suited for defining the structured, complex workflows required by modern AI agents.
Koog Agent Architecture
The fundamental architecture of a Koog agent involves a continuous loop of interaction between the user, the agent, the LLM, and a set of tools.
- User Input: The user provides a task or query to the agent.
- LLM Request: The agent sends the request, along with the system prompt and available tools, to an LLM.
- LLM Response: The model responds in one of two ways:
- Assistant Message: A direct textual answer, signaling the completion of the task.
- Tool Call: An instruction for the agent to execute a specific tool with certain parameters to gather more information or perform an action.
- Tool Execution: The agent invokes the requested tool (e.g., a system command, an API call, a file system operation).
- Feedback Loop: The result of the tool’s execution is sent back to the LLM. This loop continues until the LLM can provide a final answer via an assistant message.
The core logic and orchestration—the “brain” of the operation—are defined within the agent’s Strategy.
The Unpredictability of Tool Calling
A primary challenge in building agents, particularly with smaller local models like Mistral, is the unreliability of tool calling. The model’s decision to use a tool is probabilistic and heavily influenced by the system prompt and the model’s inherent capabilities.
Experiments show that a model might:
- Fail to generate a tool call even when a relevant tool is available.
- Hallucinate having access to information (e.g., real-time weather data) without actually using a tool.
- Get stuck in a repetitive loop, providing the same answer repeatedly without progressing.
- Recognize a tool but fail to generate the specific “tool call” response format, preventing the agent from executing it.
This unpredictability makes simple, prompt-driven agents unsuitable for business-critical applications that require reliability and deterministic workflows.
Structured Workflows with Graph-Based Strategies
Koog addresses the challenge of unpredictability by allowing developers to define agent behavior using a graph-based DSL. This shifts the control from the probabilistic LLM to a structured, type-safe workflow defined in code.
A Strategy in Koog is a graph composed of nodes and edges:
- Nodes: Represent individual, verifiable steps in a process. A node can be a simple data transformation (e.g., converting a string to an integer) or a complex operation involving an LLM call.
- Edges: Define the flow of execution from one node to another.
This DSL is heavily reliant on Kotlin’s type system. The compiler ensures that the output type of one node matches the expected input type of the next, preventing runtime errors. For instance, you cannot connect a node that outputs an Int to a node that expects a String without an explicit transformation.
// Conceptual example of the DSL
val stringToInt = node<String, Int> { it.toInt() }
val increment = node<Int, Int> { it + 1 }
// Define the execution flow
start.forwardTo(stringToInt)
stringToInt.forwardTo(increment)
increment.forwardTo(finish)
This approach also supports conditional branching, allowing the agent to follow different paths based on the data. By defining the workflow as a graph, the agent’s logic becomes explicit, readable, and reproducible.
Domain Modeling and Structured Output
For sophisticated tasks like code generation, agents must interact with domain-specific data structures. For example, before writing code, an agent might first be asked to generate a development plan, which should map to a Plan object in the application code.
Simply asking an LLM to generate JSON is often not robust enough. Koog facilitates a more reliable approach by leveraging structured output schemas. By annotating Kotlin data classes (Plan, PlanItem), developers can provide the LLM with a precise schema for its response. This forces the model to generate a response that can be reliably deserialized into the application’s domain objects, enabling type-safe interaction with the agent’s output. This is another area where keeping the agent and the application on the same platform (the JVM) is highly beneficial.