Skip to content

feat(examples): spring-ai-engram-cloud-demo — Spring AI + Engram + JamJet Cloud#3

Merged
sunilp merged 16 commits intomainfrom
feat/spring-ai-engram-demos
May 8, 2026
Merged

feat(examples): spring-ai-engram-cloud-demo — Spring AI + Engram + JamJet Cloud#3
sunilp merged 16 commits intomainfrom
feat/spring-ai-engram-demos

Conversation

@sunilp
Copy link
Copy Markdown
Member

@sunilp sunilp commented May 8, 2026

Summary

Adds the first reference demo for the JamJet JVM stack: a Spring AI 1.0 chat agent with durable memory backed by Engram and cloud observability shipped to JamJet via the existing JamjetObservationHandler.

Three Spring Boot starters wire the whole thing:

  • org.springframework.ai:spring-ai-starter-model-openai:1.0.0
  • dev.jamjet:engram-spring-boot-starter:0.2.0 (autoconfigures EngramClient against engram.base-url)
  • dev.jamjet:jamjet-cloud-sdk:0.2.0 (cloud observability — wired explicitly via a small demo-side JamjetCloudConfiguration; see "Workarounds" below)

Demo narrative

docker compose up -d                 # boots Engram on 127.0.0.1:9090
./mvnw spring-boot:run               # boots Spring AI agent on 127.0.0.1:8080

curl -X POST localhost:8080/chat?session=alice -H "Content-Type: text/plain" \
  -d "I work at Acme as a Java engineer"     # agent stores fact via Engram
curl -X POST localhost:8080/chat?session=alice -H "Content-Type: text/plain" \
  -d "Where do I work?"                       # agent recalls from Engram → "Acme"

Each chat call produces a JamJet trace with: 1 LLM span (OpenAI), N Engram tool spans (rememberFact / recallFacts), cost rollup. Verified end-to-end against cloud.jamjet.dev.

Workarounds documented

While building this demo I hit two real bugs in dev.jamjet:jamjet-cloud-spring-boot-starter:0.2.0 that are filed as upstream followups:

  1. Hard reference to LangChain4j classes in autoconfigChatModelListenerPostProcessor references dev.langchain4j.model.chat.listener.ChatModelListener directly without @ConditionalOnClass protection at the @bean signature level, so Spring-AI-only consumers get ClassNotFoundException at startup. Workaround: skip the starter, depend on jamjet-cloud-sdk:0.2.0 directly + a tiny demo-side JamjetCloudConfiguration.java that does the Spring AI half.
  2. JamjetObservationHandler.supportsContext filters by namectx.getName().startsWith("gen_ai.client") is checked when Micrometer's lifecycle still has a null context name, so the handler is never selected for any observation. Workaround: wrap with a context-class filter (ChatModelObservationContext) so selection is name-independent.

Both workarounds are confined to JamjetCloudConfiguration.java and are clearly documented in the source. Once the starter is fixed (separate PR), this demo simplifies back to using jamjet-cloud-spring-boot-starter directly.

What's in

  • Module scaffold: standalone Maven (matches examples/real-agent/ convention — no parent, own groupId dev.jamjet.examples)
  • Maven wrapper for cross-platform run
  • docker-compose.yml — Engram (Rust 0.5.0) on port 9090, configured for OpenAI-compatible LLM provider so fact extraction works
  • .env.example — placeholder keys with sign-up URLs
  • PreflightCheck — cross-platform Java env-var validator + Engram health-poller (replaces a run.sh shell script idea)
  • MemoryTools + MemoryAgent + ChatController — idiomatic Spring AI
  • JamjetCloudConfiguration — explicit cloud observability wiring (workarounds documented above)
  • DemoIntegrationTest@SpringBootTest + WireMock OpenAI + Testcontainers Engram (Rust 0.5.0 with mock LLM provider for determinism)
  • README with quickstart, screenshots, security notes, Windows notes

Test plan

  • ./mvnw verify — all tests pass (PreflightCheck unit tests + DemoIntegrationTest)
  • Manual smoke: real OpenAI key + JamJet key, three README curls, traces visible at https://api.jamjet.dev/v1/traces tagged agents=['spring-ai-engram-demo'] with proper LLM call counts and cost rollups (~$0.000045–$0.00018 per chat turn)
  • Cleanly tear down (docker compose down removes container + network)

Path forward

This demo is immediately mergeable. The JamjetObservationHandler-based wiring works correctly given the workarounds.

Once the related jamjet-cloud:feat/otlp-ingest PR ships (OTLP/JSON intake), this demo can be simplified to use Spring AI's standard OTLP exporter pointed at /v1/otlp/v1/traces — same trivial pattern as Koog. That cleanup is filed as a followup, not blocking this merge.

Stacked PR

The Kotlin Koog demo (feat/kotlin-koog-engram-demo) is stacked on this branch. It will be opened as a separate PR after this one merges.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a new Spring AI example application demonstrating a multi-turn chat agent with persistent memory capabilities. The demo integrates fact storage and recall, automatic observability tracing, and includes Docker Compose setup with comprehensive documentation and integration tests for easy local evaluation.

sunilp added 16 commits May 7, 2026 13:43
Deviations from original spec (confirmed via CLI inspection):
- Added ENGRAM_MODE=rest (default is mcp/stdio; HTTP requires explicit opt-in)
- Health endpoint is /health not /healthz (404 confirmed on /healthz)
- Added ENGRAM_EMBEDDING_PROVIDER=mock (avoids Ollama dependency for embeddings)
- Removed explicit command/port args; port 9090 is already the REST default
- Added named volume engram-data for SQLite persistence across restarts
- Recall query param is q= not query= (documented for Task 7 EngramClient wiring)
- mock LLM provider returns zero facts by design (logged warning from binary)
DemoIntegrationTest: WireMock stubs OpenAI chat completions returning
"Acme"; Testcontainers runs real Engram 0.5.0 with mock LLM provider;
@SpringBootTest asserts the /chat endpoint returns the stubbed reply.

Three companion fixes required to make the test pass:
- Upgraded Testcontainers 1.20.4 → 1.21.3 (docker-java 3.4.2) and
  moved TC BOM before spring-boot-dependencies so the version wins.
- Added api.version=1.41 as a Surefire JVM property; TC ≤1.21.x
  falls back to docker-java API 1.32 when UNKNOWN_VERSION is detected,
  and OrbStack 2.x dropped support for API < 1.40.
- Enabled -parameters compiler flag so Spring MVC resolves @RequestParam
  names at runtime without relying on debug info.
- Excluded JamjetCloudAutoConfiguration (0.2.0 references langchain4j
  which is not on the test classpath; guards the @SpringBootTest context).
…vationHandler directly with context-type filter

- Drop dev.jamjet:jamjet-cloud-spring-boot-starter:0.2.0 dep. Its autoconfig
  hard-references LangChain4j classes which forced langchain4j-core onto the
  classpath of a Spring-AI-only app.
- Depend on dev.jamjet:jamjet-cloud-sdk:0.2.0 directly + add JamjetCloudConfiguration
  in the demo that does the Spring AI half. Cleaner separation; pattern can be
  upstreamed as a 'Spring AI only' autoconfig.
- Workaround for jamjet-cloud-sdk:0.2.0 JamjetObservationHandler.supportsContext
  bug: it filters by name.startsWith('gen_ai.client'), but Micrometer calls
  supportsContext at points where ctx.getName() is still null — handler is never
  selected. Replace name-based filter with context-class filter so selection is
  name-independent.
- Verified end-to-end: chat-driven gen_ai.client.operation observations now
  flow to JamJet Cloud (traces with llm_call counts + cost rollups visible at
  /v1/traces). Tested with 3 README curls; 6 demo traces persisted at JamJet.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request introduces a complete, runnable Spring Boot demonstration application that integrates Spring AI, Engram durable memory storage, and JamJet Cloud observability. It includes Docker Compose orchestration for the Engram service, Maven wrapper infrastructure for cross-platform builds, complete application code with a multi-turn chat agent, startup validation, tests, and comprehensive documentation.

Changes

Spring AI + Engram + JamJet Cloud Demo Application

Layer / File(s) Summary
Environment & Build Setup
.env.example, .gitignore, .mvn/wrapper/maven-wrapper.properties
Environment template for API keys, build artifact exclusion rules, and Maven wrapper version pinning to 3.3.4 with Maven 3.9.9.
Maven Wrapper Scripts
mvnw, mvnw.cmd
Cross-platform Maven wrapper shells that download, verify SHA-256, extract, and execute Maven distributions with support for mvnd and credential-based downloads.
Maven Project Configuration
pom.xml
Java 21 target, BOMs for Spring Boot/AI/Testcontainers, runtime dependencies (Spring Web, Actuator, Spring AI OpenAI, Engram, JamJet Cloud), test dependencies (WireMock, Testcontainers), and build plugins.
Runtime Services
docker-compose.yml
Engram server container (image 0.5.0) configured for REST mode with mock LLM/embeddings, loopback port binding, health check, and named data volume.
Application Entry Point
src/main/java/.../DemoApplication.java
Spring Boot application class with @SpringBootApplication and main(String[] args) entry point.
Core Service Logic
src/main/java/.../MemoryAgent.java, src/main/java/.../MemoryTools.java
MemoryAgent wraps ChatClient with durable-memory system prompt and default tools. MemoryTools exposes @Tool methods to store and recall facts via EngramClient.
REST API Endpoint
src/main/java/.../ChatController.java
POST /chat endpoint accepting session parameter and message body, delegating to MemoryAgent, returning ChatResponse record with session and reply.
Startup Checks & Observability
src/main/java/.../startup/PreflightCheck.java, src/main/java/.../cloud/JamjetCloudConfiguration.java
PreflightCheck validates OPENAI_API_KEY and JAMJET_API_KEY env vars, polls Engram /health with 30-second timeout. JamjetCloudConfiguration configures JamJet Cloud and registers Micrometer ObservationHandler for Spring AI chat events.
Application Configuration
src/main/resources/application.yml
Server bind to 127.0.0.1:8080, Spring AI OpenAI with gpt-4o-mini, Engram base URL from ENGRAM_BASE_URL env (default http://127.0.0.1:9090), JamJet Cloud credentials, and INFO logging for Spring AI and dev.jamjet packages.
Tests
src/test/java/.../DemoIntegrationTest.java, src/test/java/.../PreflightCheckTest.java
DemoIntegrationTest boots app with WireMock OpenAI stub and testcontainers Engram, asserts /chat returns expected response. PreflightCheckTest validates environment-variable and health-check logic with four test cases.
Documentation
README.md
Complete guide covering demo purpose, multi-turn memory architecture, prerequisites (Java 21, Docker, API keys), run workflow, example curl commands, expected JamJet Cloud traces, security notes, and cleanup instructions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through frameworks new,
Spring AI sings, while Engram stores the clue!
JamJet observes each chat's graceful dance,
With Docker Compose and Maven's guided prance.
From session to memory, facts take their stance,
A demo complete—let the agents advance! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main addition: a new Spring AI demo integrating Engram for durable memory and JamJet Cloud for observability, covering all three primary components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/spring-ai-engram-demos

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
examples/spring-ai-engram-cloud-demo/.mvn/wrapper/maven-wrapper.properties (1)

1-3: ⚡ Quick win

Add distributionSha256Sum for supply-chain integrity.

Without it, the wrapper downloads and executes the Maven binary with no tamper detection. To prevent an attack where a vulnerable repository could distribute malicious Maven artifacts, the downloaded artifacts should be verified against a secure checksum. distributionSha256Sum has been supported by Maven Wrapper since 3.2.0.

The SHA-256 for apache-maven-3.9.9-bin.zip is available at https://downloads.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.zip.sha256.

🔒 Proposed addition
 wrapperVersion=3.3.4
 distributionType=only-script
 distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
+distributionSha256Sum=<sha256-of-apache-maven-3.9.9-bin.zip>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/spring-ai-engram-cloud-demo/.mvn/wrapper/maven-wrapper.properties`
around lines 1 - 3, Add the distributionSha256Sum property to the Maven wrapper
properties so the downloaded Maven distribution is verified; specifically,
update the existing properties block that contains wrapperVersion,
distributionType, and distributionUrl by adding distributionSha256Sum with the
SHA-256 checksum for apache-maven-3.9.9-bin.zip (obtain the exact value from
https://downloads.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.zip.sha256)
to ensure the wrapper verifies integrity before extracting the distribution.
examples/spring-ai-engram-cloud-demo/src/main/resources/application.yml (1)

24-26: ⚡ Quick win

app.engram.health-url reads ENGRAM_BASE_URL directly rather than referencing engram.base-url.

If someone overrides engram.base-url via a Spring profile, system property, or config server (rather than the env var), app.engram.health-url won't track the change and PreflightCheck will poll the wrong endpoint. Referencing the resolved property keeps the two in sync:

♻️ Proposed fix
-app:
-  engram:
-    health-url: ${ENGRAM_BASE_URL:http://127.0.0.1:9090}/health
+app:
+  engram:
+    health-url: ${engram.base-url}/health
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/spring-ai-engram-cloud-demo/src/main/resources/application.yml`
around lines 24 - 26, Change app.engram.health-url to reference the resolved
engram.base-url property instead of reading ENGRAM_BASE_URL directly: update the
YAML so app.engram.health-url uses the engram.base-url property (with the
existing ENGRAM_BASE_URL fallback as the default) so that any overrides to
engram.base-url via profiles/config server/system properties are picked up;
verify PreflightCheck (or any code reading app.engram.health-url) will therefore
poll the correct endpoint.
examples/spring-ai-engram-cloud-demo/README.md (1)

14-24: 💤 Low value

Add a language specifier to the architecture diagram code fence.

The fenced code block at line 14 has no language tag, which triggers a markdownlint MD040 warning. Adding text silences the linter.

📝 Proposed fix
-```
+```text
 User → POST /chat?session=alice ──→ Spring AI ChatClient
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/spring-ai-engram-cloud-demo/README.md` around lines 14 - 24, Add a
language specifier "text" to the fenced code block that contains the ASCII
architecture diagram (the triple backticks before the User → POST
/chat?session=alice diagram) in the README so the markdown linter MD040 is
satisfied; locate the opening triple-backtick for the diagram in README.md and
change it to ```text (leaving the diagram contents and closing backticks
unchanged).
examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java (1)

63-66: ⚡ Quick win

Magic string for ChatModelObservationContext class name is fragile.

Since spring-ai-core is already on the classpath for this demo, a direct class reference is type-safe and will produce a compile error if the class moves in a future Spring AI release, rather than silently failing to match any context.

♻️ Proposed fix: use class literal
+import org.springframework.ai.chat.observation.ChatModelObservationContext;
 ...
             public boolean supportsContext(Observation.Context ctx) {
-                return ctx != null
-                        && ctx.getClass().getName().equals("org.springframework.ai.chat.observation.ChatModelObservationContext");
+                return ctx instanceof ChatModelObservationContext;
             }

Using instanceof also handles subclasses correctly and is idiomatic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java`
around lines 63 - 66, The supportsContext(Observation.Context ctx) method uses a
fragile string comparison for
"org.springframework.ai.chat.observation.ChatModelObservationContext"; replace
that with a direct type check using the actual class
(org.springframework.ai.chat.observation.ChatModelObservationContext) via
instanceof so subclasses are handled and the compiler will catch
refactors—update the import and change the return to use ctx instanceof
ChatModelObservationContext in the supportsContext method.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/spring-ai-engram-cloud-demo/docker-compose.yml`:
- Around line 17-21: The healthcheck currently uses wget, which may not exist in
the engram-server image (ghcr.io/jamjet-labs/engram-server:0.5.0); update the
docker-compose healthcheck block to use a more portable check: replace the wget
test with a /bin/sh -c command that tries a POSIX /dev/tcp connection or falls
back to checking curl/nc if available, and if neither exists, document adding a
small sidecar or health script into the image; ensure the change is made in the
healthcheck: test entry so the container reports healthy when the server port
responds.

In `@examples/spring-ai-engram-cloud-demo/pom.xml`:
- Line 18: The project pins spring-boot.version to 3.3.5 which brings vulnerable
tomcat-embed-core; either upgrade the Spring Boot property <spring-boot.version>
to a secure 3.5.x release (e.g., 3.5.14) or add an explicit dependencyManagement
override for tomcat-embed-core to a patched 10.1.54+ version; update the
<spring-boot.version> value in the POM if choosing Option B or add/modify the
tomcat-embed-core artifact entry under <dependencyManagement> to the safe
version if choosing Option A, then run a build to verify no dependency
conflicts.

In
`@examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java`:
- Around line 44-51: The static JamjetCloud.configure call and repeated
observationRegistry.observationConfig().observationHandler registrations in
initJamjetCloud cause duplicate handlers across Spring context reloads; make the
initialization idempotent by guarding the logic (e.g., add a private static
AtomicBoolean initialized flag checked/set in initJamjetCloud) or refactor
registration into a `@Bean` method annotated with `@ConditionalOnMissingBean` so
JamjetCloud.configure(...) and the observation handler are only registered once;
alternatively check observationRegistry.observationConfig() to avoid re-adding
the same handler instance before calling observationHandler().

In
`@examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/MemoryAgent.java`:
- Around line 24-29: The chat(...) method can return null because
chatClient.prompt().call().content() may be null; update the
MemoryAgent.chat(String sessionId, String userMessage) method to capture the
result of chatClient.prompt()...call(), check whether .content() (i.e., the
AssistantMessage content) is null, and return a safe non-null value (for example
an empty string or a default message) to the controller instead of returning
null; reference the existing chat(...) method and use a local variable to hold
the response before performing the null guard and returning the safe value.

---

Nitpick comments:
In `@examples/spring-ai-engram-cloud-demo/.mvn/wrapper/maven-wrapper.properties`:
- Around line 1-3: Add the distributionSha256Sum property to the Maven wrapper
properties so the downloaded Maven distribution is verified; specifically,
update the existing properties block that contains wrapperVersion,
distributionType, and distributionUrl by adding distributionSha256Sum with the
SHA-256 checksum for apache-maven-3.9.9-bin.zip (obtain the exact value from
https://downloads.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.zip.sha256)
to ensure the wrapper verifies integrity before extracting the distribution.

In `@examples/spring-ai-engram-cloud-demo/README.md`:
- Around line 14-24: Add a language specifier "text" to the fenced code block
that contains the ASCII architecture diagram (the triple backticks before the
User → POST /chat?session=alice diagram) in the README so the markdown linter
MD040 is satisfied; locate the opening triple-backtick for the diagram in
README.md and change it to ```text (leaving the diagram contents and closing
backticks unchanged).

In
`@examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java`:
- Around line 63-66: The supportsContext(Observation.Context ctx) method uses a
fragile string comparison for
"org.springframework.ai.chat.observation.ChatModelObservationContext"; replace
that with a direct type check using the actual class
(org.springframework.ai.chat.observation.ChatModelObservationContext) via
instanceof so subclasses are handled and the compiler will catch
refactors—update the import and change the return to use ctx instanceof
ChatModelObservationContext in the supportsContext method.

In `@examples/spring-ai-engram-cloud-demo/src/main/resources/application.yml`:
- Around line 24-26: Change app.engram.health-url to reference the resolved
engram.base-url property instead of reading ENGRAM_BASE_URL directly: update the
YAML so app.engram.health-url uses the engram.base-url property (with the
existing ENGRAM_BASE_URL fallback as the default) so that any overrides to
engram.base-url via profiles/config server/system properties are picked up;
verify PreflightCheck (or any code reading app.engram.health-url) will therefore
poll the correct endpoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5613f99e-9eb5-466c-9698-229148b18899

📥 Commits

Reviewing files that changed from the base of the PR and between 1116220 and 242e328.

📒 Files selected for processing (17)
  • examples/spring-ai-engram-cloud-demo/.env.example
  • examples/spring-ai-engram-cloud-demo/.gitignore
  • examples/spring-ai-engram-cloud-demo/.mvn/wrapper/maven-wrapper.properties
  • examples/spring-ai-engram-cloud-demo/README.md
  • examples/spring-ai-engram-cloud-demo/docker-compose.yml
  • examples/spring-ai-engram-cloud-demo/mvnw
  • examples/spring-ai-engram-cloud-demo/mvnw.cmd
  • examples/spring-ai-engram-cloud-demo/pom.xml
  • examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/ChatController.java
  • examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/DemoApplication.java
  • examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/MemoryAgent.java
  • examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/MemoryTools.java
  • examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java
  • examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/startup/PreflightCheck.java
  • examples/spring-ai-engram-cloud-demo/src/main/resources/application.yml
  • examples/spring-ai-engram-cloud-demo/src/test/java/dev/jamjet/demo/springaiengram/DemoIntegrationTest.java
  • examples/spring-ai-engram-cloud-demo/src/test/java/dev/jamjet/demo/springaiengram/PreflightCheckTest.java

Comment on lines +17 to +21
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/health"]
interval: 2s
timeout: 1s
retries: 15
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verify wget is available in the engram-server container image.

The Engram server is a Rust binary distributed as ghcr.io/jamjet-labs/engram-server:0.5.0. Minimal Rust containers frequently ship without wget or curl. If the binary is absent, Docker marks the container perpetually unhealthy even when the port is responding correctly — which degrades DX (docker ps always shows unhealthy) and would break any future depends_on: condition: service_healthy usage.

A safer fallback is using /bin/sh -c with a POSIX-compliant /dev/tcp redirect or nc, both of which are more likely to be present:

🛡️ Alternative healthcheck (if wget is absent)
     healthcheck:
-      test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/health"]
+      test: ["CMD", "sh", "-c", "wget -q --spider http://localhost:9090/health || curl -sf http://localhost:9090/health"]
       interval: 2s
       timeout: 1s
       retries: 15
       start_period: 5s

Or, if neither is available, use a side-car healthcheck script bundled in the image. Verify with:

docker run --rm --entrypoint sh ghcr.io/jamjet-labs/engram-server:0.5.0 -c "which wget || which curl || echo NONE"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/health"]
interval: 2s
timeout: 1s
retries: 15
healthcheck:
test: ["CMD", "sh", "-c", "wget -q --spider http://localhost:9090/health || curl -sf http://localhost:9090/health"]
interval: 2s
timeout: 1s
retries: 15
start_period: 5s
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/spring-ai-engram-cloud-demo/docker-compose.yml` around lines 17 -
21, The healthcheck currently uses wget, which may not exist in the
engram-server image (ghcr.io/jamjet-labs/engram-server:0.5.0); update the
docker-compose healthcheck block to use a more portable check: replace the wget
test with a /bin/sh -c command that tries a POSIX /dev/tcp connection or falls
back to checking curl/nc if available, and if neither exists, document adding a
small sidecar or health script into the image; ensure the change is made in the
healthcheck: test entry so the container reports healthy when the server port
responds.

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
<spring-boot.version>3.3.5</spring-boot.version>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Is Spring AI 1.0.0 compatible with Spring Boot 3.5.x?

💡 Result:

Yes, Spring AI 1.0.0 is compatible with Spring Boot 3.5.x. The official Spring AI reference documentation explicitly states that Spring AI supports Spring Boot 3.4.x and 3.5.x [1][2]. Subsequent 1.0.x patch releases, such as 1.0.4, 1.0.6, and 1.1.x, upgrade their Spring Boot dependency to versions like 3.5.11 and 3.5.14, confirming ongoing compatibility with the 3.5.x line [3][4][5][6]. The Spring AI BOM for 1.0.0 is designed to work alongside Spring Boot's BOM without direct version enforcement on Spring Boot, allowing use with 3.5.x [1][7]. The project's README notes Spring AI 1.1.x (close to 1.0) targets Spring Boot 3.5.x [5][8].

Citations:


Upgrade Spring Boot to address CRITICAL Tomcat CVEs.

Spring Boot 3.5.14 upgrades to Tomcat 10.1.54, while the pinned 3.3.5 ships tomcat-embed-core 10.1.31, which carries 20+ CRITICAL/HIGH vulnerabilities flagged by OSV Scanner — including potential RCE via partial PUT (GHSA-83qj-6fr2-vhqg), multiple TOCTOU races, path traversal, DoS, and auth-bypass CVEs. Spring Boot 3.3 is past its OSS support window, so no patches will land there.

Even as a demo that binds to localhost, the README guides users to run it with real OpenAI and JamJet API keys, which makes the risk non-trivial.

Two remediation paths, fastest-first:

Option A — override Tomcat only (unblocks immediately, minimal compatibility risk):

🔒 Proposed fix — override Tomcat version in properties
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <maven.compiler.release>21</maven.compiler.release>
         <spring-boot.version>3.3.5</spring-boot.version>
+        <tomcat.version>10.1.54</tomcat.version>
         <spring-ai.version>1.0.0</spring-ai.version>

Option B — upgrade Spring Boot (recommended; also resolves the spring-boot/spring-core HIGH CVEs):

🔒 Proposed fix — upgrade to Spring Boot 3.5.x
-        <spring-boot.version>3.3.5</spring-boot.version>
+        <spring-boot.version>3.5.14</spring-boot.version>

Spring AI 1.0.0 is compatible with Spring Boot 3.5.x per the official Spring AI documentation; subsequent releases (1.0.4+, 1.1.x) also target 3.5.x, confirming stable support.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<spring-boot.version>3.3.5</spring-boot.version>
<spring-boot.version>3.3.5</spring-boot.version>
<tomcat.version>10.1.54</tomcat.version>
Suggested change
<spring-boot.version>3.3.5</spring-boot.version>
<spring-boot.version>3.5.14</spring-boot.version>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/spring-ai-engram-cloud-demo/pom.xml` at line 18, The project pins
spring-boot.version to 3.3.5 which brings vulnerable tomcat-embed-core; either
upgrade the Spring Boot property <spring-boot.version> to a secure 3.5.x release
(e.g., 3.5.14) or add an explicit dependencyManagement override for
tomcat-embed-core to a patched 10.1.54+ version; update the
<spring-boot.version> value in the POM if choosing Option B or add/modify the
tomcat-embed-core artifact entry under <dependencyManagement> to the safe
version if choosing Option A, then run a build to verify no dependency
conflicts.

Comment on lines +44 to +51
@PostConstruct
void initJamjetCloud() {
JamjetCloud.configure(JamjetCloudConfig.builder()
.apiKey(apiKey)
.apiUrl(apiUrl)
.agentName("spring-ai-engram-demo")
.build());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

JamjetCloud.configure and handler registration are not idempotent across Spring context reloads.

JamjetCloud.configure(...) is a static call and observationRegistry.observationConfig().observationHandler(...) appends to a global list. In integration tests that spin up multiple Spring contexts (e.g., DemoIntegrationTest with WireMock + Testcontainers), each context initialization adds another handler instance to the same ObservationRegistry, potentially producing duplicate trace spans per LLM call.

Consider guarding with an AtomicBoolean or extracting to a @Bean with @ConditionalOnMissingBean, or tracking test suite behavior to confirm this isn't observed in practice.

Also applies to: 60-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/cloud/JamjetCloudConfiguration.java`
around lines 44 - 51, The static JamjetCloud.configure call and repeated
observationRegistry.observationConfig().observationHandler registrations in
initJamjetCloud cause duplicate handlers across Spring context reloads; make the
initialization idempotent by guarding the logic (e.g., add a private static
AtomicBoolean initialized flag checked/set in initJamjetCloud) or refactor
registration into a `@Bean` method annotated with `@ConditionalOnMissingBean` so
JamjetCloud.configure(...) and the observation handler are only registered once;
alternatively check observationRegistry.observationConfig() to avoid re-adding
the same handler instance before calling observationHandler().

Comment on lines +24 to +29
public String chat(String sessionId, String userMessage) {
return chatClient.prompt()
.user("[session=" + sessionId + "] " + userMessage)
.call()
.content();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

content() can return null; guard before returning to the controller.

content() is shorthand for .chatResponse().getResult().getOutput().getContent(), and getContent() on AssistantMessage is nullable. If the model satisfies the request via a tool call and emits no text in the final turn, chat() returns null, which the controller will pass directly as the HTTP response body.

🛡️ Proposed fix
 public String chat(String sessionId, String userMessage) {
-    return chatClient.prompt()
+    String content = chatClient.prompt()
             .user("[session=" + sessionId + "] " + userMessage)
             .call()
             .content();
+    return content != null ? content : "";
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String chat(String sessionId, String userMessage) {
return chatClient.prompt()
.user("[session=" + sessionId + "] " + userMessage)
.call()
.content();
}
public String chat(String sessionId, String userMessage) {
String content = chatClient.prompt()
.user("[session=" + sessionId + "] " + userMessage)
.call()
.content();
return content != null ? content : "";
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/spring-ai-engram-cloud-demo/src/main/java/dev/jamjet/demo/springaiengram/MemoryAgent.java`
around lines 24 - 29, The chat(...) method can return null because
chatClient.prompt().call().content() may be null; update the
MemoryAgent.chat(String sessionId, String userMessage) method to capture the
result of chatClient.prompt()...call(), check whether .content() (i.e., the
AssistantMessage content) is null, and return a safe non-null value (for example
an empty string or a default message) to the controller instead of returning
null; reference the existing chat(...) method and use a local variable to hold
the response before performing the null guard and returning the safe value.

@sunilp sunilp merged commit b4a33f8 into main May 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant