systemprompt-traits
Minimal shared traits and contracts for systemprompt.io.

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>> {
}
}
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() };
}
}
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.