Skip to main content

agent_office/services/kb/
mod.rs

1use crate::domain::{Edge, Properties, PropertyValue, string_to_node_id};
2use crate::services::kb::domain::{KnowledgeBase, LinkType, LuhmannId, Note, NoteId, NoteLink, AgentId};
3use crate::storage::{GraphStorage, StorageError, EdgeDirection};
4use async_trait::async_trait;
5use thiserror::Error;
6use std::collections::{HashMap, HashSet, VecDeque};
7
8pub mod domain;
9
10#[derive(Error, Debug)]
11pub enum KbError {
12    #[error("Note not found: {0}")]
13    NoteNotFound(NoteId),
14    
15    #[error("Knowledge base not found for agent: {0}")]
16    KnowledgeBaseNotFound(AgentId),
17    
18    #[error("Agent not found: {0}")]
19    AgentNotFound(AgentId),
20    
21    #[error("Invalid link type: {0}")]
22    InvalidLinkType(String),
23    
24    #[error("Storage error: {0}")]
25    Storage(#[from] StorageError),
26    
27    #[error("Note already linked")]
28    AlreadyLinked,
29    
30    #[error("Cannot link note to itself")]
31    SelfLink,
32}
33
34pub type Result<T> = std::result::Result<T, KbError>;
35
36#[async_trait]
37pub trait KnowledgeBaseService: Send + Sync {
38    // Knowledge Base operations
39    async fn create_knowledge_base(&self, agent_id: AgentId, name: impl Into<String> + Send) -> Result<KnowledgeBase>;
40    async fn get_knowledge_base(&self, agent_id: AgentId) -> Result<KnowledgeBase>;
41    
42    // Note operations
43    async fn create_note(
44        &self,
45        agent_id: AgentId,
46        title: impl Into<String> + Send,
47        content: impl Into<String> + Send,
48    ) -> Result<Note>;
49    
50    async fn get_note(&self, note_id: NoteId) -> Result<Note>;
51    async fn update_note(&self, note: &Note) -> Result<Note>;
52    async fn delete_note(&self, note_id: NoteId) -> Result<()>;
53    async fn list_agent_notes(&self, agent_id: AgentId) -> Result<Vec<Note>>;
54    async fn search_notes(&self, agent_id: AgentId, query: &str) -> Result<Vec<Note>>;
55    
56    // Tag operations
57    async fn add_tag(&self, note_id: NoteId, tag: impl Into<String> + Send) -> Result<Note>;
58    async fn remove_tag(&self, note_id: NoteId, tag: &str) -> Result<Note>;
59    async fn list_notes_by_tag(&self, agent_id: AgentId, tag: &str) -> Result<Vec<Note>>;
60    async fn get_all_tags(&self, agent_id: AgentId) -> Result<Vec<String>>;
61    
62    // Link operations
63    async fn link_notes(
64        &self,
65        from_note_id: NoteId,
66        to_note_id: NoteId,
67        link_type: LinkType,
68        context: Option<String>,
69    ) -> Result<()>;
70    
71    async fn unlink_notes(&self, from_note_id: NoteId, to_note_id: NoteId, link_type: LinkType) -> Result<()>;
72    async fn get_links_from(&self, note_id: NoteId, link_type: Option<LinkType>) -> Result<Vec<NoteLink>>;
73    async fn get_links_to(&self, note_id: NoteId, link_type: Option<LinkType>) -> Result<Vec<NoteLink>>;
74    async fn get_backlinks(&self, note_id: NoteId) -> Result<Vec<Note>>;
75    
76    // Graph traversal
77    async fn get_related_notes(&self, note_id: NoteId, depth: usize) -> Result<Vec<Note>>;
78    async fn find_path(&self, start_note_id: NoteId, end_note_id: NoteId, max_depth: usize) -> Result<Option<Vec<NoteId>>>;
79    async fn get_note_graph(&self, note_id: NoteId, depth: usize) -> Result<NoteGraph>;
80    
81    // Luhmann ID operations (Zettelkasten addressing)
82    async fn create_note_with_luhmann_id(
83        &self,
84        agent_id: AgentId,
85        luhmann_id: LuhmannId,
86        title: impl Into<String> + Send,
87        content: impl Into<String> + Send,
88    ) -> Result<Note>;
89    
90    async fn create_note_branch(
91        &self,
92        agent_id: AgentId,
93        parent_note_id: NoteId,
94        title: impl Into<String> + Send,
95        content: impl Into<String> + Send,
96    ) -> Result<Note>;
97    
98    async fn get_note_by_luhmann_id(&self, agent_id: AgentId, luhmann_id: &LuhmannId) -> Result<Option<Note>>;
99    async fn get_next_available_id(&self, agent_id: AgentId, parent_id: Option<&LuhmannId>) -> Result<LuhmannId>;
100    async fn list_notes_by_luhmann_prefix(&self, agent_id: AgentId, prefix: &LuhmannId) -> Result<Vec<Note>>;
101}
102
103/// Represents a subgraph of related notes
104#[derive(Debug, Clone)]
105pub struct NoteGraph {
106    pub center_note_id: NoteId,
107    pub notes: Vec<Note>,
108    pub links: Vec<NoteLink>,
109}
110
111pub struct KnowledgeBaseServiceImpl<S: GraphStorage> {
112    storage: S,
113}
114
115impl<S: GraphStorage> KnowledgeBaseServiceImpl<S> {
116    pub fn new(storage: S) -> Self {
117        Self { storage }
118    }
119
120    async fn note_exists(&self, note_id: NoteId) -> Result<bool> {
121        match self.storage.get_node(note_id).await {
122            Ok(node) => Ok(Note::from_node(&node).is_some()),
123            Err(StorageError::NodeNotFound(_)) => Ok(false),
124            Err(e) => Err(KbError::Storage(e)),
125        }
126    }
127}
128
129#[async_trait]
130impl<S: GraphStorage> KnowledgeBaseService for KnowledgeBaseServiceImpl<S> {
131    async fn create_knowledge_base(&self, agent_id: AgentId, name: impl Into<String> + Send) -> Result<KnowledgeBase> {
132        // Clone agent_id for error handling
133        let agent_id_for_err = agent_id.clone();
134        // Get agent and verify it exists
135        let node_id = string_to_node_id(&agent_id);
136        let node = self.storage.get_node(node_id).await
137            .map_err(|e| match e {
138                StorageError::NodeNotFound(_) => KbError::AgentNotFound(agent_id_for_err),
139                _ => KbError::Storage(e),
140            })?;
141        
142        // Agent node becomes the knowledge base - update its properties
143        let mut kb = KnowledgeBase::new(agent_id.clone(), name);
144        kb.agent_id = agent_id.clone(); // Already set but explicit
145        
146        // Update the agent node with kb metadata
147        let mut updated_node = node.clone();
148        updated_node.properties.insert("kb_name".to_string(), PropertyValue::String(kb.name.clone()));
149        updated_node.properties.insert("kb_enabled".to_string(), PropertyValue::Boolean(true));
150        updated_node.properties.insert("next_main_id".to_string(), PropertyValue::Integer(1));
151        
152        self.storage.update_node(&updated_node).await?;
153        Ok(kb)
154    }
155
156    async fn get_knowledge_base(&self, agent_id: AgentId) -> Result<KnowledgeBase> {
157        let agent_id_err = agent_id.clone();
158        let node_id = string_to_node_id(&agent_id);
159        let node = self.storage.get_node(node_id).await
160            .map_err(|e| match e {
161                StorageError::NodeNotFound(_) => KbError::KnowledgeBaseNotFound(agent_id_err),
162                _ => KbError::Storage(e),
163            })?;
164        
165        // Check if kb_enabled is set
166        let kb_enabled = node.get_property("kb_enabled")
167            .and_then(|v| match v {
168                PropertyValue::Boolean(b) => Some(*b),
169                _ => None,
170            })
171            .unwrap_or(false);
172        
173        if !kb_enabled {
174            return Err(KbError::KnowledgeBaseNotFound(agent_id.clone()));
175        }
176        
177        let name = node.get_property("kb_name")
178            .and_then(|v| v.as_str())
179            .unwrap_or("Untitled KB")
180            .to_string();
181        
182        let next_main_id = node.get_property("next_main_id")
183            .and_then(|v| match v {
184                PropertyValue::Integer(n) => Some(*n as u32),
185                _ => Some(1),
186            })
187            .unwrap_or(1);
188        
189        // Get agent_id from node properties or use a default
190        let agent_id = node.get_property("agent_id")
191            .and_then(|v| v.as_str())
192            .map(String::from)
193            .unwrap_or_else(|| node.id.to_string());
194        
195        Ok(KnowledgeBase {
196            agent_id,
197            name,
198            description: None,
199            created_at: node.created_at,
200            next_main_id,
201        })
202    }
203
204    async fn create_note(
205        &self,
206        agent_id: AgentId,
207        title: impl Into<String> + Send,
208        content: impl Into<String> + Send,
209    ) -> Result<Note> {
210        // Clone agent_id for multiple uses
211        let agent_id_for_err = agent_id.clone();
212        
213        // Verify agent/knowledge base exists
214        let _ = self.storage.get_node(string_to_node_id(&agent_id)).await
215            .map_err(|e| match e {
216                StorageError::NodeNotFound(_) => KbError::AgentNotFound(agent_id_for_err),
217                _ => KbError::Storage(e),
218            })?;
219        
220        // Generate next available Luhmann ID for top-level note
221        let luhmann_id = self.get_next_available_id(agent_id.clone(), None).await?;
222        
223        let note = Note::new(agent_id.clone(), title, content)
224            .with_luhmann_id(luhmann_id);
225        let node = note.to_node();
226        self.storage.create_node(&node).await?;
227        
228        // Create ownership edge from agent to note
229        let edge = Edge::new(
230            "owns_note",
231            string_to_node_id(&agent_id),
232            note.id,
233            Properties::new(),
234        );
235        self.storage.create_edge(&edge).await?;
236        
237        Ok(note)
238    }
239
240    async fn get_note(&self, note_id: NoteId) -> Result<Note> {
241        let node = self.storage.get_node(note_id).await
242            .map_err(|e| match e {
243                StorageError::NodeNotFound(_) => KbError::NoteNotFound(note_id),
244                _ => KbError::Storage(e),
245            })?;
246        
247        Note::from_node(&node)
248            .ok_or_else(|| KbError::NoteNotFound(note_id))
249    }
250
251    async fn update_note(&self, note: &Note) -> Result<Note> {
252        // Verify note exists
253        self.get_note(note.id).await?;
254        
255        let node = note.to_node();
256        self.storage.update_node(&node).await?;
257        Ok(note.clone())
258    }
259
260    async fn delete_note(&self, note_id: NoteId) -> Result<()> {
261        // Verify note exists
262        self.get_note(note_id).await?;
263        
264        // Delete the note (edges will be cascade deleted)
265        self.storage.delete_node(note_id).await?;
266        Ok(())
267    }
268
269    async fn list_agent_notes(&self, agent_id: AgentId) -> Result<Vec<Note>> {
270        // Clone for error handling
271        let agent_id_err = agent_id.clone();
272        
273        // Verify agent exists
274        let agent_node_id = string_to_node_id(&agent_id);
275        let _ = self.storage.get_node(agent_node_id).await
276            .map_err(|e| match e {
277                StorageError::NodeNotFound(_) => KbError::AgentNotFound(agent_id_err),
278                _ => KbError::Storage(e),
279            })?;
280        
281        // Get all notes owned by this agent
282        let notes = self.storage
283            .get_neighbors(agent_node_id, Some("owns_note"), EdgeDirection::Outgoing)
284            .await?;
285        
286        let notes: Vec<Note> = notes.iter()
287            .filter_map(Note::from_node)
288            .collect();
289        
290        Ok(notes)
291    }
292
293    async fn search_notes(&self, agent_id: AgentId, query: &str) -> Result<Vec<Note>> {
294        let all_notes = self.list_agent_notes(agent_id).await?;
295        let query_lower = query.to_lowercase();
296        
297        let filtered: Vec<Note> = all_notes.into_iter()
298            .filter(|note| {
299                note.title.to_lowercase().contains(&query_lower) ||
300                note.content.to_lowercase().contains(&query_lower) ||
301                note.tags.iter().any(|tag| tag.to_lowercase().contains(&query_lower))
302            })
303            .collect();
304        
305        Ok(filtered)
306    }
307
308    async fn add_tag(&self, note_id: NoteId, tag: impl Into<String> + Send) -> Result<Note> {
309        let mut note = self.get_note(note_id).await?;
310        note.add_tag(tag);
311        self.update_note(&note).await?;
312        Ok(note)
313    }
314
315    async fn remove_tag(&self, note_id: NoteId, tag: &str) -> Result<Note> {
316        let mut note = self.get_note(note_id).await?;
317        note.remove_tag(tag);
318        self.update_note(&note).await?;
319        Ok(note)
320    }
321
322    async fn list_notes_by_tag(&self, agent_id: AgentId, tag: &str) -> Result<Vec<Note>> {
323        let all_notes = self.list_agent_notes(agent_id).await?;
324        let filtered: Vec<Note> = all_notes.into_iter()
325            .filter(|note| note.tags.contains(&tag.to_string()))
326            .collect();
327        Ok(filtered)
328    }
329
330    async fn get_all_tags(&self, agent_id: AgentId) -> Result<Vec<String>> {
331        let notes = self.list_agent_notes(agent_id).await?;
332        let mut tags = HashSet::new();
333        for note in notes {
334            for tag in note.tags {
335                tags.insert(tag);
336            }
337        }
338        let mut tags: Vec<String> = tags.into_iter().collect();
339        tags.sort();
340        Ok(tags)
341    }
342
343    async fn link_notes(
344        &self,
345        from_note_id: NoteId,
346        to_note_id: NoteId,
347        link_type: LinkType,
348        context: Option<String>,
349    ) -> Result<()> {
350        if from_note_id == to_note_id {
351            return Err(KbError::SelfLink);
352        }
353        
354        // Verify both notes exist
355        self.get_note(from_note_id).await?;
356        self.get_note(to_note_id).await?;
357        
358        // Create link edge with context in properties
359        let mut props = Properties::new();
360        if let Some(ctx) = context {
361            props.insert("context".to_string(), crate::domain::PropertyValue::String(ctx));
362        }
363        
364        let edge = Edge::new(
365            link_type.as_str(),
366            from_note_id,
367            to_note_id,
368            props,
369        );
370        
371        self.storage.create_edge(&edge).await?;
372        Ok(())
373    }
374
375    async fn unlink_notes(&self, from_note_id: NoteId, to_note_id: NoteId, link_type: LinkType) -> Result<()> {
376        // Get edges from the source note
377        let edges = self.storage.get_edges_from(from_note_id, Some(link_type.as_str())).await?;
378        
379        // Find and delete the specific edge
380        for edge in edges {
381            if edge.to_node_id == to_note_id {
382                self.storage.delete_edge(edge.id).await?;
383                return Ok(());
384            }
385        }
386        
387        Err(KbError::NoteNotFound(to_note_id))
388    }
389
390    async fn get_links_from(&self, note_id: NoteId, link_type: Option<LinkType>) -> Result<Vec<NoteLink>> {
391        let edges = if let Some(lt) = link_type {
392            self.storage.get_edges_from(note_id, Some(lt.as_str())).await?
393        } else {
394            self.storage.get_edges_from(note_id, None).await?
395        };
396        
397        let links: Vec<NoteLink> = edges.iter()
398            .filter_map(|edge| {
399                LinkType::from_str(&edge.edge_type).map(|lt| {
400                    let context = edge.properties.get("context")
401                        .and_then(|v| v.as_str())
402                        .map(String::from);
403                    
404                    NoteLink::new(edge.from_node_id, edge.to_node_id, lt, context)
405                })
406            })
407            .collect();
408        
409        Ok(links)
410    }
411
412    async fn get_links_to(&self, note_id: NoteId, link_type: Option<LinkType>) -> Result<Vec<NoteLink>> {
413        let edges = if let Some(lt) = link_type {
414            self.storage.get_edges_to(note_id, Some(lt.as_str())).await?
415        } else {
416            self.storage.get_edges_to(note_id, None).await?
417        };
418        
419        let links: Vec<NoteLink> = edges.iter()
420            .filter_map(|edge| {
421                LinkType::from_str(&edge.edge_type).map(|lt| {
422                    let context = edge.properties.get("context")
423                        .and_then(|v| v.as_str())
424                        .map(String::from);
425                    
426                    NoteLink::new(edge.from_node_id, edge.to_node_id, lt, context)
427                })
428            })
429            .collect();
430        
431        Ok(links)
432    }
433
434    async fn get_backlinks(&self, note_id: NoteId) -> Result<Vec<Note>> {
435        // Get all edges pointing to this note
436        let edges = self.storage.get_edges_to(note_id, None).await?;
437        
438        let mut notes = Vec::new();
439        for edge in edges {
440            if let Ok(note) = self.get_note(edge.from_node_id).await {
441                notes.push(note);
442            }
443        }
444        
445        Ok(notes)
446    }
447
448    async fn get_related_notes(&self, note_id: NoteId, depth: usize) -> Result<Vec<Note>> {
449        if depth == 0 {
450            return Ok(vec![]);
451        }
452        
453        let mut visited = HashSet::new();
454        let mut result = Vec::new();
455        let mut queue = VecDeque::new();
456        
457        queue.push_back((note_id, 0usize));
458        visited.insert(note_id);
459        
460        while let Some((current_id, current_depth)) = queue.pop_front() {
461            if current_depth >= depth {
462                continue;
463            }
464            
465            // Get all neighbors (both outgoing and incoming edges)
466            let neighbors = self.storage
467                .get_neighbors(current_id, None, EdgeDirection::Both)
468                .await?;
469            
470            for neighbor in neighbors {
471                if visited.insert(neighbor.id) {
472                    if let Some(note) = Note::from_node(&neighbor) {
473                        result.push(note);
474                        queue.push_back((neighbor.id, current_depth + 1));
475                    }
476                }
477            }
478        }
479        
480        Ok(result)
481    }
482
483    async fn find_path(&self, start_note_id: NoteId, end_note_id: NoteId, max_depth: usize) -> Result<Option<Vec<NoteId>>> {
484        if start_note_id == end_note_id {
485            return Ok(Some(vec![start_note_id]));
486        }
487        
488        let mut visited = HashSet::new();
489        let mut queue = VecDeque::new();
490        let mut parent_map: HashMap<NoteId, NoteId> = HashMap::new();
491        
492        queue.push_back((start_note_id, 0usize));
493        visited.insert(start_note_id);
494        
495        while let Some((current_id, depth)) = queue.pop_front() {
496            if depth >= max_depth {
497                continue;
498            }
499            
500            // Get neighbors (outgoing only for path finding)
501            let neighbors = self.storage
502                .get_neighbors(current_id, None, EdgeDirection::Outgoing)
503                .await?;
504            
505            for neighbor in neighbors {
506                if visited.insert(neighbor.id) {
507                    parent_map.insert(neighbor.id, current_id);
508                    
509                    if neighbor.id == end_note_id {
510                        // Reconstruct path
511                        let mut path = vec![end_note_id];
512                        let mut current = end_note_id;
513                        
514                        while let Some(&parent) = parent_map.get(&current) {
515                            path.push(parent);
516                            current = parent;
517                        }
518                        
519                        path.reverse();
520                        return Ok(Some(path));
521                    }
522                    
523                    queue.push_back((neighbor.id, depth + 1));
524                }
525            }
526        }
527        
528        Ok(None)
529    }
530
531    async fn get_note_graph(&self, note_id: NoteId, depth: usize) -> Result<NoteGraph> {
532        let center_note = self.get_note(note_id).await?;
533        let related_notes = self.get_related_notes(note_id, depth).await?;
534        
535        let mut notes = vec![center_note.clone()];
536        notes.extend(related_notes);
537        
538        // Collect all links between notes in the graph
539        let mut links = Vec::new();
540        let note_ids: HashSet<NoteId> = notes.iter().map(|n| n.id).collect();
541        
542        for note in &notes {
543            let outgoing = self.get_links_from(note.id, None).await?;
544            for link in outgoing {
545                if note_ids.contains(&link.to_note_id) {
546                    links.push(link);
547                }
548            }
549        }
550        
551        Ok(NoteGraph {
552            center_note_id: note_id,
553            notes,
554            links,
555        })
556    }
557
558    async fn create_note_with_luhmann_id(
559        &self,
560        agent_id: AgentId,
561        luhmann_id: LuhmannId,
562        title: impl Into<String> + Send,
563        content: impl Into<String> + Send,
564    ) -> Result<Note> {
565        // Clone agent_id for multiple uses
566        let agent_id_for_err = agent_id.clone();
567        
568        // Verify agent/knowledge base exists
569        let _ = self.storage.get_node(string_to_node_id(&agent_id)).await
570            .map_err(|e| match e {
571                StorageError::NodeNotFound(_) => KbError::AgentNotFound(agent_id_for_err),
572                _ => KbError::Storage(e),
573            })?;
574        
575        let note = Note::new(agent_id.clone(), title, content)
576            .with_luhmann_id(luhmann_id);
577        let node = note.to_node();
578        self.storage.create_node(&node).await?;
579        
580        // Create ownership edge from agent to note
581        let edge = Edge::new(
582            "owns_note",
583            string_to_node_id(&agent_id),
584            note.id,
585            Properties::new(),
586        );
587        self.storage.create_edge(&edge).await?;
588        
589        Ok(note)
590    }
591
592    async fn create_note_branch(
593        &self,
594        agent_id: AgentId,
595        parent_note_id: NoteId,
596        title: impl Into<String> + Send,
597        content: impl Into<String> + Send,
598    ) -> Result<Note> {
599        // Get parent note to find its Luhmann ID
600        let parent_note = self.get_note(parent_note_id).await?;
601        
602        let parent_luhmann_id = parent_note.luhmann_id
603            .ok_or_else(|| KbError::NoteNotFound(parent_note_id))?;
604        
605        // Generate the next available child ID
606        let child_id = self.get_next_available_id(agent_id.clone(), Some(&parent_luhmann_id)).await?;
607        
608        // Create the note with the Luhmann ID
609        let note = self.create_note_with_luhmann_id(
610            agent_id,
611            child_id.clone(),
612            title,
613            content,
614        ).await?;
615        
616        // Create reference link to parent
617        self.link_notes(note.id, parent_note_id, LinkType::References, Some(format!("Branch of {}", parent_luhmann_id))).await?;
618        
619        Ok(note)
620    }
621
622    async fn get_note_by_luhmann_id(&self, agent_id: AgentId, luhmann_id: &LuhmannId) -> Result<Option<Note>> {
623        // Get all notes for this agent and find the one with matching Luhmann ID
624        let notes = self.list_agent_notes(agent_id).await?;
625        
626        Ok(notes.into_iter()
627            .find(|note| note.luhmann_id.as_ref() == Some(luhmann_id)))
628    }
629
630    async fn get_next_available_id(&self, agent_id: AgentId, parent_id: Option<&LuhmannId>) -> Result<LuhmannId> {
631        let all_notes = self.list_agent_notes(agent_id).await?;
632        
633        // Collect all existing Luhmann IDs at the specified level
634        let existing_ids: Vec<LuhmannId> = all_notes
635            .iter()
636            .filter_map(|note| note.luhmann_id.clone())
637            .filter(|id| {
638                if let Some(parent) = parent_id {
639                    // Check if this ID is a direct child of the parent
640                    id.parent().as_ref() == Some(parent)
641                } else {
642                    // Top-level IDs have no parent
643                    id.parent().is_none()
644                }
645            })
646            .collect();
647        
648        if let Some(parent) = parent_id {
649            // Generate next sibling under parent
650            if existing_ids.is_empty() {
651                // First child of parent
652                Ok(parent.first_child())
653            } else {
654                // Find the last child and get next sibling
655                let mut sorted = existing_ids.clone();
656                sorted.sort();
657                if let Some(last) = sorted.last() {
658                    Ok(last.next_sibling()
659                        .unwrap_or_else(|| last.first_child()))
660                } else {
661                    Ok(parent.first_child())
662                }
663            }
664        } else {
665            // Top-level - generate next main topic ID
666            if existing_ids.is_empty() {
667                Ok(LuhmannId { parts: vec![crate::services::kb::domain::LuhmannPart::Number(1)] })
668            } else {
669                let mut sorted = existing_ids;
670                sorted.sort();
671                if let Some(last) = sorted.last() {
672                    Ok(last.next_sibling()
673                        .unwrap_or_else(|| LuhmannId { parts: vec![crate::services::kb::domain::LuhmannPart::Number(1)] }))
674                } else {
675                    Ok(LuhmannId { parts: vec![crate::services::kb::domain::LuhmannPart::Number(1)] })
676                }
677            }
678        }
679    }
680
681    async fn list_notes_by_luhmann_prefix(&self, agent_id: AgentId, prefix: &LuhmannId) -> Result<Vec<Note>> {
682        let all_notes = self.list_agent_notes(agent_id).await?;
683        
684        // Filter notes whose Luhmann ID starts with the prefix
685        let filtered: Vec<Note> = all_notes
686            .into_iter()
687            .filter(|note| {
688                if let Some(ref lid) = note.luhmann_id {
689                    lid.is_descendant_of(prefix) || lid == prefix
690                } else {
691                    false
692                }
693            })
694            .collect();
695        
696        Ok(filtered)
697    }
698}