Graph-Based Agents
Koog's graph-based strategy system lets you define agent workflows as directed graphs. This gives you fine-grained control over the agent's decision-making process.
Core Concepts
| Concept | Description |
|---|---|
| Strategy | A named workflow definition using strategy{} builder |
| Node | A processing step (LLM call, tool execution, custom logic) |
| Edge | A connection between nodes with optional conditions |
| Start/Finish | Special terminal nodes |
Strategy Builder
import ai.koog.agents.core.strategy.strategy
import ai.koog.agents.core.strategy.nodeLLMRequest
import ai.koog.agents.core.strategy.nodeExecuteTool
import ai.koog.agents.core.strategy.nodeLLMSendToolResult
import ai.koog.agents.core.strategy.nodeStart
import ai.koog.agents.core.strategy.nodeFinish
val myStrategy = strategy("simple-tool-loop") {
// Define nodes
val llmRequest by nodeLLMRequest()
val executeTool by nodeExecuteTool()
val sendToolResult by nodeLLMSendToolResult()
// Define edges
edge(nodeStart forwardTo llmRequest)
edge(llmRequest forwardTo executeTool onToolCall { true })
edge(llmRequest forwardTo nodeFinish onAssistantMessage { true })
edge(executeTool forwardTo sendToolResult)
edge(sendToolResult forwardTo llmRequest)
}
Predefined Nodes
nodeLLMRequest
Sends the current conversation to the LLM and processes the response:
val llm by nodeLLMRequest()
The LLM may respond with:
- An assistant message → triggers
onAssistantMessageedges - A tool call → triggers
onToolCalledges
nodeExecuteTool
Executes a tool call from the LLM's response:
val execute by nodeExecuteTool()
Takes the tool call from the previous LLM response, executes it, and stores the result.
nodeLLMSendToolResult
Sends the tool execution result back to the LLM:
val sendResult by nodeLLMSendToolResult()
This node sends the tool result as a tool message and gets the LLM's next response.
Special Nodes
nodeStart
The entry point of every strategy:
edge(nodeStart forwardTo firstNode)
nodeFinish
The exit point — ends the agent run:
edge(someNode forwardTo nodeFinish)
Edge Conditions
Use onToolCall and onAssistantMessage to route based on LLM response type:
onToolCall
Triggered when the LLM requests a tool call:
// Always follow this edge on tool calls
edge(llm forwardTo executeTool onToolCall { true })
// Conditional: only for specific tools
edge(llm forwardTo calculatorNode onToolCall { toolCall ->
toolCall.toolName == "calculator"
})
onAssistantMessage
Triggered when the LLM responds with a text message (no tool call):
// Always finish on assistant message
edge(llm forwardTo nodeFinish onAssistantMessage { true })
// Conditional: check message content
edge(llm forwardTo validator onAssistantMessage { message ->
message.content.contains("final answer")
})
Edge Transforms
Transform the data flowing between nodes:
edge(llm forwardTo executeTool withTransform { context ->
// Transform the tool call before execution
context.copy(args = modifiedArgs)
})
Custom Nodes
Create custom processing nodes:
import ai.koog.agents.core.strategy.node
val validator by node<String, Boolean> { input ->
// Custom logic
input.isNotBlank()
}
val formatter by node<String, String> { input ->
"Formatted: $input"
}
Complete Calculator Example (Kotlin)
import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.core.tools.SimpleTool
import ai.koog.agents.core.tools.ToolDescriptor
import ai.koog.agents.core.strategy.strategy
import ai.koog.agents.core.strategy.nodeLLMRequest
import ai.koog.agents.core.strategy.nodeExecuteTool
import ai.koog.agents.core.strategy.nodeLLMSendToolResult
import ai.koog.agents.core.strategy.nodeStart
import ai.koog.agents.core.strategy.nodeFinish
import ai.koog.agents.ext.simple.simpleOpenAIExecutor
import ai.koog.agents.ext.llm.OpenAIModels
// Define calculator tool
object CalculatorTool : SimpleTool<CalculatorTool.Args>() {
override val descriptor = ToolDescriptor(
name = "calculator",
description = "Perform arithmetic calculations"
)
data class Args(val expression: String) : Tool.Args
override suspend fun execute(args: Args): String {
return try {
evaluateExpression(args.expression).toString()
} catch (e: Exception) {
"Error: ${e.message}"
}
}
}
// Define the strategy
val calculatorStrategy = strategy("calculator-workflow") {
val llmRequest by nodeLLMRequest()
val executeTool by nodeExecuteTool()
val sendToolResult by nodeLLMSendToolResult()
// Start → LLM
edge(nodeStart forwardTo llmRequest)
// LLM → Tool execution (when tool is called)
edge(llmRequest forwardTo executeTool onToolCall { true })
// LLM → Finish (when no tool is called)
edge(llmRequest forwardTo nodeFinish onAssistantMessage { true })
// Tool execution → Send result back to LLM
edge(executeTool forwardTo sendToolResult)
// Send result → Loop back to LLM for next step
edge(sendToolResult forwardTo llmRequest)
}
// Create the agent
suspend fun main() {
val apiKey = System.getenv("OPENAI_API_KEY")
val toolRegistry = ToolRegistry {
tool(CalculatorTool)
}
val agent = AIAgent(
promptExecutor = simpleOpenAIExecutor(apiKey),
llmModel = OpenAIModels.Chat.GPT4o,
systemPrompt = """
You are a calculator assistant.
Use the calculator tool to perform arithmetic.
Always show your work step by step.
""".trimIndent(),
toolRegistry = toolRegistry,
strategy = calculatorStrategy
)
val result = agent.run("What is (15 * 23) + (47 - 12)?")
println(result)
}
Complete Calculator Example (Java)
import ai.koog.agents.core.agent.AIAgent;
import ai.koog.agents.core.tools.ToolRegistry;
import ai.koog.agents.core.strategy.StrategyBuilder;
import ai.koog.agents.core.strategy.StrategyKt;
import ai.koog.agents.ext.simple.SimpleOpenAIExecutorKt;
import ai.koog.agents.ext.llm.OpenAIModels;
public class CalculatorAgent {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("OPENAI_API_KEY");
// Define strategy
var strategy = StrategyKt.strategy("calculator-workflow", builder -> {
var llmRequest = builder.nodeLLMRequest();
var executeTool = builder.nodeExecuteTool();
var sendToolResult = builder.nodeLLMSendToolResult();
builder.edge(builder.nodeStart(), llmRequest);
builder.edge(llmRequest, executeTool, builder.onToolCall(t -> true));
builder.edge(llmRequest, builder.nodeFinish(), builder.onAssistantMessage(m -> true));
builder.edge(executeTool, sendToolResult);
builder.edge(sendToolResult, llmRequest);
return null;
});
// Build agent
var toolRegistry = ToolRegistry.builder()
.tool(new CalculatorTool())
.build();
var agent = AIAgent.builder(
SimpleOpenAIExecutorKt.simpleOpenAIExecutor(apiKey),
OpenAIModels.Chat.GPT4o
)
.systemPrompt("You are a calculator assistant. Use tools to compute.")
.toolRegistry(toolRegistry)
.strategy(strategy)
.build();
String result = agent.run("What is 42 * 17?");
System.out.println(result);
}
}
Common Patterns
ReAct Loop (Reasoning + Acting)
The most common pattern — alternate between LLM reasoning and tool execution:
val reactStrategy = strategy("react") {
val think by nodeLLMRequest()
val act by nodeExecuteTool()
val observe by nodeLLMSendToolResult()
edge(nodeStart forwardTo think)
edge(think forwardTo act onToolCall { true })
edge(think forwardTo nodeFinish onAssistantMessage { true })
edge(act forwardTo observe)
edge(observe forwardTo think) // Loop back
}
Multi-Step Pipeline
Sequential processing with different nodes:
val pipelineStrategy = strategy("pipeline") {
val analyze by nodeLLMRequest()
val research by nodeExecuteTool()
val synthesize by nodeLLMRequest()
val format by nodeLLMRequest()
edge(nodeStart forwardTo analyze)
edge(analyze forwardTo research onToolCall { true })
edge(research forwardTo synthesize)
edge(synthesize forwardTo format onAssistantMessage { true })
edge(format forwardTo nodeFinish)
}
Conditional Branching
Route to different nodes based on conditions:
val branchStrategy = strategy("branching") {
val classify by nodeLLMRequest()
val simpleHandler by nodeLLMRequest()
val complexHandler by nodeExecuteTool()
val finalResponse by nodeLLMRequest()
edge(nodeStart forwardTo classify)
// Simple questions → direct answer
edge(classify forwardTo simpleHandler onAssistantMessage { msg ->
!msg.content.contains("calculate")
})
// Complex questions → use tools
edge(classify forwardTo complexHandler onToolCall { true })
edge(simpleHandler forwardTo nodeFinish)
edge(complexHandler forwardTo finalResponse)
edge(finalResponse forwardTo nodeFinish)
}
Strategy Configuration Reference
| Builder Method | Description |
|---|---|
strategy(name) { } |
Create a named strategy |
nodeLLMRequest() |
LLM call node |
nodeExecuteTool() |
Tool execution node |
nodeLLMSendToolResult() |
Send tool result to LLM |
nodeStart |
Entry point (built-in) |
nodeFinish |
Exit point (built-in) |
node<I, O> { } |
Custom node with input/output types |
edge(from forwardTo to) |
Unconditional edge |
edge(from forwardTo to onToolCall { }) |
Edge on tool call |
edge(from forwardTo to onAssistantMessage { }) |
Edge on assistant message |
edge(from forwardTo to withTransform { }) |
Edge with data transform |