magi-code 0.96.2

Repository-aware CLI coding agent for terminal work
Documentation
use super::resolve_session_scope_path;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum SessionEventKind {
    UserInput,
    AssistantChunk,
    AssistantOutput,
    ProviderResponseItem,
    ReasoningSummary,
    ToolCall,
    ToolResult,
    ToolResultAssessment,
    ToolDisplayResult,
    CodeModeToolCall,
    CodeModeToolResult,
    CodeModeContext,
    CodeModeWarning,
    CodeModeAssessment,
    SubagentSession,
    TaskScope,
    ProviderContextItem,
    HookDiagnostic,
    HookLifecycle,
    HookContextInjection,
    TurnStatus,
    AbortRecovery,
    ProviderStreamTrace,
    ContextCache,
    SessionTitle,
    Compaction,
    Diagnostic,
    Rewind,
    SubdirInstructionLoad,
    TtsrInjection,
    SkillSuggestion,
    SessionUsage,
    SessionActiveTime,
    SessionPreferences,
}

impl SessionEventKind {
    pub(crate) const fn as_str(self) -> &'static str {
        match self {
            Self::UserInput => "user_input",
            Self::AssistantChunk => "assistant_chunk",
            Self::AssistantOutput => "assistant_output",
            Self::ProviderResponseItem => "provider_response_item",
            Self::ReasoningSummary => "reasoning_summary",
            Self::ToolCall => "tool_call",
            Self::ToolResult => "tool_result",
            Self::ToolResultAssessment => "tool_result_assessment",
            Self::ToolDisplayResult => "tool_display_result",
            Self::CodeModeToolCall => "code_mode_tool_call",
            Self::CodeModeToolResult => "code_mode_tool_result",
            Self::CodeModeContext => "code_mode_context",
            Self::CodeModeWarning => "code_mode_warning",
            Self::CodeModeAssessment => "code_mode_assessment",
            Self::SubagentSession => "subagent_session",
            Self::TaskScope => "task_scope",
            Self::ProviderContextItem => "provider_context_item",
            Self::HookDiagnostic => "hook_diagnostic",
            Self::HookLifecycle => "hook_lifecycle",
            Self::HookContextInjection => "hook_context_injection",
            Self::TurnStatus => "turn_status",
            Self::AbortRecovery => "abort_recovery",
            Self::ProviderStreamTrace => "provider_stream_trace",
            Self::ContextCache => "context_cache",
            Self::SessionTitle => "session_title",
            Self::Compaction => "compaction",
            Self::Diagnostic => "diagnostic",
            Self::Rewind => "rewind",
            Self::SubdirInstructionLoad => "subdir_instruction_load",
            Self::TtsrInjection => "ttsr_injection",
            Self::SkillSuggestion => "skill_suggestion",
            Self::SessionUsage => "session_usage",
            Self::SessionActiveTime => "session_active_time",
            Self::SessionPreferences => "session_preferences",
        }
    }

    fn parse(value: &str) -> Option<Self> {
        match value {
            "user_input" => Some(Self::UserInput),
            "assistant_chunk" => Some(Self::AssistantChunk),
            "assistant_output" => Some(Self::AssistantOutput),
            "provider_response_item" => Some(Self::ProviderResponseItem),
            "reasoning_summary" => Some(Self::ReasoningSummary),
            "tool_call" => Some(Self::ToolCall),
            "tool_result" => Some(Self::ToolResult),
            "tool_result_assessment" => Some(Self::ToolResultAssessment),
            "tool_display_result" => Some(Self::ToolDisplayResult),
            "code_mode_tool_call" => Some(Self::CodeModeToolCall),
            "code_mode_tool_result" => Some(Self::CodeModeToolResult),
            "code_mode_context" => Some(Self::CodeModeContext),
            "code_mode_warning" => Some(Self::CodeModeWarning),
            "code_mode_assessment" => Some(Self::CodeModeAssessment),
            "subagent_session" => Some(Self::SubagentSession),
            "task_scope" => Some(Self::TaskScope),
            "provider_context_item" => Some(Self::ProviderContextItem),
            "hook_diagnostic" => Some(Self::HookDiagnostic),
            "hook_lifecycle" => Some(Self::HookLifecycle),
            "hook_context_injection" => Some(Self::HookContextInjection),
            "turn_status" => Some(Self::TurnStatus),
            "abort_recovery" => Some(Self::AbortRecovery),
            "provider_stream_trace" => Some(Self::ProviderStreamTrace),
            "context_cache" => Some(Self::ContextCache),
            "session_title" => Some(Self::SessionTitle),
            "compaction" => Some(Self::Compaction),
            "diagnostic" => Some(Self::Diagnostic),
            "rewind" => Some(Self::Rewind),
            "subdir_instruction_load" => Some(Self::SubdirInstructionLoad),
            "ttsr_injection" => Some(Self::TtsrInjection),
            "skill_suggestion" => Some(Self::SkillSuggestion),
            "session_usage" => Some(Self::SessionUsage),
            "session_active_time" => Some(Self::SessionActiveTime),
            "session_preferences" => Some(Self::SessionPreferences),
            _ => None,
        }
    }

    pub(crate) const fn is_local_only(self) -> bool {
        matches!(
            self,
            Self::HookDiagnostic
                | Self::ToolDisplayResult
                | Self::CodeModeToolCall
                | Self::CodeModeToolResult
                | Self::CodeModeContext
                | Self::CodeModeWarning
                | Self::CodeModeAssessment
                | Self::ToolResultAssessment
                | Self::SubagentSession
                | Self::TaskScope
                | Self::HookLifecycle
                | Self::HookContextInjection
                | Self::TurnStatus
                | Self::AbortRecovery
                | Self::ProviderStreamTrace
                | Self::ContextCache
                | Self::SessionTitle
                | Self::Diagnostic
                | Self::Rewind
                | Self::SubdirInstructionLoad
                | Self::TtsrInjection
                | Self::SkillSuggestion
                | Self::SessionUsage
                | Self::SessionActiveTime
                | Self::SessionPreferences
        )
    }
}

impl AsRef<str> for SessionEventKind {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum TurnStatus {
    Incomplete,
    #[serde(alias = "canceled")]
    Cancelled,
    Failed,
    CompactionRequired,
    Complete,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct TurnStatusPayload {
    pub(crate) status: TurnStatus,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) assistant_text: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) error_summary: Option<String>,
}

#[derive(Deserialize)]
struct RawTurnStatusPayload {
    status: TurnStatus,
    #[serde(default)]
    assistant_text: Option<String>,
    #[serde(default)]
    error_summary: Option<String>,
}

impl<'de> Deserialize<'de> for TurnStatusPayload {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let raw = RawTurnStatusPayload::deserialize(deserializer)?;
        Ok(Self::new_with_error_summary(
            raw.status,
            raw.assistant_text.as_deref(),
            raw.error_summary.as_deref(),
        ))
    }
}

const TURN_STATUS_ERROR_SUMMARY_MAX_CHARS: usize = 2_000;

fn sanitize_turn_status_error_summary(summary: &str) -> Option<String> {
    let sanitized = crate::output::sanitize_display_text(summary);
    let trimmed = sanitized.trim();
    (!trimmed.is_empty()).then(|| {
        trimmed
            .chars()
            .take(TURN_STATUS_ERROR_SUMMARY_MAX_CHARS)
            .collect()
    })
}

impl TurnStatusPayload {
    pub(crate) fn new(status: TurnStatus, assistant_text: Option<&str>) -> Self {
        Self::new_with_error_summary(status, assistant_text, None)
    }

    pub(crate) fn new_with_error_summary(
        status: TurnStatus,
        assistant_text: Option<&str>,
        error_summary: Option<&str>,
    ) -> Self {
        Self {
            status,
            assistant_text: assistant_text.map(str::to_string),
            error_summary: (status == TurnStatus::Failed)
                .then(|| error_summary.and_then(sanitize_turn_status_error_summary))
                .flatten(),
        }
    }

    pub(crate) fn into_value(self) -> serde_json::Result<Value> {
        serde_json::to_value(self)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct SessionEvent {
    pub(crate) event_type: String,
    pub(crate) timestamp: DateTime<Utc>,
    pub(crate) session_id: String,
    #[serde(default)]
    pub(crate) cwd: PathBuf,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) session_path: Option<PathBuf>,
    pub(crate) payload: Value,
}

impl SessionEvent {
    pub(crate) fn new(
        event_type: impl Into<String>,
        session_id: String,
        cwd: PathBuf,
        payload: Value,
    ) -> Self {
        Self {
            event_type: event_type.into(),
            timestamp: Utc::now(),
            session_id,
            cwd: cwd.clone(),
            session_path: Some(resolve_session_scope_path(&cwd)),
            payload,
        }
    }

    pub(crate) fn new_kind(
        kind: SessionEventKind,
        session_id: String,
        cwd: PathBuf,
        payload: Value,
    ) -> Self {
        Self::new(kind.as_str(), session_id, cwd, payload)
    }

    pub(crate) fn kind(&self) -> Option<SessionEventKind> {
        SessionEventKind::parse(&self.event_type)
    }

    pub(crate) fn turn_status_payload(&self) -> Option<TurnStatusPayload> {
        if self.kind() != Some(SessionEventKind::TurnStatus) {
            return None;
        }
        let status = self
            .payload
            .get("status")
            .cloned()
            .and_then(|value| serde_json::from_value::<TurnStatus>(value).ok())?;
        let assistant_text = self
            .payload
            .get("assistant_text")
            .and_then(Value::as_str)
            .map(str::to_string);
        let error_summary = (status == TurnStatus::Failed)
            .then(|| {
                self.payload
                    .get("error_summary")
                    .and_then(Value::as_str)
                    .and_then(sanitize_turn_status_error_summary)
            })
            .flatten();
        Some(TurnStatusPayload {
            status,
            assistant_text,
            error_summary,
        })
    }
}