magi-code 0.96.5

Repository-aware CLI coding agent for terminal work
Documentation
//! Schema-1 display DTO selection. Internal detail is consumed, never retained or serialized.
pub(super) mod history;

pub(super) fn descriptor() -> serde_json::Value {
    serde_json::json!({"schema_version":1,"event":"tool.card","history_field":"tool_card","snapshot_parts":1,
        "card_bytes":16384,"core_bytes":2048,"cards_per_run":4096,"snapshots_per_run":16384,
        "run_snapshot_bytes":33554432,"retained_card_bytes":8388608,"blocks_per_card":16,
        "rows_per_block":32,"diff_lines_per_block":128,"children_per_block":32,"children_depth":8,
        "string_bytes":1024,"title_bytes":256,"metadata_bytes":512,"preview_bytes":4096})
}

use crate::output::{
    ActivityStatus, ToolActivityDetail,
    tool_card::{
        self, ChangeState, DiffApplication, OmissionReason, OmissionSubject,
        PublicProtectionDecision, TextMeaning, ToolBlock, ToolCardInput, ToolCardRole, ToolOutcome,
    },
};
use serde_json::{Value, json};

pub(super) const CARD_BYTES: usize = 16_384;
pub(super) const CORE_BYTES: usize = 2048;
pub(super) const CARDS_PER_RUN: usize = 4096;
pub(super) const RETAINED_BYTES: usize = 8 * 1024 * 1024;
// Initial and terminal cores/slots are never charged to optional updates.
pub(super) const OPTIONAL_BYTES: usize = 16 * 1024 * 1024;
pub(super) const OPTIONAL_SNAPSHOTS: usize = 8192;

pub(super) fn initial_card(id: &str, subagent: bool) -> Value {
    json!({
        "schema_version":1,"card_id":id,"activity_id":id,
        "parent_activity_id":null,"parent_state":"root","task_id":null,
        "tool_kind":if subagent { "subagent" } else { "other" },
        "state":"running","outcome":null,"changes":"unknown","revision":0,"origin":"live",
        "detail":{"availability":"unavailable","reason":"missing_source"},
        "title":if subagent { "Subagent" } else { "Tool" },
        "metadata":{"tool_name":null,"resource_path":null,"working_directory":null,
            "public_protection":{"decision":"not_reported","summary":null}},
        "blocks":[],"omissions":[{"subject":"blocks","reason":"missing_source","omitted_count":null}]
    })
}

pub(super) fn outcome(status: ActivityStatus) -> Option<&'static str> {
    match status {
        ActivityStatus::Success => Some("success"),
        ActivityStatus::Failed => Some("failure"),
        ActivityStatus::Canceled => Some("cancelled"),
        _ => None,
    }
}

pub(super) fn project_detail(card: &mut Value, detail: &ToolActivityDetail, terminal: bool) {
    let command = detail.params["command"]
        .as_str()
        .or_else(|| detail.params["cmd"].as_str());
    let projected = tool_card::project_tool_card(&ToolCardInput {
        tool_name: &detail.tool_name,
        detail: Some(detail),
        status: Some(detail.status),
        interrupted: false,
        bash_command: command,
        bash_mode_response: false,
        preview: "",
        body: "",
        children: &[],
        children_complete: false,
    });
    let previous_outcome = card["outcome"].clone();
    let previous_changes = card["changes"].clone();
    let protection = card["metadata"]["public_protection"].clone();
    let previous_metadata_omission = card["omissions"]
        .as_array()
        .unwrap()
        .iter()
        .find(|omission| omission["subject"] == "metadata")
        .cloned();
    card["tool_kind"] = json!(match projected.role {
        ToolCardRole::Read | ToolCardRole::ViewImage => "read",
        ToolCardRole::Grep
        | ToolCardRole::AstGrep
        | ToolCardRole::Find
        | ToolCardRole::ListFiles => "search",
        ToolCardRole::Write => "write",
        ToolCardRole::HashEdit => "edit",
        ToolCardRole::Bash => "command",
        ToolCardRole::Web => "web",
        ToolCardRole::CodeMode => "code_mode",
        ToolCardRole::SubagentBatch => "subagent",
        _ if detail.tool_name.starts_with("mcp_") => "mcp",
        _ => "other",
    });
    card["title"] = json!(projected.title);
    // Never parse the terminal metadata strings into routing/resource fields.
    card["metadata"]["tool_name"] = json!(crate::output::sanitize_display_text(&detail.tool_name));
    card["metadata"]["public_protection"] = json!({"decision": match projected.public_protection.decision {
        PublicProtectionDecision::NotReported => "not_reported", PublicProtectionDecision::Allowed => "allowed",
        PublicProtectionDecision::Blocked => "blocked", PublicProtectionDecision::ApprovalRequired => "approval_required",
        PublicProtectionDecision::Denied => "denied",
    }, "summary":projected.public_protection.summary});
    let restored_protection = card["metadata"]["public_protection"]["decision"] == "not_reported";
    if card["metadata"]["public_protection"]["decision"] == "not_reported" {
        card["metadata"]["public_protection"] = protection.clone();
    }
    card["changes"] = json!(match projected.changes {
        ChangeState::NotApplicable => "not_applicable",
        ChangeState::None => "none",
        ChangeState::Applied => "applied",
        ChangeState::Partial => "partial",
        ChangeState::Unknown => "unknown",
    });
    if previous_changes == "partial"
        || (card["changes"] == "unknown" && previous_changes != "unknown")
        || (previous_changes == "applied" && card["changes"] != "partial")
    {
        card["changes"] = previous_changes;
    }
    if terminal && previous_outcome.is_null() {
        card["outcome"] = json!(projected.outcome.map(|outcome| match outcome {
            ToolOutcome::Success => "success",
            ToolOutcome::Failure => "failure",
            ToolOutcome::Cancelled => "cancelled",
            ToolOutcome::Interrupted => "interrupted",
            ToolOutcome::Unknown => "unknown",
        }));
    }
    card["blocks"] = json!(projected.blocks.iter().map(block).collect::<Vec<_>>());
    card["omissions"] = json!([]);
    if restored_protection
        && protection["decision"] != "not_reported"
        && let Some(omission) = previous_metadata_omission
    {
        omit(card, "metadata", omission["reason"].as_str().unwrap(), None);
    }
    for omission in projected.omissions {
        omit(
            card,
            match omission.subject {
                OmissionSubject::Title => "title",
                OmissionSubject::Metadata => "metadata",
                OmissionSubject::Blocks => "blocks",
                OmissionSubject::Rows => "rows",
                OmissionSubject::Text => "text",
                OmissionSubject::DiffLines => "diff_lines",
                OmissionSubject::Children => "children",
                OmissionSubject::Progress => "progress",
            },
            match omission.reason {
                OmissionReason::Redacted => "redacted",
                OmissionReason::MissingSource => "missing_source",
                OmissionReason::NotRecorded => "not_recorded",
                OmissionReason::UnsupportedSource => "unsupported_source",
                OmissionReason::SizeLimit => "size_limit",
            },
            omission.omitted_count,
        );
    }
    if detail.applied_diff.is_some()
        && !card["blocks"]
            .as_array()
            .unwrap()
            .iter()
            .any(|block| block["type"] == "diff")
    {
        omit(card, "diff_lines", "unsupported_source", None);
    }
    bound_card(card);
}

fn block(block: &ToolBlock) -> Value {
    match block {
        ToolBlock::Text { lines } => {
            json!({"type":"text","text":lines.iter().map(|line| line.plain_text()).collect::<Vec<_>>().join("\n"),"format":"plain","language":null})
        }
        ToolBlock::Command {
            command,
            working_directory,
            exit_code,
        } => {
            json!({"type":"command","command":command,"working_directory":working_directory,"exit_code":exit_code.and_then(|code| i32::try_from(code).ok()),"signal":null})
        }
        ToolBlock::Resources { rows } => {
            json!({"type":"resources","rows":rows.iter().map(|row| json!({"kind":"resource","label":row.line.plain_text(),"location":row.location,"availability":"unknown"})).collect::<Vec<_>>()})
        }
        ToolBlock::Diff { application, lines } => {
            json!({"type":"diff","old_path":null,"new_path":null,"application":match application { DiffApplication::Applied => "applied", DiffApplication::Partial => "partial", DiffApplication::Unknown => "unknown" },"lines":lines.iter().map(|line| {
            let kind = if line.spans.iter().any(|span| span.meaning == TextMeaning::Added) { "added" } else { "removed" };
            let text = line.plain_text();
            json!({"kind":kind,"text":text.strip_prefix(if kind == "added" { '+' } else { '-' }).unwrap_or(&text),"old_line":null,"new_line":null})
        }).collect::<Vec<_>>()})
        }
        ToolBlock::Progress {
            label,
            completed,
            total,
        } => {
            json!({"type":"progress","label":label,"completed":completed,"total":total,"unit":"items"})
        }
    }
}

fn reason_priority(reason: &str) -> usize {
    [
        "redacted",
        "missing_source",
        "not_recorded",
        "unsupported_source",
        "budget_limit",
        "size_limit",
    ]
    .iter()
    .position(|value| *value == reason)
    .unwrap_or(6)
}
pub(super) fn omit(card: &mut Value, subject: &str, reason: &str, count: Option<usize>) {
    let omissions = card["omissions"].as_array_mut().expect("card omissions");
    if let Some(existing) = omissions
        .iter_mut()
        .find(|value| value["subject"] == subject)
    {
        if reason_priority(reason) < reason_priority(existing["reason"].as_str().unwrap_or("")) {
            existing["reason"] = json!(reason);
        }
        existing["omitted_count"] = Value::Null; // Selected views may overlap.
    } else {
        omissions.push(json!({"subject":subject,"reason":reason,"omitted_count":count}));
    }
    update_availability(card);
}
fn update_availability(card: &mut Value) {
    let reason = card["omissions"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|value| value["reason"].as_str())
        .min_by_key(|reason| reason_priority(reason))
        .map(str::to_owned);
    card["detail"] = match reason {
        Some(reason) => {
            json!({"availability":if card["blocks"].as_array().unwrap().is_empty() { "unavailable" } else { "truncated" },"reason":reason})
        }
        None => json!({"availability":"complete","reason":null}),
    };
}
fn bound_strings(
    value: &mut Value,
    key: &str,
    losses: &mut Vec<(&'static str, &'static str, Option<usize>)>,
) {
    match value {
        Value::String(text) => {
            if matches!(
                key,
                "activity_id" | "card_id" | "parent_activity_id" | "task_id"
            ) {
                return;
            }
            let limit = match key {
                "title" => 256,
                "tool_name" | "resource_path" | "working_directory" | "old_path" | "new_path" => {
                    512
                }
                "summary" => 256,
                "language" | "signal" => 64,
                "command" => 4096,
                "text" => 1024,
                _ => 1024,
            };
            let subject = match key {
                "title" => "title",
                "tool_name" | "resource_path" | "working_directory" | "summary" => "metadata",
                _ => "text",
            };
            let safe = crate::output::sanitize_display_text(text);
            if safe != *text {
                *text = safe;
                losses.push((subject, "redacted", None));
            }
            if text.contains("<redacted>") {
                losses.push((subject, "redacted", None));
            }
            if text.len() > limit {
                let mut end = limit;
                while !text.is_char_boundary(end) {
                    end -= 1;
                }
                losses.push((subject, "size_limit", Some(text[end..].chars().count())));
                text.truncate(end);
            }
        }
        Value::Array(values) => {
            for value in values {
                bound_strings(value, key, losses);
            }
        }
        Value::Object(fields) => {
            for (key, value) in fields {
                bound_strings(value, key, losses);
            }
        }
        Value::Number(number)
            if number
                .as_u64()
                .is_some_and(|number| number > 9_007_199_254_740_991) =>
        {
            *value = Value::Null;
            losses.push(("progress", "unsupported_source", None));
        }
        _ => {}
    }
}
pub(super) fn bytes(card: &Value) -> usize {
    serde_json::to_vec(card).expect("wire card").len()
}
pub(super) fn bound_card(card: &mut Value) {
    let mut losses = Vec::new();
    bound_strings(card, "", &mut losses);
    for (subject, reason, count) in losses {
        omit(card, subject, reason, count);
    }
    // Combined selected text/command previews never exceed the descriptor's source bound.
    let mut preview_bytes = 0;
    let mut preview_omitted = false;
    card["blocks"].as_array_mut().unwrap().retain(|block| {
        let size = block["text"].as_str().map_or(0, str::len)
            + block["command"].as_str().map_or(0, str::len);
        if preview_bytes + size > 4096 {
            preview_omitted = true;
            false
        } else {
            preview_bytes += size;
            true
        }
    });
    if preview_omitted {
        omit(card, "text", "size_limit", None);
    }
    while bytes(card) > CARD_BYTES && !card["blocks"].as_array().unwrap().is_empty() {
        let removed = card["blocks"].as_array_mut().unwrap().pop().unwrap();
        if removed["type"] == "children" {
            omit(card, "children", "size_limit", None);
        }
        omit(card, "blocks", "size_limit", None);
    }
    if bytes(card) > CARD_BYTES {
        make_core(card, "size_limit");
    }
    update_availability(card);
}
pub(super) fn make_core(card: &mut Value, reason: &str) {
    if !card["blocks"].as_array().unwrap().is_empty() {
        if card["blocks"]
            .as_array()
            .unwrap()
            .iter()
            .any(|block| block["type"] == "children")
        {
            omit(card, "children", reason, None);
        }
        card["blocks"] = json!([]);
        omit(card, "blocks", reason, None);
    }
    if card["title"] != "Tool" {
        card["title"] = json!("Tool");
        omit(card, "title", reason, None);
    }
    for key in ["tool_name", "resource_path", "working_directory"] {
        if !card["metadata"][key].is_null() {
            card["metadata"][key] = Value::Null;
            omit(card, "metadata", reason, None);
        }
    }
    if !card["metadata"]["public_protection"]["summary"].is_null() {
        card["metadata"]["public_protection"]["summary"] = Value::Null;
        omit(card, "metadata", reason, None);
    }
    update_availability(card);
}