use crate::agent::AgentRegistry;
use crate::concurrency::NodeLockManager;
use crate::context::frame::{Basis, Frame, FrameStorage};
use crate::context::query::get_node_query;
use crate::context::query::{compose_frames, CompositionPolicy};
use crate::context::queue::FrameGenerationQueue;
use crate::error::ApiError;
use crate::heads::HeadIndex;
use crate::metadata::frame_write_contract::{
build_generated_metadata, generated_metadata_input_from_payload, validate_frame_metadata,
FrameMetadataValidationInput,
};
use crate::prompt_context::PromptContextArtifactStorage;
use crate::store::NodeRecordStore;
use crate::types::{FrameID, NodeID};
use crate::views::ViewPolicy;
use hex;
use std::collections::{HashSet, VecDeque};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use tracing::{debug, info, instrument, warn};
pub use crate::context::query::view::{ContextView, ContextViewBuilder, NodeContext};
pub use crate::context::types::{CompactResult, RestoreResult, TombstoneResult};
pub struct ContextApi {
node_store: Arc<dyn NodeRecordStore + Send + Sync>,
frame_storage: Arc<FrameStorage>,
head_index: Arc<parking_lot::RwLock<HeadIndex>>,
prompt_context_storage: Arc<PromptContextArtifactStorage>,
agent_registry: Arc<parking_lot::RwLock<AgentRegistry>>,
provider_registry: Arc<parking_lot::RwLock<crate::provider::ProviderRegistry>>,
lock_manager: Arc<NodeLockManager>,
workspace_root: Option<PathBuf>,
}
impl ContextApi {
pub fn new(
node_store: Arc<dyn NodeRecordStore + Send + Sync>,
frame_storage: Arc<FrameStorage>,
head_index: Arc<parking_lot::RwLock<HeadIndex>>,
prompt_context_storage: Arc<PromptContextArtifactStorage>,
agent_registry: Arc<parking_lot::RwLock<AgentRegistry>>,
provider_registry: Arc<parking_lot::RwLock<crate::provider::ProviderRegistry>>,
lock_manager: Arc<NodeLockManager>,
) -> Self {
Self {
node_store,
frame_storage,
head_index,
prompt_context_storage,
agent_registry,
provider_registry,
lock_manager,
workspace_root: None,
}
}
pub fn with_workspace_root(
node_store: Arc<dyn NodeRecordStore + Send + Sync>,
frame_storage: Arc<FrameStorage>,
head_index: Arc<parking_lot::RwLock<HeadIndex>>,
prompt_context_storage: Arc<PromptContextArtifactStorage>,
agent_registry: Arc<parking_lot::RwLock<AgentRegistry>>,
provider_registry: Arc<parking_lot::RwLock<crate::provider::ProviderRegistry>>,
lock_manager: Arc<NodeLockManager>,
workspace_root: PathBuf,
) -> Self {
Self {
node_store,
frame_storage,
head_index,
prompt_context_storage,
agent_registry,
provider_registry,
lock_manager,
workspace_root: Some(workspace_root),
}
}
fn persist_indices(&self) -> Result<(), ApiError> {
if let Some(ref workspace_root) = self.workspace_root {
{
let head_index = self.head_index.read();
let path = HeadIndex::persistence_path(workspace_root);
head_index
.save_to_disk(&path)
.map_err(|e| ApiError::StorageError(e))?;
}
}
Ok(())
}
#[instrument(skip(self), fields(node_id = %hex::encode(node_id)))]
pub fn get_node(&self, node_id: NodeID, view: ContextView) -> Result<NodeContext, ApiError> {
let start = Instant::now();
debug!("Retrieving node context");
let frame_ids = {
let head_index = self.head_index.read();
head_index.get_all_heads_for_node(&node_id)
};
let view_policy: ViewPolicy = view.into();
let (node_record, frames, total_frame_count) = get_node_query(
self.node_store.as_ref(),
&self.frame_storage,
&frame_ids,
node_id,
&view_policy,
)?;
let duration = start.elapsed();
debug!(
frame_count = frames.len(),
total_frames = total_frame_count,
duration_ms = duration.as_millis(),
"Node context retrieved"
);
Ok(NodeContext {
node_id,
node_record,
frames,
frame_count: total_frame_count,
})
}
#[instrument(
skip(self, frame, agent_id),
fields(
node_id = %hex::encode(node_id),
agent_id = %agent_id,
frame_id = %hex::encode(frame.frame_id),
frame_type = %frame.frame_type,
content_bytes = frame.content.len()
)
)]
pub fn put_frame(
&self,
node_id: NodeID,
frame: Frame,
agent_id: String,
) -> Result<FrameID, ApiError> {
let start = Instant::now();
debug!("Creating frame");
let agent = {
let registry = self.agent_registry.read();
registry.get_or_error(&agent_id)?.clone() };
agent.verify_write()?;
let _node_record = self
.node_store
.get(&node_id)
.map_err(ApiError::from)?
.ok_or_else(|| ApiError::NodeNotFound(node_id))?;
if _node_record.tombstoned_at.is_some() {
return Err(ApiError::NodeNotFound(node_id));
}
match &frame.basis {
crate::context::frame::Basis::Node(basis_node_id) => {
if *basis_node_id != node_id {
return Err(ApiError::InvalidFrame(format!(
"Frame basis node_id {:?} does not match requested node_id {:?}",
basis_node_id, node_id
)));
}
}
crate::context::frame::Basis::Frame(_) => {
}
crate::context::frame::Basis::Both { node, .. } => {
if *node != node_id {
return Err(ApiError::InvalidFrame(format!(
"Frame basis node_id {:?} does not match requested node_id {:?}",
node, node_id
)));
}
}
}
if frame.agent_id != agent_id {
return Err(ApiError::InvalidFrame(format!(
"Frame structural agent_id '{}' does not match provided agent_id '{}'",
frame.agent_id, agent_id
)));
}
let lock = self.lock_manager.get_lock(&node_id);
let _guard = lock.write();
let previous_metadata = {
let previous_head = {
let head_index = self.head_index.read();
head_index
.get_head(&node_id, &frame.frame_type)
.map_err(ApiError::from)?
};
if let Some(previous_head_id) = previous_head {
self.frame_storage
.get(&previous_head_id)
.map_err(ApiError::from)?
.map(|stored_frame| stored_frame.metadata)
} else {
None
}
};
validate_frame_metadata(FrameMetadataValidationInput {
metadata: &frame.metadata,
actor_agent_id: &agent_id,
previous_metadata: previous_metadata.as_ref(),
})?;
self.frame_storage.store(&frame).map_err(ApiError::from)?;
{
let mut head_index = self.head_index.write();
head_index
.update_head(&node_id, &frame.frame_type, &frame.frame_id)
.map_err(ApiError::from)?;
}
self.persist_indices()?;
let duration = start.elapsed();
info!(
frame_id = %hex::encode(frame.frame_id),
duration_ms = duration.as_millis(),
"Frame created"
);
Ok(frame.frame_id)
}
pub fn collect_subtree_node_ids(&self, node_id: NodeID) -> Result<HashSet<NodeID>, ApiError> {
let mut set = HashSet::new();
set.insert(node_id);
let mut queue = VecDeque::new();
let record = self
.node_store
.get(&node_id)
.map_err(ApiError::from)?
.ok_or_else(|| ApiError::NodeNotFound(node_id))?;
for &child_id in &record.children {
queue.push_back(child_id);
}
while let Some(nid) = queue.pop_front() {
if !set.insert(nid) {
continue;
}
if let Some(rec) = self.node_store.get(&nid).map_err(ApiError::from)? {
for &child_id in &rec.children {
queue.push_back(child_id);
}
}
}
Ok(set)
}
pub fn tombstone_node(&self, node_id: NodeID) -> Result<TombstoneResult, ApiError> {
let record = self
.node_store
.get(&node_id)
.map_err(ApiError::from)?
.ok_or_else(|| ApiError::NodeNotFound(node_id))?;
if record.tombstoned_at.is_some() {
return Ok(TombstoneResult {
nodes_tombstoned: 0,
head_entries_tombstoned: 0,
});
}
let to_tombstone = self.collect_subtree_node_ids(node_id)?;
let mut nodes_tombstoned = 0u64;
let mut head_entries_tombstoned = 0u64;
for &nid in &to_tombstone {
self.node_store.tombstone(&nid).map_err(ApiError::from)?;
nodes_tombstoned += 1;
let mut head_index = self.head_index.write();
let before = head_index.get_all_heads_for_node(&nid).len();
head_index.tombstone_heads_for_node(&nid);
head_entries_tombstoned += before as u64;
}
self.persist_indices()?;
Ok(TombstoneResult {
nodes_tombstoned,
head_entries_tombstoned,
})
}
pub fn restore_node(&self, node_id: NodeID) -> Result<RestoreResult, ApiError> {
let record = self
.node_store
.get(&node_id)
.map_err(ApiError::from)?
.ok_or_else(|| ApiError::NodeNotFound(node_id))?;
if record.tombstoned_at.is_none() {
return Ok(RestoreResult {
nodes_restored: 0,
head_entries_restored: 0,
});
}
let to_restore = self.collect_subtree_node_ids(node_id)?;
let mut nodes_restored = 0u64;
let mut head_entries_restored = 0u64;
for &nid in &to_restore {
self.node_store.restore(&nid).map_err(ApiError::from)?;
nodes_restored += 1;
let mut head_index = self.head_index.write();
let before = head_index.get_all_heads_for_node(&nid).len();
head_index.restore_heads_for_node(&nid);
head_entries_restored += before as u64;
}
self.persist_indices()?;
Ok(RestoreResult {
nodes_restored,
head_entries_restored,
})
}
pub fn compact(&self, ttl_seconds: u64, purge_frames: bool) -> Result<CompactResult, ApiError> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| ApiError::ConfigError(e.to_string()))?
.as_secs();
let cutoff = now.saturating_sub(ttl_seconds);
let node_ids = self
.node_store
.list_tombstoned(Some(cutoff))
.map_err(ApiError::from)?;
let mut nodes_purged = 0u64;
let mut frames_purged = 0u64;
for &nid in &node_ids {
if purge_frames {
let frame_ids = self.head_index.read().get_all_heads_for_node(&nid);
for frame_id in frame_ids {
self.frame_storage
.purge(&frame_id)
.map_err(ApiError::from)?;
frames_purged += 1;
}
}
self.node_store
.purge(&nid, cutoff)
.map_err(ApiError::from)?;
nodes_purged += 1;
}
let head_before = self.head_index.read().heads.len();
self.head_index.write().purge_tombstoned(cutoff);
let head_after = self.head_index.read().heads.len();
let head_entries_purged = (head_before - head_after) as u64;
self.persist_indices()?;
Ok(CompactResult {
nodes_purged,
head_entries_purged,
frames_purged,
})
}
pub fn compose(
&self,
node_id: NodeID,
policy: CompositionPolicy,
) -> Result<Vec<Frame>, ApiError> {
let _node_record = self
.node_store
.get(&node_id)
.map_err(ApiError::from)?
.ok_or_else(|| ApiError::NodeNotFound(node_id))?;
let head_index = self.head_index.read();
let composed = compose_frames(
node_id,
&policy,
self.node_store.as_ref(),
&self.frame_storage,
&head_index,
)?;
drop(head_index);
Ok(composed)
}
pub fn has_agent_frame(&self, node_id: &NodeID, agent_id: &str) -> Result<bool, ApiError> {
let frame_ids = self.get_all_heads(node_id);
for frame_id in frame_ids {
if let Some(frame) = self.frame_storage.get(&frame_id).map_err(ApiError::from)? {
if let Some(frame_agent_id) = frame.agent_id() {
if frame_agent_id == agent_id {
return Ok(true);
}
}
}
}
Ok(false)
}
pub fn ensure_agent_frame(
&self,
node_id: NodeID,
agent_id: String,
frame_type: Option<String>,
_generation_queue: Option<Arc<FrameGenerationQueue>>,
) -> Result<Option<FrameID>, ApiError> {
if self.has_agent_frame(&node_id, &agent_id)? {
return Ok(None);
}
let agent = {
let registry = self.agent_registry.read();
registry.get_or_error(&agent_id)?.clone()
};
if !agent.can_write() {
return Ok(None);
}
let frame_type = frame_type.unwrap_or_else(|| format!("context-{}", agent_id));
let node_record = self
.node_store
.get(&node_id)
.map_err(ApiError::from)?
.ok_or_else(|| ApiError::NodeNotFound(node_id))?;
let content = format!(
"Node: {}\nPath: {:?}\nType: {:?}",
hex::encode(node_id),
node_record.path,
node_record.node_type
)
.into_bytes();
let basis = Basis::Node(node_id);
let content_text = String::from_utf8_lossy(&content);
let metadata_input = generated_metadata_input_from_payload(
&agent_id,
"internal",
"internal",
"internal",
&content_text,
"",
);
let metadata = build_generated_metadata(&metadata_input);
let frame = Frame::new(
basis,
content,
frame_type.clone(),
agent_id.clone(),
metadata,
)
.map_err(|e| ApiError::StorageError(e))?;
let frame_id = self.put_frame(node_id, frame, agent_id)?;
Ok(Some(frame_id))
}
pub fn get_agent(&self, agent_id: &str) -> Result<crate::agent::AgentIdentity, ApiError> {
let registry = self.agent_registry.read();
registry.get_or_error(agent_id).map(|a| a.clone())
}
pub fn get_head(
&self,
node_id: &NodeID,
frame_type: &str,
) -> Result<Option<FrameID>, ApiError> {
let head_index = self.head_index.read();
head_index
.get_head(node_id, frame_type)
.map_err(ApiError::from)
}
pub fn get_all_heads(&self, node_id: &NodeID) -> Vec<FrameID> {
let head_index = self.head_index.read();
head_index.get_all_heads_for_node(node_id)
}
pub fn latest_context(&self, node_id: NodeID) -> Result<NodeContext, ApiError> {
let view = ContextView {
max_frames: 1,
ordering: crate::views::OrderingPolicy::Recency,
filters: vec![],
};
self.get_node(node_id, view)
}
pub fn context_by_type(
&self,
node_id: NodeID,
frame_type: &str,
max_frames: usize,
) -> Result<NodeContext, ApiError> {
let view = ContextView {
max_frames,
ordering: crate::views::OrderingPolicy::Recency,
filters: vec![crate::views::FrameFilter::ByType(frame_type.to_string())],
};
self.get_node(node_id, view)
}
pub fn context_by_agent(
&self,
node_id: NodeID,
agent_id: &str,
max_frames: usize,
) -> Result<NodeContext, ApiError> {
let view = ContextView {
max_frames,
ordering: crate::views::OrderingPolicy::Recency,
filters: vec![crate::views::FrameFilter::ByAgent(agent_id.to_string())],
};
self.get_node(node_id, view)
}
pub fn combined_context_text(
&self,
node_id: NodeID,
separator: &str,
view: ContextView,
) -> Result<String, ApiError> {
let context = self.get_node(node_id, view)?;
Ok(context.combined_text(separator))
}
pub fn frame_storage(&self) -> &FrameStorage {
&self.frame_storage
}
pub fn prompt_context_storage(&self) -> &PromptContextArtifactStorage {
&self.prompt_context_storage
}
pub fn node_store(&self) -> &Arc<dyn NodeRecordStore + Send + Sync> {
&self.node_store
}
pub fn head_index(&self) -> &Arc<parking_lot::RwLock<HeadIndex>> {
&self.head_index
}
pub fn agent_registry(&self) -> &Arc<parking_lot::RwLock<AgentRegistry>> {
&self.agent_registry
}
pub fn provider_registry(
&self,
) -> &Arc<parking_lot::RwLock<crate::provider::ProviderRegistry>> {
&self.provider_registry
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::AgentIdentity;
use crate::context::frame::{Basis, Frame};
use crate::store::{NodeRecord, SledNodeRecordStore};
use crate::views::{FrameFilter, OrderingPolicy};
use std::collections::HashMap;
use tempfile::TempDir;
fn create_test_api() -> (ContextApi, TempDir) {
let temp_dir = TempDir::new().unwrap();
let store_path = temp_dir.path().join("store");
let frame_storage_path = temp_dir.path().join("frames");
let artifact_storage_path = temp_dir.path().join("artifacts");
let node_store = Arc::new(SledNodeRecordStore::new(&store_path).unwrap());
let frame_storage = Arc::new(FrameStorage::new(&frame_storage_path).unwrap());
let prompt_context_storage =
Arc::new(PromptContextArtifactStorage::new(&artifact_storage_path).unwrap());
let head_index = Arc::new(parking_lot::RwLock::new(HeadIndex::new()));
let agent_registry = Arc::new(parking_lot::RwLock::new(AgentRegistry::new()));
let lock_manager = Arc::new(NodeLockManager::new());
let provider_registry = Arc::new(parking_lot::RwLock::new(
crate::provider::ProviderRegistry::new(),
));
let api = ContextApi::new(
node_store,
frame_storage,
head_index,
prompt_context_storage,
agent_registry,
provider_registry,
lock_manager,
);
(api, temp_dir)
}
fn create_test_node_record(node_id: NodeID) -> NodeRecord {
use crate::store::NodeType;
use std::path::PathBuf;
NodeRecord {
node_id,
path: PathBuf::from("/test/file.txt"),
node_type: NodeType::File {
size: 100,
content_hash: [0u8; 32],
},
children: vec![],
parent: None,
frame_set_root: None,
metadata: Default::default(),
tombstoned_at: None,
}
}
fn required_frame_metadata(agent_id: &str) -> HashMap<String, String> {
let mut metadata = HashMap::new();
metadata.insert("agent_id".to_string(), agent_id.to_string());
metadata.insert("provider".to_string(), "provider-a".to_string());
metadata.insert("model".to_string(), "model-a".to_string());
metadata.insert("provider_type".to_string(), "local".to_string());
metadata.insert("prompt_digest".to_string(), "prompt-digest-a".to_string());
metadata.insert("context_digest".to_string(), "context-digest-a".to_string());
metadata.insert("prompt_link_id".to_string(), "prompt-link-a".to_string());
metadata
}
#[test]
fn test_get_node_not_found() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let view = ContextView {
max_frames: 100,
ordering: OrderingPolicy::Recency,
filters: vec![],
};
let result = api.get_node(node_id, view);
assert!(result.is_err());
match result {
Err(ApiError::NodeNotFound(id)) => assert_eq!(id, node_id),
_ => panic!("Expected NodeNotFound error"),
}
}
#[test]
fn test_get_node_empty_context() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
let view = ContextView {
max_frames: 100,
ordering: OrderingPolicy::Recency,
filters: vec![],
};
let context = api.get_node(node_id, view).unwrap();
assert_eq!(context.node_id, node_id);
assert_eq!(context.frames.len(), 0);
assert_eq!(context.frame_count, 0);
}
#[test]
fn test_put_frame_node_not_found() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let basis = Basis::Node(node_id);
let content = b"test content".to_vec();
let frame_type = "test".to_string();
let agent_id = "writer-1".to_string();
let metadata = required_frame_metadata(&agent_id);
let frame = Frame::new(basis, content, frame_type, agent_id.clone(), metadata).unwrap();
let result = api.put_frame(node_id, frame, agent_id);
assert!(result.is_err());
match result {
Err(ApiError::NodeNotFound(id)) => assert_eq!(id, node_id),
_ => panic!("Expected NodeNotFound error"),
}
}
#[test]
fn test_put_frame_unauthorized() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("reader-1".to_string(), crate::agent::AgentRole::Reader);
registry.register(agent);
}
let basis = Basis::Node(node_id);
let content = b"test content".to_vec();
let frame_type = "test".to_string();
let agent_id = "reader-1".to_string();
let metadata = HashMap::new();
let frame = Frame::new(basis, content, frame_type, agent_id.clone(), metadata).unwrap();
let result = api.put_frame(node_id, frame, agent_id);
assert!(result.is_err());
match result {
Err(ApiError::Unauthorized(_)) => {}
_ => panic!("Expected Unauthorized error"),
}
}
#[test]
fn test_put_frame_success() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let basis = Basis::Node(node_id);
let content = b"test content".to_vec();
let frame_type = "test".to_string();
let agent_id = "writer-1".to_string();
let metadata = required_frame_metadata(&agent_id);
let frame = Frame::new(
basis,
content,
frame_type.clone(),
agent_id.clone(),
metadata,
)
.unwrap();
let frame_id = api.put_frame(node_id, frame, agent_id).unwrap();
assert!(api.frame_storage.exists(&frame_id).unwrap());
let head_index = api.head_index.read();
let head = head_index.get_head(&node_id, &frame_type).unwrap();
assert_eq!(head, Some(frame_id));
}
#[test]
fn test_get_node_with_frames() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let basis = Basis::Node(node_id);
let content = b"test content".to_vec();
let frame_type = "test".to_string();
let agent_id = "writer-1".to_string();
let metadata = required_frame_metadata(&agent_id);
let frame = Frame::new(
basis,
content,
frame_type.clone(),
agent_id.clone(),
metadata,
)
.unwrap();
let frame_id = api.put_frame(node_id, frame.clone(), agent_id).unwrap();
let view = ContextView {
max_frames: 100,
ordering: OrderingPolicy::Recency,
filters: vec![],
};
let context = api.get_node(node_id, view).unwrap();
assert_eq!(context.node_id, node_id);
assert_eq!(context.frames.len(), 1);
assert_eq!(context.frames[0].frame_id, frame_id);
}
#[test]
fn test_has_agent_frame() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
assert!(!api.has_agent_frame(&node_id, "writer-1").unwrap());
let basis = Basis::Node(node_id);
let content = b"test content".to_vec();
let frame_type = "test".to_string();
let agent_id = "writer-1".to_string();
let metadata = required_frame_metadata(&agent_id);
let frame = Frame::new(basis, content, frame_type, agent_id.clone(), metadata).unwrap();
api.put_frame(node_id, frame, agent_id.clone()).unwrap();
assert!(api.has_agent_frame(&node_id, "writer-1").unwrap());
assert!(!api.has_agent_frame(&node_id, "writer-2").unwrap());
}
#[test]
fn test_ensure_agent_frame_creates_when_missing() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let frame_id = api
.ensure_agent_frame(node_id, "writer-1".to_string(), None, None)
.unwrap();
assert!(frame_id.is_some());
assert!(api.has_agent_frame(&node_id, "writer-1").unwrap());
let frame_id2 = api
.ensure_agent_frame(node_id, "writer-1".to_string(), None, None)
.unwrap();
assert!(frame_id2.is_none());
}
#[test]
fn test_ensure_agent_frame_skips_reader_agents() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("reader-1".to_string(), crate::agent::AgentRole::Reader);
registry.register(agent);
}
let frame_id = api
.ensure_agent_frame(node_id, "reader-1".to_string(), None, None)
.unwrap();
assert!(frame_id.is_none());
assert!(!api.has_agent_frame(&node_id, "reader-1").unwrap());
}
#[test]
fn test_ensure_agent_frame_with_custom_frame_type() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let frame_id = api
.ensure_agent_frame(
node_id,
"writer-1".to_string(),
Some("custom-type".to_string()),
None,
)
.unwrap();
assert!(frame_id.is_some());
assert!(api.has_agent_frame(&node_id, "writer-1").unwrap());
let head = api.get_head(&node_id, "custom-type").unwrap();
assert!(head.is_some());
}
#[test]
fn test_ensure_agent_frame_node_not_found() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let result = api.ensure_agent_frame(node_id, "writer-1".to_string(), None, None);
assert!(result.is_err());
match result {
Err(ApiError::NodeNotFound(id)) => assert_eq!(id, node_id),
_ => panic!("Expected NodeNotFound error"),
}
}
#[test]
fn test_frame_text_content() {
let frame = {
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"Hello, world!".to_vec();
Frame::new(
basis,
content,
"test".to_string(),
"agent-1".to_string(),
HashMap::new(),
)
.unwrap()
};
assert_eq!(frame.text_content().unwrap(), "Hello, world!");
}
#[test]
fn test_frame_agent_id() {
let frame = {
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"test".to_vec();
Frame::new(
basis,
content,
"test".to_string(),
"agent-123".to_string(),
HashMap::new(),
)
.unwrap()
};
assert_eq!(frame.agent_id(), Some("agent-123"));
}
#[test]
fn test_frame_is_type() {
let frame = {
let node_id: NodeID = [1u8; 32];
let basis = Basis::Node(node_id);
let content = b"test".to_vec();
Frame::new(
basis,
content,
"analysis".to_string(),
"agent-1".to_string(),
HashMap::new(),
)
.unwrap()
};
assert!(frame.is_type("analysis"));
assert!(!frame.is_type("summary"));
}
#[test]
fn test_node_context_text_contents() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let agent_id = "writer-1".to_string();
let basis = Basis::Node(node_id);
let frame1 = Frame::new(
basis.clone(),
b"Frame content 0".to_vec(),
"type1".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
let frame2 = Frame::new(
basis.clone(),
b"Frame content 1".to_vec(),
"type2".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
let frame3 = Frame::new(
basis.clone(),
b"Frame content 2".to_vec(),
"type3".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
api.put_frame(node_id, frame1, agent_id.clone()).unwrap();
api.put_frame(node_id, frame2, agent_id.clone()).unwrap();
api.put_frame(node_id, frame3, agent_id.clone()).unwrap();
let view = ContextView {
max_frames: 10,
ordering: OrderingPolicy::Recency,
filters: vec![],
};
let context = api.get_node(node_id, view).unwrap();
let texts = context.text_contents();
assert_eq!(texts.len(), 3);
assert!(texts.iter().any(|t| t.contains("Frame content 0")));
assert!(texts.iter().any(|t| t.contains("Frame content 1")));
assert!(texts.iter().any(|t| t.contains("Frame content 2")));
}
#[test]
fn test_node_context_combined_text() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let agent_id = "writer-1".to_string();
let basis = Basis::Node(node_id);
let frame1 = Frame::new(
basis.clone(),
b"First".to_vec(),
"type1".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
let frame2 = Frame::new(
basis.clone(),
b"Second".to_vec(),
"type2".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
api.put_frame(node_id, frame1, agent_id.clone()).unwrap();
api.put_frame(node_id, frame2, agent_id.clone()).unwrap();
let view = ContextView {
max_frames: 10,
ordering: OrderingPolicy::Recency,
filters: vec![],
};
let context = api.get_node(node_id, view).unwrap();
let combined = context.combined_text(" | ");
assert!(combined.contains("First"));
assert!(combined.contains("Second"));
assert!(combined.contains(" | "));
}
#[test]
fn test_node_context_latest_frame_of_type() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let agent_id = "writer-1".to_string();
let basis = Basis::Node(node_id);
let frame1 = Frame::new(
basis.clone(),
b"analysis1".to_vec(),
"analysis".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
let frame2 = Frame::new(
basis.clone(),
b"summary1".to_vec(),
"summary".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
let frame3 = Frame::new(
basis.clone(),
b"analysis2".to_vec(),
"analysis".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
api.put_frame(node_id, frame1, agent_id.clone()).unwrap();
api.put_frame(node_id, frame2, agent_id.clone()).unwrap();
api.put_frame(node_id, frame3, agent_id.clone()).unwrap();
let view = ContextView {
max_frames: 10,
ordering: OrderingPolicy::Recency,
filters: vec![],
};
let context = api.get_node(node_id, view).unwrap();
let latest_analysis = context.latest_frame_of_type("analysis");
assert!(latest_analysis.is_some());
assert_eq!(latest_analysis.unwrap().frame_type, "analysis");
assert_eq!(
latest_analysis.unwrap().text_content().unwrap(),
"analysis2"
);
}
#[test]
fn test_node_context_frames_by_agent() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
registry.register(AgentIdentity::new(
"agent-1".to_string(),
crate::agent::AgentRole::Writer,
));
registry.register(AgentIdentity::new(
"agent-2".to_string(),
crate::agent::AgentRole::Writer,
));
}
let basis = Basis::Node(node_id);
let frame1 = Frame::new(
basis.clone(),
b"content1".to_vec(),
"type1".to_string(),
"agent-1".to_string(),
required_frame_metadata("agent-1"),
)
.unwrap();
let frame2 = Frame::new(
basis.clone(),
b"content2".to_vec(),
"type2".to_string(),
"agent-2".to_string(),
required_frame_metadata("agent-2"),
)
.unwrap();
let frame3 = Frame::new(
basis.clone(),
b"content3".to_vec(),
"type3".to_string(),
"agent-1".to_string(),
required_frame_metadata("agent-1"),
)
.unwrap();
api.put_frame(node_id, frame1, "agent-1".to_string())
.unwrap();
api.put_frame(node_id, frame2, "agent-2".to_string())
.unwrap();
api.put_frame(node_id, frame3, "agent-1".to_string())
.unwrap();
let view = ContextView {
max_frames: 10,
ordering: OrderingPolicy::Recency,
filters: vec![],
};
let context = api.get_node(node_id, view).unwrap();
let agent1_frames = context.frames_by_agent("agent-1");
assert_eq!(agent1_frames.len(), 2);
assert!(agent1_frames
.iter()
.all(|f| f.agent_id() == Some("agent-1")));
}
#[test]
fn test_context_view_builder() {
let view = ContextView::builder()
.max_frames(50)
.recent()
.by_type("analysis")
.by_agent("agent-1")
.build();
assert_eq!(view.max_frames, 50);
assert_eq!(view.ordering, OrderingPolicy::Recency);
assert_eq!(view.filters.len(), 2);
assert!(matches!(view.filters[0], FrameFilter::ByType(_)));
assert!(matches!(view.filters[1], FrameFilter::ByAgent(_)));
}
#[test]
fn test_context_view_builder_defaults() {
let view = ContextView::builder().build();
assert_eq!(view.max_frames, 100); assert_eq!(view.ordering, OrderingPolicy::Recency); assert!(view.filters.is_empty());
}
#[test]
fn test_context_api_latest_context() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let agent_id = "writer-1".to_string();
let basis = Basis::Node(node_id);
for i in 0..5 {
let content = format!("Frame {}", i).into_bytes();
let frame = Frame::new(
basis.clone(),
content,
"test".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
api.put_frame(node_id, frame, agent_id.clone()).unwrap();
}
let context = api.latest_context(node_id).unwrap();
assert_eq!(context.frames.len(), 1); assert_eq!(context.frames[0].text_content().unwrap(), "Frame 4");
}
#[test]
fn test_context_api_context_by_type() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let agent_id = "writer-1".to_string();
let basis = Basis::Node(node_id);
let frame1 = Frame::new(
basis.clone(),
b"analysis1".to_vec(),
"analysis".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
let frame2 = Frame::new(
basis.clone(),
b"summary1".to_vec(),
"summary".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
let frame3 = Frame::new(
basis.clone(),
b"analysis2".to_vec(),
"analysis".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
api.put_frame(node_id, frame1, agent_id.clone()).unwrap();
api.put_frame(node_id, frame2, agent_id.clone()).unwrap();
api.put_frame(node_id, frame3, agent_id.clone()).unwrap();
let context = api.context_by_type(node_id, "analysis", 10).unwrap();
assert_eq!(context.frames.len(), 1);
assert!(context.frames.iter().all(|f| f.is_type("analysis")));
}
#[test]
fn test_context_api_combined_context_text() {
let (api, _temp_dir) = create_test_api();
let node_id: NodeID = [1u8; 32];
let node_record = create_test_node_record(node_id);
api.node_store.put(&node_record).unwrap();
{
let mut registry = api.agent_registry.write();
let agent = AgentIdentity::new("writer-1".to_string(), crate::agent::AgentRole::Writer);
registry.register(agent);
}
let agent_id = "writer-1".to_string();
let basis = Basis::Node(node_id);
let frame1 = Frame::new(
basis.clone(),
b"First".to_vec(),
"type1".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
let frame2 = Frame::new(
basis.clone(),
b"Second".to_vec(),
"type2".to_string(),
agent_id.clone(),
required_frame_metadata(&agent_id),
)
.unwrap();
api.put_frame(node_id, frame1, agent_id.clone()).unwrap();
api.put_frame(node_id, frame2, agent_id.clone()).unwrap();
let view = ContextView {
max_frames: 10,
ordering: OrderingPolicy::Recency,
filters: vec![],
};
let combined = api.combined_context_text(node_id, " | ", view).unwrap();
assert!(combined.contains("First"));
assert!(combined.contains("Second"));
assert!(combined.contains(" | "));
}
}