tung
10/01/2025, 3:24 PMfun main() {
try {
runBlocking(Dispatchers.IO) {
val agent = createWeatherAgent(apiClient = APIClient(), showMessage = {
println("Agent: ${it}")
print("Response > ")
readln()
})
val timezone = TimeZone.currentSystemDefault()
val message = readln()
val userInput = UserInput(
message = message,
currentDate = Clock.System.now().toLocalDateTime(timezone).date,
timezone = timezone,
)
// Print final result
val result: String = agent.run(userInput)
println(result)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun weatherVisualizationStrategy(
addDateTool: Tool<*, *>,
weatherTools: WeatherTools,
userTools: UserTools
) = strategy<UserInput, String>("weather-with-visualization") {
val setup by node<UserInput, StringSubgraphResult> { userInput ->
llm.writeSession {
updatePrompt {
system {
+"Today's date is ${userInput.currentDate}."
}
}
}
StringSubgraphResult(userInput.message)
}
val clarifyUserQuestion by subgraphWithTask<StringSubgraphResult>(
tools = listOf(addDateTool) + userTools.asTools()
) { initialMessage ->
xml {
tag("instructions") {
+"""
Clarify a user question until the locations, dates and additional information are provided.
""".trimIndent()
}
tag("initial_user_message") {
+"""
$initialMessage
""".trimIndent()
}
}
}
val collectData by subgraphWithTask<StringSubgraphResult>(
tools = weatherTools.asTools()
+ userTools.asTools()
+ ProvideStringSubgraphResult
) { input ->
xml {
tag("instructions") {
+"""
trigger get weather data by tools has provide.
""".trimIndent()
}
markdown {
h2("Requirements")
bulleted {
item("Get forecast data of location.")
}
h2("Tool usage guidelines")
"""
Use getWeatherForecast tool for each date, requesting hourly granularity when you need to make a detailed itinerary.
""".trimIndent()
}
tag("user input") {
"""
$input
""".trimIndent()
}
}
}
nodeStart then setup then clarifyUserQuestion then collectData
edge(clarifyUserQuestion forwardTo collectData)
edge(collectData forwardTo nodeFinish transformed {
it.result
})
}
//what is the weather in Ho Chi Minh city, Viet Nam?
fun createWeatherAgent(
apiClient: APIClient,
showMessage: suspend (String) -> String
): AIAgent<UserInput, String> {
val userTools = UserTools(showMessage)
val weatherTools = WeatherTools(apiClient)
val tools = ToolRegistry {
tool(::addDate)
tools(userTools)
tools(weatherTools)
tool(ProvideStringSubgraphResult)
tool(GenerateWeatherChartTool)
}
val agentConfig = AIAgentConfig(
prompt = prompt(
id = "structured-weather-agent",
params = LLMParams(temperature = 0.2)
) {
system(
content = """
You are a weather data assistant that can provide weather information and create
visualizations when requested.
""".trimIndent()
)
},
model = LLModel(
provider = LLMProvider.Ollama,
id = "llama3.2:3b",
capabilities = listOf(LLMCapability.Temperature, LLMCapability.Tools),
contextLength = 100000
),
maxAgentIterations = 200
)
return AIAgent<UserInput, String>(
promptExecutor = simpleOllamaAIExecutor(baseUrl = "<http://localhost:11434>"),
strategy = weatherVisualizationStrategy(
weatherTools = weatherTools,
userTools = userTools,
addDateTool = ::addDate.asTool()
),
agentConfig = agentConfig,
toolRegistry = tools,
installFeatures = {
install(EventHandler) {
onToolCall {
println("🔧 Tool executed: ${it.tool.descriptor.name} param: ${it.toolArgs}")
println("=".repeat(50))
}
}
}
)
}
log:
what is the weather in Ho Chi Minh city, Viet Nam?
🔧 Tool executed: showMessage param: VarArgs(args={parameter #1 message of fun com.example.koogsample.complexweather.tools.UserTools.showMessage(kotlin.String): kotlin.String=What location would you like to know the weather for? (e.g. Ho Chi Minh City, Hanoi, Da Nang)})
==================================================
Agent: What location would you like to know the weather for? (e.g. Ho Chi Minh City, Hanoi, Da Nang)
Response > Ho Chi Minh city
🔧 Tool executed: showMessage param: VarArgs(args={parameter #1 message of fun com.example.koogsample.complexweather.tools.UserTools.showMessage(kotlin.String): kotlin.String=What date would you like to know the weather for? (e.g. today, tomorrow, October 1st, 2025)})
==================================================
Agent: What date would you like to know the weather for? (e.g. today, tomorrow, October 1st, 2025)
Response > today
java.lang.IllegalStateException: Subgraph with task must always call tools, but no tool call was generated, got instead: Assistant(content=Since you didn't specify a particular time of day, I'll provide the current weather conditions for Ho Chi Minh City on this date.
However, I need to know what type of weather information you're looking for. Would you like:
1. Current weather conditions (temperature, humidity, wind speed)?
2. Forecasted weather for today or tomorrow?
3. Weather conditions for a specific time of day (morning, afternoon, evening)?
Please respond with the number of your preferred option., metaInfo=ResponseMetaInfo(timestamp=2025-10-01T15:19:07.129105Z, totalTokensCount=354, inputTokensCount=255, outputTokensCount=99, additionalInfo={}), attachments=[], finishReason=null)
at com.example.koogsample.complexweather.ComplexWeatherKt$weatherVisualizationStrategy$lambda$0$$inlined$subgraphWithTask$default$5.invokeSuspend(AIAgentSubgraphExt.kt:258)
at com.example.koogsample.complexweather.ComplexWeatherKt$weatherVisualizationStrategy$lambda$0$$inlined$subgraphWithTask$default$5.invoke(AIAgentSubgraphExt.kt)
at
Process finished with exit code 0Sergei Dubov
10/01/2025, 7:15 PMsubgraphWithTask logic. It expects the model that supports ToolChoice capability, while llama3.2:3b does not support it. So, when it receives an Assistant message instead of a tool call or a finalisation tool call, it raise the exception.
I think it can be re-written using a custom strategy instead to properly handle conditions when llm return an Assistant response… or, probably, Koog might have a subgraph implementation that handle this on it’s side.Sergei Dubov
10/01/2025, 7:18 PMsetToolChoiceRequired()Vadim Briliantov
10/01/2025, 8:25 PMsubgraphWithTask because it relies on tool calling in order to determine the final step.
I believe for Ollama this subgraphWithTask be fixed using giveFeedback node that would ask (and re-ask) the model to ONLY call tools same as I described in the articleVadim Briliantov
10/01/2025, 8:26 PMsubgraphWithTask ?
nodeDecide should go to giveFeedback instead of failure
WDYT?Sergei Dubov
10/01/2025, 8:40 PMsubgraphWithTask accordingly.tung
10/02/2025, 1:41 PMsubgraphWithTask into this strategy. The issue "Subgraph with task must always call tools" still occurs. My question is: if I want to use subgraphWithTask, do I need to use a model that supports toolChoice?.
fun weatherVisualizationStrategy(
addDateTool: Tool<*, *>, weatherTools: WeatherTools, userTools: UserTools
) = strategy<UserInput, String>("weather-with-visualization") {
val askLLM by nodeLLMRequest()
val callTool by nodeExecuteTool()
val sendToolResult by nodeLLMSendToolResult()
val setup by node<UserInput, String> { userInput ->
llm.writeSession {
updatePrompt {
system {
+"Today's date is ${userInput.currentDate}."
xml {
tag("instructions") {
+"Clarify the user's question until locations, dates and additional information are provided."
+"Then collect weather data using the available tools."
}
}
}
}
// Set all tools available from the start
tools = listOf(addDateTool.descriptor) + weatherTools.asTools()
.map { it.descriptor } + userTools.asTools()
.map { it.descriptor } + AskUser.descriptor
}
userInput.message
}
val giveFeedback by node<Any?, Message.Response> {
llm.writeSession {
updatePrompt {
user {
markdown {
h1("DO NOT CHAT WITH ME DIRECTLY! CALL TOOLS, INSTEAD.")
br()
h2("Available actions:")
h2("If you need to clarify location or dates, call `${AskUser.name}` tool")
h2("If you need to show messages, call `showMessage` tool")
h2("When ready to get weather data, call `getWeatherForecast` tool")
}
}
}
requestLLM()
}
}
edge(edgeIntermediate = nodeStart forwardTo setup)
edge(edgeIntermediate = setup forwardTo askLLM)
// Main feedback loop - exactly like the blog post
edge(edgeIntermediate = askLLM forwardTo callTool onToolCall { true })
edge(edgeIntermediate = askLLM forwardTo giveFeedback onAssistantMessage { true })
edge(edgeIntermediate = giveFeedback forwardTo callTool onToolCall { true })
edge(edgeIntermediate = giveFeedback forwardTo giveFeedback onAssistantMessage { true })
edge(
edgeIntermediate = (callTool forwardTo nodeFinish).onToolResult(
tool = weatherTools.asTools().first()
) { toolResult ->
toolResult.isSuccessful()
} transformed {
it.result?.toStringDefault().orEmpty()
})
// For other tools, send result back to LLM
edge(edgeIntermediate = callTool forwardTo sendToolResult onCondition {
it.tool != "getWeatherForecast"
})
edge(edgeIntermediate = sendToolResult forwardTo giveFeedback onAssistantMessage {
true
})
edge(edgeIntermediate = sendToolResult forwardTo callTool onToolCall { true })
}Sergei Dubov
10/03/2025, 10:36 AMsubgraphWithTask requires LLM to support ToolChoice to be able to process tools only + it also requires StructuredOutput to convert the model output to a specified Output type parameter:
public inline fun <reified Input, reified Output> AIAgentSubgraphBuilderBase<*, *>.subgraphWithTask(...)
Unfortunately, Ollama models usually does not support neither 🙂
We will think about updates for the subgraphWithTask API to support your case. For now, the best case is to use a custom strategy.tung
10/06/2025, 12:14 PMSergei Dubov
10/06/2025, 2:23 PMsubgraphWithTask API with models that do not natively support tool_choice - please see #929.tung
10/08/2025, 1:55 PMSergei Dubov
10/13/2025, 8:10 PMtung
10/14/2025, 7:58 AMconditional branching in the documentation, but do we need to manually check the user’s request, such as the question’s content or characteristics, to determine which strategy to navigate to?
Below is pseudocode for my question:
val agentConfig = AIAgentConfig(
prompt = prompt(
id = "structured-weather-agent", params = LLMParams(temperature = 0.2)
) {
system(
content = """
You are a helpful assistant.
""".trimIndent()
)
}, model = LLModel(
provider = LLMProvider.Ollama, id = "llama3.2:3b", capabilities = listOf(
LLMCapability.Temperature,
LLMCapability.Tools,
LLMCapability.ToolChoice,
LLMCapability.Completion
), contextLength = 100000
), maxAgentIterations = 200
)
val appAIAgent = AIAgent(
strategy = appStrategy(),
promptExecutor = SingleLLMPromptExecutor(
OpenRouterLLMClient(
apiKey = "",
settings = OpenRouterClientSettings(
baseUrl = "<http://localhost:11434>",
chatCompletionsPath = "/v1/chat/completions"
)
)
),
agentConfig = agentConfig
)
//app module or AIModule where can see module A,B,C,D.
fun appStrategy() = strategy<UserInput, String>("app_strategy") {
val moduleAGraph = createModuleAGraph()
val moduleBGraph = createModuleBGraph()
val moduleCGraph = createModuleCGraph()
val moduleDGraph = createModuleDGraph()
}
//place in Module A
fun AIAgentGraphStrategyBuilder<UserInput, String>.createModuleAGraph() {
val callLLM by nodeLLMRequest()
val executeTool by nodeExecuteTool()
val sendToolResult by nodeLLMSendToolResult()
val subgraphTaskA by subgraph<String, String> {
//....
}
//.... detail graph in module A
}
//place in Module B
fun AIAgentGraphStrategyBuilder<UserInput, String>.createModuleBGraph() {
val callLLM by nodeLLMRequest()
val executeTool by nodeExecuteTool()
val sendToolResult by nodeLLMSendToolResult()
//.... detail graph in module B
}
//place in Module C
fun AIAgentGraphStrategyBuilder<UserInput, String>.createModuleCGraph() {
val callLLM by nodeLLMRequest()
val executeTool by nodeExecuteTool()
val sendToolResult by nodeLLMSendToolResult()
//.... detail graph in module C
}
//place in Module D
fun AIAgentGraphStrategyBuilder<UserInput, String>.createModuleDGraph() {
val callLLM by nodeLLMRequest()
val executeTool by nodeExecuteTool()
val sendToolResult by nodeLLMSendToolResult()
//.... detail graph in module D
}Sergei Dubov
10/15/2025, 2:04 PMsubgraphs that do a pease of work, something like:
val strategy = strategy<String, String>(
"my-agent-strategy"
) {
val subgraphA by subgraph<String, String> {
val nodeA1 by node<String, String> { input ->
println("Do some work")
return@node "Output A1"
}
val nodeA2 by node<String, String> { input ->
println("Do mode work")
return@node "Output A2"
}
nodeStart then nodeA1 then nodeA2 then nodeFinish
}
val subgraphB by subgraph<String, String> {
val nodeB1 by node<String, String> { input ->
println("Do some other work")
return@node "Output B1"
}
val nodeB2 by node<String, String> { input ->
println("Do more other work")
return@node "Output B2"
}
nodeStart then nodeB1 then nodeB2 then nodeFinish
}
nodeStart then subgraphA then subgraphB then nodeFinish
}
The subgraph nodes can be defined in a separate modules and refer a dedicated logic inside a module. Please let me know if this might work for you.tung
10/15/2025, 3:32 PMtung
10/22/2025, 12:13 AMSergei Dubov
10/22/2025, 2:04 PMtung
10/22/2025, 3:20 PMThe class discriminator is missing, and no default serializers are registered in the polymorphic scope of 'RequestType' when using subGraphWithTask. The output I need should include a sealed class. I’ve checked the struct output section but couldn’t find guidance on how to apply it to my case.
I found a threads relate to polymorphic that mention use StandardJsonSchemaGenerator but seem it can’t use with subGraphWithTask.
The example StructuredOutput has been removed.
Do you have any suggestions?
https://docs.koog.ai/structured-output/#requesting-structured-responses
@Serializable
@LLMDescription("The dashboard request that was classified by the agent")
data class ClassifiedDashboardRequest(
@property:LLMDescription("Type of request: Get list dashboards or Get specific dashboard information ")
val requestType: RequestType,
@property:LLMDescription("Actual request to be performed by the application")
val userRequest: String
) {
fun toMarkdownString(): String {
return markdown {
h1("Request")
br()
h2("Request Type: ${requestType::class}")
br()
h2("userRequest: $userRequest")
}
}
}
@Serializable
sealed class RequestType {
@Serializable
object GetDashboards : RequestType()
@Serializable
data class GetWidgets(
val dashboardName: String
) : RequestType()
@Serializable
object Exit : RequestType()
}
val clarifyUserQuestion by subgraphWithTask<String, ClassifiedDashboardRequest>(
tools = userTools.asTools(),
) { input ->
xml {
+"""
- Ask the user what do they want to do.
- Currently support two actions for dashboards:
- GetDashboards: Returns a list of dashboards associated with the current account.
- GetWidgets: Returns widget belong to the dashboard.
- You should return one of them. if the user ask the question don't relate to any of them return "I don't know".
""".trimIndent()
tag("initial_user_message") {
+input
}
}
}Sergei Dubov
10/22/2025, 3:29 PMRequestType type in SerializersModule when creating a server and a client that you configure for your agent. Could you please provide the full stack trace here? I will check if this might be the root reason and can suggest after.tung
10/22/2025, 3:34 PM2025-10-22 22:32:31.051 4515-4515 System.err W kotlinx.serialization.json.internal.JsonDecodingException: Class discriminator was missing and no default serializers were registered in the polymorphic scope of 'RequestType'.
2025-10-22 22:32:31.051 4515-4515 System.err W JSON input: {}
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:413)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:43)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableElement(AbstractDecoder.kt:70)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableElement(StreamingJsonDecoder.kt:168)
2025-10-22 22:32:31.052 4515-4515 System.err W at com.example.iotp.agent.ClassifiedDashboardRequest$$serializer.deserialize(DashboardStrategy.kt:166)
2025-10-22 22:32:31.052 4515-4515 System.err W at com.example.iotp.agent.ClassifiedDashboardRequest$$serializer.deserialize(DashboardStrategy.kt:166)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:69)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.serialization.json.Json.decodeFromString(Json.kt:149)
2025-10-22 22:32:31.052 4515-4515 System.err W at com.example.iotp.agent.DashboardStrategyKt$dashboardStrategy$lambda$0$$inlined$subgraphWithTask$default$4.invokeSuspend(AIAgentSubgraphExt.kt:354)
2025-10-22 22:32:31.052 4515-4515 System.err W at com.example.iotp.agent.DashboardStrategyKt$dashboardStrategy$lambda$0$$inlined$subgraphWithTask$default$4.invoke(Unknown Source:11)
2025-10-22 22:32:31.052 4515-4515 System.err W at com.example.iotp.agent.DashboardStrategyKt$dashboardStrategy$lambda$0$$inlined$subgraphWithTask$default$4.invoke(Unknown Source:6)
2025-10-22 22:32:31.052 4515-4515 System.err W at ai.koog.agents.core.agent.entity.AIAgentNode$execute$2.invokeSuspend(AIAgentNode.kt:161)
2025-10-22 22:32:31.052 4515-4515 System.err W at ai.koog.agents.core.agent.entity.AIAgentNode$execute$2.invoke(Unknown Source:8)
2025-10-22 22:32:31.052 4515-4515 System.err W at ai.koog.agents.core.agent.entity.AIAgentNode$execute$2.invoke(Unknown Source:4)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndspatched(Undispatched.kt:66)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:165)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
2025-10-22 22:32:31.052 4515-4515 System.err W at ai.koog.agents.core.agent.entity.AIAgentNode.execute$suspendImpl(AIAgentNode.kt:155)
2025-10-22 22:32:31.052 4515-4515 System.err W at ai.koog.agents.core.agent.entity.AIAgentNode.execute(Unknown Source:0)
2025-10-22 22:32:31.052 4515-4515 System.err W at ai.koog.agents.core.agent.entity.AIAgentNodeBase.executeUnsafe(AIAgentNode.kt:129)
2025-10-22 22:32:31.052 4515-4515 System.err W at ai.koog.agents.core.agent.entity.AIAgentSubgraph.executeWithInnerContext(AIAgentSubgraph.kt:219)
2025-10-22 22:32:31.052 4515-4515 System.err W at ai.koog.agents.core.agent.entity.AIAgentSubgraph.access$executeWithInnerContext(AIAgentSubgraph.kt:40)
2025-10-22 22:32:31.052 4515-4515 System.err W at ai.koog.agents.core.agent.entity.AIAgentSubgraph$executeWithInnerContext$1.invokeSuspend(Unknown Source:15)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.coroutines.UndispatchedCoroutine.afterResume(CoroutineContext.kt:266)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:101)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:47)
2025-10-22 22:32:31.052 4515-4515 System.err W at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
2025-10-22 22:32:31.052 4515-4515 System.err W at android.os.Handler.handleCallback(Handler.java:995)
2025-10-22 22:32:31.052 4515-4515 System.err W at android.os.Handler.dispatchMessage(Handler.java:103)
2025-10-22 22:32:31.052 4515-4515 System.err W at android.os.Looper.loopOnce(Looper.java:248)
2025-10-22 22:32:31.052 4515-4515 System.err W at android.os.Looper.loop(Looper.java:338)
2025-10-22 22:32:31.053 4515-4515 System.err W at android.app.ActivityThread.main(ActivityThread.java:9067)
2025-10-22 22:32:31.053 4515-4515 System.err W at java.lang.reflect.Method.invoke(Native Method)
2025-10-22 22:32:31.053 4515-4515 System.err W at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593)
2025-10-22 22:32:31.053 4515-4515 System.err W at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932)tung
10/22/2025, 3:40 PMSerializersModule into agent config. AIAgentConfig doesn’t accept this param.