An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.

feat(tools): register CodeSearchTool in V2 registry

Implements Task 2 from Phase 5: Add CodeSearchTool to create_v2_registry with
project_root parameter. Update all call sites (orchestrator and test files)
to pass project_root path.

+154 -3
+1
src/agent/orchestrator.rs
··· 825 825 self.graph_store.clone(), 826 826 Some(self.message_bus.clone()), 827 827 Some(worker_id.clone()), 828 + self.project_path.clone(), 828 829 ); 829 830 830 831 // Create AgentRuntime
+6
src/tools/factory.rs
··· 11 11 AddEdgeTool, ChooseOptionTool, ClaimTaskTool, CreateNodeTool, LogDecisionTool, QueryNodesTool, 12 12 RecordObservationTool, RecordOutcomeTool, RevisitTool, SearchNodesTool, UpdateNodeTool, 13 13 }; 14 + use crate::tools::search::CodeSearchTool; 14 15 use crate::tools::shell::RunCommandTool; 15 16 use crate::tools::signal::SignalTool; 17 + use std::path::PathBuf; 16 18 use std::sync::Arc; 17 19 18 20 pub fn create_default_registry( ··· 52 54 graph_store: Arc<dyn GraphStore>, 53 55 message_bus: Option<Arc<dyn MessageBus>>, 54 56 agent_id: Option<AgentId>, 57 + project_root: PathBuf, 55 58 ) -> ToolRegistry { 56 59 let registry = create_default_registry(validator, permission_handler); 57 60 ··· 70 73 71 74 // Register context tools 72 75 registry.register(Arc::new(ReadAgentsMdTool::new())); 76 + 77 + // Register search tools 78 + registry.register(Arc::new(CodeSearchTool::new(project_root))); 73 79 74 80 // Register agent tools (only in multi-agent mode) 75 81 if let (Some(bus), Some(id)) = (message_bus, agent_id) {
+6 -1
tests/agent_tools_test.rs
··· 448 448 let validator = Arc::new(SecurityValidator::new(security_config).unwrap()); 449 449 let permission_handler = Arc::new(AutoApproveHandler); 450 450 451 + let project_root = std::path::PathBuf::from("/tmp"); 452 + 451 453 let registry = create_v2_registry( 452 454 validator, 453 455 permission_handler, 454 456 graph_store, 455 457 Some(message_bus), 456 458 Some("worker-1".to_string()), 459 + project_root, 457 460 ); 458 461 459 462 let tools = registry.list(); ··· 483 486 let validator = Arc::new(SecurityValidator::new(security_config).unwrap()); 484 487 let permission_handler = Arc::new(AutoApproveHandler); 485 488 486 - let registry = create_v2_registry(validator, permission_handler, graph_store, None, None); 489 + let project_root = std::path::PathBuf::from("/tmp"); 490 + 491 + let registry = create_v2_registry(validator, permission_handler, graph_store, None, None, project_root); 487 492 488 493 let tools = registry.list(); 489 494 assert!(!tools.contains(&"spawn_sub_agent".to_string()));
+6 -2
tests/graph_tools_test.rs
··· 662 662 Arc::new(SecurityValidator::new(security_config).expect("Failed to create validator")); 663 663 let permission_handler = Arc::new(AutoApproveHandler); 664 664 665 + let project_root = std::path::PathBuf::from("/tmp"); 666 + 665 667 // Create the v2 registry 666 - let registry = create_v2_registry(validator, permission_handler, graph_store, None, None); 668 + let registry = create_v2_registry(validator, permission_handler, graph_store, None, None, project_root); 667 669 668 - // Expected tool names: graph tools + legacy tools + context tools 670 + // Expected tool names: graph tools + legacy tools + context tools + search tools 669 671 let expected_tools = vec![ 670 672 // Graph tools 671 673 "create_node", ··· 687 689 "signal_completion", 688 690 // Context tools 689 691 "read_agents_md", 692 + // Search tools 693 + "code_search", 690 694 ]; 691 695 692 696 // Get all registered tool names
+135
tests/orchestrator_test.rs
··· 630 630 let next_state = orchestrator.handle_scheduling().await.unwrap(); 631 631 assert_eq!(next_state, OrchestratorState::Completing); 632 632 } 633 + 634 + // ===== Phase 5 Error Recovery & Task Reassignment Tests ===== 635 + 636 + /// Integration test: Full retry-cascade-unblock lifecycle 637 + /// Verifies v2-phase5.AC11.1, v2-phase5.AC12.1, v2-phase5.AC13.1 638 + #[tokio::test] 639 + async fn test_retry_cascade_unblock_lifecycle() { 640 + let (_, graph_store) = common::setup_test_env().await.unwrap(); 641 + let graph_store: Arc<dyn rustagent::graph::store::GraphStore> = Arc::new(graph_store); 642 + let mock_client = Arc::new(MockLlmClient::new()); 643 + 644 + let config = OrchestratorConfig { 645 + max_retries_per_task: 2, 646 + ..Default::default() 647 + }; 648 + let mut orchestrator = create_test_orchestrator(config, graph_store.clone(), mock_client); 649 + orchestrator.set_goal_id(Some("ra-p5-test".to_string())); 650 + 651 + // Create goal node 652 + let goal_node = common::create_test_goal("ra-p5-test", "proj-1", "Phase 5 test goal"); 653 + graph_store.create_node(&goal_node).await.unwrap(); 654 + 655 + // Create task A (Ready status) 656 + let task_a = common::create_test_task("ra-p5-test.1", "proj-1", "Task A", rustagent::graph::NodeStatus::Ready); 657 + graph_store.create_node(&task_a).await.unwrap(); 658 + 659 + // Create task B that DependsOn A (Ready status) 660 + let task_b = common::create_test_task("ra-p5-test.2", "proj-1", "Task B", rustagent::graph::NodeStatus::Ready); 661 + graph_store.create_node(&task_b).await.unwrap(); 662 + 663 + // Create DependsOn edge: B depends on A 664 + let depends_edge = rustagent::graph::GraphEdge { 665 + id: "e-depends1".to_string(), 666 + edge_type: rustagent::graph::EdgeType::DependsOn, 667 + from_node: "ra-p5-test.2".to_string(), // B 668 + to_node: "ra-p5-test.1".to_string(), // A 669 + label: None, 670 + created_at: chrono::Utc::now(), 671 + }; 672 + graph_store.add_edge(&depends_edge).await.unwrap(); 673 + 674 + // Create Contains edges to goal 675 + graph_store.add_edge(&rustagent::graph::GraphEdge { 676 + id: "e-contains1".to_string(), 677 + edge_type: rustagent::graph::EdgeType::Contains, 678 + from_node: "ra-p5-test".to_string(), 679 + to_node: "ra-p5-test.1".to_string(), 680 + label: None, 681 + created_at: chrono::Utc::now(), 682 + }).await.unwrap(); 683 + 684 + graph_store.add_edge(&rustagent::graph::GraphEdge { 685 + id: "e-contains2".to_string(), 686 + edge_type: rustagent::graph::EdgeType::Contains, 687 + from_node: "ra-p5-test".to_string(), 688 + to_node: "ra-p5-test.2".to_string(), 689 + label: None, 690 + created_at: chrono::Utc::now(), 691 + }).await.unwrap(); 692 + 693 + // === Step 1: Simulate Task A failing (first attempt) === 694 + let error_msg_1 = "First failure: database connection timeout"; 695 + orchestrator.handle_task_retry_or_fail("ra-p5-test.1", error_msg_1).await.unwrap(); 696 + 697 + // Verify Task A is retried (Ready state) with previous_attempt in metadata 698 + let task_a_after_retry1 = graph_store.get_node("ra-p5-test.1").await.unwrap().unwrap(); 699 + assert_eq!(task_a_after_retry1.status, rustagent::graph::NodeStatus::Ready); 700 + assert_eq!( 701 + task_a_after_retry1.metadata.get("previous_attempt"), 702 + Some(&error_msg_1.to_string()) 703 + ); 704 + assert_eq!( 705 + task_a_after_retry1.metadata.get("retry_count"), 706 + Some(&"1".to_string()) 707 + ); 708 + 709 + // Verify Task B is still Ready (not blocked yet since A is being retried) 710 + let task_b_check1 = graph_store.get_node("ra-p5-test.2").await.unwrap().unwrap(); 711 + assert_eq!(task_b_check1.status, rustagent::graph::NodeStatus::Ready); 712 + 713 + // === Step 2: Simulate Task A failing again (second attempt, exceeding max_retries) === 714 + let error_msg_2 = "Second failure: network unreachable"; 715 + orchestrator.handle_task_retry_or_fail("ra-p5-test.1", error_msg_2).await.unwrap(); 716 + 717 + // Verify Task A is now Failed (retries exhausted) 718 + let task_a_after_fail = graph_store.get_node("ra-p5-test.1").await.unwrap().unwrap(); 719 + assert_eq!(task_a_after_fail.status, rustagent::graph::NodeStatus::Failed); 720 + assert_eq!(task_a_after_fail.blocked_reason, Some(error_msg_2.to_string())); 721 + 722 + // Verify an Observation node was created for the failure 723 + let obs_nodes = graph_store.query_nodes(&rustagent::graph::store::NodeQuery { 724 + node_type: Some(rustagent::graph::NodeType::Observation), 725 + status: None, 726 + project_id: None, 727 + parent_id: None, 728 + query: None, 729 + }).await.unwrap(); 730 + assert!(!obs_nodes.is_empty(), "Expected an Observation node for task failure"); 731 + 732 + // Verify Task B is now Blocked (cascade occurred) 733 + let task_b_after_cascade = graph_store.get_node("ra-p5-test.2").await.unwrap().unwrap(); 734 + assert_eq!(task_b_after_cascade.status, rustagent::graph::NodeStatus::Blocked); 735 + assert!( 736 + task_b_after_cascade.blocked_reason.as_ref().map(|r| r.contains("ra-p5-test.1")).unwrap_or(false), 737 + "Task B should be blocked by A: {:?}", 738 + task_b_after_cascade.blocked_reason 739 + ); 740 + assert_eq!( 741 + task_b_after_cascade.metadata.get("blocker_task_id"), 742 + Some(&"ra-p5-test.1".to_string()) 743 + ); 744 + 745 + // === Step 3: Manually complete Task A (simulate external fix) === 746 + graph_store.update_node( 747 + "ra-p5-test.1", 748 + Some(rustagent::graph::NodeStatus::Completed), 749 + None, 750 + None, 751 + None, 752 + None, 753 + ).await.unwrap(); 754 + 755 + // Verify Task A is Completed 756 + let task_a_completed = graph_store.get_node("ra-p5-test.1").await.unwrap().unwrap(); 757 + assert_eq!(task_a_completed.status, rustagent::graph::NodeStatus::Completed); 758 + 759 + // === Step 4: Call handle_scheduling which triggers try_unblock_tasks === 760 + orchestrator.handle_scheduling().await.unwrap(); 761 + 762 + // Verify Task B is now Ready (unblocked) 763 + let task_b_unblocked = graph_store.get_node("ra-p5-test.2").await.unwrap().unwrap(); 764 + assert_eq!(task_b_unblocked.status, rustagent::graph::NodeStatus::Ready); 765 + assert!(task_b_unblocked.blocked_reason.is_none(), "Blocked reason should be cleared"); 766 + assert!(!task_b_unblocked.metadata.contains_key("blocker_task_id"), "blocker_task_id should be removed"); 767 + }