Skip to content

Conversation

@rubencagnie-toast
Copy link

👋 - I had a similar challenge, but then I saw your PR on the koog repo - so I figured I'd contribute to your repo

With the changes I made, I was successful in creating a strategy that does the streaming + tool calls appropriately

edge(nodeStart forwardTo mapStringToRequests)
edge(mapStringToRequests forwardTo nodeStreaming)
edge(nodeStreaming forwardTo executeMultipleTools onMultipleToolCalls { true })
edge(executeMultipleTools forwardTo mapToolCallsToRequests)
edge(mapToolCallsToRequests forwardTo nodeStreaming)
edge(nodeStreaming forwardTo nodeFinish onNoToolCalls  { true })

rubencagnie-toast and others added 7 commits September 12, 2025 14:56
- Add nullable text handling in StreamFrame.Append with optional finishReason
- Introduce index field in tool calls for proper ordering and grouping
- Add nodeLLMRequestsStreamingWithTools for handling streaming with tool support
- Implement utility functions to convert StreamFrames to Message.Response
- Update all LLM clients to support enhanced streaming with tools
- Use mapNotNull instead of map for text-only streaming to handle nulls
- Add toMessageResponses(), toTools(), and toAssistant() conversion utilities

This enables better handling of streaming responses that include both text
and tool calls, with proper support for chunked/partial responses that
need to be assembled.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Document nodeLLMRequestsStreamingWithTools function parameters and behavior
- Update StreamFrame.Append documentation to clarify nullable text and finishReason
- Update StreamFrame.ToolCall documentation to explain index and nullable fields
- Ensure compliance with CONTRIBUTING.md documentation requirements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add index parameter to all tool call() method invocations
- Update Message.Tool.Call constructor calls to include index field
- Adjust contentJson test since property moved to StreamFrame.ToolCall

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@dosier
Copy link
Owner

dosier commented Sep 13, 2025

Oo I just made some breaking changes, someone on the JB team said it's fine to introduce breaking changes, so I changed the sig of all the executeStreaming funcs to return a flow of streamframe objects (also fixed refactor issues). Are you able to merge my changes in fine @rubencagnie-toast ?

@rubencagnie
Copy link

Looking into that now

@dosier
Copy link
Owner

dosier commented Sep 13, 2025

Anyways I am off to bed now, if u haven't done it yet, I'll merge my latest changes in this pr tmmrw :)

…mplementations

- Integrated upstream changes including refactored agentic tools and MCP server support
- Resolved conflicts in streaming method signatures (executeStreaming vs executeStreamingWithTools)
- Kept local streaming event handler documentation and tests
- Updated method names to match upstream conventions (filterTextOnly)
- Preserved local documentation improvements and test enhancements
Copy link
Owner

@dosier dosier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heya, thanks, I like the StreamHandler integration, just some of the other changes I think I am a bit confused on.

I think my mistake was that I didn't realise a tool call can be chunked, I assumed they were always carrying the full payload.
After reading the OpenAI docs I see they do support delta tool calls.

Though idk if we should handle the combining of delta tool calls under the hood (the contract being that a tool call will only be emitted when/if it's fully streamed by the underlying llm api).

Maybe we should make it so there are two types of StreamFrame flows, a low level and a high level one. Not sure on the best approach here. We can also keep the design very low-level (matching openAI standard) and just introduce fancy extension functions for user friendliness.
E.g. stream.autoMergeToolCalls().onEach since I think most users don't care about partial tool calls and then having to combine them themselves every-time.

* @return A delegate that processes request messages and returns the complete response messages.
*/
@AIAgentBuilderDslMarker
public fun AIAgentSubgraphBuilderBase<*, *>.nodeLLMRequestsStreaming(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this is in-scope for my original PR, might be best added as part of a separate PR?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay after reviewing the other changes I understand this method more now, as in you can access the streamed data through the newly introduced StreamHandler in EventHandler, I am not really familiar with the whole koog api yet for building strategies and what not so this was a bit confusing to me at first.

I still feel like this may be a feature that should probably be part of it's own PR or do you think this should be considered a fundamental feature of tool calling support in the api?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to take this out. I am using this in my application, but it might be too specific to my use-case. Happy to remove it

@Serializable
public data class Append(
val text: String
val text: String?,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do u think it's worth it making text nullable? I feel like those frames can be omitted.
However, I do like to have support for finishReason, maybe it would be nicer to introduce a new StreamFrame type, like StreamFrame.End or something that contains more meta information (perhaps also token count etc).

    /**
     * Represents a frame of a streaming response from a LLM that signals the end of the stream.
     *
     * @property finishReason The reason for the stream to end.
     */
    @Serializable
    public data class End(
        val finishReason: String? = null
    ) : StreamFrame

I am not very familiar with the standard when it comes to this in llm apis. But keeping Append.text null-safe seams like such a massive benefit for the vast majority of use-cases.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I like that idea. Are we always guaranteed that the event with "finish-reason" won't have content left? If not, we might need to consider have the function return List<StreamFrame> when parsing it

@rubencagnie-toast
Copy link
Author

Heya, thanks, I like the StreamHandler integration, just some of the other changes I think I am a bit confused on.

I think my mistake was that I didn't realise a tool call can be chunked, I assumed they were always carrying the full payload. After reading the OpenAI docs I see they do support delta tool calls.

Though idk if we should handle the combining of delta tool calls under the hood (the contract being that a tool call will only be emitted when/if it's fully streamed by the underlying llm api).

Maybe we should make it so there are two types of StreamFrame flows, a low level and a high level one. Not sure on the best approach here. We can also keep the design very low-level (matching openAI standard) and just introduce fancy extension functions for user friendliness. E.g. stream.autoMergeToolCalls().onEach since I think most users don't care about partial tool calls and then having to combine them themselves every-time.

I've updated the PR with your comments. I will be off for the next 24 hours, so will catch back up on Monday

@dosier
Copy link
Owner

dosier commented Sep 13, 2025

Okay I will be off until monday too probably, I also made some changes, introducing StreamFrame.End and some flow related extensions. Nice, now I think the only thing I am not sure on how to resolve is partial tool calls.

I am not sure if every supported llm client has support for tool call streaming, if they do then we should probably place the responsibility on the user to merge and validate the tool call chunks and refactor StreamFrame.Append and StreamFrame.ToolCall to StreamFrame.Delta.Text and StreamFrame.Delta.ToolCall respectively, then offer some syntactic sugar (extension functions) to transform a flow of stream frames to a more user friendly flow.

@dosier
Copy link
Owner

dosier commented Sep 13, 2025

I opened another PR for u hahah rubencagnie#2 where I merged my latest changes in.

@dosier
Copy link
Owner

dosier commented Sep 13, 2025

Also opened another version with the removal of partial tool call streaming, since only openai's official api seems to support that, but they also send an event when a tool call is finished that contains all the required info.
rubencagnie#3

@dosier dosier merged commit f6f7279 into dosier:streaming-tools Sep 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants