Conversation
donald-pinckney
left a comment
There was a problem hiding this comment.
Self-review: temporal-spring-ai plugin
What's done well
Determinism architecture is sound. The core design — routing all non-deterministic operations (LLM calls, vector store ops, embeddings, MCP tools) through Temporal activities — is exactly right. The three-tier tool system (@DeterministicTool for pure functions, @SideEffectTool for cheap non-determinism, activity stubs for durable I/O) maps cleanly onto Temporal's primitives.
Tool execution stays in the workflow. ChatModelActivityImpl sets internalToolExecutionEnabled(false) and only passes tool definitions to the model. The actual tool dispatch happens back in the workflow via ToolCallingManager, which means tool calls respect their Temporal wrapping (activity, sideEffect, etc.).
SideEffectToolCallback correctly wraps each call in Workflow.sideEffect(), recording the result in history on first execution and replaying the stored value.
ActivityChatModel.call() handles the tool loop correctly — it recursively calls itself when the model requests tools that don't returnDirect, maintaining proper conversation history.
Issues flagged inline below
...oral-spring-ai/src/main/java/io/temporal/springai/tool/LocalActivityToolCallbackWrapper.java
Outdated
Show resolved
Hide resolved
| */ | ||
| public class LocalActivityToolCallbackWrapper implements ToolCallback { | ||
|
|
||
| private static final Map<String, ToolCallback> CALLBACK_REGISTRY = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
Medium severity — static registry lifecycle risk.
This static ConcurrentHashMap holds live ToolCallback references. Callbacks are removed in a finally block after the local activity completes, but if the workflow is evicted from the worker cache mid-execution (before finally runs), callbacks leak. Worth documenting or adding periodic cleanup.
There was a problem hiding this comment.
Done — added javadoc documenting the eviction leak risk and pointing to getRegisteredCallbackCount() for monitoring.
There was a problem hiding this comment.
Would a bounded size or TTL-based eviction strategy for the hash map make more sense, as opposed to just giving users a method to monitor the count? Even some (imo very high) limit like 10,000? Or maybe log a warning in case a user hits some huge number like that? Wdyt?
There was a problem hiding this comment.
most likely not going ahead with SandboxingAdvisor, which would also kill this.
temporal-spring-ai/src/main/java/io/temporal/springai/util/TemporalStubUtil.java
Outdated
Show resolved
Hide resolved
temporal-spring-ai/src/main/java/io/temporal/springai/model/ActivityChatModel.java
Outdated
Show resolved
Hide resolved
temporal-spring-ai/src/main/java/io/temporal/springai/plugin/SpringAiPlugin.java
Outdated
Show resolved
Hide resolved
temporal-spring-ai/src/main/java/io/temporal/springai/model/ActivityChatModel.java
Show resolved
Hide resolved
f210b63 to
35e9b29
Compare
donald-pinckney
left a comment
There was a problem hiding this comment.
Found an additional bug during testing.
temporal-spring-ai/src/main/java/io/temporal/springai/model/ActivityChatModel.java
Outdated
Show resolved
Hide resolved
6b01988 to
4fd80ec
Compare
There was a problem hiding this comment.
Obviously will delete this prior to merging ;)
ee8e3c0 to
2a40064
Compare
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>
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>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2a40064 to
969aabd
Compare
| * <p>This plugin is conditionally created by auto-configuration when Spring AI's {@link | ||
| * VectorStore} is on the classpath and a VectorStore bean is available. | ||
| */ | ||
| public class VectorStorePlugin extends SimplePlugin { |
There was a problem hiding this comment.
Is the primary reason for these being separate plugins because of spring configuration interaction? Otherwise I would think it makes more sense to have one with configurations.
There was a problem hiding this comment.
it's not just Spring config interaction, it's about not forcing transitive dependencies on users. A single plugin would either need all deps as implementation or use reflection/Object types to avoid class references (fragile/yucky). Given that normal (non-Temporal) Spring AI already ships things as separate artifacts, this seemed like the most natural spring-ai-native design.
Other option is to have implementation dependencies on Spring AI's various artifacts (for MCP, RAG, etc.), which is simpler but aligns less with Spring from what I've seen. Others could weigh in here though.
To be clear, you can't just have compileOnly deps on those optional dependencies, because Spring needs to load and introspect the plugin class at startup, so any field types that don't exist at runtime cause a ClassNotFoundException.
temporal-spring-ai/src/main/java/io/temporal/springai/plugin/VectorStorePlugin.java
Outdated
Show resolved
Hide resolved
| * | ||
| * @see ExecuteToolLocalActivity | ||
| */ | ||
| public class LocalActivityToolCallbackWrapper implements ToolCallback { |
There was a problem hiding this comment.
Is there a good reason we are making local activities so primary?
There was a problem hiding this comment.
would be resolved following other discussion below
There was a problem hiding this comment.
Pull request overview
Adds a new temporal-spring-ai module that integrates Spring AI with Temporal by making chat model calls, tool execution, embeddings, vector store operations, and MCP tool calls durable Temporal primitives.
Changes:
- Introduces
ActivityChatModel+ChatModelActivityto run LLM calls as activities while executing tools deterministically in workflows. - Adds tool conversion utilities (activity/local activity/Nexus stubs,
@DeterministicTool,@SideEffectTool) plus a sandboxing advisor for unsafe tools. - Adds Spring Boot auto-configuration + worker plugins for ChatModel, VectorStore, EmbeddingModel, and MCP, along with unit + replay determinism tests.
Reviewed changes
Copilot reviewed 45 out of 45 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| temporal-spring-ai/src/main/java/io/temporal/springai/model/ActivityChatModel.java | Workflow-safe ChatModel that delegates model calls to a Temporal activity and loops for tool execution. |
| temporal-spring-ai/src/main/java/io/temporal/springai/activity/ChatModelActivity.java | Activity interface for invoking Spring AI chat models durably. |
| temporal-spring-ai/src/main/java/io/temporal/springai/activity/ChatModelActivityImpl.java | Activity implementation converting between serializable types and Spring AI prompt/response types. |
| temporal-spring-ai/src/main/java/io/temporal/springai/model/ChatModelTypes.java | Serializable request/response/message/tool types for activity communication. |
| temporal-spring-ai/src/main/java/io/temporal/springai/chat/TemporalChatClient.java | ChatClient builder that auto-converts Temporal primitives into Spring AI tool callbacks. |
| temporal-spring-ai/src/main/java/io/temporal/springai/util/TemporalToolUtil.java | Converts provided tool objects into appropriate ToolCallback implementations. |
| temporal-spring-ai/src/main/java/io/temporal/springai/util/TemporalStubUtil.java | Detects Temporal stub types via proxy invocation handlers. |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/DeterministicTool.java | Annotation marking tools safe to execute directly in workflows. |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/SideEffectTool.java | Annotation marking tools to be executed via Workflow.sideEffect(). |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/SideEffectToolCallback.java | Wrapper executing tool callbacks inside Workflow.sideEffect() for determinism. |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/ActivityToolUtil.java | Extracts Spring AI tool definitions from activity interfaces/stubs. |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/ActivityToolCallback.java | Marker wrapper for callbacks backed by Temporal activity stubs. |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/NexusToolUtil.java | Extracts Spring AI tool definitions from Nexus service stubs. |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/NexusToolCallback.java | Marker wrapper for callbacks backed by Nexus service stubs. |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/ExecuteToolLocalActivity.java | Local-activity interface used to execute otherwise-unsafe tool callbacks deterministically. |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/ExecuteToolLocalActivityImpl.java | Local-activity implementation that looks up and executes registered callbacks. |
| temporal-spring-ai/src/main/java/io/temporal/springai/tool/LocalActivityToolCallbackWrapper.java | Wraps arbitrary callbacks into a local activity using a static callback registry. |
| temporal-spring-ai/src/main/java/io/temporal/springai/advisor/SandboxingAdvisor.java | Advisor that wraps non-Temporal-safe tool callbacks in local activities with warnings. |
| temporal-spring-ai/src/main/java/io/temporal/springai/plugin/SpringAiPlugin.java | Core worker plugin registering ChatModel + tool-execution local activity. |
| temporal-spring-ai/src/main/java/io/temporal/springai/plugin/VectorStorePlugin.java | Optional worker plugin registering VectorStore activity. |
| temporal-spring-ai/src/main/java/io/temporal/springai/plugin/EmbeddingModelPlugin.java | Optional worker plugin registering EmbeddingModel activity. |
| temporal-spring-ai/src/main/java/io/temporal/springai/plugin/McpPlugin.java | Optional worker plugin registering MCP client activity (supports deferred registration). |
| temporal-spring-ai/src/main/java/io/temporal/springai/activity/VectorStoreActivity.java | Activity interface for vector store operations. |
| temporal-spring-ai/src/main/java/io/temporal/springai/activity/VectorStoreActivityImpl.java | Vector store activity implementation + type conversions. |
| temporal-spring-ai/src/main/java/io/temporal/springai/model/VectorStoreTypes.java | Serializable types for VectorStore activity calls. |
| temporal-spring-ai/src/main/java/io/temporal/springai/activity/EmbeddingModelActivity.java | Activity interface for embedding operations. |
| temporal-spring-ai/src/main/java/io/temporal/springai/activity/EmbeddingModelActivityImpl.java | Embedding activity implementation + conversions. |
| temporal-spring-ai/src/main/java/io/temporal/springai/model/EmbeddingModelTypes.java | Serializable types for EmbeddingModel activity calls. |
| temporal-spring-ai/src/main/java/io/temporal/springai/mcp/McpClientActivity.java | Activity interface for MCP client operations. |
| temporal-spring-ai/src/main/java/io/temporal/springai/mcp/McpClientActivityImpl.java | MCP activity implementation delegating to Spring AI MCP sync clients. |
| temporal-spring-ai/src/main/java/io/temporal/springai/mcp/ActivityMcpClient.java | Workflow-side wrapper around MCP activities (cached lookups). |
| temporal-spring-ai/src/main/java/io/temporal/springai/mcp/McpToolCallback.java | ToolCallback implementation that calls MCP tools through activities. |
| temporal-spring-ai/src/main/java/io/temporal/springai/autoconfigure/SpringAiTemporalAutoConfiguration.java | Core Spring Boot auto-config producing the main Spring AI plugin bean. |
| temporal-spring-ai/src/main/java/io/temporal/springai/autoconfigure/SpringAiVectorStoreAutoConfiguration.java | Conditional auto-config for vector store plugin. |
| temporal-spring-ai/src/main/java/io/temporal/springai/autoconfigure/SpringAiEmbeddingAutoConfiguration.java | Conditional auto-config for embedding plugin. |
| temporal-spring-ai/src/main/java/io/temporal/springai/autoconfigure/SpringAiMcpAutoConfiguration.java | Conditional auto-config for MCP plugin. |
| temporal-spring-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | Registers the module’s Spring Boot auto-configurations. |
| temporal-spring-ai/src/test/java/io/temporal/springai/activity/ChatModelActivityImplTest.java | Unit tests for serializable type conversions and tool-call preservation. |
| temporal-spring-ai/src/test/java/io/temporal/springai/util/TemporalToolUtilTest.java | Unit tests for tool detection/conversion and stub detection utilities. |
| temporal-spring-ai/src/test/java/io/temporal/springai/plugin/SpringAiPluginTest.java | Unit tests for plugin registration behavior. |
| temporal-spring-ai/src/test/java/io/temporal/springai/WorkflowDeterminismTest.java | Replay test intended to validate determinism via captured history replay. |
| temporal-spring-ai/build.gradle | New module build with Java 17 target and Spring AI dependencies. |
| temporal-bom/build.gradle | Adds temporal-spring-ai to the BOM. |
| settings.gradle | Includes the new temporal-spring-ai module in the build. |
| TASK_QUEUE.json | Task tracking artifact for the module work items. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
temporal-spring-ai/src/main/java/io/temporal/springai/model/ActivityChatModel.java
Outdated
Show resolved
Hide resolved
temporal-spring-ai/src/main/java/io/temporal/springai/util/TemporalToolUtil.java
Outdated
Show resolved
Hide resolved
...g-ai/src/main/java/io/temporal/springai/autoconfigure/SpringAiTemporalAutoConfiguration.java
Show resolved
Hide resolved
temporal-spring-ai/src/test/java/io/temporal/springai/WorkflowDeterminismTest.java
Show resolved
Hide resolved
DABH
left a comment
There was a problem hiding this comment.
Looks like a ton of progress in the right direction! 🚀 Just a few questions/comments (on top of what others have already pointed out). Take them or leave them :)
| description = '''Temporal Java SDK Spring AI Plugin''' | ||
|
|
||
| ext { | ||
| springAiVersion = '1.1.0' |
There was a problem hiding this comment.
Probably clarify in the docs/readme the compatibility matrix for the module? (So - maybe not in this PR - but at least in the forthcoming docs PR)
There was a problem hiding this comment.
Good call — will add a compatibility matrix to the module README. Tracking as a follow-up task.
- 🤖 Generated with Claude Code
| */ | ||
| public class LocalActivityToolCallbackWrapper implements ToolCallback { | ||
|
|
||
| private static final Map<String, ToolCallback> CALLBACK_REGISTRY = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
Would a bounded size or TTL-based eviction strategy for the hash map make more sense, as opposed to just giving users a method to monitor the count? Even some (imo very high) limit like 10,000? Or maybe log a warning in case a user hits some huge number like that? Wdyt?
| * @see io.temporal.springai.tool.SideEffectTool | ||
| * @see LocalActivityToolCallbackWrapper | ||
| */ | ||
| public class SandboxingAdvisor implements CallAdvisor { |
There was a problem hiding this comment.
If you do go ahead with this route, it looks like tests are lacking at the moment, is there a meaningful way to explicitly test adviseCall?
There was a problem hiding this comment.
most likely not going ahead with SandboxingAdvisor
| if (serverCapabilities == null) { | ||
| serverCapabilities = activity.getServerCapabilities(); | ||
| } | ||
| return serverCapabilities; |
There was a problem hiding this comment.
Does it make sense to cache this in terms of replayability? If the workflow is replayed in some way where the activity's server capabilities have changed, we'd get a stale value? Is that a realistic thing we need to worry about?
There was a problem hiding this comment.
This doesn't matter in terms of replayability. In fact, this cache won't be hit (initially) during replay, but Temporal itself will return the cached getServerCapabilities activity result.
When this cache will be hit is during multiple calls to getServerCapabilities throughout the life of the workflow: in those cases we don't do the whole activity and thus MCP remote call, we just return the cached thing.
IMO its pretty common practice to cache server capabilities with MCP, and it would be I think a bit confusing if the MCP client were able to react to new capabilities during the execution of a workflow, but not during later replay of a workflow.
temporal-spring-ai/src/main/java/io/temporal/springai/mcp/McpClientActivityImpl.java
Outdated
Show resolved
Hide resolved
| // Note: ToolContext cannot be passed through the activity, so we ignore it here. | ||
| // If context is needed, consider using activity parameters or workflow state. |
There was a problem hiding this comment.
also would go away
temporal-spring-ai/src/main/java/io/temporal/springai/model/ChatModelTypes.java
Outdated
Show resolved
Hide resolved
temporal-spring-ai/src/main/java/io/temporal/springai/activity/EmbeddingModelActivityImpl.java
Outdated
Show resolved
Hide resolved
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>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
temporal-spring-ai/src/main/java/io/temporal/springai/plugin/SpringAiPlugin.java
Outdated
Show resolved
Hide resolved
...i/src/main/java/io/temporal/springai/autoconfigure/SpringAiVectorStoreAutoConfiguration.java
Outdated
Show resolved
Hide resolved
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>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sses 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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lActivity Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
Summary
Adds
temporal-spring-aimodule that integrates Spring AI with Temporal workflows, making AI model calls, tool execution, vector store operations, embeddings, and MCP tool calls durable Temporal primitives.Design
The core problem
Spring AI's
ChatModel.call()does two things in one method: it calls the LLM and executes any tools the LLM requests, looping until the model returns a final text response. In a Temporal workflow, these need to happen in different execution contexts:How ActivityChatModel solves this
ActivityChatModelimplements Spring AI'sChatModelinterface with the same recursivecall()→internalCall()pattern used byOpenAiChatModeland other Spring AI implementations. The difference is what happens inside:ChatModelTypes, sends it toChatModelActivity(a Temporal activity), gets back the responseToolCallingManager.executeToolCalls()runs the tools — these are ourActivityToolCallback,SideEffectToolCallback, etc. which execute as Temporal activities orWorkflow.sideEffect()internalCall()) until the model returns a final responseThe activity-side
ChatModelActivityImplsetsinternalToolExecutionEnabled(false)so the actual LLM provider (OpenAI, Anthropic, etc.) only returns tool call requests without executing them — execution happens back in the workflow.Tool classification
Tools passed to
TemporalChatClient.builder().defaultTools()are automatically classified:instanceof ActivityInvocationHandlerinstanceof LocalActivityInvocationHandler@DeterministicTool@SideEffectToolWorkflow.sideEffect()instanceof NexusServiceInvocationHandlerConditional auto-configuration
Optional integrations only load when their dependencies are on the classpath, matching Spring AI's own approach (where RAG, MCP, etc. are separate opt-in starters):
SpringAiTemporalAutoConfigurationChatModel+WorkerSpringAiPluginChatModelActivity,ExecuteToolLocalActivitySpringAiVectorStoreAutoConfigurationVectorStorebeanVectorStorePluginVectorStoreActivitySpringAiEmbeddingAutoConfigurationEmbeddingModelbeanEmbeddingModelPluginEmbeddingModelActivitySpringAiMcpAutoConfigurationMcpSyncClientclassMcpPluginMcpClientActivityRelated
Test plan
WorkflowReplayer— WorkflowDeterminismTest (2 tests)🤖 Generated with Claude Code