impyard 0.1.1

Rent the intelligence, own the governance — a control plane for imps: software colleagues whose every action passes through a gateway you control (default-deny egress, injected credentials, budgets, approval gates, audit).
//! `impyard server runs` inspection — every execution, including warm Discord
//! sessions that intentionally bypass the durable task queue.

use crate::imp::context as context_compiler;
use crate::imp::journal;
use crate::imp::memory;
use crate::run::runlog;
use crate::util::BErr;
use crate::work::queue;

pub fn ls(imp: Option<&str>, limit: usize, json: bool) -> Result<(), BErr> {
    if limit == 0 {
        return Err("--limit wants a positive integer".into());
    }
    let runs: Vec<_> = runlog::list()
        .into_iter()
        .filter(|run| imp.map(|w| run.imp == w).unwrap_or(true))
        .take(limit)
        .collect();
    if json {
        let rows: Vec<serde_json::Value> = runs
            .iter()
            .map(|run| {
                serde_json::json!({
                    "id": run.id, "imp": run.imp, "kind": run.kind,
                    "state": run.state, "started_at": run.started_at,
                    "ended_at": run.ended_at, "task_id": run.task_id,
                    "channel_id": run.channel_id,
                })
            })
            .collect();
        println!("{}", serde_json::to_string_pretty(&rows)?);
        return Ok(());
    }
    if runs.is_empty() {
        println!("no runs found");
        return Ok(());
    }
    println!(
        "{:<29}  {:<10}  {:<9}  {:<10}  {:<17}  SCOPE/TASK",
        "RUN", "IMP", "KIND", "STATE", "STARTED (UTC)"
    );
    for run in runs {
        let scope = run
            .task_id
            .as_deref()
            .map(|id| format!("task:{id}"))
            .or_else(|| run.channel_id.as_deref().map(|id| format!("channel:{id}")))
            .unwrap_or_else(|| "-".into());
        println!(
            "{:<29}  {:<10}  {:<9}  {:<10}  {:<17}  {}",
            run.id,
            run.imp,
            run.kind,
            run.state,
            short_time(&run.started_at),
            scope
        );
    }
    Ok(())
}

pub fn show(id: &str) -> Result<(), BErr> {
    let run = runlog::resolve(id).map_err(|e| -> BErr { e.into() })?;
    println!("run       {}", run.id);
    println!("imp    {}", run.imp);
    println!("kind      {}", run.kind);
    println!("state     {}", run.state);
    println!("started   {}", run.started_at);
    if let Some(ended) = &run.ended_at {
        println!("ended     {ended}");
    }
    if let Some(task) = &run.task_id {
        println!("task      {task}");
    }
    if let Some(channel) = &run.channel_id {
        println!("channel   {channel}");
    }
    if let Some(user) = &run.user_id {
        println!("user      {user}");
    }
    if let Some(record) = &run.record {
        if let Some(ended_by) = &record.ended_by {
            println!("ended by  {ended_by}");
        }
        if let Some(exit) = record.exit_code {
            println!("exit      {exit}");
        }
        if let Some(knowledge) = &record.knowledge {
            println!("knowledge {}", knowledge.state);
            println!("  mode    {}", knowledge.mode);
            println!("  base    {}", knowledge.base_commit);
            println!("  records {}", knowledge.record_namespace);
            if let Some(commit) = &knowledge.produced_commit {
                println!("  commit  {commit}");
            }
            if let Some(error) = &knowledge.error {
                println!("  error   {}", runlog::one_line(error, 200));
            }
        }
    }
    println!("path      {}", run.run_dir.display());

    if let Some(task_id) = &run.task_id {
        if let Some(task) = queue::find(task_id) {
            println!("\ntask prompt:\n{}", task.prompt);
        }
    }

    let conversation = runlog::conversation(&run.run_dir);
    if !conversation.is_empty() {
        let omitted = conversation.len().saturating_sub(50);
        println!(
            "\nconversation ({} message{}):",
            conversation.len(),
            if conversation.len() == 1 { "" } else { "s" }
        );
        if omitted > 0 {
            println!("{omitted} earlier messages omitted");
        }
        for line in conversation.iter().skip(omitted) {
            println!("  {line}");
        }
    }

    let subject = if run.imp == "?" {
        None
    } else {
        Some(format!("org/{}", run.imp))
    };
    let events = subject
        .as_deref()
        .map(|s| journal::for_run(s, &run.id))
        .unwrap_or_default();
    if !events.is_empty() {
        println!(
            "\njournal ({} event{}):",
            events.len(),
            if events.len() == 1 { "" } else { "s" }
        );
        for event in events {
            println!(
                "  {}  {:<18}  {}",
                event.get("ts").and_then(|v| v.as_str()).unwrap_or(""),
                event.get("kind").and_then(|v| v.as_str()).unwrap_or("?"),
                runlog::one_line(
                    &event
                        .get("detail")
                        .cloned()
                        .unwrap_or(serde_json::Value::Null)
                        .to_string(),
                    200
                )
            );
        }
    }

    let recalls = memory::recall_trace(&run.id);
    if !recalls.is_empty() {
        println!(
            "\nmemory recall ({} turn{}):",
            recalls.len(),
            if recalls.len() == 1 { "" } else { "s" }
        );
        for recall in recalls {
            let selected = recall
                .get("selected")
                .and_then(|v| v.as_array())
                .map(|a| {
                    a.iter()
                        .filter_map(|v| v.as_str())
                        .collect::<Vec<_>>()
                        .join(", ")
                })
                .unwrap_or_default();
            println!(
                "  {}  selected: {}",
                recall.get("ts").and_then(|v| v.as_str()).unwrap_or(""),
                if selected.is_empty() { "-" } else { &selected }
            );
        }
    }

    let contexts = context_compiler::trace_events(&run.id);
    if !contexts.is_empty() {
        println!(
            "\ncompiled context ({} event{}):",
            contexts.len(),
            if contexts.len() == 1 { "" } else { "s" }
        );
        for event in contexts {
            let status = event
                .get("status")
                .and_then(|value| value.as_str())
                .unwrap_or("?");
            let phase = event
                .get("phase")
                .and_then(|value| value.as_str())
                .unwrap_or("?");
            let used = event
                .pointer("/budget/used_chars")
                .and_then(|value| value.as_u64())
                .map(|value| format!("{value} chars"))
                .unwrap_or_else(|| "-".into());
            let blocks = event
                .get("blocks")
                .and_then(|value| value.as_array())
                .map(|items| {
                    items
                        .iter()
                        .filter_map(|item| item.get("kind").and_then(|value| value.as_str()))
                        .collect::<Vec<_>>()
                        .join(", ")
                })
                .unwrap_or_default();
            println!(
                "  {:<5} {:<8} {:>10}  {}",
                phase,
                status,
                used,
                if blocks.is_empty() { "-" } else { &blocks }
            );
        }
        println!("  exact prompts: impyard server runs context {}", run.id);
    }

    let files = runlog::files(&run.run_dir);
    if !files.is_empty() {
        println!("\nfiles:");
        for (path, bytes) in files {
            println!("  {:>10}  {path}", human_bytes(bytes));
        }
    }
    Ok(())
}

pub fn context(id: &str, all: bool) -> Result<(), BErr> {
    let run = runlog::resolve(id).map_err(|error| -> BErr { error.into() })?;
    let mut events: Vec<_> = context_compiler::trace_events(&run.id)
        .into_iter()
        .filter(|event| event.get("status").and_then(|value| value.as_str()) == Some("compiled"))
        .collect();
    if events.is_empty() {
        return Err(format!("run {} has no compiled context", run.id).into());
    }
    let session_system = events
        .iter()
        .rev()
        .find_map(|event| {
            event
                .get("system_prompt")
                .and_then(|value| value.as_str())
                .filter(|value| !value.is_empty())
                .map(String::from)
        })
        .unwrap_or_default();
    if !all {
        events = vec![events.pop().unwrap()];
    }
    for (index, event) in events.iter().enumerate() {
        if index > 0 {
            println!();
        }
        println!(
            "=== {} {}{} ===",
            event
                .get("phase")
                .and_then(|value| value.as_str())
                .unwrap_or("?"),
            event
                .get("ts")
                .and_then(|value| value.as_str())
                .unwrap_or(""),
            event
                .get("turn_id")
                .and_then(|value| value.as_str())
                .map(|id| format!(" turn {id}"))
                .unwrap_or_default()
        );
        let event_system = event
            .get("system_prompt")
            .and_then(|value| value.as_str())
            .unwrap_or("");
        let system = if !all && event_system.is_empty() {
            session_system.as_str()
        } else {
            event_system
        };
        let input = event
            .get("input_prompt")
            .and_then(|value| value.as_str())
            .unwrap_or("");
        if !system.is_empty() {
            println!("\n--- system ---\n{system}");
        }
        if !input.is_empty() {
            println!("\n--- input ---\n{input}");
        }
    }
    Ok(())
}

/// The memory recall trace for one session (was: `impyard memory explain`).
pub fn recall(id: &str) -> Result<(), BErr> {
    let run = runlog::resolve(id).map_err(|e| -> BErr { e.into() })?;
    let trace = memory::recall_trace(&run.id);
    if trace.is_empty() {
        println!("no memory recall trace for run {}", run.id);
        return Ok(());
    }
    for event in trace {
        println!("{}", serde_json::to_string_pretty(&event)?);
    }
    Ok(())
}

fn short_time(value: &str) -> String {
    if value.len() >= 16 {
        format!("{} {}", &value[..10], &value[11..16])
    } else {
        value.to_string()
    }
}

fn human_bytes(bytes: u64) -> String {
    if bytes >= 1024 * 1024 {
        format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
    } else if bytes >= 1024 {
        format!("{:.1} KiB", bytes as f64 / 1024.0)
    } else {
        format!("{bytes} B")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn formats_time_and_sizes() {
        assert_eq!(short_time("2026-07-10T21:51:18Z"), "2026-07-10 21:51");
        assert_eq!(human_bytes(12), "12 B");
        assert_eq!(human_bytes(2048), "2.0 KiB");
    }
}