Skip to main content

systemprompt_cli/commands/admin/agents/
process_stop.rs

1//! Stopping an agent's process before its configuration is deleted: through
2//! the orchestrator first, then only the recorded, marker-verified pid that
3//! holds the agent's port.
4//!
5//! Copyright (c) systemprompt.io — Business Source License 1.1.
6//! See <https://systemprompt.io> for licensing details.
7
8use std::time::Duration;
9
10use systemprompt_agent::services::agent_orchestration::{AgentOrchestrator, AgentStatus};
11use systemprompt_identifiers::{AgentName, ServiceName};
12use systemprompt_loader::subprocess::{self, ChildKind, StopOutcome};
13use systemprompt_logging::CliService;
14use systemprompt_scheduler::port_holders;
15
16const AGENT_STOP_GRACE: Duration = Duration::from_secs(2);
17
18pub async fn stop_agent_process(
19    agent_name: &AgentName,
20    agent_port: Option<u16>,
21    orchestrator: &AgentOrchestrator,
22) -> bool {
23    let recorded_pid = match orchestrator.get_status(agent_name).await {
24        Ok(AgentStatus::Running { pid, .. }) => Some(pid),
25        Ok(AgentStatus::Failed { .. }) => None,
26        Err(e) => {
27            tracing::warn!(
28                agent = %agent_name,
29                error = %e,
30                "Could not read the agent's recorded process"
31            );
32            return false;
33        },
34    };
35
36    match orchestrator.delete_agent(agent_name).await {
37        Ok(()) => {
38            tracing::debug!(agent = %agent_name, "Agent stopped via orchestrator");
39            true
40        },
41        Err(e) => {
42            tracing::warn!(
43                agent = %agent_name,
44                error = %e,
45                "Orchestrator termination failed; stopping only a verified agent process"
46            );
47            stop_verified_port_holder(agent_name, agent_port, recorded_pid).await
48        },
49    }
50}
51
52pub async fn stop_verified_port_holder(
53    agent_name: &AgentName,
54    agent_port: Option<u16>,
55    recorded_pid: Option<u32>,
56) -> bool {
57    let Some(port) = agent_port else {
58        return recorded_pid.is_none();
59    };
60
61    let holders = match port_holders(port).await {
62        Ok(holders) => holders,
63        Err(e) => {
64            tracing::warn!(
65                agent = %agent_name,
66                port,
67                error = %e,
68                "Could not read the port's holders"
69            );
70            return false;
71        },
72    };
73    let Some(&holder) = holders.first() else {
74        tracing::debug!(agent = %agent_name, port, "No process on port; agent is stopped");
75        return true;
76    };
77
78    if let Some(&stranger) = holders.iter().find(|pid| Some(**pid) != recorded_pid) {
79        CliService::warning(&format!(
80            "Process {stranger} holds port {port} but is not the recorded process of agent \
81             '{agent_name}'; refusing to kill it"
82        ));
83        return false;
84    }
85
86    CliService::info(&format!(
87        "Stopping agent '{}' (pid {}) on port {}...",
88        agent_name, holder, port
89    ));
90    let service = ServiceName::of_agent(agent_name);
91    match subprocess::stop_owned(holder, ChildKind::Agent, &service, AGENT_STOP_GRACE).await {
92        Ok(StopOutcome::Stopped(_) | StopOutcome::NotRunning) => true,
93        Ok(StopOutcome::NotOurs) => {
94            CliService::warning(&format!(
95                "Process {holder} is agent '{agent_name}''s recorded pid but carries no matching \
96                 spawn marker; refusing to kill it"
97            ));
98            false
99        },
100        Err(e) => {
101            tracing::warn!(
102                agent = %agent_name,
103                port,
104                pid = holder,
105                error = %e,
106                "Failed to stop agent process"
107            );
108            false
109        },
110    }
111}