Skip to content
tnsaiDocs
GitHub

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

  1. Implement the interface in a public class with a public no-argument constructor.
  2. 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 for PlannerHandle.Factory is named ...PlannerHandle$Factory.
  3. 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.JdbcAuditStore

On 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

InterfaceBuilt inModule
com.tnsai.memory.MemoryStoreNone
com.tnsai.spi.CheckpointerProviderNone registered. CheckpointerFactory adds memory and file in codetnsai-core
com.tnsai.spi.FeatureFlagNone
com.tnsai.annotations.ConfigResetRagConfigReset, SCOPBridgeConfigResettnsai-rag, tnsai-integration
com.tnsai.agents.groups.AgentGroupFactoryDefaultAgentGroupFactorytnsai-coordination
com.tnsai.coordination.messaging.DistributedMessageBrokerNone
com.tnsai.payment.x402.Wallet.FactoryNone

LLM providers

InterfaceBuilt inModule
com.tnsai.llm.LLMClientProviderLLMClientFactoryProvidertnsai-llm
com.tnsai.llm.errors.ProviderErrorMapperOne mapper per provider, such as OpenAIProviderErrorMapper and AnthropicProviderErrorMappertnsai-llm

Knowledge and RAG

InterfaceBuilt inModule
com.tnsai.memory.advanced.EmbeddingFunctionNone. See Embeddings
com.tnsai.memory.vector.VectorStoreProviderInMemoryVectorStoreProvidertnsai-core
com.tnsai.intelligence.rag.graph.GraphStoreProviderInMemoryGraphStoreProvider, KnowledgeToolsGraphStoreProvider, Neo4jGraphStoreProvidertnsai-rag
com.tnsai.intelligence.rag.binding.SourceLoaderLocalFileSourceLoader, HttpKnowledgeSourceLoadertnsai-rag
com.tnsai.rag.ContentExtractorStructuredTextContentExtractor, OfficeDocumentContentExtractortnsai-rag
com.tnsai.rag.ChunkContextGeneratorNone
com.tnsai.rag.RetrievalSpiDefaultRetrievalSpitnsai-rag
com.tnsai.rag.RetrievalEngineProviderDefaultRetrievalEngineProvidertnsai-rag
com.tnsai.rag.ChatKnowledgeResolverDefaultChatKnowledgeResolvertnsai-rag
com.tnsai.intelligence.rag.QueryExpanderNone
com.tnsai.intelligence.rag.RerankerNone
com.tnsai.intelligence.rag.TreeNavigatorNone

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.

InterfaceBuilt inModule
com.tnsai.planning.PlannerHandle.FactoryDefaultPlannerHandleFactorytnsai-intelligence
com.tnsai.reasoning.ReasoningStrategyHandle.FactoryDefaultReasoningStrategyHandleFactorytnsai-intelligence
com.tnsai.context.ContextManagerHandle.FactoryDefaultContextManagerFactorytnsai-intelligence
com.tnsai.feedback.FeedbackCollector.FactoryDefaultFeedbackCollectorFactorytnsai-intelligence
com.tnsai.security.SecurityEnforcerHandle.FactoryDefaultSecurityEnforcerFactorytnsai-quality
com.tnsai.invariants.InvariantCheckerHandle.FactoryDefaultInvariantCheckerFactorytnsai-quality
com.tnsai.eval.hooks.EvalHandle.FactoryDefaultEvalHandleFactorytnsai-quality
com.tnsai.protocols.ProtocolManagerHandle.FactoryDefaultProtocolManagerFactorytnsai-coordination

Security, quality and evaluation

InterfaceBuilt inModule
com.tnsai.security.PermissionProviderDefaultPermissionProvidertnsai-quality
com.tnsai.security.audit.AuditStoreInMemoryAuditStoretnsai-quality
com.tnsai.quality.sandbox.SandboxFactoryProcessSandboxFactory, ContainerSandboxFactory, WasmSandboxFactorytnsai-quality
com.tnsai.evaluation.spi.EvaluatorThe built-in evaluators, such as FaithfulnessEvaluator. See Evaluatorstnsai-evaluation

Observability, channels and MCP

InterfaceBuilt inModule
com.tnsai.observability.events.AgentEventPublisherNone
com.tnsai.observability.errors.ErrorReportPublisherSlf4jErrorReportPublishertnsai-core
com.tnsai.channels.api.ChannelAdapterTelegramAdapter, SlackChannel, DiscordChannel, WhatsAppChannel, EmailChannel, CliChanneltnsai-channels
com.tnsai.channels.api.ChannelAdapterFactoryOne factory per adapter, such as SlackChannelFactorytnsai-channels
com.tnsai.spi.McpClientFactoryDefaultMcpClientFactorytnsai-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:

MethodReturns
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:

  • MemoryStoreFactory creates fresh providers on every call, because each memory store holds one agent's state.
  • EmbeddingFunctionRegistry reads the provider descriptors first, so two installed embedding functions fail before either one is constructed.
  • ContentExtractorRegistry sorts 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.

TypeHow you get it
PaymentBrokerPaymentBroker.noOp() for free internal calls, or X402PaymentBroker from tnsai-payments. It has two methods, quote(payer, payee, service) and settle(quote). See Payments
MessageBrokerMessageBroker.inMemory(). See Messaging
ResilienceStrategyResilienceStrategy.builder() with retry, circuitBreaker, timeout and fallback, or ResilienceStrategy.noOp(). See Resilience
ToolRegistryToolRegistry.inMemory(), a standalone tool catalog. Agents do not load their tools from it
HealthcheckableImplement 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
TenantAwareA 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(...) and dynamicTool(...). See Register tools.
  • LLMConfigurationSource, for per-agent LLM overrides on the SCOP bridge: SCOPBridge.llmConfigurationSource(...). See LLM configuration.