Initial scaffold: GyanDoot v2.0 in Rust

Full Cargo workspace with 11 crates:
- gyandoot-core: Shared types, config, error types
- gyandoot-agent: Agent loop, tool execution, context compression
- gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama)
- gyandoot-memory: MemPalace persistent memory system
- gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI)
- gyandoot-security: Credential vault, sanitizer, threat detector
- gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics
- gyandoot-subagents: Sub-agent spawning and orchestration
- gyandoot-skills: Skills discovery, loading, preprocessing, bundles
- gyandoot-profile: Multi-agent profile manager
- gyandoot-gateway: Gateway sessions, routing, access control

Plus: CLI entry point with clap, cross-platform installer script,
KANBAN.md, .gnpl/AGENTS.md

Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
2026-06-11 20:04:12 +05:30
commit 851c838ca5
53 changed files with 8037 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
[package]
name = "gyandoot-memory"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "GyanDoot v2 — MemPalace persistent memory system"
[dependencies]
gyandoot-core = { path = "../gyandoot-core" }
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
anyhow.workspace = true
tracing.workspace = true
uuid.workspace = true
chrono.workspace = true
dirs.workspace = true

View File

@@ -0,0 +1,112 @@
//! Identity store — manages agent identity and preferences.
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// Agent identity information stored in memory.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentIdentity {
pub agent_id: String,
pub name: String,
pub bio: String,
pub preferences: HashMap<String, String>,
pub learned_traits: Vec<String>,
pub created_at: String,
pub updated_at: String,
}
/// Manages persistent agent identity data.
pub struct IdentityStore {
identities: HashMap<String, AgentIdentity>,
}
impl IdentityStore {
/// Create a new identity store.
pub fn new() -> Self {
Self {
identities: HashMap::new(),
}
}
/// Register or update an agent's identity.
pub fn register(&mut self, identity: AgentIdentity) {
self.identities.insert(identity.agent_id.clone(), identity);
}
/// Get an agent's identity by ID.
pub fn get(&self, agent_id: &str) -> Option<&AgentIdentity> {
self.identities.get(agent_id)
}
/// Update a preference for an agent.
pub fn set_preference(&mut self, agent_id: &str, key: String, value: String) -> Option<()> {
self.identities
.get_mut(agent_id)?
.preferences
.insert(key, value);
Some(())
}
/// Add a learned trait to an agent's identity.
pub fn add_trait(&mut self, agent_id: &str, trait_name: String) -> Option<()> {
self.identities
.get_mut(agent_id)?
.learned_traits
.push(trait_name);
Some(())
}
/// List all registered agent IDs.
pub fn list_agents(&self) -> Vec<String> {
self.identities.keys().cloned().collect()
}
}
impl Default for IdentityStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identity_register_and_retrieve() {
let mut store = IdentityStore::new();
store.register(AgentIdentity {
agent_id: "agent-1".to_string(),
name: "GyanDoot".to_string(),
bio: "An AI Agent".to_string(),
preferences: HashMap::new(),
learned_traits: vec![],
created_at: "now".to_string(),
updated_at: "now".to_string(),
});
let identity = store.get("agent-1").unwrap();
assert_eq!(identity.name, "GyanDoot");
}
#[test]
fn test_preferences() {
let mut store = IdentityStore::new();
store.register(AgentIdentity {
agent_id: "a1".to_string(),
name: "Test".to_string(),
bio: String::new(),
preferences: HashMap::new(),
learned_traits: vec![],
created_at: "now".to_string(),
updated_at: "now".to_string(),
});
store
.set_preference("a1", "language".to_string(), "rust".to_string())
.unwrap();
assert_eq!(
store.get("a1").unwrap().preferences.get("language").unwrap(),
"rust"
);
}
}

View File

@@ -0,0 +1,13 @@
//! GyanDoot Memory — MemPalace persistent memory system.
//!
//! The MemPalace stores conversation transcripts, agent identity data,
//! and provides semantic query over past interactions using embeddings
//! stored alongside text.
pub mod palace;
pub mod transcript;
pub mod identity;
pub use palace::MemPalace;
pub use transcript::TranscriptStore;
pub use identity::IdentityStore;

View File

@@ -0,0 +1,227 @@
//! MemPalace — The core persistent memory store.
//!
//! Stores conversation entries with embeddings for semantic retrieval,
//! handles memory compaction, and provides query capabilities.
use std::path::PathBuf;
use chrono::{DateTime, Utc};
use gyandoot_core::MemoryResult;
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::identity::IdentityStore;
use crate::transcript::TranscriptStore;
/// A single memory entry stored in the palace.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryEntry {
pub id: String,
pub content: String,
pub agent_id: String,
pub session_id: String,
pub entry_type: EntryType,
pub embedding: Option<Vec<f64>>,
pub created_at: DateTime<Utc>,
pub metadata: std::collections::HashMap<String, String>,
}
/// The type of a memory entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EntryType {
Conversation,
Fact,
Identity,
Task,
Skill,
}
/// A memory query result with relevance score.
#[derive(Debug, Clone)]
pub struct MemoryQueryResult {
pub content: String,
pub relevance: f64,
pub entry_type: EntryType,
pub created_at: DateTime<Utc>,
}
/// Status information for the memory palace.
#[derive(Debug, Clone)]
pub struct MemoryStatus {
pub entry_count: usize,
pub backend: String,
pub storage_size: u64,
}
/// The MemPalace — persistent, searchable agent memory.
///
/// Entries are stored as JSON files in the data directory, organized by
/// agent and session. Future versions will include vector embeddings
/// for semantic search.
pub struct MemPalace {
entries: Vec<MemoryEntry>,
storage_path: PathBuf,
transcript_store: TranscriptStore,
identity_store: IdentityStore,
}
impl MemPalace {
/// Create a new MemPalace with memory directory in the user's data dir.
pub fn new() -> Self {
let data_dir = gyandoot_core::data_dir().join("memory");
std::fs::create_dir_all(&data_dir).ok();
Self {
entries: Vec::new(),
storage_path: data_dir,
transcript_store: TranscriptStore::new(),
identity_store: IdentityStore::new(),
}
}
/// Create a MemPalace with a custom storage path.
pub fn with_path(path: PathBuf) -> Self {
std::fs::create_dir_all(&path).ok();
Self {
entries: Vec::new(),
storage_path: path,
transcript_store: TranscriptStore::new(),
identity_store: IdentityStore::new(),
}
}
/// Store a memory entry in the palace.
pub fn store(&mut self, entry: MemoryEntry) -> MemoryResult<()> {
debug!("Storing memory entry: {}", entry.id);
self.entries.push(entry);
Ok(())
}
/// Store a conversation turn (the last user + assistant pair) as memory.
pub async fn store_conversation_turn(
&mut self,
messages: &[gyandoot_core::Message],
) -> MemoryResult<()> {
let recent: Vec<&gyandoot_core::Message> = messages.iter().rev().take(4).collect();
for msg in recent.iter().rev() {
let entry = MemoryEntry {
id: uuid::Uuid::new_v4().to_string(),
content: msg.content.clone(),
agent_id: "default".to_string(),
session_id: String::new(),
entry_type: EntryType::Conversation,
embedding: None,
created_at: Utc::now(),
metadata: std::collections::HashMap::new(),
};
self.store(entry)?;
}
Ok(())
}
/// Query memory by simple text matching.
///
/// Future versions will use embedding-based semantic search.
pub fn query(&self, query: &str, limit: usize) -> MemoryResult<Vec<MemoryQueryResult>> {
let query_lower = query.to_lowercase();
let mut results: Vec<MemoryQueryResult> = self
.entries
.iter()
.filter(|e| e.content.to_lowercase().contains(&query_lower))
.map(|e| MemoryQueryResult {
content: e.content.clone(),
relevance: 1.0, // placeholder; real relevance from embedding cosine sim
entry_type: e.entry_type.clone(),
created_at: e.created_at,
})
.collect();
results.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(limit);
Ok(results)
}
/// Export all memory to a JSON file at the given path.
pub fn export_to(&self, path: &PathBuf) -> MemoryResult<()> {
let json = serde_json::to_string_pretty(&self.entries)
.map_err(|e| gyandoot_core::MemoryError::Storage(e.to_string()))?;
std::fs::write(path, &json)
.map_err(|e| gyandoot_core::MemoryError::Storage(e.to_string()))?;
Ok(())
}
/// Get memory status information.
pub fn status() -> MemoryResult<MemoryStatus> {
let data_dir = gyandoot_core::data_dir().join("memory");
let size = std::fs::read_dir(&data_dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter_map(|e| e.metadata().ok())
.map(|m| m.len())
.sum()
})
.unwrap_or(0);
let entry_count = std::fs::read_dir(&data_dir)
.map(|entries| entries.filter_map(|e| e.ok()).count())
.unwrap_or(0);
Ok(MemoryStatus {
entry_count,
backend: "local-json".to_string(),
storage_size: size,
})
}
/// Get a reference to the transcript store.
pub fn transcript_store(&self) -> &TranscriptStore {
&self.transcript_store
}
/// Get a reference to the identity store.
pub fn identity_store(&self) -> &IdentityStore {
&self.identity_store
}
}
impl Default for MemPalace {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_store_and_query() {
let mut palace = MemPalace::new();
palace
.store(MemoryEntry {
id: "1".to_string(),
content: "The user likes Python".to_string(),
agent_id: "default".to_string(),
session_id: "s1".to_string(),
entry_type: EntryType::Fact,
embedding: None,
created_at: Utc::now(),
metadata: std::collections::HashMap::new(),
})
.unwrap();
let results = palace.query("python", 10).unwrap();
assert_eq!(results.len(), 1);
assert!(results[0].content.contains("Python"));
}
#[test]
fn test_export() {
let palace = MemPalace::new();
let tmp = std::env::temp_dir().join("gyandoot_test_export.json");
palace.export_to(&tmp).unwrap();
assert!(tmp.exists());
std::fs::remove_file(tmp).ok();
}
}

View File

@@ -0,0 +1,125 @@
//! Transcript store — structured conversation log management.
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// A single turn in a conversation transcript.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscriptTurn {
pub turn_number: u64,
pub user_message: String,
pub assistant_message: String,
pub tool_calls: Vec<String>,
pub latency_ms: u64,
pub timestamp: DateTime<Utc>,
}
/// A complete conversation transcript.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transcript {
pub id: String,
pub agent_id: String,
pub session_id: String,
pub started_at: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
pub turns: Vec<TranscriptTurn>,
pub summary: Option<String>,
}
/// Manages conversation transcripts.
pub struct TranscriptStore {
transcripts: HashMap<String, Transcript>,
}
impl TranscriptStore {
/// Create a new empty transcript store.
pub fn new() -> Self {
Self {
transcripts: HashMap::new(),
}
}
/// Start a new transcript.
pub fn start_transcript(&mut self, id: String, agent_id: String, session_id: String) {
self.transcripts.insert(
id.clone(),
Transcript {
id,
agent_id,
session_id,
started_at: Utc::now(),
ended_at: None,
turns: Vec::new(),
summary: None,
},
);
}
/// Append a turn to an existing transcript.
pub fn append_turn(&mut self, transcript_id: &str, turn: TranscriptTurn) -> Option<()> {
self.transcripts.get_mut(transcript_id)?.turns.push(turn);
Some(())
}
/// End a transcript by setting the end timestamp.
pub fn end_transcript(&mut self, transcript_id: &str) -> Option<()> {
self.transcripts
.get_mut(transcript_id)?
.ended_at = Some(Utc::now());
Some(())
}
/// Set a summary for the transcript.
pub fn set_summary(&mut self, transcript_id: &str, summary: String) -> Option<()> {
self.transcripts.get_mut(transcript_id)?.summary = Some(summary);
Some(())
}
/// Get a transcript by ID.
pub fn get(&self, transcript_id: &str) -> Option<&Transcript> {
self.transcripts.get(transcript_id)
}
/// List all transcript IDs.
pub fn list_ids(&self) -> Vec<String> {
self.transcripts.keys().cloned().collect()
}
}
impl Default for TranscriptStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transcript_lifecycle() {
let mut store = TranscriptStore::new();
store.start_transcript("t1".to_string(), "agent1".to_string(), "session1".to_string());
store
.append_turn(
"t1",
TranscriptTurn {
turn_number: 1,
user_message: "Hello".to_string(),
assistant_message: "Hi there!".to_string(),
tool_calls: vec![],
latency_ms: 500,
timestamp: Utc::now(),
},
)
.unwrap();
store.end_transcript("t1").unwrap();
let t = store.get("t1").unwrap();
assert_eq!(t.turns.len(), 1);
assert!(t.ended_at.is_some());
}
}