Merge pull request 'docs(clawhdf5): document DType variants, fix unresolved doc links' (#17) from sdlc-docs/clawhdf5-types-20260514-165210 into main
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
//! AGENTS.md generation from live HDF5Memory state.
|
||||
//!
|
||||
//! Produces a structured Markdown self-description of the agent's memory file,
|
||||
//! suitable for session initialization or introspection.
|
||||
|
||||
use crate::MemoryConfig;
|
||||
use crate::cache::MemoryCache;
|
||||
use crate::knowledge::KnowledgeCache;
|
||||
use crate::session::SessionCache;
|
||||
|
||||
/// Generate AGENTS.md content from memory state components.
|
||||
pub fn generate(
|
||||
config: &MemoryConfig,
|
||||
cache: &MemoryCache,
|
||||
sessions: &SessionCache,
|
||||
knowledge: &KnowledgeCache,
|
||||
) -> String {
|
||||
let mut md = String::with_capacity(2048);
|
||||
|
||||
// Header
|
||||
md.push_str(&format!("# Agent Memory — {}\n\n", config.agent_id));
|
||||
|
||||
// Identity section
|
||||
md.push_str("## Identity\n");
|
||||
md.push_str(&format!("- Agent ID: {}\n", config.agent_id));
|
||||
md.push_str(&format!("- Memory file: {}\n", config.path.display()));
|
||||
md.push_str("- Schema version: 1.0\n");
|
||||
md.push_str(&format!("- Created: {}\n", config.created_at));
|
||||
md.push_str(&format!(
|
||||
"- Embedder: {} ({}d)\n",
|
||||
config.embedder, config.embedding_dim
|
||||
));
|
||||
md.push('\n');
|
||||
|
||||
// Memory Store section
|
||||
let active = cache.count_active();
|
||||
let total = cache.len();
|
||||
let tombstoned = total - active;
|
||||
let compression = if config.compression {
|
||||
format!("gzip({})", config.compression_level)
|
||||
} else {
|
||||
"none".to_owned()
|
||||
};
|
||||
|
||||
md.push_str("## Memory Store\n");
|
||||
md.push_str(&format!("- Active memories: {active}\n"));
|
||||
md.push_str(&format!("- Deleted (tombstoned): {tombstoned}\n"));
|
||||
md.push_str(&format!("- Total entries: {total}\n"));
|
||||
md.push_str(&format!(
|
||||
"- Storage: {}, compression={compression}\n",
|
||||
if config.float16 { "float16" } else { "float32" }
|
||||
));
|
||||
md.push('\n');
|
||||
|
||||
// Sessions section
|
||||
let session_count = sessions.len();
|
||||
let latest = sessions.latest_session_id().unwrap_or("none").to_owned();
|
||||
|
||||
md.push_str("## Sessions\n");
|
||||
md.push_str(&format!("- Total sessions: {session_count}\n"));
|
||||
md.push_str(&format!("- Most recent session: {latest}\n"));
|
||||
md.push('\n');
|
||||
|
||||
// Knowledge Graph section
|
||||
let entity_count = knowledge.entities.len();
|
||||
let relation_count = knowledge.relations.len();
|
||||
let top_entities = if knowledge.entities.is_empty() {
|
||||
"none".to_owned()
|
||||
} else {
|
||||
knowledge
|
||||
.entities
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|e| e.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
};
|
||||
|
||||
md.push_str("## Knowledge Graph\n");
|
||||
md.push_str(&format!("- Entities: {entity_count}\n"));
|
||||
md.push_str(&format!("- Relations: {relation_count}\n"));
|
||||
md.push_str(&format!("- Top entities: {top_entities}\n"));
|
||||
md.push('\n');
|
||||
|
||||
// Search Capabilities section
|
||||
md.push_str("## Search Capabilities\n");
|
||||
md.push_str("- Vector search: cosine similarity with SIMD acceleration\n");
|
||||
md.push_str("- Hybrid search: vector + BM25 (RRF)\n");
|
||||
md.push_str(&format!(
|
||||
"- Hebbian activation: enabled (boost={}, decay={})\n",
|
||||
config.hebbian_boost, config.decay_factor
|
||||
));
|
||||
|
||||
md
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{AgentMemory, HDF5Memory, MemoryEntry};
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn test_config(path: PathBuf) -> MemoryConfig {
|
||||
MemoryConfig {
|
||||
path,
|
||||
agent_id: "test-agent".to_string(),
|
||||
embedder: "openai:text-embedding-3-small".to_string(),
|
||||
embedding_dim: 384,
|
||||
chunk_size: 512,
|
||||
overlap: 50,
|
||||
float16: false,
|
||||
compression: false,
|
||||
compression_level: 0,
|
||||
compact_threshold: 0.3,
|
||||
hebbian_boost: 0.15,
|
||||
decay_factor: 0.98,
|
||||
created_at: "2025-01-01T00:00:00Z".to_string(),
|
||||
wal_enabled: false,
|
||||
wal_max_entries: 500,
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_cache(dim: usize) -> MemoryCache {
|
||||
MemoryCache::new(dim)
|
||||
}
|
||||
|
||||
fn empty_sessions() -> SessionCache {
|
||||
SessionCache::new()
|
||||
}
|
||||
|
||||
fn empty_knowledge() -> KnowledgeCache {
|
||||
KnowledgeCache::new()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_contains_agent_id() {
|
||||
let config = test_config(PathBuf::from("/tmp/test.h5"));
|
||||
let cache = empty_cache(384);
|
||||
let sessions = empty_sessions();
|
||||
let knowledge = empty_knowledge();
|
||||
|
||||
let md = generate(&config, &cache, &sessions, &knowledge);
|
||||
assert!(md.contains("test-agent"), "should contain agent_id");
|
||||
assert!(
|
||||
md.contains("# Agent Memory — test-agent"),
|
||||
"should have header with agent_id"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_contains_memory_counts() {
|
||||
let config = test_config(PathBuf::from("/tmp/test.h5"));
|
||||
let mut cache = empty_cache(4);
|
||||
let sessions = empty_sessions();
|
||||
let knowledge = empty_knowledge();
|
||||
|
||||
// Add 5 entries, tombstone 2
|
||||
for i in 0..5 {
|
||||
cache.push(
|
||||
format!("chunk {i}"),
|
||||
vec![i as f32, 0.0, 0.0, 0.0],
|
||||
"test".into(),
|
||||
1000.0,
|
||||
"s1".into(),
|
||||
"".into(),
|
||||
);
|
||||
}
|
||||
cache.mark_deleted(1);
|
||||
cache.mark_deleted(3);
|
||||
|
||||
let md = generate(&config, &cache, &sessions, &knowledge);
|
||||
assert!(md.contains("Active memories: 3"), "md = {md}");
|
||||
assert!(md.contains("Deleted (tombstoned): 2"), "md = {md}");
|
||||
assert!(md.contains("Total entries: 5"), "md = {md}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_contains_embedding_info() {
|
||||
let config = test_config(PathBuf::from("/tmp/test.h5"));
|
||||
let cache = empty_cache(384);
|
||||
let sessions = empty_sessions();
|
||||
let knowledge = empty_knowledge();
|
||||
|
||||
let md = generate(&config, &cache, &sessions, &knowledge);
|
||||
assert!(
|
||||
md.contains("openai:text-embedding-3-small"),
|
||||
"should contain embedder name"
|
||||
);
|
||||
assert!(md.contains("384d"), "should contain dimension");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_contains_entity_count() {
|
||||
let config = test_config(PathBuf::from("/tmp/test.h5"));
|
||||
let cache = empty_cache(384);
|
||||
let sessions = empty_sessions();
|
||||
let mut knowledge = empty_knowledge();
|
||||
|
||||
knowledge.add_entity("Alice", "person", -1);
|
||||
knowledge.add_entity("Bob", "person", -1);
|
||||
knowledge.add_entity("Rust", "language", -1);
|
||||
|
||||
let md = generate(&config, &cache, &sessions, &knowledge);
|
||||
assert!(md.contains("Entities: 3"), "md = {md}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_contains_session_info() {
|
||||
let config = test_config(PathBuf::from("/tmp/test.h5"));
|
||||
let cache = empty_cache(384);
|
||||
let mut sessions = empty_sessions();
|
||||
let knowledge = empty_knowledge();
|
||||
|
||||
sessions.add("sess-1", 0, 5, "api", "first session");
|
||||
sessions.add("sess-2", 6, 10, "slack", "second session");
|
||||
|
||||
let md = generate(&config, &cache, &sessions, &knowledge);
|
||||
assert!(md.contains("Total sessions: 2"), "md = {md}");
|
||||
assert!(md.contains("Most recent session: sess-2"), "md = {md}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_empty_state() {
|
||||
let config = test_config(PathBuf::from("/tmp/test.h5"));
|
||||
let cache = empty_cache(384);
|
||||
let sessions = empty_sessions();
|
||||
let knowledge = empty_knowledge();
|
||||
|
||||
let md = generate(&config, &cache, &sessions, &knowledge);
|
||||
assert!(md.contains("Active memories: 0"), "md = {md}");
|
||||
assert!(md.contains("Deleted (tombstoned): 0"), "md = {md}");
|
||||
assert!(md.contains("Total entries: 0"), "md = {md}");
|
||||
assert!(md.contains("Total sessions: 0"), "md = {md}");
|
||||
assert!(md.contains("Entities: 0"), "md = {md}");
|
||||
assert!(md.contains("Relations: 0"), "md = {md}");
|
||||
assert!(md.contains("Most recent session: none"), "md = {md}");
|
||||
assert!(md.contains("Top entities: none"), "md = {md}");
|
||||
// Valid markdown: has headers
|
||||
assert!(md.contains("# Agent Memory"));
|
||||
assert!(md.contains("## Identity"));
|
||||
assert!(md.contains("## Memory Store"));
|
||||
assert!(md.contains("## Sessions"));
|
||||
assert!(md.contains("## Knowledge Graph"));
|
||||
assert!(md.contains("## Search Capabilities"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_top_entities() {
|
||||
let config = test_config(PathBuf::from("/tmp/test.h5"));
|
||||
let cache = empty_cache(384);
|
||||
let sessions = empty_sessions();
|
||||
let mut knowledge = empty_knowledge();
|
||||
|
||||
// Add 7 entities
|
||||
for name in &["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta", "Eta"] {
|
||||
knowledge.add_entity(name, "test", -1);
|
||||
}
|
||||
|
||||
let md = generate(&config, &cache, &sessions, &knowledge);
|
||||
// Should list first 5 only
|
||||
assert!(
|
||||
md.contains("Top entities: Alpha, Beta, Gamma, Delta, Epsilon"),
|
||||
"md = {md}"
|
||||
);
|
||||
// Should NOT contain Zeta or Eta in the top entities line
|
||||
let top_line = md
|
||||
.lines()
|
||||
.find(|l| l.starts_with("- Top entities:"))
|
||||
.unwrap();
|
||||
assert!(!top_line.contains("Zeta"), "top_line = {top_line}");
|
||||
assert!(!top_line.contains("Eta"), "top_line = {top_line}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_and_read_agents_md() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let config = MemoryConfig::new(dir.path().join("test.h5"), "write-read-agent", 4);
|
||||
let mut mem = HDF5Memory::create(config).unwrap();
|
||||
|
||||
mem.save(MemoryEntry {
|
||||
chunk: "hello".into(),
|
||||
embedding: vec![1.0, 0.0, 0.0, 0.0],
|
||||
source_channel: "api".into(),
|
||||
timestamp: 1000.0,
|
||||
session_id: "s1".into(),
|
||||
tags: "".into(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
mem.write_agents_md().unwrap();
|
||||
|
||||
let read_back = HDF5Memory::read_agents_md(&dir.path().join("test.h5")).unwrap();
|
||||
let generated = mem.generate_agents_md();
|
||||
assert_eq!(read_back, generated);
|
||||
assert!(read_back.contains("write-read-agent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_hebbian_config() {
|
||||
let mut config = test_config(PathBuf::from("/tmp/test.h5"));
|
||||
config.hebbian_boost = 0.25;
|
||||
config.decay_factor = 0.95;
|
||||
let cache = empty_cache(384);
|
||||
let sessions = empty_sessions();
|
||||
let knowledge = empty_knowledge();
|
||||
|
||||
let md = generate(&config, &cache, &sessions, &knowledge);
|
||||
assert!(md.contains("boost=0.25"), "md = {md}");
|
||||
assert!(md.contains("decay=0.95"), "md = {md}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user