Skip to content

Quarkus LangChain4j ChatMemoryProvider silently leaks into AgentWithoutMemory when default model is configured #8836

Description

@jamesnetherton

Description

When Quarkus LangChain4j (QL4J) is on the classpath and a default ChatModel is configured via quarkus.langchain4j.* properties, QL4J's ChatMemoryProvider silently leaks into Camel's AgentWithoutMemory, making it stateful despite the user's explicit intent for stateless behavior.

Root Cause

QL4J replaces AiServices.builder() via SPI (AiServicesFactory) in JVM mode and via GraalVM @Substitute in native mode. When Camel calls AiServices.builder(AiAgentWithoutMemoryService.class):

  1. QL4J's AiServicesProcessor.determinedImpliedRegisterAiService() discovers AiAgentWithoutMemoryService because it has @UserMessage and @SystemMessage annotations. The AGENTIC_PACKAGE_PREFIX exclusion (dev.langchain4j.agentic.*) does not match Camel's package (org.apache.camel.component.langchain4j.agent.api).

  2. QL4J creates a synthetic QuarkusAiServiceContext CDI bean for this interface. Since no explicit chatMemoryProviderSupplier is set on the implied registration, it defaults to BEAN_CHAT_MEMORY_PROVIDER_SUPPLIER — meaning "inject ChatMemoryProvider from CDI."

  3. ChatMemoryProcessor always creates a default @ApplicationScoped ChatMemoryProvider (MessageWindowChatMemory). This gets injected into the synthetic context.

  4. QuarkusAiServiceContextFactory.create() returns this pre-configured context (with ChatMemoryProvider already set) to Camel's builder.

  5. Camel's AgentWithoutMemory.createAiAgentService() only calls .chatModel(config.getChatModel()) — it never resets chatMemoryProvider. The QL4J-injected ChatMemoryProvider survives into the built proxy.

Impact

Since AiAgentWithoutMemoryService has no @MemoryId parameter, the memory ID falls back to either a request-scope hash (HTTP routes) or the literal string "default" (non-HTTP routes like timer, file, kafka). This causes:

  • Non-HTTP routes: All AgentWithoutMemory instances across the application share a single global chat memory. Messages from unrelated conversations contaminate each other.
  • HTTP routes: Per-request isolation, but InMemoryChatMemoryStore entries are never evicted (memory leak).
  • All routes: Each call sends up to 9 prior unrelated messages to the LLM (default window size = 10), increasing token cost, latency, and producing incorrect responses.

No error or warning is logged. The agent silently behaves differently than its name and contract imply.

Steps to Reproduce

  1. Add quarkus-langchain4j-ollama (or another QL4J model provider) as a dependency
  2. Configure a default model:
    quarkus.langchain4j.ollama.base-url=http://localhost:11434
    quarkus.langchain4j.ollama.chat-model.model-id=llama3.2
  3. Create an AgentWithoutMemory with a Camel-managed ChatModel:
    new AgentWithoutMemory(new AgentConfiguration().withChatModel(chatModel))
  4. Send multiple unrelated messages through the agent
  5. Observe that responses reference content from previous, unrelated messages

Expected Behavior

AgentWithoutMemory should be stateless — each call processed independently with no conversation history, regardless of whether QL4J is on the classpath.

Why Current Tests Don't Catch This

The langchain4j-agent-ql4j integration test uses @Identifier-qualified ChatModel beans and does not configure a default QL4J model via properties. Without a default ChatModel, the CDI context bean creation fails and QuarkusAiServiceContextFactory falls back to a clean empty context. The ChatMemoryProvider doesn't leak — but only by accident, not by design.

Additional Context

The langchain4j-agent-ql4j test README acknowledges related interference: RetrievalAugmentor had to be disabled (cq-test.retrieval.augmentor.disabled=true) because "Testing with quarkus-langchain4j does not work when the RetrievalAugmentor bean is present with scope ."

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions