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