Skip to content
Merged
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
Binary file modified .gitignore
Binary file not shown.
50 changes: 42 additions & 8 deletions crates/mofa-cli/src/commands/agent/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::context::CliContext;
use crate::output::Table;
use chrono::Utc;
use colored::Colorize;
use serde::Serialize;
use std::collections::BTreeMap;
Expand All @@ -18,6 +19,8 @@ pub async fn run(ctx: &CliContext, running_only: bool, _show_all: bool) -> anyho
.map_err(|e| anyhow::anyhow!("Failed to list persisted agents: {}", e))?;

let mut merged: BTreeMap<String, AgentInfo> = BTreeMap::new();

// Process live agents from the registry first
for m in &agents_metadata {
let status = format!("{:?}", m.state);
let is_running = is_running_state(&status);
Expand All @@ -28,23 +31,26 @@ pub async fn run(ctx: &CliContext, running_only: bool, _show_all: bool) -> anyho
name: m.name.clone(),
status,
is_running,
uptime: None, // Live registry currently doesn't provide start time easily
provider: None,
model: None,
description: m.description.clone(),
},
);
}

// Merge in persisted agents
for (_, entry) in persisted_agents {
merged.entry(entry.id.clone()).or_insert_with(|| {
let status = if is_running_state(&entry.state) {
format!("{} (persisted)", entry.state)
} else {
entry.state
};
let status = entry.state.clone();
AgentInfo {
id: entry.id,
name: entry.name,
status,
is_running: false,
status: status.clone(),
is_running: is_running_state(&status),
uptime: Some(format_duration(Utc::now() - entry.started_at)),
provider: entry.provider,
model: entry.model,
description: entry.description,
}
});
Expand Down Expand Up @@ -84,17 +90,45 @@ pub async fn run(ctx: &CliContext, running_only: bool, _show_all: bool) -> anyho
Ok(())
}

/// Formats a duration into a human-readable string (e.g., "2h 15m", "45s").
fn format_duration(duration: chrono::Duration) -> String {
let seconds = duration.num_seconds();
if seconds <= 0 {
return "0s".to_string();
}
if seconds < 60 {
format!("{}s", seconds)
} else if seconds < 3600 {
format!("{}m {}s", seconds / 60, seconds % 60)
} else {
let hours = seconds / 3600;
let mins = (seconds % 3600) / 60;
format!("{}h {}m", hours, mins)
}
}

/// Agent information for display purposes.
#[derive(Debug, Clone, Serialize)]
struct AgentInfo {
id: String,
name: String,
status: String,
#[serde(skip_serializing)]
is_running: bool,
uptime: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}

/// Checks if a status string represents a running or ready agent.
fn is_running_state(status: &str) -> bool {
status == "Running" || status == "Ready"
let s = status.to_lowercase();
matches!(
s.as_str(),
"running" | "ready" | "executing" | "initializing" | "paused"
)
}
11 changes: 10 additions & 1 deletion crates/mofa-cli/src/commands/agent/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::config::loader::ConfigLoader;
use crate::context::{AgentConfigEntry, CliContext};
use chrono::Utc;
use colored::Colorize;

/// Execute the `mofa agent start` command
Expand Down Expand Up @@ -82,8 +83,12 @@ pub async fn run(
id: agent_id.to_string(),
name: agent_config.name.clone(),
state: "Running".to_string(),
started_at: Utc::now(),
provider: None, // Could be extracted from agent_config in the future
model: None, // Could be extracted from agent_config in the future
description: agent_config.description.clone(),
};

if let Err(e) = ctx.agent_store.save(agent_id, &entry) {
let rollback_result = ctx.agent_registry.unregister(agent_id).await;
match rollback_result {
Expand All @@ -105,7 +110,11 @@ pub async fn run(
}
}

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

Ok(())
}
Expand Down
17 changes: 17 additions & 0 deletions crates/mofa-cli/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::store::PersistedStore;
use crate::utils::paths;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use mofa_foundation::agent::base::BaseAgent;
use mofa_foundation::agent::components::tool::EchoTool;
use mofa_foundation::agent::session::SessionManager;
Expand All @@ -25,11 +26,24 @@ const BUILTIN_HTTP_PLUGIN_KIND: &str = "builtin:http";
const BUILTIN_ECHO_TOOL_KIND: &str = "builtin:echo";
const CLI_BASE_FACTORY_KIND: &str = "cli-base";

/// Persistent entry for agent configuration and state metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfigEntry {
/// Unique identifier for the agent.
pub id: String,
/// Human-readable name of the agent.
pub name: String,
/// Last known execution state (e.g., "Running", "Ready").
pub state: String,
/// Timestamp when the agent was last started.
pub started_at: DateTime<Utc>,
/// Optional AI provider used by the agent (e.g., "openai", "anthropic").
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
/// Optional AI model name (e.g., "gpt-4", "claude-3").
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// Optional description of the agent's purpose.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
Expand Down Expand Up @@ -340,6 +354,9 @@ mod tests {
id: "persisted-agent".to_string(),
name: "Persisted Agent".to_string(),
state: "Running".to_string(),
started_at: Utc::now(),
provider: None,
model: None,
description: None,
};
ctx1.agent_store.save("persisted-agent", &entry).unwrap();
Expand Down