Skip to main content

Overview

OpenFang’s memory substrate is a unified SQLite-backed storage system providing six layers of memory:
  1. Structured KV Store — Per-agent JSON key-value storage
  2. Semantic Search — Vector embeddings with cosine similarity
  3. Knowledge Graph — Entity-relation storage with graph traversal
  4. Session Manager — Conversation history with token tracking
  5. Task Board — Shared task queue for multi-agent collaboration
  6. Usage Store — Token counts, cost estimates, model usage tracking
All backed by a single SQLite database (~/.openfang/data/openfang.db) with schema version 5.

SQLite Architecture

Thread-Safe Async Bridge

SQLite is synchronous, but OpenFang is async. The memory substrate bridges this with:
Arc<Mutex<Connection>>
All operations use tokio::task::spawn_blocking to run SQLite queries on the blocking thread pool:
let conn = self.conn.clone();
let result = tokio::task::spawn_blocking(move || {
    let conn = conn.lock().unwrap();
    conn.execute("INSERT INTO ...", params![])?;
    Ok(())
}).await??;
This ensures:
  • Thread safety — no concurrent writes corrupt the database
  • Async compatibility — no blocking the Tokio runtime
  • Performance — WAL mode + busy_timeout=5000ms
Code reference: crates/openfang-memory/src/substrate.rs

Schema Versions

The database migrates through five schema versions:
VersionWhat Changed
V1Core tables: structured_memory, semantic_memory, knowledge_entities, knowledge_relations, sessions, agents
V2Added collaboration tables: task_board, task_claims
V3Added embeddings table for vector search
V4Added usage_events table for cost tracking
V5Added canonical_sessions table for cross-channel memory
Migrations run automatically on first boot. Idempotent (safe to run multiple times). Code reference: crates/openfang-memory/src/migration.rs

Layer 1: Structured KV Store

What It Does

Per-agent key-value storage where:
  • Keys are strings
  • Values are JSON (serde_json::Value)
Used by the memory_store and memory_recall tools.

Schema

CREATE TABLE structured_memory (
    agent_id TEXT NOT NULL,
    key TEXT NOT NULL,
    value TEXT NOT NULL,  -- JSON-serialized
    created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL,
    PRIMARY KEY (agent_id, key)
);

API

// Store a value
memory.structured_set(
    agent_id,
    "preferences",
    serde_json::json!({"theme": "dark", "lang": "en"})
)?;

// Retrieve a value
let value: Option<serde_json::Value> = memory.structured_get(agent_id, "preferences")?;

// List all keys for an agent
let pairs: Vec<(String, serde_json::Value)> = memory.list_kv(agent_id)?;

// Delete a key
memory.structured_delete(agent_id, "preferences")?;
Code reference: crates/openfang-memory/src/structured.rs

Shared Memory Namespace

A special shared namespace enables cross-agent data sharing:
const SHARED_AGENT_ID: &str = "00000000-0000-0000-0000-000000000001";

// Any agent can write to shared memory
memory.structured_set(
    SHARED_AGENT_ID,
    "project_config",
    serde_json::json!({"name": "OpenFang", "version": "0.3.25"})
)?;

// Any agent can read from shared memory
let config = memory.structured_get(SHARED_AGENT_ID, "project_config")?;
Shared memory is not capability-gated by default. Agents with MemoryRead("*") can read it. Agents with MemoryWrite("shared.*") can write to it.

What It Does

Vector embeddings for similarity-based memory retrieval. Documents are embedded using the configured embedding driver (OpenAI/Voyage/Cohere) and stored with their vectors. Queries are embedded at search time and matched by cosine similarity.

Schema

CREATE TABLE semantic_memory (
    id TEXT PRIMARY KEY,
    agent_id TEXT NOT NULL,
    content TEXT NOT NULL,
    embedding BLOB,  -- Serialized vector (msgpack)
    metadata TEXT,   -- JSON
    created_at INTEGER NOT NULL,
    access_count INTEGER DEFAULT 0,
    last_accessed INTEGER
);

CREATE INDEX idx_semantic_agent ON semantic_memory(agent_id);
CREATE INDEX idx_semantic_created ON semantic_memory(created_at);

API

// Store a document with embedding
let fragment = MemoryFragment {
    content: "OpenFang is an agent operating system built in Rust.".to_string(),
    source: MemorySource::UserInput,
    metadata: HashMap::new(),
};

memory.semantic_store(agent_id, fragment, Some(embedding_vector)).await?;

// Search by similarity
let query_embedding = embedding_driver.embed("What is OpenFang?").await?;
let results: Vec<MemoryFragment> = memory.semantic_search(
    agent_id,
    &query_embedding,
    5  // top 5 results
).await?;
Code reference: crates/openfang-memory/src/semantic.rs

Memory Decay

Memories have a decay rate — older, less-accessed memories rank lower in search results:
score = cosine_similarity * (1.0 - decay_factor)

decay_factor = min(
    (days_since_created * decay_rate) + (1.0 / (access_count + 1)),
    0.9  // max decay
)
Configured via KernelConfig.memory_decay_rate (default 0.01).

Layer 3: Knowledge Graph

What It Does

Entity-relation storage for structured knowledge. Agents can store entities (with types and properties) and relations between them. Supports graph traversal queries.

Schema

CREATE TABLE knowledge_entities (
    id TEXT PRIMARY KEY,
    agent_id TEXT NOT NULL,
    entity_type TEXT NOT NULL,
    name TEXT NOT NULL,
    properties TEXT,  -- JSON
    created_at INTEGER NOT NULL
);

CREATE TABLE knowledge_relations (
    id TEXT PRIMARY KEY,
    agent_id TEXT NOT NULL,
    from_entity TEXT NOT NULL,
    relation_type TEXT NOT NULL,
    to_entity TEXT NOT NULL,
    properties TEXT,  -- JSON
    created_at INTEGER NOT NULL,
    FOREIGN KEY(from_entity) REFERENCES knowledge_entities(id),
    FOREIGN KEY(to_entity) REFERENCES knowledge_entities(id)
);

API

// Create entities
let person = Entity {
    id: "person:john".to_string(),
    entity_type: "Person".to_string(),
    name: "John Doe".to_string(),
    properties: serde_json::json!({"age": 35, "occupation": "Engineer"}),
};

let company = Entity {
    id: "company:acme".to_string(),
    entity_type: "Company".to_string(),
    name: "Acme Corp".to_string(),
    properties: serde_json::json!({"industry": "Tech"}),
};

memory.knowledge_add_entity(agent_id, person).await?;
memory.knowledge_add_entity(agent_id, company).await?;

// Create relation
let relation = Relation {
    id: "rel:1".to_string(),
    from_entity: "person:john".to_string(),
    relation_type: "WORKS_AT".to_string(),
    to_entity: "company:acme".to_string(),
    properties: serde_json::json!({"since": "2020-01-01"}),
};

memory.knowledge_add_relation(agent_id, relation).await?;

// Query relations
let pattern = GraphPattern {
    from_entity: Some("person:john".to_string()),
    relation_type: Some("WORKS_AT".to_string()),
    to_entity: None,
};

let matches: Vec<GraphMatch> = memory.knowledge_query(agent_id, pattern).await?;
// Returns: [(person:john) -[WORKS_AT]-> (company:acme)]
Code reference: crates/openfang-memory/src/knowledge.rs

Layer 4: Session Manager

What It Does

Conversation history storage. Each agent has a session containing its message history:
  • User messages
  • Assistant messages
  • Tool use blocks
  • Tool result blocks
  • Image blocks (inline base64 or URLs)
Sessions track token counts to prevent context window overflow.

Schema

CREATE TABLE sessions (
    session_id TEXT PRIMARY KEY,
    agent_id TEXT NOT NULL,
    label TEXT,  -- Optional user-facing label
    messages TEXT NOT NULL,  -- JSON array of Message
    token_count INTEGER DEFAULT 0,
    created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL
);

CREATE INDEX idx_sessions_agent ON sessions(agent_id);
CREATE INDEX idx_sessions_label ON sessions(agent_id, label);

Message Format

pub struct Message {
    pub role: MessageRole,  // User | Assistant
    pub content: Vec<ContentBlock>,
}

pub enum ContentBlock {
    Text { text: String },
    ToolUse { id: String, name: String, input: serde_json::Value },
    ToolResult { tool_use_id: String, content: String, is_error: bool },
    Image { source: ImageSource, media_type: String },
}

API

// Create a new session
let session = memory.create_session(agent_id).await?;

// Add messages
session.messages.push(Message {
    role: MessageRole::User,
    content: vec![ContentBlock::Text { text: "Hello!".to_string() }],
});

session.messages.push(Message {
    role: MessageRole::Assistant,
    content: vec![ContentBlock::Text { text: "Hi there!".to_string() }],
});

// Save session
memory.save_session(&session).await?;

// Retrieve session
let session = memory.get_session(session_id).await?;

// List all sessions for an agent
let sessions: Vec<serde_json::Value> = memory.list_agent_sessions(agent_id).await?;

// Delete a session
memory.delete_session(session_id).await?;
Code reference: crates/openfang-memory/src/session.rs

Session Repair

Before each agent loop, session repair validates and fixes message history:
  1. Drop orphaned ToolResult messages (no matching ToolUse)
  2. Remove empty messages
  3. Merge consecutive same-role messages
  4. Validate content blocks
  5. Fix missing tool_use_id references
  6. Ensure alternating user/assistant pattern
  7. Truncate if exceeds context window
Code reference: crates/openfang-runtime/src/session_repair.rs

Session Compaction

When a session exceeds the configured threshold (default 80% of context window), block-aware compaction triggers:
  1. Keep the most recent N messages (default 20)
  2. Summarize the older messages into a single “context summary” block
  3. Insert summary at the start of the session
  4. Delete the summarized messages
Handles all content block types (Text, ToolUse, ToolResult, Image). Code reference: crates/openfang-runtime/src/compactor.rs

Layer 5: Task Board

What It Does

A shared task queue for multi-agent collaboration. Agents can:
  • task_post: Create a task with title, description, optional assignee
  • task_claim: Claim the next available task
  • task_complete: Mark a task as done with a result
  • task_list: List tasks filtered by status (pending, claimed, completed)

Schema

CREATE TABLE task_board (
    task_id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    description TEXT,
    status TEXT NOT NULL,  -- pending | claimed | completed
    created_by TEXT,
    assigned_to TEXT,
    result TEXT,
    created_at INTEGER NOT NULL,
    claimed_at INTEGER,
    completed_at INTEGER
);

CREATE INDEX idx_task_status ON task_board(status);
CREATE INDEX idx_task_assigned ON task_board(assigned_to);

API

// Post a task
let task_id = memory.task_post(
    "Analyze Q4 sales data",
    "Load CSV, compute trends, generate report",
    Some("analyst-agent")  // Optional assignee
).await?;

// Claim next available task
let task = memory.task_claim(agent_id).await?;

// Complete a task
memory.task_complete(
    task_id,
    "Report generated: Sales up 15% YoY"
).await?;

// List tasks
let pending_tasks = memory.task_list(MemoryFilter::Pending).await?;
let my_tasks = memory.task_list(MemoryFilter::AssignedTo(agent_id)).await?;
Code reference: crates/openfang-memory/src/structured.rs:task_*

Layer 6: Usage and Canonical Sessions

Usage Store

Persists token counts and cost estimates for every LLM call:
CREATE TABLE usage_events (
    id TEXT PRIMARY KEY,
    agent_id TEXT NOT NULL,
    model TEXT NOT NULL,
    prompt_tokens INTEGER NOT NULL,
    completion_tokens INTEGER NOT NULL,
    cost_usd REAL,
    timestamp INTEGER NOT NULL
);

CREATE INDEX idx_usage_agent ON usage_events(agent_id);
CREATE INDEX idx_usage_timestamp ON usage_events(timestamp);

API

// Record usage
memory.usage().record_usage(
    agent_id,
    "claude-sonnet-4-20250514",
    1234,  // prompt tokens
    567,   // completion tokens
    Some(0.0045)  // cost in USD
).await?;

// Query usage
let total_cost: f64 = memory.usage().total_cost(agent_id).await?;
let total_tokens: u64 = memory.usage().total_tokens(agent_id).await?;
let by_model: HashMap<String, u64> = memory.usage().by_model(agent_id).await?;
Code reference: crates/openfang-memory/src/usage.rs

Canonical Sessions

Cross-channel memory — tracks a user’s conversation context across multiple channels:
CREATE TABLE canonical_sessions (
    user_id TEXT NOT NULL,
    agent_id TEXT NOT NULL,
    summary TEXT,  -- Compacted context summary
    last_channel TEXT,
    message_count INTEGER DEFAULT 0,
    updated_at INTEGER NOT NULL,
    PRIMARY KEY (user_id, agent_id)
);
When a user messages an agent:
  1. Load canonical session for (user_id, agent_id)
  2. Inject summary into system prompt (if exists)
  3. Run agent loop
  4. Update canonical session with new message count
  5. Compact canonical session if threshold exceeded
Result: User can start a conversation on Telegram, continue it on Discord, and the agent remembers the full context. Code reference: crates/openfang-memory/src/consolidation.rs

Memory Operations in Practice

Example: Multi-Agent Research Pipeline

// Agent 1: Researcher collects data
let research_data = serde_json::json!({
    "topic": "OpenFang architecture",
    "sources": [
        "https://github.com/RightNow-AI/openfang",
        "https://openfang.sh/docs"
    ],
    "findings": "14-crate modular design..."
});

memory.structured_set(researcher_id, "research_output", research_data)?;

// Agent 2: Summarizer reads the data
let data = memory.structured_get(researcher_id, "research_output")?;
let summary = summarize(&data);

memory.structured_set(summarizer_id, "summary", summary)?;

// Agent 3: Publisher reads the summary
let summary = memory.structured_get(summarizer_id, "summary")?;
publish_to_blog(&summary)?;

Example: Knowledge Graph Construction

// Extract entities from text
let entities = extract_entities("John Doe works at Acme Corp");

// Add to knowledge graph
for entity in entities {
    memory.knowledge_add_entity(agent_id, entity).await?;
}

// Add relations
memory.knowledge_add_relation(agent_id, Relation {
    from_entity: "person:john",
    relation_type: "WORKS_AT",
    to_entity: "company:acme",
    properties: serde_json::json!({"role": "Engineer"}),
}).await?;

// Query: "Who works at Acme?"
let pattern = GraphPattern {
    from_entity: None,
    relation_type: Some("WORKS_AT"),
    to_entity: Some("company:acme"),
};

let results = memory.knowledge_query(agent_id, pattern).await?;
// Returns: [person:john]

Example: Task Collaboration

// Agent A posts a task
let task_id = memory.task_post(
    "Analyze sales data",
    "Load Q4 CSV and compute trends",
    None  // Any agent can claim
).await?;

// Agent B (running on a different machine) claims it
let task = memory.task_claim(agent_b_id).await?;

// Agent B completes the task
let result = analyze_sales(task.description)?;
memory.task_complete(task_id, &result).await?;

// Agent A checks completion
let completed_tasks = memory.task_list(MemoryFilter::Completed).await?;

Performance Characteristics

OperationLatencyNotes
Structured get~0.5msSingle SELECT by primary key
Structured set~1msINSERT OR REPLACE
Semantic search~10-50msDepends on embedding count
Knowledge query~2-5msGraph traversal with JOINs
Session load~1-2msSingle SELECT + JSON deserialize
Session save~2-3msUPDATE with JSON serialize
Task claim~1-2msSELECT + UPDATE with row lock
Usage record~0.5msSingle INSERT
Measured on M1 Pro (2021) with 100k memory entries. SQLite is fast.

Configuration

~/.openfang/config.toml
[memory]
path = "~/.openfang/data/openfang.db"
decay_rate = 0.01  # Memory decay factor (0.0-1.0)

# Session compaction
compaction_threshold = 0.8  # Compact when 80% of context window used
compaction_keep_recent = 20  # Keep most recent 20 messages

# Canonical session settings
canonical_compaction_threshold = 50  # Compact after 50 messages
canonical_summary_max_tokens = 1000  # Max tokens for summary

Next Steps

Use Memory Tools

Learn to use memory_store and memory_recall

Build a Knowledge Graph

Create entity-relation graphs

Agent Collaboration

Use the task board for multi-agent workflows

Architecture

Understand the full system architecture

Build docs developers (and LLMs) love