systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use systemprompt_logging::{AiRequestInfo, CliService, ExecutionStep, TaskInfo};
use tabled::settings::Style;
use tabled::{Table, Tabled};

#[derive(Debug, Tabled)]
pub struct TaskInfoRow {
    #[tabled(rename = "Task ID")]
    pub task_id: String,
    #[tabled(rename = "Agent")]
    pub agent_name: String,
    #[tabled(rename = "Status")]
    pub status: String,
    #[tabled(rename = "Started")]
    pub started_at: String,
    #[tabled(rename = "Duration")]
    pub duration: String,
}

#[derive(Debug, Tabled)]
pub struct StepRow {
    #[tabled(rename = "#")]
    pub step_number: i32,
    #[tabled(rename = "Type")]
    pub step_type: String,
    #[tabled(rename = "Title")]
    pub title: String,
    #[tabled(rename = "Status")]
    pub status: String,
    #[tabled(rename = "Duration")]
    pub duration: String,
}

#[derive(Debug, Tabled)]
pub struct AiRequestRow {
    #[tabled(rename = "Model")]
    pub model: String,
    #[tabled(rename = "Max")]
    pub max_tokens: String,
    #[tabled(rename = "Tokens")]
    pub tokens: String,
    #[tabled(rename = "Cost")]
    pub cost: String,
    #[tabled(rename = "Latency")]
    pub latency: String,
}

#[derive(Debug, Tabled)]
pub struct ToolCallRow {
    #[tabled(rename = "Tool")]
    pub tool_name: String,
    #[tabled(rename = "Server")]
    pub server: String,
    #[tabled(rename = "Status")]
    pub status: String,
    #[tabled(rename = "Duration")]
    pub duration: String,
}

#[derive(Debug, Tabled)]
pub struct ArtifactRow {
    #[tabled(rename = "ID")]
    pub artifact_id: String,
    #[tabled(rename = "Type")]
    pub artifact_type: String,
    #[tabled(rename = "Name")]
    pub name: String,
    #[tabled(rename = "Source")]
    pub source: String,
    #[tabled(rename = "Tool")]
    pub tool_name: String,
}

pub fn truncate(s: &str, max_len: usize) -> String {
    let s = s.replace('\n', " ").replace('\r', "");
    if s.len() > max_len {
        format!("{}...", &s[..max_len.saturating_sub(3)])
    } else {
        s
    }
}

pub fn print_section(title: &str) {
    CliService::section(title);
}

pub fn print_content_block(content: &str) {
    for line in content.lines() {
        CliService::info(&format!("  {line}"));
    }
}

pub fn print_task_info(task_info: &TaskInfo) {
    let rows = vec![TaskInfoRow {
        task_id: task_info.task_id.as_str()[..8].to_string(),
        agent_name: task_info
            .agent_name
            .clone()
            .unwrap_or_else(|| "-".to_string()),
        status: task_info.status.clone(),
        started_at: task_info
            .started_at
            .map_or_else(|| "-".to_string(), |t| t.format("%H:%M:%S").to_string()),
        duration: task_info
            .execution_time_ms
            .map_or_else(|| "-".to_string(), |ms| format!("{}ms", ms)),
    }];

    print_section("TASK");
    let table = Table::new(rows).with(Style::rounded()).to_string();
    CliService::info(&table);

    if task_info.status == "failed" {
        if let Some(ref error) = task_info.error_message {
            if !error.is_empty() {
                CliService::error("Error:");
                print_content_block(error);
            }
        }
    }
}

pub fn print_user_input(input: Option<&String>) {
    if let Some(text) = input {
        print_section("USER INPUT");
        CliService::info(&format!("  {text}"));
    }
}

pub fn print_agent_response(response: Option<&String>) {
    if let Some(text) = response {
        print_section("AGENT RESPONSE");
        print_content_block(text);
    }
}

pub fn print_execution_steps(steps: &[ExecutionStep]) {
    if steps.is_empty() {
        return;
    }

    let step_rows: Vec<StepRow> = steps
        .iter()
        .enumerate()
        .map(|(i, s)| StepRow {
            step_number: (i + 1) as i32,
            step_type: s.step_type.clone().unwrap_or_else(|| "unknown".to_string()),
            title: truncate(&s.title.clone().unwrap_or_else(String::new), 40),
            status: s.status.clone(),
            duration: s
                .duration_ms
                .map_or_else(|| "-".to_string(), |ms| format!("{}ms", ms)),
        })
        .collect();

    print_section("EXECUTION STEPS");
    let table = Table::new(step_rows).with(Style::rounded()).to_string();
    CliService::info(&table);

    for step in steps {
        if step.status == "failed" {
            if let Some(ref error) = step.error_message {
                if !error.is_empty() {
                    let step_type = step.step_type.clone().unwrap_or_else(|| "step".to_string());
                    CliService::error(&format!("  {step_type} failed:"));
                    print_content_block(error);
                }
            }
        }
    }
}

pub fn print_ai_requests(requests: &[AiRequestInfo]) -> Vec<String> {
    if requests.is_empty() {
        return vec![];
    }

    let request_ids: Vec<String> = requests.iter().map(|r| r.id.to_string()).collect();

    let ai_rows: Vec<AiRequestRow> = requests
        .iter()
        .map(|r| AiRequestRow {
            model: format!("{}/{}", r.provider, r.model),
            max_tokens: r
                .max_tokens
                .map_or_else(|| "-".to_string(), |t| t.to_string()),
            tokens: format!(
                "{} (in:{}, out:{})",
                r.input_tokens.unwrap_or(0) + r.output_tokens.unwrap_or(0),
                r.input_tokens.unwrap_or(0),
                r.output_tokens.unwrap_or(0)
            ),
            cost: format!("${:.4}", r.cost_microdollars as f64 / 1_000_000.0),
            latency: r
                .latency_ms
                .map_or_else(|| "-".to_string(), |ms| format!("{}ms", ms)),
        })
        .collect();

    print_section("AI REQUESTS");
    let table = Table::new(ai_rows).with(Style::rounded()).to_string();
    CliService::info(&table);

    request_ids
}