SPI reference
TnsAI finds many of its implementations with Java's ServiceLoader. An interface lives in one module, often tnsai-core, and an optional module implements it and lists the implementation in a META-INF/services/ file. When the optional module is on the classpath, the framework picks the implementation up. When it is absent, the framework falls back to a no-op or reports that the feature is unavailable.
Register an implementation
- Implement the interface in a public class with a public no-argument constructor.
- Create
src/main/resources/META-INF/services/<interface binary name>. For a nested interface, the binary name puts$before the nested name, so the file forPlannerHandle.Factoryis named...PlannerHandle$Factory. - Put the fully qualified class name of your implementation on its own line in that file.
For example, an audit store that writes to your own database:
# src/main/resources/META-INF/services/com.tnsai.security.audit.AuditStore
com.example.audit.JdbcAuditStoreOn the module path, declare the implementation with provides ... with ... in module-info.java instead.
Service interfaces
These are the interfaces the framework loads with ServiceLoader. "Built in" lists the implementations that TnsAI's own modules register.
Agents and runtime
| Interface | Built in | Module |
|---|---|---|
com.tnsai.memory.MemoryStore | None | |
com.tnsai.spi.CheckpointerProvider | None registered. CheckpointerFactory adds memory and file in code | tnsai-core |
com.tnsai.spi.FeatureFlag | None | |
com.tnsai.annotations.ConfigReset | RagConfigReset, SCOPBridgeConfigReset | tnsai-rag, tnsai-integration |
com.tnsai.agents.groups.AgentGroupFactory | DefaultAgentGroupFactory | tnsai-coordination |
com.tnsai.coordination.messaging.DistributedMessageBroker | None | |
com.tnsai.payment.x402.Wallet.Factory | None |
LLM providers
| Interface | Built in | Module |
|---|---|---|
com.tnsai.llm.LLMClientProvider | LLMClientFactoryProvider | tnsai-llm |
com.tnsai.llm.errors.ProviderErrorMapper | One mapper per provider, such as OpenAIProviderErrorMapper and AnthropicProviderErrorMapper | tnsai-llm |
Knowledge and RAG
| Interface | Built in | Module |
|---|---|---|
com.tnsai.memory.advanced.EmbeddingFunction | None. See Embeddings | |
com.tnsai.memory.vector.VectorStoreProvider | InMemoryVectorStoreProvider | tnsai-core |
com.tnsai.intelligence.rag.graph.GraphStoreProvider | InMemoryGraphStoreProvider, KnowledgeToolsGraphStoreProvider, Neo4jGraphStoreProvider | tnsai-rag |
com.tnsai.intelligence.rag.binding.SourceLoader | LocalFileSourceLoader, HttpKnowledgeSourceLoader | tnsai-rag |
com.tnsai.rag.ContentExtractor | StructuredTextContentExtractor, OfficeDocumentContentExtractor | tnsai-rag |
com.tnsai.rag.ChunkContextGenerator | None | |
com.tnsai.rag.RetrievalSpi | DefaultRetrievalSpi | tnsai-rag |
com.tnsai.rag.RetrievalEngineProvider | DefaultRetrievalEngineProvider | tnsai-rag |
com.tnsai.rag.ChatKnowledgeResolver | DefaultChatKnowledgeResolver | tnsai-rag |
com.tnsai.intelligence.rag.QueryExpander | None | |
com.tnsai.intelligence.rag.Reranker | None | |
com.tnsai.intelligence.rag.TreeNavigator | None |
Optional-module handles
tnsai-core reaches the intelligence, quality and coordination modules through a nested Factory interface on each handle. Factory.discover() returns the first registered factory, or null when the module is absent, and the caller then uses the handle's no-op.
| Interface | Built in | Module |
|---|---|---|
com.tnsai.planning.PlannerHandle.Factory | DefaultPlannerHandleFactory | tnsai-intelligence |
com.tnsai.reasoning.ReasoningStrategyHandle.Factory | DefaultReasoningStrategyHandleFactory | tnsai-intelligence |
com.tnsai.context.ContextManagerHandle.Factory | DefaultContextManagerFactory | tnsai-intelligence |
com.tnsai.feedback.FeedbackCollector.Factory | DefaultFeedbackCollectorFactory | tnsai-intelligence |
com.tnsai.security.SecurityEnforcerHandle.Factory | DefaultSecurityEnforcerFactory | tnsai-quality |
com.tnsai.invariants.InvariantCheckerHandle.Factory | DefaultInvariantCheckerFactory | tnsai-quality |
com.tnsai.eval.hooks.EvalHandle.Factory | DefaultEvalHandleFactory | tnsai-quality |
com.tnsai.protocols.ProtocolManagerHandle.Factory | DefaultProtocolManagerFactory | tnsai-coordination |
Security, quality and evaluation
| Interface | Built in | Module |
|---|---|---|
com.tnsai.security.PermissionProvider | DefaultPermissionProvider | tnsai-quality |
com.tnsai.security.audit.AuditStore | InMemoryAuditStore | tnsai-quality |
com.tnsai.quality.sandbox.SandboxFactory | ProcessSandboxFactory, ContainerSandboxFactory, WasmSandboxFactory | tnsai-quality |
com.tnsai.evaluation.spi.Evaluator | The built-in evaluators, such as FaithfulnessEvaluator. See Evaluators | tnsai-evaluation |
Observability, channels and MCP
| Interface | Built in | Module |
|---|---|---|
com.tnsai.observability.events.AgentEventPublisher | None | |
com.tnsai.observability.errors.ErrorReportPublisher | Slf4jErrorReportPublisher | tnsai-core |
com.tnsai.channels.api.ChannelAdapter | TelegramAdapter, SlackChannel, DiscordChannel, WhatsAppChannel, EmailChannel, CliChannel | tnsai-channels |
com.tnsai.channels.api.ChannelAdapterFactory | One factory per adapter, such as SlackChannelFactory | tnsai-channels |
com.tnsai.spi.McpClientFactory | DefaultMcpClientFactory | tnsai-mcp |
How lookups are cached
Most lookups go through com.tnsai.spi.SpiLoader, which keeps one immutable snapshot of the providers per service type and class loader:
| Method | Returns |
|---|---|
SpiLoader.load(type) | Every provider, in ServiceLoader order |
SpiLoader.findFirst(type) | The first provider |
SpiLoader.invalidate(type), SpiLoader.invalidateAll() | Nothing; they drop the cached snapshots, for tests and redeploys |
The cache holds its keys and snapshots weakly, so it does not keep an application class loader alive after a redeploy. Three lookups call ServiceLoader directly instead:
MemoryStoreFactorycreates fresh providers on every call, because each memory store holds one agent's state.EmbeddingFunctionRegistryreads the provider descriptors first, so two installed embedding functions fail before either one is constructed.ContentExtractorRegistrysorts extractors by class name, so load order never decides which one handles a format. A built-in extractor gives way to yours for the same format, and two of your own that claim one format fail.
Checkpointers
CheckpointerFactory.getInstance() collects every CheckpointerProvider and adds two of its own: memory (priority -100) and file (priority -50, JSON files). When two providers share a name, the one with the higher priority wins.
CheckpointerFactory factory = CheckpointerFactory.getInstance();
List<String> available = factory.availableProviders();
Checkpointer files = factory.create("file", Map.of("path", "./data"));
Checkpointer best = factory.createBest(Map.of("path", "./data")); // highest-priority available provider
Checkpointer memory = factory.createInMemory();A provider implements name(), isAvailable() and create(config), and can override description(), priority() and validateSpec(config). TnsAI ships no database provider. This example shows the shape of one you would write yourself and register under META-INF/services/com.tnsai.spi.CheckpointerProvider:
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) {
return new MyPostgresCheckpointer((String) config.get("url")); // your Checkpointer implementation
}
}Types you create in code
These types are not loaded with ServiceLoader. You create them with their factories or implement them on your own components.
| Type | How you get it |
|---|---|
PaymentBroker | PaymentBroker.noOp() for free internal calls, or X402PaymentBroker from tnsai-payments. It has two methods, quote(payer, payee, service) and settle(quote). See Payments |
MessageBroker | MessageBroker.inMemory(). See Messaging |
ResilienceStrategy | ResilienceStrategy.builder() with retry, circuitBreaker, timeout and fallback, or ResilienceStrategy.noOp(). See Resilience |
ToolRegistry | ToolRegistry.inMemory(), a standalone tool catalog. Agents do not load their tools from it |
Healthcheckable | Implement checkHealth(Duration) on your LLM client or memory store. With AgentBuilder.withReachabilityChecks(true), build() probes the agent's LLM client and memory store when they implement it, and probes MCP servers through McpClientFactory |
TenantAware | A marker for memory stores that keep tenants apart. When AgentBuilder.tenantId(...) is set, agent validation warns if the memory store does not implement it |
CognitiveModel
CognitiveModel holds beliefs, goals and intentions, and reason(context) returns the next Action, if there is one. CognitiveModel.bdi() returns a builder with withBelief, withGoal and build(). CognitiveModel.reactive() returns a reactive model. snapshot() and restore(state) save and reload the model's state. AgentBuilder does not wire a cognitive model; you keep and call it yourself.
Two other extension points are registered in code as well:
- Agent tools:
AgentBuilder.builtInTools(...),toolPojos(...)anddynamicTool(...). See Register tools. LLMConfigurationSource, for per-agent LLM overrides on the SCOP bridge:SCOPBridge.llmConfigurationSource(...). See LLM configuration.