Skip to main content

Overview

The AIAgentFeature system provides a powerful way to extend agent capabilities through a plugin architecture. Features can intercept agent lifecycle events, modify behavior, add new functionality, and manage their own state - all without modifying core agent code.

Feature Architecture

Features in Koog are:
  • Modular: Each feature is self-contained
  • Configurable: Features have their own configuration
  • Type-safe: Features are strongly typed with their implementation
  • Pipeline-integrated: Features hook into the agent execution pipeline

Feature Interface

public interface AIAgentFeature<TConfig : FeatureConfig, TFeatureImpl : Any> {
    /**
     * A key used to uniquely identify the feature in agent storage
     */
    public val key: AIAgentStorageKey<TFeatureImpl>
    
    /**
     * Creates and returns an initial configuration for the feature
     */
    public fun createInitialConfig(): TConfig
}

Specialized Feature Interfaces

AIAgentGraphFeature

For features that work with graph-based agents:
public interface AIAgentGraphFeature<TConfig : FeatureConfig, TFeatureImpl : Any> 
    : AIAgentFeature<TConfig, TFeatureImpl> {
    /**
     * Installs the feature into the graph pipeline
     */
    public fun install(config: TConfig, pipeline: AIAgentGraphPipeline): TFeatureImpl
}

AIAgentFunctionalFeature

For features that work with functional agents:
public interface AIAgentFunctionalFeature<TConfig : FeatureConfig, TFeatureImpl : Any> 
    : AIAgentFeature<TConfig, TFeatureImpl> {
    public fun install(config: TConfig, pipeline: AIAgentFunctionalPipeline): TFeatureImpl
}

AIAgentPlannerFeature

For features that work with planner agents:
public interface AIAgentPlannerFeature<TConfig : FeatureConfig, TFeatureImpl : Any> 
    : AIAgentFeature<TConfig, TFeatureImpl> {
    public fun install(config: TConfig, pipeline: AIAgentPlannerPipeline): TFeatureImpl
}

Installing Features

Basic Installation

Install features during agent creation:
val agent = AIAgent(
    promptExecutor = promptExecutor,
    llmModel = OpenAIModels.Chat.GPT4o,
    toolRegistry = toolRegistry
) {
    // Install features here
    install(MyFeature) {
        // Configure the feature
        option1 = "value"
        option2 = 42
    }
}

Feature Context

The installFeatures lambda provides a FeatureContext:
val agent = AIAgent(...) {
    // FeatureContext receiver
    install(EventHandler) {
        onToolCall { ctx ->
            println("Tool: ${ctx.tool.name}")
        }
    }
    
    install(TraceFeature) {
        enableConsoleLogging = true
    }
}

Built-In Features

Event Handler

Capture and respond to agent lifecycle events:
import ai.koog.agents.features.eventHandler.feature.handleEvents

val agent = AIAgent(...) {
    handleEvents {
        // Agent lifecycle
        onAgentStarting { ctx ->
            logger.info("Agent ${ctx.agentId} starting")
        }
        
        onAgentCompleted { ctx ->
            logger.info("Agent completed with result: ${ctx.result}")
        }
        
        onAgentExecutionFailed { ctx ->
            logger.error("Agent failed", ctx.throwable)
        }
        
        // Node execution
        onNodeExecutionStarting { ctx ->
            logger.debug("Node ${ctx.nodeName} starting")
        }
        
        onNodeExecutionCompleted { ctx ->
            logger.debug("Node ${ctx.nodeName} completed")
        }
        
        // LLM calls
        onLLMCallStarting { ctx ->
            logger.debug("LLM call starting with ${ctx.messages.size} messages")
        }
        
        onLLMCallCompleted { ctx ->
            logger.debug("LLM returned: ${ctx.response}")
        }
        
        // Tool calls
        onToolCallStarting { ctx ->
            println("Calling tool: ${ctx.tool.name}")
            println("Arguments: ${ctx.toolArgs}")
        }
        
        onToolCallCompleted { ctx ->
            println("Tool ${ctx.tool.name} returned: ${ctx.result}")
        }
        
        onToolCallFailed { ctx ->
            logger.error("Tool ${ctx.tool.name} failed", ctx.throwable)
        }
        
        // Subgraph execution
        onSubgraphExecutionStarting { ctx ->
            logger.debug("Subgraph ${ctx.subgraphName} starting")
        }
        
        onSubgraphExecutionCompleted { ctx ->
            logger.debug("Subgraph ${ctx.subgraphName} completed")
        }
    }
}
The Event Handler feature is the most commonly used feature for debugging and monitoring agent execution.

Trace Feature

Capture detailed execution traces:
import ai.koog.agents.features.trace.feature.tracing

val agent = AIAgent(...) {
    tracing {
        enableConsoleLogging = true
        enableFileLogging = true
        logFilePath = "agent-trace.log"
    }
}

// Access trace data
val trace = agent.getFeature<TraceFeature>()
val events = trace.getEvents()

OpenTelemetry Integration

Export telemetry data:
import ai.koog.agents.features.opentelemetry.feature.openTelemetry

val agent = AIAgent(...) {
    openTelemetry {
        serviceName = "my-agent"
        endpoint = "http://localhost:4317"
        enableMetrics = true
        enableTraces = true
    }
}

Snapshot Feature

Save and restore agent state:
import ai.koog.agents.features.snapshot.feature.snapshots

val agent = AIAgent(...) {
    snapshots {
        autoSave = true
        saveInterval = 30.seconds
    }
}

// Manually save snapshot
val snapshot = agent.getFeature<SnapshotFeature>()
snapshot.save()

// Restore from snapshot
snapshot.restore(snapshotId)

Creating Custom Features

Simple Feature Example

Create a feature that counts tool calls:
// Feature implementation
class ToolCallCounter {
    private var count = 0
    
    fun increment() {
        count++
    }
    
    fun getCount() = count
}

// Feature configuration
class ToolCallCounterConfig : FeatureConfig {
    var logInterval: Int = 10
}

// Feature definition
object ToolCallCounterFeature : AIAgentGraphFeature<ToolCallCounterConfig, ToolCallCounter> {
    override val key = AIAgentStorageKey<ToolCallCounter>("tool-call-counter")
    
    override fun createInitialConfig() = ToolCallCounterConfig()
    
    override fun install(
        config: ToolCallCounterConfig,
        pipeline: AIAgentGraphPipeline
    ): ToolCallCounter {
        val counter = ToolCallCounter()
        
        // Intercept tool calls
        pipeline.interceptToolCallCompleted(this) { ctx ->
            counter.increment()
            
            val count = counter.getCount()
            if (count % config.logInterval == 0) {
                println("Tool calls: $count")
            }
        }
        
        return counter
    }
}

// Extension function for convenience
fun FeatureContext.countToolCalls(
    configure: ToolCallCounterConfig.() -> Unit = {}
) {
    install(ToolCallCounterFeature, configure)
}

// Usage
val agent = AIAgent(...) {
    countToolCalls {
        logInterval = 5
    }
}

Accessing Features

Retrieve installed features:
// Get feature from agent
val counter = agent.getFeature<ToolCallCounter>()
val count = counter.getCount()

// Check if feature is installed
if (agent.hasFeature<ToolCallCounter>()) {
    val counter = agent.getFeature<ToolCallCounter>()
}

Pipeline Interception

Features can intercept various points in the agent pipeline:

Agent Lifecycle

pipeline.interceptAgentStarting(this) { ctx ->
    // Called when agent starts
}

pipeline.interceptAgentCompleted(this) { ctx ->
    // Called when agent completes successfully
}

pipeline.interceptAgentExecutionFailed(this) { ctx ->
    // Called when agent fails
}

pipeline.interceptAgentClosing(this) { ctx ->
    // Called when agent is closing
}

Strategy Execution

pipeline.interceptStrategyStarting(this) { ctx ->
    // Called when strategy execution starts
}

pipeline.interceptStrategyCompleted(this) { ctx ->
    // Called when strategy completes
}

Node Execution (Graph agents only)

pipeline.interceptNodeExecutionStarting(this) { ctx ->
    // Called before node executes
}

pipeline.interceptNodeExecutionCompleted(this) { ctx ->
    // Called after node completes
}

pipeline.interceptNodeExecutionFailed(this) { ctx ->
    // Called when node fails
}

LLM Calls

pipeline.interceptLLMCallStarting(this) { ctx ->
    // Called before LLM call
    // ctx.messages contains the messages being sent
}

pipeline.interceptLLMCallCompleted(this) { ctx ->
    // Called after LLM responds
    // ctx.response contains the LLM response
}

Tool Calls

pipeline.interceptToolCallStarting(this) { ctx ->
    // Called before tool execution
    // ctx.tool, ctx.toolArgs available
}

pipeline.interceptToolValidationFailed(this) { ctx ->
    // Called when tool argument validation fails
}

pipeline.interceptToolCallCompleted(this) { ctx ->
    // Called after tool execution
    // ctx.result contains the tool result
}

pipeline.interceptToolCallFailed(this) { ctx ->
    // Called when tool execution fails
}

Streaming

pipeline.interceptLLMStreamingStarting(this) { ctx ->
    // Called when streaming starts
}

pipeline.interceptLLMStreamingFrameReceived(this) { ctx ->
    // Called for each streaming frame
}

pipeline.interceptLLMStreamingCompleted(this) { ctx ->
    // Called when streaming completes
}

Feature State Management

Features can maintain state across agent execution:
class StatefulFeature {
    private val state = mutableMapOf<String, Any>()
    
    fun set(key: String, value: Any) {
        state[key] = value
    }
    
    fun get(key: String): Any? = state[key]
}

object StatefulFeaturePlugin : AIAgentGraphFeature<FeatureConfig, StatefulFeature> {
    override val key = AIAgentStorageKey<StatefulFeature>("stateful")
    
    override fun createInitialConfig() = object : FeatureConfig {}
    
    override fun install(
        config: FeatureConfig,
        pipeline: AIAgentGraphPipeline
    ): StatefulFeature {
        val feature = StatefulFeature()
        
        pipeline.interceptToolCallCompleted(this) { ctx ->
            feature.set("lastTool", ctx.tool.name)
            feature.set("lastResult", ctx.result)
        }
        
        return feature
    }
}

Multi-Agent Features

Some features work across multiple agents:

A2A (Agent-to-Agent) Client

import ai.koog.agents.features.a2a.client.a2aClient

val agent = AIAgent(...) {
    a2aClient {
        serverUrl = "http://other-agent:8080"
        enableDiscovery = true
    }
}

A2A Server

import ai.koog.agents.features.a2a.server.a2aServer

val agent = AIAgent(...) {
    a2aServer {
        port = 8080
        registerServices = true
    }
}

Best Practices

Feature Design

  1. Single responsibility: One feature, one concern
  2. Minimal configuration: Provide sensible defaults
  3. Efficient interception: Only intercept events you need
  4. Clean state management: Clean up resources in onAgentClosing

Performance

Minimize Interception Overhead: Each interceptor adds latency. Only intercept events you actually use.
// ❌ Bad: Intercepting but doing nothing
pipeline.interceptToolCallCompleted(this) { ctx ->
    // Empty handler
}

// ✅ Good: Only intercept when needed
if (config.trackToolCalls) {
    pipeline.interceptToolCallCompleted(this) { ctx ->
        trackToolCall(ctx)
    }
}

Error Handling

Handle errors gracefully in interceptors:
pipeline.interceptToolCallCompleted(this) { ctx ->
    try {
        processToolResult(ctx.result)
    } catch (e: Exception) {
        logger.error("Feature processing failed", e)
        // Don't let feature errors crash the agent
    }
}

Testing Features

Unit Testing

Test feature logic independently:
@Test
fun testToolCallCounter() {
    val counter = ToolCallCounter()
    
    counter.increment()
    counter.increment()
    
    assertEquals(2, counter.getCount())
}

Integration Testing

Test features with agents:
@Test
fun testCounterFeature() = runTest {
    val agent = AIAgent(
        promptExecutor = mockExecutor,
        llmModel = OpenAIModels.Chat.GPT4o,
        toolRegistry = toolRegistry
    ) {
        countToolCalls()
    }
    
    agent.run("Test input")
    
    val counter = agent.getFeature<ToolCallCounter>()
    assertTrue(counter.getCount() > 0)
}

Next Steps

Event Handler

Learn about event handling in detail

Memory Features

Add memory capabilities to agents

Tracing

Debug and monitor agent execution

Custom Features

Build your own features

Build docs developers (and LLMs) love