use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::mpsc::Sender;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ActivityType {
Thinking,
Analyzing,
LlmCall,
LlmWaiting,
LlmResponse,
ToolDiscovery,
ToolExecuting,
ToolComplete,
ToolFailed,
Memory,
McpCall,
Validation,
Warning,
Error,
Info,
Debug,
Started,
Completed,
Cancelled,
}
impl ActivityType {
pub fn icon(&self) -> &'static str {
match self {
ActivityType::Thinking => "đ§ ",
ActivityType::Analyzing => "đ",
ActivityType::LlmCall => "đ¤",
ActivityType::LlmWaiting => "âŗ",
ActivityType::LlmResponse => "đĨ",
ActivityType::ToolDiscovery => "đ§",
ActivityType::ToolExecuting => "âī¸",
ActivityType::ToolComplete => "â",
ActivityType::ToolFailed => "â",
ActivityType::Memory => "đž",
ActivityType::McpCall => "đ",
ActivityType::Validation => "đ",
ActivityType::Warning => "â ī¸",
ActivityType::Error => "â",
ActivityType::Info => "âšī¸",
ActivityType::Debug => "đ",
ActivityType::Started => "âļ",
ActivityType::Completed => "â",
ActivityType::Cancelled => "âš",
}
}
pub fn color(&self) -> &'static str {
match self {
ActivityType::Thinking | ActivityType::Analyzing => "cyan",
ActivityType::LlmCall | ActivityType::LlmWaiting | ActivityType::LlmResponse => "blue",
ActivityType::ToolDiscovery => "magenta",
ActivityType::ToolExecuting => "yellow",
ActivityType::ToolComplete | ActivityType::Completed => "green",
ActivityType::ToolFailed | ActivityType::Error => "red",
ActivityType::Memory => "cyan",
ActivityType::McpCall => "magenta",
ActivityType::Validation => "blue",
ActivityType::Warning => "yellow",
ActivityType::Info | ActivityType::Debug => "gray",
ActivityType::Started => "green",
ActivityType::Cancelled => "yellow",
}
}
pub fn label(&self) -> &'static str {
match self {
ActivityType::Thinking => "THINK",
ActivityType::Analyzing => "ANALYZE",
ActivityType::LlmCall => "LLMâ",
ActivityType::LlmWaiting => "WAIT",
ActivityType::LlmResponse => "LLMâ",
ActivityType::ToolDiscovery => "TOOLS",
ActivityType::ToolExecuting => "EXEC",
ActivityType::ToolComplete => "DONE",
ActivityType::ToolFailed => "FAIL",
ActivityType::Memory => "MEM",
ActivityType::McpCall => "MCP",
ActivityType::Validation => "VALID",
ActivityType::Warning => "WARN",
ActivityType::Error => "ERROR",
ActivityType::Info => "INFO",
ActivityType::Debug => "DEBUG",
ActivityType::Started => "START",
ActivityType::Completed => "DONE",
ActivityType::Cancelled => "CANCEL",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivityEvent {
pub activity_type: ActivityType,
pub message: String,
pub timestamp: DateTime<Utc>,
pub details: Option<ActivityDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivityDetails {
pub tool_name: Option<String>,
pub tool_args: Option<String>,
pub duration_ms: Option<u64>,
pub tokens: Option<TokenCount>,
pub error: Option<String>,
pub metadata: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenCount {
pub input: u32,
pub output: u32,
}
impl ActivityEvent {
pub fn new(activity_type: ActivityType, message: impl Into<String>) -> Self {
Self {
activity_type,
message: message.into(),
timestamp: Utc::now(),
details: None,
}
}
pub fn with_details(mut self, details: ActivityDetails) -> Self {
self.details = Some(details);
self
}
pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
let details = self.details.get_or_insert(ActivityDetails {
tool_name: None,
tool_args: None,
duration_ms: None,
tokens: None,
error: None,
metadata: None,
});
details.tool_name = Some(tool_name.into());
self
}
pub fn with_args(mut self, args: impl Into<String>) -> Self {
let details = self.details.get_or_insert(ActivityDetails {
tool_name: None,
tool_args: None,
duration_ms: None,
tokens: None,
error: None,
metadata: None,
});
details.tool_args = Some(args.into());
self
}
pub fn with_duration(mut self, duration_ms: u64) -> Self {
let details = self.details.get_or_insert(ActivityDetails {
tool_name: None,
tool_args: None,
duration_ms: None,
tokens: None,
error: None,
metadata: None,
});
details.duration_ms = Some(duration_ms);
self
}
pub fn with_tokens(mut self, input: u32, output: u32) -> Self {
let details = self.details.get_or_insert(ActivityDetails {
tool_name: None,
tool_args: None,
duration_ms: None,
tokens: None,
error: None,
metadata: None,
});
details.tokens = Some(TokenCount { input, output });
self
}
pub fn with_error(mut self, error: impl Into<String>) -> Self {
let details = self.details.get_or_insert(ActivityDetails {
tool_name: None,
tool_args: None,
duration_ms: None,
tokens: None,
error: None,
metadata: None,
});
details.error = Some(error.into());
self
}
pub fn format_display(&self) -> String {
let icon = self.activity_type.icon();
let label = self.activity_type.label();
let time = self.timestamp.format("%H:%M:%S");
let mut output = format!("[{}] {} {}: {}", time, icon, label, self.message);
if let Some(ref details) = self.details {
if let Some(ref tool) = details.tool_name {
output.push_str(&format!(" [{}]", tool));
}
if let Some(duration) = details.duration_ms {
output.push_str(&format!(" ({}ms)", duration));
}
if let Some(ref tokens) = details.tokens {
output.push_str(&format!(" [{}â{}]", tokens.input, tokens.output));
}
}
output
}
pub fn format_compact(&self) -> String {
let icon = self.activity_type.icon();
let mut output = format!("{} {}", icon, self.message);
if let Some(ref details) = self.details {
if let Some(ref tool) = details.tool_name {
output.push_str(&format!(" [{}]", tool));
}
if let Some(duration) = details.duration_ms {
output.push_str(&format!(" ({}ms)", duration));
}
}
output
}
}
impl ActivityEvent {
pub fn thinking(message: impl Into<String>) -> Self {
Self::new(ActivityType::Thinking, message)
}
pub fn analyzing(message: impl Into<String>) -> Self {
Self::new(ActivityType::Analyzing, message)
}
pub fn llm_call(message: impl Into<String>) -> Self {
Self::new(ActivityType::LlmCall, message)
}
pub fn llm_waiting() -> Self {
Self::new(ActivityType::LlmWaiting, "Waiting for LLM response...")
}
pub fn llm_response(input_tokens: u32, output_tokens: u32) -> Self {
Self::new(ActivityType::LlmResponse, "Received LLM response")
.with_tokens(input_tokens, output_tokens)
}
pub fn tool_discovery(count: usize) -> Self {
Self::new(
ActivityType::ToolDiscovery,
format!("Discovered {} available tools", count),
)
}
pub fn tool_executing(tool_name: impl Into<String>, args: Option<String>) -> Self {
let name = tool_name.into();
let msg = format!("Executing tool: {}", name);
let mut event = Self::new(ActivityType::ToolExecuting, msg).with_tool(&name);
if let Some(a) = args {
let truncated = if a.len() > 100 {
format!("{}...", &a[..100])
} else {
a
};
event = event.with_args(truncated);
}
event
}
pub fn tool_complete(tool_name: impl Into<String>, duration_ms: u64) -> Self {
let name = tool_name.into();
Self::new(ActivityType::ToolComplete, format!("Tool completed: {}", name))
.with_tool(name)
.with_duration(duration_ms)
}
pub fn tool_failed(tool_name: impl Into<String>, error: impl Into<String>) -> Self {
let name = tool_name.into();
Self::new(ActivityType::ToolFailed, format!("Tool failed: {}", name))
.with_tool(name)
.with_error(error)
}
pub fn memory(operation: &str, key: &str) -> Self {
Self::new(
ActivityType::Memory,
format!("Memory {}: {}", operation, key),
)
}
pub fn mcp_call(server: &str, method: &str) -> Self {
Self::new(ActivityType::McpCall, format!("MCP {} â {}", server, method))
}
pub fn warning(message: impl Into<String>) -> Self {
Self::new(ActivityType::Warning, message)
}
pub fn error(message: impl Into<String>) -> Self {
Self::new(ActivityType::Error, message)
}
pub fn info(message: impl Into<String>) -> Self {
Self::new(ActivityType::Info, message)
}
pub fn started(agent_name: &str) -> Self {
Self::new(
ActivityType::Started,
format!("Starting execution for agent: {}", agent_name),
)
}
pub fn completed(duration_ms: u64) -> Self {
Self::new(
ActivityType::Completed,
format!("Execution completed in {}ms", duration_ms),
)
.with_duration(duration_ms)
}
pub fn cancelled() -> Self {
Self::new(ActivityType::Cancelled, "Execution cancelled by user")
}
}
#[derive(Clone)]
pub struct ActivityLogger {
sender: Sender<ActivityEvent>,
}
impl ActivityLogger {
pub fn new(sender: Sender<ActivityEvent>) -> Self {
Self { sender }
}
pub fn log(&self, event: ActivityEvent) {
let _ = self.sender.send(event);
}
pub fn thinking(&self, message: impl Into<String>) {
self.log(ActivityEvent::thinking(message));
}
pub fn analyzing(&self, message: impl Into<String>) {
self.log(ActivityEvent::analyzing(message));
}
pub fn llm_call(&self, message: impl Into<String>) {
self.log(ActivityEvent::llm_call(message));
}
pub fn llm_waiting(&self) {
self.log(ActivityEvent::llm_waiting());
}
pub fn llm_response(&self, input_tokens: u32, output_tokens: u32) {
self.log(ActivityEvent::llm_response(input_tokens, output_tokens));
}
pub fn tool_executing(&self, tool_name: impl Into<String>, args: Option<String>) {
self.log(ActivityEvent::tool_executing(tool_name, args));
}
pub fn tool_complete(&self, tool_name: impl Into<String>, duration_ms: u64) {
self.log(ActivityEvent::tool_complete(tool_name, duration_ms));
}
pub fn tool_failed(&self, tool_name: impl Into<String>, error: impl Into<String>) {
self.log(ActivityEvent::tool_failed(tool_name, error));
}
pub fn warning(&self, message: impl Into<String>) {
self.log(ActivityEvent::warning(message));
}
pub fn error(&self, message: impl Into<String>) {
self.log(ActivityEvent::error(message));
}
pub fn info(&self, message: impl Into<String>) {
self.log(ActivityEvent::info(message));
}
pub fn started(&self, agent_name: &str) {
self.log(ActivityEvent::started(agent_name));
}
pub fn completed(&self, duration_ms: u64) {
self.log(ActivityEvent::completed(duration_ms));
}
pub fn cancelled(&self) {
self.log(ActivityEvent::cancelled());
}
}
pub struct NoopActivityLogger;
impl NoopActivityLogger {
pub fn log(&self, _event: ActivityEvent) {}
pub fn thinking(&self, _message: impl Into<String>) {}
pub fn analyzing(&self, _message: impl Into<String>) {}
pub fn llm_call(&self, _message: impl Into<String>) {}
pub fn llm_waiting(&self) {}
pub fn llm_response(&self, _input_tokens: u32, _output_tokens: u32) {}
pub fn tool_executing(&self, _tool_name: impl Into<String>, _args: Option<String>) {}
pub fn tool_complete(&self, _tool_name: impl Into<String>, _duration_ms: u64) {}
pub fn tool_failed(&self, _tool_name: impl Into<String>, _error: impl Into<String>) {}
pub fn warning(&self, _message: impl Into<String>) {}
pub fn error(&self, _message: impl Into<String>) {}
pub fn info(&self, _message: impl Into<String>) {}
pub fn started(&self, _agent_name: &str) {}
pub fn completed(&self, _duration_ms: u64) {}
pub fn cancelled(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_activity_event_creation() {
let event = ActivityEvent::thinking("Processing user request");
assert_eq!(event.activity_type, ActivityType::Thinking);
assert_eq!(event.message, "Processing user request");
}
#[test]
fn test_activity_event_with_details() {
let event = ActivityEvent::tool_executing("kubectl", Some("get pods".to_string()));
assert!(event.details.is_some());
let details = event.details.unwrap();
assert_eq!(details.tool_name, Some("kubectl".to_string()));
}
#[test]
fn test_activity_event_formatting() {
let event = ActivityEvent::tool_complete("kubectl", 234);
let formatted = event.format_compact();
assert!(formatted.contains("â"));
assert!(formatted.contains("234ms"));
}
#[test]
fn test_activity_type_icons() {
assert_eq!(ActivityType::Thinking.icon(), "đ§ ");
assert_eq!(ActivityType::ToolExecuting.icon(), "âī¸");
assert_eq!(ActivityType::Error.icon(), "â");
}
}