Skip to main content

systemprompt_cli/commands/admin/agents/
delete.rs

1//! `admin agents delete` command with target resolution and orchestrated
2//! teardown.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use anyhow::{Context, Result, anyhow};
8use clap::Args;
9use std::path::Path;
10use std::sync::Arc;
11
12use super::process_stop::stop_agent_process;
13use super::types::AgentDeleteOutput;
14use crate::CliConfig;
15use crate::context::CommandContext;
16use crate::interactive::{Prompter, require_confirmation, resolve_required};
17use crate::shared::CommandOutput;
18use systemprompt_agent::AgentState;
19use systemprompt_agent::services::a2a_server::streaming::webhook_client::HttpWebhookBroadcaster;
20use systemprompt_agent::services::agent_orchestration::AgentOrchestrator;
21use systemprompt_agent::services::config_authoring::AgentConfigAuthoringService;
22use systemprompt_config::ProfileBootstrap;
23use systemprompt_identifiers::AgentName;
24use systemprompt_loader::ConfigLoader;
25use systemprompt_logging::CliService;
26use systemprompt_oauth::JwtValidationProviderImpl;
27
28#[derive(Debug, Args)]
29pub struct DeleteArgs {
30    #[arg(
31        help = "Agent name (required in non-interactive mode)",
32        value_parser = crate::shared::parse_agent_name
33    )]
34    pub name: Option<AgentName>,
35
36    #[arg(long, help = "Delete all agents")]
37    pub all: bool,
38
39    #[arg(short = 'y', long, help = "Skip confirmation prompts")]
40    pub yes: bool,
41
42    #[arg(long, help = "Force delete even if process cannot be stopped")]
43    pub force: bool,
44}
45
46pub(super) async fn execute(args: DeleteArgs, ctx: &CommandContext) -> Result<CommandOutput> {
47    let prompter = ctx.prompter();
48    let config = &ctx.cli;
49    let services_config = ConfigLoader::load().context("Failed to load services configuration")?;
50
51    let agents_to_delete = resolve_targets(&args, prompter, &services_config, config)?;
52
53    require_confirmation(
54        prompter,
55        &delete_confirm_message(args.all, &agents_to_delete),
56        args.yes,
57        config,
58    )?;
59
60    let profile = ProfileBootstrap::get().context("Failed to get profile")?;
61    let authoring = AgentConfigAuthoringService::new(Path::new(&profile.paths.services));
62
63    let orchestrator = build_orchestrator(ctx)
64        .await
65        .context("Cannot stop agents safely without the orchestrator")?;
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
72            .agents
73            .get(agent_name.as_str())
74            .map(|c| c.port);
75        let process_stopped = stop_agent_process(agent_name, agent_port, &orchestrator).await;
76        match delete_single_agent(agent_name, process_stopped, &authoring, args.force) {
77            Ok(()) => deleted.push(agent_name.clone()),
78            Err(error) => errors.push(format!("{error:#}")),
79        }
80    }
81
82    if !deleted.is_empty() {
83        ConfigLoader::reload().with_context(|| {
84            "Agent(s) deleted but configuration validation failed. Please check the configuration."
85        })?;
86    }
87
88    if !errors.is_empty() {
89        return Err(anyhow!(
90            "{}\nFailed to delete:\n{}",
91            delete_success_message(&deleted),
92            errors.join("\n")
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_manifest::ServicesConfig,
106    config: &CliConfig,
107) -> Result<Vec<AgentName>> {
108    let available: Vec<AgentName> = services_config.agents.keys().map(AgentName::new).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            .map(AgentName::new)
119        })?)
120    };
121    validate_delete_targets(requested, &available)
122}
123
124pub fn validate_delete_targets(
125    requested: Option<AgentName>,
126    available: &[AgentName],
127) -> Result<Vec<AgentName>> {
128    let agents = match requested {
129        Some(name) => {
130            if !available.contains(&name) {
131                return Err(anyhow!("Agent '{}' not found", name));
132            }
133            vec![name]
134        },
135        None => available.to_vec(),
136    };
137
138    if agents.is_empty() {
139        return Err(anyhow!("No agents to delete"));
140    }
141
142    Ok(agents)
143}
144
145#[must_use]
146pub fn delete_confirm_message(all: bool, targets: &[AgentName]) -> String {
147    if all {
148        format!("Delete ALL {} agents?", targets.len())
149    } else {
150        format!("Delete agent '{}'?", targets[0])
151    }
152}
153
154#[must_use]
155pub fn delete_success_message(deleted: &[AgentName]) -> String {
156    if deleted.len() == 1 {
157        format!("Agent '{}' deleted successfully", deleted[0])
158    } else {
159        format!("{} agent(s) deleted successfully", deleted.len())
160    }
161}
162
163async fn build_orchestrator(ctx: &CommandContext) -> Result<AgentOrchestrator> {
164    let app = ctx.app_context().await?;
165    let jwt_provider = Arc::new(
166        JwtValidationProviderImpl::from_config().context("Failed to create JWT provider")?,
167    );
168    let broadcaster = HttpWebhookBroadcaster::from_config(app.config())
169        .context("Failed to initialize webhook broadcaster")?;
170
171    let agent_state = Arc::new(AgentState::new(
172        Arc::clone(app.db_pool()),
173        Arc::new(app.config().clone()),
174        jwt_provider,
175        Arc::clone(app.a2a_repositories()),
176        Arc::new(broadcaster),
177    ));
178
179    AgentOrchestrator::new(agent_state, Arc::clone(app.app_paths_arc()), None)
180        .await
181        .context("Failed to initialize agent orchestrator")
182}
183
184pub fn delete_single_agent(
185    agent_name: &AgentName,
186    process_stopped: bool,
187    authoring: &AgentConfigAuthoringService,
188    force: bool,
189) -> Result<()> {
190    CliService::info(&format!("Deleting agent '{}'...", agent_name));
191
192    if !process_stopped && !force {
193        let msg = format!(
194            "Failed to stop agent '{}' process. Use --force to delete anyway.",
195            agent_name
196        );
197        CliService::error(&msg);
198        return Err(anyhow!(msg));
199    }
200
201    if !process_stopped {
202        CliService::warning(&format!(
203            "Force deleting agent '{}' (process may still be running)",
204            agent_name
205        ));
206    }
207
208    match authoring.delete(agent_name) {
209        Ok(()) => {
210            CliService::success(&format!("Agent '{}' deleted", agent_name));
211            Ok(())
212        },
213        Err(e) => {
214            CliService::error(&format!("Failed to delete agent '{}': {}", agent_name, e));
215            Err(e).with_context(|| format!("Failed to delete agent '{agent_name}'"))
216        },
217    }
218}