Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/mofa-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ indicatif = "0.17"
comfy-table = "7"
dirs-next = "2"

# Process management
nix = { version = "0.29", features = ["process", "signal"] }

# Database (optional, for db init command)
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "mysql", "sqlite"], optional = true }

Expand Down
15 changes: 14 additions & 1 deletion crates/mofa-cli/src/commands/agent/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub async fn run(ctx: &CliContext, running_only: bool, _show_all: bool) -> anyho
println!(" No agents registered.");
println!();
println!(
" Use {} to start an agent.",
" Use {} to register an agent.",
"mofa agent start <agent_id>".cyan()
);
return Ok(());
Expand All @@ -81,9 +81,22 @@ pub async fn run(ctx: &CliContext, running_only: bool, _show_all: bool) -> anyho
println!("{}", table);
}

println!();
println!(" Total: {} agent(s)", filtered.len());

Ok(())
}

/// Format timestamp as human-readable string
fn format_timestamp(millis: u64) -> String {
use chrono::{DateTime, Local};
use std::time::UNIX_EPOCH;

let duration = std::time::Duration::from_millis(millis);
let datetime = DateTime::<Local>::from(UNIX_EPOCH + duration);
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
}

#[derive(Debug, Clone, Serialize)]
struct AgentInfo {
id: String,
Expand Down
50 changes: 35 additions & 15 deletions crates/mofa-cli/src/commands/agent/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
use crate::config::loader::ConfigLoader;
use crate::context::{AgentConfigEntry, CliContext};
use colored::Colorize;
use std::path::{Path, PathBuf};
use tracing::info;

/// Execute the `mofa agent start` command
pub async fn run(
Expand All @@ -18,20 +20,30 @@ pub async fn run(
println!(" Mode: {}", "daemon".yellow());
}

// Check if agent is already registered
if ctx.agent_registry.contains(agent_id).await {
anyhow::bail!("Agent '{}' is already registered", agent_id);
// Check if agent already exists
if ctx.persistent_agents.exists(agent_id).await {
let existing = ctx.persistent_agents.get(agent_id).await;
if let Some(agent) = existing {
if agent.last_state == crate::state::AgentProcessState::Running {
anyhow::bail!(
"Agent '{}' is already running (PID: {})",
agent_id,
agent.process_id.unwrap_or(0)
);
}
println!(
" {} Agent exists but is not running. Restarting...",
"!".yellow()
);
}
}

// Load agent configuration
let agent_config = if let Some(path) = config_path {
// Load or discover agent configuration
let (config_file, agent_name) = if let Some(path) = config_path {
println!(" Config: {}", path.display().to_string().cyan());
let loader = ConfigLoader::new();
let cli_config = loader.load(path)?;
println!(" Agent: {}", cli_config.agent.name.white());
ctx.process_manager.validate_config(path)?;

// Convert CLI AgentConfig to kernel AgentConfig
mofa_kernel::agent::config::AgentConfig::new(agent_id, &cli_config.agent.name)
(path.to_path_buf(), agent_id.to_string())
} else {
// Try to auto-discover configuration
let loader = ConfigLoader::new();
Expand All @@ -41,17 +53,20 @@ pub async fn run(
" Config: {} (auto-discovered)",
found_path.display().to_string().cyan()
);
let cli_config = loader.load(&found_path)?;
println!(" Agent: {}", cli_config.agent.name.white());
mofa_kernel::agent::config::AgentConfig::new(agent_id, &cli_config.agent.name)
ctx.process_manager.validate_config(&found_path)?;

(found_path, agent_id.to_string())
}
None => {
println!(" {} No config file found, using defaults", "!".yellow());
mofa_kernel::agent::config::AgentConfig::new(agent_id, agent_id)
(PathBuf::new(), agent_id.to_string())
}
}
};

// Create agent config
let agent_config = mofa_kernel::agent::config::AgentConfig::new(agent_id, &agent_name);

// Check if a matching factory type is available
let mut factory_types = ctx.agent_registry.list_factory_types().await;
if factory_types.is_empty() {
Expand All @@ -72,6 +87,11 @@ pub async fn run(
);
}

// Check if agent already exists
if ctx.agent_registry.contains(agent_id).await {
anyhow::bail!("Agent '{}' is already registered", agent_id);
}

// Try to create via factory
ctx.agent_registry
.create_and_register(&selected_factory, agent_config.clone())
Expand Down Expand Up @@ -105,7 +125,7 @@ pub async fn run(
}
}

println!("{} Agent '{}' started", "✓".green(), agent_id);
println!("{} Agent '{}' started successfully", "✓".green(), agent_id);

Ok(())
}
Expand Down
42 changes: 24 additions & 18 deletions crates/mofa-cli/src/commands/agent/stop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::context::CliContext;
use colored::Colorize;
use tracing::info;

/// Execute the `mofa agent stop` command
pub async fn run(
Expand All @@ -11,37 +12,42 @@ pub async fn run(
) -> anyhow::Result<()> {
println!("{} Stopping agent: {}", "→".green(), agent_id.cyan());

// Check if agent exists in registry or store
let in_registry = ctx.agent_registry.contains(agent_id).await;

let previous_entry = ctx
.agent_store
.get(agent_id)
.map_err(|e| anyhow::anyhow!("Failed to load persisted agent '{}': {}", agent_id, e))?;

// When commands run in separate CLI invocations, runtime registry state can be absent.
// In that case, treat stop as a persisted-state transition if the agent exists on disk.
if !ctx.agent_registry.contains(agent_id).await {
if let Some(mut entry) = previous_entry.clone() {
if !force_persisted_stop {
anyhow::bail!(
"Agent '{}' is not active in runtime registry. Use --force-persisted-stop to mark persisted state as Stopped.",
agent_id
);
}
let in_store = previous_entry.is_some();

if !in_registry && !in_store {
anyhow::bail!("Agent '{}' not found", agent_id);
}

// When commands run in separate CLI invocations, runtime registry state can be absent.
// In that case, if force_persisted_stop is true, update the persisted state.
if !in_registry && in_store && force_persisted_stop {
if let Some(mut entry) = previous_entry {
entry.state = "Stopped".to_string();
ctx.agent_store
.save(agent_id, &entry)
.map_err(|e| anyhow::anyhow!("Failed to update agent '{}': {}", agent_id, e))?;

println!(
"{} Agent '{}' was not running; updated persisted state to Stopped",
"!".yellow(),
"{} Agent '{}' persisted state updated to Stopped",
"✓".green(),
agent_id
);
return Ok(());
}
}

// If not in registry and no force flag, error out
if !in_registry {
anyhow::bail!(
"Agent '{}' not found in registry or persisted store",
"Agent '{}' is not active in runtime registry. Use --force-persisted-stop to update persisted state.",
agent_id
);
}
Expand Down Expand Up @@ -91,13 +97,13 @@ pub async fn run(
agent_id
);
} else {
println!(
"{} Agent '{}' was not in the registry",
"!".yellow(),
agent_id
);
println!(" {} Agent is not running", "!".yellow());
}

println!("{} Agent '{}' stopped", "✓".green(), agent_id);

info!("Agent '{}' stopped", agent_id);

Ok(())
}

Expand Down
24 changes: 24 additions & 0 deletions crates/mofa-cli/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! CLI context providing access to backend services
use crate::state::PersistentAgentRegistry;
use crate::store::PersistedStore;
use crate::utils::AgentProcessManager;
use crate::utils::paths;
use async_trait::async_trait;
use mofa_foundation::agent::base::BaseAgent;
Expand Down Expand Up @@ -62,6 +64,10 @@ pub struct CliContext {
pub plugin_store: PersistedStore<PluginSpecEntry>,
/// Persistent tool source specifications
pub tool_store: PersistedStore<ToolSpecEntry>,
/// Persistent agent state storage
pub persistent_agents: Arc<PersistentAgentRegistry>,
/// Agent process manager for spawning/managing processes
pub process_manager: AgentProcessManager,
/// In-memory plugin registry
pub plugin_registry: Arc<SimplePluginRegistry>,
/// In-memory tool registry
Expand Down Expand Up @@ -94,12 +100,21 @@ impl CliContext {
let mut tool_registry = ToolRegistry::new();
replay_persisted_tools(&mut tool_registry, &tool_store)?;

let agents_dir = data_dir.join("agents");
let persistent_agents = Arc::new(PersistentAgentRegistry::new(agents_dir).await.map_err(
|e| anyhow::anyhow!("Failed to initialize persistent agent registry: {}", e),
)?);

let process_manager = AgentProcessManager::new(config_dir.clone());

Ok(Self {
session_manager,
agent_registry,
agent_store,
plugin_store,
tool_store,
persistent_agents,
process_manager,
plugin_registry,
tool_registry,
data_dir,
Expand Down Expand Up @@ -132,12 +147,21 @@ impl CliContext {
let mut tool_registry = ToolRegistry::new();
replay_persisted_tools(&mut tool_registry, &tool_store)?;

let agents_dir = data_dir.join("agents");
let persistent_agents = Arc::new(PersistentAgentRegistry::new(agents_dir).await.map_err(
|e| anyhow::anyhow!("Failed to initialize persistent agent registry: {}", e),
)?);

let process_manager = AgentProcessManager::new(config_dir.clone());

Ok(Self {
session_manager,
agent_registry,
agent_store,
plugin_store,
tool_store,
persistent_agents,
process_manager,
plugin_registry,
tool_registry,
data_dir,
Expand Down
1 change: 1 addition & 0 deletions crates/mofa-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod config;
mod context;
mod output;
mod render;
mod state;
mod store;
mod tui;
mod utils;
Expand Down
Loading
Loading