Overview
PromptExecutor is responsible for executing prompts against language models, handling streaming responses, and managing LLM interactions. It serves as the bridge between the agent framework and underlying LLM providers.
In agent contexts, the executor is wrapped by ContextualPromptExecutor which adds feature pipeline integration and event interception.
Class Definition
public abstract class PromptExecutor : Closeable
Core Methods
execute
Executes a prompt and returns the response messages.
public abstract suspend fun execute(
prompt: Prompt,
model: LLModel,
tools: List<ToolDescriptor>
): List<Message.Response>
The prompt to send to the language model, containing messages and configuration
The language model to use for execution
tools
List<ToolDescriptor>
required
List of tool descriptors available for the LLM to use
List of response messages from the language model. Typically contains one response, but may contain multiple for choice-based models.
executeStreaming
Executes a streaming call to the language model.
public abstract fun executeStreaming(
prompt: Prompt,
model: LLModel,
tools: List<ToolDescriptor>
): Flow<StreamFrame>
The prompt to send to the language model
The language model to use for streaming
tools
List<ToolDescriptor>
required
List of available tool descriptors
A Flow of StreamFrame objects representing the streaming response. Each frame contains partial response data.
executeMultipleChoices
Executes a prompt with multiple choice generation.
public abstract suspend fun executeMultipleChoices(
prompt: Prompt,
model: LLModel,
tools: List<ToolDescriptor>
): List<LLMChoice>
The prompt to send to the language model
The language model to use
tools
List<ToolDescriptor>
required
List of available tool descriptors
List of choice objects, each containing a sequence of response messages
moderate
Executes content moderation on a prompt.
public abstract suspend fun moderate(
prompt: Prompt,
model: LLModel
): ModerationResult
The language model to use for moderation
The moderation result indicating whether content violates policies
models
Retrieves available language models.
public abstract suspend fun models(): List<LLModel>
List of available language models from the provider
Schema Generation
getStandardJsonSchemaGenerator
Gets a JSON schema generator for structured outputs.
public abstract fun getStandardJsonSchemaGenerator(
model: LLModel
): StandardJsonSchemaGenerator
The language model to generate schemas for
return
StandardJsonSchemaGenerator
Schema generator for creating JSON schemas from Kotlin types
getBasicJsonSchemaGenerator
Gets a basic JSON schema generator.
public abstract fun getBasicJsonSchemaGenerator(
model: LLModel
): BasicJsonSchemaGenerator
The language model to generate schemas for
Basic schema generator for simpler JSON schema needs
ContextualPromptExecutor
Wraps PromptExecutor with feature pipeline integration:
public class ContextualPromptExecutor(
private val executor: PromptExecutor,
private val context: AIAgentContext
) : PromptExecutor()
The contextual wrapper:
- Triggers pipeline events before and after LLM calls
- Allows features to intercept and modify prompts
- Provides logging and debugging information
- Integrates with agent execution lifecycle
Usage Examples
Basic Execution
val executor = PromptExecutor.create(
apiKey = "your-api-key",
provider = LLMProvider.OPENAI
)
val prompt = Prompt {
user("What is the capital of France?")
}
val model = LLModel.GPT4
val responses = executor.execute(
prompt = prompt,
model = model,
tools = emptyList()
)
println(responses.first().content)
executor.close()
Streaming Response
val executor = PromptExecutor.create(
apiKey = apiKey,
provider = LLMProvider.ANTHROPIC
)
val prompt = Prompt {
user("Tell me a story")
}
executor.executeStreaming(
prompt = prompt,
model = LLModel.CLAUDE_3_5_SONNET,
tools = emptyList()
).collect { frame ->
when (frame) {
is StreamFrame.Content -> print(frame.text)
is StreamFrame.Done -> println("\nDone!")
else -> { /* handle other frames */ }
}
}
executor.close()
val tools = listOf(
SearchTool().descriptor,
CalculatorTool().descriptor
)
val prompt = Prompt {
system("You are a helpful assistant with access to tools")
user("What is the weather in Paris?")
}
val responses = executor.execute(
prompt = prompt,
model = model,
tools = tools
)
// Handle potential tool calls
responses.forEach { response ->
when (response) {
is Message.Assistant.ToolCall -> {
println("LLM wants to call: ${response.toolCalls}")
}
is Message.Assistant.Text -> {
println("Response: ${response.content}")
}
}
}
Multiple Choices
val prompt = Prompt {
user("Generate creative names for a coffee shop")
}
val choices = executor.executeMultipleChoices(
prompt = prompt,
model = model.copy(numberOfChoices = 3),
tools = emptyList()
)
choices.forEachIndexed { index, choice ->
println("Choice ${index + 1}: ${choice.first().content}")
}
Content Moderation
val prompt = Prompt {
user(userInput)
}
val moderationResult = executor.moderate(
prompt = prompt,
model = model
)
if (moderationResult.flagged) {
println("Content flagged: ${moderationResult.categories}")
} else {
// Safe to proceed
val response = executor.execute(prompt, model, tools)
}
In Agent Context
When used within an agent, the executor is automatically wrapped:
class MyStrategy : AIAgentGraphStrategy<String, String> {
override val name = "my-strategy"
override suspend fun execute(
context: AIAgentGraphContext,
input: String
): String? {
// The context.llm uses ContextualPromptExecutor internally
// This triggers pipeline events and feature interception
val response = context.llm.prompt {
user(input)
}.execute()
return response.content
}
}
Provider-Specific Executors
Create executors for specific providers:
// OpenAI
val openAIExecutor = OpenAIPromptExecutor(
apiKey = openAIKey,
organization = "org-id"
)
// Anthropic
val anthropicExecutor = AnthropicPromptExecutor(
apiKey = anthropicKey
)
// Google (Gemini)
val geminiExecutor = GeminiPromptExecutor(
apiKey = geminiKey
)
// OpenRouter
val openRouterExecutor = OpenRouterPromptExecutor(
apiKey = openRouterKey
)
// Ollama (local)
val ollamaExecutor = OllamaPromptExecutor(
baseUrl = "http://localhost:11434"
)
Feature Pipeline Integration
The ContextualPromptExecutor integrates with the agent pipeline:
// Features can intercept LLM calls
pipeline.interceptLLMCallStarting(feature) { eventContext ->
// Modify the prompt before execution
eventContext.context.llm.prompt = Prompt {
system("Enhanced system prompt")
messages(eventContext.prompt.messages)
}
}
pipeline.interceptLLMCallCompleted(feature) { eventContext ->
// Process responses after execution
eventContext.responses.forEach { response ->
println("LLM said: ${response.content}")
}
}
Best Practices
Resource Management
- Always close the executor when done using
executor.close()
- Use
use { } blocks for automatic cleanup
- Executors maintain HTTP clients and connections
- Don’t create multiple executors unnecessarily
Error Handling
- LLM calls can fail due to network issues, rate limits, or API errors
- Implement retry logic for transient failures
- Handle timeout exceptions appropriately
- Validate API keys and credentials before use
Testing
Mock executors for testing:
val mockExecutor = object : PromptExecutor() {
override suspend fun execute(
prompt: Prompt,
model: LLModel,
tools: List<ToolDescriptor>
): List<Message.Response> {
return listOf(
Message.Assistant.Text("Mocked response")
)
}
// Implement other abstract methods...
}
Or use the testing utilities:
val mockExecutor = getMockExecutor(toolRegistry, eventHandler) {
mockLLMAnswer("Hello!") onRequestContains "Hello"
mockLLMAnswer("Default response").asDefaultResponse
}
Source Reference
Defined in: agents-core/src/commonMain/kotlin/ai/koog/agents/core/feature/ContextualPromptExecutor.kt