Skip to main content

bamboo_agent/agent/loop_module/
mod.rs

1//! Agent execution loop
2//!
3//! This module implements the main agent execution loop that processes
4//! user requests, executes tools, and manages the conversation flow.
5//!
6//! # Components
7//!
8//! - **Runner**: Main loop implementation
9//! - **Stream**: Streaming response handling
10//! - **TodoContext**: Todo list integration
11//! - **TodoEvaluation**: Progress evaluation logic
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use bamboo_agent::agent::loop_module::{run_agent_loop, AgentLoopConfig};
17//!
18//! let config = AgentLoopConfig::default();
19//! let result = run_agent_loop(session, provider, config).await?;
20//! ```
21
22pub mod config;
23pub mod runner;
24pub mod stream;
25pub mod todo_context;
26pub mod todo_evaluation;
27
28pub use config::AgentLoopConfig;
29pub use runner::{run_agent_loop, run_agent_loop_with_config};
30pub use todo_context::TodoLoopContext;
31pub use todo_evaluation::{evaluate_todo_progress, TodoEvaluationResult};
32
33#[cfg(test)]
34mod tests {
35    use std::sync::Arc;
36
37    use crate::agent::core::composition::CompositionExecutor;
38    use crate::agent::core::tools::{
39        execute_tool_call, handle_tool_result_with_agentic_support, AgenticToolResult,
40        FunctionCall, ToolCall, ToolExecutor, ToolHandlingOutcome, ToolRegistry, ToolResult,
41    };
42    use crate::agent::core::{AgentEvent, Session};
43    use crate::agent::tools::BuiltinToolExecutor;
44    use tokio::sync::mpsc;
45
46    use crate::agent::loop_module::config::AgentLoopConfig;
47
48    fn make_tool_call(id: &str, name: &str, arguments: &str) -> ToolCall {
49        ToolCall {
50            id: id.to_string(),
51            tool_type: "function".to_string(),
52            function: FunctionCall {
53                name: name.to_string(),
54                arguments: arguments.to_string(),
55            },
56        }
57    }
58
59    #[test]
60    fn agent_loop_config_default() {
61        let config = AgentLoopConfig::default();
62        assert_eq!(config.max_rounds, 50);
63        assert!(config.system_prompt.is_none());
64        assert!(config.additional_tool_schemas.is_empty());
65        assert!(config.tool_registry.is_empty());
66        assert!(config.composition_executor.is_none());
67        assert!(config.skill_manager.is_none());
68        assert!(!config.skip_initial_user_message);
69    }
70
71    #[test]
72    fn skip_initial_message_flag() {
73        let config = AgentLoopConfig {
74            skip_initial_user_message: true,
75            ..Default::default()
76        };
77        assert!(config.skip_initial_user_message);
78    }
79
80    #[tokio::test]
81    async fn need_clarification_sends_event() {
82        let (event_tx, mut event_rx) = mpsc::channel(8);
83        let tools: Arc<dyn ToolExecutor> = Arc::new(BuiltinToolExecutor::new());
84        let mut session = Session::new("s1", "test-model");
85        let tool_call = make_tool_call("call_parent", "smart_tool", "{}");
86        let result = ToolResult {
87            success: true,
88            result: serde_json::to_string(&AgenticToolResult::NeedClarification {
89                question: "Which file should I inspect?".to_string(),
90                options: Some(vec!["src/main.rs".to_string(), "src/lib.rs".to_string()]),
91            })
92            .unwrap(),
93            display_preference: None,
94        };
95
96        let outcome = handle_tool_result_with_agentic_support(
97            &result,
98            &tool_call,
99            &event_tx,
100            &mut session,
101            tools.as_ref(),
102            None,
103        )
104        .await;
105
106        assert_eq!(outcome, ToolHandlingOutcome::AwaitingClarification);
107
108        let event = event_rx.recv().await.expect("missing clarification event");
109        match event {
110            AgentEvent::NeedClarification { question, options } => {
111                assert_eq!(question, "Which file should I inspect?");
112                assert_eq!(
113                    options,
114                    Some(vec!["src/main.rs".to_string(), "src/lib.rs".to_string()])
115                );
116            }
117            other => panic!("unexpected event: {other:?}"),
118        }
119    }
120
121    #[tokio::test]
122    async fn need_more_actions_executes_sub_actions() {
123        let (event_tx, mut event_rx) = mpsc::channel(16);
124        let tools: Arc<dyn ToolExecutor> = Arc::new(BuiltinToolExecutor::new());
125        let mut session = Session::new("s2", "test-model");
126        let sub_action = make_tool_call("call_sub", "get_current_dir", "{}");
127        let parent_call = make_tool_call("call_parent", "smart_tool", "{}");
128        let result = ToolResult {
129            success: true,
130            result: serde_json::to_string(&AgenticToolResult::NeedMoreActions {
131                actions: vec![sub_action.clone()],
132                reason: "Need workspace context".to_string(),
133            })
134            .unwrap(),
135            display_preference: None,
136        };
137
138        let outcome = handle_tool_result_with_agentic_support(
139            &result,
140            &parent_call,
141            &event_tx,
142            &mut session,
143            tools.as_ref(),
144            None,
145        )
146        .await;
147
148        assert_eq!(outcome, ToolHandlingOutcome::Continue);
149        assert!(session
150            .messages
151            .iter()
152            .any(
153                |message| message.tool_call_id.as_deref() == Some("call_sub")
154                    && !message.content.is_empty()
155            ));
156
157        let mut saw_sub_start = false;
158        let mut saw_sub_complete = false;
159
160        while let Ok(event) = event_rx.try_recv() {
161            match event {
162                AgentEvent::ToolStart { tool_call_id, .. } if tool_call_id == "call_sub" => {
163                    saw_sub_start = true;
164                }
165                AgentEvent::ToolComplete { tool_call_id, .. } if tool_call_id == "call_sub" => {
166                    saw_sub_complete = true;
167                }
168                _ => {}
169            }
170        }
171
172        assert!(saw_sub_start);
173        assert!(saw_sub_complete);
174    }
175
176    #[tokio::test]
177    async fn execute_tool_call_falls_back_when_composition_misses_tool() {
178        let tools: Arc<dyn ToolExecutor> = Arc::new(BuiltinToolExecutor::new());
179        let composition_executor =
180            Arc::new(CompositionExecutor::new(Arc::new(ToolRegistry::new())));
181        let tool_call = make_tool_call("call_sub", "get_current_dir", "{}");
182
183        let result = execute_tool_call(&tool_call, tools.as_ref(), Some(composition_executor))
184            .await
185            .expect("fallback execution should succeed");
186
187        assert!(result.success);
188        assert!(!result.result.is_empty());
189    }
190}