This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 945dddabe7d2 CAMEL-23704: Use isolated exchange copy per tool in 
langchain4j-tools
945dddabe7d2 is described below

commit 945dddabe7d20120c4471b676f3915bb939a819c
Author: Karol Krawczyk <[email protected]>
AuthorDate: Wed Jun 17 09:29:51 2026 +0200

    CAMEL-23704: Use isolated exchange copy per tool in langchain4j-tools
    
    When the LLM requests multiple tool invocations, each tool now runs on
    its own independent copy of the original exchange (via 
ExchangeHelper.createCopy),
    mirroring the multicast/splitter isolation pattern. This prevents message 
body,
    argument headers, and exceptions from leaking between tool invocations.
    
    The per-iteration ROUTE_STOP reset from CAMEL-21937 is subsumed by the 
per-tool
    exchange isolation; the post-loop reset is kept since copyResults 
propagates the
    last tool's routeStop flag onto the parent exchange.
    
    Closes #24062
---
 .../tools/LangChain4jToolsProducer.java            | 24 +++++++++++++---------
 .../tools/LangChain4jToolMultipleCallsTest.java    | 17 +++++++++------
 .../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc    | 12 +++++++++++
 3 files changed, 37 insertions(+), 16 deletions(-)

diff --git 
a/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolsProducer.java
 
b/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolsProducer.java
index 395f96cfce29..f49ac949559c 100644
--- 
a/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolsProducer.java
+++ 
b/components/camel-ai/camel-langchain4j-tools/src/main/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolsProducer.java
@@ -46,6 +46,7 @@ import org.apache.camel.TypeConverter;
 import 
org.apache.camel.component.langchain4j.tools.spec.CamelToolExecutorCache;
 import 
org.apache.camel.component.langchain4j.tools.spec.CamelToolSpecification;
 import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -119,6 +120,8 @@ public class LangChain4jToolsProducer extends 
DefaultProducer {
             return null;
         }
 
+        final Exchange baseline = ExchangeHelper.createCopy(exchange, true);
+
         // First talk to the model to get the tools to be called
         int i = 0;
         do {
@@ -129,7 +132,7 @@ public class LangChain4jToolsProducer extends 
DefaultProducer {
             }
 
             // Only invoke the tools ... the response will be computed on the 
next loop
-            invokeTools(chatMessages, exchange, response, toolPair);
+            invokeTools(chatMessages, exchange, response, toolPair, baseline);
             LOG.debug("Finished iteration {}", i);
             i++;
         } while (true);
@@ -153,7 +156,8 @@ public class LangChain4jToolsProducer extends 
DefaultProducer {
     }
 
     private void invokeTools(
-            List<ChatMessage> chatMessages, Exchange exchange, 
Response<AiMessage> response, ToolPair toolPair) {
+            List<ChatMessage> chatMessages, Exchange exchange, 
Response<AiMessage> response, ToolPair toolPair,
+            Exchange baseline) {
         int i = 0;
         List<ToolExecutionRequest> toolExecutionRequests = 
response.content().toolExecutionRequests();
         for (ToolExecutionRequest toolExecutionRequest : 
toolExecutionRequests) {
@@ -167,13 +171,11 @@ public class LangChain4jToolsProducer extends 
DefaultProducer {
                 continue;
             }
 
-            // Reset route stop flag from previous tool invocation to prevent
-            // stop() EIP in one tool from short-circuiting subsequent tools
-            exchange.setRouteStop(false);
-
             final CamelToolSpecification camelToolSpecification = 
toolPair.callableTools().stream()
                     .filter(c -> 
c.getToolSpecification().name().equals(toolName)).findFirst().get();
 
+            final Exchange toolExchange = ExchangeHelper.createCopy(baseline, 
true);
+
             try {
                 TypeConverter typeConverter = 
endpoint.getCamelContext().getTypeConverter();
 
@@ -213,22 +215,24 @@ public class LangChain4jToolsProducer extends 
DefaultProducer {
                                 headerValue = value;
                             }
 
-                            exchange.getMessage().setHeader(name, headerValue);
+                            toolExchange.getMessage().setHeader(name, 
headerValue);
                         });
 
                 // Execute the consumer route
 
-                
camelToolSpecification.getConsumer().getProcessor().process(exchange);
+                
camelToolSpecification.getConsumer().getProcessor().process(toolExchange);
                 i++;
             } catch (Exception e) {
                 // How to handle this exception?
-                exchange.setException(e);
+                toolExchange.setException(e);
             }
 
+            ExchangeHelper.copyResults(exchange, toolExchange);
+
             chatMessages.add(new ToolExecutionResultMessage(
                     toolExecutionRequest.id(),
                     toolExecutionRequest.name(),
-                    exchange.getIn().getBody(String.class)));
+                    toolExchange.getIn().getBody(String.class)));
         }
 
         // Clear route stop flag after all tools so it does not leak
diff --git 
a/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolMultipleCallsTest.java
 
b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolMultipleCallsTest.java
index 39a60358a9be..9c277d3b8c64 100644
--- 
a/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolMultipleCallsTest.java
+++ 
b/components/camel-ai/camel-langchain4j-tools/src/test/java/org/apache/camel/component/langchain4j/tools/LangChain4jToolMultipleCallsTest.java
@@ -47,7 +47,7 @@ public class LangChain4jToolMultipleCallsTest extends 
CamelTestSupport {
             .build();
 
     private volatile boolean intermediateCalled = false;
-    private volatile boolean intermediateHasValidBody = false;
+    private volatile boolean intermediateIsolated = false;
 
     @Override
     protected void setupResources() throws Exception {
@@ -82,10 +82,15 @@ public class LangChain4jToolMultipleCallsTest extends 
CamelTestSupport {
                         .process(exchange -> {
                             String body = 
exchange.getIn().getBody(String.class);
                             intermediateCalled = true;
-                            if (exchange.getIn().getHeader("longitude", 
String.class).contains("0") &&
-                                    exchange.getIn().getHeader("latitude", 
String.class).contains("51") &&
-                                    body.contains("51.50758961965397") && 
body.contains("-0.13388057363742217")) {
-                                intermediateHasValidBody = true;
+                            // CAMEL-23704: the forecast tool must see its own 
argument headers but not
+                            // inherit the previous tool's 'name' header or 
output body (it receives the
+                            // original incoming chat messages)
+                            boolean ownHeaders = 
exchange.getIn().getHeader("longitude", String.class).contains("0")
+                                    && exchange.getIn().getHeader("latitude", 
String.class).contains("51");
+                            boolean noHeaderLeak = 
exchange.getIn().getHeader("name") == null;
+                            boolean originalBody = body != null && 
body.contains("meteorologist");
+                            if (ownHeaders && noHeaderLeak && originalBody) {
+                                intermediateIsolated = true;
                             }
                         })
                         .setBody(simple("""
@@ -135,6 +140,6 @@ public class LangChain4jToolMultipleCallsTest extends 
CamelTestSupport {
         // depending on the reasoning model used to test, the result is 
different, but we asked for Celcius degree and 3 should be part of it
         Assertions.assertThat(responseContent).containsIgnoringCase("3");
         Assertions.assertThat(intermediateCalled).isTrue();
-        Assertions.assertThat(intermediateHasValidBody).isTrue();
+        Assertions.assertThat(intermediateIsolated).isTrue();
     }
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index 927432c7e34f..c04f31cc63ed 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -34,6 +34,18 @@ endpoint URI, which could leave the data format incompletely 
configured. This al
 `marshal()` / `unmarshal()` DSL, which already honors the global 
configuration. Options specified on the endpoint URI
 continue to take precedence over the global configuration.
 
+=== camel-langchain4j-tools
+
+When the LLM requests multiple tool invocations within a single request, each 
tool is now invoked on its
+own independent copy of the exchange (similar to the multicast/splitter 
patterns) instead of sharing a single
+exchange across all tools. This guarantees complete isolation: the message 
body, argument headers, and any
+exception produced by one tool no longer leak into subsequent tool invocations 
of the same request.
+
+As a consequence, a tool route no longer receives the previous tool's output 
as its message body, nor the
+previous tool's argument headers. Each tool route starts from the original 
incoming exchange and only sees
+its own declared arguments as headers. Routes that (intentionally or 
accidentally) relied on this leaked state
+between tools must be adjusted to carry such data explicitly.
+
 === camel-core
 
 ==== Simple language: internal builder classes reorganized

Reply via email to