systemprompt_cli/commands/admin/agents/
delete.rs1use anyhow::{Context, Result, anyhow};
2use clap::Args;
3use std::path::Path;
4use std::sync::Arc;
5
6use super::types::AgentDeleteOutput;
7use crate::CliConfig;
8use crate::interactive::{Prompter, require_confirmation, resolve_required};
9use crate::shared::CommandOutput;
10use systemprompt_agent::AgentState;
11use systemprompt_agent::services::agent_orchestration::AgentOrchestrator;
12use systemprompt_agent::services::config_authoring::AgentConfigAuthoringService;
13use systemprompt_config::ProfileBootstrap;
14use systemprompt_loader::ConfigLoader;
15use systemprompt_logging::CliService;
16use systemprompt_oauth::JwtValidationProviderImpl;
17use systemprompt_runtime::AppContext;
18use systemprompt_scheduler::ProcessCleanup;
19
20#[derive(Debug, Args)]
21pub struct DeleteArgs {
22 #[arg(help = "Agent name (required in non-interactive mode)")]
23 pub name: Option<String>,
24
25 #[arg(long, help = "Delete all agents")]
26 pub all: bool,
27
28 #[arg(short = 'y', long, help = "Skip confirmation prompts")]
29 pub yes: bool,
30
31 #[arg(long, help = "Force delete even if process cannot be stopped")]
32 pub force: bool,
33}
34
35pub(super) async fn execute(
36 args: DeleteArgs,
37 prompter: &dyn Prompter,
38 config: &CliConfig,
39) -> Result<CommandOutput> {
40 let services_config = ConfigLoader::load().context("Failed to load services configuration")?;
41
42 let agents_to_delete = resolve_targets(&args, prompter, &services_config, config)?;
43
44 require_confirmation(
45 prompter,
46 &delete_confirm_message(args.all, &agents_to_delete),
47 args.yes,
48 config,
49 )?;
50
51 let profile = ProfileBootstrap::get().context("Failed to get profile")?;
52 let authoring = AgentConfigAuthoringService::new(Path::new(&profile.paths.services));
53
54 let orchestrator = match build_orchestrator().await {
55 Ok(orchestrator) => orchestrator,
56 Err(message) => {
57 return Ok(CommandOutput::card_value(
58 "Delete Failed",
59 &AgentDeleteOutput {
60 deleted: vec![],
61 message,
62 },
63 ));
64 },
65 };
66
67 let mut deleted = Vec::new();
68 let mut errors = Vec::new();
69
70 for agent_name in &agents_to_delete {
71 let agent_port = services_config.agents.get(agent_name).map(|c| c.port);
72 let result = delete_single_agent(
73 agent_name,
74 agent_port,
75 orchestrator.as_ref(),
76 &authoring,
77 args.force,
78 )
79 .await;
80 match result {
81 Ok(()) => deleted.push(agent_name.clone()),
82 Err(msg) => errors.push(msg),
83 }
84 }
85
86 if !errors.is_empty() && deleted.is_empty() {
87 return Err(anyhow!("Failed to delete agents:\n{}", errors.join("\n")));
88 }
89
90 if !deleted.is_empty() {
91 ConfigLoader::load().with_context(|| {
92 "Agent(s) deleted but configuration validation failed. Please check the configuration."
93 })?;
94 }
95
96 let message = delete_success_message(&deleted);
97 let output = AgentDeleteOutput { deleted, message };
98
99 Ok(CommandOutput::card_value("Delete Agent", &output))
100}
101
102fn resolve_targets(
103 args: &DeleteArgs,
104 prompter: &dyn Prompter,
105 services_config: &systemprompt_models::ServicesConfig,
106 config: &CliConfig,
107) -> Result<Vec<String>> {
108 let available: Vec<String> = services_config.agents.keys().cloned().collect();
109 let requested = if args.all {
110 None
111 } else {
112 Some(resolve_required(args.name.clone(), "name", config, || {
113 super::shared::prompt_agent_selection(
114 prompter,
115 "Select agent to delete",
116 services_config,
117 )
118 })?)
119 };
120 validate_delete_targets(requested, &available)
121}
122
123pub fn validate_delete_targets(
126 requested: Option<String>,
127 available: &[String],
128) -> Result<Vec<String>> {
129 let agents = match requested {
130 Some(name) => {
131 if !available.contains(&name) {
132 return Err(anyhow!("Agent '{}' not found", name));
133 }
134 vec![name]
135 },
136 None => available.to_vec(),
137 };
138
139 if agents.is_empty() {
140 return Err(anyhow!("No agents to delete"));
141 }
142
143 Ok(agents)
144}
145
146#[must_use]
147pub fn delete_confirm_message(all: bool, targets: &[String]) -> String {
148 if all {
149 format!("Delete ALL {} agents?", targets.len())
150 } else {
151 format!("Delete agent '{}'?", targets[0])
152 }
153}
154
155#[must_use]
156pub fn delete_success_message(deleted: &[String]) -> String {
157 if deleted.len() == 1 {
158 format!("Agent '{}' deleted successfully", deleted[0])
159 } else {
160 format!("{} agent(s) deleted successfully", deleted.len())
161 }
162}
163
164async fn build_orchestrator() -> Result<Option<AgentOrchestrator>, String> {
165 let ctx = match AppContext::new().await {
166 Ok(ctx) => ctx,
167 Err(e) => {
168 tracing::debug!(error = %e, "Failed to create AppContext for agent deletion");
169 return Ok(None);
170 },
171 };
172
173 let jwt_provider = match JwtValidationProviderImpl::from_config() {
174 Ok(p) => Arc::new(p),
175 Err(e) => {
176 tracing::debug!(error = %e, "Failed to create JWT provider");
177 return Err(format!("Failed to initialize: {e}"));
178 },
179 };
180
181 let agent_state = Arc::new(AgentState::new(
182 Arc::clone(ctx.db_pool()),
183 Arc::new(ctx.config().clone()),
184 jwt_provider,
185 ));
186
187 Ok(
188 AgentOrchestrator::new(agent_state, Arc::clone(ctx.app_paths_arc()), None)
189 .await
190 .ok(),
191 )
192}
193
194async fn delete_single_agent(
195 agent_name: &str,
196 agent_port: Option<u16>,
197 orchestrator: Option<&AgentOrchestrator>,
198 authoring: &AgentConfigAuthoringService,
199 force: bool,
200) -> Result<(), String> {
201 CliService::info(&format!("Deleting agent '{}'...", agent_name));
202
203 let process_stopped = stop_agent_process(agent_name, agent_port, orchestrator).await;
204
205 if !process_stopped && !force {
206 let msg = format!(
207 "Failed to stop agent '{}' process. Use --force to delete anyway.",
208 agent_name
209 );
210 CliService::error(&msg);
211 return Err(msg);
212 }
213
214 if !process_stopped && force {
215 CliService::warning(&format!(
216 "Force deleting agent '{}' (process may still be running)",
217 agent_name
218 ));
219 }
220
221 match authoring.delete(agent_name) {
222 Ok(()) => {
223 CliService::success(&format!("Agent '{}' deleted", agent_name));
224 Ok(())
225 },
226 Err(e) => {
227 CliService::error(&format!("Failed to delete agent '{}': {}", agent_name, e));
228 Err(format!("{}: {}", agent_name, e))
229 },
230 }
231}
232
233async fn stop_agent_process(
234 agent_name: &str,
235 agent_port: Option<u16>,
236 orchestrator: Option<&AgentOrchestrator>,
237) -> bool {
238 if let Some(orch) = orchestrator {
239 match orch.delete_agent(agent_name).await {
240 Ok(()) => {
241 tracing::debug!(agent = %agent_name, "Agent stopped via orchestrator");
242 return true;
243 },
244 Err(e) => {
245 tracing::debug!(
246 agent = %agent_name,
247 error = %e,
248 "Orchestrator termination failed, trying port-based cleanup"
249 );
250 },
251 }
252 }
253
254 let Some(port) = agent_port else {
255 tracing::debug!(agent = %agent_name, "No port configured, assuming not running");
256 return true;
257 };
258
259 if ProcessCleanup::check_port(port).is_none() {
260 tracing::debug!(agent = %agent_name, port, "No process on port, assuming stopped");
261 return true;
262 }
263
264 CliService::info(&format!(
265 "Stopping agent '{}' on port {}...",
266 agent_name, port
267 ));
268
269 let Some(pid) = ProcessCleanup::check_port(port) else {
270 tracing::warn!(agent = %agent_name, port, "No process found on port to stop");
271 return false;
272 };
273
274 if !ProcessCleanup::kill_process(pid) {
275 tracing::warn!(agent = %agent_name, port, pid, "Failed to kill process on port");
276 return false;
277 }
278
279 tracing::debug!(agent = %agent_name, port, pid, "Killed process on port");
280 true
281}