JetBrains Koog: A Kotlin-Native Agent Framework That Skips the Python Tax
If your backend is Kotlin or Java and you want to add AI agents, you’ve been forced to either wrap a Python service or accept a framework that treats your language as a second-class citizen — until now.
Background
LangChain has become the de-facto standard for orchestrating LLM-powered workflows. It’s flexible, well-documented, and has a massive ecosystem. But it’s Python-first, and when you’re running a JVM backend — whether Spring Boot, Ktor, or Android — that creates real friction. You either embed a Python subprocess, call a separate microservice, or use LangChain4j, which is a community-maintained Java port that lags behind the original and lacks native Kotlin idioms.
JetBrains announced Koog at KotlinConf 2025 to address exactly this gap. The stated goal: a Kotlin-native agent framework with the same depth LangChain has, but built from the ground up for the JVM ecosystem — with Kotlin Multiplatform as a first-class target. As of version 0.7.x, it’s production-capable and supports JVM, Android, iOS, JavaScript, and WebAssembly targets.
How It Works
Koog models an AI agent as a strategy graph: a directed graph of nodes where each node can invoke an LLM, execute a tool, branch on conditions, or run sub-graphs in parallel. You define this graph using a Kotlin DSL, and the runtime executes it, managing state, history, retries, and observability transparently.
The core abstraction is the AIAgent, which wraps a strategy (a graph or a simpler functional sequence) and a set of tools. Tools are just annotated Kotlin functions — Koog uses Kotlin serialization to auto-generate the JSON descriptors that LLMs need to call them. No separate schema file, no boilerplate.
State persistence is where Koog diverges most sharply from LangChain. Rather than just checkpointing message history, Koog persists the entire state machine at any graph node. This means a crashed agent can resume from the exact strategy step it was on — not just replay from the last stored message. Backends include local disk, S3, or any database, and the new RollbackToolRegistry in v0.5.0 can undo tool side effects when rolling back to a checkpoint.
On the observability side, Koog ships with native OpenTelemetry exporters for Langfuse and W&B Weave. Every LLM request, tool call, and node transition is traced. In LangChain you’d typically wire this yourself via callbacks; in Koog it’s a one-liner install(OpenTelemetry) block on any agent.
LangChain’s Python async model is built on asyncio. Koog’s async story is Kotlin coroutines: cancellation is structured, backpressure is idiomatic, and there’s no GIL. Parallel graph branches use a MapReduce-style API — launch branches with fork, transform results asynchronously, collect at the end.
Code
LangChain (Python) — defining a tool and wiring an agent:
from langchain.tools import tool
from langchain.agents import initialize_agent
@tool
def get_weather(city: str) -> str:
"""Returns current weather for a city."""
return fetch_weather_api(city)
agent = initialize_agent(
tools=[get_weather],
llm=ChatOpenAI(model="gpt-4o"),
agent="openai-functions",
verbose=True,
)
agent.run("What's the weather in Berlin?")
Koog (Kotlin) — equivalent agent with typed tool:
val weatherTool = tool(
name = "get_weather",
description = "Returns current weather for a city"
) { city: String ->
fetchWeatherApi(city) // any suspend fun in your codebase
}
val agent = AIAgent(
promptMessage = "What's the weather in Berlin?",
llmClient = OpenAILLMClient("gpt-4o"),
tools = listOf(weatherTool),
strategy = strategy("weather-agent") {
val result = node { callLlm() }
edge(result to finish())
}
)
runBlocking { agent.run() }
Adding persistence (Koog only — no LangChain equivalent in-framework):
val agent = AIAgent(...) {
install(Persistence) {
storage = S3AgentStorage(bucket = "my-agents")
// Agent state machine is checkpointed at every node transition
}
install(OpenTelemetry) {
addLangfuseExporter(
langfuseUrl = "https://cloud.langfuse.com",
langfusePublicKey = System.getenv("LANGFUSE_KEY"),
langfuseSecretKey = System.getenv("LANGFUSE_SECRET")
)
}
}
Parallel branches (MapReduce-style):
strategy("parallel-research") {
val search = parallelNode {
listOf("topic A", "topic B", "topic C").map { query ->
fork { callLlm(query) } // launched concurrently
}
}
val merge = node { summarize(search.results) }
edge(search to merge)
edge(merge to finish())
}
Trade-offs & Limitations
Koog is still pre-1.0 (current: 0.7.x), and the API surface has shifted across releases — v0.3 to v0.5 introduced breaking changes to tool descriptors and the non-graph strategy API. If you’re building on it now, pin your version and treat minor bumps as potentially breaking. The Python/LangChain ecosystem has a two-year head start in community integrations, vector store connectors, and pre-built chain templates. Koog’s ecosystem is growing but thin by comparison. iOS support was added by a community contributor in v0.4.0 and is newer than the JVM path. The graph-based strategy DSL has a real learning curve if you’re used to LangChain’s chain-of-thought or LCEL (LangChain Expression Language) approach — it’s more explicit and more powerful, but also more verbose for simple cases.
My Take
Koog is the right call if your team already lives on the JVM and you want AI agents that fit into your existing Spring Boot or Ktor services without a Python sidecar. The state-machine persistence model is genuinely better than anything LangChain offers out of the box — it’s the difference between “replay from message history” and “resume exactly where you crashed.” The Kotlin Multiplatform angle is particularly interesting for teams like those building KMP libraries: you can run the same agent logic on JVM, Android, and iOS without forking. For teams already on Python with a mature LangChain setup, the switching cost isn’t worth it yet. But for greenfield JVM projects or anyone already using KMP, Koog removes what was the last real blocker to writing agents in Kotlin.
Tags: Kotlin, AI Agents, KMP, JetBrains
