Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
4f4df9e
Add temporal-spring-ai module for Spring AI integration
donald-pinckney Apr 6, 2026
31bc77e
Document callback registry lifecycle risk and add stream() override
donald-pinckney Apr 6, 2026
079089a
Add tests for temporal-spring-ai (T1-T4)
donald-pinckney Apr 6, 2026
b62adfa
Update TASK_QUEUE.json: T1-T4, T9, T11 completed
donald-pinckney Apr 6, 2026
e538674
Add T14 (NPE bug) to TASK_QUEUE.json
donald-pinckney Apr 6, 2026
c98af78
Fix UUID non-determinism, null metadata NPE, and unbounded tool loop
donald-pinckney Apr 7, 2026
54a5d40
Split SpringAiPlugin into conditional auto-configuration (T6)
donald-pinckney Apr 7, 2026
58804ad
Update TASK_QUEUE.json: T5, T6, T7, T10, T14 completed
donald-pinckney Apr 7, 2026
f4b1028
Update TASK_QUEUE.json: T12 completed
donald-pinckney Apr 7, 2026
e509673
Replace fragile string matching with instanceof in TemporalStubUtil (T8)
donald-pinckney Apr 7, 2026
0cc143e
Update TASK_QUEUE.json: T8 completed
donald-pinckney Apr 7, 2026
b09d2ff
Use WorkflowReplayer for proper replay determinism tests
donald-pinckney Apr 7, 2026
8ba4eb0
Simplify stream() exception message
donald-pinckney Apr 7, 2026
4b7aa19
Revert tool call iteration limit, match Spring AI's recursive pattern
donald-pinckney Apr 7, 2026
969aabd
Fix javadoc reference for publishToMavenLocal
donald-pinckney Apr 8, 2026
615ff92
Use SimplePlugin builder for VectorStore and EmbeddingModel plugins
donald-pinckney Apr 9, 2026
f6d781c
Clean up TASK_QUEUE.json: remove completed tasks, add T15
donald-pinckney Apr 9, 2026
336cc7b
Add link to proposed design for T15
donald-pinckney Apr 9, 2026
6d4d166
Triage Copilot and DABH review comments into TASK_QUEUE
donald-pinckney Apr 9, 2026
32e6f99
T16: Guard assistantMessage.getMedia() against null
donald-pinckney Apr 9, 2026
d9b5002
T17: Include Nexus stubs in unrecognized tool type error message
donald-pinckney Apr 9, 2026
bec50f8
T18: Use ObjectProvider to fix NoUniqueBeanDefinitionException
donald-pinckney Apr 10, 2026
9ebc25b
T19: Make replay test exercise tool calls
donald-pinckney Apr 10, 2026
6c3a107
T20: Handle duplicate MCP client names with clear error
donald-pinckney Apr 10, 2026
4e6003d
T21: Use float[] instead of List<Double> in EmbeddingModelTypes
donald-pinckney Apr 10, 2026
5d19df9
T18 fix: Use getIfUnique() instead of getIfAvailable()
donald-pinckney Apr 10, 2026
52c6d4b
Add T28: Restore plugin classes as public API per tconley review
donald-pinckney Apr 10, 2026
01fec57
Clean up TASK_QUEUE: remove completed, mark superseded
donald-pinckney Apr 10, 2026
1b5c56e
T28: Restore VectorStorePlugin and EmbeddingModelPlugin as public cla…
donald-pinckney Apr 10, 2026
bc6275f
T23: Resolved — MCP capability caching is correct
donald-pinckney Apr 10, 2026
60f2941
T24: Change from discussion to fix — rawContent should be String
donald-pinckney Apr 10, 2026
528e538
T22: Defer starter artifact to after PR merge
donald-pinckney Apr 10, 2026
d58700f
T25: Replied to DABH about docs. Added T29 for README follow-up.
donald-pinckney Apr 10, 2026
4e600da
T29: Bump to high priority, do in this PR
donald-pinckney Apr 10, 2026
e19463b
T15: Also remove LocalActivityToolCallbackWrapper and ExecuteToolLoca…
donald-pinckney Apr 10, 2026
2c97806
T24: Change ChatModelTypes.Message rawContent from Object to String
donald-pinckney Apr 10, 2026
70c8594
T29: Add README with compatibility matrix and quick start
donald-pinckney Apr 10, 2026
b11249a
T30: Fix edge CI Java version mismatch
donald-pinckney Apr 10, 2026
42f46d6
T31: Skip temporal-spring-ai on JDK < 17
donald-pinckney Apr 10, 2026
7cb1dbc
T15: Plain tools execute in workflow context by default
donald-pinckney Apr 10, 2026
f0d0f9a
Clean up TASK_QUEUE: remove completed and superseded
donald-pinckney Apr 13, 2026
7e7e5ad
Update README with tool execution model
donald-pinckney Apr 13, 2026
8e505f7
README: mention programmatic plugin setup for optional integrations
donald-pinckney Apr 13, 2026
bc7f5b1
Merge branch 'master' into d/20260406-164203
donald-pinckney Apr 13, 2026
024126d
Merge branch 'master' into d/20260406-164203
donald-pinckney Apr 16, 2026
bbcb85e
Remove TASK_QUEUE.json
donald-pinckney Apr 16, 2026
88910d6
Publish temporal-spring-ai from the JDK 11 release job via toolchains
donald-pinckney Apr 16, 2026
0fbfec4
Merge remote-tracking branch 'origin/master' into d/20260406-164203
donald-pinckney Apr 17, 2026
b6de0a7
Align markdown tables
donald-pinckney Apr 17, 2026
48780de
Skip temporal-spring-ai tests when testJavaVersion < 17
donald-pinckney Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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>
  • Loading branch information
donald-pinckney and claude committed Apr 8, 2026
commit 54a5d401df6abec19850eb02ed600e9b60dc0f84
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.temporal.springai.autoconfigure;

import io.temporal.springai.plugin.EmbeddingModelPlugin;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;

/**
* Auto-configuration for EmbeddingModel integration with Temporal.
*
* <p>Conditionally creates an {@link EmbeddingModelPlugin} when {@code spring-ai-rag} is on the
* classpath and an {@link EmbeddingModel} bean is available.
*/
@AutoConfiguration(after = SpringAiTemporalAutoConfiguration.class)
@ConditionalOnClass(name = "org.springframework.ai.embedding.EmbeddingModel")
@ConditionalOnBean(EmbeddingModel.class)
public class SpringAiEmbeddingAutoConfiguration {

@Bean
public EmbeddingModelPlugin embeddingModelPlugin(EmbeddingModel embeddingModel) {
return new EmbeddingModelPlugin(embeddingModel);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.temporal.springai.autoconfigure;

import io.temporal.springai.plugin.McpPlugin;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;

/**
* Auto-configuration for MCP (Model Context Protocol) integration with Temporal.
*
* <p>Conditionally creates a {@link McpPlugin} when {@code spring-ai-mcp} and the MCP client
* library are on the classpath.
*/
@AutoConfiguration(after = SpringAiTemporalAutoConfiguration.class)
@ConditionalOnClass(name = "io.modelcontextprotocol.client.McpSyncClient")
public class SpringAiMcpAutoConfiguration {

@Bean
public McpPlugin mcpPlugin() {
return new McpPlugin();
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
package io.temporal.springai.autoconfigure;

import io.temporal.springai.plugin.SpringAiPlugin;
import java.util.Map;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Bean;
import org.springframework.lang.Nullable;

/**
* Auto-configuration for the Spring AI Temporal plugin.
* Core auto-configuration for the Spring AI Temporal plugin.
*
* <p>Automatically registers {@link SpringAiPlugin} as a bean when Spring AI and Temporal SDK are
* on the classpath. The plugin then auto-registers Spring AI activities with all Temporal workers.
* <p>Creates the {@link SpringAiPlugin} bean which registers {@link
* io.temporal.springai.activity.ChatModelActivity} and {@link
* io.temporal.springai.tool.ExecuteToolLocalActivity} with all Temporal workers.
*
* <p>Optional integrations are handled by separate auto-configuration classes:
*
* <ul>
* <li>{@link SpringAiVectorStoreAutoConfiguration} - VectorStore support
* <li>{@link SpringAiEmbeddingAutoConfiguration} - EmbeddingModel support
* <li>{@link SpringAiMcpAutoConfiguration} - MCP support
* </ul>
*/
@AutoConfiguration
@ConditionalOnClass(
name = {"org.springframework.ai.chat.model.ChatModel", "io.temporal.worker.Worker"})
@Import(SpringAiPlugin.class)
public class SpringAiTemporalAutoConfiguration {}
public class SpringAiTemporalAutoConfiguration {

@Bean
public SpringAiPlugin springAiPlugin(
@Autowired Map<String, ChatModel> chatModels,
@Autowired(required = false) @Nullable ChatModel primaryChatModel) {
return new SpringAiPlugin(chatModels, primaryChatModel);
}
Comment thread
donald-pinckney marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.temporal.springai.autoconfigure;

import io.temporal.springai.plugin.VectorStorePlugin;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;

/**
* Auto-configuration for VectorStore integration with Temporal.
*
* <p>Conditionally creates a {@link VectorStorePlugin} when {@code spring-ai-rag} is on the
* classpath and a {@link VectorStore} bean is available.
*/
@AutoConfiguration(after = SpringAiTemporalAutoConfiguration.class)
@ConditionalOnClass(name = "org.springframework.ai.vectorstore.VectorStore")
@ConditionalOnBean(VectorStore.class)
public class SpringAiVectorStoreAutoConfiguration {

@Bean
public VectorStorePlugin vectorStorePlugin(VectorStore vectorStore) {
return new VectorStorePlugin(vectorStore);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.temporal.springai.plugin;

import io.temporal.common.SimplePlugin;
import io.temporal.springai.activity.EmbeddingModelActivityImpl;
import io.temporal.worker.Worker;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.embedding.EmbeddingModel;

/**
* Temporal plugin that registers {@link io.temporal.springai.activity.EmbeddingModelActivity} with
* workers.
*
* <p>This plugin is conditionally created by auto-configuration when Spring AI's {@link
* EmbeddingModel} is on the classpath and an EmbeddingModel bean is available.
*/
public class EmbeddingModelPlugin extends SimplePlugin {

private static final Logger log = LoggerFactory.getLogger(EmbeddingModelPlugin.class);

private final EmbeddingModel embeddingModel;

public EmbeddingModelPlugin(EmbeddingModel embeddingModel) {
super("io.temporal.spring-ai-embedding");
this.embeddingModel = embeddingModel;
}

@Override
public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {
worker.registerActivitiesImplementations(new EmbeddingModelActivityImpl(embeddingModel));
log.info("Registered EmbeddingModelActivity for task queue {}", taskQueue);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.temporal.springai.plugin;

import io.modelcontextprotocol.client.McpSyncClient;
import io.temporal.common.SimplePlugin;
import io.temporal.springai.mcp.McpClientActivityImpl;
import io.temporal.worker.Worker;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
* Temporal plugin that registers {@link io.temporal.springai.mcp.McpClientActivity} with workers.
*
* <p>This plugin is conditionally created by auto-configuration when MCP classes are on the
* classpath. MCP clients may be created late by Spring AI's auto-configuration, so this plugin
* supports deferred registration via {@link SmartInitializingSingleton}.
*/
public class McpPlugin extends SimplePlugin
implements ApplicationContextAware, SmartInitializingSingleton {

private static final Logger log = LoggerFactory.getLogger(McpPlugin.class);

private List<McpSyncClient> mcpClients = List.of();
private ApplicationContext applicationContext;
private final List<Worker> pendingWorkers = new ArrayList<>();

public McpPlugin() {
super("io.temporal.spring-ai-mcp");
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

@SuppressWarnings("unchecked")
private List<McpSyncClient> getMcpClients() {
if (!mcpClients.isEmpty()) {
return mcpClients;
}

if (applicationContext != null && applicationContext.containsBean("mcpSyncClients")) {
try {
Object bean = applicationContext.getBean("mcpSyncClients");
if (bean instanceof List<?> clientList && !clientList.isEmpty()) {
mcpClients = (List<McpSyncClient>) clientList;
log.info("Found {} MCP client(s) in ApplicationContext", mcpClients.size());
}
} catch (Exception e) {
log.debug("Failed to get mcpSyncClients bean: {}", e.getMessage());
}
}

return mcpClients;
}

@Override
public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {
List<McpSyncClient> clients = getMcpClients();
if (!clients.isEmpty()) {
worker.registerActivitiesImplementations(new McpClientActivityImpl(clients));
log.info(
"Registered McpClientActivity ({} clients) for task queue {}", clients.size(), taskQueue);
} else {
pendingWorkers.add(worker);
log.debug("MCP clients not yet available; will attempt registration after initialization");
}
}

@Override
public void afterSingletonsInstantiated() {
if (pendingWorkers.isEmpty()) {
return;
}

List<McpSyncClient> clients = getMcpClients();
if (clients.isEmpty()) {
log.debug("No MCP clients found after all beans initialized");
pendingWorkers.clear();
return;
}

for (Worker worker : pendingWorkers) {
worker.registerActivitiesImplementations(new McpClientActivityImpl(clients));
log.info("Registered deferred McpClientActivity ({} clients)", clients.size());
}
pendingWorkers.clear();
}
}
Loading