Overview
AIAgentFeature is the interface for creating modular, installable capabilities that extend agent behavior. Features integrate with the agent pipeline to intercept lifecycle events, modify behavior, and add functionality.
Features are the primary extension mechanism in Koog. They provide lifecycle hooks, configuration management, and storage integration.
Base Interface
public interface AIAgentFeature<TConfig : FeatureConfig, TFeatureImpl : Any>
Type Parameters
The type representing the configuration for this feature
The type of the feature implementation that will be installed
Properties
key
AIAgentStorageKey<TFeatureImpl>
required
A unique key used to identify and store the feature instance within the agent
Methods
createInitialConfig
Creates and returns an initial configuration for the feature.
public fun createInitialConfig(): TConfig
The initial configuration instance for this feature
Specialized Feature Interfaces
AIAgentGraphFeature
Feature for graph-based agents.
public interface AIAgentGraphFeature<TConfig : FeatureConfig, TFeatureImpl : Any> :
AIAgentFeature<TConfig, TFeatureImpl> {
public fun install(
config: TConfig,
pipeline: AIAgentGraphPipeline
): TFeatureImpl
}
The configuration for the feature
pipeline
AIAgentGraphPipeline
required
The graph pipeline to install the feature into
The installed feature implementation instance
AIAgentFunctionalFeature
Feature for functional-style agents.
public interface AIAgentFunctionalFeature<TConfig : FeatureConfig, TFeatureImpl : Any> :
AIAgentFeature<TConfig, TFeatureImpl> {
public fun install(
config: TConfig,
pipeline: AIAgentFunctionalPipeline
): TFeatureImpl
}
AIAgentPlannerFeature
Feature for planner-based agents.
public interface AIAgentPlannerFeature<TConfig : FeatureConfig, TFeatureImpl : Any> :
AIAgentFeature<TConfig, TFeatureImpl> {
public fun install(
config: TConfig,
pipeline: AIAgentPlannerPipeline
): TFeatureImpl
}
Feature Configuration
All feature configurations must extend FeatureConfig:
public interface FeatureConfig
Creating a Custom Feature
Basic Feature Implementation
// 1. Define configuration
class LoggingFeatureConfig : FeatureConfig {
var logLevel: LogLevel = LogLevel.INFO
var includeToolCalls: Boolean = true
var includePrompts: Boolean = false
}
// 2. Define feature implementation
class LoggingFeatureImpl(
private val config: LoggingFeatureConfig,
private val pipeline: AIAgentGraphPipeline
) {
private val logger = KotlinLogging.logger {}
fun logMessage(message: String) {
when (config.logLevel) {
LogLevel.DEBUG -> logger.debug { message }
LogLevel.INFO -> logger.info { message }
LogLevel.WARN -> logger.warn { message }
LogLevel.ERROR -> logger.error { message }
}
}
}
// 3. Create feature
object LoggingFeature : AIAgentGraphFeature<LoggingFeatureConfig, LoggingFeatureImpl> {
override val key = AIAgentStorageKey<LoggingFeatureImpl>("logging")
override fun createInitialConfig() = LoggingFeatureConfig()
override fun install(
config: LoggingFeatureConfig,
pipeline: AIAgentGraphPipeline
): LoggingFeatureImpl {
val impl = LoggingFeatureImpl(config, pipeline)
// Install interceptors
pipeline.interceptLLMCallStarting(this) { context ->
if (config.includePrompts) {
impl.logMessage(
"LLM Call: ${context.prompt.messages.last().content}"
)
}
}
pipeline.interceptToolCallStarting(this) { context ->
if (config.includeToolCalls) {
impl.logMessage(
"Tool Call: ${context.toolName}(${context.toolArgs})"
)
}
}
return impl
}
}
Installing Features
val agent = AIAgent(
promptExecutor = executor,
agentConfig = config,
strategy = strategy,
toolRegistry = tools
) {
// Install with default config
install(LoggingFeature)
// Install with custom config
install(LoggingFeature) {
logLevel = LogLevel.DEBUG
includeToolCalls = true
includePrompts = true
}
}
Feature Examples
Memory Feature
Stores conversation history:
class MemoryFeatureConfig : FeatureConfig {
var maxMessages: Int = 100
var persistToFile: Boolean = false
var filePath: String? = null
}
class MemoryFeatureImpl(
private val config: MemoryFeatureConfig
) {
private val messages = mutableListOf<Message>()
fun addMessage(message: Message) {
messages.add(message)
if (messages.size > config.maxMessages) {
messages.removeAt(0)
}
}
fun getMessages(): List<Message> = messages.toList()
fun clear() = messages.clear()
}
object MemoryFeature : AIAgentGraphFeature<MemoryFeatureConfig, MemoryFeatureImpl> {
override val key = AIAgentStorageKey<MemoryFeatureImpl>("memory")
override fun createInitialConfig() = MemoryFeatureConfig()
override fun install(
config: MemoryFeatureConfig,
pipeline: AIAgentGraphPipeline
): MemoryFeatureImpl {
val impl = MemoryFeatureImpl(config)
// Store LLM messages
pipeline.interceptLLMCallCompleted(this) { context ->
context.responses.forEach { response ->
impl.addMessage(response)
}
}
return impl
}
}
Retry Feature
Retries failed operations:
class RetryFeatureConfig : FeatureConfig {
var maxRetries: Int = 3
var retryDelay: Duration = 1.seconds
var exponentialBackoff: Boolean = true
}
class RetryFeatureImpl(
private val config: RetryFeatureConfig
) {
suspend fun <T> withRetry(block: suspend () -> T): T {
var lastException: Throwable? = null
var delay = config.retryDelay
repeat(config.maxRetries) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
if (attempt < config.maxRetries - 1) {
delay(delay)
if (config.exponentialBackoff) {
delay *= 2
}
}
}
}
throw lastException!!
}
}
object RetryFeature : AIAgentGraphFeature<RetryFeatureConfig, RetryFeatureImpl> {
override val key = AIAgentStorageKey<RetryFeatureImpl>("retry")
override fun createInitialConfig() = RetryFeatureConfig()
override fun install(
config: RetryFeatureConfig,
pipeline: AIAgentGraphPipeline
): RetryFeatureImpl {
return RetryFeatureImpl(config)
}
}
Tracing Feature
Logs execution traces:
class TracingFeatureConfig : FeatureConfig {
var writer: TraceWriter = TraceFeatureMessageLogWriter()
var includeTimestamps: Boolean = true
var includeTokenCounts: Boolean = true
}
class TracingFeatureImpl(
private val config: TracingFeatureConfig,
private val pipeline: AIAgentGraphPipeline
) {
private val traces = mutableListOf<TraceEvent>()
fun addTrace(event: TraceEvent) {
traces.add(event)
config.writer.write(event)
}
}
object TracingFeature : AIAgentGraphFeature<TracingFeatureConfig, TracingFeatureImpl> {
override val key = AIAgentStorageKey<TracingFeatureImpl>("tracing")
override fun createInitialConfig() = TracingFeatureConfig()
override fun install(
config: TracingFeatureConfig,
pipeline: AIAgentGraphPipeline
): TracingFeatureImpl {
val impl = TracingFeatureImpl(config, pipeline)
pipeline.interceptAgentStarting(this) { context ->
impl.addTrace(TraceEvent.AgentStarted(
agentId = context.agent.id,
timestamp = if (config.includeTimestamps)
Clock.System.now() else null
))
}
pipeline.interceptAgentCompleted(this) { context ->
impl.addTrace(TraceEvent.AgentCompleted(
agentId = context.agentId,
result = context.result,
timestamp = if (config.includeTimestamps)
Clock.System.now() else null
))
}
return impl
}
}
Cost Tracking Feature
Tracks LLM usage costs:
class CostTrackingFeatureConfig : FeatureConfig {
var models: Map<String, CostPerToken> = emptyMap()
var alertThreshold: Double? = null
}
data class CostPerToken(
val inputCostPer1kTokens: Double,
val outputCostPer1kTokens: Double
)
class CostTrackingFeatureImpl(
private val config: CostTrackingFeatureConfig
) {
private var totalCost: Double = 0.0
private val costsByModel = mutableMapOf<String, Double>()
fun addUsage(model: String, inputTokens: Int, outputTokens: Int) {
val pricing = config.models[model] ?: return
val cost = (inputTokens / 1000.0 * pricing.inputCostPer1kTokens) +
(outputTokens / 1000.0 * pricing.outputCostPer1kTokens)
totalCost += cost
costsByModel[model] = (costsByModel[model] ?: 0.0) + cost
config.alertThreshold?.let { threshold ->
if (totalCost >= threshold) {
println("Warning: Cost threshold exceeded: $$totalCost")
}
}
}
fun getTotalCost(): Double = totalCost
fun getCostsByModel(): Map<String, Double> = costsByModel.toMap()
}
object CostTrackingFeature : AIAgentGraphFeature<CostTrackingFeatureConfig, CostTrackingFeatureImpl> {
override val key = AIAgentStorageKey<CostTrackingFeatureImpl>("cost-tracking")
override fun createInitialConfig() = CostTrackingFeatureConfig()
override fun install(
config: CostTrackingFeatureConfig,
pipeline: AIAgentGraphPipeline
): CostTrackingFeatureImpl {
val impl = CostTrackingFeatureImpl(config)
pipeline.interceptLLMCallCompleted(this) { context ->
// Extract token counts from response
val usage = context.responses.firstOrNull()?.usage
usage?.let {
impl.addUsage(
model = context.model.id,
inputTokens = it.inputTokens,
outputTokens = it.outputTokens
)
}
}
return impl
}
}
Accessing Features
Retrieve installed features from the pipeline:
class MyStrategy : AIAgentGraphStrategy<String, String> {
override val name = "my-strategy"
override suspend fun execute(
context: AIAgentGraphContext,
input: String
): String? {
// Access installed feature
val memory = context.pipeline.feature(
MemoryFeatureImpl::class,
MemoryFeature
)
memory?.let {
val history = it.getMessages()
println("History: ${history.size} messages")
}
return input
}
}
Built-in Features
Koog provides several built-in features:
Tracing
install(TracingFeature) {
writer = TraceFeatureMessageFileWriter("trace.log")
includeTimestamps = true
}
Event Handling
install(EventHandlingFeature) {
onAgentStarted { event ->
println("Agent started: ${event.agentId}")
}
onToolCalled { event ->
println("Tool called: ${event.toolName}")
}
}
Snapshot/Rollback
install(SnapshotFeature) {
enableAutoSnapshot = true
snapshotInterval = 5.seconds
}
Feature Composition
Combine features for complex behavior:
val agent = AIAgent(...) {
// Memory for conversation history
install(MemoryFeature) {
maxMessages = 50
}
// Tracing for debugging
install(TracingFeature) {
writer = TraceFeatureMessageLogWriter()
}
// Cost tracking for monitoring
install(CostTrackingFeature) {
models = mapOf(
"gpt-4" to CostPerToken(0.03, 0.06),
"claude-3-opus" to CostPerToken(0.015, 0.075)
)
alertThreshold = 1.0
}
// Custom logging
install(LoggingFeature) {
logLevel = LogLevel.DEBUG
includeToolCalls = true
}
}
Best Practices
Feature Design
- Use descriptive, unique keys for feature storage
- Keep feature implementations focused and single-purpose
- Provide sensible default configurations
- Document configuration options clearly
- Consider feature interactions and ordering
- Use the pipeline for all event interception
Common Pitfalls
- Don’t modify pipeline state in configuration blocks
- Avoid heavy initialization in
createInitialConfig()
- Be careful with feature dependencies
- Don’t store mutable state in feature config
- Always clean up resources in feature implementations
Testing Features
@Test
fun testLoggingFeature() = runTest {
val agent = AIAgent(...) {
install(LoggingFeature) {
logLevel = LogLevel.DEBUG
}
}
val result = agent.run("test input")
// Access feature for verification
val logging = agent.pipeline.feature(
LoggingFeatureImpl::class,
LoggingFeature
)
assertNotNull(logging)
}
Source Reference
Defined in: agents-core/src/commonMain/kotlin/ai/koog/agents/core/feature/AIAgentFeature.kt