Add koog module: a tool-calling agent, a strategy graph with a Java node and the Spring Boot starter on Koog 1.3.0, tested against a scripted OpenAI-compatible endpoint

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b6FHj1ptP5Fej2eFDo
This commit is contained in:
Claude
2026-11-11 11:28:34 +05:30
parent 58eaf4c2e1
commit a620d91d0a
28 changed files with 867 additions and 0 deletions
@@ -0,0 +1,18 @@
agent.run("Is checkout healthy?") -> checkout is DEGRADED
HTTP calls the model endpoint received: 2
POST /v1/chat/completions
POST /v1/chat/completions
Request 1 - messages:
system: You are an on-call assistant. Use the tools to answer; never guess a status.
user: Is checkout healthy?
Request 1 - tools Koog derived from ServiceTools:
{"name":"getServiceStatus","description":"Returns the current status of one service: HEALTHY, DEGRADED or DOWN","parameters":{"type":"object","properties":{"service":{"description":"Service name, for example checkout","type":"string"}},"required":["service"]}}
{"name":"error_rate","description":"Returns the percentage of failed requests for one service over the last N minutes","parameters":{"type":"object","properties":{"service":{"description":"Service name, for example checkout","type":"string"},"minutes":{"description":"Window in minutes","type":"integer"}},"required":["service","minutes"]}}
Request 2 - messages (the tool call and its result are now in the history):
{"role":"system","content":"You are an on-call assistant. Use the tools to answer; never guess a status."}
{"role":"user","content":"Is checkout healthy?"}
{"role":"assistant","tool_calls":[{"id":"call_1","function":{"name":"getServiceStatus","arguments":"{\"service\":\"checkout\"}"},"type":"function"}]}
{"role":"tool","content":"\"DEGRADED\"","tool_call_id":"call_1"}
@@ -0,0 +1,23 @@
Graph, as MermaidDiagramGenerator prints it:
---
title: redacting_single_run
---
stateDiagram
state "redact" as redact
state "callLLM" as callLLM
state "executeTool" as executeTool
state "sendToolResult" as sendToolResult
[*] --> redact
redact --> callLLM
callLLM --> executeTool : transformed
callLLM --> [*] : transformed
executeTool --> sendToolResult
sendToolResult --> executeTool : transformed
sendToolResult --> [*] : transformed
The user typed: My key is sk-ABCDEFGH12345678. Is checkout healthy?
The model got: My key is [REDACTED]. Is checkout healthy?
Answer: checkout is DEGRADED
Secret present in any request body: false
+17
View File
@@ -0,0 +1,17 @@
1. The tool throws IllegalArgumentException("Unknown service: nope")
agent.run returned: There is no such service.
LLM calls: 2
what the model was told: tool: Tool with name 'getServiceStatus' failed to execute due to the error: Unknown service: nope!
2. The model sends {"minutes":"soon"} for an int parameter
what the model was told: Tool with name 'error_rate' failed to parse arguments due to the error: Failed to parse literal '"soon"' as an int value at element: $.primitive | JSON input: "soon"
3. The model calls restartService, which is not registered
the history sent back, last two messages:
{"role":"assistant","content":"{\"tool_call_id\":\"c1\",\"tool_name\":\"restartService\",\"tool_args\":{\"service\":\"checkout\"}}"}
{"role":"user","content":"{\"tool_call_id\":\"c1\",\"tool_name\":\"restartService\",\"tool_result\":\"Tool with name 'restartService' not found in the tool registry. Use one of the available tools.\"}"}
4. The model answers every request with another tool call, maxIterations(6)
AIAgentMaxNumberOfIterationsReachedException
LLM calls made before it stopped: 3
default maxIterations of an agent you do not configure: 50
+4
View File
@@ -0,0 +1,4 @@
run 1: first
run 2: second
messages sent on run 2: 1
{"role":"user","content":"hello again"}
+11
View File
@@ -0,0 +1,11 @@
PromptExecutor beans the starter registered (only OpenAI is configured):
multiLLMPromptExecutor
openAIExecutor
GET /ask?q=How bad are checkout errors?
-> checkout fails 4.2% of requests over 15 minutes
Model endpoint saw: [POST /v1/chat/completions, POST /v1/chat/completions]
Tool argument the model sent: minutes=15 (an int, parsed by Koog)
context.getBean(PromptExecutor.class) with one provider configured:
NoUniqueBeanDefinitionException: expected single matching bean but found 2: multiLLMPromptExecutor,openAIExecutor
@@ -0,0 +1,4 @@
ai.koog.openai.api-key is empty, ai.koog.openai.enabled=true is set:
startup failed: true
cause: NoSuchBeanDefinitionException
message: No qualifying bean of type 'ai.koog.prompt.executor.model.PromptExecutor' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier("openAIExecutor")}
+11
View File
@@ -0,0 +1,11 @@
kotlin-stdlib on the classpath (correct, Kotlin BOM imported first):
kotlin-stdlib-2.3.10.jar
Same classpath with kotlin-stdlib 1.9.25, which is what the Spring Boot 3.5.16 BOM picks when it comes first:
kotlin-stdlib-1.9.25.jar
$ java -cp <that classpath> com.ankurm.koog.TrapMain
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/time/Instant
at ai.koog.utils.time.KoogClock.<clinit>(KoogClock.kt:27)
at ai.koog.prompt.executor.model.PromptExecutorBuilder.openAI$default(PromptExecutorBuilder.kt:85)
at ai.koog.prompt.executor.model.PromptExecutorBuilder.openAI(PromptExecutorBuilder.kt)