systemprompt-traits 0.2.1

Trait-first interface contracts for systemprompt.io AI governance infrastructure. Repository, provider, and service abstractions shared across the MCP governance pipeline.
Documentation

systemprompt-traits

Minimal shared traits and contracts for systemprompt.io.

Crates.io Documentation License: BUSL-1.1

Overview

Part of the Shared layer in the systemprompt.io architecture. Infrastructure · Self-Hosted Deployment

This crate provides the core trait definitions that enable polymorphism, dependency injection, and consistent patterns across the systemprompt.io codebase.

Installation

Add to your Cargo.toml:

[dependencies]
systemprompt-traits = "0.0.1"

Feature Flags

Feature Default Description
web No Axum router types for ApiModule trait

Traits

Repository Traits

Repository - Base trait for all repository implementations

use systemprompt_traits::{Repository, RepositoryError};

impl Repository for MyRepository {
    type Pool = DbPool;
    type Error = RepositoryError;

    fn pool(&self) -> &Self::Pool {
        &self.db_pool
    }
}

CrudRepository<T> - Generic CRUD operations trait

use systemprompt_traits::CrudRepository;

impl CrudRepository<User> for UserRepository {
    type Id = String;

    async fn create(&self, entity: User) -> Result<User, Self::Error> { ... }
    async fn get(&self, id: Self::Id) -> Result<Option<User>, Self::Error> { ... }
    async fn update(&self, entity: User) -> Result<User, Self::Error> { ... }
    async fn delete(&self, id: Self::Id) -> Result<(), Self::Error> { ... }
    async fn list(&self) -> Result<Vec<User>, Self::Error> { ... }
}

RepositoryError - Standard error type for repository operations

pub enum RepositoryError {
    DatabaseError(sqlx::Error),
    NotFound(String),
    SerializationError(serde_json::Error),
    InvalidData(String),
    ConstraintViolation(String),
    GenericError(anyhow::Error),
}

Context Traits

AppContext - Application context trait for dependency injection

use systemprompt_traits::AppContext;

impl AppContext for MyAppContext {
    fn config(&self) -> Arc<dyn ConfigProvider> { ... }
    fn module_registry(&self) -> Arc<dyn ModuleRegistry> { ... }
    fn database_handle(&self) -> Arc<dyn DatabaseHandle> { ... }
}

ConfigProvider - Configuration provider trait

impl ConfigProvider for Config {
    fn get(&self, key: &str) -> Option<String> { ... }
    fn database_url(&self) -> &str { ... }
    fn system_path(&self) -> &str { ... }
    fn jwt_secret(&self) -> &str { ... }
    fn api_port(&self) -> u16 { ... }
}

ModuleRegistry - Module registry trait for dynamic module management

impl ModuleRegistry for MyModuleRegistry {
    fn get_module(&self, name: &str) -> Option<Arc<dyn Module>> { ... }
    fn list_modules(&self) -> Vec<String> { ... }
}

Service Traits

Service - Base service trait with lifecycle methods

use systemprompt_traits::Service;

#[async_trait]
impl Service for MyService {
    fn name(&self) -> &str { "my-service" }

    async fn start(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ... }
    async fn stop(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ... }
    async fn health_check(&self) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> { ... }
}

AsyncService - Async service trait for long-running background tasks

use systemprompt_traits::AsyncService;

#[async_trait]
impl AsyncService for MyAsyncService {
    async fn run(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        // Long-running task
    }
}

Module Traits

Module - Core module trait for systemprompt.io modules

#[async_trait]
impl Module for MyModule {
    fn name(&self) -> &str { "my-module" }
    fn version(&self) -> &str { "1.0.0" }
    fn display_name(&self) -> &str { "My Module" }
    async fn initialize(&self) -> Result<(), Box<dyn std::error::Error>> { ... }
}

ApiModule - Module trait with REST API support

#[async_trait]
impl ApiModule for MyApiModule {
    fn router(&self, ctx: Arc<dyn AppContext>) -> axum::Router { ... }
}

Usage Patterns

When to Use Traits vs Concrete Types

Use Traits When:

  • You need dependency injection for testing
  • You want to support multiple implementations
  • You're defining interfaces between modules
  • You need polymorphic behavior

Use Concrete Types When:

  • Performance is critical and trait objects add overhead
  • There's only one implementation
  • The API is module-internal
  • Type inference is important

Testing with Traits

Traits enable easy mocking:

#[cfg(test)]
mod tests {
    use super::*;

    struct MockRepository {
        pool: MockPool,
    }

    impl Repository for MockRepository {
        type Pool = MockPool;
        type Error = RepositoryError;

        fn pool(&self) -> &Self::Pool { &self.pool }
    }

    #[tokio::test]
    async fn test_with_mock() {
        let repo = MockRepository { pool: MockPool::new() };
        // Test using trait methods
    }
}

Error Handling

All repository errors automatically convert to ApiError:

use systemprompt_models::{ApiError, RepositoryError};

let result: Result<User, RepositoryError> = repo.get_user("id").await;
let api_result: Result<User, ApiError> = result.map_err(|e| e.into());

Architecture

This crate follows the Interface Segregation Principle from SOLID:

  • Traits are small and focused
  • Clients depend only on the methods they use
  • No fat interfaces or forced implementations

Dependencies

Minimal dependencies to avoid circular deps:

  • async-trait - Async trait support
  • anyhow - Error handling
  • axum - Router type for ApiModule
  • inventory - Module registration
  • thiserror - Error derive macros
  • sqlx - Database types for Repository
  • serde_json - Serialization errors

No dependencies on other systemprompt.io crates - this is intentional to prevent circular dependencies.

Tool Provider Traits

ToolProvider - Abstract tool discovery and execution

Enables modules to use tools without depending on specific implementations (e.g., MCP).

use systemprompt_traits::{ToolProvider, ToolContext, ToolDefinition};

#[async_trait]
impl ToolProvider for MyToolProvider {
    async fn list_tools(&self, agent_name: &str, context: &ToolContext)
        -> ToolProviderResult<Vec<ToolDefinition>> { ... }
    async fn call_tool(&self, request: &ToolCallRequest, service_id: &str, context: &ToolContext)
        -> ToolProviderResult<ToolCallResult> { ... }
    async fn refresh_connections(&self, agent_name: &str) -> ToolProviderResult<()> { ... }
    async fn health_check(&self) -> ToolProviderResult<HashMap<String, bool>> { ... }
}

Supporting types: ToolDefinition, ToolCallRequest, ToolCallResult, ToolContent, ToolContext, ToolProviderError

LLM Provider Traits

LlmProvider - Abstract LLM interactions

use systemprompt_traits::{LlmProvider, ChatRequest, ChatResponse};

#[async_trait]
impl LlmProvider for MyProvider {
    async fn chat(&self, request: &ChatRequest) -> LlmProviderResult<ChatResponse> { ... }
    async fn stream_chat(&self, request: &ChatRequest) -> LlmProviderResult<ChatStream> { ... }
    fn default_model(&self) -> &str { "model-name" }
    fn supports_model(&self, model: &str) -> bool { ... }
    fn supports_streaming(&self) -> bool { true }
    fn supports_tools(&self) -> bool { true }
}

ToolExecutor - Execute tools during conversations

use systemprompt_traits::{ToolExecutor, ToolExecutionContext};

#[async_trait]
impl ToolExecutor for MyExecutor {
    async fn execute(&self, tool_calls: Vec<ToolCallRequest>, tools: &[ToolDefinition],
        context: &ToolExecutionContext) -> (Vec<ToolCallRequest>, Vec<ToolCallResult>) { ... }
}

Supporting types: ChatMessage, ChatRole, ChatRequest, ChatResponse, SamplingParameters, TokenUsage, ToolExecutionContext

Usage

use async_trait::async_trait;
use systemprompt_traits::Service;

struct HealthPinger;

#[async_trait]
impl Service for HealthPinger {
    fn name(&self) -> &str { "health-pinger" }
    async fn start(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Ok(()) }
    async fn stop(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Ok(()) }
    async fn health_check(&self) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> { Ok(true) }
}

License

Business Source License 1.1 - See LICENSE for details.