magi-code 0.105.1

Repository-aware CLI coding agent for terminal work
Documentation
//! Bounded, allowlisted task telemetry. No task context, reasoning or result text.
use super::*;
use crate::output::{ActivityMetadata, NormalizedUsageAggregate};
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct TaskTelemetry {
    pub(crate) index: usize,
    pub(crate) scoped_id: Option<String>,
    pub(crate) intent: Option<String>,
    pub(crate) profile: Option<String>,
    pub(crate) agent: Option<String>,
    pub(crate) provider: Option<String>,
    pub(crate) model: Option<String>,
    pub(crate) outcome: Option<String>,
    pub(crate) usage: Option<NormalizedUsageAggregate>,
    pub(crate) activity: Vec<String>,
    pub(crate) activity_omitted: Option<u64>,
    pub(crate) text_limited: bool,
    pub(crate) changed_paths: Option<u64>,
    pub(crate) calls_failed: Option<u64>,
    pub(crate) calls_unknown: Option<u64>,
    pub(crate) calls_changed: Option<u64>,
}

impl TaskTelemetry {
    pub(crate) fn new(index: usize) -> Self {
        Self {
            index,
            scoped_id: None,
            intent: None,
            profile: None,
            agent: None,
            provider: None,
            model: None,
            outcome: None,
            usage: None,
            activity: Vec::new(),
            activity_omitted: None,
            text_limited: false,
            changed_paths: None,
            calls_failed: None,
            calls_unknown: None,
            calls_changed: None,
        }
    }

    pub(crate) fn set_intent(&mut self, intent: &str) {
        self.intent = Some(self.select_text(intent));
    }

    fn select_text(&mut self, text: &str) -> String {
        let safe = sanitized_single_line(text);
        let mut end = safe.len().min(240);
        while !safe.is_char_boundary(end) {
            end -= 1;
        }
        self.text_limited |= end != safe.len();
        safe[..end].to_owned()
    }

    pub(crate) fn metadata(&mut self, metadata: &ActivityMetadata) {
        for (key, value) in &metadata.fields {
            if matches!(
                key.as_str(),
                "intent" | "identity" | "agent" | "provider" | "model"
            ) {
                let value = Some(self.select_text(value));
                match key.as_str() {
                    "intent" => self.intent = value,
                    "identity" => self.profile = value,
                    "agent" => self.agent = value,
                    "provider" => self.provider = value,
                    "model" => self.model = value,
                    _ => unreachable!(),
                }
            }
        }
        if metadata
            .fields
            .iter()
            .any(|(key, value)| key == "outcome" && value == "unknown")
        {
            self.outcome = Some("unknown".into());
        }
    }

    pub(crate) fn activity(&mut self, text: &str) {
        if self.activity.len() < 8 {
            let text = self.select_text(text);
            self.activity.push(text);
        } else {
            self.activity_omitted = self.activity_omitted.map(|count| count.saturating_add(1));
            self.text_limited = true;
        }
    }

    pub(crate) fn block(&self, persistence: &str) -> ToolBlock {
        let mut rows = Vec::new();
        let mut fact = |label: &str, value: Option<String>| {
            rows.push(ResultRow {
                label: label.into(),
                value,
                outcome: ResultOutcome::Unknown,
            })
        };
        for (label, value) in [
            ("Task execution identity", &self.scoped_id),
            ("Task intent", &self.intent),
            ("Task profile", &self.profile),
            ("Task agent", &self.agent),
            ("Task provider", &self.provider),
            ("Task model", &self.model),
            ("Task outcome", &self.outcome),
        ] {
            fact(label, value.clone());
        }
        fact("Task persistence", Some(persistence.into()));
        fact("Task facts limited", Some(self.text_limited.to_string()));
        fact(
            "Task changed paths",
            self.changed_paths.map(|n| n.to_string()),
        );
        fact(
            "Task failed calls",
            self.calls_failed.map(|n| n.to_string()),
        );
        fact(
            "Task unknown calls",
            self.calls_unknown.map(|n| n.to_string()),
        );
        fact(
            "Task calls with effects",
            self.calls_changed.map(|n| n.to_string()),
        );
        fact(
            "Task activity count",
            self.activity_omitted.map(|omitted| {
                omitted
                    .saturating_add(self.activity.len() as u64)
                    .to_string()
            }),
        );
        fact(
            "Task activity omitted",
            self.activity_omitted.map(|n| n.to_string()),
        );
        fact(
            "Usage provenance",
            self.usage.map(|_| "normalized_provider_usage".into()),
        );
        for (label, value) in [
            (
                "Usage effective input tokens",
                self.usage.map(|u| u.whole_run.effective_input),
            ),
            (
                "Usage output tokens",
                self.usage.map(|u| u.whole_run.output),
            ),
            (
                "Usage cache read tokens",
                self.usage
                    .filter(|u| u.whole_run.cache_known)
                    .map(|u| u.whole_run.cache_read),
            ),
            (
                "Usage latest request sequence",
                self.usage.and_then(|u| u.latest_request_sequence),
            ),
            (
                "Usage latest effective input tokens",
                self.usage.and_then(|u| u.latest).map(|u| u.effective_input),
            ),
            (
                "Usage latest output tokens",
                self.usage.and_then(|u| u.latest).map(|u| u.output),
            ),
            (
                "Usage latest cache read tokens",
                self.usage
                    .and_then(|u| u.latest)
                    .filter(|u| u.cache_known)
                    .map(|u| u.cache_read),
            ),
        ] {
            fact(label, value.map(|n| n.to_string()));
        }
        fact(
            "Usage cache known",
            self.usage.map(|u| u.whole_run.cache_known.to_string()),
        );
        fact(
            "Usage latest final",
            self.usage.map(|u| u.latest_final.to_string()),
        );
        ToolBlock::Results { rows }
    }

    fn valid(&self) -> bool {
        self.scoped_id
            .as_ref()
            .is_none_or(|id| !id.is_empty() && id.len() <= 1024 && sanitize_display_text(id) == *id)
            && [
                &self.intent,
                &self.profile,
                &self.agent,
                &self.provider,
                &self.model,
            ]
            .into_iter()
            .flatten()
            .all(|text| text.len() <= 240 && sanitized_single_line(text) == *text)
            && self.outcome.as_deref().is_none_or(|v| {
                matches!(
                    v,
                    "success" | "failure" | "cancelled" | "running" | "unknown"
                )
            })
            && self.activity.len() <= 8
            && self
                .activity
                .iter()
                .all(|text| text.len() <= 240 && sanitized_single_line(text) == *text)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct TaskBatchTelemetry {
    pub(crate) schema_version: u8,
    pub(crate) tasks: Vec<TaskTelemetry>,
}
impl TaskBatchTelemetry {
    pub(crate) fn parse(value: &Value) -> Option<Self> {
        if serde_json::to_vec(value).ok()?.len() > 64 * 1024 {
            return None;
        }
        let report: Self = serde_json::from_value(value.clone()).ok()?;
        if report.schema_version != 1
            || report.tasks.is_empty()
            || report.tasks.len() > crate::output::MAX_SUBAGENT_TASKS
            || !report
                .tasks
                .iter()
                .enumerate()
                .all(|(index, task)| task.index == index && task.valid())
        {
            return None;
        }
        let mut identities = std::collections::HashSet::new();
        if report
            .tasks
            .iter()
            .filter_map(|task| task.scoped_id.as_ref())
            .any(|id| !identities.insert(id))
        {
            return None;
        }
        // Nullable fields must exist. Legacy/malformed records must not default to fabricated facts.
        if report
            .tasks
            .iter()
            .zip(value["tasks"].as_array()?)
            .any(|(task, source)| serde_json::to_value(task).ok().as_ref() != Some(source))
        {
            return None;
        }
        Some(report)
    }

    pub(crate) fn parse_for_tasks(value: &Value, params: &Value) -> Option<Self> {
        let report = Self::parse(value)?;
        let tasks = params["tasks"].as_array()?;
        if tasks.len() != report.tasks.len()
            || !tasks.iter().zip(&report.tasks).all(|(source, task)| {
                let Some(intent) = source["intent"].as_str() else {
                    return false;
                };
                let mut expected = TaskTelemetry::new(task.index);
                expected.set_intent(intent);
                task.intent == expected.intent
            })
        {
            return None;
        }
        Some(report)
    }

    pub(crate) fn matches_parent(&self, parent: &crate::output::ActivityId) -> bool {
        self.tasks.iter().all(|task| {
            task.scoped_id.as_deref() == Some(parent.child(format!("g{}", task.index + 1)).as_str())
        })
    }

    pub(crate) fn block(&self) -> ToolBlock {
        let count = |outcome: &str| {
            self.tasks
                .iter()
                .filter(|task| task.outcome.as_deref() == Some(outcome))
                .count()
        };
        let rows = [
            ("Tasks total", self.tasks.len()),
            ("Tasks done", count("success")),
            ("Tasks failed", count("failure")),
            ("Tasks running", count("running")),
            (
                "Tasks unknown",
                self.tasks
                    .iter()
                    .filter(|task| {
                        task.outcome.is_none() || task.outcome.as_deref() == Some("unknown")
                    })
                    .count(),
            ),
            ("Tasks cancelled", count("cancelled")),
        ]
        .into_iter()
        .map(|(label, count)| ResultRow {
            label: label.into(),
            value: Some(count.to_string()),
            outcome: ResultOutcome::Unknown,
        })
        .collect();
        ToolBlock::Results { rows }
    }
}