# 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

1. Core defines an interface (e.g., `CheckpointerProvider`)
2. An optional module implements it (e.g., `PostgresCheckpointerProvider`)
3. The implementation is registered in `META-INF/services/<interface-fqcn>`
4. At runtime, TnsAI 0.14.0 (`TnsAI@b021c635`, TAN-2903) discovers
   implementations through `com.tnsai.spi.SpiLoader`, which caches an
   immutable snapshot per `(serviceType, thread-context classloader)`.
   Maven Central `0.13.0` called `ServiceLoader.load()` on every
   lookup; `0.14.1` uses the cached loader.
5. 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 in `ServiceLoader` order
- `SpiLoader.findFirst(Class<T>)` — first provider only, still cached
- `SpiLoader.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 provider
- `EmbeddingFunctionRegistry` and `ContentExtractorRegistry` —
  descriptor-first `ServiceLoader.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.

```java
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:**

```java
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.

```java
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](../capabilities/tools/registration.md).

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](../capabilities/llm/configuration-sources.md).

`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).

```java
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)`.

```java
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).

```java
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`.

```java
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).

```java
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:

```java
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`:

```java
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`).

```java
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.

```java
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()` — returns `null` if `tnsai-quality` is absent
- `FeedbackCollector.Factory.discover()` — returns `null` if `tnsai-intelligence` is absent
- `ContextManagerHandle.Factory.discover()` — returns `null` if `tnsai-intelligence` is absent
- `SecurityEnforcerHandle.Factory.discover()` — returns `null` if `tnsai-quality` is absent; callers use `SecurityEnforcerHandle.NOOP` (there is no `tnsai-security` module)

This pattern allows core code to initialize with no-op implementations when optional modules are not on the classpath:

```java
EvalHandle.Factory factory = EvalHandle.Factory.discover();
this.evalHandle = factory != null ? factory.create() : EvalHandle.NOOP;
```

## Related Documentation

- [Tools](../capabilities/tools/registration.md) -- per-agent tool registration via `builtInTools` / `toolPojos` / `dynamicTool`
- [Action System](../agents/fundamentals/action-system.md) -- ActionExecutor and typed executors
- [Resilience](../agents/reliability/resilience.md) -- RetryPolicy and resilience configuration
- [Advanced Agent Features](../agents/advanced.md) -- how SPI discoveries are used in Agent
