| 1 | //! `/agent` command. |
| 2 | |
| 3 | use crate::commands::traits::{CommandInfo, RegisterCommand}; |
| 4 | use crate::localization::MessageId; |
| 5 | use crate::tui::app::{App, AppAction}; |
| 6 | |
| 7 | use super::CommandResult; |
| 8 | |
| 9 | pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo { |
| 10 | name: "agent", |
| 11 | aliases: &["daili"], |
| 12 | usage: "/agent [N] <task>", |
| 13 | description_id: MessageId::CmdAgentDescription, |
| 14 | }; |
| 15 | |
| 16 | pub(in crate::commands) struct AgentCmd; |
| 17 | |
| 18 | impl RegisterCommand for AgentCmd { |
| 19 | fn info() -> &'static CommandInfo { |
| 20 | &COMMAND_INFO |
| 21 | } |
| 22 | |
| 23 | fn execute(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 24 | agent(app, arg) |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | pub fn agent(_app: &mut App, arg: Option<&str>) -> CommandResult { |
| 29 | if let Some(action) = parse_agent_control_action(arg) { |
| 30 | if action.action == "cancel" { |
| 31 | return CommandResult::with_message_and_action( |
| 32 | format!("Cancelling agent {}...", action.agent_id), |
| 33 | AppAction::CancelSubAgent { |
| 34 | agent_id: action.agent_id, |
| 35 | }, |
| 36 | ); |
| 37 | } |
| 38 | let message = format!( |
| 39 | "Call `agent` with action `{}`, agent_id `{}`, then summarize the returned status for the user. Do not start a new agent.", |
| 40 | action.action, action.agent_id |
| 41 | ); |
| 42 | return CommandResult::with_message_and_action( |
| 43 | format!("Agent {} requested for {}.", action.action, action.agent_id), |
| 44 | AppAction::SendMessage(message), |
| 45 | ); |
| 46 | } |
| 47 | |
| 48 | let (max_depth, task) = match super::util::parse_depth_prefixed_arg(arg, 1) { |
| 49 | Ok(parsed) => parsed, |
| 50 | Err(message) => return CommandResult::error(message), |
| 51 | }; |
| 52 | let task = match task { |
| 53 | Some(task) if !task.trim().is_empty() => task.trim().to_string(), |
| 54 | _ => { |
| 55 | return CommandResult::error( |
| 56 | "Usage: /agent [N] <task>\n\n\ |
| 57 | Opens a persistent sub-agent session with recursive agent depth N (0-3, default 1).", |
| 58 | ); |
| 59 | } |
| 60 | }; |
| 61 | let message = format!( |
| 62 | "Launch one sub-agent for this task by calling `agent` with name `slash_agent`, `prompt: {task:?}`, and `max_depth: {max_depth}`. Use `handle_read` on the returned transcript_handle if you need more detail. Verify any claimed side effects before reporting success." |
| 63 | ); |
| 64 | CommandResult::with_message_and_action( |
| 65 | format!("Opening persistent sub-agent at depth {max_depth}..."), |
| 66 | AppAction::SendMessage(message), |
| 67 | ) |
| 68 | } |
| 69 | |
| 70 | struct AgentControlAction { |
| 71 | action: &'static str, |
| 72 | agent_id: String, |
| 73 | } |
| 74 | |
| 75 | fn parse_agent_control_action(arg: Option<&str>) -> Option<AgentControlAction> { |
| 76 | let arg = arg?.trim(); |
| 77 | let (action, rest) = arg.split_once(char::is_whitespace)?; |
| 78 | let action = match action { |
| 79 | "status" | "inspect" => "status", |
| 80 | "peek" | "progress" => "peek", |
| 81 | "cancel" | "stop" | "abort" => "cancel", |
| 82 | _ => return None, |
| 83 | }; |
| 84 | let agent_id = rest.trim(); |
| 85 | if agent_id.is_empty() || agent_id.contains(char::is_whitespace) { |
| 86 | return None; |
| 87 | } |
| 88 | Some(AgentControlAction { |
| 89 | action, |
| 90 | agent_id: agent_id.to_string(), |
| 91 | }) |
| 92 | } |
| 93 | |
| 94 | #[cfg(test)] |
| 95 | mod tests { |
| 96 | use super::*; |
| 97 | use std::path::PathBuf; |
| 98 | |
| 99 | use crate::tui::app::TuiOptions; |
| 100 | |
| 101 | fn test_app() -> App { |
| 102 | let options = TuiOptions { |
| 103 | ..crate::test_support::test_tui_options(PathBuf::from(".")) |
| 104 | }; |
| 105 | App::new(options, &crate::config::Config::default()) |
| 106 | } |
| 107 | |
| 108 | #[test] |
| 109 | fn agent_control_actions_route_to_existing_agent_tool() { |
| 110 | let mut app = test_app(); |
| 111 | let result = agent(&mut app, Some("peek agent_123")); |
| 112 | |
| 113 | assert!(!result.is_error); |
| 114 | let Some(AppAction::SendMessage(message)) = result.action else { |
| 115 | panic!("expected SendMessage action"); |
| 116 | }; |
| 117 | assert!(message.contains("action `peek`")); |
| 118 | assert!(message.contains("agent_id `agent_123`")); |
| 119 | assert!(message.contains("Do not start a new agent")); |
| 120 | |
| 121 | let result = agent(&mut app, Some("cancel agent_123")); |
| 122 | let Some(AppAction::CancelSubAgent { agent_id }) = result.action else { |
| 123 | panic!("expected CancelSubAgent action"); |
| 124 | }; |
| 125 | assert_eq!(agent_id, "agent_123"); |
| 126 | } |
| 127 | } |
| 128 |