# Koog
Koog is a Kotlin-based framework designed to build and run AI agents entirely in idiomatic Kotlin.
# Documentation
# Overview
Koog is an open-source JetBrains framework for building AI agents designed specifically for the JVM ecosystem. It provides a first-class development experience for both Kotlin and Java developers, featuring an idiomatic, type-safe Kotlin DSL and fluent builder-style Java APIs.
While Java developers can leverage the full power of Koog on the JVM using idiomatic APIs, Kotlin developers can also deploy agents across JS, WasmJS, Android, and iOS targets using Kotlin Multiplatform.
- [**Quickstart**](quickstart/)
______________________________________________________________________
Build and run your first AI agent
- [**Glossary**](glossary/)
______________________________________________________________________
Learn the essential terms
- [**Module versioning**](module-versioning/)
______________________________________________________________________
Understand stable vs. beta modules and API guarantees
## Agents
Learn about [agents in general](agents/) and how to create different types of agents using Koog:
- [**Basic agents**](agents/basic-agents/)
______________________________________________________________________
Use a predefined strategy that works for most common use cases
- [**Functional agents**](agents/functional-agents/)
______________________________________________________________________
Define custom logic as a lambda function in plain Kotlin or Java
- [**Graph-based agents**](agents/graph-based-agents/)
______________________________________________________________________
Implement a custom workflow as a strategy graph
- [**Planner agents**](agents/planner-agents/) beta
______________________________________________________________________
Iteratively build and execute a plan until the state matches the desired conditions
## Core components
Learn about the core components of Koog agents in detail:
- [**Prompts**](prompts/)
______________________________________________________________________
Create, manage, and run prompts that drive the agent's interaction with the LLM
- [**Strategies**](predefined-agent-strategies/)
______________________________________________________________________
Design the agent's intended workflow as a directed graph
- [**Tools**](tools/)
______________________________________________________________________
Enable the agent to interact with external data sources and services
- [**Features**](features/)
______________________________________________________________________
Extend and enhance the functionality of AI agents
## Advanced usage
- [**History compression**](history-compression/)
______________________________________________________________________
Optimize token usage while maintaining context in long-running conversations using advanced techniques
- [**Agent persistence**](features/agent-persistence/)
______________________________________________________________________
Restore the agent state at specific points during execution
- [**Structured output**](structured-output/)
______________________________________________________________________
Generate responses in structured formats
- [**Streaming API**](streaming-api/)
______________________________________________________________________
Process responses in real-time with streaming support and parallel tool calls
- [**Knowledge retrieval**](embeddings/) beta
______________________________________________________________________
Retain and retrieve knowledge across conversations using [vector embeddings](embeddings/) and [RAG](retrieval-augmented-generation/)
- [**Tracing**](features/tracing/)
______________________________________________________________________
Debug and monitor agent execution with detailed, configurable tracing
- [**Long Term Memory**](features/long-term-memory/) beta
______________________________________________________________________
Integrate vector databases and memory providers for RAG and persistent memory.
## Integrations
- [**Model Context Protocol (MCP)**](model-context-protocol/) beta
______________________________________________________________________
Use MCP tools directly in AI agents
- [**Spring Boot**](spring-boot/) beta
______________________________________________________________________
Add Koog to your Spring applications
- [**Ktor**](ktor-plugin/) beta
______________________________________________________________________
Integrate Koog with Ktor servers
- [**OpenTelemetry**](features/open-telemetry/)
______________________________________________________________________
Trace, log, and measure your agent with popular observability tools
- [**A2A Protocol**](a2a/) beta
______________________________________________________________________
Connect agents and services over a shared protocol
# Key features
Key features of Koog include:
- **Idiomatic Kotlin and Java support**: Choose between a type-safe Kotlin DSL or a dedicated, fluent Java builder API. The Java API is designed to feel natural to Java teams, using standard thread pool executors instead of exposing coroutines.
- **Reliability and fault-tolerance**: Handle failures with built-in retries and restore the agent state at specific points during execution with the agent persistence feature.
- **Intelligent history compression**: Optimize token usage while maintaining context in long-running conversations using advanced built-in history compression techniques.
- **Enterprise-ready integrations**: Utilize integration with popular JVM frameworks such as Spring Boot and Ktor to embed Koog into your applications.
- **Observability with OpenTelemetry exporters**: Monitor and debug applications with built-in support for popular observability providers (W&B Weave, Langfuse).
- **LLM switching and seamless history adaptation**: Switch to a different LLM at any point without losing the existing conversation history or reroute between multiple LLM providers.
- **Multiplatform development**: For agents written in Kotlin, deploy agents across JVM, JS, WasmJS, Android, and iOS targets using Kotlin Multiplatform.
- **Model Context Protocol integration**: Use Model Context Protocol (MCP) tools in AI agents.
- **Knowledge retrieval and memory**: Retain and retrieve knowledge across conversations using vector embeddings and RAG.
- **Powerful Streaming API**: Process responses in real-time with streaming support and parallel tool calls.
- **Modular feature system**: Customize agent capabilities through a composable architecture.
- **Flexible graph workflows**: Design complex agent behaviors using intuitive graph-based workflows.
- **Custom tool creation**: Enhance your agents with tools that access external systems and APIs.
- **Comprehensive tracing**: Debug and monitor agent execution with detailed, configurable tracing.
# Versioning
Koog follows [Semantic Versioning](https://semver.org/) with the format `X.Y.Z` (e.g., `1.2.0`).
The framework is API-stable: once a public API is released, it will not be broken without a major version bump.
## Version Components
| Component | Name | Format | Meaning |
| --------- | ------ | ------- | ---------------------------------------------------------------------- |
| `X` | Major | `X.y.z` | Breaking changes to existing APIs |
| `Y` | Minor | `x.Y.z` | New API additions and deprecations; all existing APIs continue to work |
| `Z` | Bugfix | `x.y.Z` | Bug fixes only; no API changes |
### Major (`X`)
- May introduce breaking changes to existing APIs.
- Old APIs may be removed.
- A migration guide will be provided.
- Released at most once per year.
### Minor (`Y`)
- May add new APIs.
- May deprecate existing APIs (with replacements provided), but deprecated APIs remain functional.
- No breaking changes — all code that compiled against the previous minor version continues to compile and work.
- Released at most once per month.
### Bugfix (`Z`)
- Contains bug fixes only.
- No API additions, removals, or deprecations.
- Released at most once per week.
## Deprecation Policy
APIs deprecated in a minor release (`Y`) will remain available until at least the next major release (`X`). Deprecation warnings will indicate the recommended replacement.
## Stable and Beta Modules
Some modules are considered experimental and published with a `-beta` version suffix (e.g., `1.2.0-beta`) rather than the standard `X.Y.Z`. A module may be beta for one of several reasons:
- **External integrations** — the underlying LLM provider API or external framework (e.g., Spring AI) may itself be unstable or subject to frequent or expected change.
- **Experimental functionality** — the feature area is still being explored and the API shape may evolve (e.g., GOAP planning strategies).
- **Experimental protocols** — the module implements a protocol that is not yet stable itself (e.g., A2A, Kotlin MCP).
While every effort is made to keep beta modules stable, some API changes may occur across minor releases. Beta changes will not affect any stable module.
A stable module at version `X.Y.Z` is always compatible with a beta module at version `X.Y.Z-beta` (and vice versa). All modules can be updated in sync.
### Umbrella Modules
| Module | Version | Contents |
| ----------------------- | ------------ | ------------------------------------------------------------------------ |
| `koog-agents` | `1.2.0` | All stable modules (transitive) — recommended starting point |
| `koog-agents-additions` | `1.2.0-beta` | Most beta/experimental modules (except standalone external integrations) |
### Module Versions
| Module | Version |
| ------------------------------------ | ------- |
| `agents` | `1.2.0` |
| `agents-core` | `1.2.0` |
| `agents-features` | `1.2.0` |
| `agents-features-chat-history-jdbc` | `1.2.0` |
| `agents-features-chat-memory-sql` | `1.2.0` |
| `agents-features-event-handler` | `1.2.0` |
| `agents-features-memory` | `1.2.0` |
| `agents-features-opentelemetry` | `1.2.0` |
| `agents-features-persistence-jdbc` | `1.2.0` |
| `agents-features-snapshot` | `1.2.0` |
| `agents-features-sql` | `1.2.0` |
| `agents-features-tokenizer` | `1.2.0` |
| `agents-features-trace` | `1.2.0` |
| `agents-mcp-metadata` | `1.2.0` |
| `agents-test` | `1.2.0` |
| `agents-tools` | `1.2.0` |
| `agents-utils` | `1.2.0` |
| `embeddings` | `1.2.0` |
| `embeddings-base` | `1.2.0` |
| `embeddings-llm` | `1.2.0` |
| `http-client` | `1.2.0` |
| `http-client-core` | `1.2.0` |
| `http-client-java` | `1.2.0` |
| `http-client-ktor` | `1.2.0` |
| `http-client-okhttp` | `1.2.0` |
| `http-client-test` | `1.2.0` |
| `koog-agents` | `1.2.0` |
| `koog-spring-ai` | `1.2.0` |
| `prompt` | `1.2.0` |
| `prompt-cache` | `1.2.0` |
| `prompt-cache-files` | `1.2.0` |
| `prompt-cache-model` | `1.2.0` |
| `prompt-executor` | `1.2.0` |
| `prompt-executor-anthropic-client` | `1.2.0` |
| `prompt-executor-bedrock-client` | `1.2.0` |
| `prompt-executor-cached` | `1.2.0` |
| `prompt-executor-clients` | `1.2.0` |
| `prompt-executor-model` | `1.2.0` |
| `prompt-executor-ollama-client` | `1.2.0` |
| `prompt-executor-openai-client` | `1.2.0` |
| `prompt-executor-openai-client-base` | `1.2.0` |
| `prompt-executor-openrouter-client` | `1.2.0` |
| `prompt-llm` | `1.2.0` |
| `prompt-markdown` | `1.2.0` |
| `prompt-model` | `1.2.0` |
| `prompt-processor` | `1.2.0` |
| `prompt-structure` | `1.2.0` |
| `prompt-tokenizer` | `1.2.0` |
| `prompt-xml` | `1.2.0` |
| `rag-base` | `1.2.0` |
| `serialization` | `1.2.0` |
| `serialization-core` | `1.2.0` |
| `serialization-jackson` | `1.2.0` |
| `serialization-test` | `1.2.0` |
| `test-tck` | `1.2.0` |
| `test-utils` | `1.2.0` |
| `utils` | `1.2.0` |
| Module | Version |
| ---------------------------------------- | ------------ |
| `a2a-client` | `1.2.0-beta` |
| `a2a-core` | `1.2.0-beta` |
| `a2a-server` | `1.2.0-beta` |
| `a2a-test` | `1.2.0-beta` |
| `a2a-test-server-tck` | `1.2.0-beta` |
| `a2a-transport-client-jsonrpc-http` | `1.2.0-beta` |
| `a2a-transport-core-jsonrpc` | `1.2.0-beta` |
| `a2a-transport-server-jsonrpc-http` | `1.2.0-beta` |
| `agents-ext` | `1.2.0-beta` |
| `agents-features-a2a-client` | `1.2.0-beta` |
| `agents-features-a2a-core` | `1.2.0-beta` |
| `agents-features-a2a-server` | `1.2.0-beta` |
| `agents-features-acp` | `1.2.0-beta` |
| `agents-features-chat-history-aws` | `1.2.0-beta` |
| `agents-features-longterm-memory` | `1.2.0-beta` |
| `agents-features-longterm-memory-aws` | `1.2.0-beta` |
| `agents-mcp` | `1.2.0-beta` |
| `agents-mcp-server` | `1.2.0-beta` |
| `agents-planner` | `1.2.0-beta` |
| `koog-agents-additions` | `1.2.0-beta` |
| `koog-ktor` | `1.2.0-beta` |
| `koog-spring-ai-common` | `1.2.0-beta` |
| `koog-spring-ai-starter-chat-memory` | `1.2.0-beta` |
| `koog-spring-ai-starter-model-chat` | `1.2.0-beta` |
| `koog-spring-ai-starter-model-embedding` | `1.2.0-beta` |
| `koog-spring-ai-starter-vector-store` | `1.2.0-beta` |
| `koog-spring-boot-starter` | `1.2.0-beta` |
| `prompt-cache-redis` | `1.2.0-beta` |
| `prompt-executor-dashscope-client` | `1.2.0-beta` |
| `prompt-executor-deepseek-client` | `1.2.0-beta` |
| `prompt-executor-google-client` | `1.2.0-beta` |
| `prompt-executor-litert-client` | `1.2.0-beta` |
| `prompt-executor-llms-all` | `1.2.0-beta` |
| `prompt-executor-mistralai-client` | `1.2.0-beta` |
| `rag-vector` | `1.2.0-beta` |
# LLM providers
Koog works with major LLM providers and also supports local models using [Ollama](https://ollama.com/). The following providers are currently supported:
| LLM provider | Choose for |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [OpenAI](https://platform.openai.com/docs/overview) (including [Azure OpenAI Service](https://azure.microsoft.com/en-us/products/ai-foundry/models/openai)) | Advanced models with a wide range of capabilities. |
| [Anthropic](https://www.anthropic.com/) | Long contexts and prompt caching. |
| [Google](https://ai.google.dev/) β | Multimodal processing (audio, video), large contexts. |
| [DeepSeek](https://www.deepseek.com/) β | Cost-effective reasoning and coding. |
| [OpenRouter](https://openrouter.ai/) | One integration with an access to multiple models from multiple providers for flexibility, provider comparison, and unified API. |
| [Amazon Bedrock](https://aws.amazon.com/bedrock/) | AWS-native environment, enterprise security and compliance, multi-provider access. |
| [Mistral](https://mistral.ai/) β | European data hosting, GDPR compliance. |
| [Alibaba](https://www.alibabacloud.com/en?_p_lc=1) β ([DashScope](https://dashscope.aliyun.com/) OpenAI-compatible client) | Large contexts and cost-efficient Qwen models. |
| [Ollama](https://ollama.com/) | Privacy, local development, offline operation, and no API costs. |
The table below shows the LLM capabilities that Koog supports and which providers offer these capabilities in their models.
| LLM capability | OpenAI | Anthropic | Google β | DeepSeek β | OpenRouter | Amazon Bedrock | Mistral β | Alibaba β (DashScope OpenAI-compatible client) | Ollama (local models) |
| ------------------------------- | ---------------------------- | ------------------------------- | --------------------------------------------- | ---------- | ---------------- | ---------------- | ------------------------------- | ---------------------------------------------- | --------------------- |
| Supported input | Text, image, audio, document | Text, image, document[1](#fn:1) | Text, image, audio, video, document[1](#fn:1) | Text | Differs by model | Differs by model | Text, image, document[1](#fn:1) | Text, image, audio, video[1](#fn:1) | Text, image[1](#fn:1) |
| Response streaming | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Tools | ✓ | ✓ | ✓ | ✓ | ✓ | ✓[1](#fn:1) | ✓ | ✓ | ✓ |
| Tool choice | ✓ | ✓ | ✓ | ✓ | ✓ | ✓[1](#fn:1) | ✓ | ✓ | – |
| Structured output (JSON Schema) | ✓ | ✓[1](#fn:1) | ✓ | ✓ | ✓[1](#fn:1) | – | ✓ | ✓[1](#fn:1) | ✓ |
| Multiple choices | ✓ | – | ✓ | – | ✓[1](#fn:1) | ✓[1](#fn:1) | ✓ | ✓[1](#fn:1) | – |
| Temperature | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Speculation | ✓[1](#fn:1) | – | – | – | ✓[1](#fn:1) | – | ✓[1](#fn:1) | ✓[1](#fn:1) | – |
| Content moderation | ✓ | – | – | – | – | ✓ | ✓ | – | ✓ |
| Embeddings | ✓ | – | – | – | – | ✓ | ✓ | – | ✓ |
| Prompt caching | ✓[1](#fn:1) | ✓ | – | – | – | – | – | – | – |
| Completion | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Local execution | – | – | – | – | – | – | – | – | ✓ |
Note
Koog supports the most commonly used capabilities for creating AI agents. LLMs from each provider may have additional features that Koog does not currently support. To learn more, refer to [Model capabilities](../model-capabilities/).
## Working with providers
Koog lets you work with LLM providers on two levels:
- Using an **LLM client** for direct interaction with a specific provider. Each client implements the `LLMClient` interface, handling authentication, request formatting, and response parsing for the provider. For details, see [LLM clients](../prompts/llm-clients/).
- Using a **prompt executor** for a higher-level abstraction that wraps one or multiple LLM clients, manages their lifecycles, and unifies an interface across providers. It can switch between providers and optionally fall back to a configured provider and LLM using the corresponding client. You can either create your own executor or use a pre-defined prompt executor for a specific provider. For details, see [Prompt executors](../prompts/prompt-executors/).
Using a prompt executor offers a higher‑level layer over one or more LLMClients. It manages client lifecycles and exposes a unified interface across providers. In multi‑provider setups, it can route requests between providers and optionally fall back to a designated client when needed for core requests. You can create your own executor or use pre‑defined ones—both single‑provider and multi‑provider options are available.
## Next steps
- [Create and run an agent](../quickstart/) with a specific LLM provider.
- Learn more about [prompts](../prompts/).
______________________________________________________________________
1. Capability is supported only by some models of the provider. [↩](#fnref:1 "Jump back to footnote 1 in the text")[↩](#fnref2:1 "Jump back to footnote 1 in the text")[↩](#fnref3:1 "Jump back to footnote 1 in the text")[↩](#fnref4:1 "Jump back to footnote 1 in the text")[↩](#fnref5:1 "Jump back to footnote 1 in the text")[↩](#fnref6:1 "Jump back to footnote 1 in the text")[↩](#fnref7:1 "Jump back to footnote 1 in the text")[↩](#fnref8:1 "Jump back to footnote 1 in the text")[↩](#fnref9:1 "Jump back to footnote 1 in the text")[↩](#fnref10:1 "Jump back to footnote 1 in the text")[↩](#fnref11:1 "Jump back to footnote 1 in the text")[↩](#fnref12:1 "Jump back to footnote 1 in the text")[↩](#fnref13:1 "Jump back to footnote 1 in the text")[↩](#fnref14:1 "Jump back to footnote 1 in the text")[↩](#fnref15:1 "Jump back to footnote 1 in the text")[↩](#fnref16:1 "Jump back to footnote 1 in the text")[↩](#fnref17:1 "Jump back to footnote 1 in the text")[↩](#fnref18:1 "Jump back to footnote 1 in the text")
# Glossary
## Agent
- **Agent**: an AI entity that can interact with tools, handle complex workflows, and communicate with users.
- **LLM (Large Language Model)**: the underlying AI model that powers agent capabilities.
- **Message**: a unit of communication in the agent system that represents data passed from a user, assistant, or system.
- **Prompt**: the conversation history provided to an LLM that consists of messages from a user, assistant, and system.
- **System prompt**: instructions provided to an agent to guide its behavior, define its role, and supply key information necessary for its tasks.
- **Context**: the environment in which LLM interactions occur, with access to the conversation history and tools.
- **LLM session**: a structured way to interact with LLMs that includes the conversation history, available tools, and methods to make requests.
## Agent workflow
- **Strategy**: a defined workflow for an agent that consists of sequential subgraphs. The strategy defines how the agent processes input, interacts with tools, and generates output. A strategy graph consists of nodes connected by edges that represent transitions between nodes.
### Strategy graphs
- **Graph**: a structure of nodes connected by edges that defines an agent strategy workflow.
- **Node**: a fundamental building block of an agent strategy workflow that represents a specific operation or transformation.
- **Edge**: a connection between nodes in an agent graph that defines the flow of operations, often with conditions that specify when to follow each edge.
- **Conditions**: rules that determine when to follow a particular edge.
- **Subgraph**: a self-contained unit of processing within an agent strategy, with its own set of tools, context, and responsibilities.
## Tools
- **Tool**: a function that an agent can use to perform specific tasks or access external systems. The agent is aware of the available tools and their arguments but lacks knowledge of their implementation details.
- **Tool call**: a request from an LLM to run a specific tool using the provided arguments. It functions similarly to a function call.
- **Tool descriptor**: tool metadata that includes its name, description, and parameters.
- **Tool registry**: a list of tools available to an agent. The registry informs the agent about the available tools.
- **Tool result**: an output produced by running a tool. For example, if the tool is a method, the result would be its return value.
## History compression
- **History compression**: the process of reducing the size of the conversation history to manage token usage by applying various compression strategies. To learn more, see [History compression](../history-compression/).
## Features
- **Feature**: a component that extends and enhances the functionality of AI agents.
### EventHandler feature
- **EventHandler**: a feature that enables monitoring and responding to various agent events, providing hooks for tracking agent lifecycle, handling errors, and processing tool invocations throughout the workflow.
# Quickstart
# Quickstart
This guide will help you start using Koog in your project.
## Prerequisites
Ensure your environment and project meet the following requirements:
- JDK 17+
- Kotlin 2.2.0+
- Gradle 8.0+ or Maven 3.8+
## Install Koog
Add the [Koog package](https://central.sonatype.com/artifact/ai.koog/koog-agents/) as a dependency:
build.gradle.kts
```
dependencies {
// Stable
implementation("ai.koog:koog-agents:1.2.0")
// Beta
implementation("ai.koog:koog-agents-additions:1.2.0-beta")
}
```
build.gradle
```
dependencies {
// Stable
implementation 'ai.koog:koog-agents:1.2.0'
// Beta
implementation 'ai.koog:koog-agents-additions:1.2.0-beta'
}
```
pom.xml
```
ai.koogkoog-agents-jvm1.2.0ai.koogkoog-agents-additions-jvm1.2.0-beta
```
Module Versioning
Koog follows Semantic Versioning (`X.Y.Z`). Stable modules (e.g., `1.0.0`) have guaranteed APIs, while beta modules (e.g., `1.0.0-beta`) are experimental and may change between releases.
See [Module versioning](../module-versioning/) for details.
Nightly builds
Nightly builds from the develop branch are published to the [JetBrains Grazie Maven](https://packages.jetbrains.team/maven/p/grazi/grazie-platform-public) repository.
To use a nightly build, add the following repository to your build configuration: `https://packages.jetbrains.team/maven/p/grazi/grazie-platform-public`.
Then update your Koog dependency to the desired nightly version. Nightly versions follow the pattern `[next-major-version]-develop-[date]-[time]`.
You can browse the available nightly builds [here](https://packages.jetbrains.team/maven/p/grazi/grazie-platform-public/ai/koog/koog-agents/).
## Set up an API key
Koog requires either an API key from a [supported LLM provider](../llm-providers/) or a locally running LLM.
Warning
Avoid hardcoding API keys in the source code. Use environment variables to store API keys.
Get your [OpenAI API key](https://platform.openai.com/api-keys) and assign it to the `OPENAI_API_KEY` environment variable.
```
export OPENAI_API_KEY=your-api-key
```
```
setx OPENAI_API_KEY "your-api-key"
```
Get your [Anthropic API key](https://console.anthropic.com/settings/keys) and assign it to the `ANTHROPIC_API_KEY` environment variable.
```
export ANTHROPIC_API_KEY=your-api-key
```
```
setx ANTHROPIC_API_KEY "your-api-key"
```
Get your [Gemini API key](https://aistudio.google.com/app/api-keys) and assign it to the `GOOGLE_API_KEY` environment variable.
```
export GOOGLE_API_KEY=your-api-key
```
```
setx GOOGLE_API_KEY "your-api-key"
```
Get your [DeepSeek API key](https://platform.deepseek.com/api_keys) and assign it to the `DEEPSEEK_API_KEY` environment variable.
```
export DEEPSEEK_API_KEY=your-api-key
```
```
setx DEEPSEEK_API_KEY "your-api-key"
```
Get your [OpenRouter API key](https://openrouter.ai/keys) and assign it to the `OPENROUTER_API_KEY` environment variable.
```
export OPENROUTER_API_KEY=your-api-key
```
```
setx OPENROUTER_API_KEY "your-api-key"
```
[Generate an Amazon Bedrock API key](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html) and assign it to the `BEDROCK_API_KEY` environment variable.
```
export BEDROCK_API_KEY=your-api-key
```
```
setx BEDROCK_API_KEY "your-api-key"
```
Get your [Mistral API key](https://console.mistral.ai/api-keys) and assign it to the `MISTRAL_API_KEY` environment variable.
```
export MISTRAL_API_KEY=your-api-key
```
```
setx MISTRAL_API_KEY "your-api-key"
```
Run a local LLM in Ollama as described in the [Ollama documentation](https://docs.ollama.com/quickstart).
## Create your first Koog agent
The following example creates and runs a simple Koog agent using the [`GPT-4o`](https://platform.openai.com/docs/models/gpt-4o) model via the OpenAI API.
```
fun main() = runBlocking {
// Get the OpenAI API key from the OPENAI_API_KEY environment variable
val apiKey = System.getenv("OPENAI_API_KEY")
?: error("The API key is not set.")
// Create an agent
val agent = AIAgent(
promptExecutor = MultiLLMPromptExecutor(OpenAILLMClient(apiKey)),
llmModel = OpenAIModels.Chat.GPT4o
)
// Run the agent
val result = agent.run("Hello! How can you help me?")
println(result)
}
```
```
// Get the OpenAI API key from the OPENAI_API_KEY environment variable
String apiKey = System.getenv("OPENAI_API_KEY");
if (apiKey == null) {
throw new RuntimeException("The API key is not set.");
}
// Create an agent
AIAgent agent = AIAgent.builder()
.promptExecutor(new MultiLLMPromptExecutor(openAIClient(apiKey)))
.llmModel(OpenAIModels.Chat.GPT4o)
.build();
// Run the agent
String result = agent.run("Hello! How can you help me?");
System.out.println(result);
```
The example can produce the following output:
```
Hello! I'm here to help you with whatever you need. Here are just a few things I can do:
- Answer questions.
- Explain concepts or topics you're curious about.
- Provide step-by-step instructions for tasks.
- Offer advice, notes, or ideas.
- Help with research or summarize complex material.
- Write or edit text, emails, or other documents.
- Brainstorm creative projects or solutions.
- Solve problems or calculations.
Let me know what you need help with—I’m here for you!
```
The following example creates and runs a simple Koog agent using the [`Claude Opus 4.1`](https://www.anthropic.com/news/claude-opus-4-1) model via the Anthropic API.
```
fun main() = runBlocking {
// Get the Anthropic API key from the ANTHROPIC_API_KEY environment variable
val apiKey = System.getenv("ANTHROPIC_API_KEY")
?: error("The API key is not set.")
// Create an agent
val agent = AIAgent(
promptExecutor = MultiLLMPromptExecutor(AnthropicLLMClient(apiKey)),
llmModel = AnthropicModels.Opus_4_1
)
// Run the agent
val result = agent.run("Hello! How can you help me?")
println(result)
}
```
```
// Get the Anthropic API key from the ANTHROPIC_API_KEY environment variable
String apiKey = System.getenv("ANTHROPIC_API_KEY");
if (apiKey == null) {
throw new RuntimeException("The API key is not set.");
}
// Create an agent
AIAgent agent = AIAgent.builder()
.promptExecutor(new MultiLLMPromptExecutor(anthropicClient(apiKey)))
.llmModel(AnthropicModels.Opus_4_1)
.build();
// Run the agent
String result = agent.run("Hello! How can you help me?");
System.out.println(result);
```
The example can produce the following output:
```
Hello! I can help you with:
- **Answering questions** and explaining topics
- **Writing** - drafting, editing, proofreading
- **Learning** - homework, math, study help
- **Problem-solving** and brainstorming
- **Research** and information finding
- **General tasks** - instructions, planning, recommendations
What do you need help with today?
```
The following example creates and runs a simple Koog agent using the [`Gemini 2.5 Pro`](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/2-5-pro) model via the Gemini API.
```
fun main() = runBlocking {
// Get the Gemini API key from the GOOGLE_API_KEY environment variable
val apiKey = System.getenv("GOOGLE_API_KEY")
?: error("The API key is not set.")
// Create an agent
val agent = AIAgent(
promptExecutor = MultiLLMPromptExecutor(GoogleLLMClient(apiKey)),
llmModel = GoogleModels.Gemini2_5Pro
)
// Run the agent
val result = agent.run("Hello! How can you help me?")
println(result)
}
```
```
// Get the Gemini API key from the GOOGLE_API_KEY environment variable
String apiKey = System.getenv("GOOGLE_API_KEY");
if (apiKey == null) {
throw new RuntimeException("The API key is not set.");
}
// Create an agent
AIAgent agent = AIAgent.builder()
.promptExecutor(new MultiLLMPromptExecutor(googleClient(apiKey)))
.llmModel(GoogleModels.Gemini2_5Pro)
.build();
// Run the agent
String result = agent.run("Hello! How can you help me?");
System.out.println(result);
```
The example can produce the following output:
```
I'm an AI that can help you with tasks involving language and information. You can ask me to:
* **Answer questions**
* **Write or edit text** (emails, stories, code, etc.)
* **Brainstorm ideas**
* **Summarize long documents**
* **Plan things** (like trips or projects)
* **Be a creative partner**
Just tell me what you need
```
The following example creates and runs a simple Koog agent using the `deepseek-v4-flash` model via the DeepSeek API.
```
fun main() = runBlocking {
// Get the DeepSeek API key from the DEEPSEEK_API_KEY environment variable
val apiKey = System.getenv("DEEPSEEK_API_KEY")
?: error("The API key is not set.")
// Create an agent
val agent = AIAgent(
promptExecutor = MultiLLMPromptExecutor(DeepSeekLLMClient(apiKey)),
llmModel = DeepSeekModels.DeepSeekV4Flash
)
// Run the agent
val result = agent.run("Hello! How can you help me?")
println(result)
}
```
```
// Get the DeepSeek API key from the DEEPSEEK_API_KEY environment variable
String apiKey = System.getenv("DEEPSEEK_API_KEY");
if (apiKey == null) {
throw new RuntimeException("The API key is not set.");
}
// Create an agent
AIAgent agent = AIAgent.builder()
.promptExecutor(new MultiLLMPromptExecutor(deepSeekClient(apiKey)))
.llmModel(DeepSeekModels.DeepSeekV4Flash)
.build();
// Run the agent
String result = agent.run("Hello! How can you help me?");
System.out.println(result);
```
The example can produce the following output:
```
Hello! I'm here to assist you with a wide range of tasks, including answering questions, providing information, helping with problem-solving, offering creative ideas, and even just chatting. Whether you need help with research, writing, learning something new, or simply want to discuss a topic, feel free to ask—I’m happy to help! 😊
```
The following example creates and runs a simple Koog agent using the [`GPT-4o`](https://openrouter.ai/openai/gpt-4o) model via the OpenRouter API.
```
fun main() = runBlocking {
// Get the OpenRouter API key from the OPENROUTER_API_KEY environment variable
val apiKey = System.getenv("OPENROUTER_API_KEY")
?: error("The API key is not set.")
// Create an agent
val agent = AIAgent(
promptExecutor = MultiLLMPromptExecutor(OpenRouterLLMClient(apiKey)),
llmModel = OpenRouterModels.GPT4o
)
// Run the agent
val result = agent.run("Hello! How can you help me?")
println(result)
}
```
```
// Get the OpenRouter API key from the OPENROUTER_API_KEY environment variable
String apiKey = System.getenv("OPENROUTER_API_KEY");
if (apiKey == null) {
throw new RuntimeException("The API key is not set.");
}
// Create an agent
AIAgent agent = AIAgent.builder()
.promptExecutor(new MultiLLMPromptExecutor(openRouterClient(apiKey)))
.llmModel(OpenRouterModels.GPT4o)
.build();
// Run the agent
String result = agent.run("Hello! How can you help me?");
System.out.println(result);
```
The example can produce the following output:
```
I can answer questions, help with writing, solve problems, organize tasks, and more—just let me know what you need!
```
The following example creates and runs a simple Koog agent using the [`Claude Sonnet 4.5`](https://www.anthropic.com/news/claude-sonnet-4-5) model via the Bedrock API.
```
fun main() = runBlocking {
// Get the Bedrock API key from the BEDROCK_API_KEY environment variable
val apiKey = System.getenv("BEDROCK_API_KEY")
?: error("The API key is not set.")
// Create an agent
val agent = AIAgent(
promptExecutor = MultiLLMPromptExecutor(
BedrockLLMClient(
StaticBearerTokenProvider(apiKey),
BedrockClientSettings()
)
),
llmModel = BedrockModels.AnthropicClaude4_5Sonnet
)
// Run the agent
val result = agent.run("Hello! How can you help me?")
println(result)
}
```
```
// Get the Bedrock API key from the BEDROCK_API_KEY environment variable
String apiKey = System.getenv("BEDROCK_API_KEY");
if (apiKey == null) {
throw new RuntimeException("The API key is not set.");
}
// Create an agent
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleBedrockExecutorWithBearerToken(apiKey, new BedrockClientSettings()))
.llmModel(BedrockModels.INSTANCE.getAnthropicClaude4_5Sonnet())
.build();
// Run the agent
String result = agent.run("Hello! How can you help me?");
System.out.println(result);
```
The example can produce the following output:
```
Hello! I'm a helpful assistant and I can assist you in many ways, including:
- **Answering questions** on a wide range of topics (science, history, technology, etc.)
- **Writing help** - drafting emails, essays, creative content, or editing text
- **Problem-solving** - working through math problems, logic puzzles, or troubleshooting issues
- **Learning support** - explaining concepts, providing study notes, or tutoring
- **Planning & organizing** - helping with projects, schedules, or breaking down tasks
- **Coding assistance** - explaining programming concepts or helping debug code
- **Creative brainstorming** - generating ideas for projects, stories, or solutions
- **General conversation** - discussing topics or just chatting
What would you like help with today?
```
The following example creates and runs a simple Koog agent using the [`Mistral Medium 3.1`](https://docs.mistral.ai/models/mistral-medium-3-1-25-08) model via the Mistral AI API.
```
fun main() = runBlocking {
// Get the Mistral AI API key from the MISTRAL_API_KEY environment variable
val apiKey = System.getenv("MISTRAL_API_KEY")
?: error("The API key is not set.")
// Create an agent
val agent = AIAgent(
promptExecutor = MultiLLMPromptExecutor(MistralAILLMClient(apiKey)),
llmModel = MistralAIModels.Chat.MistralMedium31
)
// Run the agent
val result = agent.run("Hello! How can you help me?")
println(result)
}
```
```
// Get the Mistral AI API key from the MISTRAL_API_KEY environment variable
String apiKey = System.getenv("MISTRAL_API_KEY");
if (apiKey == null) {
throw new RuntimeException("The API key is not set.");
}
// Create an agent
AIAgent agent = AIAgent.builder()
.promptExecutor(new MultiLLMPromptExecutor(mistralAIClient(apiKey)))
.llmModel(MistralAIModels.Chat.MistralMedium31)
.build();
// Run the agent
String result = agent.run("Hello! How can you help me?");
System.out.println(result);
```
The example can produce the following output:
```
I can assist you with a wide range of topics and tasks. Here are some examples:
1. **Answering questions**: I can provide information on various subjects, including history, science, technology, literature, and more.
2. **Providing definitions**: If you're unsure about the meaning of a word or phrase, I can help define it for you.
3. **Generating text**: Whether it's writing an email, creating content for social media, or composing a story, I can help with text generation.
4. **Translation**: I can translate text from one language to another.
5. **Conversation**: We can have a chat about any topic that interests you, and I'll respond accordingly.
6. **Language practice**: If you're learning a new language, I can help with pronunciation, grammar, and vocabulary practice.
7. **Brainstorming**: If you're stuck on a problem or need ideas for a project, I can help brainstorm solutions.
8. **Summarization**: If you have a long piece of text and want a summary, I can condense it for you.
What's on your mind? Is there something specific you'd like help with?
```
The following example creates and runs a simple Koog agent using the [`llama3.2`](https://ollama.com/library/llama3.2) model running locally via Ollama.
```
fun main() = runBlocking {
// Create an agent
val agent = AIAgent(
promptExecutor = MultiLLMPromptExecutor(OllamaClient()),
llmModel = OllamaModels.Meta.LLAMA_3_2
)
// Run the agent
val result = agent.run("Hello! How can you help me?")
println(result)
}
```
```
// Create an agent
AIAgent agent = AIAgent.builder()
.promptExecutor(new MultiLLMPromptExecutor(ollamaClient("http://localhost:11434")))
.llmModel(OllamaModels.Meta.LLAMA_3_2)
.build();
// Run the agent
String result = agent.run("Hello! How can you help me?");
System.out.println(result);
```
The example can produce the following output:
```
I can assist with various tasks such as answering questions, providing information, and even helping with language-related tasks like proofreading or writing suggestions. What's on your mind today?
```
## Next steps
- Learn more about [agent types](../agents/)
# Agents
# Basic agents
A basic agent uses a predefined strategy with a simple execution flow that works for most common use cases. It accepts a string input (a question, request, or task description) and sends this input to the configured LLM. The LLM may decide to call provided tools. The agent will execute the tools and send the results back to the LLM. This repeats until the LLM does not request any more tool calls and returns a string response. The agent then outputs this response.
In [Graph-based agents](../graph-based-agents/), you can see how to re-create the predefined strategy graph used by basic agents.
Prerequisites
Ensure your environment and project meet the following requirements:
- JDK 17+
- Kotlin 2.2.0+
- Gradle 8.0+ or Maven 3.8+
Add the [Koog package](https://central.sonatype.com/artifact/ai.koog/koog-agents/) as a dependency:
build.gradle.kts
```
dependencies {
// Stable
implementation("ai.koog:koog-agents:1.2.0")
// Beta
implementation("ai.koog:koog-agents-additions:1.2.0-beta")
}
```
build.gradle
```
dependencies {
// Stable
implementation 'ai.koog:koog-agents:1.2.0'
// Beta
implementation 'ai.koog:koog-agents-additions:1.2.0-beta'
}
```
pom.xml
```
ai.koogkoog-agents-jvm1.2.0ai.koogkoog-agents-additions-jvm1.2.0-beta
```
Get an API key from an LLM provider or run a local LLM via Ollama. For more information, see [Quickstart](../../quickstart/).
Examples on this page assume that you have set the `OPENAI_API_KEY` environment variable.
## Create a minimal agent
To create the most basic agent, instantiate [`AIAgent`](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.agent/-a-i-agent/index.html) and provide a [prompt executor](../../prompts/prompt-executors/) with a [language model](../../model-capabilities/#creating-a-model-llmodel-configuration):
```
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
llmModel = OpenAIModels.Chat.GPT4o
)
```
This agent will expect a string as input and return a string as output. To run the agent, use the `run()` function with some user input:
```
fun main() = runBlocking {
val result = agent.run("Hello! How can you help me?")
println(result)
}
```
```
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")))
.llmModel(OpenAIModels.Chat.GPT4o)
.build();
```
This agent expects a string as input and returns a string as output. To run the agent, use the `run()` method with some user input:
```
String result = agent.run("Hello! How can you help me?");
System.out.println(result);
```
The agent will return a generic answer, such as:
```
I can assist with a wide range of topics and tasks. Here are some examples:
1. **Answering questions**: I can provide information on various subjects, from science and history to entertainment and culture.
2. **Generating text**: I can help with writing tasks, such as suggesting alternative phrases, providing definitions, or even creating entire articles or stories.
3. **Translation**: I can translate text from one language to another, including popular languages such as Spanish, French, German, Chinese, and many more.
4. **Conversation**: I can engage in natural-sounding conversations, using context and understanding to respond to questions and statements.
5. **Brainstorming**: I can help generate ideas for creative projects, such as writing stories, composing music, or coming up with business ideas.
6. **Learning**: I can help with language learning, explaining grammar rules, vocabulary, and pronunciation.
7. **Calculations**: I can perform mathematical calculations, including basic arithmetic, algebra, and more advanced math concepts.
What's on your mind? Do you have a specific question, topic, or task you'd like to tackle?
```
## Add a system prompt
Provide a [system message](../../prompts/prompt-creation/#system-message) to define the agent's role as well as the purpose, context, and instructions related to the task.
```
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("YOUR_API_KEY")),
systemPrompt = "You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.",
llmModel = OpenAIModels.Chat.GPT4o
)
```
```
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")))
.systemPrompt("You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.")
.llmModel(OpenAIModels.Chat.GPT4o)
.build();
```
The instructions in the system prompt will guide the agent's response:
```
I'm here to help you navigate the wild world of internet memes!
What's on your mind? Are you trying to understand a specific meme, need help finding a popular joke, or perhaps want some recommendations for trending memes? Let me know, and I'll do my best to provide you with some LOLs!
```
## Configure LLM output
You can provide some [LLM parameters](../../llm-parameters/#llm-parameter-reference) directly to the agent constructor (Kotlin) or via the builder methods (Java) to customize the behavior of the LLM. For example, use the `temperature` parameter to adjust the randomness of the generated responses:
```
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("YOUR_API_KEY")),
systemPrompt = "You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.",
llmModel = OpenAIModels.Chat.GPT4o,
temperature = 0.7
)
```
```
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")))
.systemPrompt("You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.")
.llmModel(OpenAIModels.Chat.GPT4o)
.temperature(0.7)
.build();
```
Here are some response examples with different temperature values:
```
I'm here to help you navigate the wild world of internet memes! Whether you're looking for explanations, examples, or just want to share a meme with someone, I'm your go-to expert. What's on your mind? Got a specific meme in mind that's got you curious? Or maybe you need some meme-related advice? Fire away!
```
```
I'm here to help you navigate the wild world of internet memes!
What's on your mind? Need help understanding a specific meme, finding a popular joke or trend, or maybe even creating your own meme? Let's get this meme party started!
```
```
I'd be happy to help you navigate the wild world of internet memes!
Whether you're looking for explanations of classic memes, suggestions for new ones to try out, or just want to discuss your favorite meme culture trends, I'm here to assist. What's on your mind?
Do you have a specific question about memes (e.g., "What does this meme mean?"), or are you looking for some meme-related recommendations (e.g., "Can you recommend a funny meme to share with friends?"). Let me know how I can help!
```
## Add tools
Agents can use [tools](../../tools/) to perform specific tasks.
First, create a tool by annotating a function (Kotlin) or method (Java) with the [`@Tool`](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools.annotations/-tool/index.html) annotation:
```
@Tool
@LLMDescription("Ask the user a question by sending it to stdout and return the answer from stdin")
fun askUser(
@LLMDescription("Question from the agent")
question: String
): String {
println(question)
return readln()
}
```
Then, use the [`ToolRegistry`](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool-registry/index.html) to make this tool available to the agent:
```
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("YOUR_API_KEY")),
systemPrompt = "You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.",
llmModel = OpenAIModels.Chat.GPT4o,
temperature = 0.7,
toolRegistry = ToolRegistry {
tool(::askUser)
}
)
```
In the example, `askUser` is a tool that helps the agent maintain a conversation with the user via printing and reading from the console. If the agent decides to ask the user a question, it can call this tool that writes to `stdout` via `println()` and reads from `stdin` via `readln()`.
```
// Create a ToolSet class
class UserConversationTools implements ToolSet {
@Tool
@LLMDescription("Ask the user a question by sending it to stdout and return the answer from stdin")
public String askUser(
@LLMDescription("Question from the agent")
String question
) {
System.out.println(question);
Scanner scanner = new Scanner(System.in);
return scanner.nextLine();
}
}
```
Then, use the [`ToolRegistry`](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool-registry/index.html) to make this tool available to the agent:
```
UserConversationTools askUser = new UserConversationTools();
ToolRegistry toolRegistry = ToolRegistry.builder()
.tools(askUser)
.build();
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")))
.systemPrompt("You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.")
.llmModel(OpenAIModels.Chat.GPT4o)
.temperature(0.7)
.toolRegistry(toolRegistry)
.build();
```
In the example, `askUser` is a tool that helps the agent maintain a conversation with the user via printing and reading from the console.
Here is an example interaction with the agent:
```
Agent: Which meme would you like me to explain? Please choose from: Grumpy Cat, Success Kid, or Doge.
User: Explain Doge
Agent:
**Doge**
Doge is a popular internet meme that originated on the imageboard website 4chan in 2013. The meme features a picture of a Shiba Inu dog with a caption written in a playful, exaggerated style.
The typical format of a Doge meme includes:
* A photo of a Shiba Inu dog
* A caption written in all capital letters, using a intentionally simplistic and childlike tone
* Exaggerated or made-up words or phrases, often used to convey a humorous or nonsensical idea
Examples of Doge memes might include:
* "Such wow. Such happy."
* "I had fun today!"
* "Wow, I am good at napping."
The meme is known for its lighthearted and playful tone, and is often used to express excitement, happiness, or silliness. The meme has since become a cultural phenomenon, with countless variations and parodies emerging online.
```
## Adjust agent iterations
To avoid infinite loops, Koog allows any agent to take a limited number of steps (50 by default). Use the `maxIterations` parameter to either increase this limit if you expect the agent to require more steps (such as tool calls and LLM requests) or decrease it for agents that require only a few steps. For example, a simple agent described here is not likely to require more than 10 steps:
```
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("YOUR_API_KEY")),
systemPrompt = "You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.",
llmModel = OpenAIModels.Chat.GPT4o,
temperature = 0.7,
toolRegistry = ToolRegistry {
tool(::askUser)
},
maxIterations = 10
)
```
```
// Create a ToolSet class
class UserConversationTools implements ToolSet {
@Tool
@LLMDescription("Ask the user a question by sending it to stdout and return the answer from stdin")
public String askUser(
@LLMDescription("Question from the agent")
String question
) {
System.out.println(question);
Scanner scanner = new Scanner(System.in);
return scanner.nextLine();
}
}
// In main method:
UserConversationTools askUser = new UserConversationTools();
ToolRegistry toolRegistry = ToolRegistry.builder()
.tools(askUser)
.build();
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")))
.systemPrompt("You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.")
.llmModel(OpenAIModels.Chat.GPT4o)
.temperature(0.7)
.toolRegistry(toolRegistry)
.maxIterations(10)
.build();
```
Tip
Instead of passing the model, temperature, max iterations, and other parameters directly to the Kotlin constructor or Java builder, you can also define and pass them as a separate configuration object. For more information, see [Agent configuration](../#agent-configuration).
## Handle events during agent runtime
To assist with testing and debugging, as well as making hooks for chained agent interactions, Koog provides the [EventHandler](https://api.koog.ai/agents/agents-features/agents-features-event-handler/ai.koog.agents.features.eventHandler.feature/-event-handler/index.html) feature.
Call the `handleEvents()` function inside the agent constructor lambda to install the feature and register event handlers:
```
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("YOUR_API_KEY")),
systemPrompt = "You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.",
llmModel = OpenAIModels.Chat.GPT4o,
temperature = 0.7,
toolRegistry = ToolRegistry {
tool(::askUser)
},
maxIterations = 10
){
handleEvents {
// Handle tool calls
onToolCallStarting { eventContext ->
println("Tool called: ${eventContext.toolName} with args ${eventContext.toolArgs}")
}
}
}
```
Use the `.install()` method on the agent builder to register event handlers with `EventHandler.Feature`:
```
// Create a ToolSet class
class UserConversationTools implements ToolSet {
@Tool
@LLMDescription("Ask the user a question by sending it to stdout and return the answer from stdin")
public String askUser(
@LLMDescription("Question from the agent")
String question
) {
System.out.println(question);
Scanner scanner = new Scanner(System.in);
return scanner.nextLine();
}
}
// In main method:
UserConversationTools askUser = new UserConversationTools();
ToolRegistry toolRegistry = ToolRegistry.builder()
.tools(askUser)
.build();
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")))
.systemPrompt("You are an expert in internet memes. Be helpful, friendly, and answer user questions concisely, showing your knowledge of memes.")
.llmModel(OpenAIModels.Chat.GPT4o)
.temperature(0.7)
.toolRegistry(toolRegistry)
.maxIterations(10)
.install(EventHandler.Feature, config -> {
config.onToolCallStarting(eventContext -> {
System.out.println("Tool called: " + eventContext.getToolName() +
" with args " + eventContext.getToolArgs());
});
})
.build();
```
The agent will now output something similar to the following when it calls the `askUser` tool:
```
Tool called: askUser with args {"question":"Which meme would you like me to explain?"}
```
For more information about Koog agent features, see [Features](../../features/).
## Next steps
- Learn more about building [graph-based agents](../graph-based-agents/) and [functional agents](../functional-agents/)
# Graph-based agents
With graph-based agents, you model the behavior as an explicit state machine: nodes of a graph strategy represent actions (LLM calls, tool execution) and edges represent data flow between nodes.
The main advantages of graph-based agents are:
- Easy to visualize
- State persistence
- Composable architecture
Prerequisites
Ensure your environment and project meet the following requirements:
- JDK 17+
- Kotlin 2.2.0+
- Gradle 8.0+ or Maven 3.8+
Add the [Koog package](https://central.sonatype.com/artifact/ai.koog/koog-agents/) as a dependency:
build.gradle.kts
```
dependencies {
// Stable
implementation("ai.koog:koog-agents:1.2.0")
// Beta
implementation("ai.koog:koog-agents-additions:1.2.0-beta")
}
```
build.gradle
```
dependencies {
// Stable
implementation 'ai.koog:koog-agents:1.2.0'
// Beta
implementation 'ai.koog:koog-agents-additions:1.2.0-beta'
}
```
pom.xml
```
ai.koogkoog-agents-jvm1.2.0ai.koogkoog-agents-additions-jvm1.2.0-beta
```
Get an API key from an LLM provider or run a local LLM via Ollama. For more information, see [Quickstart](../../quickstart/).
Examples on this page assume that you are running Llama 3.2 locally via Ollama.
This page describes how to re-create the strategy graph used by [basic agents](../basic-agents/). It sends a request to an LLM and then either outputs the response (if the LLM responded with an assistant message) or executes a tool (if the LLM requested a tool call). In case of a tool call, the agent sends the tool result to the LLM and then either outputs the response or executes a tool.
Here is an illustration of the strategy graph:
```
---
config:
flowchart:
defaultRenderer: "elk"
---
graph TB
subgraph nodeStart
Input
end
subgraph nodeFinish
Output
end
subgraph nodeSendInput
llmRequest(Request LLM)
end
subgraph nodeExecuteTool
executeTool(Execute tool call)
end
subgraph nodeSendToolResult
sendToolResult(Request LLM)
end
Input --String--> llmRequest
llmRequest --Message.Assistant--> onToolCalls{{onToolCalls}}
llmRequest --Message.Assistant--> onTextMessage{{onTextMessage}}
onTextMessage --String--> Output
onToolCalls --ToolCalls--> executeTool --ReceivedToolResults--> sendToolResult
sendToolResult --Message.Assistant--> onToolCalls
sendToolResult --Message.Assistant--> onTextMessage
```
## Build a strategy graph
In Koog, you implement a strategy using [`AIAgentGraphStrategyBuilder`](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.builder/-a-i-agent-graph-strategy-builder/index.html). Just like every node has an input and output type, the strategy as a whole also defines some input and output type. This example assumes that the input and output types are strings, which means the agent implementing this strategy will expect a string and return a string.
To create a strategy, use the [`strategy()`](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.builder/strategy.html) function with two generics as the input and output types, provide a unique identifier for the strategy, and define the nodes and edges.
```
val calculatorAgentStrategy = strategy("Simple calculator") {
val nodeSendInput by nodeLLMRequest()
val nodeExecuteTool by nodeExecuteTools()
val nodeSendToolResult by nodeLLMSendToolResults()
edge(nodeStart forwardTo nodeSendInput)
edge(nodeSendInput forwardTo nodeFinish onTextMessage { true })
edge(nodeSendInput forwardTo nodeExecuteTool onToolCalls { true })
edge(nodeExecuteTool forwardTo nodeSendToolResult)
edge(nodeSendToolResult forwardTo nodeFinish onTextMessage { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCalls { true })
}
```
```
var calculatorAgentStrategy = AIAgentGraphStrategy.builder("Simple calculator")
.withInput(String.class)
.withOutput(String.class);
var nodeSendInput = AIAgentNode.llmRequest("nodeSendInput");
var nodeExecuteTool = AIAgentNode.executeTools("nodeExecuteTool");
var nodeSendToolResult = AIAgentNode.llmSendToolResults("nodeSendToolResult");
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(calculatorAgentStrategy.nodeStart)
.to(nodeSendInput)
.build());
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(nodeSendInput)
.to(calculatorAgentStrategy.nodeFinish)
.onTextMessage()
.build());
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(nodeSendInput)
.to(nodeExecuteTool)
.onToolCalls()
.build());
calculatorAgentStrategy.edge(nodeExecuteTool, nodeSendToolResult);
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(nodeSendToolResult)
.to(calculatorAgentStrategy.nodeFinish)
.onTextMessage()
.build());
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(nodeSendToolResult)
.to(nodeExecuteTool)
.onToolCalls()
.build());
```
This example uses only [predefined nodes](../../nodes-and-components/), but you can also create [custom nodes](../../custom-nodes/).
Every strategy graph must have a path from `nodeStart` to `nodeFinish` connected by [edges](../../custom-strategy-graphs/#edges). Edges can have conditions to determine when to follow a particular edge. Edges can also transform the output of the previous node before passing it to the next one. This is necessary to connect nodes that have non-matching output and input types.
In the previous example, `onToolCalls { true }` means that the edge will follow only if the previous node returned an assistant message containing at least one tool call (`MessagePart.Tool.Call`).
When using `onTextMessage { true }`, the edge will follow only if the previous node returned an assistant message containing text parts (`MessagePart.Text`). This function also extracts and joins the text content of those parts, effectively transforming `Message.Assistant` to `String`, because `nodeFinish` expects a string.
Tip
Instead of `onTextMessage { true }`, you can do the following:
```
onMessageParts(MessagePart.Text::class) transformed { it.joinToString("\n") { part -> part.text } }
```
Or:
```
onCondition { it is Message.Assistant } transformed { (it as Message.Assistant).parts.filterIsInstance().joinToString("\n") { part -> part.text } }
```
## Create and run the agent
Let's create an agent instance with this strategy and run it:
```
val calculatorAgentStrategy = strategy("Simple calculator") {
val nodeSendInput by nodeLLMRequest()
val nodeExecuteTool by nodeExecuteTools()
val nodeSendToolResult by nodeLLMSendToolResults()
edge(nodeStart forwardTo nodeSendInput)
edge(nodeSendInput forwardTo nodeFinish onTextMessage { true })
edge(nodeSendInput forwardTo nodeExecuteTool onToolCalls { true })
edge(nodeExecuteTool forwardTo nodeSendToolResult)
edge(nodeSendToolResult forwardTo nodeFinish onTextMessage { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCalls { true })
}
val mathAgent = AIAgent(
promptExecutor = simpleOllamaAIExecutor(),
llmModel = OllamaModels.Meta.LLAMA_3_2,
strategy = calculatorAgentStrategy
)
fun main() = runBlocking {
val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.")
println(result)
}
```
```
var calculatorAgentStrategy = AIAgentGraphStrategy.builder("Simple calculator")
.withInput(String.class)
.withOutput(String.class);
var nodeSendInput = AIAgentNode.llmRequest("nodeSendInput");
var nodeExecuteTool = AIAgentNode.executeTools("nodeExecuteTool");
var nodeSendToolResult = AIAgentNode.llmSendToolResults("nodeSendToolResult");
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(calculatorAgentStrategy.nodeStart)
.to(nodeSendInput)
.build());
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(nodeSendInput)
.to(calculatorAgentStrategy.nodeFinish)
.onTextMessage()
.build());
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(nodeSendInput)
.to(nodeExecuteTool)
.onToolCalls()
.build());
calculatorAgentStrategy.edge(nodeExecuteTool, nodeSendToolResult);
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(nodeSendToolResult)
.to(calculatorAgentStrategy.nodeFinish)
.onTextMessage()
.build());
calculatorAgentStrategy.edge(AIAgentEdge.builder()
.from(nodeSendToolResult)
.to(nodeExecuteTool)
.onToolCalls()
.build());
var promptExecutor = PromptExecutor.builder()
.ollama("http://localhost:11434")
.build();
AIAgent mathAgent = AIAgent.builder()
.promptExecutor(promptExecutor)
.llmModel(OllamaModels.Meta.LLAMA_3_2)
.graphStrategy(calculatorAgentStrategy.build())
.build();
String result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.", null);
System.out.println(result);
```
When you run this agent, it will respond with something like this:
```
To calculate this, I'll follow the order of operations:
1. Multiply 3 by 4: 3 * 4 = 12
2. Multiply the result by 5: 12 * 5 = 60
3. Add 10: 60 + 10 = 70
4. Add 123: 70 + 123 = 193
The final answer is 193.
```
However, since this agent doesn't have any tools, the LLM never returns a tool call and simply generates the whole answer. This is what effectively happens:
```
---
config:
flowchart:
defaultRenderer: "elk"
---
graph LR
subgraph nodeStart
Input
end
subgraph nodeFinish
Output
end
subgraph nodeSendInput
llmRequest(Request LLM)
end
Input --String--> llmRequest --Message.Assistant--> onTextMessage{{onTextMessage}} --String--> Output
```
Even though it is correct in this case, the answer will depend on the arithmetic abilities of the underlying LLM. To make sure the calculations are correct, we should provide the agent with math tools. Then the LLM will be able to decide to call tools that perform the calculations deterministically.
## Add tools
Define [tools](../../tools/) for performing math operations and add them to a [ToolRegistry](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool-registry/index.html):
```
@LLMDescription("Tools for performing math operations")
class MathTools : ToolSet {
@Tool
@LLMDescription("Adds two numbers and returns the result")
fun add(a: Int, b: Int): Int {
// This is not necessary, but it helps to see the tool call in the console output
println("Adding $a and $b...")
return a + b
}
@Tool
@LLMDescription("Multiplies two numbers and returns the result")
fun multiply(a: Int, b: Int): Int {
// This is not necessary, but it helps to see the tool call in the console output
println("Multiplying $a and $b...")
return a * b
}
}
val toolRegistry = ToolRegistry {
tools(MathTools())
}
```
```
@LLMDescription("Tools for performing math operations")
public static class MathTools implements ToolSet {
@Tool
@LLMDescription("Adds two numbers and returns the result")
public int add(int a, int b) {
// This is not necessary, but it helps to see the tool call in the console output
System.out.println("Adding " + a + " and " + b + "...");
return a + b;
}
@Tool
@LLMDescription("Multiplies two numbers and returns the result")
public int multiply(int a, int b) {
// This is not necessary, but it helps to see the tool call in the console output
System.out.println("Multiplying " + a + " and " + b + "...");
return a * b;
}
}
public static void main(String[] args) {
ToolRegistry toolRegistry = ToolRegistry.builder()
.tools(new MathTools())
.build();
}
```
Add the tool registry to the agent configuration:
```
val mathAgent = AIAgent(
promptExecutor = simpleOllamaAIExecutor(),
llmModel = OllamaModels.Meta.LLAMA_3_2,
strategy = calculatorAgentStrategy,
toolRegistry = toolRegistry
)
fun main() = runBlocking {
val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.")
println(result)
}
```
```
AIAgent mathAgent = AIAgent.builder()
.promptExecutor(promptExecutor)
.llmModel(OllamaModels.Meta.LLAMA_3_2)
.graphStrategy(calculatorAgentStrategy.build())
.toolRegistry(toolRegistry)
.build();
String result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.", null);
System.out.println(result);
```
When you run the agent now, it will respond with something like this:
```
Multiplying 3 and 4...
The output from the first operation was multiplied by 5:
5 * 12 = 60
Then, 10 was added to the result:
60 + 10 = 70
Finally, 123 was added to the result:
70 + 123 = 193
```
According to this output, the agent correctly performed the calculations, but it only called the `multiply` tool once instead of calling the corresponding tool for every operation. We can help the agent by describing its role and providing instructions for using appropriate tools in the system prompt.
## Provide a system prompt
A [system prompt](../../prompts/prompt-creation/#system-message) defines the agent's role and instructions for performing tasks. In our example, it is important to describe how the agent should process complex multistep calculations:
```
val mathAgent = AIAgent(
promptExecutor = simpleOllamaAIExecutor(),
llmModel = OllamaModels.Meta.LLAMA_3_2,
systemPrompt = """
You are a simple calculator assistant.
You can add and multiply two numbers using the 'add' and 'multiply' tools.
When the user provides input, extract the numbers and operations they requested.
Use the appropriate tool for the first operation, then the next one, and so on, until you calculate the result.
Always respond with a clear, friendly message showing the calculation and result.
""".trimIndent(),
toolRegistry = toolRegistry,
strategy = calculatorAgentStrategy
)
fun main() = runBlocking {
val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.")
println(result)
}
```
```
AIAgent mathAgent = AIAgent.builder()
.promptExecutor(promptExecutor)
.llmModel(OllamaModels.Meta.LLAMA_3_2)
.systemPrompt("You are a simple calculator assistant. You can add and multiply two numbers using the 'add' and 'multiply' tools. When the user provides input, extract the numbers and operations they requested. Use the appropriate tool for the first operation, then the next one, and so on, until you calculate the result. Always respond with a clear, friendly message showing the calculation and result.")
.graphStrategy(calculatorAgentStrategy.build())
.toolRegistry(toolRegistry)
.build();
String result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.", null);
System.out.println(result);
```
When you run the agent now, it will respond with something like this:
```
Multiplying 3 and 4...
Multiplying 12 and 5...
Adding 60 and 10...
Adding 70 and 123...
The final result is: 193
```
As you can see, the agent now correctly calls the appropriate tool for each operation, ensuring that it performs the calculations deterministically instead of risking a hallucinated result.
## Next steps
- Compare to [functional agents](../functional-agents/) and [planner agents](../planner-agents/)
- Enhance your agent by [installing features](../../features/)
- Improve the predictability and reliability with [structured output](../../structured-output/)
# Functional agents
With functional agents, you implement the logic as a function that handles user input, interacts with LLMs, calls tools if necessary, and produces the final output. Compared to [graph-based agents](../graph-based-agents/), this usually means faster prototyping with the following downsides:
- Not easy to visualize
- No state persistence
Prerequisites
Ensure your environment and project meet the following requirements:
- JDK 17+
- Kotlin 2.2.0+
- Gradle 8.0+ or Maven 3.8+
Add the [Koog package](https://central.sonatype.com/artifact/ai.koog/koog-agents/) as a dependency:
build.gradle.kts
```
dependencies {
// Stable
implementation("ai.koog:koog-agents:1.2.0")
// Beta
implementation("ai.koog:koog-agents-additions:1.2.0-beta")
}
```
build.gradle
```
dependencies {
// Stable
implementation 'ai.koog:koog-agents:1.2.0'
// Beta
implementation 'ai.koog:koog-agents-additions:1.2.0-beta'
}
```
pom.xml
```
ai.koogkoog-agents-jvm1.2.0ai.koogkoog-agents-additions-jvm1.2.0-beta
```
Get an API key from an LLM provider or run a local LLM via Ollama. For more information, see [Quickstart](../../quickstart/).
Examples on this page assume that you are running Llama 3.2 locally via Ollama.
This page describes how to implement a functional strategy to quickly prototype some custom logic for your agent.
## Create a minimal functional agent
To create a minimal functional agent, use the same [`AIAgent`](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.agent/-a-i-agent/index.html) interface as for a [basic agent](../basic-agents/) and pass an instance of [`AIAgentFunctionalStrategy`](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.agent/-a-i-agent-functional-strategy/index.html) to it. You can define a functional strategy that expects an input and returns an output, makes one LLM call, then returns the content of the assistant message from the response.
In Kotlin, the most convenient way is to use the `functionalStrategy {...}` DSL method. In Java, you can use the `functionalStrategy` method on the `AIAgent` builder.
```
val strategy = functionalStrategy { input ->
val response = requestLLM(input)
response.parts.filterIsInstance().joinToString("\n") { it.text }
}
val mathAgent = AIAgent(
promptExecutor = simpleOllamaAIExecutor(),
llmModel = OllamaModels.Meta.LLAMA_3_2,
strategy = strategy
)
fun main() = runBlocking {
val result = mathAgent.run("What is 12 × 9?")
println(result)
}
```
```
AIAgent mathAgent = AIAgent.builder()
.promptExecutor(SimpleLLMExecutorsKt.simpleOllamaAIExecutor("http://localhost:11434"))
.llmModel(OllamaModels.Meta.LLAMA_3_2)
.functionalStrategy("mathStrategy", (AIAgentFunctionalContext context, String input) -> {
Message.Response response = context.requestLLM(input);
if (response instanceof Message.Assistant) {
return ((Message.Assistant) response).getContent();
}
return "";
})
.build();
String result = mathAgent.run("What is 12 × 9?");
System.out.println(result);
```
The agent can produce the following output:
```
The answer to 12 × 9 is 108.
```
## Make sequential LLM calls
You can extend the previous strategy to make multiple sequential LLM calls:
```
fun Message.Assistant.text(): String =
parts.filterIsInstance().joinToString("\n") { it.text }
val strategy = functionalStrategy { input ->
// The first LLM call produces an initial draft based on the user input
val draft = requestLLM("Draft: $input").text()
// The second LLM call improves the initial draft
val improved = requestLLM("Improve and clarify.").text()
// The final LLM call formats the improved text and returns the result
requestLLM("Format the result as bold.").text()
}
```
```
AIAgent mathAgent = AIAgent.builder()
.promptExecutor(simpleOllamaAIExecutor("http://localhost:11434"))
.systemPrompt("You are a precise math assistant.")
.llmModel(OllamaModels.Meta.LLAMA_3_2)
.functionalStrategy((AIAgentFunctionalContext context, String input) -> {
// The first LLM call produces an initial draft based on the user input
Message.Response draftResponse = context.requestLLM("Draft: " + input);
String draft = "";
if (draftResponse instanceof Message.Assistant) {
draft = ((Message.Assistant) draftResponse).getContent();
}
// The second LLM call improves the initial draft
Message.Response improvedResponse = context.requestLLM("Improve and clarify.");
String improved = "";
if (improvedResponse instanceof Message.Assistant) {
improved = ((Message.Assistant) improvedResponse).getContent();
}
// The final LLM call formats the improved text and returns the result
Message.Response finalResponse = context.requestLLM("Format the result as bold.");
if (finalResponse instanceof Message.Assistant) {
return ((Message.Assistant) finalResponse).getContent();
}
return "";
})
.build();
```
The agent can produce the following output:
```
To calculate the product of 12 and 9, we multiply these two numbers together.
12 × 9 = **108**
```
## Add tools
In many cases, a functional agent needs to complete specific tasks, such as reading and writing data, calling APIs, or performing other deterministic operations. In Koog, you expose such capabilities as [tools](../../tools/) and let the LLM decide when to call them.
Here is what you need to do:
1. Create an [annotation-based tool](../../tools/annotation-based-tools/).
1. Add it to a tool registry and pass the registry to the agent.
1. Make sure the agent strategy can identify tool calls in LLM responses, execute the requested tools, send their results back to the LLM, and repeat the process until there are no tool calls remaining.
```
@LLMDescription("Tools for performing math operations")
class MathTools : ToolSet {
@Tool
@LLMDescription("Multiplies two numbers and returns the result")
fun multiply(a: Int, b: Int): Int {
// This is not necessary, but it helps to see the tool call in the console output
println("Multiplying $a and $b...")
return a * b
}
}
val toolRegistry = ToolRegistry {
tool(MathTools()::multiply)
}
val strategy = functionalStrategy { input ->
// Send the user input to the LLM
var response = requestLLM(input)
// Only loop while the LLM requests tools
var toolCalls = response.parts.filterIsInstance()
while (toolCalls.isNotEmpty()) {
// Execute the tools and return the results
val results = executeTools(toolCalls)
// Send the tool results back to the LLM. The LLM may call more tools or return a final output
response = sendToolResults(results)
toolCalls = response.parts.filterIsInstance()
}
// When no tool calls remain, extract and return the assistant message content from the response
response.parts.filterIsInstance().joinToString("\n") { it.text }
}
val mathAgentWithTools = AIAgent(
promptExecutor = simpleOllamaAIExecutor(),
llmModel = OllamaModels.Meta.LLAMA_3_2,
toolRegistry = toolRegistry,
strategy = strategy
)
fun main() = runBlocking {
val result = mathAgentWithTools.run("Multiply 3 by 4, then multiply the result by 5.")
println(result)
}
```
```
@LLMDescription(description = "Tools for performing math operations")
public static class MathTools implements ToolSet {
@Tool
@LLMDescription(description = "Multiplies two numbers and returns the result")
public int multiply(int a, int b) {
// This is not necessary, but it helps to see the tool call in the console output
System.out.println("Multiplying " + a + " and " + b + "...");
return a * b;
}
}
public static void main(String[] args) {
MathTools mathTools = new MathTools();
ToolRegistry toolRegistry = ToolRegistry.builder()
.tools(mathTools)
.build();
AIAgent mathAgentWithTools = AIAgent.builder()
.promptExecutor(SimpleLLMExecutorsKt.simpleOllamaAIExecutor("http://localhost:11434"))
.llmModel(OllamaModels.Meta.LLAMA_3_2)
.toolRegistry(toolRegistry)
.functionalStrategy("mathWithTools", (AIAgentFunctionalContext context, String input) -> {
// Send the user input to the LLM
List responses = context.requestLLMMultiple(input);
// Only loop while the LLM requests tools
while (context.containsToolCalls(responses)) {
// Extract tool calls from the response
List pendingCalls = context.extractToolCalls(responses);
// Execute the tools and return the results
List results = context.executeMultipleTools(pendingCalls, false);
// Send the tool results back to the LLM
responses = context.sendMultipleToolResults(results);
}
// Extract and return the assistant message content from the response
Message.Response finalResponse = responses.get(0);
if (finalResponse instanceof Message.Assistant) {
return ((Message.Assistant) finalResponse).getContent();
}
return "";
})
.build();
String result = mathAgentWithTools.run("Multiply 3 by 4, then multiply the result by 5.");
System.out.println(result);
}
```
The agent can produce the following output:
```
Multiplying 3 and 4...
Multiplying 12 and 5...
The result of multiplying 3 by 4 is 12. Multiplying 12 by 5 gives us a final answer of 60.
```
## Next steps
- Learn how to create [graph-based agents](../graph-based-agents/)
# Prompts
# Prompts
Prompts are instructions for Large Language Models (LLMs) that guide them in generating responses. They define the content and structure of your interactions with LLMs. This section describes how to create and run prompts with Koog.
## Creating prompts
In Koog, prompts are instances of the [**Prompt**](https://api.koog.ai/prompt/prompt-model/ai.koog.prompt.dsl/-prompt/index.html) data class with the following properties:
- `id`: A unique identifier for the prompt.
- `messages`: A list of messages that represent the conversation with the LLM.
- `params`: Optional [LLM configuration parameters](prompt-creation/#prompt-parameters) (such as temperature, tool choice, and others).
Although you can instantiate the `Prompt` class directly, the recommended way to create prompts is by using the [Kotlin DSL](prompt-creation/) or the Java builder API, which provide a structured way to define the conversation.
Note
Kotlin examples on this page use the Kotlin DSL. Java examples use the `Prompt.builder("id")` builder with explicit methods like `system(...)`, `user(...)`, `assistant(...)`, `toolCall(...)`, `toolResult(...)`, and `withOutput(Foo.class)` where applicable.
```
val myPrompt = prompt("hello-koog") {
system("You are a helpful assistant.")
user("What is Koog?")
}
```
```
var myPrompt = Prompt.builder("hello-koog")
.system("You are a helpful assistant.")
.user("What is Koog?")
.build();
```
Note
AI agents can take a simple text prompt as input. They automatically convert the text prompt to the Prompt object and send it to the LLM for execution. This is useful for a [basic agent](../agents/basic-agents/) that only needs to run a single request and does not require complex conversation logic.
## Running prompts
Koog provides two levels of abstraction for running prompts against LLMs: LLM clients and prompt executors. Both accept Prompt objects and can be used for direct prompt execution, without an AI agent. The execution flow is the same for both clients and executors:
```
flowchart TB
A([Prompt built with Kotlin DSL or Java builder])
B{LLM client or prompt executor}
C[LLM provider]
D([Response to your application])
A -->|"passed to"| B
B -->|"sends request"| C
C -->|"returns response"| B
B -->|"returns result"| D
```
- [**LLM clients**](llm-clients/)
______________________________________________________________________
Low‑level interfaces for direct interaction with specific LLM providers. Use them when you work with a single provider and do not need advanced lifecycle management.
- [**Prompt executors**](prompt-executors/)
______________________________________________________________________
High-level abstractions that manage the lifecycles of one or multiple LLM clients. Use them when you need a unified API for running prompts across multiple providers, with dynamic switching between them and fallbacks.
## Optimizing performance and handling failures
Koog allows you to optimize performance and handle failures when running prompts.
- [**LLM response caching**](llm-response-caching/)
______________________________________________________________________
Cache LLM responses to optimize performance and reduce costs for repeated requests.
- [**Handling failures**](handling-failures/)
______________________________________________________________________
Use built-in retries, timeouts, and other error handling mechanisms in your application.
## Prompts in AI agents
In Koog, AI agents maintain and manage prompts during their lifecycle. While LLM clients or executors are used to run prompts, agents handle the flow of prompt updates, ensuring the conversation history remains relevant and consistent.
The prompt lifecycle in an agent usually includes several stages:
1. Initial prompt setup.
1. Automatic prompt updates.
1. Context window management.
1. Manual prompt management.
### Initial prompt setup
When you [initialize an agent](../quickstart/#create-your-first-koog-agent), you can define a [system message](prompt-creation/#system-message) that sets the agent's behavior. Then, when you call the agent's `run()` method, you typically provide an initial [user message](prompt-creation/#user-messages) as input. Together, these messages form the agent's initial prompt. For example:
```
// Create an agent
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(apiKey),
systemPrompt = "You are a helpful assistant.",
llmModel = OpenAIModels.Chat.GPT4o
)
// Run the agent
val result = agent.run("What is Koog?")
```
```
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")))
.systemPrompt("You are a helpful assistant. Answer user questions concisely.")
.llmModel(OpenAIModels.Chat.GPT4o)
.build();
var result = agent.run("What is Koog?");
```
In the example, the agent automatically converts the text prompt to the Prompt object and sends it to the prompt executor:
```
flowchart TB
A([Your application])
B{{Configured AI agent}}
C["Text prompt"]
D["Prompt object"]
E{{Prompt executor}}
F[LLM provider]
A -->|"run() with text"| B
B -->|"takes"| C
C -->|"converted to"| D
D -->|"sent via"| E
E -->|"calls"| F
F -->|"responds to"| E
E -->|"result to"| B
B -->|"result to"| A
```
For more advanced configurations, you can also use [AIAgentConfig](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.agent.config/-a-i-agent-config/index.html) to define the agent's initial prompt.
### Automatic prompt updates
As the agent runs its strategy, [predefined nodes](../nodes-and-components/) automatically update the prompt. For example:
- [`nodeLLMRequest`](../nodes-and-components/#nodellmrequest): Appends a user message to the prompt and captures the LLM response.
- [`nodeLLMSendToolResult`](../nodes-and-components/#nodellmsendtoolresult): Appends tool execution results to the conversation.
- [`nodeAppendPrompt`](../nodes-and-components/#nodeappendprompt): Inserts specific messages into the prompt at any point in the workflow.
### Context window management
To avoid exceeding the LLM context window in long-running interactions, agents can use the [history compression](../history-compression/) feature.
### Manual prompt management
For complex workflows, you can manage the prompt manually using [LLM sessions](../sessions/). In an agent strategy or custom node, you can use `llm.writeSession` to access and change the `Prompt` object. This lets you add, remove, or reorder messages as needed.
# Creating prompts
Koog provides a structured way to create prompts with control over message types, their order, and content:
- For **Kotlin** users, through a type-safe Kotlin DSL.
- For **Java** users, through a fluent builder API.
## Basic structure
The `prompt()` function in Kotlin or the `Prompt.builder()` in Java create a Prompt object with a unique ID and a list of messages:
```
val prompt = prompt("unique_prompt_id") {
// List of messages
}
```
```
Prompt prompt = Prompt.builder("unique_prompt_id")
// List of messages
.build();
```
## Message types
The Kotlin DSL and the Java builder API support the following types of messages, each of which corresponds to a specific role in a conversation:
- **System message**: Provides the context, instructions, and constraints to the LLM, defining its behavior.
- **User message**: Represents the user input.
- **Assistant message**: Represents LLM responses that are used for few-shot learning or to continue the conversation.
- **Tool message**: Represents tool calls and their results.
```
val prompt = prompt("unique_prompt_id") {
// Add a system message to set the context
system("You are a helpful assistant with access to tools.")
// Add a user message
user("What is 5 + 3 ?")
// Add an assistant message
assistant("The result is 8.")
}
```
```
Prompt prompt = Prompt.builder("unique_prompt_id")
// Add a system message to set the context
.system("You are a helpful assistant with access to tools.")
// Add a user message
.user("What is 5 + 3 ?")
// Add an assistant message
.assistant("The result is 8.")
.build();
```
### System message
A system message defines the LLM behavior and sets the context for the entire conversation. It can specify the model's role, tone, provide guidelines and constraints on responses, and provide response examples.
To create the system message, provide a string as an argument to the `system()` Kotlin function or Java method:
```
val prompt = prompt("system_message") {
system("You are a helpful assistant that explains technical concepts.")
}
```
```
Prompt prompt = Prompt.builder("system_message")
.system("You are a helpful assistant that explains technical concepts.")
.build();
```
### User messages
A user message represents input from the user. To create the user message, provide a string as an argument to the `user()` Kotlin function or Java method:
```
val prompt = prompt("user_message") {
system("You are a helpful assistant.")
user("What is Koog?")
}
```
```
Prompt prompt = Prompt.builder("user_message")
.system("You are a helpful assistant.")
.user("What is Koog?")
.build();
```
Most user messages contain plain text, but they can also include multimodal content, such as images, audio, video, and documents. For details and examples, see [Multimodal content](multimodal-content/).
### Assistant messages
An assistant message represents an LLM response, which can be used for few-shot learning in future similar interactions, to continue a conversation, or to demonstrate the expected output structure.
To create the assistant message, provide a string as an argument to the `assistant()` Kotlin function or Java method:
```
val prompt = prompt("article_review") {
system("Evaluate the article.")
// Example 1
user("The article is clear and easy to understand.")
assistant("positive")
// Example 2
user("The article is hard to read but it's clear and useful.")
assistant("neutral")
// Example 3
user("The article is confusing and misleading.")
assistant("negative")
// New input to classify
user("The article is interesting and helpful.")
}
```
```
Prompt prompt = Prompt.builder("article_review")
.system("Evaluate the article.")
// Example 1
.user("The article is clear and easy to understand.")
.assistant("positive")
// Example 2
.user("The article is hard to read but it's clear and useful.")
.assistant("neutral")
// Example 3
.user("The article is confusing and misleading.")
.assistant("negative")
// New input to classify
.user("The article is interesting and helpful.")
.build();
```
### Tool messages
A tool message represents a tool call and its result, which can be used to pre-fill the history of tool calls.
Tip
An LLM generates tool calls during execution. Pre-filling them is helpful for few-shot learning or demonstrating how the tools are expected to be used.
To create the tool message, call the `tool()` function in Kotlin or the `toolCall()` and `toolResult()` methods in Java:
```
val prompt = prompt("calculator_example") {
system("You are a helpful assistant with access to tools.")
user("What is 5 + 3?")
// Tool call
toolCall(
id = "calculator_tool_id",
tool = "calculator",
args = """{"operation": "add", "a": 5, "b": 3}"""
)
// Tool result
toolResult(
id = "calculator_tool_id",
tool = "calculator",
output = "8"
)
// LLM response based on tool result
assistant("The result of 5 + 3 is 8.")
user("What is 4 + 5?")
}
```
```
Prompt prompt = Prompt.builder("calculator_example")
.system("You are a helpful assistant with access to tools.")
.user("What is 5 + 3?")
// Tool call
.toolCall("calculator_tool_id", "calculator", "{\"operation\": \"add\", \"a\": 5, \"b\": 3}")
// Tool result
.toolResult("calculator_tool_id", "calculator", "8")
// LLM response based on tool result
.assistant("The result of 5 + 3 is 8.")
.user("What is 4 + 5?")
.build();
```
## Text message builders
Warning
Text message builders are available only in Kotlin.
When building a `system()`, `user()`, or `assistant()` message, you can use helper [text-building-functions](https://api.koog.ai/prompt/prompt-model/ai.koog.prompt.text/-text-content-builder/index.html) for rich text formatting.
```
val prompt = prompt("text_example") {
user {
+"Review the following code snippet:"
+"fun greet(name: String) = println(\"Hello, \$name!\")"
// Paragraph break
br()
text("Please include in your explanation:")
// Indent content
padding(" ") {
+"1. What the function does."
+"2. How string interpolation works."
}
}
}
```
You can also use the [Markdown](https://api.koog.ai/prompt/prompt-markdown/ai.koog.prompt.markdown/markdown.html) and [XML](https://api.koog.ai/prompt/prompt-xml/ai.koog.prompt.xml/xml.html) builders to add the content in the corresponding format.
```
val prompt = prompt("markdown_xml_example") {
// A user message in Markdown format
user {
markdown {
h2("Evaluate the article using the following criteria:")
bulleted {
item { +"Clarity and readability" }
item { +"Accuracy of information" }
item { +"Usefulness to the reader" }
}
}
}
// An assistant message in XML format
assistant {
xml {
xmlDeclaration()
tag("review") {
tag("clarity") { text("positive") }
tag("accuracy") { text("neutral") }
tag("usefulness") { text("positive") }
}
}
}
}
```
Tip
You can mix the text building functions with the XML and Markdown builders.
## Prompt parameters
Prompts can be customized by configuring parameters that control the LLM's behavior.
```
val prompt = prompt(
id = "custom_params",
params = LLMParams(
temperature = 0.7,
numberOfChoices = 1,
toolChoice = LLMParams.ToolChoice.Auto
)
) {
system("You are a creative writing assistant.")
user("Write a song about winter.")
}
```
```
// Create params first
LLMParams params = new LLMParams(
0.7, // temperature
null, // maxTokens
1, // numberOfChoices
null, // speculation
null, // schema
LLMParams.ToolChoice.Auto.INSTANCE, // toolChoice
null, // user
null // additionalProperties
);
Prompt prompt = Prompt.builder("custom_params")
.system("You are a creative writing assistant.")
.user("Write a song about winter.")
.build();
// Apply params to the built prompt
prompt = prompt.withParams(params);
```
The following parameters are supported:
- `temperature`: Controls randomness in the model's responses.
- `toolChoice`: Controls tool calling behavior of the model.
- `numberOfChoices`: Requests multiple alternative responses.
- `schema`: Defines the structure for the model's response format.
- `maxTokens`: Limits the number of tokens in the response.
- `speculation`: Provides a hint about the expected response format (only supported by specific models).
For more information, see [LLM parameters](../../llm-parameters/).
## Extending existing prompts
You can extend an existing prompt by calling the `prompt()` function in Kotlin or the `Prompt.builder()` in Java with the existing prompt as an argument:
```
val basePrompt = prompt("base") {
system("You are a helpful assistant.")
user("Hello!")
assistant("Hi! How can I help you?")
}
val extendedPrompt = prompt(basePrompt) {
user("What's the weather like?")
}
```
```
Prompt basePrompt = Prompt.builder("base")
.system("You are a helpful assistant.")
.user("Hello!")
.assistant("Hi! How can I help you?")
.build();
Prompt extendedPrompt = Prompt.builder(String.valueOf(basePrompt))
.user("What's the weather like?")
.build();
```
This creates a new prompt that includes all messages from `basePrompt` and the new user message.
## Next steps
- Learn how to work with [multimodal content](multimodal-content/).
- Run prompts with [LLM clients](../llm-clients/) if you work with a single LLM provider.
- Run prompts with [prompt executors](../prompt-executors/) if you work with multiple LLM providers.
- Learn how to use llm cache with [cache control](cache-control/).
# Multimodal content
Multimodal content refers to content of different types, such as text, images, audio, video, and files. Koog lets you send images, audio, video, and files to LLMs within the `user` message along with text. You can add them to the `user` message by using the corresponding functions in Kotlin or methods in Java:
- `image()`: Attaches images (JPG, PNG, WebP, GIF).
- `audio()`: Attaches audio files (MP3, WAV, FLAC).
- `video()`: Attaches video files (MP4, AVI, MOV).
- `file()` / `binaryFile()` / `textFile()`: Attaches documents (PDF, TXT, MD, etc.).
Each function or method supports two ways of configuring attachment parameters, so you can:
- Pass a URL or a file path to the function or method, and it automatically handles attachment parameters. For `file()`, `binaryFile()`, and `textFile()`, you must also provide the MIME type.
- Create and pass a `ContentPart` object to the function or method for custom control over attachment parameters.
Note
Multimodal content support varies by [LLM provider](../../../llm-providers/). Check the provider documentation for supported content types.
### Auto-configured attachments
If you pass a URL or a file path to the attachment functions or methods, Koog automatically constructs the corresponding attachment parameters based on the file extension.
The general format of the `user` message that includes a text message and a list of auto-configured attachments is as follows:
```
user {
+"Describe these images:"
image("https://example.com/test.png")
image(Path("/path/to/image.png"))
+"Focus on the main subjects."
}
```
```
ContentPartsBuilder partsBuilder = new ContentPartsBuilder();
partsBuilder.text("Describe these images:");
partsBuilder.image("https://example.com/test.png");
partsBuilder.text("Focus on the main subjects.");
Prompt prompt = Prompt.builder("image_analysis")
.user(partsBuilder.build())
.build();
```
In Kotlin, the `+` operator adds text content to the user message along with the attachments. In Java, use the `text()` method of `ContentPartsBuilder`.
### Custom-configured attachments
The [`ContentPart`](https://api.koog.ai/prompt/prompt-model/ai.koog.prompt.message/-content-part/index.html) interface lets you configure parameters for each attachment individually.
All attachments implement the `ContentPart.Attachment` interface. You can create an instance of a specific implementation for each attachment, configure its parameters, and pass it to the corresponding `image()`, `audio()`, `video()`, or `file()` functions in Kotlin or methods in Java.
The general format of the `user` message that includes a text message and a list of custom-configured attachments is as follows:
```
user {
+"Describe this image"
image(
AttachmentSource.Image(
content = AttachmentContent.URL("https://example.com/capture.png"),
format = "png",
mimeType = "image/png",
fileName = "capture.png"
)
)
}
```
```
Prompt prompt = Prompt.builder("custom_image")
.user(List.of(
new ContentPart.Text("Describe this image"),
new ContentPart.Image(
new AttachmentContent.URL("https://example.com/capture.png"),
"png",
"image/png",
"capture.png"
)
))
.build();
```
Koog provides the following specialized classes for each media type that implement the `ContentPart.Attachment` interface:
- [`ContentPart.Image`](api:prompt-model::ai.koog.prompt.message.ContentPart.Image): image attachments, such as JPG or PNG files.
- [`ContentPart.Audio`](api:prompt-model::ai.koog.prompt.message.ContentPart.Audio): audio attachments, such as MP3 or WAV files.
- [`ContentPart.Video`](api:prompt-model::ai.koog.prompt.message.ContentPart.Video): video attachments, such as MP4 or AVI files.
- [`ContentPart.File`](api:prompt-model::ai.koog.prompt.message.ContentPart.File): file attachments, such as PDF or TXT files.
All `ContentPart.Attachment` types accept the following parameters:
| Name | Data type | Required | Description |
| ---------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content` | [AttachmentContent](https://api.koog.ai/prompt/prompt-model/ai.koog.prompt.message/-attachment-content/index.html) | Yes | The source of the provided file content. |
| `format` | String | Yes | The format of the provided file. For example, `png`. |
| `mimeType` | String | Only for `ContentPart.File` | The MIME Type of the provided file. For `ContentPart.Image`, `ContentPart.Audio`, and `ContentPart.Video`, it defaults to `/` (for example, `image/png`). For `ContentPart.File`, it must be explicitly provided. |
| `fileName` | String? | No | The name of the provided file including the extension. For example, `screenshot.png`. |
#### Attachment content
Implementations of the AttachmentContent interface define the type and source of content that is provided as input to the LLM:
- [`AttachmentContent.URL`](https://api.koog.ai/prompt/prompt-model/ai.koog.prompt.message/-attachment-content/-u-r-l/index.html) defines the URL of the provided content:
```
AttachmentContent.URL("https://example.com/image.png")
```
- [`AttachmentContent.Binary.Bytes`](https://api.koog.ai/prompt/prompt-model/ai.koog.prompt.message/-attachment-content/-binary/index.html) defines the file content as a byte array:
```
AttachmentContent.Binary.Bytes(byteArrayOf(/* ... */))
```
- [`AttachmentContent.Binary.Base64`](https://api.koog.ai/prompt/prompt-model/ai.koog.prompt.message/-attachment-content/-binary/index.html) defines the file content as a Base64-encoded string containing file data:
```
AttachmentContent.Binary.Base64("iVBORw0KGgoAAAANS...")
```
- [`AttachmentContent.PlainText`](https://api.koog.ai/prompt/prompt-model/ai.koog.prompt.message/-attachment-content/-plain-text/index.html) defines the file content as plain text (for [`ContentPart.File`](api:prompt-model::ai.koog.prompt.message.ContentPart.File) only):
```
AttachmentContent.PlainText("This is the file content.")
```
### Mixed attachments
In addition to providing different types of attachments in separate prompts or messages, you can also provide multiple and mixed types of attachments in a single `user()` message:
```
val prompt = prompt("mixed_content") {
system("You are a helpful assistant.")
user {
+"Compare the image with the document content."
image(Path("/path/to/image.png"))
binaryFile(Path("/path/to/page.pdf"), "application/pdf")
+"Structure the result as a table"
}
}
```
```
Prompt prompt = Prompt.builder("mixed_content_example")
.system("You are a helpful assistant.")
.user(List.of(
new ContentPart.Text("Please analyze this image and the attached document."),
new ContentPart.Image(
new AttachmentContent.URL("https://example.com/image.png"),
"png",
"image/png",
"image.png"
),
new ContentPart.File(
new AttachmentContent.URL("https://example.com/document.pdf"),
"pdf",
"application/pdf",
"document.pdf"
),
new ContentPart.Text("Summarize the differences.")
))
.build();
```
## Next steps
- Run prompts with [LLM clients](../../llm-clients/) if you work with a single LLM provider.
- Run prompts with [prompt executors](../../prompt-executors/) if you work with multiple LLM providers.
# Handling failures
This page describes how to handle failures for LLM clients and prompt executors using the built-in retry and timeout mechanisms.
## Retry functionality
When working with LLM providers, transient errors like rate limits or temporary service unavailability may occur. The `RetryingLLMClient` decorator adds automatic retry logic to any LLM client in both Kotlin and Java.
### Basic usage
Wrap any existing client with the retry capability:
```
// Wrap any client with the retry capability
val client = OpenAILLMClient(apiKey)
val resilientClient = RetryingLLMClient(client)
// Now all operations will automatically retry on transient errors
val response = resilientClient.execute(prompt, OpenAIModels.Chat.GPT4o)
```
```
OpenAILLMClient client = openAIClient(apiKey);
RetryingLLMClient resilientClient = new RetryingLLMClient(client);
// Now all operations will automatically retry on transient errors
List response = resilientClient.execute(prompt, OpenAIModels.Chat.GPT4o);
```
### Configuring retry behavior
By default, `RetryingLLMClient` configures an LLM client with the maximum of 3 retry attempts, a 1-second initial delay, and a 30-second maximum delay. You can specify a different retry configuration using a `RetryConfig` passed to `RetryingLLMClient`. For example:
```
// Use the predefined configuration
val conservativeClient = RetryingLLMClient(
delegate = client,
config = RetryConfig.CONSERVATIVE
)
```
```
OpenAILLMClient client = openAIClient(apiKey);
// Use the predefined configuration
RetryingLLMClient conservativeClient = new RetryingLLMClient(
client,
RetryConfig.Companion.getCONSERVATIVE()
);
```
Koog provides several predefined retry configurations available via `RetryConfig` in Kotlin and `RetryConfig.Companion` in Java:
| Configuration (Kotlin) | Max attempts | Initial delay | Max delay | Use case |
| -------------------------- | ------------ | ------------- | --------- | -------------------------------------------------------------------------------------------------------- |
| `RetryConfig.DISABLED` | 1 (no retry) | - | - | Development, testing, and debugging. |
| `RetryConfig.CONSERVATIVE` | 3 | 2s | 30s | Background or scheduled tasks where reliability is more important than speed. |
| `RetryConfig.AGGRESSIVE` | 5 | 500ms | 20s | Critical operations where fast recovery from transient errors is more important than reducing API calls. |
| `RetryConfig.PRODUCTION` | 3 | 1s | 20s | General production use. |
You can use them directly or create custom configurations:
```
// Or create a custom configuration
val customClient = RetryingLLMClient(
delegate = client,
config = RetryConfig(
maxAttempts = 5,
initialDelay = 1.seconds,
maxDelay = 30.seconds,
backoffMultiplier = 2.0,
jitterFactor = 0.2
)
)
```
### Retry error patterns
By default, the `RetryingLLMClient` recognizes common transient errors. This behavior is controlled by the [`RetryConfig.retryablePatterns`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/ai.koog.prompt.executor.clients.retry/-retry-config/retryable-patterns.html) patterns. Each pattern is represented by [`RetryablePattern`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/ai.koog.prompt.executor.clients.retry/-retryable-pattern/index.html) that checks the error message from a failed request and determines whether it should be retried.
Koog provides the predefined retry configurations and patterns that work across all the supported LLM providers. You can keep the defaults or customize them for your specific needs.
#### Pattern types
You can use the following pattern types and combine any number of them:
- `RetryablePattern.Status`: Matches a specific HTTP status code in the error message (such as `429`, `500`,`502`, etc.).
- `RetryablePattern.Keyword`: Matches a keyword in the error message (such as `rate limit` or `request timeout`).
- `RetryablePattern.Regex`: Matches a regular expression in the error message.
- `RetryablePattern.Custom`: Matches a custom logic using a lambda function.
If any pattern returns `true`, the error is considered retryable, and the LLM client retries the request.
#### Default patterns
Unless you customize the retry configuration, the following patterns are used by default:
- **HTTP status codes**:
- `429`: Rate limit
- `500`: Internal server error
- `502`: Bad gateway
- `503`: Service unavailable
- `504`: Gateway timeout
- `529`: Anthropic overloaded
- **Error keywords**:
- rate limit
- too many requests
- request timeout
- connection timeout
- read timeout
- write timeout
- connection reset by peer
- connection refused
- temporarily unavailable
- service unavailable
These default patterns are defined in Koog as [`RetryConfig.DEFAULT_PATTERNS`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/ai.koog.prompt.executor.clients.retry/-retry-config/-companion/-d-e-f-a-u-l-t_-p-a-t-t-e-r-n-s.html).
#### Custom patterns
You can define custom patterns for your specific needs:
```
val config = RetryConfig(
retryablePatterns = listOf(
RetryablePattern.Status(429), // Specific status code
RetryablePattern.Keyword("quota"), // Keyword in error message
RetryablePattern.Regex(Regex("ERR_\\d+")), // Custom regex pattern
RetryablePattern.Custom { error -> // Custom logic
error.contains("temporary") && error.length > 20
}
)
)
```
You can also append custom patterns to the default `RetryConfig.DEFAULT_PATTERNS`:
```
val config = RetryConfig(
retryablePatterns = RetryConfig.DEFAULT_PATTERNS + listOf(
RetryablePattern.Keyword("custom_error")
)
)
```
### Streaming with retry
Streaming operations can optionally be retried. This feature is disabled by default.
```
val config = RetryConfig(
maxAttempts = 3
)
val client = RetryingLLMClient(baseClient, config)
val stream = client.executeStreaming(prompt, OpenAIModels.Chat.GPT4o)
```
Note
Streaming retries only apply to connection failures that occur before the first token is received. Once streaming has started, the retry logic is disabled. If an error occurs during streaming, the operation is terminated.
### Retry with prompt executors
When working with prompt executors, you can wrap the underlying LLM client with a retry mechanism before creating the executor in both Kotlin and Java. To learn more about prompt executors, see [Prompt executors](../prompt-executors/).
```
// Single provider executor with retry
val resilientClient = RetryingLLMClient(
OpenAILLMClient(System.getenv("OPENAI_API_KEY")),
RetryConfig.PRODUCTION
)
val executor = MultiLLMPromptExecutor(resilientClient)
// Multi-provider executor with flexible client configuration
val multiExecutor = MultiLLMPromptExecutor(
LLMProvider.OpenAI to RetryingLLMClient(
OpenAILLMClient(System.getenv("OPENAI_API_KEY")),
RetryConfig.CONSERVATIVE
),
LLMProvider.Anthropic to RetryingLLMClient(
AnthropicLLMClient(System.getenv("ANTHROPIC_API_KEY")),
RetryConfig.AGGRESSIVE
),
// The Bedrock client already has a built-in AWS SDK retry
LLMProvider.Bedrock to BedrockLLMClient(
identityProvider = StaticCredentialsProvider {
accessKeyId = System.getenv("AWS_ACCESS_KEY_ID")
secretAccessKey = System.getenv("AWS_SECRET_ACCESS_KEY")
sessionToken = System.getenv("AWS_SESSION_TOKEN")
},
),
)
```
```
// Single provider executor with retry (Java)
RetryingLLMClient resilientClient = new RetryingLLMClient(
openAIClient(System.getenv("OPENAI_API_KEY")),
RetryConfig.Companion.getPRODUCTION()
);
MultiLLMPromptExecutor executor = new MultiLLMPromptExecutor(resilientClient);
// Multi-provider executor with flexible client configuration (Java)
LLMClient openai = new RetryingLLMClient(
openAIClient(System.getenv("OPENAI_API_KEY")),
RetryConfig.Companion.getCONSERVATIVE()
);
LLMClient anthropic = new RetryingLLMClient(
anthropicClient(System.getenv("ANTHROPIC_API_KEY")),
RetryConfig.Companion.getAGGRESSIVE()
);
Map clients = Map.of(
LLMProvider.OpenAI, openai,
LLMProvider.Anthropic, anthropic
);
MultiLLMPromptExecutor multiExecutor = new MultiLLMPromptExecutor(clients);
```
## Timeout configuration
All LLM clients support timeout configuration in both Kotlin and Java to prevent hanging requests. You can specify timeout values for network connections when creating the client using the [`ConnectionTimeoutConfig`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/ai.koog.prompt.executor.clients/-connection-timeout-config/index.html) class.
`ConnectionTimeoutConfig` has the following properties:
| Property | Default Value | Description |
| ---------------------- | -------------------- | ------------------------------------------------------------- |
| `connectTimeoutMillis` | 60 seconds (60,000) | Maximum time to establish a connection to the server. |
| `requestTimeoutMillis` | 15 minutes (900,000) | Maximum time for the entire request to complete. |
| `socketTimeoutMillis` | 15 minutes (900,000) | Maximum time to wait for data over an established connection. |
You can customize these values for your specific needs. For example:
```
val client = OpenAILLMClient(
apiKey = apiKey,
settings = OpenAIClientSettings(
timeoutConfig = ConnectionTimeoutConfig(
connectTimeoutMillis = 5000, // 5 seconds to establish connection
requestTimeoutMillis = 60000, // 60 seconds for the entire request
socketTimeoutMillis = 120000 // 120 seconds for data on the socket
)
)
)
```
```
String apiKey = System.getenv("OPENAI_API_KEY");
ConnectionTimeoutConfig timeouts = new ConnectionTimeoutConfig(
5000L, // connectTimeoutMillis
60000L, // requestTimeoutMillis
120000L // socketTimeoutMillis
);
OpenAIClientSettings settings = new OpenAIClientSettings(
"https://api.openai.com", // baseUrl
timeouts,
"v1/chat/completions", // chatCompletionsPath
"v1/responses", // responsesAPIPath
"v1/embeddings", // embeddingsPath
"v1/moderations", // moderationsPath
"v1/models" // modelsPath
);
OpenAILLMClient client = openAIClient(apiKey, settings);
```
Tip
For long-running or streaming calls, set higher values for `requestTimeoutMillis` and `socketTimeoutMillis`.
## Error handling
When working with LLMs in production, you need to implement error handling, including:
- **Try-catch blocks** to handle unexpected errors.
- **Logging errors with context** for debugging.
- **Fallbacks** for critical operations.
- **Monitoring retry patterns** to identify recurring issues.
Here is an example of error handling in Kotlin and Java:
```
val logger = LoggerFactory.getLogger("Example")
val resilientClient = RetryingLLMClient(
OpenAILLMClient(System.getenv("OPENAI_API_KEY")),
RetryConfig.PRODUCTION
)
val prompt = prompt("test") { user("Hello") }
val model = OpenAIModels.Chat.GPT4o
fun processResponse(response: Any) { /* implmenentation */ }
fun scheduleRetryLater() { /* implmenentation */ }
fun notifyAdministrator() { /* implmenentation */ }
fun useDefaultResponse() { /* implmenentation */ }
try {
val response = resilientClient.execute(prompt, model)
processResponse(response)
} catch (e: Exception) {
logger.error("LLM operation failed", e)
when {
e.message?.contains("rate limit") == true -> {
// Handle rate limiting specifically
scheduleRetryLater()
}
e.message?.contains("invalid api key") == true -> {
// Handle authentication errors
notifyAdministrator()
}
else -> {
// Fall back to an alternative solution
useDefaultResponse()
}
}
}
```
```
Logger logger = LoggerFactory.getLogger("Example");
RetryingLLMClient resilientClient = new RetryingLLMClient(
openAIClient(System.getenv("OPENAI_API_KEY")),
RetryConfig.PRODUCTION
);
Prompt prompt = Prompt.builder("test")
.user("Hello")
.build();
MultiLLMPromptExecutor promptExecutor = new MultiLLMPromptExecutor(resilientClient);
Consumer processResponse = (resp) -> { /* implementation */ };
Runnable scheduleRetryLater = () -> { /* implementation */ };
Runnable notifyAdministrator = () -> { /* implementation */ };
Runnable useDefaultResponse = () -> { /* implementation */ };
try {
Message.Assistant response = promptExecutor.execute(prompt, OpenAIModels.Chat.GPT4o);
processResponse.accept(response);
} catch (Exception e) {
logger.error("LLM operation failed", e);
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
if (msg.contains("rate limit")) {
scheduleRetryLater.run();
} else if (msg.contains("invalid api key")) {
notifyAdministrator.run();
} else {
useDefaultResponse.run();
}
}
```
# LLM response caching
For repeated requests that you run with a prompt executor, you can cache LLM responses to optimize performance and reduce costs in both Kotlin and Java. In Koog, caching is available for all prompt executors through `CachedPromptExecutor`, which is a wrapper around `PromptExecutor` that adds caching functionality. It lets you store responses from previously executed prompts and retrieve them when the same prompts are run again.
To create a cached prompt executor in Kotlin or Java, perform the following:
1. Create a prompt executor for which you want to cache responses.
1. Create a `CachedPromptExecutor` instance by providing the desired cache and the prompt executor you created.
1. Run the created `CachedPromptExecutor` with the desired prompt and model.
Here is an example:
```
// Create a prompt executor
val client = OpenAILLMClient(System.getenv("OPENAI_API_KEY"))
val promptExecutor = MultiLLMPromptExecutor(client)
// Create a cached prompt executor
val cachedExecutor = CachedPromptExecutor(
cache = FilePromptCache(Path("path/to/your/cache/directory")),
nested = promptExecutor
)
// Run cached prompt executor for the first time
// This will perform an actual LLM request
val firstTime = measureTimeMillis {
val firstResponse = cachedExecutor.execute(prompt, OpenAIModels.Chat.GPT4o)
val text = firstResponse.parts.filterIsInstance().joinToString("\n") { it.text }
println("First response: $text")
}
println("First execution took: ${firstTime}ms")
// Run cached prompt executor for the second time
// This will return the result immediately from the cache
val secondTime = measureTimeMillis {
val secondResponse = cachedExecutor.execute(prompt, OpenAIModels.Chat.GPT4o)
val text = secondResponse.parts.filterIsInstance().joinToString("\n") { it.text }
println("Second response: $text")
}
println("Second execution took: ${secondTime}ms")
```
```
// Create a prompt
Prompt prompt = Prompt.builder("test")
.user("Hello")
.build();
// Create a prompt executor
OpenAILLMClient client = openAIClient(System.getenv("OPENAI_API_KEY"));
MultiLLMPromptExecutor promptExecutor = new MultiLLMPromptExecutor(client);
// Create a cached prompt executor
FilePromptCache cache = new FilePromptCache(Path.of("path/to/your/cache/directory"), null);
CachedPromptExecutor cachedExecutor = new CachedPromptExecutor(cache, promptExecutor, Clock.System.INSTANCE);
// Run cached prompt executor for the first time
// This will perform an actual LLM request
long start1 = System.nanoTime();
List firstResponse = cachedExecutor.execute(prompt, OllamaModels.Meta.LLAMA_3_2);
long firstTimeMs = (System.nanoTime() - start1) / 1_000_000L;
System.out.println("First response: " + firstResponse.getFirst().getContent());
System.out.println("First execution took: " + firstTimeMs + "ms");
// Run cached prompt executor for the second time
// This will return the result immediately from the cache
long start2 = System.nanoTime();
List secondResponse = cachedExecutor.execute(prompt, OllamaModels.Meta.LLAMA_3_2);
long secondTimeMs = (System.nanoTime() - start2) / 1_000_000L;
System.out.println("Second response: " + secondResponse.getFirst().getContent());
System.out.println("Second execution took: " + secondTimeMs + "ms");
```
The example produces the following output:
```
First response: Hello! It seems like we're starting a new conversation. What can I help you with today?
First execution took: 48ms
Second response: Hello! It seems like we're starting a new conversation. What can I help you with today?
Second execution took: 1ms
```
The second response is retrieved from the cache, which took only 1ms.
Note
- If you call `executeStreaming()` in Kotlin or `executeStreamingWithPublisher()` in Java with the cached prompt executor, it produces a response as a single chunk.
- If you call `moderate()` with the cached prompt executor in either Kotlin or Java, it forwards the request to the nested prompt executor and does not use the cache.
- Caching of multiple choice responses (`executeMultipleChoices()`) is not supported in either Kotlin or Java.
# Prompt caching control
Prompt caching control lets you instruct a supported LLM provider to store a portion of your prompt server-side, so that subsequent requests that share the same prefix can be served from the cache instead of reprocessing the tokens. This reduces both latency and cost for repetitive workloads such as multi-turn conversations, large system prompts, or fixed tool definitions.
Prompt caching vs. response caching
Prompt caching control is a **provider-side** feature: the provider stores the prompt prefix, not the response. This is different from [`CachedPromptExecutor`](../../llm-response-caching/), which stores complete LLM responses locally so that identical prompts skip the network call entirely.
Koog supports prompt caching control for **Anthropic** and **Amazon Bedrock**.
## Anthropic
Anthropic supports two complementary approaches to prompt caching.
### Automatic caching (request-level)
Set the `cacheControl` property on [`AnthropicParams`](../../../llm-parameters/) and pass it to your prompt. Anthropic will automatically place the cache breakpoint at the last cacheable block in the request, without you having to annotate individual messages. This is the recommended approach for multi-turn conversations.
```
// Enable automatic caching with the default 5-minute TTL
val params = AnthropicParams(cacheControl = AnthropicCacheControl.Default)
val prompt = prompt("assistant", params = params) {
system("You are a helpful assistant with a very long system prompt...")
user("What can you help me with?")
}
val response = client.execute(prompt, AnthropicModels.Sonnet_4)
println(response)
```
```
// Enable automatic caching with the default 5-minute TTL
AnthropicParams params = new AnthropicParams(
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null,
AnthropicCacheControl.Default.INSTANCE
);
Prompt prompt = Prompt.builder("assistant")
.system("You are a helpful assistant with a very long system prompt...")
.user("What can you help me with?")
.build()
.withParams(params);
```
### Manual caching (block-level)
Attach a `cacheControl` argument to individual messages or tool definitions to place the cache breakpoint at a specific position. Everything up to and including the annotated block is eligible for caching.
#### System messages
```
val prompt = prompt("assistant") {
// Cache the system prompt for 1 hour
system("You are a knowledgeable assistant...", AnthropicCacheControl.OneHour)
user("Summarize the latest AI research.")
}
val response = client.execute(prompt, AnthropicModels.Sonnet_4)
println(response)
```
```
Prompt prompt = Prompt.builder("assistant")
// Cache the system prompt for 1 hour
.system("You are a knowledgeable assistant...", AnthropicCacheControl.OneHour.INSTANCE)
.user("Summarize the latest AI research.")
.build();
```
#### User and assistant messages
```
val prompt = prompt("conversation") {
system("You are a helpful assistant.")
// Cache after a large user message (e.g. document content)
user(listOf(MessagePart.Text("Here is a long document: ...", cacheControl = AnthropicCacheControl.Default)))
assistant(listOf(MessagePart.Text("I have read the document.")))
user("Summarize it.")
}
val response = client.execute(prompt, AnthropicModels.Sonnet_4)
println(response)
```
```
Prompt prompt = Prompt.builder("conversation")
.system("You are a helpful assistant.")
// Cache after a large user message (e.g. document content)
.user(List.of(new ContentPart.Text("Here is a long document: ...")), AnthropicCacheControl.Default.INSTANCE)
.assistant("I have read the document.", AnthropicCacheControl.Default.INSTANCE)
.user("Summarize it.")
.build();
```
#### Tool definitions
When a tool list is fixed across many requests, caching the last tool definition means all tool schemas are cached together.
```
val searchTool = ToolDescriptor(
name = "web_search",
description = "Search the web for information.",
requiredParameters = listOf(
ToolParameterDescriptor("query", "Search query", ToolParameterType.String)
),
// Cache all tool definitions up to and including this one
cacheControl = AnthropicCacheControl.Default
)
```
```
ToolDescriptor searchTool = new ToolDescriptor(
"web_search",
"Search the web for information.",
List.of(
new ToolParameterDescriptor("query", "Search query", ToolParameterType.String.INSTANCE)
),
Collections.emptyList(),
// Cache all tool definitions up to and including this one
AnthropicCacheControl.Default.INSTANCE
);
```
### Cache TTL options
| Option | TTL | Price multiplier |
| ------------------------------- | --------- | ---------------------- |
| `AnthropicCacheControl.Default` | 5 minutes | 1.25× base input price |
| `AnthropicCacheControl.OneHour` | 1 hour | 2× base input price |
Cache writes are charged at a higher rate than regular input tokens, but cache reads are cheaper. See the [Anthropic prompt caching docs](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) for current pricing.
### Monitoring cache usage
Anthropic reports cache statistics in the response usage. These are accessible via the raw API response and can be observed through tracing or logging features.
| Field | Meaning |
| -------------------------- | ---------------------------------------- |
| `cacheReadInputTokens` | Tokens read from an existing cache entry |
| `cacheCreationInputTokens` | Tokens written to a new cache entry |
### Combining automatic and block-level caching
Both modes can be used simultaneously. Block-level `cacheControl` markers give you fine-grained control over breakpoint positions, while the request-level `cacheControl` in `AnthropicParams` handles the tail of the conversation automatically.
```
// Block-level: pin the system prompt in the 1-hour cache tier
// Automatic: let Anthropic manage breakpoints for the conversation tail
val params = AnthropicParams(cacheControl = AnthropicCacheControl.Default)
val prompt = prompt("combined", params = params) {
system("You are a helpful assistant...", AnthropicCacheControl.OneHour)
user("Hello!")
}
```
```
// Block-level: pin the system prompt in the 1-hour cache tier
// Automatic: let Anthropic manage breakpoints for the conversation tail
AnthropicParams params = new AnthropicParams(
AnthropicCacheControl.Default.INSTANCE
);
Prompt prompt = Prompt.builder("combined")
.system("You are a helpful assistant...", AnthropicCacheControl.OneHour.INSTANCE)
.user("Hello!")
.build()
.withParams(params);
```
______________________________________________________________________
## Amazon Bedrock
Amazon Bedrock uses a block-level caching model via the Converse API. When `cacheControl` is set on a message or tool, Bedrock inserts a `CachePoint` block immediately after the annotated element.
Note
Bedrock prompt caching is a JVM-only feature, as the Bedrock client itself is JVM-only.
### System messages
```
val prompt = prompt("assistant") {
// Cache the system prompt using the default TTL
system("You are a knowledgeable assistant...", BedrockCacheControl.Default)
user("What is prompt caching?")
}
val response = client.execute(prompt, BedrockModels.AnthropicClaude4Sonnet)
println(response)
```
```
Prompt prompt = Prompt.builder("assistant")
// Cache the system prompt using the default TTL
.system("You are a knowledgeable assistant...", BedrockCacheControl.Default.INSTANCE)
.user("What is prompt caching?")
.build();
```
### User and assistant messages
```
val prompt = prompt("conversation") {
system("You are a helpful assistant.")
// Cache after the large context message
user("Here is the document: ...", BedrockCacheControl.FiveMinutes)
assistant(listOf(MessagePart.Text("I have read the document.")))
user("Summarize it.")
}
val response = client.execute(prompt, BedrockModels.AnthropicClaude4Sonnet)
println(response)
```
```
Prompt prompt = Prompt.builder("conversation")
.system("You are a helpful assistant.")
// Cache after the large context message
.user("Here is the document: ...", BedrockCacheControl.FiveMinutes.INSTANCE)
.assistant("I have read the document.", BedrockCacheControl.Default.INSTANCE)
.user("Summarize it.")
.build();
```
### Tool definitions
```
val searchTool = ToolDescriptor(
name = "web_search",
description = "Search the web for information.",
requiredParameters = listOf(
ToolParameterDescriptor("query", "Search query", ToolParameterType.String)
),
// Cache all tool definitions up to and including this one
cacheControl = BedrockCacheControl.Default
)
```
```
ToolDescriptor searchTool = new ToolDescriptor(
"web_search",
"Search the web for information.",
List.of(
new ToolParameterDescriptor("query", "Search query", ToolParameterType.String.INSTANCE)
),
Collections.emptyList(),
// Cache all tool definitions up to and including this one
BedrockCacheControl.Default.INSTANCE
);
```
### Cache TTL options
| Option | TTL |
| --------------------------------- | --------------------------------------- |
| `BedrockCacheControl.Default` | Provider default (no explicit TTL sent) |
| `BedrockCacheControl.FiveMinutes` | 5 minutes |
| `BedrockCacheControl.OneHour` | 1 hour |
See the [Amazon Bedrock prompt caching docs](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html) for supported models and pricing.
______________________________________________________________________
## Choosing a caching strategy
| Situation | Recommended approach |
| ------------------------------------------------- | --------------------------------------------------------------- |
| Multi-turn chat with a large, fixed system prompt | Anthropic automatic caching or Bedrock block-level on system |
| Stable tool definitions reused across requests | Block-level `cacheControl` on the last tool definition |
| Long document passed as user context | Block-level `cacheControl` on the user message |
| Arbitrary multi-turn conversation (Anthropic) | Automatic caching via `AnthropicParams.cacheControl` |
| Need 1-hour cache retention | `AnthropicCacheControl.OneHour` / `BedrockCacheControl.OneHour` |
# Running prompts
# LLM clients
LLM clients are designed for direct interaction with LLM providers. Each client implements the [`LLMClient`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/ai.koog.prompt.executor.clients/-l-l-m-client/index.html) interface, which provides methods for executing prompts and streaming responses.
You can use an LLM client when you work with a single LLM provider and don't need advanced lifecycle management. If you need to manage multiple LLM providers, use a [prompt executor](../prompt-executors/).
The table below shows the available LLM clients and their capabilities.
| LLM provider | LLMClient | Tool calling | Streaming | Multiple choices | Embeddings | Moderation | Model listing | Notes |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------- | ---------------- | ---------- | ----------- | ------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [OpenAI](https://platform.openai.com/docs/overview) | [OpenAILLMClient](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openai-client/ai.koog.prompt.executor.clients.openai/-open-a-i-l-l-m-client/index.html) | ✓ | ✓ | ✓ | ✓ | ✓[1](#fn:1) | ✓ | |
| [Anthropic](https://www.anthropic.com/) | [AnthropicLLMClient](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/prompt-executor-anthropic-client/ai.koog.prompt.executor.clients.anthropic/-anthropic-l-l-m-client/index.html) | ✓ | ✓ | - | - | - | - | - |
| [Google](https://ai.google.dev/) β | [GoogleLLMClient](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/ai.koog.prompt.executor.clients.google/-google-l-l-m-client/index.html) | ✓ | ✓ | ✓ | ✓ | - | ✓ | - |
| [DeepSeek](https://www.deepseek.com/) β | [DeepSeekLLMClient](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/prompt-executor-deepseek-client/ai.koog.prompt.executor.clients.deepseek/-deep-seek-l-l-m-client/index.html) | ✓ | ✓ | ✓ | - | - | ✓ | OpenAI-compatible chat client. |
| [OpenRouter](https://openrouter.ai/) | [OpenRouterLLMClient](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openrouter-client/ai.koog.prompt.executor.clients.openrouter/-open-router-l-l-m-client/index.html) | ✓ | ✓ | ✓ | - | - | ✓ | OpenAI-compatible router client. |
| [Amazon Bedrock](https://aws.amazon.com/bedrock/) | [BedrockLLMClient](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/prompt-executor-bedrock-client/ai.koog.prompt.executor.clients.bedrock/-bedrock-l-l-m-client/index.html) | ✓ | ✓ | - | ✓ | ✓[2](#fn:2) | - | JVM-only AWS SDK client that supports multiple model families. |
| [Mistral](https://mistral.ai/) β | [MistralAILLMClient](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/prompt-executor-mistralai-client/ai.koog.prompt.executor.clients.mistralai/-mistral-a-i-l-l-m-client/index.html) | ✓ | ✓ | ✓ | ✓ | ✓[3](#fn:3) | ✓ | OpenAI-compatible client. |
| [Alibaba](https://www.alibabacloud.com/en?_p_lc=1) β | [DashScopeLLMClient](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/prompt-executor-dashscope-client/ai.koog.prompt.executor.clients.dashscope/-dashscope-l-l-m-client/index.html) | ✓ | ✓ | ✓ | - | - | ✓ | OpenAI-compatible client that exposes provider-specific parameters (`enableSearch`, `parallelToolCalls`, `enableThinking`). |
| [Ollama](https://ollama.com/) | [OllamaClient](https://api.koog.ai/prompt/prompt-executor/prompt-executor-clients/prompt-executor-ollama-client/ai.koog.prompt.executor.ollama.client/-ollama-client/index.html) | ✓ | ✓ | - | ✓ | ✓ | - | Local server client with model management APIs. |
## Running a prompt
To run a prompt using an LLM client, perform the following:
1. Create an LLM client that handles the connection between your application and LLM providers.
1. Call the `execute()` method with the prompt and LLM as arguments.
Here is an example that uses `OpenAILLMClient` to run prompts:
```
fun main() = runBlocking {
// Create an OpenAI client
val apiKey = System.getenv("OPENAI_API_KEY")
val client = OpenAILLMClient(apiKey)
// Create a prompt
val prompt = prompt("prompt_name", LLMParams()) {
// Add a system message to set the context
system("You are a helpful assistant.")
// Add a user message
user("Tell me about Kotlin")
// You can also add assistant messages for few-shot examples
assistant("Kotlin is a modern programming language...")
// Add another user message
user("What are its key features?")
}
// Run the prompt
val response = client.execute(prompt, OpenAIModels.Chat.GPT4o)
// Print the response
println(response)
}
```
```
// Create an OpenAI client
String apiKey = System.getenv("OPENAI_API_KEY");
OpenAILLMClient client = openAIClient(apiKey);
// Create a prompt
Prompt prompt = Prompt.builder("prompt_name")
// Add a system message to set the context
.system("You are a helpful assistant.")
// Add a user message
.user("Tell me about Kotlin")
// You can also add assistant messages for few-shot examples
.assistant("Kotlin is a modern programming language...")
// Add another user message
.user("What are its key features?")
.build();
// Run the prompt
List response = client.execute(prompt, OpenAIModels.Chat.GPT4o, Collections.emptyList());
// Print the response
System.out.println(response);
client.close();
```
## Streaming responses
Note
Available for all LLM clients.
When you need to process responses as they are generated, you can use the `executeStreaming()` method in Kotlin or `executeStreamingWithPublisher()` in Java to stream the model output.
The streaming API provides different frame types:
- **Delta frames** (`TextDelta`, `ReasoningDelta`, `ToolCallDelta`) — incremental content that arrives in chunks
- **Complete frames** (`TextComplete`, `ReasoningComplete`, `ToolCallComplete`) — full content after all deltas are received
- **End frame** (`End`) — signals stream completion with finish reason
For models that support reasoning (such as Claude Sonnet 4.5 or GPT-o1), reasoning frames will be emitted during streaming. See the [Streaming API documentation](../../streaming-api/) for more details on working with frames.
```
// Set up the OpenAI client with your API key
val token = System.getenv("OPENAI_API_KEY")
val client = OpenAILLMClient(token)
val response = client.executeStreaming(
prompt = prompt("stream_demo") { user("Stream this response in short chunks.") },
model = OpenAIModels.Chat.GPT4_1
)
response.collect { frame ->
when (frame) {
is StreamFrame.TextDelta -> print(frame.text)
is StreamFrame.ReasoningDelta -> print("[Reasoning] ${frame.text}")
is StreamFrame.ToolCallComplete -> println("\nTool call: ${frame.name}")
is StreamFrame.End -> println("\n[done] Reason: ${frame.finishReason}")
else -> {} // Handle other frame types if needed
}
}
```
```
// Set up the OpenAI client with your API key
String token = System.getenv("OPENAI_API_KEY");
OpenAILLMClient client = openAIClient(token);
Prompt prompt = Prompt.builder("stream_demo")
.user("Stream this response in short chunks.")
.build();
Publisher response = client.executeStreamingWithPublisher(prompt, OpenAIModels.Chat.GPT4_1);
// Subscribe to the Publisher to consume frames
response.subscribe(new Subscriber() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription s) {
this.subscription = s;
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(StreamFrame frame) {
switch (frame) {
case StreamFrame.TextDelta delta ->
System.out.print(delta.getText());
case StreamFrame.ReasoningDelta reasoning ->
System.out.print("[Reasoning] " + reasoning.getText());
case StreamFrame.ToolCallComplete toolCall ->
System.out.println("\nTool call: " + toolCall.getName());
case StreamFrame.End end ->
System.out.println("\n[done] Reason: " + end.getFinishReason());
default -> {} // Handle other frame types
}
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onComplete() { }
});
```
## Multiple choices
Note
Available for all LLM clients except `GoogleLLMClient`, `BedrockLLMClient`, and `OllamaClient`
You can request multiple alternative responses from the model in a single call by using the `executeMultipleChoices()` method. It requires additionally specifying the [`numberOfChoices`](../prompt-creation/#prompt-parameters) LLM parameter in the prompt being executed.
```
fun main() = runBlocking {
val apiKey = System.getenv("OPENAI_API_KEY")
val client = OpenAILLMClient(apiKey)
val choices = client.executeMultipleChoices(
prompt = prompt("n_best", params = LLMParams(numberOfChoices = 3)) {
system("You are a creative assistant.")
user("Give me three different opening lines for a story.")
},
model = OpenAIModels.Chat.GPT4o
)
choices.forEachIndexed { i, choice ->
val text = choice.parts.filterIsInstance().joinToString(" ") { it.text }
println("Line #${i + 1}: $text")
}
}
```
```
String apiKey = System.getenv("OPENAI_API_KEY");
OpenAILLMClient client = openAIClient(apiKey);
// Configure parameters (LLMParams constructor requires all 8 arguments in Java)
LLMParams params = new LLMParams(
null, // temperature
null, // maxTokens
3, // numberOfChoices
null, // speculation
null, // schema
null, // toolChoice
null, // user
null // additionalProperties
);
Prompt prompt = Prompt.builder("n_best")
.system("You are a creative assistant.")
.user("Give me three different opening lines for a story.")
.build()
.withParams(params);
// LLMChoice is a type alias for List
List> choices = client.executeMultipleChoices(
prompt,
OpenAIModels.Chat.GPT4o
);
for (int i = 0; i < choices.size(); i++) {
List choice = choices.get(i);
StringBuilder text = new StringBuilder();
for (Message.Response msg : choice) {
text.append(msg.getContent()).append(" ");
}
System.out.println("Line #" + (i + 1) + ": " + text.toString().trim());
}
```
## Listing available models
Note
Available for all LLM clients except `AnthropicLLMClient`, `BedrockLLMClient`, and `OllamaClient`.
To get a list of available model IDs supported by the LLM client, use the `models()` method:
```
fun main() = runBlocking {
val apiKey = System.getenv("OPENAI_API_KEY")
val client = OpenAILLMClient(apiKey)
val models: List = client.models()
models.forEach { println(it.id) }
}
```
```
String apiKey = System.getenv("OPENAI_API_KEY");
OpenAILLMClient client = openAIClient(apiKey);
List models = client.models();
for (LLModel model : models) {
System.out.println(model.getId());
}
```
## Embeddings
Note
Available for `OpenAILLMClient`, `GoogleLLMClient`, `BedrockLLMClient`, `MistralAILLMClient`, and `OllamaClient`.
You convert text into embedding vectors using the `embed()` method. Choose an embedding model and pass your text to this method:
```
fun main() = runBlocking {
val apiKey = System.getenv("OPENAI_API_KEY")
val client = OpenAILLMClient(apiKey)
val embedding = client.embed(
text = "This is a sample text for embedding",
model = OpenAIModels.Embeddings.TextEmbedding3Large
)
println("Embedding size: ${embedding.size}")
}
```
## Moderation
Note
Available for the following LLM clients: `OpenAILLMClient`, `BedrockLLMClient`, `MistralAILLMClient`, `OllamaClient`.
You can use the `moderate()` method with a moderation model to check whether a prompt contains inappropriate content:
```
fun main() = runBlocking {
val apiKey = System.getenv("OPENAI_API_KEY")
val client = OpenAILLMClient(apiKey)
val result = client.moderate(
prompt = prompt("moderation") {
user("This is a test message that may contain offensive content.")
},
model = OpenAIModels.Moderation.Omni
)
println(result)
}
```
```
String apiKey = System.getenv("OPENAI_API_KEY");
OpenAILLMClient client = openAIClient(apiKey);
Prompt prompt = Prompt.builder("moderation")
.user("This is a test message that may contain offensive content.")
.build();
ModerationResult result = client.moderate(prompt, OpenAIModels.Moderation.Omni);
System.out.println(result);
```
## Integration with prompt executors
[Prompt executors](../prompt-executors/) wrap LLM clients and provide additional functionality, such as routing, fallbacks, and unified usage across providers. They are recommended for production use, as they offer flexibility when working with multiple providers.
______________________________________________________________________
1. Supports moderation via the OpenAI Moderation API. [↩](#fnref:1 "Jump back to footnote 1 in the text")
1. Moderation requires Guardrails configuration. [↩](#fnref:2 "Jump back to footnote 2 in the text")
1. Supports moderation via the Mistral `v1/moderations` endpoint. [↩](#fnref:3 "Jump back to footnote 3 in the text")
# HTTP clients
Every LLM client in Koog expects a [`KoogHttpClient`](api:http-client-core::ai.koog.http.client.KoogHttpClient) — the abstract HTTP contract the framework uses to talk to providers. You hand one in at construction.
You can build that `KoogHttpClient` yourself, but it's real work: each provider has its own base URL, auth header shape, content-type, and SSE conventions. Getting all of that right per provider is exactly what [`KoogHttpClient.Factory`](api:http-client-core::ai.koog.http.client.KoogHttpClient.Factory) exists to spare you. You pass in a `Factory` and the provider client calls `Factory.create(...)` with the parameters that fit its API.
Four backend factories ship out of the box — Ktor, the JDK `HttpClient`, OkHttp, and Spring's `WebClient` — and you can implement your own.
## How it works
One factory works for any provider: pick a backend once and use it across clients.
```
fun main() {
val factory = KtorKoogHttpClient.Factory()
val openai = OpenAILLMClient(
apiKey = System.getenv("OPENAI_API_KEY"),
settings = OpenAIClientSettings(),
httpClientFactory = factory,
)
val anthropic = AnthropicLLMClient(
apiKey = System.getenv("ANTHROPIC_API_KEY"),
settings = AnthropicClientSettings(),
httpClientFactory = factory,
)
}
```
```
import ai.koog.http.client.ktor.KtorKoogHttpClient;
import ai.koog.prompt.executor.clients.anthropic.AnthropicClientSettings;
import ai.koog.prompt.executor.clients.anthropic.AnthropicLLMClient;
import ai.koog.prompt.executor.clients.openai.OpenAIClientSettings;
import ai.koog.prompt.executor.clients.openai.OpenAILLMClient;
KtorKoogHttpClient.Factory factory = new KtorKoogHttpClient.Factory();
OpenAILLMClient openai = new OpenAILLMClient(
System.getenv("OPENAI_API_KEY"),
new OpenAIClientSettings(),
factory
);
AnthropicLLMClient anthropic = new AnthropicLLMClient(
System.getenv("ANTHROPIC_API_KEY"),
new AnthropicClientSettings(),
factory
);
```
## Supported HTTP client flavors
| Module | Notes |
| ------------------------------------------------------------------- | --------------------------------------------- |
| [`http-client-ktor`](api:http-client-ktor:) | The only backend usable from non-JVM targets. |
| [`http-client-java`](api:http-client-java:) | Wraps the JDK 11+ `java.net.http.HttpClient`. |
| [`http-client-okhttp`](api:http-client-okhttp:) | Backed by OkHttp. Android-friendly. |
| [`http-client-spring-webclient`](api:http-client-spring-webclient:) | Backed by Spring `WebClient`. |
## Convenience APIs and factory auto-discovery
On JVM and Android, you can construct each LLM client without passing a factory explicitly.
Behind the scenes, [`HttpClientFactoryResolver`](api:http-client-core::ai.koog.http.client.HttpClientFactoryResolver) uses `java.util.ServiceLoader` to resolve `KoogHttpClient.Factory` from the runtime classpath:
- Every backend module provides a `ServiceLoader` registration.
- Resolution succeeds only when exactly one factory is visible on the runtime classpath.
- `prompt-executor-llms-all` declares `http-client-ktor` as a `runtimeOnly` dependency, so you get Ktor by default without compile-time exposure to that module.
- `simpleExecutor(apiKey)` and `PromptExecutorBuilder.(apiKey)` use the same resolution path.
```
fun main() {
val apiKey = System.getenv("OPENAI_API_KEY")
val client = OpenAILLMClient(apiKey)
val executor = simpleOpenAIExecutor(apiKey)
}
```
```
import static ai.koog.prompt.executor.clients.openai.OpenAIClientFactory.openAIClient;
import static ai.koog.prompt.executor.llms.all.SimplePromptExecutors.simpleOpenAIExecutor;
String apiKey = System.getenv("OPENAI_API_KEY");
OpenAILLMClient client = openAIClient(apiKey);
PromptExecutor executor = simpleOpenAIExecutor(apiKey);
```
Auto-discovery is not supported on KMP at the moment, so the convenience methods are not available outside the JVM either. From `commonMain`, pass a `Factory` explicitly.
### Auto-discovery gotchas
- **Zero backends on the runtime classpath** → `IllegalStateException` on first resolution. Add a backend module to the runtime classpath, or pass a `Factory` explicitly.
- **Two or more backends** → same exception; the message names the providers it found. Exclude all but one with Gradle (`exclude(module = "http-client-ktor")` on the offending dependency) or pass a `Factory` explicitly at the call site.
## Custom backends
Any class implementing `KoogHttpClient.Factory` works. To make it auto-discoverable on the JVM, register it as a `ServiceLoader` provider:
```
src/main/resources/META-INF/services/ai.koog.http.client.KoogHttpClient$Factory
```
The file contains a single line: the fully qualified name of your factory class. The literal `$` (separator for the nested `Factory` class) is correct — the file is `KoogHttpClient$Factory`, not `KoogHttpClient.Factory`.
If you don't want auto-discovery, skip the registration and pass your factory explicitly everywhere.
# Prompt executors
Prompt executors provide a higher-level abstraction that lets you manage the lifecycle of one or multiple LLM clients. You can work with multiple LLM providers through a unified interface, abstracting from provider-specific details, with dynamic switching between them and fallbacks.
## Executor types
Koog provides three main types of prompt executors that implement the [`PromptExecutor`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-model/ai.koog.prompt.executor.model/-prompt-executor/index.html) interface:
| Type | Class | Description |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Single-provider | [`SingleLLMPromptExecutor`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-model/ai.koog.prompt.executor.llms/-single-l-l-m-prompt-executor/index.html) | Wraps a single LLM client for one provider. Use this executor if your agent only requires switching between models within a single LLM provider. |
| Multi-provider | [`MultiLLMPromptExecutor`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-model/ai.koog.prompt.executor.llms/-multi-l-l-m-prompt-executor/index.html) | Wraps multiple LLM clients and routes calls based on the LLM provider. It can optionally use a configured fallback provider and LLM when the requested client is unavailable. Use this executor if your agent needs to switch between LLMs from different providers. |
| Routing | [`RoutingLLMPromptExecutor`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-model/ai.koog.prompt.executor.llms/-routing-l-l-m-prompt-executor/index.html) | Distributes requests to a given LLM model across multiple client instances using routing strategies. Use this executor to avoid rate limits, improve throughput, and implement failover strategies with load balancing. |
## Creating a single-provider executor
To create a prompt executor for a specific LLM provider, perform the following:
1. Configure an LLM client for a specific provider with the corresponding API key.
1. Create a prompt executor using [`MultiLLMPromptExecutor`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-model/ai.koog.prompt.executor.llms/-multi-l-l-m-prompt-executor/index.html).
Here is an example:
```
val openAIClient = OpenAILLMClient(System.getenv("OPENAI_API_KEY"))
val promptExecutor = MultiLLMPromptExecutor(openAIClient)
```
```
OpenAILLMClient openAIClient = openAIClient(System.getenv("OPENAI_API_KEY"));
MultiLLMPromptExecutor promptExecutor = new MultiLLMPromptExecutor(openAIClient);
```
## Creating a multi-provider executor
To create a prompt executor that works with multiple LLM providers, do the following:
1. Configure clients for the required LLM providers with the corresponding API keys.
1. Pass the configured clients to the [`MultiLLMPromptExecutor`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-model/ai.koog.prompt.executor.llms/-multi-l-l-m-prompt-executor/index.html) class constructor to create a prompt executor with multiple LLM providers.
```
val openAIClient = OpenAILLMClient(System.getenv("OPENAI_API_KEY"))
val ollamaClient = OllamaClient()
val multiExecutor = MultiLLMPromptExecutor(
LLMProvider.OpenAI to openAIClient,
LLMProvider.Ollama to ollamaClient
)
```
```
OpenAILLMClient openAIClient = openAIClient(System.getenv("OPENAI_API_KEY"));
OllamaClient ollamaClient = ollamaClient();
MultiLLMPromptExecutor promptExecutor = new MultiLLMPromptExecutor(openAIClient, ollamaClient);
```
## Creating a routing executor
Experimental API
Routing capabilities are experimental and may change in future releases. To use them, opt in with `@OptIn(ExperimentalRoutingApi::class)`.
To create a prompt executor that distributes requests across multiple LLM client instances using routing strategies, do the following:
1. Configure multiple client instances (they can be for the same or different LLM providers) with the corresponding API keys.
1. Create a router using a routing strategy, such as [`RoundRobinRouter`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-model/ai.koog.prompt.executor.llms/-round-robin-router/index.html).
1. Pass the router to the [`RoutingLLMPromptExecutor`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-model/ai.koog.prompt.executor.llms/-routing-l-l-m-prompt-executor/index.html) class constructor.
This is useful for avoiding rate limits, improving throughput, and implementing failover strategies.
```
// Create multiple client instances
val openAI1 = OpenAILLMClient(apiKey = "openai-key-1")
val openAI2 = OpenAILLMClient(apiKey = "openai-key-2")
val anthropic = AnthropicLLMClient(apiKey = "anthropic-key")
// Create router with round-robin strategy
val router = RoundRobinRouter(openAI1, openAI2, anthropic)
// Create routing executor
val routingExecutor = RoutingLLMPromptExecutor(router)
```
```
// Create multiple client instances
OpenAILLMClient openAI1 = openAIClient("openai-key-1");
OpenAILLMClient openAI2 = openAIClient("openai-key-2");
AnthropicLLMClient anthropic = anthropicClient("anthropic-key");
// Create router with round-robin strategy
RoundRobinRouter router = new RoundRobinRouter(openAI1, openAI2, anthropic);
// Create routing executor
RoutingLLMPromptExecutor routingExecutor = new RoutingLLMPromptExecutor(router);
```
When you execute prompts with this executor, requests to OpenAI models will alternate between `openAI1` and `openAI2` using the round-robin strategy. Requests to Anthropic models always go to the single `anthropic` client, as round-robin maintains an independent counter per provider.
You can also implement custom routing strategies by creating a class that implements the [`LLMClientRouter`](https://api.koog.ai/prompt/prompt-executor/prompt-executor-model/ai.koog.prompt.executor.llms/-l-l-m-client-router/index.html) interface.
## Pre-defined prompt executors
For faster setup, Koog provides ready-to-use executor implementations for common providers in both Kotlin and Java.
The following table includes the **pre-defined single-provider executors** that return `SingleLLMPromptExecutor` configured with a specific LLM client.
| LLM provider | Prompt executor | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| OpenAI | [simpleOpenAIExecutor](https://api.koog.ai/prompt/prompt-executor/prompt-executor-llms-all/ai.koog.prompt.executor.llms.all/simple-open-a-i-executor.html) | Wraps `OpenAILLMClient` that runs prompts with OpenAI models. |
| OpenAI | [simpleAzureOpenAIExecutor](https://api.koog.ai/prompt/prompt-executor/prompt-executor-llms-all/ai.koog.prompt.executor.llms.all/simple-azure-open-a-i-executor.html) | Wraps `OpenAILLMClient` configured for using Azure OpenAI Service. |
| Anthropic | [simpleAnthropicExecutor](https://api.koog.ai/prompt/prompt-executor/prompt-executor-llms-all/ai.koog.prompt.executor.llms.all/simple-anthropic-executor.html) | Wraps `AnthropicLLMClient` that runs prompts with Anthropic models. |
| Google | [simpleGoogleAIExecutor](https://api.koog.ai/prompt/prompt-executor/prompt-executor-llms-all/ai.koog.prompt.executor.llms.all/simple-google-a-i-executor.html) | Wraps `GoogleLLMClient` that runs prompts with Google models. |
| OpenRouter | [simpleOpenRouterExecutor](https://api.koog.ai/prompt/prompt-executor/prompt-executor-llms-all/ai.koog.prompt.executor.llms.all/simple-open-router-executor.html) | Wraps `OpenRouterLLMClient` that runs prompts with OpenRouter. |
| Amazon Bedrock | [simpleBedrockExecutor](https://api.koog.ai/prompt/prompt-executor/prompt-executor-llms-all/ai.koog.prompt.executor.llms.all/simple-bedrock-executor.html) | Wraps `BedrockLLMClient` that runs prompts with AWS Bedrock. |
| Amazon Bedrock | [simpleBedrockExecutorWithBearerToken](https://api.koog.ai/prompt/prompt-executor/prompt-executor-llms-all/ai.koog.prompt.executor.llms.all/simple-bedrock-executor-with-bearer-token.html) | Wraps `BedrockLLMClient` and uses the provided Bedrock API key to send requests. |
| Mistral | [simpleMistralAIExecutor](https://api.koog.ai/prompt/prompt-executor/prompt-executor-llms-all/ai.koog.prompt.executor.llms.all/simple-mistral-a-i-executor.html) | Wraps `MistralAILLMClient` that runs prompts with Mistral models. |
| Ollama | [simpleOllamaAIExecutor](https://api.koog.ai/prompt/prompt-executor/prompt-executor-llms-all/ai.koog.prompt.executor.llms.all/simple-ollama-a-i-executor.html) | Wraps `OllamaClient` that runs prompts with Ollama. |
Here is an example of creating a pre-defined executor:
```
// Create an OpenAI executor
val promptExecutor = simpleOpenAIExecutor("OPENAI_API_KEY")
```
```
// Create an OpenAI executor
PromptExecutor openAIExecutor = simpleOpenAIExecutor("OPENAI_API_KEY");
```
## Running a prompt
To run a prompt using a prompt executor, do the following:
1. Create a prompt executor.
1. Run the prompt with the specific LLM using the `execute()` method.
Here is an example:
```
// Create an OpenAI executor
val promptExecutor = simpleOpenAIExecutor("OPENAI_API_KEY")
// Execute a prompt
val response = promptExecutor.execute(
prompt = prompt("demo") { user("Summarize this.") },
model = OpenAIModels.Chat.GPT4o
)
```
```
// Create an OpenAI executor
PromptExecutor promptExecutor = simpleOpenAIExecutor("OPENAI_API_KEY");
// Create a prompt
Prompt prompt = Prompt.builder("demo")
.user("Summarize this.")
.build();
// Run the prompt
List response = promptExecutor.execute(prompt, OpenAIModels.Chat.GPT4o);
```
This will run the prompt with the `GPT4o` model and return the response.
Note
The prompt executors provide methods to run prompts using various capabilities, such as streaming, multiple choice generation, and content moderation. Since prompt executors wrap LLM clients, each executor supports the capabilities of the corresponding client. For details, refer to [LLM clients](../llm-clients/).
## Switching between providers
When you work with multiple LLM providers using `MultiLLMPromptExecutor`, you can switch between them. The process is as follows:
1. Create an LLM client instance for each provider you want to use.
1. Create a `MultiLLMPromptExecutor` that maps LLM providers to LLM clients.
1. Run a prompt with a model from the corresponding client passed as an argument to the `execute()` method. The prompt executor will use the corresponding client based on the model provider to run the prompt.
Here is an example of switching between providers:
```
// Create LLM clients for OpenAI, Anthropic, and Google providers
val openAIClient = OpenAILLMClient("OPENAI_API_KEY")
val anthropicClient = AnthropicLLMClient("ANTHROPIC_API_KEY")
val googleClient = GoogleLLMClient("GOOGLE_API_KEY")
// Create a MultiLLMPromptExecutor that maps LLM providers to LLM clients
val executor = MultiLLMPromptExecutor(
LLMProvider.OpenAI to openAIClient,
LLMProvider.Anthropic to anthropicClient,
LLMProvider.Google to googleClient
)
// Create a prompt
val p = prompt("demo") { user("Summarize this.") }
// Run the prompt with an OpenAI model; the prompt executor automatically switches to the OpenAI client
val openAIResult = executor.execute(p, OpenAIModels.Chat.GPT4o)
// Run the prompt with an Anthropic model; the prompt executor automatically switches to the Anthropic client
val anthropicResult = executor.execute(p, AnthropicModels.Sonnet_4_5)
```
```
// Create LLM clients for OpenAI, Anthropic, and Google providers
OpenAILLMClient openAIClient = openAIClient("OPENAI_API_KEY");
AnthropicLLMClient anthropicClient = anthropicClient("ANTHROPIC_API_KEY");
GoogleLLMClient googleClient = googleClient("GOOGLE_API_KEY");
// Create a MultiLLMPromptExecutor that maps LLM providers to LLM clients
MultiLLMPromptExecutor promptExecutor = new MultiLLMPromptExecutor(
Map.of(
LLMProvider.OpenAI, openAIClient,
LLMProvider.Anthropic, anthropicClient,
LLMProvider.Google, googleClient
)
);
// Create a prompt
Prompt prompt = Prompt.builder("demo")
.user("Summarize this.")
.build();
// Run the prompt with an OpenAI model; the prompt executor automatically switches to the OpenAI client
List openAIResult = promptExecutor.execute(prompt, OpenAIModels.Chat.GPT4o);
// Run the prompt with an Anthropic model; the prompt executor automatically switches to the Anthropic client
List anthropicResult = promptExecutor.execute(prompt, AnthropicModels.Sonnet_4_5);
```
You can optionally configure a fallback LLM provider and model to use when the requested client is unavailable. For details, refer to [Configuring fallbacks](#configuring-fallbacks).
## Configuring fallbacks
Multi-provider and routing prompt executors can be configured to use a fallback LLM provider and model when the requested LLM client is unavailable.
To configure the fallback mechanism, pass fallback settings when creating a `MultiLLMPromptExecutor` or `RoutingLLMPromptExecutor`:
```
val openAIClient = OpenAILLMClient(System.getenv("OPENAI_API_KEY"))
val ollamaClient = OllamaClient()
val multiExecutor = MultiLLMPromptExecutor(
LLMProvider.OpenAI to openAIClient,
LLMProvider.Ollama to ollamaClient,
fallback = MultiLLMPromptExecutor.FallbackPromptExecutorSettings(
fallbackProvider = LLMProvider.Ollama,
fallbackModel = OllamaModels.Meta.LLAMA_3_2
)
)
```
```
OpenAILLMClient openAIClient = openAIClient(System.getenv("OPENAI_API_KEY"));
OllamaClient ollamaClient = ollamaClient();
MultiLLMPromptExecutor multiExecutor = new MultiLLMPromptExecutor(
Map.of(
LLMProvider.OpenAI, openAIClient,
LLMProvider.Ollama, ollamaClient
),
new MultiLLMPromptExecutor.FallbackPromptExecutorSettings(
LLMProvider.Ollama,
OllamaModels.Meta.LLAMA_3_2
)
);
```
If you pass a model from an LLM provider that is not included in the `MultiLLMPromptExecutor`, the prompt executor will use the fallback model:
```
// Create a prompt
val p = prompt("demo") { user("Summarize this") }
// If you pass a Google model, the prompt executor will use the fallback model, as the Google client is not included
val response = multiExecutor.execute(p, GoogleModels.Gemini2_5Pro)
```
```
// Create a prompt
Prompt p = Prompt.builder("demo")
.user("Summarize this")
.build();
// If you pass a Google model, the prompt executor will use the fallback model, as the Google client is not included
List response = multiExecutor.execute(p, GoogleModels.Gemini2_5Pro);
```
Note
Fallbacks are available for the `execute()` and `executeMultipleChoices()` methods only.
# Tools
# Overview
Agents use tools to perform specific tasks or access external systems.
## Tool workflow
The Koog framework offers the following workflow for working with tools in Kotlin and Java:
1. Create a custom tool or use one of the built-in tools.
1. Add the tool to a tool registry.
1. Pass the tool registry to an agent.
1. Use the tool with the agent.
### Available tool types
There are three types of tools in the Koog framework:
- Built-in tools that provide functionality for agent-user interaction and conversation management. For details, see [Built-in tools](built-in-tools/).
- Annotation-based custom tools that let you expose functions as tools to LLMs. For details, see [Annotation-based tools](annotation-based-tools/).
- Custom tools that let you control tool parameters, metadata, execution logic, and how it is registered and invoked. For details, see [Class-based tools](class-based-tools/).
### Tool registry
Before you can use a tool in an agent, you must add it to a tool registry. The tool registry manages all tools available to the agent.
The key features of the tool registry:
- Organizes tools.
- Supports merging of multiple tool registries.
- Provides methods to retrieve tools by name or type.
To learn more, see [ToolRegistry](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool-registry/index.html).
Here is an example of how to create the tool registry and add the tool to it:
```
val toolRegistry = ToolRegistry {
tools(myTool)
}
```
```
// Create an instance of your ToolSet
MyToolSet myTool = new MyToolSet();
// Build the ToolRegistry and register tools from the ToolSet
ToolRegistry toolRegistry = ToolRegistry.builder()
.tools(myTool)
.build();
```
To merge multiple tool registries, do the following:
```
val firstToolRegistry = ToolRegistry {
tools(firstSampleTool)
}
val secondToolRegistry = ToolRegistry {
tools(secondSampleTool)
}
val newRegistry = firstToolRegistry + secondToolRegistry
```
```
// Create instances of your ToolSets
FirstToolSet firstSampleTool = new FirstToolSet();
SecondToolSet secondSampleTool = new SecondToolSet();
// Build separate tool registries
ToolRegistry firstToolRegistry = ToolRegistry.builder()
.tools(firstSampleTool)
.build();
ToolRegistry secondToolRegistry = ToolRegistry.builder()
.tools(secondSampleTool)
.build();
ToolRegistry newRegistry = firstToolRegistry.plus(secondToolRegistry);
```
### Passing tools to an agent
To enable an agent to use a tool, you need to provide a tool registry that contains this tool as an argument when creating the agent:
```
// Agent initialization
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
systemPrompt = "You are a helpful assistant with strong mathematical skills.",
llmModel = OpenAIModels.Chat.GPT4o,
// Pass your tool registry to the agent
toolRegistry = toolRegistry
)
```
```
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")))
.systemPrompt("You are a helpful assistant with strong mathematical skills.")
.llmModel(OpenAIModels.Chat.GPT4o)
.toolRegistry(ToolRegistry.builder()
.tools(secondSampleTool)
.build()
)
.build();
```
### Calling tools
There are several ways to call tools within your agent code. The recommended approach is to use the provided methods in the agent context rather than calling tools directly, as this ensures proper handling of tool operation within the agent environment.
Tip
Ensure you have implemented proper [error handling](../features/agent-event-handlers/) in your tools to prevent agent failure.
The tools are called within a specific session context represented by `AIAgentLLMWriteSession`. It provides several methods for calling tools so that you can:
- Call a tool with the given arguments.
- Call a tool by its name and the given arguments.
- Call a tool by the provided tool class and arguments.
- Call a tool of the specified type with the given arguments.
- Call a tool that returns a raw string result.
For more details, the API reference for [AIAgentLLMWriteSession](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.agent.session/-a-i-agent-l-l-m-write-session/index.html).
#### Parallel tool calls
You can also call tools in parallel using the `toParallelToolCallsRaw` extension. For example:
```
@Serializable
data class Book(
val title: String,
val author: String,
val description: String
)
class BookTool() : SimpleTool(
argsType = typeToken(),
name = NAME,
description = "A tool to parse book information from Markdown"
) {
companion object {
const val NAME = "book"
}
override suspend fun execute(args: Book): String {
println("${args.title} by ${args.author}:\n ${args.description}")
return "Done"
}
}
val strategy = strategy("strategy-name") {
/*...*/
val myNode by node { _ ->
llm.writeSession {
flow {
emit(Book("Book 1", "Author 1", "Description 1"))
}.toParallelToolCallsRaw(BookTool::class).collect()
}
}
}
```
```
```
#### Calling tools from nodes
When building agent workflows with nodes, you can use special nodes to call tools:
- **nodeExecuteTool**: calls a single tool call and returns its result. For details, see [API-reference](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.extension/node-execute-tool.html).
- **nodeExecuteSingleTool** that calls a specific tool with the provided arguments. For details, see [API-reference](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.extension/node-execute-single-tool.html).
- **nodeExecuteMultipleTools** that performs multiple tool calls and returns their results. For details, see [API-reference](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.extension/node-execute-multiple-tools.html).
- **nodeLLMSendToolResult** that sends a tool result to the LLM and gets a response. For details, see [API-reference](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.extension/node-l-l-m-send-tool-result.html).
- **nodeLLMSendMultipleToolResults** that sends multiple tool results to the LLM. For details, see [API-reference](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.extension/node-l-l-m-send-multiple-tool-results.html).
## Using agents as tools
The framework provides the capability to convert any AI agent into a tool that can be used by other agents. This powerful feature enables you to create hierarchical agent architectures where specialized agents can be called as tools by higher-level orchestrating agents.
### Converting agents to tools
To convert an agent into a tool, use the `AIAgentService` and the `createAgentTool()` extension function:
```
// Create a specialized agent service, responsible for creating financial analysis agents.
val analysisAgentService = AIAgentService(
promptExecutor = simpleOpenAIExecutor(apiKey),
llmModel = OpenAIModels.Chat.GPT4o,
systemPrompt = "You are a financial analysis specialist.",
toolRegistry = analysisToolRegistry
)
// Create a tool that would run financial analysis agent once called.
val analysisAgentTool = analysisAgentService.createAgentTool(
agentName = "analyzeTransactions",
agentDescription = "Performs financial transaction analysis",
inputDescription = "Transaction analysis request",
inputType = typeToken(),
)
```
```
```
### Using agent tools in other agents
Once converted to a tool, you can add the agent tool to another agent's tool registry:
```
// Create a coordinator agent that can use specialized agents as tools
val coordinatorAgent = AIAgent(
promptExecutor = simpleOpenAIExecutor(apiKey),
llmModel = OpenAIModels.Chat.GPT4o,
systemPrompt = "You coordinate different specialized services.",
toolRegistry = ToolRegistry {
tool(analysisAgentTool)
// Add other tools as needed
}
)
```
```
```
### Agent tool execution
When an agent tool is called:
1. The arguments are deserialized according to the input descriptor.
1. The wrapped agent is executed with the deserialized input.
1. The agent's output is serialized and returned as the tool result.
### Benefits of agents as tools
- **Modularity**: Break complex workflows into specialized agents.
- **Reusability**: Use the same specialized agent across multiple coordinator agents.
- **Separation of concerns**: Each agent can focus on its specific domain.
# Built-in tools
Koog provides built-in tools for Kotlin and Java to help you quickly prototype and experiment with agent-user interaction. These tools are not intended for production use. To use them, add `ai.koog:agents-ext` to your dependencies. The following built-in tools are available:
| Tool | Name | Description |
| ----------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| SayToUser | `__say_to_user__` | Lets the agent send a message to the user. It prints the agent message to the console with the `Agent says:` prefix. |
| AskUser | `__ask_user__` | Lets the agent ask the user for input. It prints the agent message to the console and waits for user response. |
| ExitTool | `__exit__` | Lets the agent finish the conversation and terminate the session. |
| ReadFileTool | `__read_file__` | Reads text file with optional line range selection. Returns formatted content with metadata using 0-based line indexing. |
| EditFileTool | `__edit_file__` | Makes a single, targeted text replacement in a file; can also create new files or fully replace contents. |
| ListDirectoryTool | `__list_directory__` | Lists directory contents as a hierarchical tree with optional depth control and glob filtering. |
| WriteFileTool | `__write_file__` | Writes text content to a file (creating parent directories if needed). |
## Registering built-in tools
Like any other tool, a built-in tool must be added to the tool registry to become available for an agent. Here is an example:
```
// Create a tool registry with all built-in tools
val toolRegistry = ToolRegistry {
tool(SayToUser)
tool(AskUser)
tool(ExitTool)
tool(ReadFileTool(JVMFileSystemProvider.ReadOnly))
tool(ListDirectoryTool(JVMFileSystemProvider.ReadOnly))
tool(WriteFileTool(JVMFileSystemProvider.ReadWrite))
}
// Pass the registry when creating an agent
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(apiToken),
systemPrompt = "You are a helpful assistant.",
llmModel = OpenAIModels.Chat.GPT4o,
toolRegistry = toolRegistry
)
```
You can create a comprehensive set of capabilities for your agent by combining built-in tools and custom tools within the same registry in both Kotlin and Java. To learn more about custom tools, see [Annotation-based tools](../annotation-based-tools/) and [Class-based tools](../class-based-tools/).
# Annotation-based tools
Annotation-based tools provide a declarative way to expose functions and methods as tools for large language models (LLMs) in both Kotlin and Java. By using annotations, you can transform any function or method into a tool that LLMs can understand and use.
This approach is useful when you need to expose existing functionality to LLMs in Kotlin or Java without implementing tool descriptions manually.
Note
Annotation-based tools are JVM-only and not available for other platforms. For multiplatform support, use the [class-based tool API](../class-based-tools/).
## Key annotations
To start using annotation-based tools in your project, you need to understand the following key annotations:
| Annotation | Description |
| ----------------- | ----------------------------------------------------------------------- |
| `@Tool` | Marks functions that should be exposed as tools to LLMs. |
| `@LLMDescription` | Provides descriptive information about your tools and their components. |
## @Tool annotation
The `@Tool` annotation is used to mark functions (Kotlin) or methods (Java) that should be exposed as tools to LLMs. The functions and methods annotated with `@Tool` are collected by reflection from objects that implement the `ToolSet` interface. For details, see [Implement the ToolSet interface](#1-implement-the-toolset-interface).
### Definition
```
@Target(AnnotationTarget.FUNCTION)
public annotation class Tool(val customName: String = "")
```
### Parameters
| Name | Required | Description |
| ------------ | -------- | ---------------------------------------------------------------------------------------- |
| `customName` | No | Specifies a custom name for the tool. If not provided, the name of the function is used. |
### Usage
To mark a function or method as a tool, apply the `@Tool` annotation to this function or method in a class that implements the `ToolSet` interface:
```
class MyToolSet : ToolSet {
@Tool
fun myTool(): String {
// Tool implementation
return "Result"
}
@Tool(customName = "customToolName")
fun anotherTool(): String {
// Tool implementation
return "Result"
}
}
```
```
public class MyToolSet implements ToolSet {
@Tool
public String myTool() {
// Tool implementation
return "Result";
}
@Tool(customName = "customToolName")
public String anotherTool() {
// Tool implementation
return "Result";
}
}
```
## @LLMDescription annotation
The `@LLMDescription` annotation provides descriptive information about code elements (classes, functions, methods, parameters, and so on) to LLMs. This helps LLMs understand the purpose and usage of these elements.
### Definition
```
@Target(
AnnotationTarget.PROPERTY,
AnnotationTarget.CLASS,
AnnotationTarget.TYPE,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.FUNCTION
)
public annotation class LLMDescription(val description: String)
```
### Parameters
| Name | Required | Description |
| ------------- | -------- | ---------------------------------------------- |
| `description` | Yes | A string that describes the annotated element. |
### Usage
The `@LLMDescription` annotation can be applied at various levels. For example:
- Function level:
```
@Tool
@LLMDescription("Performs a specific operation and returns the result")
fun myTool(): String {
// Function implementation
return "Result"
}
```
```
@Tool
@LLMDescription(description = "Performs a specific operation and returns the result")
public String myTool() {
// Function implementation
return "Result";
}
```
- Parameter level:
```
@Tool
@LLMDescription("Processes input data")
fun processTool(
@LLMDescription("The input data to process")
input: String,
@LLMDescription("Optional configuration parameters")
config: String = ""
): String {
// Function implementation
return "Processed: $input with config: $config"
}
```
```
@Tool
@LLMDescription(description = "Processes input data")
public String processTool(
@LLMDescription(description = "The input data to process") String input,
@LLMDescription(description = "Optional configuration parameters") String config
) {
// Function implementation
return "Processed: " + input + " with config: " + config;
}
```
## Creating a tool
### 1. Implement the ToolSet interface
Create a class that implements the [`ToolSet`](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools.reflect/-tool-set/index.html) interface. This interface marks your class as a container for tools.
```
class MyFirstToolSet : ToolSet {
// Tools will go here
}
```
```
public class MyFirstToolSet implements ToolSet {
// Tools will go here
}
```
### 2. Add tool functions
Add functions or methods to your class and annotate them with `@Tool` to expose them as tools:
```
class MyFirstToolSet : ToolSet {
@Tool
fun getWeather(location: String): String {
// In a real implementation, you would call a weather API
return "The weather in $location is sunny and 72°F"
}
}
```
```
public class MyFirstToolSet implements ToolSet {
@Tool
public String getWeather(String location) {
// In a real implementation, you would call a weather API
return "The weather in " + location + " is sunny and 72°F";
}
}
```
### 3. Add descriptions
Add `@LLMDescription` annotations to provide context for the LLM:
```
@LLMDescription("Tools for getting weather information")
class MyFirstToolSet : ToolSet {
@Tool
@LLMDescription("Get the current weather for a location")
fun getWeather(
@LLMDescription("The city and state/country")
location: String
): String {
// In a real implementation, you would call a weather API
return "The weather in $location is sunny and 72°F"
}
}
```
```
@LLMDescription(description = "Tools for getting weather information")
public class MyFirstToolSet implements ToolSet {
@Tool
@LLMDescription(description = "Get the current weather for a location")
public String getWeather(
@LLMDescription(description = "The city and state/country") String location
) {
// In a real implementation, you would call a weather API
return "The weather in " + location + " is sunny and 72°F";
}
}
```
### 4. Use your tools with an agent
Now you can use your tools with an agent:
```
fun main() {
runBlocking {
// Create your tool set
val weatherTools = MyFirstToolSet()
// Create an agent with your tools
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(apiToken),
systemPrompt = "Provide weather information for a given location.",
llmModel = OpenAIModels.Chat.GPT4o,
toolRegistry = ToolRegistry {
tools(weatherTools)
}
)
// The agent can now use your weather tools
agent.run("What's the weather like in New York?")
}
}
```
```
String apiToken = System.getenv("OPENAI_API_KEY");
// Create your tool set
MyFirstToolSet weatherTools = new MyFirstToolSet();
ToolRegistry toolRegistry = ToolRegistry.builder()
.tools(weatherTools)
.build();
// Create an agent with your tools
AIAgent agent = AIAgent.builder()
.promptExecutor(simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")))
.systemPrompt("Provide weather information for a given location.")
.llmModel(OpenAIModels.Chat.GPT4o)
.toolRegistry(toolRegistry)
.build();
// The agent can now use your weather tools
String result = agent.run("What's the weather like in New York?");
System.out.println(result);
```
## Usage examples
Here are some real-world examples of tool annotations.
### Basic example: Switch controller
This example shows a simple tool set for controlling a switch:
```
@LLMDescription("Tools for controlling a switch")
class SwitchTools(val switch: Switch) : ToolSet {
@Tool
@LLMDescription("Switches the state of the switch")
fun switch(
@LLMDescription("The state to set (true for on, false for off)")
state: Boolean
): String {
switch.switch(state)
return "Switched to ${if (state) "on" else "off"}"
}
@Tool
@LLMDescription("Returns the current state of the switch")
fun switchState(): String {
return "Switch is ${if (switch.isOn()) "on" else "off"}"
}
}
```
```
public class Switch {
private boolean state;
public Switch(boolean state) {
this.state = state;
}
// "switch" is a reserved keyword in Java, so we use a different method name
public void setState(boolean state) {
this.state = state;
}
public boolean isOn() {
return state;
}
}
@LLMDescription(description = "Tools for controlling a switch")
public class SwitchTools implements ToolSet {
private final Switch sw;
public SwitchTools(Switch sw) {
this.sw = sw;
}
@Tool
@LLMDescription(description = "Switches the state of the switch")
public String switchStateTo(
@LLMDescription(description = "The state to set (true for on, false for off)") boolean state
) {
sw.setState(state);
return "Switched to " + (state ? "on" : "off");
}
@Tool
@LLMDescription(description = "Returns the current state of the switch")
public String switchState() {
return "Switch is " + (sw.isOn() ? "on" : "off");
}
}
```
When an LLM needs to control a switch, it can understand the following information from the provided description:
- The purpose and functionality of the tools.
- The required parameters for using the tools.
- The acceptable values for each parameter.
- The expected return values upon execution.
### Advanced example: Diagnostic tools
This example shows a more complex tool set for device diagnostics:
```
@LLMDescription("Tools for performing diagnostics and troubleshooting on devices")
class DiagnosticToolSet : ToolSet {
@Tool
@LLMDescription("Run diagnostic on a device to check its status and identify any issues")
fun runDiagnostic(
@LLMDescription("The ID of the device to diagnose")
deviceId: String,
@LLMDescription("Additional information for the diagnostic (optional)")
additionalInfo: String = ""
): String {
// Implementation
return "Diagnostic results for device $deviceId"
}
@Tool
@LLMDescription("Analyze an error code to determine its meaning and possible solutions")
fun analyzeError(
@LLMDescription("The error code to analyze (e.g., 'E1001')")
errorCode: String
): String {
// Implementation
return "Analysis of error code $errorCode"
}
}
```
```
@LLMDescription(description = "Tools for performing diagnostics and troubleshooting on devices")
public class DiagnosticToolSet implements ToolSet {
// Convenience overload (not exposed as a tool)
public String runDiagnostic(String deviceId) {
return runDiagnostic(deviceId, "");
}
@Tool
@LLMDescription(description = "Run diagnostic on a device to check its status and identify any issues")
public String runDiagnostic(
@LLMDescription(description = "The ID of the device to diagnose") String deviceId,
@LLMDescription(description = "Additional information for the diagnostic (optional)") String additionalInfo
) {
// Implementation
return "Diagnostic results for device " + deviceId;
}
@Tool
@LLMDescription(description = "Analyze an error code to determine its meaning and possible solutions")
public String analyzeError(
@LLMDescription(description = "The error code to analyze (e.g., 'E1001')") String errorCode
) {
// Implementation
return "Analysis of error code " + errorCode;
}
}
```
## Best practices
- **Provide clear descriptions**: write clear, concise descriptions that explain the purpose and behavior of tools, parameters, and return values.
- **Describe all parameters**: add `@LLMDescription` to all parameters to help LLMs understand what each parameter is for.
- **Use consistent naming**: use consistent naming conventions for tools and parameters to make them more intuitive.
- **Group related tools**: group related tools in the same `ToolSet` implementation and provide a class-level description.
- **Return informative results**: make sure tool return values provide clear information about the result of the operation.
- **Handle errors gracefully**: include error handling in your tools and return informative error messages.
- **Document default values**: when parameters have default values (Kotlin) or overloads (Java), document this in the description.
- **Keep tools focused**: Each tool should perform a specific, well-defined task rather than trying to do too many things.
## Troubleshooting common issues
When working with tool annotations, you might encounter some common issues.
### Tools not being recognized
If the agent does not recognize your tools, check the following:
- Your class implements the `ToolSet` interface.
- All tool functions or methods are annotated with `@Tool`.
- Tool functions or methods have appropriate return types (`String` is recommended for simplicity).
- Your tools are properly registered with the agent.
### Unclear tool descriptions
If the LLM does not use your tools correctly or misunderstands their purpose, try the following:
- Use primitive parameter types when possible (`String`, `Boolean`, `Int` in Kotlin, or `String`, `boolean`, `int` in Java).
- Clearly describe the expected format in the parameter description.
- For complex types, consider using `String` parameters with a specific format and parse them in your tool.
- Include examples of valid inputs in your parameter descriptions.
- Note that Java doesn't support default parameters. Use method overloading instead.
### Parameter type issues
If the LLM provides incorrect parameter types, try the following:
- Use simple parameter types when possible (`String`, `Boolean`, `Int`).
- Clearly describe the expected format in the parameter description.
- For complex types, consider using `String` parameters with a specific format and parse them in your tool.
- Include examples of valid inputs in your parameter descriptions.
### Performance issues
If your tools cause performance problems, try the following:
- Keep tool implementations lightweight.
- For resource-intensive operations, consider implementing asynchronous processing.
- Cache results when appropriate.
- Log tool usage to identify bottlenecks.
# Class-based tools
This section explains the API designed for scenarios that require enhanced flexibility and customized behavior. With this approach in Kotlin, you have full control over a tool, including its parameters, metadata, execution logic, and how it is registered and invoked. In Java, tools are created using annotation-based methods with reflection-based registration.
This level of control is ideal for creating sophisticated tools that extend basic use cases, enabling seamless integration into agent sessions and workflows.
This page describes how to implement a tool in both Kotlin and Java, manage tools through registries, call them, and use within node-based agent architectures.
Note
The API is multiplatform for Kotlin. Java tools are implemented using annotation-based methods and registered via reflection. This lets you use the same tools across different platforms in Kotlin, while Java provides full JVM interoperability.
## Tool implementation
The Koog framework provides the following approaches for implementing tools:
For Kotlin:
- Using the base class `Tool` for all tools. You should use this class when you need to return non-text results or require complete control over the tool behavior.
- Using the `SimpleTool` class that extends the base `Tool` class and simplifies the creation of tools that return text results. You should use this approach for scenarios where the tool only needs to return a text.
Both approaches use the same core components but differ in implementation and the results they return.
For Java:
- Using annotation-based methods (`@Tool` and `@LLMDescription`) with reflection-based registration. This is the recommended approach for Java interoperability, as subclassing Kotlin's `Tool` or `SimpleTool` from Java is not supported due to suspend function limitations.
### Tool class (Kotlin)
The [`Tool`](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool/index.html) abstract class is the base class for creating tools in Kotlin. It lets you create tools that accept specific argument types (`Args`) and return results of various types (`Result`).
Each tool consists of the following components:
| Component | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Args` | The serializable data class that defines arguments required for the tool. |
| `Result` | The serializable type of result that the tool returns. If you want to present tool results in a custom format, please inherit [ToolResult.TextSerializable](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool-result/-text-serializable/index.html) class and implement `textForLLM(): String` method |
| `argsSerializer` | The overridden variable that defines how the arguments for the tool are deserialized. See also [argsSerializer](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool/args-serializer.html). |
| `resultSerializer` | The overridden variable that defines how the result of the tool is deserialized. See also [resultSerializer](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool/result-serializer.html). If you chose to inherit [ToolResult.TextSerializable](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool-result/-text-serializable/index.html) consider using `ToolResultUtils.toTextSerializer()` |
| `descriptor` | The overridden variable that specifies tool metadata: - `name` - `description` - `requiredParameters` (empty by default) - `optionalParameters` (empty by default) See also [descriptor](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool/descriptor.html). |
| `execute()` | The function that implements the logic of the tool. It takes arguments of type `Args` and returns a result of type `Result`. See also execute(). |
Java Implementation
In Java, instead of subclassing `Tool`, use annotation-based methods with `@Tool` and `@LLMDescription`. The framework handles serialization and registration automatically through reflection. For more details, see [Annotation-based methods](#annotation-based-methods-java) below.
Tip
Ensure your tools have clear descriptions and well-defined parameter names to make it easier for the LLM to understand and use them properly. In Kotlin, use the `descriptor` property; in Java, use `@LLMDescription` annotations.
#### Usage example
Here is an example of a custom tool implementation using the `Tool` class that returns a numeric result:
```
// Implement a simple calculator tool that adds two digits
object CalculatorTool : Tool(
argsType = typeToken(),
resultType = typeToken(),
name = "calculator",
description = "A simple calculator that can add two digits (0-9)."
) {
// Arguments for the calculator tool
@Serializable
data class Args(
@property:LLMDescription("The first digit to add (0-9)")
val digit1: Int,
@property:LLMDescription("The second digit to add (0-9)")
val digit2: Int
) {
init {
require(digit1 in 0..9) { "digit1 must be a single digit (0-9)" }
require(digit2 in 0..9) { "digit2 must be a single digit (0-9)" }
}
}
// Function to add two digits
override suspend fun execute(args: Args): Int = args.digit1 + args.digit2
}
```
After implementing your tool, you need to add it to a tool registry and then use it with an agent. For details, see [Tool registry](../#tool-registry).
For more details, see [API reference](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool/index.html).
#### Reading the agent context from a tool
Tools that need the agent's full state (LLM context, run id, configuration, storage, ...) extend `AgentContextAwareTool` instead of `Tool`. The framework injects the live `AIAgentContext` driving the call, and the tool receives it as a typed parameter rather than reading it out of the argument schema.
```
// A tool that reads the live AIAgentContext driving the call.
object TracingCalculatorTool : AgentContextAwareTool(
argsType = typeToken(),
resultType = typeToken(),
name = "tracing_calculator",
description = "Adds two digits and emits a log line tagged with the agent run id."
) {
@Serializable
data class Args(
@property:LLMDescription("The first digit to add (0-9)")
val digit1: Int,
@property:LLMDescription("The second digit to add (0-9)")
val digit2: Int
)
override suspend fun execute(args: Args, context: AIAgentContext): Int {
val runId = context.runId
// ... use runId for cross-cutting context (logging, tracing, correlation)
return args.digit1 + args.digit2
}
}
```
`AgentContextAwareTool` is dispatched by the framework via a per-call `ToolCallMetadata` side channel that the framework manages on the tool's behalf. Invoking such a tool outside an agent run throws `IllegalStateException` because no `AIAgentContext` was injected; production code should always go through `ContextualAgentEnvironment`, and unit tests can supply the context explicitly via `ToolCallMetadata.of(AgentContextAwareTool.AgentContextKey to context)`.
#### Reading raw per-call metadata
A small number of tools want to read caller- or feature-contributed entries that are *not* the agent context (for example a distributed-tracing span id contributed by an observability feature). These tools extend `ToolBase` directly, which exposes the full `ToolCallMetadata` bag:
```
object SpanAwareCalculatorTool : ToolBase(
argsType = typeToken(),
resultType = typeToken(),
name = "span_aware_calculator",
description = "Adds two digits, propagating a tracing span id from caller or feature metadata."
) {
@Serializable
data class Args(
@property:LLMDescription("The first digit to add (0-9)")
val digit1: Int,
@property:LLMDescription("The second digit to add (0-9)")
val digit2: Int
)
override suspend fun execute(args: Args, metadata: ToolCallMetadata): Int {
val traceSpanId = metadata["trace.span.id"] as? String
// ... use traceSpanId for cross-cutting context (logging, tracing, correlation)
return args.digit1 + args.digit2
}
}
```
Callers can pass metadata through `SafeTool.execute(args, serializer, metadata)` or directly through `AIAgentEnvironment.executeTool(toolCall, metadata)`. Features can contribute metadata for every tool call during installation by calling `pipeline.provideToolCallMetadata(this) { eventContext -> mapOf(...) }`. Caller-supplied metadata always wins over feature contributions on key collision.
Existing tools that extend `Tool` and override `execute(args)` continue to work unchanged: the framework dispatches them through the same path and discards any `ToolCallMetadata`. To opt in to metadata, switch to `AgentContextAwareTool` (typed context access) or `ToolBase` (raw bag access).
### SimpleTool class (Kotlin)
The [`SimpleTool`](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-simple-tool/index.html) abstract class extends `Tool` and simplifies the creation of tools that return text results.
Each simple tool consists of the following components:
| Component | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Args` | The serializable data class that defines arguments required for the custom tool. |
| `argsSerializer` | The overridden variable that defines how the arguments for the tool are serialized. See also [argsSerializer](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool/args-serializer.html). |
| `descriptor` | The overridden variable that specifies tool metadata: - `name` - `description` - `requiredParameters` (empty by default) - `optionalParameters` (empty by default) See also [descriptor](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool/descriptor.html). |
| `doExecute()` | The overridden function that describes the main action performed by the tool. It takes arguments of type `Args` and returns a `String`. See also [doExecute()](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-simple-tool/do-execute.html). |
Java Implementation
In Java, the equivalent approach is to use annotation-based methods that return `String`. The framework automatically handles the text result wrapping. For more details, see [Annotation-based methods](#annotation-based-methods-java) below.
Tip
Ensure your tools have clear descriptions and well-defined parameter names to make it easier for the LLM to understand and use them properly. In Kotlin, use the `descriptor` and constructor parameters; in Java, use `@Tool` and `@LLMDescription` annotations.
#### Usage example
Here is an example of a custom tool implementation using `SimpleTool` in Kotlin:
```
// Create a tool that casts a string expression to a double value
object CastToDoubleTool : SimpleTool(
argsType = typeToken(),
name = "cast_to_double",
description = "casts the passed expression to double or returns 0.0 if the expression is not castable"
) {
// Define tool arguments
@Serializable
data class Args(
@property:LLMDescription("An expression to case to double")
val expression: String,
@property:LLMDescription("A comment on how to process the expression")
val comment: String
)
// Function that executes the tool with the provided arguments
override suspend fun execute(args: Args): String {
return "Result: ${castToDouble(args.expression)}, " + "the comment was: ${args.comment}"
}
// Function to cast a string expression to a double value
private fun castToDouble(expression: String): Double {
return expression.toDoubleOrNull() ?: 0.0
}
}
```
### Annotation-based methods (Java)
To implement tools in Java, instead of subclassing `Tool` or `SimpleTool`, use annotation-based methods with `@Tool` and `@LLMDescription`. Koog handles serialization and registration automatically through reflection. To learn more about the implementation, see Java examples below.
#### Usage examples
This is an example of a tool implementation in Java, equivalent to using the `Tool` class in Kotlin.
```
// Java equivalent: implement the tool as a Java method and register it via ToolRegistry.builder().
// This is the recommended Java interop path instead of subclassing the Kotlin Tool base class.
public final class CalculatorTool {
private CalculatorTool() {}
@Tool(customName = "calculator")
@LLMDescription(description = "A simple calculator that can add two digits (0-9).")
public static int calculator(
@LLMDescription(description = "The first digit to add (0-9)") int digit1,
@LLMDescription(description = "The second digit to add (0-9)") int digit2
) {
if (digit1 < 0 || digit1 > 9) throw new IllegalArgumentException("digit1 must be a single digit (0-9)");
if (digit2 < 0 || digit2 > 9) throw new IllegalArgumentException("digit2 must be a single digit (0-9)");
return digit1 + digit2;
}
public static ToolRegistry registry() throws NoSuchMethodException {
return ToolRegistry.builder()
.tool(CalculatorTool.class.getMethod("calculator", int.class, int.class))
.build();
}
}
// Note: Subclassing the Kotlin Tool and overriding a suspend execute(...) from Java is not supported.
// The Java interop uses reflection-based registration of Java methods as tools.
```
Here is an example of a tool implementation in Java, equivalent to using the `SimpleTool` class in Kotlin. This example implements a simple tool that returns a text result.
```
// Java equivalent of SimpleTool: provide a Java method and register it as a tool.
public final class CastToDoubleTool {
private CastToDoubleTool() {}
@Tool(customName = "cast_to_double")
@LLMDescription(description = "casts the passed expression to double or returns 0.0 if the expression is not castable")
public static String castToDouble(
@LLMDescription(description = "An expression to case to double") String expression,
@LLMDescription(description = "A comment on how to process the expression") String comment
) {
double value;
try {
value = Double.parseDouble(expression);
} catch (Exception e) {
value = 0.0;
}
return "Result: " + value + ", the comment was: " + comment;
}
public static ToolRegistry registry() throws NoSuchMethodException {
return ToolRegistry.builder()
.tool(CastToDoubleTool.class.getMethod("castToDouble", String.class, String.class))
.build();
}
}
// Note: Extending Kotlin SimpleTool from Java is not required; registering a Java method is the idiomatic approach.
```
### Sending tool result to LLM in custom format
For Kotlin:
If you are not happy with JSON results sent to LLM (in some cases, LLMs can work better if tool output is structured as Markdown, for instance), you have to follow the following steps:
1. Implement `ToolResult.TextSerializable` interface, and override `textForLLM()` method
1. Override `resultSerializer` using `ToolResultUtils.toTextSerializer()`
For Java:
Return formatted text (such as Markdown) directly as a `String` from your annotated method. The framework handles this automatically.
#### Example
Here is an example showing custom formatted output in both Kotlin and Java:
```
// A tool that edits file
object EditFile : Tool(
argsType = typeToken(),
resultType = typeToken(),
name = "edit_file",
description = "Edits the given file"
) {
// Define tool arguments
@Serializable
public data class Args(
val path: String,
val original: String,
val replacement: String
)
@Serializable
public data class Result(
private val patchApplyResult: PatchApplyResult
) {
@Serializable
public sealed interface PatchApplyResult {
@Serializable
public data class Success(val updatedContent: String) : PatchApplyResult
@Serializable
public sealed class Failure(public val reason: String) : PatchApplyResult
}
// Textual output (in Markdown format) that will be visible to the LLM after the tool finishes.
fun textForLLM(): String = markdown {
if (patchApplyResult is PatchApplyResult.Success) {
line {
bold("Successfully").text(" edited file (patch applied)")
}
} else {
line {
text("File was ")
.bold("not")
.text(" modified (patch application failed: ${(patchApplyResult as PatchApplyResult.Failure).reason})")
}
}
}
override fun toString(): String = textForLLM()
}
// Function that executes the tool with the provided arguments
override suspend fun execute(args: Args): Result {
return TODO("Implement file edit")
}
}
```
```
import ai.koog.agents.core.tools.ToolRegistry;
import ai.koog.agents.core.tools.annotations.LLMDescription;
import ai.koog.agents.core.tools.annotations.Tool;
// Java equivalent: return Markdown text directly to the LLM from a Java method and register it as a tool.
// This avoids needing a custom serializable Result type (which would require Kotlin serialization support).
public final class EditFile {
private EditFile() {}
@Tool(customName = "edit_file")
@LLMDescription(description = "Edits the given file")
public static String editFile(
String path,
String original,
String replacement
) {
// TODO: Implement file edit logic; below is a placeholder illustrating Markdown output
boolean success = false;
if (success) {
return "**Successfully** edited file (patch applied)";
} else {
return "File was **not** modified (patch application failed: reason)";
}
}
public static ToolRegistry registry() throws NoSuchMethodException {
return ToolRegistry.builder()
.tool(EditFile.class.getMethod("editFile", String.class, String.class, String.class))
.build();
}
}
// Note: If you need a structured custom Result object from Java, you must expose a Kotlin @Serializable type
// or another serializer-aware type. Returning String works out-of-the-box with Koog's Java interop.
```
After implementing your tool in Kotlin or Java, you need to add it to a tool registry and then use it with an agent. For details, see [Tool registry](../tools/index#tool-registry).
# Events
# Agent events
Agent events are actions or interactions that occur as part of an agent workflow. They include:
- Agent lifecycle events
- Strategy events
- Node execution events
- LLM call events
- LLM streaming events
- Tool execution events
Note: Feature events are defined in the agents-core module and live under the package `ai.koog.agents.core.feature.model.events`. Features such as `agents-features-trace`, and `agents-features-event-handler` consume these events to process and forward messages created during agent execution.
## Predefined event types
Koog provides predefined event types that can be used in custom message processors. The predefined events can be classified into several categories, depending on the entity they relate to:
- [Agent events](#agent-events)
- [Strategy events](#strategy-events)
- [Node events](#node-events)
- [Subgraph events](#subgraph-events)
- [LLM call events](#llm-call-events)
- [LLM streaming events](#llm-streaming-events)
- [Tool execution events](#tool-execution-events)
### Agent events
#### AgentStartingEvent
Represents the start of an agent run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `agentId` | String | Yes | | The unique identifier of the AI agent. |
| `runId` | String | Yes | | The unique identifier of the AI agent run. |
#### AgentCompletedEvent
Represents the end of an agent run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `agentId` | String | Yes | | The unique identifier of the AI agent. |
| `runId` | String | Yes | | The unique identifier of the AI agent run. |
| `result` | String | Yes | | The result of the agent run. Can be `null` if there is no result. |
#### AgentExecutionFailedEvent
Represents the occurrence of an error during an agent run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `agentId` | String | Yes | | The unique identifier of the AI agent. |
| `runId` | String | Yes | | The unique identifier of the AI agent run. |
| `error` | AIAgentError | Yes | | The specific error that occurred during the agent run. For more information, see [AIAgentError](#aiagenterror). |
#### AgentClosingEvent
Represents the closure or termination of an agent. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `agentId` | String | Yes | | The unique identifier of the AI agent. |
The `AIAgentError` class provides more details about an error that occurred during an agent run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| ------------ | --------- | -------- | ------- | ---------------------------------------------------------------- |
| `message` | String | Yes | | The message that provides more details about the specific error. |
| `stackTrace` | String | Yes | | The collection of stack records until the last executed code. |
| `cause` | String | No | null | The cause of the error, if available. |
The `AgentExecutionInfo` class provides contextual information about the execution path, enabling tracking of nested execution contexts within an agent run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| ---------- | ------------------ | -------- | ------- | --------------------------------------------------------------------------------------------- |
| `parent` | AgentExecutionInfo | No | null | Reference to the parent execution context. If null, this represents the root execution level. |
| `partName` | String | Yes | | A string representing the name of the current part or segment of the execution. |
### Strategy events
#### GraphStrategyStartingEvent
Represents the start of a graph-based strategy run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy run. |
| `strategyName` | String | Yes | | The name of the strategy. |
| `graph` | StrategyEventGraph | Yes | | The graph structure representing the strategy workflow. |
#### FunctionalStrategyStartingEvent
Represents the start of a functional strategy run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy run. |
| `strategyName` | String | Yes | | The name of the strategy. |
#### StrategyCompletedEvent
Represents the end of a strategy run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy run. |
| `strategyName` | String | Yes | | The name of the strategy. |
| `result` | String | Yes | | The result of the run. Can be `null` if there is no result. |
### Node events
#### NodeExecutionStartingEvent
Represents the start of a node run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy run. |
| `nodeName` | String | Yes | | The name of the node whose run started. |
| `input` | JsonElement | No | null | The input value for the node. |
#### NodeExecutionCompletedEvent
Represents the end of a node run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy run. |
| `nodeName` | String | Yes | | The name of the node whose run ended. |
| `input` | JsonElement | No | null | The input value for the node. |
| `output` | JsonElement | No | null | The output value produced by the node. |
#### NodeExecutionFailedEvent
Represents an error that occurred during a node run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy run. |
| `nodeName` | String | Yes | | The name of the node where the error occurred. |
| `input` | JsonElement | No | null | The input data provided to the node. |
| `error` | AIAgentError | Yes | | The specific error that occurred during the node run. For more information, see [AIAgentError](#aiagenterror). |
### Subgraph events
#### SubgraphExecutionStartingEvent
Represents the start of a subgraph run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy run. |
| `subgraphName` | String | Yes | | The name of the subgraph whose run started. |
| `input` | JsonElement | No | null | The input value for the subgraph. |
#### SubgraphExecutionCompletedEvent
Represents the end of a subgraph run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy run. |
| `subgraphName` | String | Yes | | The name of the subgraph whose run ended. |
| `input` | JsonElement | No | null | The input value for the subgraph. |
| `output` | JsonElement | No | null | The output value produced by the subgraph. |
#### SubgraphExecutionFailedEvent
Represents an error that occurred during a subgraph run. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy run. |
| `subgraphName` | String | Yes | | The name of the subgraph where the error occurred. |
| `input` | JsonElement | No | null | The input data provided to the subgraph. |
| `error` | AIAgentError | Yes | | The specific error that occurred during the subgraph run. For more information, see [AIAgentError](#aiagenterror). |
### LLM call events
#### LLMCallStartingEvent
Represents the start of an LLM call. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ---------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the LLM run. |
| `prompt` | Prompt | Yes | | The prompt that is sent to the model. For more information, see [Prompt](#prompt). |
| `model` | ModelInfo | Yes | | The model information. See [ModelInfo](#modelinfo). |
| `tools` | List | Yes | | The list of tools that the model can call. |
The `Prompt` class represents a data structure for a prompt, consisting of a list of messages, a unique identifier, and optional parameters for language model settings. Includes the following fields:
| Name | Data type | Required | Default | Description |
| ---------- | --------- | -------- | ----------- | ------------------------------------------------------------ |
| `messages` | List | Yes | | The list of messages that the prompt consists of. |
| `id` | String | Yes | | The unique identifier for the prompt. |
| `params` | LLMParams | No | LLMParams() | The settings that control the way the LLM generates content. |
The `ModelInfo` class represents information about a language model, including its provider, model identifier, and characteristics. Includes the following fields:
| Name | Data type | Required | Default | Description |
| ----------------- | --------- | -------- | ------- | ---------------------------------------------------------------- |
| `provider` | String | Yes | | The provider identifier (e.g., "openai", "google", "anthropic"). |
| `model` | String | Yes | | The model identifier (e.g., "gpt-4", "claude-3"). |
| `displayName` | String | No | null | Optional human-readable display name for the model. |
| `contextLength` | Long | No | null | Maximum number of tokens the model can process. |
| `maxOutputTokens` | Long | No | null | Maximum number of tokens the model can generate. |
#### LLMCallCompletedEvent
Represents the end of an LLM call. Includes the following fields:
| Name | Data type | Required | Default | Description |
| -------------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the LLM run. |
| `prompt` | Prompt | Yes | | The prompt used in the call. |
| `model` | ModelInfo | Yes | | The model information. See [ModelInfo](#modelinfo). |
| `responses` | List | Yes | | One or more responses returned by the model. |
| `moderationResponse` | ModerationResult | No | null | The moderation response, if any. |
#### LLMCallFailedEvent
Represents the occurrence of an error during an LLM call. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the LLM run. |
| `prompt` | Prompt | Yes | | The prompt that was sent to the model. |
| `model` | ModelInfo | Yes | | The model information. See [ModelInfo](#modelinfo). |
| `tools` | List | Yes | | The list of tools that the model could call. |
| `error` | AIAgentError | Yes | | The specific error that occurred during the call. For more information, see [AIAgentError](#aiagenterror). |
### LLM streaming events
#### LLMStreamingStartingEvent
Represents the start of an LLM streaming call. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the LLM run. |
| `prompt` | Prompt | Yes | | The prompt that is sent to the model. |
| `model` | ModelInfo | Yes | | The model information. See [ModelInfo](#modelinfo). |
| `tools` | List | Yes | | The list of tools that the model can call. |
#### LLMStreamingFrameReceivedEvent
Represents a streaming frame received from the LLM. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the LLM run. |
| `prompt` | Prompt | Yes | | The prompt that is sent to the model. |
| `model` | ModelInfo | Yes | | The model information. See [ModelInfo](#modelinfo). |
| `frame` | StreamFrame | Yes | | The frame received from the stream. |
#### LLMStreamingFailedEvent
Represents the occurrence of an error during an LLM streaming call. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the LLM run. |
| `prompt` | Prompt | Yes | | The prompt that is sent to the model. |
| `model` | ModelInfo | Yes | | The model information. See [ModelInfo](#modelinfo). |
| `error` | AIAgentError | Yes | | The specific error that occurred during streaming. For more information, see [AIAgentError](#aiagenterror). |
#### LLMStreamingCompletedEvent
Represents the end of an LLM streaming call. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the LLM run. |
| `prompt` | Prompt | Yes | | The prompt that is sent to the model. |
| `model` | ModelInfo | Yes | | The model information. See [ModelInfo](#modelinfo). |
| `tools` | List | Yes | | The list of tools that the model can call. |
### Tool execution events
#### ToolCallStartingEvent
Represents the event of a model calling a tool. Includes the following fields:
| Name | Data type | Required | Default | Description |
| --------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy/agent run. |
| `toolCallId` | String | No | null | The identifier of the tool call, if available. |
| `toolName` | String | Yes | | The name of the tool. |
| `toolArgs` | JsonObject | Yes | | The arguments that are provided to the tool. |
#### ToolValidationFailedEvent
Represents the occurrence of a validation error during a tool call. Includes the following fields:
| Name | Data type | Required | Default | Description |
| ----------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------ |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy/agent run. |
| `toolCallId` | String | No | null | The identifier of the tool call, if available. |
| `toolName` | String | Yes | | The name of the tool for which validation failed. |
| `toolArgs` | JsonObject | Yes | | The arguments that are provided to the tool. |
| `toolDescription` | String | No | null | A description of the tool that encountered the validation error. |
| `message` | String | No | null | A message describing the validation error. |
| `error` | AIAgentError | Yes | | The specific error that occurred. For more information, see [AIAgentError](#aiagenterror). |
#### ToolCallFailedEvent
Represents a failure to execute a tool. Includes the following fields:
| Name | Data type | Required | Default | Description |
| ----------------- | ------------------ | -------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the strategy/agent run. |
| `toolCallId` | String | No | null | The identifier of the tool call, if available. |
| `toolName` | String | Yes | | The name of the tool. |
| `toolArgs` | JsonObject | Yes | | The arguments that are provided to the tool. |
| `toolDescription` | String | No | null | A description of the tool that failed. |
| `error` | AIAgentError | Yes | | The specific error that occurred when trying to call a tool. For more information, see [AIAgentError](#aiagenterror). |
#### ToolCallCompletedEvent
Represents a successful tool call with the return of a result. Includes the following fields:
| Name | Data type | Required | Default | Description |
| ----------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |
| `eventId` | String | Yes | | A unique identifier for the event or a group of events. |
| `executionInfo` | AgentExecutionInfo | Yes | | Provides contextual information about the execution associated with this event. |
| `runId` | String | Yes | | The unique identifier of the run. |
| `toolCallId` | String | No | null | The identifier of the tool call. |
| `toolName` | String | Yes | | The name of the tool. |
| `toolArgs` | JsonObject | Yes | | The arguments provided to the tool. |
| `toolDescription` | String | No | null | A description of the tool that was executed. |
| `result` | JsonElement | No | null | The result of the tool call. |
## FAQ and troubleshooting
The following section includes commonly asked questions and answers related to the Tracing feature.
### How do I trace only specific parts of my agent's execution?
Use the `messageFilter` property to filter events. For example, to trace only node execution:
```
install(Tracing) {
val fileWriter = TraceFeatureMessageFileWriter.create(outputPath)
addMessageProcessor(fileWriter)
// Only trace LLM calls
fileWriter.setMessageFilter { message ->
message is LLMCallStartingEvent || message is LLMCallCompletedEvent
}
}
```
```
.install(Tracing.Feature, config -> {
var fileWriter = TraceFeatureMessageFileWriter.create(outputPath);
config.addMessageProcessor(fileWriter);
// Only trace LLM calls
fileWriter.setMessageFilter(message ->
message instanceof LLMCallStartingEvent || message instanceof LLMCallCompletedEvent
);
})
```
### Can I use multiple message processors?
Yes, you can add multiple message processors to trace to different destinations simultaneously:
```
install(Tracing) {
addMessageProcessor(TraceFeatureMessageLogWriter(logger))
addMessageProcessor(TraceFeatureMessageFileWriter.create(outputPath))
addMessageProcessor(TraceFeatureMessageRemoteWriter(connectionConfig))
}
```
```
.install(Tracing.Feature, config -> {
config.addMessageProcessor(TraceFeatureMessageLogWriter.create(logger));
config.addMessageProcessor(TraceFeatureMessageFileWriter.create(outputPath));
config.addMessageProcessor(new TraceFeatureMessageRemoteWriter());
})
```
### How can I create a custom message processor?
Implement the `FeatureMessageProcessor` interface:
```
class CustomTraceProcessor : FeatureMessageProcessor() {
override suspend fun processMessage(message: FeatureMessage) {
// Custom processing logic
if (message is NodeExecutionStartingEvent) {
// Process node start event
} else if (message is LLMCallCompletedEvent) {
// Process LLM call end event
} else {
// Handle other event types
}
}
override suspend fun close() {
// Close connections if established
}
}
val agent = AIAgent(
promptExecutor = simpleOllamaAIExecutor(),
llmModel = OllamaModels.Meta.LLAMA_3_2,
) {
install(Tracing) {
// Use your custom processor
addMessageProcessor(CustomTraceProcessor())
}
}
```
```
class CustomTraceProcessor extends FeatureMessageProcessor {
@Override
protected void handleMessage(FeatureMessage message) {
// Custom processing logic
if (message instanceof NodeExecutionStartingEvent) {
// Process node start event
} else if (message instanceof LLMCallCompletedEvent) {
// Process LLM call end event
} else {
// Handle other event types
}
}
@Override
public void handleClose() {
// Close connections if established
}
}
var agent = AIAgent.builder()
.promptExecutor(PromptExecutor.builder().ollama().build())
.llmModel(OllamaModels.Meta.LLAMA_3_2)
.install(Tracing.Feature, config -> {
// Use your custom processor
config.addMessageProcessor(new CustomTraceProcessor());
})
.build();
```
For more information about existing event types that can be handled by message processors, see [Predefined event types](#predefined-event-types).
# Strategies
# Predefined nodes and components
Nodes are the fundamental building blocks of agent workflows in the Koog framework. Each node represents a specific operation or transformation in the workflow, and they can be connected using edges to define the flow of execution.
In general, nodes let you encapsulate complex logic into reusable components that can be easily integrated into different agent workflows. This guide will walk you through the existing nodes that can be used in your agent strategies.
Each node is essentially a function (Kotlin) or action (Java) that takes an input of a specific type and returns an output of a specific type.
```
graph LR
in:::hidden
out:::hidden
subgraph node ["Node"]
execute(Do stuff)
end
in --Input--> execute --Output--> out
classDef hidden display: none;
```
Here is how you can define a node that expects a string as input and returns the length of the string (an integer) as output:
```
val nodeLength by node { input ->
input.length
}
```
```
var nodeLength = AIAgentNode.builder("nodeLength")
.withInput(String.class)
.withOutput(Integer.class)
.withAction((input, ctx) -> input.length())
.build();
```
For more information, see [node()](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.builder/node.html) (Kotlin) or [AIAgentNode.builder()](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.agent.entity/-a-i-agent-node/-companion/builder.html) for Java.
## Utility nodes
### Pass-through node
A simple pass-through node that does nothing and returns the input as output. For details, see [nodeDoNothing](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.extension/node-do-nothing.html) (Kotlin) or [AIAgentNode.doNothing()](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.agent.entity/-a-i-agent-node/-companion/do-nothing.html) (Java).
```
graph LR
in:::hidden
out:::hidden
subgraph node ["Pass-through node"]
execute(Do nothing)
end
in ---|T| execute --T--> out
classDef hidden display: none;
```
You can use this node for the following purposes:
- Create a placeholder node in your graph.
- Create a connection point without modifying the data.
Here is an example:
```
val passthrough by nodeDoNothing("passthrough")
edge(nodeStart forwardTo passthrough)
edge(passthrough forwardTo nodeFinish)
```
```
var passthrough = AIAgentNode.builder("passthrough")
.withInput(String.class)
.withOutput(String.class)
.withAction((input, ctx) -> input)
.build();
strategy.edge(strategy.nodeStart, passthrough);
strategy.edge(passthrough, strategy.nodeFinish);
```
## LLM nodes
### Prompt preparation node
**A node that adds messages to the LLM prompt using the provided prompt builder. This is useful for modifying the conversation context before making an actual LLM request.** For details, see [nodeAppendPrompt](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.extension/node-append-prompt.html) (Kotlin) or [AIAgentNode.appendPrompt()](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.agent.entity/-a-i-agent-node-builder-with-input/append-prompt.html) (Java).
```
graph LR
in:::hidden
out:::hidden
subgraph node ["Prompt preparation node"]
execute(Append prompt)
end
in ---|T| execute --T--> out
classDef hidden display: none;
```
You can use this node for the following purposes:
- Add system instructions to the prompt.
- Insert user messages into the conversation.
- Prepare the context for subsequent LLM requests.
Here is an example:
```
val firstNode by node {
// Transform input to output
}
val secondNode by node