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

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

commit c9d4f78d98b6fce3f51f7efd88cf0216f192af01
Author: James Netherton <[email protected]>
AuthorDate: Mon Sep 14 13:45:46 2026 +0100

    Exclude AiAgentWithoutMemoryService from Quarkus LangChain4j implied AI 
service registration
    
    When Quarkus LangChain4j is present with a default chat model configured, it
    registers AiAgentWithoutMemoryService as an implied AI service backed by 
the CDI
    ChatMemoryProvider, silently making AgentWithoutMemory stateful.
    
    Fixes #8836
    
    Co-Authored-By: Jiri Ondrusek <[email protected]>
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 .../SupportLangchain4jQl4jProcessor.java           | 12 ++++++
 .../agent/it/Langchain4jAgentQl4jResource.java}    | 41 +++++++++---------
 .../agent/it/Langchain4jAgentQl4jRoutes.java}      | 28 +++---------
 .../src/main/resources/application.properties      |  4 ++
 .../agent/it/Langchain4jAgentQl4jTest.java         | 50 +++++++++++++++++++++-
 5 files changed, 93 insertions(+), 42 deletions(-)

diff --git 
a/extensions-support/langchain4j-ql4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/ql4j/deployment/SupportLangchain4jQl4jProcessor.java
 
b/extensions-support/langchain4j-ql4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/ql4j/deployment/SupportLangchain4jQl4jProcessor.java
index d21f55f276..805868b04d 100644
--- 
a/extensions-support/langchain4j-ql4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/ql4j/deployment/SupportLangchain4jQl4jProcessor.java
+++ 
b/extensions-support/langchain4j-ql4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/ql4j/deployment/SupportLangchain4jQl4jProcessor.java
@@ -24,6 +24,7 @@ import dev.langchain4j.guardrail.Guardrail;
 import dev.langchain4j.guardrail.InputGuardrail;
 import dev.langchain4j.guardrail.OutputGuardrail;
 import io.quarkiverse.langchain4j.RegisterAiService;
+import 
io.quarkiverse.langchain4j.deployment.ExcludeFromImpliedAiServiceBuildItem;
 import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
 import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
 import io.quarkus.deployment.annotations.BuildProducer;
@@ -49,6 +50,8 @@ class SupportLangchain4jQl4jProcessor {
     private static final DotName REGISTER_AI_SERVICES_DOTNAME = 
DotName.createSimple(RegisterAiService.class);
     private static final DotName JSON_EXTRACTOR_OUTPUT_GUARDRAIL = DotName
             
.createSimple("dev.langchain4j.guardrails.JsonExtractorOutputGuardrail");
+    private static final DotName AI_AGENT_WITHOUT_MEMORY_SERVICE = DotName
+            
.createSimple("org.apache.camel.component.langchain4j.agent.api.AiAgentWithoutMemoryService");
     private static final Logger LOG = 
Logger.getLogger(SupportLangchain4jQl4jProcessor.class);
 
     @BuildStep
@@ -127,6 +130,15 @@ class SupportLangchain4jQl4jProcessor {
                 .build());
     }
 
+    @BuildStep
+    void excludeCamelAgentInterfacesFromImpliedRegistration(
+            CombinedIndexBuildItem combinedIndex,
+            BuildProducer<ExcludeFromImpliedAiServiceBuildItem> 
excludedFromImplied) {
+        if 
(combinedIndex.getIndex().getClassByName(AI_AGENT_WITHOUT_MEMORY_SERVICE) != 
null) {
+            excludedFromImplied.produce(new 
ExcludeFromImpliedAiServiceBuildItem(AI_AGENT_WITHOUT_MEMORY_SERVICE.toString()));
+        }
+    }
+
     @BuildStep
     void validateAndRegisterAiServices(
             CombinedIndexBuildItem indexBuildItem,
diff --git 
a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
 
b/integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jResource.java
similarity index 52%
copy from 
integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
copy to 
integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jResource.java
index 6dfceb6f57..45e27302e7 100644
--- 
a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
+++ 
b/integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jResource.java
@@ -16,27 +16,28 @@
  */
 package org.apache.camel.quarkus.component.langchain4j.agent.it;
 
-import io.quarkus.test.common.QuarkusTestResource;
-import io.quarkus.test.junit.QuarkusTest;
-import io.restassured.RestAssured;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.apache.camel.FluentProducerTemplate;
 
-import static org.hamcrest.Matchers.*;
+@Path("/langchain4j-agent-ql4j")
+@ApplicationScoped
+public class Langchain4jAgentQl4jResource {
+    @Inject
+    FluentProducerTemplate producerTemplate;
 
-@ExtendWith(Langchain4jTestWatcher.class)
-@QuarkusTestResource(Langchain4jAgentTestResource.class)
-@QuarkusTest
-class Langchain4jAgentQl4jTest {
-    @Test
-    void simpleUserMessage() {
-        RestAssured.given()
-                .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE)
-                .post("/langchain4j-agent/simple")
-                .then()
-                .statusCode(200)
-                .body(
-                        not(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE),
-                        containsString("Apache Camel"));
+    @Path("/simple")
+    @POST
+    @Consumes(MediaType.TEXT_PLAIN)
+    @Produces(MediaType.TEXT_PLAIN)
+    public String simple(String userMessage) {
+        return producerTemplate.to("seda:simple-agent-without-request-context")
+                .withBody(userMessage)
+                .request(String.class);
     }
 }
diff --git 
a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
 
b/integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jRoutes.java
similarity index 52%
copy from 
integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
copy to 
integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jRoutes.java
index 6dfceb6f57..ed2fe0b523 100644
--- 
a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
+++ 
b/integration-tests/langchain4j-agent-ql4j/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jRoutes.java
@@ -16,27 +16,13 @@
  */
 package org.apache.camel.quarkus.component.langchain4j.agent.it;
 
-import io.quarkus.test.common.QuarkusTestResource;
-import io.quarkus.test.junit.QuarkusTest;
-import io.restassured.RestAssured;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
+import org.apache.camel.builder.RouteBuilder;
 
-import static org.hamcrest.Matchers.*;
-
-@ExtendWith(Langchain4jTestWatcher.class)
-@QuarkusTestResource(Langchain4jAgentTestResource.class)
-@QuarkusTest
-class Langchain4jAgentQl4jTest {
-    @Test
-    void simpleUserMessage() {
-        RestAssured.given()
-                .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE)
-                .post("/langchain4j-agent/simple")
-                .then()
-                .statusCode(200)
-                .body(
-                        not(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE),
-                        containsString("Apache Camel"));
+public class Langchain4jAgentQl4jRoutes extends RouteBuilder {
+    @Override
+    public void configure() throws Exception {
+        // Invoke the agent on a seda consumer thread where there is no active 
CDI request context
+        from("seda:simple-agent-without-request-context")
+                .to("direct:simple-agent");
     }
 }
diff --git 
a/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties
 
b/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties
index 58f1089f9b..033697c56c 100644
--- 
a/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties
+++ 
b/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties
@@ -23,3 +23,7 @@ quarkus.langchain4j.devservices.preload=false
 # The custom AiService receives registered ai-tools (all-tools default for 
user-built
 # services), so it needs a tool-capable model; orca-mini rejects requests 
carrying tools
 agent.tests.custom-service-model-id=granite4:3b
+
+# Configure a default QL4J model to trigger implied AI service registration
+quarkus.langchain4j.ollama.base-url=${langchain4j.ollama.base-url}
+quarkus.langchain4j.ollama.chat-model.model-id=orca-mini
diff --git 
a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
 
b/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
index 6dfceb6f57..12c94db143 100644
--- 
a/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
+++ 
b/integration-tests/langchain4j-agent-ql4j/src/test/java/org/apache/camel/quarkus/component/langchain4j/agent/it/Langchain4jAgentQl4jTest.java
@@ -20,23 +20,71 @@ import io.quarkus.test.common.QuarkusTestResource;
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.RestAssured;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
 import org.junit.jupiter.api.extension.ExtendWith;
 
 import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 
 @ExtendWith(Langchain4jTestWatcher.class)
 @QuarkusTestResource(Langchain4jAgentTestResource.class)
 @QuarkusTest
 class Langchain4jAgentQl4jTest {
+    private static final String SIMPLE_ENDPOINT = "/langchain4j-agent/simple";
+    private static final String SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT = 
"/langchain4j-agent-ql4j/simple";
+    private static final String STATELESS_SECRET = "PARROT42";
+
     @Test
     void simpleUserMessage() {
         RestAssured.given()
                 .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE)
-                .post("/langchain4j-agent/simple")
+                .post(SIMPLE_ENDPOINT)
                 .then()
                 .statusCode(200)
                 .body(
                         not(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE),
                         containsString("Apache Camel"));
     }
+
+    @Test
+    @DisabledIfEnvironmentVariable(named = "LANGCHAIN4J_OLLAMA_BASE_URL", 
matches = ".+", disabledReason = "This test uses WireMock strict matching")
+    void agentWithoutMemoryIsStatelessWiremock() {
+        String first = RestAssured.given()
+                .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE)
+                .post(SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT)
+                .then()
+                .statusCode(200)
+                .extract().asString();
+
+        String second = RestAssured.given()
+                .body(Langchain4jAgentTest.TEST_USER_MESSAGE_SIMPLE)
+                .post(SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT)
+                .then()
+                .statusCode(200)
+                .extract().asString();
+
+        assertFalse(first.isEmpty(), "First response should not be empty");
+        assertFalse(second.isEmpty(), "Second response should not be empty");
+    }
+
+    @Test
+    @EnabledIfEnvironmentVariable(named = "LANGCHAIN4J_OLLAMA_BASE_URL", 
matches = ".+", disabledReason = "Requires a real LLM — set 
LANGCHAIN4J_OLLAMA_BASE_URL")
+    void agentWithoutMemoryIsStatelessRealApi() {
+        RestAssured.given()
+                .body("The secret code is " + STATELESS_SECRET + ". 
Acknowledge it.")
+                .post(SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT)
+                .then()
+                .statusCode(200);
+
+        String second = RestAssured.given()
+                .body("What secret code did I tell you? Reply ONLY the code or 
say UNKNOWN.")
+                .post(SIMPLE_WITHOUT_REQUEST_CONTEXT_ENDPOINT)
+                .then()
+                .statusCode(200)
+                .extract().asString();
+
+        assertFalse(second.contains(STATELESS_SECRET),
+                "Agent should NOT remember the secret across calls — memory 
leaked");
+    }
 }

Reply via email to