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,162 @@
//! Context compression logic.
//!
//! When the agent's conversation context grows too large, this module
//! intelligently compresses or summarizes older messages while preserving
//! critical information.
use gyandoot_core::Message;
use tracing::debug;
/// Compresses conversation context when it exceeds size thresholds.
pub struct ContextCompressor {
max_messages_before_compress: usize,
}
impl ContextCompressor {
/// Create a new context compressor.
///
/// By default, compression triggers when there are more than 30 messages
/// (roughly corresponding to 15 turns of conversation).
pub fn new() -> Self {
Self {
max_messages_before_compress: 30,
}
}
/// Create a compressor with a custom message threshold.
pub fn with_threshold(max_messages: usize) -> Self {
Self {
max_messages_before_compress: max_messages,
}
}
/// Compress the message list by summarizing older messages.
///
/// Strategy:
/// - Keep the system prompt (first message).
/// - Keep the most recent N messages (recent context).
/// - Summarize the messages in between into a single condensed message.
/// - Preserve all tool call/result pairs.
pub async fn compress(&self, messages: Vec<Message>) -> Vec<Message> {
if messages.len() <= self.max_messages_before_compress {
return messages;
}
debug!(
"Compressing {} messages (threshold: {})",
messages.len(),
self.max_messages_before_compress
);
let mut compressed: Vec<Message> = Vec::with_capacity(self.max_messages_before_compress);
// Always keep the system prompt
if let Some(system) = messages.first() {
compressed.push(system.clone());
}
// Keep the most recent N-2 messages (reserve space for summary)
let recent_count = (self.max_messages_before_compress / 2).max(2);
let keep_start = messages.len().saturating_sub(recent_count);
// Summarize middle messages
let middle: Vec<&Message> = messages[1..keep_start]
.iter()
.filter(|m| m.role != gyandoot_core::MessageRole::Tool)
.collect();
if !middle.is_empty() {
let summary = self.summarize_messages(&middle);
compressed.push(Message::new(
gyandoot_core::MessageRole::System,
format!("[Summarized context from previous conversation: {}]", summary),
));
}
// Append the most recent messages
for msg in messages.iter().skip(keep_start) {
compressed.push(msg.clone());
}
debug!("Compressed to {} messages", compressed.len());
compressed
}
/// Create a text summary from a slice of messages.
fn summarize_messages(&self, messages: &[&Message]) -> String {
let count = messages.len();
let user_msgs: Vec<&str> = messages
.iter()
.filter(|m| m.role == gyandoot_core::MessageRole::User)
.map(|m| m.content.as_str())
.collect();
if user_msgs.is_empty() {
format!("{} previous assistant messages", count)
} else if user_msgs.len() <= 3 {
format!(
"Earlier discussion: {}",
user_msgs.join(" | ")
)
} else {
format!(
"{} previous turns covering topics: {}, ... (and {} more)",
count,
user_msgs[..3].join(", "),
user_msgs.len().saturating_sub(3)
)
}
}
}
impl Default for ContextCompressor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use gyandoot_core::MessageRole;
#[tokio::test]
async fn test_no_compress_below_threshold() {
let compressor = ContextCompressor::with_threshold(10);
let messages: Vec<Message> = (0..5)
.map(|i| Message::new(MessageRole::User, format!("message {}", i)))
.collect();
let result = compressor.compress(messages.clone()).await;
assert_eq!(result.len(), 5);
}
#[tokio::test]
async fn test_compress_above_threshold() {
let compressor = ContextCompressor::with_threshold(10);
let mut messages = vec![Message::new(MessageRole::System, "You are an AI.")];
for i in 0..20 {
messages.push(Message::new(MessageRole::User, format!("user message {}", i)));
messages.push(Message::new(MessageRole::Assistant, format!("response {}", i)));
}
let result = compressor.compress(messages).await;
assert!(result.len() < 20);
assert_eq!(result[0].content, "You are an AI.");
}
#[test]
fn test_summarize_empty() {
let compressor = ContextCompressor::new();
let summary = compressor.summarize_messages(&[]);
assert!(summary.contains("0"));
}
#[test]
fn test_summarize_few() {
let compressor = ContextCompressor::new();
let msg = Message::new(MessageRole::User, "hello");
let summary = compressor.summarize_messages(&[&msg]);
assert!(summary.contains("hello"));
}
}