Garrison Memory

The Garrison is Paladin AI's conversation memory system. When attached to a Paladin it stores and retrieves conversation history, giving the agent context across multiple reasoning loops and between invocations.

Garrison is defined in crates/paladin-ports/src/output/garrison_port.rs (the GarrisonPort trait) with adapter implementations in crates/paladin-memory/src/garrison/.


Table of Contents

  1. Concepts
  2. Quick Start
  3. Garrison Adapters
  4. GarrisonPort Trait
  5. GarrisonConfig
  6. Conversation Roles
  7. Attaching to a Paladin
  8. Long-Term Memory with Embeddings
  9. config.yml Reference
  10. Error Handling
  11. Best Practices

Concepts

TermDefinition
GarrisonThe memory subsystem; stores conversation entries
GarrisonEntryA single message with role, content, timestamp, and optional token count
ConversationRoleUser, Assistant, System, or Tool
GarrisonConfigWindow size, token budget, and eviction strategy
GarrisonStatsEntry count, total token count, optional storage size

Quick Start

use paladin_memory::garrison::InMemoryGarrison;
use paladin_core::platform::container::garrison::{GarrisonConfig, GarrisonEntry, ConversationRole};
use paladin_ports::output::garrison_port::GarrisonPort;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let garrison = Arc::new(InMemoryGarrison::new(GarrisonConfig::default()));

    // Store a user message
    garrison.remember(GarrisonEntry::new(
        ConversationRole::User,
        "What is Rust's ownership model?".to_string(),
    )).await?;

    // Store the assistant reply
    garrison.remember(GarrisonEntry::new(
        ConversationRole::Assistant,
        "Rust's ownership model ensures memory safety without a GC...".to_string(),
    )).await?;

    // Recall last 10 entries
    let history = garrison.recall_recent(10).await?;
    for entry in &history {
        println!("{:?}: {}", entry.role, entry.content);
    }

    // Search by keyword
    let results = garrison.search("ownership", 5).await?;
    println!("Found {} messages about ownership", results.len());

    Ok(())
}

Garrison Adapters

Both adapters are in crates/paladin-memory/src/garrison/.

InMemoryGarrison

PropertyValue
PersistenceNone (process-scoped)
PerformanceO(1) write, O(N) search
Use caseDevelopment, testing, short-lived sessions
use paladin_memory::garrison::InMemoryGarrison;
use paladin_core::platform::container::garrison::GarrisonConfig;

let garrison = InMemoryGarrison::new(GarrisonConfig::default());

SqliteGarrison

PropertyValue
PersistenceSQLite file (survives restarts)
PerformanceO(log N) indexed read, FTS5 full-text search
Use caseSingle-agent production deployments
use paladin_memory::garrison::SqliteGarrison;
use paladin_core::platform::container::garrison::GarrisonConfig;

let garrison = SqliteGarrison::connect(
    "./garrison.db",
    GarrisonConfig::default(),
    "my-paladin-id",
).await?;

SqliteGarrison::connect() creates the file and runs migrations automatically.


GarrisonPort Trait

Full interface defined in crates/paladin-ports/src/output/garrison_port.rs:

#[async_trait]
pub trait GarrisonPort: Send + Sync {
    /// Store a new conversation entry
    async fn remember(&self, entry: GarrisonEntry) -> Result<(), GarrisonError>;

    /// Retrieve the N most recent entries (newest last)
    async fn recall_recent(&self, limit: usize) -> Result<Vec<GarrisonEntry>, GarrisonError>;

    /// Full-text search across stored entries
    async fn search(&self, query: &str, limit: usize) -> Result<Vec<GarrisonEntry>, GarrisonError>;

    /// Clear all entries from this garrison
    async fn forget_all(&self) -> Result<(), GarrisonError>;

    /// Get storage statistics
    async fn stats(&self) -> Result<GarrisonStats, GarrisonError>;
}

GarrisonStats

pub struct GarrisonStats {
    pub entry_count: usize,   // Total stored entries
    pub total_tokens: u32,    // Cumulative token count
    pub size_bytes: Option<u64>, // Storage size (adapters may not support this)
}

GarrisonConfig

use paladin_core::platform::container::garrison::GarrisonConfig;

// max_entries: window size; max_tokens: token budget per context window
let config = GarrisonConfig::new(200, Some(8000));
FieldDefaultDescription
max_entries100Maximum entries to retain in the window
max_tokensNoneOptional token budget; triggers eviction when exceeded
eviction_strategyOldestOldest removes oldest entries when window is full

Conversation Roles

use paladin_core::platform::container::garrison::ConversationRole;

ConversationRole::User       // Human turn
ConversationRole::Assistant  // Paladin / LLM turn
ConversationRole::System     // System instruction
ConversationRole::Tool       // Tool call result

Attaching to a Paladin

use paladin::application::services::paladin::paladin_builder::PaladinBuilder;
use paladin_memory::garrison::SqliteGarrison;
use paladin_core::platform::container::garrison::GarrisonConfig;
use paladin_ports::output::garrison_port::GarrisonPort;
use std::sync::Arc;

let garrison: Arc<dyn GarrisonPort> = Arc::new(
    SqliteGarrison::connect("./memory.db", GarrisonConfig::default(), "agent-1").await?
);

let paladin = PaladinBuilder::new(llm_port)
    .system_prompt("You are a persistent memory assistant.")
    .with_garrison(garrison)
    .build()
    .await?;

Once attached, the Paladin automatically:

  1. Retrieves recent history before each LLM call.
  2. Appends the user turn and assistant response after each loop.

Long-Term Memory with Embeddings

The LongTermGarrisonPort trait (also in garrison_port.rs) extends GarrisonPort with semantic similarity search using vector embeddings:

pub trait LongTermGarrisonPort: GarrisonPort {
    async fn remember_with_embedding(
        &self,
        entry: GarrisonEntry,
        embedding: Vec<f32>,
    ) -> Result<(), GarrisonError>;

    async fn search_similar(
        &self,
        query_embedding: Vec<f32>,
        limit: usize,
    ) -> Result<Vec<GarrisonEntry>, GarrisonError>;
}

For full vector-based semantic memory, consider Sanctum — see Sanctum Vector Memory.


config.yml Reference

garrison:
  type: sqlite        # "in_memory" or "sqlite"
  path: ./garrison.db # SQLite only
  max_entries: 100
  max_tokens: 8000    # Optional token budget
  eviction_strategy: oldest  # "oldest" (default)

Error Handling

GarrisonError variants:

VariantCauseRecovery
StorageError(String)Database / IO failureCheck path, permissions, disk space
SerializationError(String)Corrupt entry dataClear and rebuild garrison
TokenizationError(String)Token counting failureCheck tokenizer config
NotFoundEntry missingExpected after forget_all()

Best Practices

  • Always use SqliteGarrison in productionInMemoryGarrison loses all history when the process restarts.
  • Set max_tokens to stay within the LLM's context window; large histories degrade performance.
  • Use one Garrison per Paladin — shared garrisons across multiple agents mix conversation contexts and confuse the LLM.
  • Call forget_all() between sessions if context carry-over is undesirable (e.g., fresh chat sessions).
  • Use search() to retrieve relevant past entries rather than dumping the full history into the prompt.