1use 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#[derive(Debug, Clone)]
20pub struct NavigationResult {
21 pub position: BlockId,
23 pub refreshed: bool,
25 pub neighborhood: CursorNeighborhood,
27}
28
29#[derive(Debug, Clone, Default)]
31pub struct ExpandOptions {
32 pub depth: usize,
34 pub view_mode: ViewMode,
36 pub roles: Option<Vec<String>>,
38 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#[derive(Debug, Clone)]
73pub struct ExpansionResult {
74 pub root: BlockId,
76 pub levels: Vec<Vec<BlockId>>,
78 pub total_blocks: usize,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ExpandDirection {
85 Down,
87 Up,
89 Both,
91 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#[derive(Debug, Clone, Default)]
108pub struct SearchOptions {
109 pub limit: usize,
111 pub min_similarity: f32,
113 pub roles: Option<Vec<String>>,
115 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#[derive(Debug, Clone)]
142pub struct FindResult {
143 pub matches: Vec<BlockId>,
145 pub total_searched: usize,
147}
148
149#[derive(Debug, Clone)]
151pub struct BlockView {
152 pub block_id: BlockId,
154 pub content: Option<String>,
156 pub role: Option<String>,
158 pub tags: Vec<String>,
160 pub children_count: usize,
162 pub incoming_edges: usize,
164 pub outgoing_edges: usize,
166}
167
168#[derive(Debug, Clone)]
170pub struct NeighborhoodView {
171 pub position: BlockId,
173 pub ancestors: Vec<BlockView>,
175 pub children: Vec<BlockView>,
177 pub siblings: Vec<BlockView>,
179 pub connections: Vec<(BlockView, EdgeType)>,
181}
182
183pub struct AgentTraversal {
185 sessions: RwLock<HashMap<AgentSessionId, AgentSession>>,
187 document: Arc<RwLock<Document>>,
189 rag_provider: Option<Arc<dyn RagProvider>>,
191 global_limits: GlobalLimits,
193 circuit_breaker: CircuitBreaker,
195 depth_guard: DepthGuard,
197}
198
199impl AgentTraversal {
200 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 pub fn with_rag_provider(mut self, provider: Arc<dyn RagProvider>) -> Self {
214 self.rag_provider = Some(provider);
215 self
216 }
217
218 pub fn with_global_limits(mut self, limits: GlobalLimits) -> Self {
220 self.global_limits = limits;
221 self
222 }
223
224 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 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 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 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 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 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 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 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 session.cursor.move_to(target);
344 session.touch();
345 session.metrics.record_navigation();
346 session.budget.record_traversal();
347
348 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 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 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 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 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 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 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 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 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 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 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 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 if let Some(t) = tag {
586 if !block.metadata.tags.contains(&t.to_string()) {
587 continue;
588 }
589 }
590
591 if let Some(l) = label {
593 if block.metadata.label.as_deref() != Some(l) {
594 continue;
595 }
596 }
597
598 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 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 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 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 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 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 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 let max_depth = max_length.unwrap_or(10);
757 let path = self.bfs_path(&doc, &from, &to, max_depth)?;
758
759 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 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 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 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 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 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 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 let mut current = *position;
1095 for _ in 0..5 {
1096 if let Some(parent) = doc.parent(¤t) {
1097 neighborhood.ancestors.push(*parent);
1098 current = *parent;
1099 } else {
1100 break;
1101 }
1102 }
1103
1104 neighborhood.children = doc.children(position).to_vec();
1106
1107 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 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 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 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(¤t) {
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 ¤t_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 for child in doc.children(¤t) {
1313 if !visited.contains(child) {
1314 visited.insert(*child);
1315 parent_map.insert(*child, current);
1316
1317 if child == to {
1318 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 if let Some(parent) = doc.parent(¤t) {
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 for (_, target) in doc.edge_index.outgoing_from(¤t) {
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 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 let _ = traversal.create_session(SessionConfig::default()).unwrap();
1422 let _ = traversal.create_session(SessionConfig::default()).unwrap();
1423
1424 let result = traversal.create_session(SessionConfig::default());
1426 assert!(matches!(
1427 result,
1428 Err(AgentError::MaxSessionsReached { max: 2 })
1429 ));
1430 }
1431}