Commit 686a62d
Add temporal-spring-ai module (#2829)
* Add temporal-spring-ai module for Spring AI integration
Adds a new module that integrates Spring AI with Temporal workflows,
enabling durable AI model calls, vector store operations, embeddings,
and MCP tool execution as Temporal activities.
Key components:
- ActivityChatModel: ChatModel implementation backed by activities
- TemporalChatClient: Temporal-aware ChatClient with tool detection
- SpringAiPlugin: Auto-registers Spring AI activities with workers
- Tool system: @DeterministicTool, @SideEffectTool, activity-backed tools
- MCP integration: ActivityMcpClient for durable MCP tool calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Document callback registry lifecycle risk and add stream() override
T9: Add javadoc to LocalActivityToolCallbackWrapper explaining the leak
risk when workflows are evicted from worker cache mid-execution.
T11: Override stream() in ActivityChatModel to throw
UnsupportedOperationException with a clear message, since streaming
through Temporal activities is not supported.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add tests for temporal-spring-ai (T1-T4)
T1: ChatModelActivityImplTest (10 tests) - type conversion between
ChatModelTypes and Spring AI types, multi-model resolution,
tool definition passthrough, model options mapping.
T2: TemporalToolUtilTest (22 tests) - tool detection and conversion
for @DeterministicTool, @SideEffectTool, stub type detection,
error cases for unknown/null types.
T3: WorkflowDeterminismTest (2 tests) - verifies workflows using
ActivityChatModel with tools complete without non-determinism
errors in the Temporal test environment.
T4: SpringAiPluginTest (10 tests) - plugin registration with various
bean combinations, multi-model support, default model resolution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update TASK_QUEUE.json: T1-T4, T9, T11 completed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add T14 (NPE bug) to TASK_QUEUE.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix UUID non-determinism, null metadata NPE, and unbounded tool loop
T5: Replace UUID.randomUUID() with Workflow.randomUUID() in
LocalActivityToolCallbackWrapper to ensure deterministic replay.
T7: Convert recursive tool call loop in ActivityChatModel.call() to
iterative loop with MAX_TOOL_CALL_ITERATIONS (10) limit to prevent
infinite recursion from misbehaving models.
T14: Fix NPE when ChatResponse metadata is null by only calling
.metadata() on the builder when metadata is non-null.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Split SpringAiPlugin into conditional auto-configuration (T6)
Split the monolithic SpringAiPlugin into one core plugin + three
optional plugins, each with its own @ConditionalOnClass-guarded
auto-configuration:
- SpringAiPlugin: core chat + ExecuteToolLocalActivity (always)
- VectorStorePlugin: VectorStore activity (when spring-ai-rag present)
- EmbeddingModelPlugin: EmbeddingModel activity (when spring-ai-rag present)
- McpPlugin: MCP activity (when spring-ai-mcp present)
This fixes ClassNotFoundException when optional deps aren't on the
runtime classpath. compileOnly scopes now work correctly because
Spring skips loading the conditional classes entirely when the
@ConditionalOnClass check fails.
Also resolves T10 (unnecessary MCP reflection) — McpPlugin directly
references McpClientActivityImpl instead of using Class.forName().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update TASK_QUEUE.json: T5, T6, T7, T10, T14 completed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update TASK_QUEUE.json: T12 completed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Replace fragile string matching with instanceof in TemporalStubUtil (T8)
Use direct instanceof checks against the SDK's internal invocation
handler classes instead of string-matching on class names. Since the
plugin lives in the SDK repo, any handler rename would break
compilation rather than silently failing at runtime.
ChildWorkflowInvocationHandler is package-private so it still uses a
class name check (endsWith instead of contains for precision).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update TASK_QUEUE.json: T8 completed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use WorkflowReplayer for proper replay determinism tests
Previously the tests just ran workflows forward. Now they capture
the event history after execution and replay it with
WorkflowReplayer.replayWorkflowExecution(), which will throw
NonDeterministicException if the workflow code generates different
commands on replay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Simplify stream() exception message
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert tool call iteration limit, match Spring AI's recursive pattern
Remove MAX_TOOL_CALL_ITERATIONS and the iterative loop. Use recursive
internalCall() matching Spring AI's OpenAiChatModel pattern. Temporal's
activity timeouts and workflow execution timeout already bound runaway
tool loops.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix javadoc reference for publishToMavenLocal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use SimplePlugin builder for VectorStore and EmbeddingModel plugins
Replace VectorStorePlugin and EmbeddingModelPlugin subclasses with
SimplePlugin.newBuilder().registerActivitiesImplementations() in the
auto-config classes. These plugins are trivial activity registrations
that don't need custom subclasses when the builder already supports
this.
SpringAiPlugin stays as a subclass (has getter API for chat models).
McpPlugin stays as a subclass (needs SmartInitializingSingleton for
deferred registration).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clean up TASK_QUEUE.json: remove completed tasks, add T15
Remove all completed/reverted tasks. Add T15 for the tool execution
model change discussed in PR review (run plain tools in workflow
context by default, remove @DeterministicTool and SandboxingAdvisor).
Blocked on finalizing review discussion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add link to proposed design for T15
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Triage Copilot and DABH review comments into TASK_QUEUE
T16-T21: fixes (NPE guard, error message, multi-ChatModel bug,
replay test, duplicate MCP names, embedding boxing)
T22-T24: design discussions to have with Don (starter artifact,
MCP caching, Object vs String)
T25-T27: replies (docs, SandboxingAdvisor tests, ToolContext drop —
last two likely moot if T15 lands)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T16: Guard assistantMessage.getMedia() against null
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T17: Include Nexus stubs in unrecognized tool type error message
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T18: Use ObjectProvider to fix NoUniqueBeanDefinitionException
Multiple ChatModel beans without @primary caused startup failure.
ObjectProvider.getIfAvailable() returns null gracefully instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T19: Make replay test exercise tool calls
ToolCallingStubChatModel returns a tool call request on first call,
then final text after receiving the tool response. This verifies the
full model -> tool -> model loop replays deterministically.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T20: Handle duplicate MCP client names with clear error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T21: Use float[] instead of List<Double> in EmbeddingModelTypes
Avoids boxing 1536+ Double objects per embedding. float[] matches
Spring AI's native embedding representation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T18 fix: Use getIfUnique() instead of getIfAvailable()
getIfAvailable() still throws NoUniqueBeanDefinitionException when
multiple beans exist without @primary. getIfUnique() returns null
in that case, which is what we want — SpringAiPlugin falls back to
the first entry in the map.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add T28: Restore plugin classes as public API per tconley review
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clean up TASK_QUEUE: remove completed, mark superseded
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T28: Restore VectorStorePlugin and EmbeddingModelPlugin as public classes
Use SimplePlugin's builder constructor (super(Builder)) so the classes
are named and user-creatable (new VectorStorePlugin(vectorStore)) while
still using the builder internally rather than overriding initializeWorker.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T23: Resolved — MCP capability caching is correct
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T24: Change from discussion to fix — rawContent should be String
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T22: Defer starter artifact to after PR merge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T25: Replied to DABH about docs. Added T29 for README follow-up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T29: Bump to high priority, do in this PR
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T15: Also remove LocalActivityToolCallbackWrapper and ExecuteToolLocalActivity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T24: Change ChatModelTypes.Message rawContent from Object to String
Spring AI's Content.getText() returns String. We always cast to String
on both sides. Object type gave false flexibility that would
ClassCastException at runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T29: Add README with compatibility matrix and quick start
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T30: Fix edge CI Java version mismatch
Edge CI sets edgeDepsTest which compiles temporal-sdk targeting Java 21.
Our module hardcoded Java 17, causing Gradle to reject the dependency
at resolution time. Now uses 21 when edgeDepsTest is set, 17 otherwise.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T31: Skip temporal-spring-ai on JDK < 17
Docker CI runs Java 11 which can't compile --release 17. Conditionally
exclude the module from settings.gradle and BOM when the build JDK is
below 17.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* T15: Plain tools execute in workflow context by default
Tools passed to defaultTools() that aren't activity stubs, nexus stubs,
or @SideEffectTool now execute directly in workflow context. The user
is responsible for determinism — they can call activities, sideEffect,
child workflows, etc. from within their tool methods.
Removed:
- @DeterministicTool annotation (plain tools are the default now)
- SandboxingAdvisor (no more automatic wrapping)
- LocalActivityToolCallbackWrapper, ExecuteToolLocalActivity,
ExecuteToolLocalActivityImpl (only used by SandboxingAdvisor)
- ExecuteToolLocalActivity registration from SpringAiPlugin
This matches how other Temporal AI integrations handle tools.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clean up TASK_QUEUE: remove completed and superseded
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update README with tool execution model
Document the three tool types: activity stubs (auto-detected),
@SideEffectTool (wrapped in sideEffect), and plain tools (execute
in workflow context, user responsible for determinism).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* README: mention programmatic plugin setup for optional integrations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove TASK_QUEUE.json
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Publish temporal-spring-ai from the JDK 11 release job via toolchains
The module was previously excluded from the build whenever Gradle ran on
JDK <17, which silently dropped it from the release and snapshot publish
jobs (both use JDK 11). Switch to the same pattern temporal-sdk already
uses: resolve a JDK 17+ compiler/javadoc/launcher via Gradle toolchains
when the running JVM is older, and drop the conditional include guards
in settings.gradle and the BOM so the artifact always publishes.
Shared javadoc config in gradle/java.gradle now keys flag choices off
the javadoc tool's language version instead of JavaVersion.current(),
so modules that override javadocTool (temporal-spring-ai on JDK 11)
don't get JDK 9-12-only flags passed to a JDK 17 tool.
Add JDK 17 alongside JDK 11 in the publish workflows so toolchain
discovery has a matching installation to resolve.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Align markdown tables
* Skip temporal-spring-ai tests when testJavaVersion < 17
The unit_test_jdk8 CI job runs tests with -PtestJavaVersion=11 to
verify Java 8 bytecode works on an older runtime. temporal-spring-ai
emits Java 17 bytecode, so its classes can't load on JDK 11 — the
test JVM fails with UnsupportedClassVersionError before any test runs.
Gate the test task on testJavaVersion >= springAiReleaseInt so the
module is silently skipped in that job, which is what we want: there
is no meaningful "Spring AI on JDK 11" configuration to test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>1 parent b6f4283 commit 686a62d
40 files changed
Lines changed: 3971 additions & 0 deletions
File tree
- temporal-bom
- temporal-spring-ai
- src
- main
- java/io/temporal/springai
- activity
- autoconfigure
- chat
- mcp
- model
- plugin
- tool
- util
- resources/META-INF/spring
- test/java/io/temporal/springai
- activity
- plugin
- util
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
Lines changed: 25 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
0 commit comments