bamboo_agent/agent/core/tools/agentic.rs
1//! Agentic tool execution framework.
2//!
3//! This module provides a framework for implementing autonomous agent tools
4//! that can iterate and interact with the environment through tool calls.
5//!
6//! # Overview
7//!
8//! The agentic system allows tools to:
9//! - Execute multiple iterations toward a goal
10//! - Maintain state across iterations
11//! - Record interactions between user, assistant, and tools
12//! - Perform autonomous code review and refinement
13//!
14//! # Key Types
15//!
16//! - [`ToolGoal`] - Defines the objective and parameters for an agentic tool
17//! - [`AgenticContext`] - Execution context with state and interaction history
18//! - [`Interaction`] - Records of user/assistant/tool exchanges
19//! - [`ToolExecutor`] - Trait for executing tool calls
20//! - [`AgenticTool`] - Trait for autonomous agentic tools
21//!
22//! # Example
23//!
24//! ```rust,ignore
25//! use bamboo_agent::agent::core::tools::agentic::*;
26//!
27//! let goal = ToolGoal::new("Review code for bugs", json!({"file": "main.rs"}));
28//! let context = AgenticContext::new(executor);
29//! let tool = SmartCodeReviewTool::new();
30//! let result = tool.execute(goal, context).await?;
31//! ```
32
33use async_trait::async_trait;
34use chrono::{DateTime, Utc};
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37use std::sync::Arc;
38use tokio::sync::RwLock;
39
40use crate::agent::core::tools::{FunctionCall, ToolCall, ToolError};
41
42/// Represents the objective and parameters for an agentic tool execution.
43///
44/// A `ToolGoal` defines what an agentic tool should accomplish, including
45/// the intent, input parameters, and iteration limits.
46///
47/// # Fields
48///
49/// * `intent` - High-level description of what the tool should achieve
50/// * `params` - JSON parameters for the tool execution
51/// * `max_iterations` - Maximum number of iterations before stopping (default: 10)
52///
53/// # Example
54///
55/// ```rust,ignore
56/// let goal = ToolGoal::new("Fix all bugs", json!({"files": ["main.rs", "lib.rs"]}))
57/// .with_max_iterations(20);
58/// ```
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ToolGoal {
61 pub intent: String,
62 /// Input parameters for the tool execution
63 pub params: Value,
64 /// Maximum iterations before stopping
65 pub max_iterations: usize,
66}
67
68impl ToolGoal {
69 /// Create a new tool goal with intent and parameters.
70 ///
71 /// Uses the default maximum iteration count of 10.
72 ///
73 /// # Arguments
74 ///
75 /// * `intent` - High-level description of the goal
76 /// * `params` - JSON parameters for execution
77 ///
78 /// # Example
79 ///
80 /// ```rust,ignore
81 /// let goal = ToolGoal::new("Review code", json!({"path": "src/main.rs"}));
82 /// ```
83 pub fn new(intent: impl Into<String>, params: Value) -> Self {
84 Self {
85 intent: intent.into(),
86 params,
87 max_iterations: 10,
88 }
89 }
90
91 /// Set custom maximum iteration count.
92 ///
93 /// # Arguments
94 ///
95 /// * `max_iterations` - Maximum iterations allowed
96 ///
97 /// # Example
98 ///
99 /// ```rust,ignore
100 /// let goal = ToolGoal::new("Complex task", params)
101 /// .with_max_iterations(50);
102 /// ```
103 pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
104 self.max_iterations = max_iterations;
105 self
106 }
107}
108
109/// Role of an interaction participant.
110///
111/// Identifies who generated an interaction in the agentic loop.
112///
113/// # Variants
114///
115/// * `User` - Human user input
116/// * `Assistant` - AI assistant response
117/// * `Tool` - Tool execution result
118/// * `System` - System message
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
120#[serde(rename_all = "lowercase")]
121pub enum InteractionRole {
122 User,
123 Assistant,
124 Tool,
125 System,
126}
127
128/// Represents a single interaction in the agentic execution loop.
129///
130/// Records exchanges between user, assistant, and tools during
131/// autonomous agent execution.
132///
133/// # Variants
134///
135/// * `User` - User message with timestamp
136/// * `Assistant` - Assistant response with optional metadata
137/// * `ToolAction` - Tool call initiated
138/// * `ToolObservation` - Tool execution result
139/// * `System` - System message
140///
141/// # Example
142///
143/// ```rust,ignore
144/// let user_msg = Interaction::User {
145/// message: "Fix the bug".to_string(),
146/// timestamp: Utc::now(),
147/// };
148///
149/// let tool_action = Interaction::ToolAction {
150/// call: tool_call,
151/// timestamp: Utc::now(),
152/// };
153/// ```
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(tag = "type", rename_all = "snake_case")]
156pub enum Interaction {
157 /// User message
158 User {
159 /// Message content from user
160 message: String,
161 /// When the message was sent
162 timestamp: DateTime<Utc>,
163 },
164 /// Assistant response
165 Assistant {
166 /// Response message
167 message: String,
168 /// Optional metadata (e.g., confidence, reasoning)
169 metadata: Option<Value>,
170 /// When the response was generated
171 timestamp: DateTime<Utc>,
172 },
173 /// Tool call initiated
174 ToolAction {
175 /// The tool call being made
176 call: ToolCall,
177 /// When the call was initiated
178 timestamp: DateTime<Utc>,
179 },
180 /// Tool execution result
181 ToolObservation {
182 /// Name of the tool that was executed
183 tool_name: String,
184 /// Output/result from the tool
185 output: String,
186 /// When the observation was made
187 timestamp: DateTime<Utc>,
188 },
189 /// System message
190 System {
191 /// System message content
192 message: String,
193 /// When the message was generated
194 timestamp: DateTime<Utc>,
195 },
196}
197
198impl Interaction {
199 fn from_role(
200 role: InteractionRole,
201 content: impl Into<String>,
202 metadata: Option<Value>,
203 ) -> Self {
204 let content = content.into();
205 let timestamp = Utc::now();
206
207 match role {
208 InteractionRole::User => Self::User {
209 message: content,
210 timestamp,
211 },
212 InteractionRole::Assistant => Self::Assistant {
213 message: content,
214 metadata,
215 timestamp,
216 },
217 InteractionRole::Tool => Self::ToolObservation {
218 tool_name: "agentic_tool".to_string(),
219 output: content,
220 timestamp,
221 },
222 InteractionRole::System => Self::System {
223 message: content,
224 timestamp,
225 },
226 }
227 }
228}
229
230/// Execution context for agentic tools.
231///
232/// Maintains state, interaction history, and tool executor reference
233/// for autonomous agent execution loops.
234///
235/// # Fields
236///
237/// * `state` - Shared mutable state for cross-iteration persistence
238/// * `interaction_history` - Record of all interactions
239/// * `base_executor` - Tool executor for running tools
240///
241/// # Thread Safety
242///
243/// The `state` field uses `Arc<RwLock<Value>>` for thread-safe
244/// concurrent access from multiple async tasks.
245///
246/// # Example
247///
248/// ```rust,ignore
249/// let executor = Arc::new(BuiltinToolExecutor::new());
250/// let context = AgenticContext::new(executor);
251///
252/// // Record interactions
253/// context.record_interaction(InteractionRole::User, "Hello");
254///
255/// // Update state
256/// context.update_state(json!({"step": 1})).await;
257///
258/// // Check iterations
259/// if !context.increment_iteration(10) {
260/// // Continue execution
261/// }
262/// ```
263pub struct AgenticContext {
264 /// Shared mutable state (thread-safe)
265 pub state: Arc<RwLock<Value>>,
266 /// History of all interactions
267 pub interaction_history: Vec<Interaction>,
268 /// Tool executor reference
269 pub base_executor: Arc<dyn ToolExecutor>,
270 /// Current iteration count
271 iteration_count: usize,
272}
273
274impl AgenticContext {
275 /// Create a new agentic context with empty state.
276 ///
277 /// # Arguments
278 ///
279 /// * `base_executor` - Tool executor for running tools
280 ///
281 /// # Example
282 ///
283 /// ```rust,ignore
284 /// let executor = Arc::new(MyExecutor::new());
285 /// let context = AgenticContext::new(executor);
286 /// ```
287 pub fn new(base_executor: Arc<dyn ToolExecutor>) -> Self {
288 Self {
289 state: Arc::new(RwLock::new(serde_json::json!({}))),
290 interaction_history: Vec::new(),
291 base_executor,
292 iteration_count: 0,
293 }
294 }
295
296 /// Create context with custom initial state.
297 ///
298 /// # Arguments
299 ///
300 /// * `base_executor` - Tool executor
301 /// * `initial_state` - Initial state value
302 ///
303 /// # Example
304 ///
305 /// ```rust,ignore
306 /// let state = json!({"files": ["main.rs"]});
307 /// let context = AgenticContext::with_state(executor, state);
308 /// ```
309 pub fn with_state(base_executor: Arc<dyn ToolExecutor>, initial_state: Value) -> Self {
310 Self {
311 state: Arc::new(RwLock::new(initial_state)),
312 interaction_history: Vec::new(),
313 base_executor,
314 iteration_count: 0,
315 }
316 }
317
318 /// Record a simple interaction without metadata.
319 ///
320 /// # Arguments
321 ///
322 /// * `role` - Role of the interaction source
323 /// * `content` - Interaction content
324 pub fn record_interaction(&mut self, role: InteractionRole, content: impl Into<String>) {
325 self.interaction_history
326 .push(Interaction::from_role(role, content, None));
327 }
328
329 /// Record an interaction with metadata.
330 ///
331 /// # Arguments
332 ///
333 /// * `role` - Role of the interaction source
334 /// * `content` - Interaction content
335 /// * `metadata` - Additional metadata
336 pub fn record_interaction_with_metadata(
337 &mut self,
338 role: InteractionRole,
339 content: impl Into<String>,
340 metadata: Value,
341 ) {
342 self.interaction_history
343 .push(Interaction::from_role(role, content, Some(metadata)));
344 }
345
346 /// Record a tool call action.
347 ///
348 /// # Arguments
349 ///
350 /// * `call` - Tool call to record
351 pub fn record_tool_action(&mut self, call: ToolCall) {
352 self.interaction_history.push(Interaction::ToolAction {
353 call,
354 timestamp: Utc::now(),
355 });
356 }
357
358 /// Record a tool execution result.
359 ///
360 /// # Arguments
361 ///
362 /// * `tool_name` - Name of the tool
363 /// * `output` - Tool execution output
364 pub fn record_tool_observation(
365 &mut self,
366 tool_name: impl Into<String>,
367 output: impl Into<String>,
368 ) {
369 self.interaction_history.push(Interaction::ToolObservation {
370 tool_name: tool_name.into(),
371 output: output.into(),
372 timestamp: Utc::now(),
373 });
374 }
375
376 /// Increment iteration count and check limit.
377 ///
378 /// # Arguments
379 ///
380 /// * `max_iterations` - Maximum allowed iterations
381 ///
382 /// # Returns
383 ///
384 /// `true` if limit exceeded, `false` if within limit
385 pub fn increment_iteration(&mut self, max_iterations: usize) -> bool {
386 self.iteration_count += 1;
387 self.iteration_count > max_iterations
388 }
389
390 /// Get current iteration count.
391 pub fn iteration_count(&self) -> usize {
392 self.iteration_count
393 }
394
395 /// Check if this is the first iteration.
396 pub fn is_first_iteration(&self) -> bool {
397 self.iteration_count == 0
398 }
399
400 /// Read shared state asynchronously.
401 ///
402 /// Returns a read guard that releases when dropped.
403 pub async fn read_state(&self) -> tokio::sync::RwLockReadGuard<'_, Value> {
404 self.state.read().await
405 }
406
407 /// Write shared state asynchronously.
408 ///
409 /// Returns a write guard that releases when dropped.
410 pub async fn write_state(&self) -> tokio::sync::RwLockWriteGuard<'_, Value> {
411 self.state.write().await
412 }
413
414 /// Replace state with a new value.
415 ///
416 /// # Arguments
417 ///
418 /// * `new_state` - New state value
419 pub async fn update_state(&self, new_state: Value) {
420 let mut state = self.state.write().await;
421 *state = new_state;
422 }
423
424 /// Merge partial state into existing state.
425 ///
426 /// Only updates keys that exist in the partial value.
427 /// Requires both existing and partial states to be JSON objects.
428 ///
429 /// # Arguments
430 ///
431 /// * `partial` - Partial state to merge
432 pub async fn merge_state(&self, partial: Value) {
433 let mut state = self.state.write().await;
434 if let Value::Object(ref mut existing) = *state {
435 if let Value::Object(partial_map) = partial {
436 for (key, value) in partial_map {
437 existing.insert(key, value);
438 }
439 }
440 }
441 }
442}
443
444/// Trait for executing tool calls in agentic context.
445///
446/// Implement this trait to provide custom tool execution logic
447/// for agentic tools.
448///
449/// # Required Methods
450///
451/// - `execute` - Execute a tool call
452///
453/// # Example
454///
455/// ```rust,ignore
456/// struct MyExecutor;
457///
458/// #[async_trait]
459/// impl ToolExecutor for MyExecutor {
460/// async fn execute(&self, call: &ToolCall) -> Result<ToolResult, ToolError> {
461/// // Execute tool and return result
462/// Ok(ToolResult::success("Done"))
463/// }
464/// }
465/// ```
466#[async_trait]
467pub trait ToolExecutor: Send + Sync {
468 /// Execute a tool call.
469 async fn execute(&self, call: &ToolCall) -> Result<ToolResult, ToolError>;
470}
471
472/// Result from agentic tool execution.
473///
474/// Indicates the outcome of an agentic tool's execution step.
475///
476/// # Variants
477///
478/// * `Success` - Task completed successfully
479/// * `Error` - Execution failed
480/// * `NeedClarification` - Need user input
481/// * `NeedMoreActions` - More tool calls needed
482///
483/// # Example
484///
485/// ```rust,ignore
486/// let result = ToolResult::success("Task completed");
487/// let error = ToolResult::error("Failed to execute");
488/// let clarify = ToolResult::need_clarification("Which file?");
489/// let more = ToolResult::need_more_actions(vec![tool_call], "Need to read file");
490/// ```
491#[derive(Debug, Clone, Serialize, Deserialize)]
492#[serde(tag = "type", content = "data")]
493pub enum ToolResult {
494 Success {
495 result: String,
496 },
497 Error {
498 error: String,
499 },
500 NeedClarification {
501 question: String,
502 options: Option<Vec<String>>,
503 },
504 NeedMoreActions {
505 /// Tool calls to execute
506 actions: Vec<ToolCall>,
507 /// Reason for needing more actions
508 reason: String,
509 },
510}
511
512/// Type alias for agentic tool results.
513pub type AgenticToolResult = ToolResult;
514
515impl ToolResult {
516 /// Create a success result.
517 pub fn success(result: impl Into<String>) -> Self {
518 Self::Success {
519 result: result.into(),
520 }
521 }
522
523 /// Create an error result.
524 pub fn error(error: impl Into<String>) -> Self {
525 Self::Error {
526 error: error.into(),
527 }
528 }
529
530 /// Create a clarification request without options.
531 pub fn need_clarification(question: impl Into<String>) -> Self {
532 Self::NeedClarification {
533 question: question.into(),
534 options: None,
535 }
536 }
537
538 /// Create a clarification request with predefined options.
539 pub fn need_clarification_with_options(
540 question: impl Into<String>,
541 options: Vec<String>,
542 ) -> Self {
543 Self::NeedClarification {
544 question: question.into(),
545 options: Some(options),
546 }
547 }
548
549 /// Request more tool executions.
550 pub fn need_more_actions(actions: Vec<ToolCall>, reason: impl Into<String>) -> Self {
551 Self::NeedMoreActions {
552 actions,
553 reason: reason.into(),
554 }
555 }
556
557 /// Check if result is successful.
558 pub fn is_success(&self) -> bool {
559 matches!(self, Self::Success { .. })
560 }
561
562 /// Check if result is an error.
563 pub fn is_error(&self) -> bool {
564 matches!(self, Self::Error { .. })
565 }
566
567 /// Check if clarification is needed.
568 pub fn needs_clarification(&self) -> bool {
569 matches!(self, Self::NeedClarification { .. })
570 }
571
572 /// Check if more actions are needed.
573 pub fn needs_more_actions(&self) -> bool {
574 matches!(self, Self::NeedMoreActions { .. })
575 }
576}
577
578/// Trait for implementing autonomous agentic tools.
579///
580/// Agentic tools can execute multiple iterations, maintain state,
581/// and make autonomous decisions about which tools to call.
582///
583/// # Required Methods
584///
585/// - `name()` - Tool identifier
586/// - `description()` - Tool description
587/// - `execute()` - Execute the agentic tool
588///
589/// # Difference from Regular Tools
590///
591/// Unlike regular [`Tool`](crate::agent::core::tools::registry::Tool) trait:
592/// - Receives a [`ToolGoal`] instead of direct arguments
593/// - Has access to [`AgenticContext`] for state and history
594/// - Can execute multiple iterations
595/// - Returns [`ToolResult`] with continuation options
596///
597/// # Example
598///
599/// ```rust,ignore
600/// struct SmartCodeReviewTool;
601///
602/// #[async_trait]
603/// impl AgenticTool for SmartCodeReviewTool {
604/// fn name(&self) -> &str {
605/// "smart_code_review"
606/// }
607///
608/// fn description(&self) -> &str {
609/// "Autonomously review and fix code issues"
610/// }
611///
612/// async fn execute(
613/// &self,
614/// goal: ToolGoal,
615/// context: &mut AgenticContext,
616/// ) -> Result<ToolResult, ToolError> {
617/// // Iterate until goal achieved
618/// loop {
619/// if context.increment_iteration(goal.max_iterations) {
620/// return Ok(ToolResult::error("Max iterations reached"));
621/// }
622///
623/// // Analyze code and decide actions
624/// // Execute tools via context.base_executor
625/// // Update state and record interactions
626///
627/// if goal_achieved(&context).await {
628/// return Ok(ToolResult::success("Review complete"));
629/// }
630/// }
631/// }
632/// }
633/// ```
634#[async_trait]
635pub trait AgenticTool: Send + Sync {
636 /// Get tool name.
637 fn name(&self) -> &str;
638
639 /// Get tool description.
640 fn description(&self) -> &str;
641
642 /// Execute the agentic tool with goal and context.
643 ///
644 /// # Arguments
645 ///
646 /// * `goal` - Execution goal and parameters
647 /// * `context` - Execution context with state and history
648 ///
649 /// # Returns
650 ///
651 /// Tool execution result indicating success, error, or need for clarification.
652 async fn execute(
653 &self,
654 goal: ToolGoal,
655 context: &mut AgenticContext,
656 ) -> Result<ToolResult, ToolError>;
657}
658
659/// Convert agentic tool result to standard tool result.
660///
661/// Maps [`ToolResult`] from this module to the standard
662/// [`crate::agent::core::tools::types::ToolResult`] format.
663///
664/// # Arguments
665///
666/// * `agentic_result` - Agentic tool result to convert
667///
668/// # Returns
669///
670/// Standard tool result with appropriate display preferences.
671///
672/// # Mapping
673///
674/// - `Success` → `success: true, display_preference: None`
675/// - `Error` → `success: false, display_preference: "error"`
676/// - `NeedClarification` → `success: true, display_preference: "clarification"`
677/// - `NeedMoreActions` → `success: true, display_preference: "continuation"`
678pub fn convert_to_standard_result(
679 agentic_result: ToolResult,
680) -> crate::agent::core::tools::types::ToolResult {
681 match agentic_result {
682 ToolResult::Success { result } => crate::agent::core::tools::types::ToolResult {
683 success: true,
684 result,
685 display_preference: None,
686 },
687 ToolResult::Error { error } => crate::agent::core::tools::types::ToolResult {
688 success: false,
689 result: error,
690 display_preference: Some("error".to_string()),
691 },
692 ToolResult::NeedClarification { question, .. } => {
693 crate::agent::core::tools::types::ToolResult {
694 success: true,
695 result: question,
696 display_preference: Some("clarification".to_string()),
697 }
698 }
699 ToolResult::NeedMoreActions { reason, .. } => {
700 crate::agent::core::tools::types::ToolResult {
701 success: true,
702 result: reason,
703 display_preference: Some("actions_needed".to_string()),
704 }
705 }
706 }
707}
708
709/// Convert standard tool result to agentic tool result.
710///
711/// Inverse of [`convert_to_standard_result`].
712///
713/// # Arguments
714///
715/// * `standard_result` - Standard tool result to convert
716///
717/// # Returns
718///
719/// Agentic tool result based on success and display preference.
720///
721/// # Mapping
722///
723/// - `success: true` + `"clarification"` → `NeedClarification`
724/// - `success: true` + `"actions_needed"` → `NeedMoreActions`
725/// - `success: true` + other → `Success`
726/// - `success: false` → `Error`
727pub fn convert_from_standard_result(
728 standard_result: crate::agent::core::tools::types::ToolResult,
729) -> ToolResult {
730 if standard_result.success {
731 match standard_result.display_preference.as_deref() {
732 Some("clarification") => ToolResult::NeedClarification {
733 question: standard_result.result,
734 options: None,
735 },
736 Some("actions_needed") => ToolResult::NeedMoreActions {
737 actions: Vec::new(),
738 reason: standard_result.result,
739 },
740 _ => ToolResult::Success {
741 result: standard_result.result,
742 },
743 }
744 } else {
745 ToolResult::Error {
746 error: standard_result.result,
747 }
748 }
749}
750
751/// Smart code review tool with autonomous execution.
752///
753/// An agentic tool that autonomously reviews code, identifies issues,
754/// and plans fixes using multiple tool calls.
755///
756/// # Capabilities
757///
758/// - Chooses appropriate review strategy
759/// - Asks clarifying questions when needed
760/// - Plans and executes follow-up actions
761/// - Tracks progress across iterations
762///
763/// # Example
764///
765/// ```rust,ignore
766/// use bamboo_agent::agent::core::tools::agentic::*;
767///
768/// let tool = SmartCodeReviewTool::new();
769/// let executor = Arc::new(BuiltinToolExecutor::new());
770/// let mut context = AgenticContext::new(executor);
771///
772/// let goal = ToolGoal::new(
773/// "Review code for bugs",
774/// json!({"files": ["src/main.rs"]})
775/// );
776///
777/// let result = tool.execute(goal, &mut context).await?;
778/// ```
779pub struct SmartCodeReviewTool {
780 /// Tool name
781 name: String,
782 /// Tool description
783 description: String,
784}
785
786impl Default for SmartCodeReviewTool {
787 fn default() -> Self {
788 Self {
789 name: "smart_code_review".to_string(),
790 description: "Autonomous reviewer that chooses review strategy, asks clarifying questions, and plans follow-up tool actions.".to_string(),
791 }
792 }
793}
794
795impl SmartCodeReviewTool {
796 /// Create a new smart code review tool.
797 pub fn new() -> Self {
798 Self::default()
799 }
800
801 /// Collect code findings from content analysis.
802 ///
803 /// Analyzes code for common issues and patterns.
804 ///
805 /// # Arguments
806 ///
807 /// * `content` - Code content to analyze
808 ///
809 /// # Returns
810 ///
811 /// Tuple of (findings list, has_critical_issues)
812 fn collect_findings(&self, content: &str) -> (Vec<String>, bool) {
813 let mut findings = Vec::new();
814 let mut has_critical = false;
815
816 if content.contains("unsafe ") {
817 has_critical = true;
818 findings.push("critical: unsafe block detected".to_string());
819 }
820
821 if content.contains("unwrap()") {
822 findings.push("warning: unwrap() detected".to_string());
823 }
824
825 if content.contains("todo!") || content.contains("TODO") {
826 findings.push("warning: unresolved TODO detected".to_string());
827 }
828
829 let long_line_count = content.lines().filter(|line| line.len() > 120).count();
830 if long_line_count > 0 {
831 findings.push(format!(
832 "warning: {} line(s) exceed 120 characters",
833 long_line_count
834 ));
835 }
836
837 (findings, has_critical)
838 }
839
840 /// Choose review strategy based on content and findings.
841 ///
842 /// # Arguments
843 ///
844 /// * `content` - Code content
845 /// * `findings` - List of findings
846 ///
847 /// # Returns
848 ///
849 /// Strategy name: "quick", "standard", or "deep"
850 fn choose_strategy(&self, content: &str, findings: &[String]) -> &'static str {
851 let line_count = content.lines().count();
852
853 if line_count > 300 || findings.len() > 3 {
854 "deep"
855 } else if line_count > 80 || !findings.is_empty() {
856 "standard"
857 } else {
858 "quick"
859 }
860 }
861
862 /// Build follow-up tool actions for code review.
863 ///
864 /// Creates tool calls for running linters and tests.
865 ///
866 /// # Arguments
867 ///
868 /// * `file_path` - Optional file path to review
869 ///
870 /// # Returns
871 ///
872 /// List of tool calls to execute
873 fn build_actions(&self, file_path: Option<&str>) -> Vec<ToolCall> {
874 let path_hint = file_path.unwrap_or("<unknown-file>");
875
876 vec![
877 ToolCall {
878 id: "agentic-action-1".to_string(),
879 tool_type: "function".to_string(),
880 function: FunctionCall {
881 name: "execute_command".to_string(),
882 arguments: serde_json::json!({
883 "command": format!("cargo clippy --all-targets --all-features -- {}", path_hint)
884 })
885 .to_string(),
886 },
887 },
888 ToolCall {
889 id: "agentic-action-2".to_string(),
890 tool_type: "function".to_string(),
891 function: FunctionCall {
892 name: "execute_command".to_string(),
893 arguments: serde_json::json!({
894 "command": "cargo test"
895 })
896 .to_string(),
897 },
898 },
899 ]
900 }
901}
902
903#[async_trait]
904impl AgenticTool for SmartCodeReviewTool {
905 fn name(&self) -> &str {
906 &self.name
907 }
908
909 fn description(&self) -> &str {
910 &self.description
911 }
912
913 async fn execute(
914 &self,
915 goal: ToolGoal,
916 context: &mut AgenticContext,
917 ) -> Result<ToolResult, ToolError> {
918 context.record_interaction(
919 InteractionRole::System,
920 format!("Start smart review for intent: {}", goal.intent),
921 );
922
923 let content = goal.params.get("content").and_then(Value::as_str);
924 if content.is_none() {
925 return Ok(ToolResult::need_clarification_with_options(
926 "I need the source code content to run a review. Please provide `content`."
927 .to_string(),
928 vec![
929 "Paste the full file content".to_string(),
930 "Provide a trimmed snippet and target concerns".to_string(),
931 ],
932 ));
933 }
934
935 let content = content.unwrap_or_default();
936 if content.trim().is_empty() {
937 return Ok(ToolResult::need_clarification(
938 "The provided content is empty. Please share non-empty code.".to_string(),
939 ));
940 }
941
942 let first_iteration = context.is_first_iteration();
943
944 if context.increment_iteration(goal.max_iterations) {
945 return Ok(ToolResult::error(format!(
946 "max_iterations reached: {}",
947 goal.max_iterations
948 )));
949 }
950
951 let file_path = goal.params.get("file_path").and_then(Value::as_str);
952 let (findings, has_critical) = self.collect_findings(content);
953 let strategy = self.choose_strategy(content, &findings).to_string();
954
955 context
956 .merge_state(serde_json::json!({
957 "intent": goal.intent,
958 "strategy": strategy,
959 "findings": findings,
960 "line_count": content.lines().count(),
961 }))
962 .await;
963
964 if has_critical && first_iteration {
965 return Ok(ToolResult::need_clarification_with_options(
966 "I found critical safety signals (for example `unsafe`). Should I continue with defensive recommendations or stop for manual review?".to_string(),
967 vec![
968 "Continue with defensive recommendations".to_string(),
969 "Stop and return only critical issues".to_string(),
970 ],
971 ));
972 }
973
974 if strategy == "deep" {
975 let actions = self.build_actions(file_path);
976 let auto_execute = goal
977 .params
978 .get("auto_execute_actions")
979 .and_then(Value::as_bool)
980 .unwrap_or(false);
981
982 if auto_execute {
983 if let Some(first_action) = actions.first() {
984 context.record_tool_action(first_action.clone());
985 let action_result = context.base_executor.execute(first_action).await?;
986 context.record_tool_observation(
987 first_action.function.name.as_str(),
988 format!("{:?}", action_result),
989 );
990 }
991 } else {
992 return Ok(ToolResult::need_more_actions(
993 actions,
994 "Deep review requires lint/test evidence before final verdict.",
995 ));
996 }
997 }
998
999 let summary = serde_json::json!({
1000 "strategy": strategy,
1001 "finding_count": findings.len(),
1002 "findings": findings,
1003 });
1004
1005 Ok(ToolResult::success(
1006 serde_json::to_string_pretty(&summary).unwrap_or_default(),
1007 ))
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014
1015 struct MockExecutor;
1016
1017 #[async_trait]
1018 impl ToolExecutor for MockExecutor {
1019 async fn execute(&self, _call: &ToolCall) -> Result<ToolResult, ToolError> {
1020 Ok(ToolResult::success("mock-executed"))
1021 }
1022 }
1023
1024 #[test]
1025 fn tool_goal_should_store_intent_params_and_iteration_limit() {
1026 let goal = ToolGoal::new(
1027 "review rust",
1028 serde_json::json!({ "content": "fn main() {}" }),
1029 )
1030 .with_max_iterations(3);
1031
1032 assert_eq!(goal.intent, "review rust");
1033 assert_eq!(goal.max_iterations, 3);
1034 assert_eq!(goal.params["content"], "fn main() {}");
1035 }
1036
1037 #[test]
1038 fn interaction_history_should_use_enum_variants() {
1039 let executor: Arc<dyn ToolExecutor> = Arc::new(MockExecutor);
1040 let mut context = AgenticContext::new(executor);
1041
1042 context.record_interaction(InteractionRole::User, "Review this function");
1043
1044 assert_eq!(context.interaction_history.len(), 1);
1045 assert!(matches!(
1046 &context.interaction_history[0],
1047 Interaction::User { message, .. } if message == "Review this function"
1048 ));
1049 }
1050
1051 #[test]
1052 fn tool_result_should_support_new_agentic_variants() {
1053 let clarification = ToolResult::need_clarification_with_options(
1054 "Which standard should I follow?",
1055 vec!["Rust style".to_string(), "Project style".to_string()],
1056 );
1057 assert!(clarification.needs_clarification());
1058
1059 let actions = ToolResult::need_more_actions(
1060 vec![ToolCall {
1061 id: "1".to_string(),
1062 tool_type: "function".to_string(),
1063 function: FunctionCall {
1064 name: "execute_command".to_string(),
1065 arguments: "{}".to_string(),
1066 },
1067 }],
1068 "Need more evidence",
1069 );
1070 assert!(actions.needs_more_actions());
1071 }
1072
1073 #[tokio::test]
1074 async fn smart_review_should_ask_for_content_when_missing() {
1075 let tool = SmartCodeReviewTool::new();
1076 let executor: Arc<dyn ToolExecutor> = Arc::new(MockExecutor);
1077 let mut context = AgenticContext::new(executor);
1078
1079 let goal = ToolGoal::new("review", serde_json::json!({}));
1080 let result = tool.execute(goal, &mut context).await.unwrap();
1081
1082 assert!(result.needs_clarification());
1083 }
1084
1085 #[tokio::test]
1086 async fn smart_review_should_request_more_actions_for_deep_review() {
1087 let tool = SmartCodeReviewTool::new();
1088 let executor: Arc<dyn ToolExecutor> = Arc::new(MockExecutor);
1089 let mut context = AgenticContext::new(executor);
1090
1091 let large_code = (0..360)
1092 .map(|idx| format!("fn f{}() {{ println!(\"{}\") }}", idx, idx))
1093 .collect::<Vec<_>>()
1094 .join("\n");
1095
1096 let goal = ToolGoal::new(
1097 "review",
1098 serde_json::json!({
1099 "file_path": "src/lib.rs",
1100 "content": large_code,
1101 }),
1102 );
1103
1104 let result = tool.execute(goal, &mut context).await.unwrap();
1105 assert!(result.needs_more_actions());
1106 }
1107
1108 #[tokio::test]
1109 async fn smart_review_should_return_success_for_small_clean_code() {
1110 let tool = SmartCodeReviewTool::new();
1111 let executor: Arc<dyn ToolExecutor> = Arc::new(MockExecutor);
1112 let mut context = AgenticContext::new(executor);
1113
1114 let goal = ToolGoal::new(
1115 "review",
1116 serde_json::json!({
1117 "content": "fn sum(a: i32, b: i32) -> i32 { a + b }",
1118 }),
1119 )
1120 .with_max_iterations(5);
1121
1122 let result = tool.execute(goal, &mut context).await.unwrap();
1123 assert!(result.is_success());
1124 }
1125}