Skip to content

feat: add vendor-neutral streaming tool loop - #1826

Merged
igordayen merged 1 commit into
embabel:mainfrom
arnabnandy7:feature/streaming-tool-loop
Aug 1, 2026
Merged

feat: add vendor-neutral streaming tool loop#1826
igordayen merged 1 commit into
embabel:mainfrom
arnabnandy7:feature/streaming-tool-loop

Conversation

@arnabnandy7

@arnabnandy7 arnabnandy7 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces a vendor-neutral streaming tool loop that mirrors the existing blocking ToolLoop architecture while retaining compatibility with the original LlmMessageStreamer contract.

Closes #1800.

Changes

  • Adds StreamingToolLoop and DefaultStreamingToolLoop.
  • Uses StreamingToolLoop.execute(...) for symmetry with the blocking ToolLoop.execute(...) API.
  • Retains the original three-argument LlmMessageStreamer.stream(...): Flux<String> contract.
  • Adds LlmMessageStreamer.streamInference(...) for a single provider-neutral streaming inference.
  • Adds LlmInferenceStreamEvent for incremental content and assembled terminal assistant responses.
  • Moves streaming tool execution, conversation continuation, and dynamic tool injection under Embabel’s control.
  • Reuses the shared blocking/streaming tool-execution and injection support introduced in refactor: extract shared tool execution support #1837.
  • Updates the Spring AI adapter to call ChatModel.stream() directly.
  • Uses Spring AI’s MessageAggregator to assemble partial tool-call fragments.
  • Applies ToolInjectionStrategy between inference turns.
  • Enables UnfoldingTool children to become available in subsequent streaming calls.
  • Preserves streamed content emitted before and between tool-call turns.
  • Preserves tool decorators, tool-call context, inspectors, missing-tool handling, iteration limits, and returnDirect.
  • Adds focused unit tests and a LangChain4j/Ollama integration test.
  • Updates the streaming documentation.

Architecture

The streaming path now follows the same provider-neutral pattern as blocking calls:

Blocking:
LlmMessageSender → ToolLoop → DefaultToolLoop

Streaming:
LlmMessageStreamer → StreamingToolLoop → DefaultStreamingToolLoop

Provider integrations perform one streaming inference and expose the assembled assistant response. Embabel manages tool execution, conversation history, dynamic tool injection, and continuation across inference turns.

The streaming loop uses the shared tool-execution support from #1837, keeping inspector callbacks, result conversion, decoration, injection, and deduplication consistent with the blocking loop.

Compatibility

The original public signature remains the functional-interface method:

LlmMessageStreamer.stream(
    messages,
    tools,
    toolCallInspectors,
): Flux<String>

This preserves the existing method descriptor for callers and implementations.

The new method:

LlmMessageStreamer.streamInference(
    messages,
    tools,
): Flux<LlmInferenceStreamEvent>

provides the terminal assistant message required by the Embabel-managed streaming tool loop.

Existing implementations can continue returning raw content through stream(...). Provider adapters that participate in the new loop override streamInference(...) to emit incremental Content events followed by one Complete event containing the assembled assistant message and any requested tool calls.

Tool-call inspectors are owned by DefaultStreamingToolLoop when using the new path because that layer performs the actual tool execution. This provides consistent callback behavior across Spring AI, LangChain4j, and future provider integrations while avoiding provider-specific or duplicate callbacks.

Dependency Boundary

The provider-neutral streaming contracts and tool loop contain no Spring AI imports or Spring AI types.

Spring-specific message conversion, response aggregation, and model interaction remain under the existing spi.support.springai.streaming implementation package.

LangChain4j dependencies are test-scoped and confined to the Ollama autoconfigure module.

Unfolding Tools

The integration coverage verifies the complete unfolding sequence:

  1. The initial inference receives only the unfolding facade.
  2. The model invokes the facade.
  3. The facade is removed and its child tool is injected.
  4. The next inference receives the child in its actual tool set.
  5. The model invokes the child successfully.
  6. The child result appears in the final streamed response.

This confirms that the child tool is registered for the next inference rather than only being advertised in the facade’s result message.

Validation

Focused streaming unit tests:

mvn -pl embabel-agent-api -am \
  -Dtest=StreamingToolLoopTest,SpringAiLlmMessageStreamerTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

Result:

Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

LangChain4j/Ollama integration:

OLLAMA_BASE_URL=http://localhost:11434 \
OLLAMA_MODEL=qwen2.5:3b \
mvn -pl embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure \
  -Dtest=LangChain4jStreamingToolLoopIT test

Result:

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

@igordayen igordayen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@arnabnandy7 - thank you for contributing - very valuable! Please see comments. Also, it would be good to have a LangChain4j streaming test similar to the one in the "experimental" repo shared earlier. Thanks again!

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

@arnabnandy7 - thank you for contributing - very valuable! Please see comments. Also, it would be good to have a LangChain4j streaming test similar to the one in the "experimental" repo shared earlier. Thanks again!

Thanks. I’ll add a LangChain4j streaming integration test modeled on LangChainToolLoopIT.

@arnabnandy7
arnabnandy7 requested a review from igordayen July 26, 2026 07:02
@igordayen

Copy link
Copy Markdown
Contributor

@arnabnandy7 - great development, thank you very much for contributing!

@igordayen igordayen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@arnabnandy7 - added few more comments. praised development.

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

@arnabnandy7 - added few more comments. praised development.

Thanks I'll take up the next task, but since weekend is over it'll need sometime to clearing up those suggestions.

Maybe in next 2-3 days I'll push it. Appreciating your kind words and review.

@igordayen

Copy link
Copy Markdown
Contributor

@arnabnandy7 - could you please verify, time permitting:

  • proper scope: internal, private, sealed
  • no circular dependency between packages is introduced
  • Thank you

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

@arnabnandy7 - could you please verify, time permitting:

  • proper scope: internal, private, sealed
  • no circular dependency between packages is introduced
  • Thank you

Verified based on your suggestion:

  1. LlmInferenceStreamEvent is a sealed interface.
  2. DefaultStreamingToolLoop, SpringAiLlmMessageStreamer, and the shared tool-execution helpers are internal.
  3. Mutable implementation state and orchestration methods remain private.
  4. LlmMessageStreamer and StreamingToolLoop remain public because they are the vendor-neutral extension contracts.
  5. The shared helper is in spi.loop.support. The streaming package imports it, but the support package does not import the streaming package, so no circular dependency is introduced.
  6. The complete Maven reactor also passes under JDK 21.

@arnabnandy7
arnabnandy7 requested a review from igordayen July 27, 2026 18:59
igordayen referenced this pull request Jul 27, 2026
Signed-off-by: King Star <mcxin.y@gmail.com>
@igordayen

Copy link
Copy Markdown
Contributor

@arnabnandy7 - please also keep an eye on

PR #1819

file embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/streaming/ThinkingStreamSupport.kt
Seems overlapping?
Thanks!

@arnabnandy7

arnabnandy7 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@arnabnandy7 - please also keep an eye on

PR #1819

file embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/streaming/ThinkingStreamSupport.kt
Seems overlapping?
Thanks!

@igordayen I just checked, yes it'll require conflict resolution in that case I'd request to keep this PR in second in queue so that once #1819 merged I can resolve the conflict in meaningful manner.

@igordayen

Copy link
Copy Markdown
Contributor

@igordayen I just checked, yes it'll require conflict resolution in that case I'd request to keep this PR in second in queue so that once #1819 merged I can resolve the conflict in meaningful manner.

Thanks, @arnabnandy7. Could you please review #1819, please comment on it, especially on the newly introduced Streaming support artifact. Thank you

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

@igordayen local test results ended up 1 test failure most likely due to low end system configuration and low storage space remaining.

mvn -pl embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure -am `
  '-Dtest=LLMOllamaStreamingBuilderIT' `
  '-Dsurefire.failIfNoSpecifiedTests=false' test

Resulted

Tests run: 3, Failures: 1, Errors: 2, Skipped: 0
BUILD FAILURE

Environment that was used in this test;

Ollama:       0.32.5
Model:        qwen3:latest
Parameters:   8.2B
Quantization: Q4_K_M
Size:         5.2 GB on disk / 5.9 GB loaded
Processor:    100% CPU
Capabilities: completion, tools, thinking
System RAM:   8 GB

I conclude with this my machine being too constrained for the tests’ 10/150-second limits. I guess I would require you to run this for all 3 to do a validation.

Report.xml
SureFireSummary.txt

A/B comparison confirms these observed failures are not introduced by this PR. Both branches produced the same result:

Branch Tests Failures Errors
upstream/main (065df682c) 3 1 2
feature/streaming-tool-loop (6299c6505) 3 1 2

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

@igordayen please proceed with testing, it's passing now;

The reduced-load Ollama regression passed completely.

Test configuration:

  • Model used during test: qwen3:1.7b
  • Actual parameters: 2.0B
  • Quantization: Q4_K_M
  • Capabilities: completion, tools, thinking
  • Branch: feature/streaming-tool-loop
  • JDK: 21

Result:

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

All three paths passed:

  • Tool/object streaming
  • Raw-text streaming
  • Direct Spring Ollama streaming

Total test-class time was 57.40 seconds, compared with the 8.2B model exceeding the 150-second limits.

@igordayen

Copy link
Copy Markdown
Contributor

Thanks, I will.
What pattern do you observe from logs when testing streaming+tooling+thinking? thanks.

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

Thanks, I will. What pattern do you observe from logs when testing streaming+tooling+thinking? thanks.

@igordayen If you try with qwen3:1.7b, the observable pattern is:

  1. The model reasons before requesting the tool.
  2. The tool call is assembled and executed.
  3. A second model inference starts with the tool result.
  4. Only the final answer is emitted through the content stream.

The reasoning generated around the tool-call boundary is not exposed as separate streaming events. In particular, thinking before/between tool calls is absent from the consumer-visible stream, even though the tool executes and the final response completes successfully.

Therefore, the existing Ollama tests confirm that streaming and tooling work, but they do not confirm preservation of thinking across tool calls. This matches the limitation discussed in the issue and is the reason for keeping thinking separate from ordinary content in the vendor-neutral streaming abstraction.

PS: I just added LangChain4j streaming integration test as well.

@igordayen igordayen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@arnabnandy7 - good progress, thanks, could you please review comments

Comment thread embabel-agent-api/pom.xml Outdated
@igordayen

Copy link
Copy Markdown
Contributor

Tested OpenAI and Anthropic Integration tests:

git status
On branch feature/streaming-tool-loop

streaming-logs.txt

Could you please review the logs?
If I'm not mistaken, Anthropic reasoning is captured between tool calls too.
Thanks.

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

Tested OpenAI and Anthropic Integration tests:

git status On branch feature/streaming-tool-loop

streaming-logs.txt

Could you please review the logs? If I'm not mistaken, Anthropic reasoning is captured between tool calls too. Thanks.

You are correct that application-level Thinking events appear both before and after tool execution in these logs. The observed sequence is thinking events, two tool calls, another thinking event, streamed objects, and a final thinking event.

One distinction is that this test classifies non-JSON streamed prose as Thinking. Therefore, it confirms that application-level thinking events survive the tool boundary, but it does not by itself establish that Anthropic native extended-reasoning blocks are being preserved. Dedicated native-reasoning event coverage would be needed to confirm that separately.

@arnabnandy7
arnabnandy7 requested a review from igordayen July 29, 2026 16:33
@igordayen

Copy link
Copy Markdown
Contributor

One distinction is that this test classifies non-JSON streamed prose as Thinking. Therefore, it confirms that application-level thinking events survive the tool boundary, but it does not by itself establish that Anthropic native extended-reasoning blocks are being preserved. Dedicated native-reasoning event coverage would be needed to confirm that separately

==> Is this something you manage to have - specifically Anthropic thinking preservation before/after tool call?

As I do not remember seeing it before, that was one of the reasons to have this PR.

So assume I need to come back to the 'main' branch and re-test, thanks

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

One distinction is that this test classifies non-JSON streamed prose as Thinking. Therefore, it confirms that application-level thinking events survive the tool boundary, but it does not by itself establish that Anthropic native extended-reasoning blocks are being preserved. Dedicated native-reasoning event coverage would be needed to confirm that separately

==> Is this something you manage to have - specifically Anthropic thinking preservation before/after tool call?

As I do not remember seeing it before, that was one of the reasons to have this PR.

So assume I need to come back to the 'main' branch and re-test, thanks

@igordayen this PR preserves streamed text across the tool boundary. Content received before the tool call is forwarded, the tool executes, and content from the continuation inference is forwarded afterward.

However, after tracing Spring AI’s Anthropic representation, I need to qualify the native-thinking aspect. Spring AI marks Anthropic thinking chunks with metadata["thinking"] = true, while the current vendor-neutral adapter forwards only their text as Content. The downstream converter can emit those lines as Thinking, but it currently loses the provider’s native thinking marker.

Therefore, an A/B run against main is useful, but the existing logs alone do not prove native Anthropic thinking preservation. We should explicitly enable Anthropic thinking and assert native thinking events before and after a tool call. This would establish whether the PR merely preserves visible prose or fully preserves Anthropic thinking semantics.

Would you like me to add the provider-neutral thinking marker and the Anthropic integration coverage to this PR?

@igordayen

Copy link
Copy Markdown
Contributor

Re-tested main branch:


14:53:56.755 [main] INFO  LLMAnthropicStreamingBuilderIT - Stream subscription started
14:53:58.326 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: 'I'll provide'
14:53:58.512 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' you with the two hottest months in Florida and convert their temperatures to Celsius.\n\nThe two hottest months in Florida are typically **July** and **August**, with average'
14:53:58.513 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'I'll provide you with the two hottest months in Florida and convert their temperatures to Celsius.'
14:53:58.518 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received thinking: I'll provide you with the two hottest months in Florida and convert their temperatures to Celsius.
14:53:58.917 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' high temperatures around 92°F (33°C). Let me convert these temperatures to Celsius for'
14:53:59.394 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' you.'
14:53:59.774 [boundedElastic-2] DEBUG DefaultToolCallingManager - Executing tool call: convertFromFahrenheitToCelsius
14:53:59.778 [boundedElastic-2] INFO  LLMAnthropicStreamingBuilderIT - beforeToolCall: tool=convertFromFahrenheitToCelsius, argsLength=17
14:53:59.781 [boundedElastic-2] INFO  Embabel - [mystifying_payne] calling tool convertFromFahrenheitToCelsius({"inputTemp": 92})
14:53:59.788 [boundedElastic-2] INFO  Embabel - [mystifying_payne] tool convertFromFahrenheitToCelsius returned 30 in 6ms with payload {"inputTemp": 92}
14:53:59.791 [boundedElastic-2] INFO  LLMAnthropicStreamingBuilderIT - afterToolCall: tool=convertFromFahrenheitToCelsius, status=Text, resultLength=2, durationMs=10
14:53:59.791 [boundedElastic-2] DEBUG DefaultToolCallingManager - Executing tool call: convertFromFahrenheitToCelsius
14:53:59.792 [boundedElastic-2] INFO  LLMAnthropicStreamingBuilderIT - beforeToolCall: tool=convertFromFahrenheitToCelsius, argsLength=17
14:53:59.792 [boundedElastic-2] INFO  Embabel - [mystifying_payne] calling tool convertFromFahrenheitToCelsius({"inputTemp": 92})
14:53:59.793 [boundedElastic-2] INFO  Embabel - [mystifying_payne] tool convertFromFahrenheitToCelsius returned 30 in 1ms with payload {"inputTemp": 92}
14:53:59.793 [boundedElastic-2] INFO  LLMAnthropicStreamingBuilderIT - afterToolCall: tool=convertFromFahrenheitToCelsius, status=Text, resultLength=2, durationMs=1
14:53:59.803 [boundedElastic-2] DEBUG CacheEligibilityResolver - Caching not enabled for tool definition, cacheStrategy=NONE
14:54:01.192 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: 'The two'
14:54:01.687 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' hottest months in Florida are July and August, with average high temperatures typically around'
14:54:02.296 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' 92°F (30°C).\n\nHere are the results in JSONL format:\n\n{"name":"'
14:54:02.296 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'The two hottest months in Florida are typically **July** and **August**, with average high temperatures around 92°F (33°C). Let me convert these temperatures to Celsius for you.The two hottest months in Florida are July and August, with average high temperatures typically around 92°F (30°C).'
14:54:02.296 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received thinking: The two hottest months in Florida are typically **July** and **August**, with average high temperatures around 92°F (33°C). Let me convert these temperatures to Celsius for you.The two hottest months in Florida are July and August, with average high temperatures typically around 92°F (30°C).
14:54:02.297 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'Here are the results in JSONL format:'
14:54:02.297 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received thinking: Here are the results in JSONL format:
14:54:02.608 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: 'July","temperature":30}\n{"name":"August","temperature":30}'
14:54:02.609 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '{"name":"July","temperature":30}'
14:54:02.612 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received object: July
14:54:02.613 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - FINAL LINE: '{"name":"August","temperature":30}'
14:54:02.614 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '{"name":"August","temperature":30}'
14:54:02.614 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received object: August
14:54:02.614 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test stream completed successfully
14:54:02.615 [main] INFO  LLMAnthropicStreamingBuilderIT - Integration streaming test completed successfully with 5 total events
14:54:02.627 [SpringApplicationShutdownHook] INFO  JvmType - Clearing JvmType children cache (0 entries)


it appears that thinking before tool invocation still gets preserved.

@igordayen

Copy link
Copy Markdown
Contributor

Could you please reconfirm unfolding tool behavior. Thank you.

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

Could you please reconfirm unfolding tool behavior. Thank you.

I'll check and share by tomorrow EOD

@igordayen

igordayen commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Compiling TODOs:

  1. Retest after merging native thinking with Anthropic:
 LlmOptions thinkingOptions = new LlmOptions().withThinking(Thinking.withTokenBudget(8000));
            PromptRunner runner = ai.withDefaultLlm().withLlm(thinkingOptions);
  1. Retest Unfolding tools ==> DONE!
  2. Strong rationale for dropping 3 tools-related args.
  3. ToolLoop is having 3-args interface, named "exec". Keep the name for symmetry: "execute Streaming tool loop"
  4. Are the changed signatures backward compatible
  5. Dependency o Spring AI is only limited to spring ai artifacts

Thanks.

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

Could you please reconfirm unfolding tool behavior. Thank you.

Thanks for confirming that thinking before tool invocation remains preserved.

I also reconfirmed unfolding-tool behavior using the real LangChain4j/Ollama streaming path. The first request exposes only the unfolding facade. After invoking it, the next request exposes the unfolded child tool, which Ollama successfully invokes before returning its result in the final streamed response.

The test passed:

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

This confirms that the unfolded child is added to the actual tool set supplied on the following inference turn, rather than merely being advertised in the facade’s result message.

@arnabnandy7
arnabnandy7 force-pushed the feature/streaming-tool-loop branch 2 times, most recently from 905c2e6 to 390032e Compare July 31, 2026 17:18
@igordayen

Copy link
Copy Markdown
Contributor

@arnabnandy7 - could you please advise on inquiries:

  1. Retest after merging native thinking with Anthropic:
    LlmOptions thinkingOptions = new LlmOptions().withThinking(Thinking.withTokenBudget(8000));
    PromptRunner runner = ai.withDefaultLlm().withLlm(thinkingOptions);
    i will do this
  2. Strong rationale for dropping 3 tools-related args.
  3. ToolLoop is having 3-args interface, named "exec". Keep the name for symmetry: "execute Streaming tool loop"
  4. Are the changed signatures backward compatible
  5. Dependency o Spring AI is only limited to spring ai artifacts
    Thanks

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

@arnabnandy7 - could you please advise on inquiries:

  1. Retest after merging native thinking with Anthropic:
    LlmOptions thinkingOptions = new LlmOptions().withThinking(Thinking.withTokenBudget(8000));
    PromptRunner runner = ai.withDefaultLlm().withLlm(thinkingOptions);
    i will do this
  2. Strong rationale for dropping 3 tools-related args.
  3. ToolLoop is having 3-args interface, named "exec". Keep the name for symmetry: "execute Streaming tool loop"
  4. Are the changed signatures backward compatible
  5. Dependency o Spring AI is only limited to spring ai artifacts
    Thanks

I'm working on these To-dos only, thanks for reminding me. 😊

@arnabnandy7
arnabnandy7 force-pushed the feature/streaming-tool-loop branch from 390032e to e1c23f1 Compare July 31, 2026 17:33
@arnabnandy7

Copy link
Copy Markdown
Contributor Author

@igordayen thanks for consolidating the TODOs. Here is the current status:

  1. Retest after merging native thinking with Anthropic:

As noted, you will run this using:

LlmOptions thinkingOptions =
    new LlmOptions().withThinking(Thinking.withTokenBudget(8000));
PromptRunner runner = ai.withDefaultLlm().withLlm(thinkingOptions);
  1. Strong rationale for dropping 3 tools-related args.

I restored the original three-argument contract:

stream(messages, tools, toolCallInspectors): Flux<String>

This preserves compatibility for existing callers and implementations.

The new streamInference(messages, tools) method intentionally does not accept inspectors. It performs exactly one provider inference and does not execute tools. Tool-call inspectors belong to DefaultStreamingToolLoop, because that layer now owns actual tool execution. Keeping inspectors there ensures that callbacks surround the real execution consistently across Spring AI, LangChain4j, and future providers, while avoiding provider-specific or duplicate callbacks.

  1. ToolLoop is having 3-args interface, named "exec". Keep the name for symmetry: "execute Streaming tool loop"

StreamingToolLoop.stream(...) has been renamed to:

StreamingToolLoop.execute(...)

This now follows the naming of the blocking ToolLoop.execute(...) contract.

  1. Are the changed signatures backward compatible

The existing public LlmMessageStreamer.stream(messages, tools, toolCallInspectors): Flux<String> signature has been retained as the functional-interface method.

The event-oriented behavior required by the Embabel-managed streaming loop is exposed through the additional default streamInference(...) method. Existing implementations remain valid, while adapters that support the new tool loop can override streamInference(...) to provide the assembled terminal assistant message and tool calls.

A regression test verifies that the original raw-content streaming signature still works.

  1. Dependency o Spring AI is only limited to spring ai artifacts

The provider-neutral streaming package has no Spring AI imports or Spring AI types in its public contracts. Spring-specific message conversion, aggregation, and model interaction remain under the existing spi.support.springai.streaming implementation package.

The LangChain4j dependencies are test-scoped and confined to the Ollama autoconfigure module.

Verification after these changes:

Streaming unit tests:
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0

LangChain4j/Ollama integration:
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

The Ollama integration also reconfirmed that the unfolding facade is removed, the child tool is added to the next inference, and the child is successfully invoked.

@igordayen

Copy link
Copy Markdown
Contributor

Thanks, im offline now, will resume late night. Long drive. Please share your discord contact. Thanks

@arnabnandy7

arnabnandy7 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, im offline now, will resume late night. Long drive. Please share your discord contact. Thanks

I'm available in discord with arnabnandy2, safe travel. Take care.

@arnabnandy7
arnabnandy7 force-pushed the feature/streaming-tool-loop branch from e1c23f1 to 8c9a698 Compare August 1, 2026 05:41
@arnabnandy7

Copy link
Copy Markdown
Contributor Author

Sanity check is completed and PR description updated including the latest two merge related refactors.

It's open for final review.

@igordayen igordayen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@arnabnandy7 - could you please review few inquiries, thank you

Signed-off-by: Arnab Nandy <arnab_nandy7@yahoo.com>
@arnabnandy7
arnabnandy7 force-pushed the feature/streaming-tool-loop branch from 8c9a698 to 3ac9b73 Compare August 1, 2026 18:44
@arnabnandy7

Copy link
Copy Markdown
Contributor Author

@igordayen latest test results with comments being addressed

Test class Tests Failures Errors Skipped
StreamingToolLoopTest 5 0 0 0
SpringAiLlmMessageStreamerTest 4 0 0 0
LangChain4jStreamingToolLoopIT 2 0 0 0
Total 11 0 0 0

@arnabnandy7
arnabnandy7 requested a review from igordayen August 1, 2026 18:56

@igordayen igordayen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@arnabnandy7 - long journey, thank you, looks good.

@igordayen
igordayen merged commit 5d28af7 into embabel:main Aug 1, 2026
7 checks passed
@igordayen

Copy link
Copy Markdown
Contributor

Results without native thinking - looks good so far:

16:19:11.549 [main] DEBUG StreamingLlmOperationsImpl - STREAMING FORMAT INSTRUCTIONS: 
Your response should be in JSONL (JSON Lines) format.
Each line must contain exactly one JSON object that strictly adheres to the provided schema.
Do not include any explanations in the JSON objects themselves.
Do not include markdown code blocks or wrap responses in arrays.
Ensure RFC7464 compliant JSON Lines, one valid JSON object per line.


Here is the JSON Schema instance each JSON object must adhere to:
```{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "name" : {
      "type" : "string"
    },
    "temperature" : {
      "type" : "integer"
    }
  },
  "additionalProperties" : false
}```


16:19:11.592 [main] INFO  LLMAnthropicStreamingBuilderIT - Stream subscription started
16:19:11.610 [main] DEBUG CacheEligibilityResolver - Caching not enabled for tool definition, cacheStrategy=NONE
16:19:14.430 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: 'I'll provide'
16:19:14.926 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' information about the two hottest months in Florida and convert their temperatures to Celsius.\n\nThe two hottest months in Florida are typically **July'
16:19:14.926 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'I'll provide information about the two hottest months in Florida and convert their temperatures to Celsius.'
16:19:14.931 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received thinking: I'll provide information about the two hottest months in Florida and convert their temperatures to Celsius.
16:19:15.233 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '** and **August**, with average high temperatures around 90-92°F (though'
16:19:15.745 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' it can vary slightly by location).\n\nLet me convert these temperatures to Celsius:'
16:19:15.745 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'The two hottest months in Florida are typically **July** and **August**, with average high temperatures around 90-92°F (though it can vary slightly by location).'
16:19:15.745 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received thinking: The two hottest months in Florida are typically **July** and **August**, with average high temperatures around 90-92°F (though it can vary slightly by location).
16:19:16.902 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - beforeToolCall: tool=convertFromFahrenheitToCelsius, argsLength=17
16:19:16.902 [boundedElastic-1] WARN  ObservabilityTool - No parent observation for tool call convertFromFahrenheitToCelsius with input: {"inputTemp": 90}, observation registry: ObservationRegistry.NOOP
16:19:16.905 [boundedElastic-1] INFO  Embabel - [clever_aryabhata] calling tool convertFromFahrenheitToCelsius({"inputTemp": 90})
16:19:16.912 [boundedElastic-1] INFO  Embabel - [clever_aryabhata] tool convertFromFahrenheitToCelsius returned 29 in 6ms with payload {"inputTemp": 90}
16:19:16.915 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - afterToolCall: tool=convertFromFahrenheitToCelsius, status=Text, resultLength=2, durationMs=11
16:19:16.917 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - beforeToolCall: tool=convertFromFahrenheitToCelsius, argsLength=17
16:19:16.917 [boundedElastic-1] WARN  ObservabilityTool - No parent observation for tool call convertFromFahrenheitToCelsius with input: {"inputTemp": 92}, observation registry: ObservationRegistry.NOOP
16:19:16.917 [boundedElastic-1] INFO  Embabel - [clever_aryabhata] calling tool convertFromFahrenheitToCelsius({"inputTemp": 92})
16:19:16.917 [boundedElastic-1] INFO  Embabel - [clever_aryabhata] tool convertFromFahrenheitToCelsius returned 30 in 0ms with payload {"inputTemp": 92}
16:19:16.917 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - afterToolCall: tool=convertFromFahrenheitToCelsius, status=Text, resultLength=2, durationMs=0
16:19:16.928 [boundedElastic-1] DEBUG CacheEligibilityResolver - Caching not enabled for tool definition, cacheStrategy=NONE
16:19:17.794 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: 'The two'
16:19:18.304 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' hottest months in Florida are **July** and **August**. Based on typical average high'
16:19:18.817 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' temperatures:\n\n{"name":"July","temperature":30}\n{"name":"August","temperature":30}\n\nNote: These temperatures represent typical'
16:19:18.817 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'Let me convert these temperatures to Celsius:The two hottest months in Florida are **July** and **August**. Based on typical average high temperatures:'
16:19:18.817 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received thinking: Let me convert these temperatures to Celsius:The two hottest months in Florida are **July** and **August**. Based on typical average high temperatures:
16:19:18.817 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '{"name":"July","temperature":30}'
16:19:18.820 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received object: July
16:19:18.821 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '{"name":"August","temperature":30}'
16:19:18.821 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received object: August
16:19:19.206 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' average high temperatures converted to Celsius. July typically reaches around 92°F (33°C) and August around 92°F (33°C)'
16:19:19.636 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' in many parts of Florida, though actual temperatures can vary by location within the state.'
16:19:19.638 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - FINAL LINE: 'Note: These temperatures represent typical average high temperatures converted to Celsius. July typically reaches around 92°F (33°C) and August around 92°F (33°C) in many parts of Florida, though actual temperatures can vary by location within the state.'
16:19:19.639 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'Note: These temperatures represent typical average high temperatures converted to Celsius. July typically reaches around 92°F (33°C) and August around 92°F (33°C) in many parts of Florida, though actual temperatures can vary by location within the state.'
16:19:19.639 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test received thinking: Note: These temperatures represent typical average high temperatures converted to Celsius. July typically reaches around 92°F (33°C) and August around 92°F (33°C) in many parts of Florida, though actual temperatures can vary by location within the state.
16:19:19.640 [boundedElastic-1] INFO  LLMAnthropicStreamingBuilderIT - Integration test stream completed successfully
16:19:19.640 [main] INFO  LLMAnthropicStreamingBuilderIT - Integration streaming test completed successfully with 6 total events
16:19:19.654 [SpringApplicationShutdownHook] INFO  JvmType - Clearing JvmType children cache (0 entries)

@igordayen

Copy link
Copy Markdown
Contributor

With NATIVE Thinking - requires attention:

16:29:16.399 [main] DEBUG CacheEligibilityResolver - Caching not enabled for tool definition, cacheStrategy=NONE
16:29:17.767 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: 'The user is asking about'
16:29:18.166 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' the two hottest months in Florida and their highest temperatures. They want me to convert the temperatures to Celsius using the available tools'
16:29:18.673 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '.\n\nBased on general knowledge, the hottest months in Florida are typically July and August. The average high temperatures during'
16:29:18.673 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'The user is asking about the two hottest months in Florida and their highest temperatures. They want me to convert the temperatures to Celsius using the available tools.'
16:29:18.679 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: The user is asking about the two hottest months in Florida and their highest temperatures. They want me to convert the temperatures to Celsius using the available tools.
16:29:19.111 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' these months are around 90-92°F.\n\nHowever, I need to be precise about what "highest'
16:29:19.111 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'Based on general knowledge, the hottest months in Florida are typically July and August. The average high temperatures during these months are around 90-92°F.'
16:29:19.111 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: Based on general knowledge, the hottest months in Florida are typically July and August. The average high temperatures during these months are around 90-92°F.
16:29:19.624 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' temperatures" means - this could mean:\n1. Average high temperatures for those months\n2. Record high temperatures ever recorded in those'
16:29:19.624 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'However, I need to be precise about what "highest temperatures" means - this could mean:'
16:29:19.624 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: However, I need to be precise about what "highest temperatures" means - this could mean:
16:29:19.625 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '1. Average high temperatures for those months'
16:29:19.625 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: 1. Average high temperatures for those months
16:29:19.959 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' months\n\nSince the user asks for "their respective highest temperatures" and wants exactly two months'
16:29:19.959 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '2. Record high temperatures ever recorded in those months'
16:29:19.959 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: 2. Record high temperatures ever recorded in those months
16:29:20.399 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ', I'll interpret this as the typical peak temperatures during the'
16:29:21.131 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' hottest months. In Florida:\n- July: typically around 91-92°F average high'
16:29:21.131 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'Since the user asks for "their respective highest temperatures" and wants exactly two months, I'll interpret this as the typical peak temperatures during the hottest months. In Florida:'
16:29:21.131 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: Since the user asks for "their respective highest temperatures" and wants exactly two months, I'll interpret this as the typical peak temperatures during the hottest months. In Florida:
16:29:21.415 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '\n- August: typically around 91-92°F average high\n\nFor conversion purposes, I'll use reasonable values. Let me use:'
16:29:21.415 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '- July: typically around 91-92°F average high'
16:29:21.416 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: - July: typically around 91-92°F average high
16:29:21.416 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '- August: typically around 91-92°F average high'
16:29:21.416 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: - August: typically around 91-92°F average high
16:29:21.847 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '\n- July: 92°F\n- August: 91°F\n\nI need to convert these from Fahrenheit to Celsius using the convertFromFahrenheitToCelsius function.\n\nLet'
16:29:21.847 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'For conversion purposes, I'll use reasonable values. Let me use:'
16:29:21.847 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: For conversion purposes, I'll use reasonable values. Let me use:
16:29:21.848 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '- July: 92°F'
16:29:21.848 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: - July: 92°F
16:29:21.848 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '- August: 91°F'
16:29:21.848 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: - August: 91°F
16:29:21.849 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'I need to convert these from Fahrenheit to Celsius using the convertFromFahrenheitToCelsius function.'
16:29:21.849 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: I need to convert these from Fahrenheit to Celsius using the convertFromFahrenheitToCelsius function.
16:29:22.359 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' me make the function calls to convert both temperatures.'
16:29:22.382 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '\nThe user is asking for the two hottest months in Florida and their highest temperatures,'
16:29:22.383 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'Let me make the function calls to convert both temperatures.'
16:29:22.383 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: Let me make the function calls to convert both temperatures.
16:29:22.973 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' converted to Celsius. Based on climatological data, the two hottest months in Florida are typically July and August, with average high'
16:29:23.286 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' temperatures around 91-92°F. I'll convert these temperatures from Fahrenheit to Celsius using the available tool.\n'
16:29:23.286 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'The user is asking for the two hottest months in Florida and their highest temperatures, converted to Celsius. Based on climatological data, the two hottest months in Florida are typically July and August, with average high temperatures around 91-92°F. I'll convert these temperatures from Fahrenheit to Celsius using the available tool.'
16:29:23.286 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: The user is asking for the two hottest months in Florida and their highest temperatures, converted to Celsius. Based on climatological data, the two hottest months in Florida are typically July and August, with average high temperatures around 91-92°F. I'll convert these temperatures from Fahrenheit to Celsius using the available tool.
16:29:24.137 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - beforeToolCall: tool=convertFromFahrenheitToCelsius, argsLength=17
16:29:24.138 [boundedElastic-1] WARN ObservabilityTool - No parent observation for tool call convertFromFahrenheitToCelsius with input: {"inputTemp": 92}, observation registry: ObservationRegistry.NOOP
16:29:24.140 [boundedElastic-1] INFO Embabel - [exciting_burnell] calling tool convertFromFahrenheitToCelsius({"inputTemp": 92})
16:29:24.147 [boundedElastic-1] INFO Embabel - [exciting_burnell] tool convertFromFahrenheitToCelsius returned 30 in 6ms with payload {"inputTemp": 92}
16:29:24.150 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - afterToolCall: tool=convertFromFahrenheitToCelsius, status=Text, resultLength=2, durationMs=11
16:29:24.151 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - beforeToolCall: tool=convertFromFahrenheitToCelsius, argsLength=17
16:29:24.151 [boundedElastic-1] WARN ObservabilityTool - No parent observation for tool call convertFromFahrenheitToCelsius with input: {"inputTemp": 91}, observation registry: ObservationRegistry.NOOP
16:29:24.152 [boundedElastic-1] INFO Embabel - [exciting_burnell] calling tool convertFromFahrenheitToCelsius({"inputTemp": 91})
16:29:24.152 [boundedElastic-1] INFO Embabel - [exciting_burnell] tool convertFromFahrenheitToCelsius returned 29 in 0ms with payload {"inputTemp": 91}
16:29:24.152 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - afterToolCall: tool=convertFromFahrenheitToCelsius, status=Text, resultLength=2, durationMs=1
16:29:24.164 [boundedElastic-1] DEBUG CacheEligibilityResolver - Caching not enabled for tool definition, cacheStrategy=NONE
16:29:25.331 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '<think'
16:29:25.841 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '>\nThe two hottest months in Florida are typically July and August. Based on climate data:'
16:29:25.841 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: ''
16:29:25.841 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking:
16:29:26.250 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '\n- July: Average high around 91-92°F\n- August: Average high around 91-92°F\n\nI'
16:29:26.250 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'The two hottest months in Florida are typically July and August. Based on climate data:'
16:29:26.250 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: The two hottest months in Florida are typically July and August. Based on climate data:
16:29:26.251 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '- July: Average high around 91-92°F'
16:29:26.251 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: - July: Average high around 91-92°F
16:29:26.251 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '- August: Average high around 91-92°F'
16:29:26.251 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: - August: Average high around 91-92°F
16:29:26.762 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ''ve converted the temperatures from Fahrenheit to Celsius using the tool:\n- 92°F = 30°C\n- 91°F = '
16:29:26.762 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'I've converted the temperatures from Fahrenheit to Celsius using the tool:'
16:29:26.762 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: I've converted the temperatures from Fahrenheit to Celsius using the tool:
16:29:26.763 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '- 92°F = 30°C'
16:29:26.763 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: - 92°F = 30°C
16:29:27.274 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '29°C\n\nSince both months have very similar temperatures, I'll use July at'
16:29:27.274 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '- 91°F = 29°C'
16:29:27.275 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: - 91°F = 29°C
16:29:27.684 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' 92°F (30°C) and August at 91°F (29°C) as the two hottest months, though they're nearly'
16:29:28.201 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' tied.\n\nNow I need to output this in JSON Lines format according to the schema provided,'
16:29:28.201 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'Since both months have very similar temperatures, I'll use July at 92°F (30°C) and August at 91°F (29°C) as the two hottest months, though they're nearly tied.'
16:29:28.202 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: Since both months have very similar temperatures, I'll use July at 92°F (30°C) and August at 91°F (29°C) as the two hottest months, though they're nearly tied.
16:29:28.582 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' which requires "name" (string) and "temperature" (integer) fields.\n\n\n{"name": "July",'
16:29:28.583 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: 'Now I need to output this in JSON Lines format according to the schema provided, which requires "name" (string) and "temperature" (integer) fields.'
16:29:28.583 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking: Now I need to output this in JSON Lines format according to the schema provided, which requires "name" (string) and "temperature" (integer) fields.
16:29:28.583 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: ''
16:29:28.583 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received thinking:
16:29:28.810 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: ' "temperature": 30}\n{"name": "August", "temperature": 29}'
16:29:28.810 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '{"name": "July", "temperature": 30}'
16:29:28.813 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received object: July
16:29:28.816 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - FINAL LINE: '{"name": "August", "temperature": 29}'
16:29:28.817 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '{"name": "August", "temperature": 29}'
16:29:28.817 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test received object: August
16:29:28.817 [boundedElastic-1] INFO LLMAnthropicStreamingBuilderIT - Integration test stream completed successfully
16:29:28.818 [main] INFO LLMAnthropicStreamingBuilderIT - Integration streaming test completed successfully with 26 total events
16:29:28.832 [SpringApplicationShutdownHook] INFO JvmType - Clearing JvmType children cache (0 entries)



Note:

[16:29:25.331 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '<think'
16:29:25.841 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - RAW CHUNK: '>\nThe two hottest months in Florida are typically July and August. Based on climate data:'
16:29:25.841 [boundedElastic-1] TRACE StreamingLlmOperationsImpl - COMPLETE LINE: '


it was discussed separately on ```generate stream```

How do you know in advance how long the sequence of raw chunks <think>.......</think> is? If it's too long, it defeats the purpose of streaming.

But putting this aside, could you please carefully verify every line in the log to ensure the reasoning before calling the tool execution and in between is preserved?

thats how to enable:

LlmOptions thinkingOptions = new LlmOptions().withThinking(Thinking.withTokenBudget(8000));

    // Given: Use the existing streaming test LLM (configured as "best")
    PromptRunner runner = ai.withDefaultLlm().withLlm(thinkingOptions)```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming Tool Loop

2 participants