Skip to content

Commit 8255de8

Browse files
committed
Fix reasoning messages in nodes
- Add `onReasoningMessage` edge transformation for reasoning message filtering - Reorder `onMultipleToolCalls` edge to prioritize execution flow - Expand `EdgeTransformationDslMarker` target scope to include classes and annotate `AIAgentEdgeBuilderIntermediate` - Add `skipReasoningMessage` support and `requestLLMMultipleWithoutTools` implementation - Document `skipReasoningMessage` behavior in `nodeLLMRequest` API function
1 parent 3d1fd3d commit 8255de8

File tree

7 files changed

+113
-17
lines changed

7 files changed

+113
-17
lines changed

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/agent/AIAgentSimpleStrategies.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ private fun singleRunWithParallelAbility(parallelTools: Boolean) = strategy("sin
5252

5353
edge(nodeExecuteTool forwardTo nodeSendToolResult)
5454

55+
edge(nodeSendToolResult forwardTo nodeExecuteTool onMultipleToolCalls { true })
56+
5557
edge(
5658
nodeSendToolResult forwardTo nodeFinish
5759
onMultipleAssistantMessages { true }
5860
transformed { it.joinToString("\n") { message -> message.content } }
5961
)
60-
61-
edge(nodeSendToolResult forwardTo nodeExecuteTool onMultipleToolCalls { true })
6262
}
6363

6464
private fun singleRunModeStrategy() = strategy("single_run") {

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/agent/session/AIAgentLLMSession.kt

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,21 @@ public sealed class AIAgentLLMSession(
122122
protected suspend fun executeSingle(prompt: Prompt, tools: List<ToolDescriptor>): Message.Response =
123123
executeMultiple(prompt, tools).first()
124124

125+
/**
126+
* Sends a request to the language model without utilizing any tools and returns multiple responses.
127+
*
128+
* @return A list of response messages from the language model.
129+
*/
130+
public open suspend fun requestLLMMultipleWithoutTools(): List<Message.Response> {
131+
validateSession()
132+
133+
val promptWithDisabledTools = prompt
134+
.withUpdatedParams { toolChoice = null }
135+
.let { preparePrompt(it, emptyList()) }
136+
137+
return executeMultiple(promptWithDisabledTools, emptyList())
138+
}
139+
125140
/**
126141
* Sends a request to the language model without utilizing any tools and returns the response.
127142
*
@@ -130,10 +145,14 @@ public sealed class AIAgentLLMSession(
130145
* to ensure compatibility with the underlying LLM client's behavior. It then executes the request
131146
* and retrieves the response from the LLM.
132147
*
148+
* When `skipReasoningMessage` is true, filters out reasoning messages and returns the first
149+
* non-reasoning response.
150+
*
151+
* @param skipReasoningMessage If true, skips reasoning messages and returns first non-reasoning response (default: true).
133152
* @return The response message from the language model after executing the request, represented
134153
* as a [Message.Response] instance.
135154
*/
136-
public open suspend fun requestLLMWithoutTools(): Message.Response {
155+
public open suspend fun requestLLMWithoutTools(skipReasoningMessage: Boolean = true): Message.Response {
137156
validateSession()
138157
/*
139158
Not all LLM providers support tool list when tool choice is set to "none", so we are rewriting all tool messages to regular messages,
@@ -143,7 +162,11 @@ public sealed class AIAgentLLMSession(
143162
.withUpdatedParams { toolChoice = null }
144163
.let { preparePrompt(it, emptyList()) }
145164

146-
return executeSingle(promptWithDisabledTools, emptyList())
165+
return if (skipReasoningMessage) {
166+
executeMultiple(promptWithDisabledTools, emptyList()).first { it !is Message.Reasoning }
167+
} else {
168+
executeSingle(promptWithDisabledTools, emptyList())
169+
}
147170
}
148171

149172
/**
@@ -205,11 +228,19 @@ public sealed class AIAgentLLMSession(
205228
* Sends a request to the underlying LLM and returns the first response.
206229
* This method ensures the session is active before executing the request.
207230
*
231+
* When `skipReasoningMessage` is true, filters out reasoning messages and returns the first
232+
* non-reasoning response.
233+
*
234+
* @param skipReasoningMessage If true, skips reasoning messages and returns first non-reasoning response (default: true).
208235
* @return The first response message from the LLM after executing the request.
209236
*/
210-
public open suspend fun requestLLM(): Message.Response {
237+
public open suspend fun requestLLM(skipReasoningMessage: Boolean = true): Message.Response {
211238
validateSession()
212-
return executeSingle(prompt, tools)
239+
return if (skipReasoningMessage) {
240+
executeMultiple(prompt, tools).first { it !is Message.Reasoning }
241+
} else {
242+
executeSingle(prompt, tools)
243+
}
213244
}
214245

215246
/**

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/agent/session/AIAgentLLMWriteSession.kt

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -365,16 +365,40 @@ public class AIAgentLLMWriteSession internal constructor(
365365
prompt.withParams(newParams)
366366
}
367367

368+
/**
369+
* Sends a request to the language model without utilizing any tools, returns multiple responses,
370+
* and updates the prompt with the received messages.
371+
*
372+
* @return A list of response messages from the language model.
373+
*/
374+
override suspend fun requestLLMMultipleWithoutTools(): List<Message.Response> {
375+
return super.requestLLMMultipleWithoutTools().also { responses ->
376+
appendPrompt {
377+
responses.forEach { message(it) }
378+
}
379+
}
380+
}
381+
368382
/**
369383
* Sends a request to the Language Model (LLM) without including any tools, processes the response,
370384
* and updates the prompt with the returned message.
371385
*
372386
* LLM might answer only with a textual assistant message.
373387
*
388+
* When `skipReasoningMessage` is true, all responses (including reasoning) are added to the prompt,
389+
* but only the first non-reasoning response is returned.
390+
*
391+
* @param skipReasoningMessage If true, filters out reasoning messages from the return value (default: true).
374392
* @return the response from the LLM after processing the request, as a [Message.Response].
375393
*/
376-
override suspend fun requestLLMWithoutTools(): Message.Response {
377-
return super.requestLLMWithoutTools().also { response -> appendPrompt { message(response) } }
394+
override suspend fun requestLLMWithoutTools(skipReasoningMessage: Boolean): Message.Response {
395+
return if (skipReasoningMessage) {
396+
requestLLMMultipleWithoutTools().first { it !is Message.Reasoning }
397+
} else {
398+
super.requestLLMWithoutTools(skipReasoningMessage).also { response ->
399+
appendPrompt { message(response) }
400+
}
401+
}
378402
}
379403

380404
/**
@@ -412,11 +436,19 @@ public class AIAgentLLMWriteSession internal constructor(
412436
* Makes an asynchronous request to a Large Language Model (LLM) and updates the current prompt
413437
* with the response received from the LLM.
414438
*
439+
* When `skipReasoningMessage` is true, all responses (including reasoning) are added to the prompt,
440+
* but only the first non-reasoning response is returned.
441+
*
442+
* @param skipReasoningMessage If true, filters out reasoning messages from the return value (default: true).
415443
* @return A [Message.Response] object containing the response from the LLM.
416444
*/
417-
override suspend fun requestLLM(): Message.Response {
418-
return super.requestLLM().also { response ->
419-
appendPrompt { message(response) }
445+
override suspend fun requestLLM(skipReasoningMessage: Boolean): Message.Response {
446+
return if (skipReasoningMessage) {
447+
requestLLMMultiple().first { it !is Message.Reasoning }
448+
} else {
449+
super.requestLLM(skipReasoningMessage).also { response ->
450+
appendPrompt { message(response) }
451+
}
420452
}
421453
}
422454

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/dsl/builder/AIAgentEdgeBuilder.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import ai.koog.agents.core.utils.Option
1010
* to ensure its proper highlighting.
1111
*/
1212
@DslMarker
13-
@Target(AnnotationTarget.FUNCTION)
13+
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
1414
public annotation class EdgeTransformationDslMarker
1515

1616
/**
@@ -54,6 +54,7 @@ public class AIAgentEdgeBuilder<IncomingOutput, OutgoingInput, CompatibleOutput
5454
* the originating node's output into an intermediate representation
5555
* or filtering the flow based on specific conditions.
5656
*/
57+
@EdgeTransformationDslMarker
5758
public class AIAgentEdgeBuilderIntermediate<IncomingOutput, IntermediateOutput, OutgoingInput> internal constructor(
5859
internal val fromNode: AIAgentNodeBase<*, IncomingOutput>,
5960
internal val toNode: AIAgentNodeBase<OutgoingInput, *>,

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/dsl/extension/AIAgentEdges.kt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,19 @@ public infix fun <IncomingOutput, IntermediateOutput, OutgoingInput> AIAgentEdge
195195
.transformed { it.content }
196196
}
197197

198+
/**
199+
* Creates an edge that filters a reasoning message based on a custom condition
200+
*
201+
* @param block A function that evaluates whether to accept a reasoning message
202+
*/
203+
@EdgeTransformationDslMarker
204+
public infix fun <IncomingOutput, IntermediateOutput, OutgoingInput> AIAgentEdgeBuilderIntermediate<IncomingOutput, IntermediateOutput, OutgoingInput>.onReasoningMessage(
205+
block: suspend (Message.Reasoning) -> Boolean
206+
): AIAgentEdgeBuilderIntermediate<IncomingOutput, Message.Reasoning, OutgoingInput> {
207+
return onIsInstance(Message.Reasoning::class)
208+
.onCondition { signature -> block(signature) }
209+
}
210+
198211
/**
199212
* Creates an edge that filters assistant messages based on a custom condition and extracts their content.
200213
*
@@ -206,7 +219,21 @@ public infix fun <IncomingOutput, OutgoingInput> AIAgentEdgeBuilderIntermediate<
206219
): AIAgentEdgeBuilderIntermediate<IncomingOutput, List<Message.Assistant>, OutgoingInput> {
207220
return onIsInstance(List::class)
208221
.transformed { it.filterIsInstance<Message.Assistant>() }
209-
.onCondition { toolResults -> block(toolResults) }
222+
.onCondition { messages -> block(messages) }
223+
}
224+
225+
/**
226+
* Creates an edge that filters lists of reasoning messages based on a custom condition.
227+
*
228+
* @param block A function that evaluates whether to accept a list of reasoning messages
229+
*/
230+
public infix fun <IncomingOutput, OutgoingInput> AIAgentEdgeBuilderIntermediate<IncomingOutput, List<Message.Response>, OutgoingInput>.onMultipleReasoningMessages(
231+
block: suspend (List<Message.Reasoning>) -> Boolean
232+
): AIAgentEdgeBuilderIntermediate<IncomingOutput, List<Message.Reasoning>, OutgoingInput> {
233+
return onIsInstance(List::class)
234+
.transformed { it.filterIsInstance<Message.Reasoning>() }
235+
.onCondition { it.isNotEmpty() }
236+
.onCondition { messages -> block(messages) }
210237
}
211238

212239
/**

agents/agents-core/src/commonMain/kotlin/ai/koog/agents/core/dsl/extension/AIAgentNodes.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,18 @@ public fun AIAgentSubgraphBuilderBase<*, *>.nodeLLMSendMessageForceOneTool(
132132
/**
133133
* A node that appends a user message to the LLM prompt and gets a response with optional tool usage.
134134
*
135+
* When `skipReasoningMessage` is enabled, the node requests multiple responses and returns the first
136+
* non-reasoning message. Otherwise, it returns a single response that may include reasoning.
137+
*
135138
* @param name Optional node name.
136139
* @param allowToolCalls Controls whether LLM can use tools (default: true).
140+
* @param skipReasoningMessage If true, filters out reasoning messages and returns the first non-reasoning response (default: true).
137141
*/
138142
@AIAgentBuilderDslMarker
139143
public fun AIAgentSubgraphBuilderBase<*, *>.nodeLLMRequest(
140144
name: String? = null,
141-
allowToolCalls: Boolean = true
145+
allowToolCalls: Boolean = true,
146+
skipReasoningMessage: Boolean = true,
142147
): AIAgentNodeDelegate<String, Message.Response> =
143148
node(name) { message ->
144149
llm.writeSession {
@@ -147,9 +152,9 @@ public fun AIAgentSubgraphBuilderBase<*, *>.nodeLLMRequest(
147152
}
148153

149154
if (allowToolCalls) {
150-
requestLLM()
155+
requestLLM(skipReasoningMessage)
151156
} else {
152-
requestLLMWithoutTools()
157+
requestLLMWithoutTools(skipReasoningMessage)
153158
}
154159
}
155160
}

prompt/prompt-structure/src/commonMain/kotlin/ai/koog/prompt/structure/PromptExecutorExtensions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public suspend fun <T> PromptExecutor.executeStructured(
170170
config: StructuredRequestConfig<T>,
171171
): Result<StructuredResponse<T>> {
172172
val updatedPrompt = config.updatePrompt(model, prompt)
173-
val response = this.execute(prompt = updatedPrompt, model = model).single()
173+
val response = this.execute(prompt = updatedPrompt, model = model).filterNot { it is Message.Reasoning }.single()
174174

175175
return runCatching {
176176
require(response is Message.Assistant) { "Response for structured output must be an assistant message, got ${response::class.simpleName} instead" }

0 commit comments

Comments
 (0)