Skip to content

feat(spring): jamjet-agent Spring Boot starter (Track 9 Phase C, completes the ADK) - #10

Merged
sunilp merged 4 commits into
mainfrom
feat/adk-track-9c-spring-starter
Jun 28, 2026
Merged

feat(spring): jamjet-agent Spring Boot starter (Track 9 Phase C, completes the ADK)#10
sunilp merged 4 commits into
mainfrom
feat/adk-track-9c-spring-starter

Conversation

@sunilp

@sunilp sunilp commented Jun 28, 2026

Copy link
Copy Markdown
Member

What

Track 9 Phase C (the last piece): a Spring Boot starter for the Java Agent. A new jamjet-agent-spring-boot-starter module so a Spring developer adds one dependency, declares an Agent bean with @Tool Spring beans and governance, and gets a governed durable agent running on the engine.

  • JamjetAgentAutoConfiguration (registered via the Boot 3 AutoConfiguration.imports) exposes a JamjetEngineClient, a ToolRegistry, a ToolBeanRegistrar, and a JavaToolWorkerLifecycle from JamjetAgentProperties (jamjet.agent.*). Every bean is @ConditionalOnMissingBean, so an application can override any of them.
  • ToolBeanRegistrar scans Spring beans for @Tool methods and registers them into the shared ToolRegistry, resolving the real CGLIB target first so proxied beans (for example @Transactional components) are not missed.
  • JavaToolWorkerLifecycle (a SmartLifecycle) drains the java_tool queue on a daemon thread on context start and stops it cleanly on shutdown, gated by jamjet.agent.worker.enabled.

The starter depends only on jamjet-agent and Spring Boot. It deliberately does not pull in spring-ai or langchain4j, so the auto-configuration's bean return types always link and the known @ConditionalOnClass NoClassDefFoundError is avoided.

Safety

A whole-branch review confirmed: the dependency set avoids the @ConditionalOnClass bug; the CGLIB @Tool resolution is correct and proven by a non-vacuous test (a genuinely CGLIB-proxied bean is registered, and removing the unwrap makes it vanish); the worker lifecycle starts without blocking startup and stops cleanly with no leaked thread, and is disableable; and the ToolRegistry is a single shared bean across the registrar, the worker, and an Agent.

Tests

mvn verify green: 11 module tests (the bean wiring, @ConditionalOnMissingBean overrides, the plain and CGLIB-proxied @Tool scanning, the lifecycle present/absent by flag, duplicate-tool-name rejection, and the worked Spring agent), plus the upstream jamjet-runtime-core (85) and jamjet-agent (78).

Track 9 complete

With Phase A (the engine java_tool queue), the fenced completion hardening, Phase B (the Java Agent builder and durable tool-worker), and this Spring starter, a JVM developer can author a governed durable agent in idiomatic Java or Spring. This is the last track of the 9-track ADK plan.

Follow-ups

A vestigial properties field, @Tool discovery on superclasses (shared with the core registry), and the Jackson baseline bump (deferred from Phase B).

Summary by CodeRabbit

  • New Features

    • Added a Spring Boot starter for integrating JamJet agents into applications.
    • Auto-configures the engine client, tool registry, and optional background worker support.
    • Provides configurable settings for runtime URL, auth, tenant, worker ID, heartbeat, and polling behavior.
    • Includes an end-to-end calculator example showing durable agent execution and tool registration.
  • Documentation

    • Added setup and usage guidance for Spring Boot integration, configuration, and example agent authoring.
  • Tests

    • Added coverage for auto-configuration, tool discovery, proxy handling, duplicate tool names, and example wiring.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sunilp, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 36 minutes and 26 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc6447a5-14bd-4c1a-a2f6-01fa559ffb40

📥 Commits

Reviewing files that changed from the base of the PR and between 8ce6902 and ae01ecf.

📒 Files selected for processing (4)
  • jamjet-agent-spring-boot-starter/README.md
  • jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/JavaToolWorkerLifecycle.java
  • jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/ToolBeanRegistrar.java
  • jamjet-agent-spring-boot-starter/src/test/java/dev/jamjet/agent/spring/ToolScanningTest.java
📝 Walkthrough

Walkthrough

A new jamjet-agent-spring-boot-starter Maven module is introduced. It adds JamjetAgentProperties for configuration binding, JamjetAgentAutoConfiguration for conditional bean wiring, ToolBeanRegistrar for proxy-aware @Tool discovery, JavaToolWorkerLifecycle as a SmartLifecycle wrapper, tests, a calculator example, and a README.

Changes

jamjet-agent Spring Boot Starter

Layer / File(s) Summary
Configuration properties and POM setup
pom.xml, jamjet-agent-spring-boot-starter/pom.xml, jamjet-agent-spring-boot-starter/src/main/java/.../JamjetAgentProperties.java
Adds the starter module to the parent POM, declares the new POM with Spring Boot 3.3.5 dependency management, and defines JamjetAgentProperties with engine URL/auth fields and a nested Worker config object for enablement and drain tuning.
ToolBeanRegistrar: proxy-aware @Tool scanning
jamjet-agent-spring-boot-starter/src/main/java/.../ToolBeanRegistrar.java
Implements SmartInitializingSingleton and BeanFactoryAware to scan all singleton beans post-startup, unwrap CGLIB AOP proxies to their raw targets, detect @Tool-annotated methods via reflection, and register qualifying holders into the shared ToolRegistry in deterministic lexicographic order, failing on duplicate tool names.
JavaToolWorkerLifecycle: SmartLifecycle wrapper
jamjet-agent-spring-boot-starter/src/main/java/.../JavaToolWorkerLifecycle.java
Wraps JavaToolWorker as a Spring SmartLifecycle: start() spawns a named daemon thread running the blocking poll loop; stop() calls worker.stop(), interrupts the thread, calls worker.close(), and joins up to 5 seconds with interrupt-status preservation.
JamjetAgentAutoConfiguration and imports
jamjet-agent-spring-boot-starter/src/main/java/.../JamjetAgentAutoConfiguration.java, jamjet-agent-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
Registers all conditional beans (JamjetEngineClient, ToolRegistry, ToolBeanRegistrar, and optionally JavaToolWorkerLifecycle gated by jamjet.agent.worker.enabled); the imports file registers the class for Spring Boot auto-configuration discovery.
Auto-configuration and tool scanning tests
jamjet-agent-spring-boot-starter/src/test/java/.../JamjetAgentAutoConfigurationTest.java, jamjet-agent-spring-boot-starter/src/test/java/.../ToolScanningTest.java
Verifies bean presence, property binding and defaults, worker lifecycle conditionality, and @ConditionalOnMissingBean override behavior; separately tests plain and CGLIB-proxied tool registration and duplicate-name context failure.
Calculator example: tools, agent config, wiring test, and app
jamjet-agent-spring-boot-starter/src/test/java/.../example/CalculatorTools.java, .../CalculatorAgentConfig.java, .../CalculatorSpringWiringTest.java, .../CalculatorSpringApplication.java
CalculatorTools exposes a @Tool arithmetic method; CalculatorAgentConfig builds an Agent with governance controls wired to the shared ToolRegistry; CalculatorSpringWiringTest asserts registry identity, governance properties, and IR compilation; CalculatorSpringApplication demonstrates a live durable run.
README
jamjet-agent-spring-boot-starter/README.md
Documents integration model, dependency coordinates, auto-configured beans, configuration properties, idiomatic @Tool authoring, governance builder usage, durable run example, external runtime requirements, and CGLIB proxy invocation behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jamjet-labs/jamjet-runtime-java#9: Introduces the core JamjetEngineClient, ToolRegistry, JavaToolWorker, and @Tool annotation that this starter directly wires into the Spring Boot context.

Poem

🐇 A starter is born, with beans in a row,
@Tool methods found wherever they grow,
CGLIB unwrapped, the proxy laid bare,
A daemon thread polling with dutiful care,
Five seconds to join, then gracefully done—
Spring Boot and JamJet now run as one! 🌸

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.77% 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 clearly identifies the new Spring Boot starter and matches the main change in the PR.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adk-track-9c-spring-starter

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
jamjet-agent-spring-boot-starter/README.md (2)

109-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Incomplete CGLIB section or missing trailing newline.

The CGLIB section ends abruptly at line 113-114 with an unclosed narrative. Add a concluding sentence or transition, and ensure the file terminates with a newline.

🤖 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 `@jamjet-agent-spring-boot-starter/README.md` around lines 109 - 114, The
CGLIB-proxied tool beans section in README is incomplete and ends abruptly, so
update the documentation by adding a closing sentence or transition in the
CGLIB-proxied tool beans section and ensure the README ends with a trailing
newline. Keep the wording consistent with the surrounding prose and use the
existing “CGLIB-proxied tool beans” heading and “@Tool methods” explanation to
place the fix.

26-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify the actual bean type in the auto-configuration table.

The table says JavaToolWorker (as a SmartLifecycle), but the auto-configuration exposes JavaToolWorkerLifecycle (which wraps JavaToolWorker). Users autowiring by type may expect JavaToolWorker itself. Consider rewording to JavaToolWorkerLifecycle or noting it explicitly.

🤖 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 `@jamjet-agent-spring-boot-starter/README.md` around lines 26 - 29, Update the
auto-configuration table entry to match the actual bean exposed by the starter:
the current `JavaToolWorker (as a SmartLifecycle)` wording is misleading because
the configuration registers `JavaToolWorkerLifecycle` wrapping `JavaToolWorker`.
Reword the row in the README to reference `JavaToolWorkerLifecycle` explicitly,
or clarify that the lifecycle bean is what gets auto-configured so users know
what type is available for autowiring.
🤖 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
`@jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/JavaToolWorkerLifecycle.java`:
- Around line 43-45: `JavaToolWorkerLifecycle.start()` is creating the worker
thread as a daemon, which allows the JVM to exit before `JavaToolWorker`
finishes processing. Update the thread setup in `start()` so the `worker::run`
thread is non-daemon, and rely on `stop()` in `JavaToolWorkerLifecycle` for
shutdown coordination; use the `thread`, `worker`, and `start()`/`stop()`
symbols to locate the change.

In
`@jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/ToolBeanRegistrar.java`:
- Around line 72-76: `afterSingletonsInstantiated()` in `ToolBeanRegistrar` only
scans already-created singletons via `beanFactory.getSingleton(name)`, so lazy
`@Tool` beans are skipped and never registered. Update the registration flow to
discover candidate tool beans from bean metadata and register them even if they
have not been instantiated yet, or explicitly detect lazy tool beans and fail
fast with a clear error. Use the existing `ToolBeanRegistrar`,
`afterSingletonsInstantiated()`, and `ToolRegistry` flow to locate and fix the
incomplete discovery logic.

---

Nitpick comments:
In `@jamjet-agent-spring-boot-starter/README.md`:
- Around line 109-114: The CGLIB-proxied tool beans section in README is
incomplete and ends abruptly, so update the documentation by adding a closing
sentence or transition in the CGLIB-proxied tool beans section and ensure the
README ends with a trailing newline. Keep the wording consistent with the
surrounding prose and use the existing “CGLIB-proxied tool beans” heading and
“@Tool methods” explanation to place the fix.
- Around line 26-29: Update the auto-configuration table entry to match the
actual bean exposed by the starter: the current `JavaToolWorker (as a
SmartLifecycle)` wording is misleading because the configuration registers
`JavaToolWorkerLifecycle` wrapping `JavaToolWorker`. Reword the row in the
README to reference `JavaToolWorkerLifecycle` explicitly, or clarify that the
lifecycle bean is what gets auto-configured so users know what type is available
for autowiring.
🪄 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: 37400d05-9c48-427b-bddc-cfed8145f24c

📥 Commits

Reviewing files that changed from the base of the PR and between 1720cd0 and 8ce6902.

📒 Files selected for processing (14)
  • jamjet-agent-spring-boot-starter/README.md
  • jamjet-agent-spring-boot-starter/pom.xml
  • jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/JamjetAgentAutoConfiguration.java
  • jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/JamjetAgentProperties.java
  • jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/JavaToolWorkerLifecycle.java
  • jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/ToolBeanRegistrar.java
  • jamjet-agent-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  • jamjet-agent-spring-boot-starter/src/test/java/dev/jamjet/agent/spring/JamjetAgentAutoConfigurationTest.java
  • jamjet-agent-spring-boot-starter/src/test/java/dev/jamjet/agent/spring/ToolScanningTest.java
  • jamjet-agent-spring-boot-starter/src/test/java/dev/jamjet/agent/spring/example/CalculatorAgentConfig.java
  • jamjet-agent-spring-boot-starter/src/test/java/dev/jamjet/agent/spring/example/CalculatorSpringApplication.java
  • jamjet-agent-spring-boot-starter/src/test/java/dev/jamjet/agent/spring/example/CalculatorSpringWiringTest.java
  • jamjet-agent-spring-boot-starter/src/test/java/dev/jamjet/agent/spring/example/CalculatorTools.java
  • pom.xml

Comment on lines +43 to +45
thread = new Thread(worker::run, threadName);
thread.setDaemon(true);
thread.start();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## JavaToolWorkerLifecycle\n'
sed -n '1,220p' jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/JavaToolWorkerLifecycle.java

printf '\n## Search for related lifecycle / worker references\n'
rg -n "JavaToolWorkerLifecycle|worker::run|setDaemon\\(|SmartLifecycle|DisposableBean|`@PreDestroy`|shutdown|non-web|CLI|background-worker|durable worker|java_tool" \
  jamjet-agent-spring-boot-starter src . -g '!**/target/**' -g '!**/build/**'

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 19392


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## README excerpt\n'
sed -n '1,140p' jamjet-agent-spring-boot-starter/README.md

printf '\n## Auto-configuration and properties excerpts\n'
sed -n '1,180p' jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/JamjetAgentAutoConfiguration.java
printf '\n---\n'
sed -n '1,180p' jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/JamjetAgentProperties.java

printf '\n## Example application excerpt\n'
sed -n '1,160p' jamjet-agent-spring-boot-starter/src/test/java/dev/jamjet/agent/spring/example/CalculatorSpringApplication.java

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 15436


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## JavaToolWorker thread/executor setup\n'
sed -n '1,260p' jamjet-agent/src/main/java/dev/jamjet/agent/worker/JavaToolWorker.java

printf '\n## JavaToolWorker later sections\n'
sed -n '260,420p' jamjet-agent/src/main/java/dev/jamjet/agent/worker/JavaToolWorker.java

printf '\n## Calculator example test maybe references lifecycle expectations\n'
sed -n '1,240p' jamjet-agent-spring-boot-starter/src/test/java/dev/jamjet/agent/spring/JamjetAgentAutoConfigurationTest.java

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 22687


Run the Spring worker on a non-daemon thread.
JavaToolWorkerLifecycle.start() marks the worker as daemon, so a non-web Spring Boot app can exit before java_tool drains. If this starter is meant to keep processing for the life of the context, let stop() handle shutdown instead.

Suggested change
        thread = new Thread(worker::run, threadName);
-        thread.setDaemon(true);
        thread.start();
📝 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
thread = new Thread(worker::run, threadName);
thread.setDaemon(true);
thread.start();
thread = new Thread(worker::run, threadName);
thread.start();
🤖 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
`@jamjet-agent-spring-boot-starter/src/main/java/dev/jamjet/agent/spring/JavaToolWorkerLifecycle.java`
around lines 43 - 45, `JavaToolWorkerLifecycle.start()` is creating the worker
thread as a daemon, which allows the JVM to exit before `JavaToolWorker`
finishes processing. Update the thread setup in `start()` so the `worker::run`
thread is non-daemon, and rely on `stop()` in `JavaToolWorkerLifecycle` for
shutdown coordination; use the `thread`, `worker`, and `start()`/`stop()`
symbols to locate the change.

…d + README

Finding 1: scan bean definitions via getType (no forced instantiation) so a @lazy @tool bean is discovered and registered instead of silently dropped by the old singleton-only scan. getBean is called only for holders that declare a @tool method (no eager creation of unrelated lazy beans), restricted to singletons. CGLIB target resolution and the duplicate-name fail-fast are preserved. Adds a slice test that fails before the fix and passes after.

Finding 2: document why the JavaToolWorker thread is a daemon (graceful stop() handles the normal path; on hard JVM exit the engine reclaims the leased item via lease expiry; non-daemon would risk hanging shutdown). No behavior change.

Finding 3: README names the autowirable JavaToolWorkerLifecycle bean and finishes the CGLIB-proxied tool beans section.
@sunilp
sunilp merged commit 420489f into main Jun 28, 2026
2 checks passed
@sunilp
sunilp deleted the feat/adk-track-9c-spring-starter branch June 28, 2026 21:29
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