Koog
If you build AI features on the JVM right now, you’ve probably already hit two extremes:
- the controlled path: one request in, one model response out
- the hand-wavy path: “agents” that sound impressive until you have to explain what the code is actually doing
Koog is interesting because the first step is small enough to evaluate without turning your application into a science project. For a Java engineer, the practical mental model is straightforward: create an AIAgent, wire a prompt executor, choose a model, call run(...), and get a String back from a String input.
That makes it a reasonable candidate when you want to move beyond a plain prompt call but still keep the first implementation understandable.
This article stays on that path: get one working Koog agent running on the JVM, then look at the first customizations and tool choices that actually matter.
What matters up front
Before you write code, the baseline is:
- JDK 17+
- Kotlin 2.2.0+
- Gradle 8.0+ or Maven 3.8+
If your application is mostly Java, it is still worth saying plainly that Koog lives in a Kotlin-centered JVM world. That is not a dealbreaker. It just means you should validate the build path early instead of assuming a purely Java-first experience.
For dependencies, keep the version aligned across both build paths:
- Gradle examples use
ai.koog:koog-agents:1.0.0 - Maven examples use
ai.koog:koog-agents-jvm:1.0.0
I would still verify the exact coordinates in the build you actually ship, but there is no reason to carry a fake version mismatch into your internal notes.
Koog also needs either:
- an API key from a supported LLM provider, or
- a locally running LLM
That sounds simple, but it affects how you test, deploy, and compare environments. Validate that choice early.
Gradle
dependencies {
implementation("ai.koog:koog-agents:1.0.0")
}
Maven
<dependencies>
<dependency>
<groupId>ai.koog</groupId>
<artifactId>koog-agents-jvm</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
Do not hardcode the API key
For a hosted model path, use environment variables. Do not paste credentials into source.
For OpenAI, the environment variable is:
export OPENAI_API_KEY=your_key_here
On Windows PowerShell:
$env:OPENAI_API_KEY="your_key_here"
That sounds obvious, but “just for the sample” is how secrets end up in commits, terminal history, and internal snippets. Avoid the cleanup exercise entirely.
If you choose a local LLM instead, keep the same engineering habit: validate the dependency you need before traffic hits it.
The smallest useful Koog agent
For a first pass, keep the contract brutally simple:
- input:
String - output:
String
That is the documented basic shape of an AIAgent here, and it is the right place to start. You can worry about richer typing later.
The OpenAI path is also intentionally small:
AIAgentsimpleOpenAIExecutor(apiKey)OpenAIModels.Chat.GPT4oagent.run(...)
Minimal Java example
import ai.koog.agents.core.agent.AIAgent;
import ai.koog.prompt.executor.clients.openai.OpenAIModels;
import static ai.koog.prompt.executor.clients.openai.OpenAIExecutorKt.simpleOpenAIExecutor;
public class FirstKoogAgent {
public static void main(String[] args) {
String apiKey = System.getenv("OPENAI_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("OPENAI_API_KEY is not set");
}
AIAgent<String, String> agent = AIAgent.<String, String>builder()
.promptExecutor(simpleOpenAIExecutor(apiKey))
.llmModel(OpenAIModels.Chat.GPT4o)
.build();
String response = agent.run("Hello! How can you help me?");
System.out.println(response);
}
}
That is a good first milestone because it proves the boring things first:
- your dependency setup works
- credentials are loading
- the model path is reachable
AIAgentruns in your environment
For evaluation work, that is enough. Get this running before you add anything else.
What this basic agent gives you
At this stage, keep your claims modest and your expectations clear. A basic AIAgent here is a string-in, string-out component executed with run(...).
That may sound almost too simple, but that is exactly why it is useful for early evaluation. You can decide whether the library fits your codebase before you start layering on more moving parts.
The first customizations you’ll actually want
Once the minimal path works, the next things worth caring about are the ones that shape behavior without turning the example into guesswork.
Two supported levers matter immediately:
systemPrompt- behavior parameters such as
temperature = 0.7
1. Add a systemPrompt
If the agent has any job beyond generic chat, give it explicit instructions.
A systemPrompt is where you define things like:
- role
- purpose
- context
- instructions
That is the first step from “it answers” to “it behaves like it has a job.”
If I were putting a first agent behind a service boundary, I would use the system prompt to constrain tone, make clarification behavior explicit, and stop the model from implying actions it did not actually perform.
2. Adjust behavior parameters deliberately
Koog supports customizing LLM behavior with parameters such as:
temperature = 0.7
That does not mean 0.7 is automatically the right value. It means the behavior is tunable, and you should tune it based on the job.
For a first application-oriented agent, I usually prefer consistency over novelty. The exact number is your call, but the broader point is simple: make behavior choices intentionally, not by accident.
I am being careful here for a reason: if you are working in Java, avoid assuming builder parity for every Kotlin customization until you verify it in your codebase. The supported concepts are clear; exact Java builder syntax for every option is something I would confirm in the project before treating it as settled.
Tools are where Koog gets more interesting
A no-tool agent is fine for proving the path. Tools are what turn that path into something that can interact with the outside world.
For Java, the important guidance is clear: use annotation-based tools.
More specifically:
- Java uses
@Tooland@LLMDescription - registration is reflection-based
- tools are registered through
ToolRegistry - subclassing Kotlin
ToolorSimpleToolfrom Java is not supported because of suspend-function limitations
That last point is worth remembering. If you are writing Java, do not waste time trying to force the Kotlin subclassing model into a Java inheritance pattern that is not meant to work there.
A simple conceptual Java tool
import ai.koog.agents.core.tools.annotation.Tool;
import ai.koog.agents.core.tools.annotation.LLMDescription;
public class UserTools {
@Tool
@LLMDescription("Ask the user for clarification and return their answer")
public String askUser(String question) {
System.out.println(question);
return "user response";
}
}
Keep this example conceptual. The exact Java registration syntax is something I would verify directly in the codebase before putting it in a template, but the model is clear enough:
- annotate tool methods
- register them through
ToolRegistry - make them available to the agent
That is the point where an agent stops being only a text generator and starts gaining access to constrained application capabilities.
Tool design advice for a first real agent
Even without getting fancy, there are a few rules that will save you time.
1. Start with narrow tools
Good first tools are specific:
- fetch one piece of data
- validate one identifier
- calculate one thing
That keeps the contract obvious and gives the model fewer ways to misuse the capability.
2. Be careful with side effects
If a tool changes state, treat it with more suspicion than a read-only tool.
For a first agent, I would strongly prefer tools that read, validate, or calculate rather than mutate. That is not a Koog rule; it is just sane engineering.
3. Treat demo I/O like demo I/O
The console conversation example is useful for understanding the tool model. It is not a server design.
A tool that writes to stdout and reads from stdin is fine for local exploration. Inside a real service, that is not a shortcut. It is a dead end.
Kotlin tool support is richer than the Java path
If your team is willing to write Kotlin for tool implementations, Koog exposes a deeper tool model there.
The supported options include:
Tool<Args, Result>as the base class for custom toolsSimpleTool<Args>for text-only resultsAgentContextAwareTool<Args, Result>when a tool needs live agent stateToolBase<Args, Result>when you need raw per-call metadata
A few details matter:
- argument and result types must be serializable
Tool<Args, Result>requires serializers, a descriptor, and anexecute()implementation- if a custom result needs a specific LLM-facing text format, the result should inherit
ToolResult.TextSerializableand implementtextForLLM() ToolResultUtils.toTextSerializer()is suggested for serialization in that caseAgentContextAwareToolgets the currentAIAgentContext- calling an
AgentContextAwareTooloutside an agent run throwsIllegalStateExceptionunless context is supplied viaToolCallMetadata - metadata can be passed through
SafeTool.execute(args, serializer, metadata)orAIAgentEnvironment.executeTool(toolCall, metadata) - caller-supplied metadata overrides feature-contributed metadata on key collisions
You do not need any of that for the first Java-based evaluation, but it does tell you something important: if your use case grows more sophisticated, Kotlin is where the tool surface gets more expressive.
Failure cases to expect early
You do not need production traffic to find the first rough edges.
Missing or bad OPENAI_API_KEY
This is the boring failure, which means it happens constantly.
Check for the variable early. Fail fast. Do not wait for the first live request to discover that credentials are missing or invalid.
Build and dependency confusion
The Gradle and Maven coordinates do not line up cleanly in the examples. That is exactly the kind of issue that burns time in evaluation work.
Verify:
- which artifact you are actually using
- whether it resolves cleanly in your build
- whether your packaging assumptions still hold
Again, not glamorous, but this is the work that decides whether a library is easy to adopt in a real JVM codebase.
How I’d fit this into a Java application
If you already have a Java or Spring application, the safest adoption pattern is narrow.
A reasonable boundary looks like this:
- your application handles transport, auth, validation, and operational policy
- Koog handles the agent execution path
That separation keeps the experiment honest. You are evaluating one concern at a time instead of mixing application responsibilities with model behavior and tool wiring all at once.
I would also keep the first Koog path isolated enough that you can answer practical questions in staging:
- does the dependency setup work in the deployed build
- are credentials or local-model dependencies configured correctly
- does the basic agent call path behave the way your service expects
Those are verification questions, not product claims, and they are exactly the right questions to ask early.
OpenAI is a clean starting point, not the whole story
For the first walkthrough, OpenAI with OpenAIModels.Chat.GPT4o is the clean path because it is the most direct documented setup.
That does not mean Koog is locked to only that one setup. What is safe to say is narrower and more useful: Koog works with either an API key from a supported LLM provider or a locally running LLM.
That is enough for an evaluation strategy:
- start with the smallest known-good hosted path
- prove the first agent works
- then verify any local-model or alternate-provider setup you care about
That order removes variables instead of adding them.
A practical recommendation for your first evaluation
If I were evaluating Koog in a real JVM codebase, I would do it in this order:
- create the smallest
AIAgent<String, String> - load
OPENAI_API_KEYfrom the environment - use
simpleOpenAIExecutor(apiKey) - set
OpenAIModels.Chat.GPT4o - call
agent.run(...) - add a short
systemPrompt - tune behavior parameters such as temperature if needed
- add one narrow tool
That sequence keeps the first success small and the next failure easy to isolate.
Final take
Koog makes a good first impression because the initial path is tiny:
AIAgentsimpleOpenAIExecutor(System.getenv("OPENAI_API_KEY"))OpenAIModels.Chat.GPT4oagent.run(...)
That is exactly what I want in an evaluation: not a grand platform story, just a short path to a real request.
The part worth respecting is everything that comes after the hello-world moment:
- prompt discipline
- tool boundaries
- build verification
- credential handling
- runtime checks in the environment you actually operate
So yes, Koog is a sensible place to start if you are a Java engineer exploring agent-style behavior on the JVM. Just keep the first version honest. Prove the minimal path, verify the build details, add one narrow tool, and only then decide whether it deserves a larger role in the system.