Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Key-Value Store

The key-value store provides persistent, namespaced storage for structured data. Unlike memory (which stores conversation messages by session), the store holds arbitrary key-value items organized into hierarchical namespaces. It supports CRUD operations, namespace listing, and optional semantic search when an embeddings model is configured.

The Store Trait

The Store trait is defined in synaptic-core and implemented in synaptic-store:

#[async_trait]
pub trait Store: Send + Sync {
    async fn put(&self, namespace: &[&str], key: &str, value: Item) -> Result<(), SynapticError>;
    async fn get(&self, namespace: &[&str], key: &str) -> Result<Option<Item>, SynapticError>;
    async fn delete(&self, namespace: &[&str], key: &str) -> Result<(), SynapticError>;
    async fn search(&self, namespace: &[&str], query: &SearchQuery) -> Result<Vec<Item>, SynapticError>;
    async fn list_namespaces(&self, prefix: &[&str]) -> Result<Vec<Vec<String>>, SynapticError>;
}

Namespace Hierarchy

Namespaces are arrays of strings, forming a path-like hierarchy:

// Store user preferences
store.put(&["users", "alice", "preferences"], "theme", item).await?;

// Store project data
store.put(&["projects", "my-app", "config"], "settings", item).await?;

// List all user namespaces
let namespaces = store.list_namespaces(&["users"]).await?;
// [["users", "alice", "preferences"], ["users", "bob", "preferences"]]

Items in different namespaces are completely isolated. A get or search in one namespace never returns items from another.

Item

The Item struct holds the stored value:

pub struct Item {
    pub key: String,
    pub value: Value,           // serde_json::Value
    pub namespace: Vec<String>,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
    pub score: Option<f32>,     // populated by semantic search
}

The score field is None for regular CRUD operations and is populated only when items are returned from a semantic search query.

InMemoryStore

The built-in implementation uses Arc<RwLock<HashMap>> for thread-safe concurrent access:

use synaptic::store::InMemoryStore;

let store = InMemoryStore::new();

Suitable for development, testing, and applications that don't need persistence across restarts. For production use, implement the Store trait with a database backend.

When an embeddings model is configured, the store supports semantic search -- finding items by meaning rather than exact key match:

use synaptic::store::InMemoryStore;

let store = InMemoryStore::with_embeddings(embeddings_model);

// Items are automatically embedded when stored
store.put(&["docs"], "rust-intro", item).await?;

// Search by semantic similarity
let results = store.search(&["docs"], &SearchQuery {
    query: Some("programming language".into()),
    limit: 5,
    ..Default::default()
}).await?;

Each returned item has a score field (0.0 to 1.0) indicating semantic similarity to the query.

Store vs. Memory

AspectStoreMemory (MemoryStore)
PurposeGeneral key-value storageConversation message history
Keyed byNamespace + keySession ID
Value typeArbitrary JSON (Value)Message
OperationsCRUD + search + listAppend + load + clear
SearchSemantic (with embeddings)Not applicable
Use caseAgent knowledge, user profiles, configurationChat history, context management

Use memory for conversation state. Use the store for everything else -- agent knowledge bases, user preferences, cached computations, cross-session data.

Store in the Graph

The store is accessible within graph nodes through the ToolRuntime:

// Inside a RuntimeAwareTool
async fn call_with_runtime(&self, args: Value, runtime: &ToolRuntime) -> Result<Value, SynapticError> {
    if let Some(store) = &runtime.store {
        let item = store.get(&["memory"], "context").await?;
        // Use stored data in tool execution
    }
    Ok(json!({"status": "ok"}))
}

This enables tools to read and write persistent data during graph execution without passing the store through function arguments.

See Also