Skip to main content

ucp_agent/
operations.rs

1//! Core operations for agent graph traversal.
2
3use crate::cursor::{CursorNeighborhood, ViewMode};
4use crate::error::{AgentError, AgentSessionId, Result};
5use crate::rag::{RagProvider, RagSearchOptions, RagSearchResults};
6use crate::safety::{CircuitBreaker, DepthGuard, GlobalLimits};
7use crate::session::{AgentSession, SessionConfig};
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10use std::time::Instant;
11use ucm_core::{BlockId, Document, EdgeType};
12use ucm_engine::traversal::{NavigateDirection, TraversalEngine, TraversalFilter, TraversalOutput};
13use ucp_codegraph::{
14    is_codegraph_document, render_codegraph_context_prompt, CodeGraphContextUpdate,
15    CodeGraphDetailLevel, CodeGraphRenderConfig,
16};
17
18/// Result of a navigation operation.
19#[derive(Debug, Clone)]
20pub struct NavigationResult {
21    /// New position after navigation.
22    pub position: BlockId,
23    /// Whether the neighborhood was refreshed.
24    pub refreshed: bool,
25    /// Current neighborhood.
26    pub neighborhood: CursorNeighborhood,
27}
28
29/// Options for expansion operations.
30#[derive(Debug, Clone, Default)]
31pub struct ExpandOptions {
32    /// Maximum depth to expand.
33    pub depth: usize,
34    /// View mode for results.
35    pub view_mode: ViewMode,
36    /// Filter by semantic roles.
37    pub roles: Option<Vec<String>>,
38    /// Filter by tags.
39    pub tags: Option<Vec<String>>,
40}
41
42impl ExpandOptions {
43    pub fn new() -> Self {
44        Self {
45            depth: 3,
46            ..Default::default()
47        }
48    }
49
50    pub fn with_depth(mut self, depth: usize) -> Self {
51        self.depth = depth;
52        self
53    }
54
55    pub fn with_view_mode(mut self, mode: ViewMode) -> Self {
56        self.view_mode = mode;
57        self
58    }
59
60    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
61        self.roles = Some(roles);
62        self
63    }
64
65    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
66        self.tags = Some(tags);
67        self
68    }
69}
70
71/// Result of an expansion operation.
72#[derive(Debug, Clone)]
73pub struct ExpansionResult {
74    /// Root of the expansion.
75    pub root: BlockId,
76    /// Expanded blocks by depth level.
77    pub levels: Vec<Vec<BlockId>>,
78    /// Total blocks expanded.
79    pub total_blocks: usize,
80}
81
82/// Direction for expansion.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ExpandDirection {
85    /// Expand to children (descendants).
86    Down,
87    /// Expand to parents (ancestors).
88    Up,
89    /// Expand in both directions.
90    Both,
91    /// Expand via semantic edges only.
92    Semantic,
93}
94
95impl From<ucl_parser::ast::ExpandDirection> for ExpandDirection {
96    fn from(d: ucl_parser::ast::ExpandDirection) -> Self {
97        match d {
98            ucl_parser::ast::ExpandDirection::Down => ExpandDirection::Down,
99            ucl_parser::ast::ExpandDirection::Up => ExpandDirection::Up,
100            ucl_parser::ast::ExpandDirection::Both => ExpandDirection::Both,
101            ucl_parser::ast::ExpandDirection::Semantic => ExpandDirection::Semantic,
102        }
103    }
104}
105
106/// Options for search operations.
107#[derive(Debug, Clone, Default)]
108pub struct SearchOptions {
109    /// Maximum results to return.
110    pub limit: usize,
111    /// Minimum similarity threshold.
112    pub min_similarity: f32,
113    /// Filter by semantic roles.
114    pub roles: Option<Vec<String>>,
115    /// Filter by tags.
116    pub tags: Option<Vec<String>>,
117}
118
119impl SearchOptions {
120    pub fn new() -> Self {
121        Self {
122            limit: 10,
123            min_similarity: 0.0,
124            roles: None,
125            tags: None,
126        }
127    }
128
129    pub fn with_limit(mut self, limit: usize) -> Self {
130        self.limit = limit;
131        self
132    }
133
134    pub fn with_min_similarity(mut self, threshold: f32) -> Self {
135        self.min_similarity = threshold;
136        self
137    }
138}
139
140/// Result of a find operation.
141#[derive(Debug, Clone)]
142pub struct FindResult {
143    /// Matching block IDs.
144    pub matches: Vec<BlockId>,
145    /// Total blocks searched.
146    pub total_searched: usize,
147}
148
149/// View of a block's content.
150#[derive(Debug, Clone)]
151pub struct BlockView {
152    /// Block ID.
153    pub block_id: BlockId,
154    /// Content (based on view mode).
155    pub content: Option<String>,
156    /// Semantic role.
157    pub role: Option<String>,
158    /// Tags.
159    pub tags: Vec<String>,
160    /// Children count.
161    pub children_count: usize,
162    /// Incoming edges count.
163    pub incoming_edges: usize,
164    /// Outgoing edges count.
165    pub outgoing_edges: usize,
166}
167
168/// View of the cursor neighborhood.
169#[derive(Debug, Clone)]
170pub struct NeighborhoodView {
171    /// Current position.
172    pub position: BlockId,
173    /// Parent blocks.
174    pub ancestors: Vec<BlockView>,
175    /// Child blocks.
176    pub children: Vec<BlockView>,
177    /// Sibling blocks.
178    pub siblings: Vec<BlockView>,
179    /// Connected blocks via semantic edges.
180    pub connections: Vec<(BlockView, EdgeType)>,
181}
182
183/// Main interface for agent graph traversal operations.
184pub struct AgentTraversal {
185    /// Active sessions.
186    sessions: RwLock<HashMap<AgentSessionId, AgentSession>>,
187    /// The document being traversed.
188    document: Arc<RwLock<Document>>,
189    /// Optional RAG provider for semantic search.
190    rag_provider: Option<Arc<dyn RagProvider>>,
191    /// Global limits for all sessions.
192    global_limits: GlobalLimits,
193    /// Circuit breaker for fault tolerance.
194    circuit_breaker: CircuitBreaker,
195    /// Depth guard for recursion protection.
196    depth_guard: DepthGuard,
197}
198
199impl AgentTraversal {
200    /// Create a new agent traversal system.
201    pub fn new(document: Document) -> Self {
202        Self {
203            sessions: RwLock::new(HashMap::new()),
204            document: Arc::new(RwLock::new(document)),
205            rag_provider: None,
206            global_limits: GlobalLimits::default(),
207            circuit_breaker: CircuitBreaker::new(5, std::time::Duration::from_secs(30)),
208            depth_guard: DepthGuard::new(100),
209        }
210    }
211
212    /// Create with a RAG provider.
213    pub fn with_rag_provider(mut self, provider: Arc<dyn RagProvider>) -> Self {
214        self.rag_provider = Some(provider);
215        self
216    }
217
218    /// Create with custom global limits.
219    pub fn with_global_limits(mut self, limits: GlobalLimits) -> Self {
220        self.global_limits = limits;
221        self
222    }
223
224    /// Update the internal document with a new copy.
225    ///
226    /// Use this when you've added blocks to the original document
227    /// after creating the AgentTraversal.
228    pub fn update_document(&self, document: Document) -> Result<()> {
229        let mut doc = self
230            .document
231            .write()
232            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
233        *doc = document;
234        Ok(())
235    }
236
237    /// Get a clone of the internal document.
238    pub fn get_document(&self) -> Result<Document> {
239        let doc = self
240            .document
241            .read()
242            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
243        Ok(doc.clone())
244    }
245
246    // ==================== Session Management ====================
247
248    /// Create a new agent session.
249    pub fn create_session(&self, config: SessionConfig) -> Result<AgentSessionId> {
250        self.circuit_breaker.can_proceed()?;
251
252        let mut sessions = self
253            .sessions
254            .write()
255            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
256
257        if sessions.len() >= self.global_limits.max_sessions {
258            return Err(AgentError::MaxSessionsReached {
259                max: self.global_limits.max_sessions,
260            });
261        }
262
263        let doc = self
264            .document
265            .read()
266            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
267
268        // Get start block or default to document root
269        let start_block = config.start_block.unwrap_or(doc.root);
270
271        let session = AgentSession::new(start_block, config);
272        let session_id = session.id.clone();
273        sessions.insert(session_id.clone(), session);
274
275        Ok(session_id)
276    }
277
278    /// Get a reference to a session.
279    pub fn get_session(
280        &self,
281        id: &AgentSessionId,
282    ) -> Result<std::sync::RwLockReadGuard<'_, HashMap<AgentSessionId, AgentSession>>> {
283        let sessions = self
284            .sessions
285            .read()
286            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
287        if !sessions.contains_key(id) {
288            return Err(AgentError::SessionNotFound(id.clone()));
289        }
290        Ok(sessions)
291    }
292
293    /// Close a session.
294    pub fn close_session(&self, id: &AgentSessionId) -> Result<()> {
295        let mut sessions = self
296            .sessions
297            .write()
298            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
299
300        let session = sessions
301            .get_mut(id)
302            .ok_or_else(|| AgentError::SessionNotFound(id.clone()))?;
303
304        session.complete();
305        sessions.remove(id);
306        Ok(())
307    }
308
309    // ==================== Navigation ====================
310
311    /// Navigate to a specific block.
312    pub fn navigate_to(
313        &self,
314        session_id: &AgentSessionId,
315        target: BlockId,
316    ) -> Result<NavigationResult> {
317        self.circuit_breaker.can_proceed()?;
318        let _guard = self.depth_guard.try_enter()?;
319        let start = Instant::now();
320
321        let mut sessions = self
322            .sessions
323            .write()
324            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
325
326        let session = sessions
327            .get_mut(session_id)
328            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
329
330        session.check_can_traverse()?;
331
332        // Verify block exists
333        let doc = self
334            .document
335            .read()
336            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
337
338        if doc.get_block(&target).is_none() {
339            return Err(AgentError::BlockNotFound(target));
340        }
341
342        // Move cursor
343        session.cursor.move_to(target);
344        session.touch();
345        session.metrics.record_navigation();
346        session.budget.record_traversal();
347
348        // Refresh neighborhood
349        let neighborhood = self.compute_neighborhood(&doc, &target)?;
350        session.cursor.update_neighborhood(neighborhood.clone());
351
352        session.metrics.record_execution_time(start.elapsed());
353
354        Ok(NavigationResult {
355            position: target,
356            refreshed: true,
357            neighborhood,
358        })
359    }
360
361    /// Go back in navigation history.
362    pub fn go_back(&self, session_id: &AgentSessionId, steps: usize) -> Result<NavigationResult> {
363        self.circuit_breaker.can_proceed()?;
364        let start = Instant::now();
365
366        let mut sessions = self
367            .sessions
368            .write()
369            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
370
371        let session = sessions
372            .get_mut(session_id)
373            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
374
375        session.check_can_traverse()?;
376
377        let position = session
378            .cursor
379            .go_back(steps)
380            .ok_or(AgentError::EmptyHistory)?;
381
382        session.touch();
383        session.metrics.record_navigation();
384
385        // Refresh neighborhood
386        let doc = self
387            .document
388            .read()
389            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
390
391        let neighborhood = self.compute_neighborhood(&doc, &position)?;
392        session.cursor.update_neighborhood(neighborhood.clone());
393
394        session.metrics.record_execution_time(start.elapsed());
395
396        Ok(NavigationResult {
397            position,
398            refreshed: true,
399            neighborhood,
400        })
401    }
402
403    // ==================== Expansion ====================
404
405    /// Expand from a block in a given direction.
406    pub fn expand(
407        &self,
408        session_id: &AgentSessionId,
409        block_id: BlockId,
410        direction: ExpandDirection,
411        options: ExpandOptions,
412    ) -> Result<ExpansionResult> {
413        self.circuit_breaker.can_proceed()?;
414        let _guard = self.depth_guard.try_enter()?;
415        let start = Instant::now();
416
417        let sessions = self
418            .sessions
419            .read()
420            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
421
422        let session = sessions
423            .get(session_id)
424            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
425
426        session.check_can_traverse()?;
427
428        // Check depth limit
429        if options.depth > session.limits.max_expand_depth {
430            return Err(AgentError::DepthLimitExceeded {
431                current: options.depth,
432                max: session.limits.max_expand_depth,
433            });
434        }
435
436        let doc = self
437            .document
438            .read()
439            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
440
441        // Build traversal filter
442        let filter = self.build_traversal_filter(&options);
443
444        let levels = match direction {
445            ExpandDirection::Down => self.expand_down(&doc, &block_id, options.depth, &filter)?,
446            ExpandDirection::Up => self.expand_up(&doc, &block_id, options.depth)?,
447            ExpandDirection::Both => {
448                let mut down = self.expand_down(&doc, &block_id, options.depth, &filter)?;
449                let up = self.expand_up(&doc, &block_id, options.depth)?;
450                down.extend(up);
451                down
452            }
453            ExpandDirection::Semantic => self.expand_semantic(&doc, &block_id, options.depth)?,
454        };
455
456        let total_blocks: usize = levels.iter().map(|l| l.len()).sum();
457
458        // Update metrics
459        drop(sessions);
460        let mut sessions_mut = self
461            .sessions
462            .write()
463            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
464
465        if let Some(session) = sessions_mut.get_mut(session_id) {
466            session.metrics.record_expansion(total_blocks);
467            session.budget.record_traversal();
468            session.metrics.record_execution_time(start.elapsed());
469            session.touch();
470        }
471
472        Ok(ExpansionResult {
473            root: block_id,
474            levels,
475            total_blocks,
476        })
477    }
478
479    // ==================== Search ====================
480
481    /// Perform semantic search (requires RAG provider).
482    pub async fn search(
483        &self,
484        session_id: &AgentSessionId,
485        query: &str,
486        options: SearchOptions,
487    ) -> Result<RagSearchResults> {
488        self.circuit_breaker.can_proceed()?;
489        let start = Instant::now();
490
491        let rag = self
492            .rag_provider
493            .as_ref()
494            .ok_or(AgentError::RagNotConfigured)?;
495
496        {
497            let sessions = self
498                .sessions
499                .read()
500                .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
501
502            let session = sessions
503                .get(session_id)
504                .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
505
506            session.check_can_search()?;
507        }
508
509        let rag_options = RagSearchOptions::new()
510            .with_limit(options.limit)
511            .with_min_similarity(options.min_similarity);
512
513        let results = rag.search(query, rag_options).await?;
514
515        // Store results for CTX ADD RESULTS
516        let mut sessions = self
517            .sessions
518            .write()
519            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
520
521        if let Some(session) = sessions.get_mut(session_id) {
522            session.store_results(results.block_ids());
523            session.metrics.record_search();
524            session.metrics.record_execution_time(start.elapsed());
525            session.touch();
526        }
527
528        Ok(results)
529    }
530
531    /// Find blocks by pattern (no RAG required).
532    pub fn find_by_pattern(
533        &self,
534        session_id: &AgentSessionId,
535        role: Option<&str>,
536        tag: Option<&str>,
537        label: Option<&str>,
538        pattern: Option<&str>,
539    ) -> Result<FindResult> {
540        self.circuit_breaker.can_proceed()?;
541        let start = Instant::now();
542
543        let sessions = self
544            .sessions
545            .read()
546            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
547
548        let session = sessions
549            .get(session_id)
550            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
551
552        session.check_can_search()?;
553
554        let doc = self
555            .document
556            .read()
557            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
558
559        let mut matches = Vec::new();
560        let mut total_searched = 0;
561
562        // Build regex if pattern provided
563        let regex = pattern
564            .map(regex::Regex::new)
565            .transpose()
566            .map_err(|e| AgentError::Internal(format!("Invalid regex pattern: {}", e)))?;
567
568        for block in doc.blocks.values() {
569            total_searched += 1;
570
571            // Filter by role
572            if let Some(r) = role {
573                let block_role = block
574                    .metadata
575                    .semantic_role
576                    .as_ref()
577                    .map(|sr| sr.category.as_str())
578                    .unwrap_or("");
579                if block_role != r {
580                    continue;
581                }
582            }
583
584            // Filter by tag
585            if let Some(t) = tag {
586                if !block.metadata.tags.contains(&t.to_string()) {
587                    continue;
588                }
589            }
590
591            // Filter by label
592            if let Some(l) = label {
593                if block.metadata.label.as_deref() != Some(l) {
594                    continue;
595                }
596            }
597
598            // Filter by content pattern
599            if let Some(ref re) = regex {
600                let content = self.extract_content_text(&block.content);
601                if !re.is_match(&content) {
602                    continue;
603                }
604            }
605
606            matches.push(block.id);
607        }
608
609        // Store results for CTX ADD RESULTS
610        drop(sessions);
611        let mut sessions_mut = self
612            .sessions
613            .write()
614            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
615
616        if let Some(session) = sessions_mut.get_mut(session_id) {
617            session.store_results(matches.clone());
618            session.metrics.record_search();
619            session.metrics.record_execution_time(start.elapsed());
620            session.touch();
621        }
622
623        Ok(FindResult {
624            matches,
625            total_searched,
626        })
627    }
628
629    // ==================== View ====================
630
631    /// View a specific block.
632    pub fn view_block(
633        &self,
634        session_id: &AgentSessionId,
635        block_id: BlockId,
636        mode: ViewMode,
637    ) -> Result<BlockView> {
638        self.circuit_breaker.can_proceed()?;
639
640        let sessions = self
641            .sessions
642            .read()
643            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
644
645        let session = sessions
646            .get(session_id)
647            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
648
649        session.check_active()?;
650
651        let doc = self
652            .document
653            .read()
654            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
655
656        self.view_block_internal(&doc, &block_id, &mode)
657    }
658
659    /// View the neighborhood around the current cursor position.
660    pub fn view_neighborhood(&self, session_id: &AgentSessionId) -> Result<NeighborhoodView> {
661        self.circuit_breaker.can_proceed()?;
662
663        let sessions = self
664            .sessions
665            .read()
666            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
667
668        let session = sessions
669            .get(session_id)
670            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
671
672        session.check_active()?;
673
674        let position = session.cursor.position;
675        let view_mode = session.cursor.view_mode.clone();
676
677        // Release session lock before calling view_block
678        drop(sessions);
679
680        let doc = self
681            .document
682            .read()
683            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
684
685        let neighborhood = self.compute_neighborhood(&doc, &position)?;
686
687        // Build views for each block in neighborhood
688        let ancestors: Vec<BlockView> = neighborhood
689            .ancestors
690            .iter()
691            .filter_map(|id| self.view_block_internal(&doc, id, &view_mode).ok())
692            .collect();
693
694        let children: Vec<BlockView> = neighborhood
695            .children
696            .iter()
697            .filter_map(|id| self.view_block_internal(&doc, id, &view_mode).ok())
698            .collect();
699
700        let siblings: Vec<BlockView> = neighborhood
701            .siblings
702            .iter()
703            .filter_map(|id| self.view_block_internal(&doc, id, &view_mode).ok())
704            .collect();
705
706        let connections: Vec<(BlockView, EdgeType)> = neighborhood
707            .connections
708            .iter()
709            .filter_map(|(id, edge_type)| {
710                self.view_block_internal(&doc, id, &view_mode)
711                    .ok()
712                    .map(|view| (view, edge_type.clone()))
713            })
714            .collect();
715
716        Ok(NeighborhoodView {
717            position,
718            ancestors,
719            children,
720            siblings,
721            connections,
722        })
723    }
724
725    // ==================== Path Finding ====================
726
727    /// Find a path between two blocks.
728    pub fn find_path(
729        &self,
730        session_id: &AgentSessionId,
731        from: BlockId,
732        to: BlockId,
733        max_length: Option<usize>,
734    ) -> Result<Vec<BlockId>> {
735        self.circuit_breaker.can_proceed()?;
736        let _guard = self.depth_guard.try_enter()?;
737        let start = Instant::now();
738
739        let sessions = self
740            .sessions
741            .read()
742            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
743
744        let session = sessions
745            .get(session_id)
746            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
747
748        session.check_can_traverse()?;
749
750        let doc = self
751            .document
752            .read()
753            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
754
755        // Simple BFS path finding
756        let max_depth = max_length.unwrap_or(10);
757        let path = self.bfs_path(&doc, &from, &to, max_depth)?;
758
759        // Update metrics
760        drop(sessions);
761        let mut sessions_mut = self
762            .sessions
763            .write()
764            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
765
766        if let Some(session) = sessions_mut.get_mut(session_id) {
767            session.metrics.record_traversal();
768            session.budget.record_traversal();
769            session.metrics.record_execution_time(start.elapsed());
770            session.touch();
771        }
772
773        Ok(path)
774    }
775
776    // ==================== Context Operations ====================
777
778    /// Add a block to the context window.
779    pub fn context_add(
780        &self,
781        session_id: &AgentSessionId,
782        block_id: BlockId,
783        _reason: Option<String>,
784        _relevance: Option<f32>,
785    ) -> Result<()> {
786        let doc = self
787            .document
788            .read()
789            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
790        if doc.get_block(&block_id).is_none() {
791            return Err(AgentError::BlockNotFound(block_id));
792        }
793        let codegraph_doc = is_codegraph_document(&doc);
794
795        let mut sessions = self
796            .sessions
797            .write()
798            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
799
800        let session = sessions
801            .get_mut(session_id)
802            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
803
804        session.check_can_modify_context()?;
805        session.context_blocks.insert(block_id);
806        if codegraph_doc {
807            session.ensure_codegraph_context().select_block(
808                &doc,
809                block_id,
810                CodeGraphDetailLevel::SymbolCard,
811            );
812        }
813
814        session.metrics.record_context_add(1);
815        session.touch();
816        Ok(())
817    }
818
819    /// Add all last results to context.
820    pub fn context_add_results(&self, session_id: &AgentSessionId) -> Result<Vec<BlockId>> {
821        let doc = self
822            .document
823            .read()
824            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
825        let codegraph_doc = is_codegraph_document(&doc);
826
827        let mut sessions = self
828            .sessions
829            .write()
830            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
831
832        let session = sessions
833            .get_mut(session_id)
834            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
835
836        session.check_can_modify_context()?;
837
838        let results = session.get_last_results()?.to_vec();
839        for block_id in &results {
840            session.context_blocks.insert(*block_id);
841            if codegraph_doc {
842                session.ensure_codegraph_context().select_block(
843                    &doc,
844                    *block_id,
845                    CodeGraphDetailLevel::SymbolCard,
846                );
847            }
848        }
849        session.metrics.record_context_add(results.len());
850        session.touch();
851
852        Ok(results)
853    }
854
855    /// Remove a block from context.
856    pub fn context_remove(&self, session_id: &AgentSessionId, block_id: BlockId) -> Result<()> {
857        let mut sessions = self
858            .sessions
859            .write()
860            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
861
862        let session = sessions
863            .get_mut(session_id)
864            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
865
866        session.check_can_modify_context()?;
867        session.context_blocks.remove(&block_id);
868        if let Some(context) = session.codegraph_context.as_mut() {
869            context.remove_block(block_id);
870        }
871        session.metrics.record_context_remove();
872        session.touch();
873
874        Ok(())
875    }
876
877    /// Clear the context window.
878    pub fn context_clear(&self, session_id: &AgentSessionId) -> Result<()> {
879        let mut sessions = self
880            .sessions
881            .write()
882            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
883
884        let session = sessions
885            .get_mut(session_id)
886            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
887
888        session.check_can_modify_context()?;
889        session.context_blocks.clear();
890        if let Some(context) = session.codegraph_context.as_mut() {
891            context.clear();
892        }
893        session.touch();
894
895        Ok(())
896    }
897
898    /// Set focus block.
899    pub fn context_focus(
900        &self,
901        session_id: &AgentSessionId,
902        block_id: Option<BlockId>,
903    ) -> Result<()> {
904        let doc = self
905            .document
906            .read()
907            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
908        if let Some(block_id) = block_id {
909            if doc.get_block(&block_id).is_none() {
910                return Err(AgentError::BlockNotFound(block_id));
911            }
912        }
913        let codegraph_doc = is_codegraph_document(&doc);
914
915        let mut sessions = self
916            .sessions
917            .write()
918            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
919
920        let session = sessions
921            .get_mut(session_id)
922            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
923
924        session.check_can_modify_context()?;
925        session.set_focus(block_id);
926        if codegraph_doc {
927            session.ensure_codegraph_context().set_focus(&doc, block_id);
928        }
929        session.touch();
930
931        Ok(())
932    }
933
934    pub fn codegraph_seed_overview(
935        &self,
936        session_id: &AgentSessionId,
937    ) -> Result<CodeGraphContextUpdate> {
938        let doc = self
939            .document
940            .read()
941            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
942        if !is_codegraph_document(&doc) {
943            return Err(AgentError::Internal(
944                "document is not a codegraph".to_string(),
945            ));
946        }
947
948        let mut sessions = self
949            .sessions
950            .write()
951            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
952        let session = sessions
953            .get_mut(session_id)
954            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
955        session.check_can_modify_context()?;
956
957        let update = session.ensure_codegraph_context().seed_overview(&doc);
958        for block_id in session.ensure_codegraph_context().selected_block_ids() {
959            session.context_blocks.insert(block_id);
960        }
961        session.focus_block = update.focus;
962        session.touch();
963        Ok(update)
964    }
965
966    pub fn codegraph_expand_file(
967        &self,
968        session_id: &AgentSessionId,
969        block_id: BlockId,
970    ) -> Result<CodeGraphContextUpdate> {
971        self.codegraph_update(session_id, |doc, context| {
972            context.expand_file(doc, block_id)
973        })
974    }
975
976    pub fn codegraph_expand_dependencies(
977        &self,
978        session_id: &AgentSessionId,
979        block_id: BlockId,
980        relation_filter: Option<&str>,
981    ) -> Result<CodeGraphContextUpdate> {
982        self.codegraph_update(session_id, |doc, context| {
983            context.expand_dependencies(doc, block_id, relation_filter)
984        })
985    }
986
987    pub fn codegraph_expand_dependents(
988        &self,
989        session_id: &AgentSessionId,
990        block_id: BlockId,
991        relation_filter: Option<&str>,
992    ) -> Result<CodeGraphContextUpdate> {
993        self.codegraph_update(session_id, |doc, context| {
994            context.expand_dependents(doc, block_id, relation_filter)
995        })
996    }
997
998    pub fn codegraph_hydrate_source(
999        &self,
1000        session_id: &AgentSessionId,
1001        block_id: BlockId,
1002        padding: usize,
1003    ) -> Result<CodeGraphContextUpdate> {
1004        self.codegraph_update(session_id, |doc, context| {
1005            context.hydrate_source(doc, block_id, padding)
1006        })
1007    }
1008
1009    pub fn codegraph_collapse(
1010        &self,
1011        session_id: &AgentSessionId,
1012        block_id: BlockId,
1013        include_descendants: bool,
1014    ) -> Result<CodeGraphContextUpdate> {
1015        self.codegraph_update(session_id, |doc, context| {
1016            context.collapse(doc, block_id, include_descendants)
1017        })
1018    }
1019
1020    pub fn render_codegraph_context(
1021        &self,
1022        session_id: &AgentSessionId,
1023        config: CodeGraphRenderConfig,
1024    ) -> Result<String> {
1025        let doc = self
1026            .document
1027            .read()
1028            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
1029        if !is_codegraph_document(&doc) {
1030            return Err(AgentError::Internal(
1031                "document is not a codegraph".to_string(),
1032            ));
1033        }
1034
1035        let sessions = self
1036            .sessions
1037            .read()
1038            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
1039        let session = sessions
1040            .get(session_id)
1041            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
1042        let context = session.codegraph_context.as_ref().ok_or_else(|| {
1043            AgentError::Internal("codegraph context has not been initialized".to_string())
1044        })?;
1045        Ok(render_codegraph_context_prompt(&doc, context, &config))
1046    }
1047
1048    // ==================== Internal Helpers ====================
1049
1050    fn codegraph_update<F>(
1051        &self,
1052        session_id: &AgentSessionId,
1053        update_fn: F,
1054    ) -> Result<CodeGraphContextUpdate>
1055    where
1056        F: FnOnce(&Document, &mut ucp_codegraph::CodeGraphContextSession) -> CodeGraphContextUpdate,
1057    {
1058        let doc = self
1059            .document
1060            .read()
1061            .map_err(|_| AgentError::Internal("Failed to acquire document lock".to_string()))?;
1062        if !is_codegraph_document(&doc) {
1063            return Err(AgentError::Internal(
1064                "document is not a codegraph".to_string(),
1065            ));
1066        }
1067
1068        let mut sessions = self
1069            .sessions
1070            .write()
1071            .map_err(|_| AgentError::Internal("Failed to acquire sessions lock".to_string()))?;
1072        let session = sessions
1073            .get_mut(session_id)
1074            .ok_or_else(|| AgentError::SessionNotFound(session_id.clone()))?;
1075        session.check_can_modify_context()?;
1076
1077        let update = update_fn(&doc, session.ensure_codegraph_context());
1078        if let Some(context) = session.codegraph_context.as_ref() {
1079            session.context_blocks = context.selected.keys().copied().collect();
1080        }
1081        session.focus_block = update.focus;
1082        session.touch();
1083        Ok(update)
1084    }
1085
1086    fn compute_neighborhood(
1087        &self,
1088        doc: &Document,
1089        position: &BlockId,
1090    ) -> Result<CursorNeighborhood> {
1091        let mut neighborhood = CursorNeighborhood::new();
1092
1093        // Get ancestors
1094        let mut current = *position;
1095        for _ in 0..5 {
1096            if let Some(parent) = doc.parent(&current) {
1097                neighborhood.ancestors.push(*parent);
1098                current = *parent;
1099            } else {
1100                break;
1101            }
1102        }
1103
1104        // Get children
1105        neighborhood.children = doc.children(position).to_vec();
1106
1107        // Get siblings
1108        if let Some(parent) = doc.parent(position) {
1109            neighborhood.siblings = doc
1110                .children(parent)
1111                .iter()
1112                .filter(|id| *id != position)
1113                .copied()
1114                .collect();
1115        }
1116
1117        // Get semantic connections via edge index
1118        for (edge_type, target) in doc.edge_index.outgoing_from(position) {
1119            neighborhood.connections.push((*target, edge_type.clone()));
1120        }
1121
1122        neighborhood.stale = false;
1123        Ok(neighborhood)
1124    }
1125
1126    fn view_block_internal(
1127        &self,
1128        doc: &Document,
1129        block_id: &BlockId,
1130        mode: &ViewMode,
1131    ) -> Result<BlockView> {
1132        let block = doc
1133            .get_block(block_id)
1134            .ok_or(AgentError::BlockNotFound(*block_id))?;
1135
1136        let content = match mode {
1137            ViewMode::IdsOnly => None,
1138            ViewMode::Preview { length } => {
1139                let text = self.extract_content_text(&block.content);
1140                Some(text.chars().take(*length).collect())
1141            }
1142            ViewMode::Full => Some(self.extract_content_text(&block.content)),
1143            ViewMode::Metadata => None,
1144            ViewMode::Adaptive { .. } => Some(self.extract_content_text(&block.content)),
1145        };
1146
1147        let outgoing_edges = doc.edge_index.outgoing_from(block_id).len();
1148        let incoming_edges = doc.edge_index.incoming_to(block_id).len();
1149
1150        Ok(BlockView {
1151            block_id: *block_id,
1152            content,
1153            role: block
1154                .metadata
1155                .semantic_role
1156                .as_ref()
1157                .map(|r| r.category.as_str().to_string()),
1158            tags: block.metadata.tags.clone(),
1159            children_count: doc.children(block_id).len(),
1160            incoming_edges,
1161            outgoing_edges,
1162        })
1163    }
1164
1165    fn extract_content_text(&self, content: &ucm_core::Content) -> String {
1166        match content {
1167            ucm_core::Content::Text(t) => t.text.clone(),
1168            ucm_core::Content::Code(c) => c.source.clone(),
1169            ucm_core::Content::Table(t) => format!("Table: {} rows", t.rows.len()),
1170            ucm_core::Content::Math(m) => m.expression.clone(),
1171            ucm_core::Content::Media(m) => {
1172                m.alt_text.clone().unwrap_or_else(|| "Media".to_string())
1173            }
1174            ucm_core::Content::Json { .. } => "JSON data".to_string(),
1175            ucm_core::Content::Binary { .. } => "Binary data".to_string(),
1176            ucm_core::Content::Composite { children, .. } => {
1177                format!("Composite: {} children", children.len())
1178            }
1179        }
1180    }
1181
1182    fn build_traversal_filter(&self, options: &ExpandOptions) -> TraversalFilter {
1183        let mut filter = TraversalFilter::default();
1184
1185        if let Some(ref roles) = options.roles {
1186            filter.include_roles = roles.clone();
1187        }
1188
1189        if let Some(ref tags) = options.tags {
1190            filter.include_tags = tags.clone();
1191        }
1192
1193        filter
1194    }
1195
1196    fn expand_down(
1197        &self,
1198        doc: &Document,
1199        block_id: &BlockId,
1200        depth: usize,
1201        filter: &TraversalFilter,
1202    ) -> Result<Vec<Vec<BlockId>>> {
1203        let engine = TraversalEngine::new();
1204        let result = engine
1205            .navigate(
1206                doc,
1207                Some(*block_id),
1208                NavigateDirection::BreadthFirst,
1209                Some(depth),
1210                Some(filter.clone()),
1211                TraversalOutput::StructureOnly,
1212            )
1213            .map_err(|e| AgentError::EngineError(e.to_string()))?;
1214
1215        // Group by depth level
1216        let mut levels: Vec<Vec<BlockId>> = vec![Vec::new(); depth + 1];
1217        for node in result.nodes {
1218            if node.depth <= depth {
1219                levels[node.depth].push(node.id);
1220            }
1221        }
1222
1223        // Remove empty trailing levels
1224        while levels.last().map(|l| l.is_empty()).unwrap_or(false) {
1225            levels.pop();
1226        }
1227
1228        Ok(levels)
1229    }
1230
1231    fn expand_up(
1232        &self,
1233        doc: &Document,
1234        block_id: &BlockId,
1235        depth: usize,
1236    ) -> Result<Vec<Vec<BlockId>>> {
1237        let mut levels = Vec::new();
1238        let mut current = *block_id;
1239
1240        for _ in 0..depth {
1241            if let Some(parent) = doc.parent(&current) {
1242                levels.push(vec![*parent]);
1243                current = *parent;
1244            } else {
1245                break;
1246            }
1247        }
1248
1249        Ok(levels)
1250    }
1251
1252    fn expand_semantic(
1253        &self,
1254        doc: &Document,
1255        block_id: &BlockId,
1256        depth: usize,
1257    ) -> Result<Vec<Vec<BlockId>>> {
1258        let mut levels = Vec::new();
1259        let mut visited = std::collections::HashSet::new();
1260        let mut current_level = vec![*block_id];
1261        visited.insert(*block_id);
1262
1263        for _ in 0..depth {
1264            let mut next_level = Vec::new();
1265
1266            for id in &current_level {
1267                for (_, target) in doc.edge_index.outgoing_from(id) {
1268                    if !visited.contains(target) {
1269                        visited.insert(*target);
1270                        next_level.push(*target);
1271                    }
1272                }
1273            }
1274
1275            if next_level.is_empty() {
1276                break;
1277            }
1278
1279            levels.push(next_level.clone());
1280            current_level = next_level;
1281        }
1282
1283        Ok(levels)
1284    }
1285
1286    fn bfs_path(
1287        &self,
1288        doc: &Document,
1289        from: &BlockId,
1290        to: &BlockId,
1291        max_depth: usize,
1292    ) -> Result<Vec<BlockId>> {
1293        use std::collections::{HashSet, VecDeque};
1294
1295        if from == to {
1296            return Ok(vec![*from]);
1297        }
1298
1299        let mut visited = HashSet::new();
1300        let mut queue = VecDeque::new();
1301        let mut parent_map: HashMap<BlockId, BlockId> = HashMap::new();
1302
1303        queue.push_back((*from, 0));
1304        visited.insert(*from);
1305
1306        while let Some((current, depth)) = queue.pop_front() {
1307            if depth >= max_depth {
1308                continue;
1309            }
1310
1311            // Check children
1312            for child in doc.children(&current) {
1313                if !visited.contains(child) {
1314                    visited.insert(*child);
1315                    parent_map.insert(*child, current);
1316
1317                    if child == to {
1318                        // Reconstruct path
1319                        let mut path = vec![*to];
1320                        let mut c = *to;
1321                        while let Some(p) = parent_map.get(&c) {
1322                            path.push(*p);
1323                            c = *p;
1324                        }
1325                        path.reverse();
1326                        return Ok(path);
1327                    }
1328
1329                    queue.push_back((*child, depth + 1));
1330                }
1331            }
1332
1333            // Check parent
1334            if let Some(parent) = doc.parent(&current) {
1335                if !visited.contains(parent) {
1336                    visited.insert(*parent);
1337                    parent_map.insert(*parent, current);
1338
1339                    if parent == to {
1340                        let mut path = vec![*to];
1341                        let mut c = *to;
1342                        while let Some(p) = parent_map.get(&c) {
1343                            path.push(*p);
1344                            c = *p;
1345                        }
1346                        path.reverse();
1347                        return Ok(path);
1348                    }
1349
1350                    queue.push_back((*parent, depth + 1));
1351                }
1352            }
1353
1354            // Check semantic edges
1355            for (_, target) in doc.edge_index.outgoing_from(&current) {
1356                if !visited.contains(target) {
1357                    visited.insert(*target);
1358                    parent_map.insert(*target, current);
1359
1360                    if target == to {
1361                        let mut path = vec![*to];
1362                        let mut c = *to;
1363                        while let Some(p) = parent_map.get(&c) {
1364                            path.push(*p);
1365                            c = *p;
1366                        }
1367                        path.reverse();
1368                        return Ok(path);
1369                    }
1370
1371                    queue.push_back((*target, depth + 1));
1372                }
1373            }
1374        }
1375
1376        Err(AgentError::NoPathExists {
1377            from: *from,
1378            to: *to,
1379        })
1380    }
1381}
1382
1383#[cfg(test)]
1384mod tests {
1385    use super::*;
1386
1387    fn create_test_document() -> Document {
1388        Document::create()
1389    }
1390
1391    #[test]
1392    fn test_create_session() {
1393        let doc = create_test_document();
1394        let traversal = AgentTraversal::new(doc);
1395
1396        let session_id = traversal.create_session(SessionConfig::default()).unwrap();
1397        assert!(!session_id.0.is_nil());
1398    }
1399
1400    #[test]
1401    fn test_close_session() {
1402        let doc = create_test_document();
1403        let traversal = AgentTraversal::new(doc);
1404
1405        let session_id = traversal.create_session(SessionConfig::default()).unwrap();
1406        assert!(traversal.close_session(&session_id).is_ok());
1407
1408        // Session should be removed
1409        assert!(traversal.close_session(&session_id).is_err());
1410    }
1411
1412    #[test]
1413    fn test_max_sessions_limit() {
1414        let doc = create_test_document();
1415        let traversal = AgentTraversal::new(doc).with_global_limits(GlobalLimits {
1416            max_sessions: 2,
1417            ..Default::default()
1418        });
1419
1420        // Create 2 sessions
1421        let _ = traversal.create_session(SessionConfig::default()).unwrap();
1422        let _ = traversal.create_session(SessionConfig::default()).unwrap();
1423
1424        // Third should fail
1425        let result = traversal.create_session(SessionConfig::default());
1426        assert!(matches!(
1427            result,
1428            Err(AgentError::MaxSessionsReached { max: 2 })
1429        ));
1430    }
1431}