SPI Reference
TnsAI.Core uses Java's ServiceLoader mechanism extensively for cross-module extensibility. SPI interfaces define contracts in the core module; implementations live in optional modules and are discovered at runtime via META-INF/services/ registration.
How SPI Works in TnsAI
- Core defines an interface (e.g.,
CheckpointerProvider) - An optional module implements it (e.g.,
PostgresCheckpointerProvider) - The implementation is registered in
META-INF/services/<interface-fqcn> - At runtime, TnsAI 0.14.0 (
TnsAI@b021c635, TAN-2903) discovers implementations throughcom.tnsai.spi.SpiLoader, which caches an immutable snapshot per(serviceType, thread-context classloader). Maven Central0.13.0calledServiceLoader.load()on every lookup;0.14.1uses the cached loader. - Core uses the implementation without compile-time dependency on the module
Many SPI interfaces also use the Factory.discover() pattern where a nested Factory interface has a static discover() method that returns null when no implementation is on the classpath.
SpiLoader (0.14.0)
com.tnsai.spi.SpiLoader (@since 0.14.0) is the cached discovery
helper used by Core and Intelligence hot paths:
SpiLoader.load(Class<T>)— immutable providers inServiceLoaderorderSpiLoader.findFirst(Class<T>)— first provider only, still cachedSpiLoader.invalidate(Class)/invalidateAll()— tests and redeploy
Cache keys and snapshots are weak so a parent-loaded SpiLoader does
not pin an application classloader across redeploy.
Exceptions that stay on raw ServiceLoader:
MemoryStoreFactory— each store instance is agent-scoped mutable state, so every factory call must construct a fresh providerEmbeddingFunctionRegistryandContentExtractorRegistry— descriptor-firstServiceLoader.stream()so ambiguity fails before any provider constructor runs
RAG callers that reuse the cache: RetrievalSpi,
ChatKnowledgeResolver, VectorStoreProviderRegistry,
GraphStoreProviderRegistry, SourceLoaderRegistry,
QueryExpanderRegistry, RerankerRegistry, TreeNavigatorRegistry.
Core SPI Interfaces
Types in com.tnsai.spi (0.13.0). Discovery is not uniform — some are
ServiceLoader providers, some are markers, some are in-memory factories.
| Type | How you get it |
|---|---|
CheckpointerProvider / CheckpointerFactory | CheckpointerFactory.getInstance() discovers providers |
CognitiveModel | CognitiveModel.bdi() / .reactive() factories |
MessageBroker | MessageBroker.inMemory() (and module implementations) |
ResilienceStrategy | ResilienceStrategy.builder() / .noOp() |
Healthcheckable | Marker — instanceof on wired LLM/memory/MCP components |
TenantAware | Marker — instanceof when AgentBuilder.tenantId(...) is set |
McpClientFactory | ServiceLoader — META-INF/services/com.tnsai.spi.McpClientFactory in tnsai-mcp |
ToolRegistry | ToolRegistry.inMemory() — not META-INF; agent tools still use AgentBuilder |
CheckpointerProvider
com.tnsai.spi.CheckpointerProvider -- pluggable storage backends for agent state checkpointing.
public interface CheckpointerProvider {
String name();
default String description() { return name() + " checkpointer"; }
boolean isAvailable();
Checkpointer create(Map<String, Object> config);
default int priority() { return 0; }
default Optional<String> validateSpec(Map<String, Object> config) { return Optional.empty(); }
}| Method | Description |
|---|---|
name() | Unique provider name (e.g., "memory", "postgres", "sqlite") |
isAvailable() | Check if dependencies are present (e.g., JDBC driver) |
create(Map<String, Object> config) | Create a Checkpointer with config (url, path, username, etc.) |
priority() | Higher = preferred when multiple providers available |
validateSpec(config) | Validate config without creating -- returns error message or empty |
Registration: META-INF/services/com.tnsai.spi.CheckpointerProvider
Example implementation:
public class PostgresCheckpointerProvider implements CheckpointerProvider {
@Override
public String name() { return "postgres"; }
@Override
public boolean isAvailable() {
try {
Class.forName("org.postgresql.Driver");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
@Override
public Checkpointer create(Map<String, Object> config) {
String url = (String) config.get("url");
return new PostgreSQLCheckpointer(url);
}
}CheckpointerFactory
com.tnsai.spi.CheckpointerFactory is a singleton factory that discovers CheckpointerProvider implementations and provides a unified creation API.
CheckpointerFactory factory = CheckpointerFactory.getInstance();
// List providers
List<String> available = factory.availableProviders();
// Create by name
Checkpointer cp = factory.create("postgres", Map.of(
"url", "jdbc:postgresql://localhost/mydb",
"username", "user",
"password", "pass"
));
// Auto-select best available
Checkpointer cp = factory.createBest(Map.of("path", "./data"));
// Default in-memory
Checkpointer cp = factory.createInMemory();Built-in providers: "memory" (priority -100, always available) and "file" (priority -50, JSON files).
Tool registration is not a META-INF SPI
Agent tools are not discovered via ServiceLoader. There is no ToolProvider file under META-INF/services. Register tools per agent with AgentBuilder.builtInTools(BuiltInTool...), AgentBuilder.toolPojos(Object...), or AgentBuilder.dynamicTool(DynamicToolMethod) — see Tool Integration.
Each agent still builds a per-instance ToolMethodRegistry at AgentBuilder.build() from those explicit registrations. That registry is not a process-wide singleton.
LLMConfigurationSource is registered programmatically too. It lives in the integration module's SCOP package and supplies per-agent LLM overrides, but it has no META-INF/services entry and is not discovered by ServiceLoader. Register one on the bridge with SCOPBridge.llmConfigurationSource(...), using the folder(Path), environment() or noOp() factories — see External LLM Configuration.
com.tnsai.spi.ToolRegistry is a separate in-memory catalog API (ToolRegistry.inMemory()). It lives in the SPI package and is listed in package-info, but it has no META-INF/services/com.tnsai.spi.ToolRegistry registration. Do not treat it as the way AgentBuilder loads tools.
CognitiveModel
com.tnsai.spi.CognitiveModel -- unified API for agent cognitive architectures (BDI, Reactive, Hybrid).
public interface CognitiveModel {
String getModelType();
// Beliefs
void addBelief(Belief belief);
boolean removeBelief(String beliefContent);
List<Belief> getBeliefs();
List<Belief> queryBeliefs(String pattern);
void clearBeliefs();
// Goals
void addGoal(Goal goal);
boolean removeGoal(String goalId);
List<Goal> getGoals();
Optional<Goal> getTopGoal();
// Intentions
void addIntention(Intention intention);
void completeIntention(String intentionId);
List<Intention> getActiveIntentions();
Optional<Intention> getCurrentIntention();
// Reasoning cycle
Optional<Action> reason(Map<String, Object> context);
void reset();
CognitiveState snapshot();
void restore(CognitiveState state);
// Factory methods
static BDIModelBuilder bdi() { ... }
static CognitiveModel reactive() { ... }
}Inner records: Belief(content, confidence, timestamp, metadata), Goal(id, description, priority, status, parameters), Intention(id, goalId, planDescription, status, steps, currentStep), Action(type, description, parameters), CognitiveState(beliefs, goals, intentions, metadata).
CognitiveModel model = CognitiveModel.bdi()
.withBelief("User prefers concise answers")
.withGoal("Help the user effectively")
.build();
model.addBelief(Belief.of("User is a developer", 0.9));
Optional<Action> next = model.reason(Map.of("input", "help me debug"));MessageBroker
com.tnsai.spi.MessageBroker -- abstraction for agent-to-agent message passing (direct, pub/sub, request-reply, broadcast).
public interface MessageBroker {
void send(String targetAgentId, Message message);
void publish(String topic, Message message);
String subscribe(String agentId, Consumer<Message> handler);
String subscribeTopic(String topic, Consumer<Message> handler);
void unsubscribe(String subscriptionId);
CompletableFuture<Message> request(String targetAgentId, Message request);
void broadcast(Message message);
void close();
static MessageBroker inMemory() { ... }
}Message (record): id, from, to, topic, payload (Object), headers (Map), timestamp.
MessageBroker broker = MessageBroker.inMemory();
broker.subscribe("agent-1", msg -> System.out.println("Got: " + msg.payload()));
broker.publish("tasks", Message.of("Process data"));
CompletableFuture<Message> reply = broker.request("agent-1", Message.of("status?"));ResilienceStrategy
com.tnsai.spi.ResilienceStrategy -- unified abstraction for resilience patterns (retry, circuit breaker, timeout, fallback).
public interface ResilienceStrategy {
<T> T execute(Callable<T> operation) throws Exception;
default <T> T executeUnchecked(Supplier<T> operation) { ... }
default void execute(Runnable operation) throws Exception { ... }
default ResilienceStrategy andThen(ResilienceStrategy after) { ... }
default String name() { ... }
default boolean isHealthy() { return true; }
default void reset() { }
static Builder builder() { ... }
static ResilienceStrategy noOp() { ... }
}The builder composes retry, circuit breaker, timeout, and fallback:
ResilienceStrategy strategy = ResilienceStrategy.builder()
.retry(3, Duration.ofMillis(500))
.circuitBreaker(5, Duration.ofSeconds(30))
.timeout(Duration.ofSeconds(10))
.fallback(() -> "default value")
.build();
String result = strategy.execute(() -> riskyOperation());Strategies can also be composed with andThen:
ResilienceStrategy combined = retryStrategy.andThen(timeoutStrategy);Healthcheckable
com.tnsai.spi.Healthcheckable is a marker SPI, not a ServiceLoader catalog.
Components that can answer a cheap reachability probe implement
checkHealth(Duration) and return HealthStatus (never throw). Typical
implementers: LLMClient, MemoryStore, MCP handshake adapters.
AgentBuilder.withReachabilityChecks(true) runs those probes at
build() time. A component that does not implement the marker is skipped
(instanceof Healthcheckable).
HealthStatus status = ((Healthcheckable) client).checkHealth(Duration.ofSeconds(2));
if (!status.reachable()) {
// status.errorMessage() is log-safe — no credentials
}TenantAware
com.tnsai.spi.TenantAware is an empty marker interface. Implement it on
stores, tools, or MCP clients that honour AgentBuilder.tenantId(...) as a
hard isolation boundary. Pre-flight validation (AGENT-V012) warns when a
tenant id is set but a wired component is not TenantAware. The framework
does not inspect how you isolate (row filter vs schema vs database).
McpClientFactory
com.tnsai.spi.McpClientFactory lets core's reachability pipeline probe an
@MCPTool server URL without a compile-time dependency on tnsai-mcp.
tnsai-mcp ships DefaultMcpClientFactory in
META-INF/services/com.tnsai.spi.McpClientFactory. That is the only
com.tnsai.spi.* service file in the 0.13.0 tree. Without tnsai-mcp on the
classpath the validator stays silent.
public interface McpClientFactory {
Healthcheckable probeFor(String serverUrl, Map<String, Object> config);
}The returned Healthcheckable must follow the same cheap / idempotent /
in-budget / never-throw contract.
Factory.discover() Pattern
Many SPI interfaces use an inner Factory interface with a static discover() method. Examples include:
EvalHandle.Factory.discover()— returnsnulliftnsai-qualityis absentFeedbackCollector.Factory.discover()— returnsnulliftnsai-intelligenceis absentContextManagerHandle.Factory.discover()— returnsnulliftnsai-intelligenceis absentSecurityEnforcerHandle.Factory.discover()— returnsnulliftnsai-qualityis absent; callers useSecurityEnforcerHandle.NOOP(there is notnsai-securitymodule)
This pattern allows core code to initialize with no-op implementations when optional modules are not on the classpath:
EvalHandle.Factory factory = EvalHandle.Factory.discover();
this.evalHandle = factory != null ? factory.create() : EvalHandle.NOOP;Related Documentation
- Tools -- per-agent tool registration via
builtInTools/toolPojos/dynamicTool - Action System -- ActionExecutor and typed executors
- Resilience -- RetryPolicy and resilience configuration
- Advanced Agent Features -- how SPI discoveries are used in Agent