Expand description
§Persistent Storage
§When to Use It
Use the storage interfaces when a plugin must persist data across restarts (for example: conversation history, behavioral memory, files, or vector embeddings).
§Available Storage Capabilities
The storage system exposes four capability traits in synapto_interface::storage:
RecordStore: Ordered record persistence with upsert, retrieval by key order (ascending or descending), single-item deletion, and sliding-window cutoff trimming (trim_records_before).KeyValueStore: Basic key-value operations (get,set,delete,get_all) within a collection.FileStore: Binary blob storage (save_file,get_file,delete_file) within a collection.VectorStore: Vector index preparation (setup_collection), vector batch insertion (insert_vectors), similarity search (search_vectors), and filter deletion (delete_vectors).
§Storage Architecture
§Connection Pooling (StorageRegistry)
Multiple plugins requesting the same storage provider share a single underlying connection pool via StorageRegistry. The registry caches instances by their TypeId.
§Namespace Isolation
Storage implementations must isolate data for each plugin using the plugin_namespace string provided during connection initialization.
§Storage Configuration Resolution
During plugin initialization (context.store::<S>()), the runtime invokes StorageConfigResolver. It parses the configuration block matching the crate and storage struct names into S::Config.
§Using Storage in a Plugin
Define the plugin with a generic type parameter S bounded by the required store traits and StorageConnection:
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use synapto_interface::plugin::{Plugin, PluginInitContext, PluginRegistry};
use synapto_interface::storage::{RecordStore, StorageConnection};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MemoryItem {
pub content: String,
}
pub struct MemoryPlugin<S: RecordStore + StorageConnection> {
store: Arc<S>,
}
#[async_trait]
impl<S: RecordStore + StorageConnection> Plugin for MemoryPlugin<S> {
async fn create(context: &PluginInitContext<'_>) -> Result<Self, String> {
let store = context.store::<S>().await?;
Ok(Self { store })
}
fn register<R: PluginRegistry + ?Sized>(self: Arc<Self>, _registry: &mut R) {}
}
impl<S: RecordStore + StorageConnection> MemoryPlugin<S> {
pub async fn record(&self, key: &str, content: String) -> Result<(), String> {
self.store
.upsert_record("memories", key, MemoryItem { content })
.await
}
}§Creating a Custom Storage Provider
To create a new storage provider, implement:
StorageProviderPoolon your shared connection or client instance.StorageConnectionon your per-plugin scoped handle.- One or more store traits (
RecordStore,KeyValueStore,FileStore,VectorStore).
§Local File-Backed vs Remote Storage
- Local Storage Providers: If the provider writes to local disk, parameterize the struct with
P: DataDirProviderto receive the data directory at compile time. Usetype Config = EmptyStorageConfig;. - Remote Storage Providers: If the provider connects to a network database, define a custom configuration struct deriving
Deserialize(e.g.MyDbConfig) and settype Config = MyDbConfig;.
§Step-by-Step Implementation Example
use async_trait::async_trait;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::marker::PhantomData;
use std::sync::Arc;
use synapto_interface::data_dir::DataDirProvider;
use synapto_interface::storage::{
EmptyStorageConfig, KeyValueStore, RecordStore, SortOrder,
StorageConnection, StorageProviderPool, StorageRegistry,
};
// 1. Shared connection pool / client instance
struct CustomDbClient {
// Database client / connection pool handle
}
impl StorageProviderPool for CustomDbClient {}
// 2. Per-plugin scoped storage handle
pub struct CustomStorage<P: DataDirProvider> {
client: Arc<CustomDbClient>,
namespace: String,
_marker: PhantomData<P>,
}
// 3. Connection lifecycle implementation
#[async_trait]
impl<P: DataDirProvider> StorageConnection for CustomStorage<P> {
type Config = EmptyStorageConfig;
async fn connect(
_config: Self::Config,
storage_registry: Arc<StorageRegistry>,
plugin_namespace: &str,
) -> Result<Self, String> {
let base_path = P::get_data_dir();
let client = storage_registry
.get_or_init::<CustomDbClient, _, _, String>(|| async move {
// Initialize the database client once
Ok(CustomDbClient {})
})
.await?;
Ok(Self {
client,
namespace: plugin_namespace.to_string(),
_marker: PhantomData,
})
}
}
// 4. Implement required storage capability traits
#[async_trait]
impl<P: DataDirProvider> KeyValueStore for CustomStorage<P> {
async fn set<T>(&self, collection: &str, key: &str, value: T) -> Result<(), String>
where
T: Serialize + Send + Sync + 'static,
{
// Save value scoped to self.namespace and collection
Ok(())
}
async fn get<T>(&self, collection: &str, key: &str) -> Result<Option<T>, String>
where
T: DeserializeOwned + Send + Sync + 'static,
{
// Retrieve value
Ok(None)
}
async fn delete(&self, collection: &str, key: &str) -> Result<(), String> {
// Delete key
Ok(())
}
async fn get_all<T>(&self, collection: &str) -> Result<Vec<T>, String>
where
T: DeserializeOwned + Send + Sync + 'static,
{
// List all items
Ok(Vec::new())
}
}
#[async_trait]
impl<P: DataDirProvider> RecordStore for CustomStorage<P> {
async fn upsert_record<T>(&self, collection: &str, key: &str, value: T) -> Result<(), String>
where
T: Serialize + Send + Sync + 'static,
{
Ok(())
}
async fn get_ordered_records<T>(
&self,
collection: &str,
limit: Option<usize>,
order: SortOrder,
) -> Result<Vec<(String, T)>, String>
where
T: DeserializeOwned + Send + Sync + 'static,
{
Ok(Vec::new())
}
async fn delete_record(&self, collection: &str, key: &str) -> Result<(), String> {
Ok(())
}
async fn trim_records_before(&self, collection: &str, cutoff_key: &str) -> Result<(), String> {
Ok(())
}
}Structs§
- Empty
Storage Config - Storage
Handle - Opaque handle encapsulating storage connection pooling and configuration resolution.
- Storage
Registry
Enums§
Traits§
- File
Store - Trait for storing, retrieving, and deleting raw binary files.
- KeyValue
Store - For storing and retrieving items by a unique string ID.
- Record
Store - Storage
Config Resolver - Storage
Connection - The entry point for a generic storage adapter. It guarantees that plugins can seamlessly initialize their underlying connection using the shared StorageHandle without requiring manual setup in main.rs.
- Storage
Provider Pool - A marker trait for safe DB connection pooling
- Vector
Store