1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::sync::Arc;
4
5use crate::{
6 api::Model,
7 app::{
8 ApprovalDecision, ToolExecutor,
9 conversation::{Message, MessageData, UserContent},
10 },
11 config::LlmConfigProvider,
12};
13
14use crate::app::{AgentEvent, AgentExecutor, AgentExecutorRunRequest};
15use steer_macros::tool_external as tool;
16use steer_tools::tools::{GLOB_TOOL_NAME, GREP_TOOL_NAME, LS_TOOL_NAME, VIEW_TOOL_NAME};
17use steer_tools::{ToolCall, ToolError, ToolSchema};
18use tokio_util::sync::CancellationToken;
19
20#[derive(Deserialize, Debug, Serialize, JsonSchema)]
21pub struct DispatchAgentParams {
22 pub prompt: String,
24}
25
26const DISPATCH_AGENT_TOOLS: [&str; 4] =
27 [GLOB_TOOL_NAME, GREP_TOOL_NAME, LS_TOOL_NAME, VIEW_TOOL_NAME];
28
29fn format_dispatch_agent_tools() -> String {
30 DISPATCH_AGENT_TOOLS
31 .iter()
32 .map(|tool| tool.to_string())
33 .collect::<Vec<String>>()
34 .join(", ")
35}
36
37tool! {
38 pub struct DispatchAgentTool {
39 pub llm_config_provider: Arc<LlmConfigProvider>,
40 pub workspace: Arc<dyn crate::workspace::Workspace>,
41 } {
42 params: DispatchAgentParams,
43 output: steer_tools::result::AgentResult,
44 variant: Agent,
45 description: &format!(r#"Launch a new agent that has access to the following tools: {}. When you are searching for a keyword or file and are not confident that you will find the right match on the first try, use the Agent tool to perform the search for you.
46
47When to use the Agent tool:
48- If you are searching for a keyword like "config" or "logger", or for questions like "which file does X?", the Agent tool is strongly recommended
49
50When NOT to use the Agent tool:
51- If you want to read a specific file path, use the {VIEW_TOOL_NAME} or {GLOB_TOOL_NAME} tool instead of the Agent tool, to find the match more quickly
52- If you are searching for a specific class definition like "class Foo", use the {GREP_TOOL_NAME} tool instead, to find the match more quickly
53- If you are searching for code within a specific file or set of 2-3 files, use the {GREP_TOOL_NAME} tool instead, to find the match more quickly
54
55Usage notes:
561. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
572. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
583. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
594. The agent's outputs should generally be trusted
605. IMPORTANT: The agent can not modify files. If you want to modify files, do it directly instead of going through the agent."#, format_dispatch_agent_tools()),
61 name: "dispatch_agent",
62 require_approval: false
63 }
64
65 async fn run(
66 tool: &DispatchAgentTool,
67 params: DispatchAgentParams,
68 context: &steer_tools::ExecutionContext,
69 ) -> std::result::Result<steer_tools::result::AgentResult, ToolError> {
70 let token = context.cancellation_token.clone();
71
72 let api_client = Arc::new(crate::api::Client::new_with_provider((*tool.llm_config_provider).clone())); let agent_executor = AgentExecutor::new(api_client);
74
75 let tool_executor = Arc::new(ToolExecutor::with_workspace(tool.workspace.clone()));
76
77 let available_tools: Vec<ToolSchema> = tool_executor.get_tool_schemas().await;
78 let tool_approval_callback = move |_tool_call: ToolCall| {
79 async move { Ok(ApprovalDecision::Approved) }
80 };
81
82 let tool_execution_callback =
83 move |tool_call: ToolCall, callback_token: CancellationToken| {
84 let executor = tool_executor.clone();
85 async move {
86 executor
87 .execute_tool_with_cancellation(&tool_call, callback_token)
88 .await
89 }
90 };
91
92 let initial_messages = vec![Message {
94 data: MessageData::User {
95 content: vec![UserContent::Text { text: params.prompt }],
96 },
97 timestamp: Message::current_timestamp(),
98 id: Message::generate_id("user", Message::current_timestamp()),
99 parent_message_id: None,
100 }];
101
102 let system_prompt = create_dispatch_agent_system_prompt(&tool.workspace)
103 .await
104 .map_err(|e| ToolError::execution(DISPATCH_AGENT_TOOL_NAME, format!("Failed to create system prompt: {e}")))?;
105
106 let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(100);
108
109 let operation_result = agent_executor
111 .run(
112 AgentExecutorRunRequest
113 {
114 model: Model::Claude3_7Sonnet20250219, initial_messages,
116 system_prompt: Some(system_prompt),
117 available_tools,
118 tool_approval_callback,
119 tool_execution_callback,
120 },
121 event_tx,
122 token,
123 )
124 .await;
125
126 let mut final_text = String::new();
130 while let Ok(event) = event_rx.try_recv() {
134 if let AgentEvent::MessageFinal(msg) = event {
135 if final_text.is_empty() {
136 final_text = msg.extract_text();
137 }
138 }
139 }
140
141
142 match operation_result {
143 Ok(message) => {
144 if final_text.is_empty() {
146 final_text = message.extract_text();
147 }
148 Ok(steer_tools::result::AgentResult {
149 content: final_text,
150 })
151 }
152 Err(e) => {
153 Err(ToolError::execution(DISPATCH_AGENT_TOOL_NAME, e.to_string()))
154 }
155 }
156 }
157}
158
159pub async fn create_dispatch_agent_system_prompt(
160 workspace: &Arc<dyn crate::workspace::Workspace>,
161) -> crate::error::Result<String> {
162 let env_info = workspace.environment().await?;
164 let env_context = env_info.as_context();
165
166 let dispatch_prompt = format!(
167 r#"You are an agent for a CLI-based coding tool. Given the user's prompt, you should use the tools available to you to answer the user's question.
168
169Notes:
1701. IMPORTANT: You should be concise, direct, and to the point, since your responses will be displayed on a command line interface. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".
1712. When relevant, share file names and code snippets relevant to the query
1723. Any file paths you return in your final response MUST be absolute. DO NOT use relative paths.
173
174{env_context}
175"#
176 );
177
178 Ok(dispatch_prompt)
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use dotenvy::dotenv;
185 use steer_workspace::local::LocalWorkspace;
186
187 #[tokio::test]
188 #[ignore] async fn test_dispatch_agent() {
190 dotenv().ok();
192
193 let _api_key =
195 std::env::var("CLAUDE_API_KEY").expect("CLAUDE_API_KEY must be set for this test");
196
197 let temp_dir = tempfile::tempdir().unwrap(); std::fs::write(
200 temp_dir.path().join("search_code.rs"),
201 "fn find_stuff() {}
202fn search_database() {}
203",
204 )
205 .unwrap();
206
207 let auth_storage = Arc::new(crate::test_utils::InMemoryAuthStorage::new());
208 let llm_config_provider = Arc::new(LlmConfigProvider::new(auth_storage));
209
210 let context = steer_tools::ExecutionContext::new("test_tool_call".to_string())
212 .with_working_directory(temp_dir.path().to_path_buf())
213 .with_cancellation_token(tokio_util::sync::CancellationToken::new());
214
215 let prompt = "Find all files that contain definitions of functions or methods related to search or find operations. Return only the absolute file path.";
217
218 let params = DispatchAgentParams {
219 prompt: prompt.to_string(),
220 };
221
222 let tool_instance = DispatchAgentTool {
224 llm_config_provider,
225 workspace: Arc::new(
226 LocalWorkspace::with_path(temp_dir.path().to_path_buf())
227 .await
228 .unwrap(),
229 ),
230 };
231
232 let result = run(&tool_instance, params, &context).await;
234
235 assert!(result.is_ok(), "Agent execution failed: {:?}", result.err());
237 let response = result.unwrap();
238 assert!(!response.content.is_empty(), "Response should not be empty");
239 assert!(
240 response.content.contains("search_code.rs"),
241 "Response should contain the file path"
242 ); println!("Dispatch agent response: {}", response.content);
245 println!("Dispatch agent test passed successfully!");
246 }
247}