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,
ServiceLoader.load()discovers all implementations - 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.
Core SPI Interfaces
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 an SPI
Since v0.6.0, tool discovery is no longer SPI-driven. There is no ToolProvider SPI, no global ToolRegistry, no auto-discovery. Tools are registered explicitly per agent via AgentBuilder.builtInTools(BuiltInTool...), AgentBuilder.toolPojos(Object...), or AgentBuilder.dynamicTool(DynamicToolMethod) — see Tool Integration.
The only "registry" left is each agent's per-instance ToolMethodRegistry, built at AgentBuilder.build() time from the explicitly-registered POJOs and dynamic tools. It is not a service, not a singleton, and not discoverable via ServiceLoader.
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);Factory.discover() Pattern
Many SPI interfaces use an inner Factory interface with a static discover() method. Examples include:
EvalHandle.Factory.discover()-- returnsnullif tnsai-quality is absentFeedbackCollector.Factory.discover()-- returnsnullif tnsai-intelligence is absentContextManagerHandle.Factory.discover()-- returnsnullif tnsai-intelligence is absentSecurityEnforcerHandle.Factory.discover()-- returnsNOOPif tnsai-security is absent
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