magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

#[derive(Debug)]
pub struct AgentSession {
    pub(super) provider_id: String,
    pub(super) model: String,
    pub(super) system_prompt: String,
    pub(super) context_budget: ContextBudget,
    pub(super) thinking_level: crate::thinking::ThinkingLevel,
    pub(super) text_verbosity: Option<TextVerbosity>,
    pub(super) thinking_levels: Vec<crate::thinking::ThinkingLevel>,
    pub(super) send_default_reasoning_summary: bool,
    pub(super) reasoning_updates_enabled: bool,
    pub(super) replay_cache: Arc<Mutex<ConversationReplayCache>>,
    pub(super) completion_verification: CompletionVerificationSettings,
    pub(super) show_jev_debug_messages: bool,
    pub(super) skill_suggestion: Option<skill_suggestion::SkillSuggestionContext>,
}

impl Clone for AgentSession {
    fn clone(&self) -> Self {
        Self {
            provider_id: self.provider_id.clone(),
            model: self.model.clone(),
            system_prompt: self.system_prompt.clone(),
            context_budget: self.context_budget.clone(),
            thinking_level: self.thinking_level,
            text_verbosity: self.text_verbosity,
            thinking_levels: self.thinking_levels.clone(),
            send_default_reasoning_summary: self.send_default_reasoning_summary,
            reasoning_updates_enabled: self.reasoning_updates_enabled,
            // Cloned agents can run concurrently for unrelated child sessions. Keep each cache
            // bounded and independent rather than serializing or thrashing across children.
            replay_cache: Arc::new(Mutex::new(ConversationReplayCache::default())),
            completion_verification: self.completion_verification.clone(),
            show_jev_debug_messages: self.show_jev_debug_messages,
            skill_suggestion: self.skill_suggestion.clone(),
        }
    }
}

impl PartialEq for AgentSession {
    fn eq(&self, other: &Self) -> bool {
        self.provider_id == other.provider_id
            && self.model == other.model
            && self.system_prompt == other.system_prompt
            && self.context_budget == other.context_budget
            && self.thinking_level == other.thinking_level
            && self.text_verbosity == other.text_verbosity
            && self.thinking_levels == other.thinking_levels
            && self.send_default_reasoning_summary == other.send_default_reasoning_summary
            && self.reasoning_updates_enabled == other.reasoning_updates_enabled
            && self.completion_verification == other.completion_verification
            && self.show_jev_debug_messages == other.show_jev_debug_messages
            && self.skill_suggestion == other.skill_suggestion
    }
}

impl Eq for AgentSession {}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AgentSessionConfig {
    pub(super) provider_id: String,
    pub(super) model: String,
    pub(super) context_budget: ContextBudget,
    pub(super) thinking_level: crate::thinking::ThinkingLevel,
    pub(super) text_verbosity: Option<TextVerbosity>,
    pub(super) thinking_levels: Vec<crate::thinking::ThinkingLevel>,
    pub(super) send_default_reasoning_summary: bool,
}

impl AgentSessionConfig {
    pub(crate) fn new(provider_id: impl Into<String>, model: impl Into<String>) -> Self {
        Self {
            provider_id: normalize_agent_identifier(provider_id.into(), "provider"),
            model: normalize_agent_identifier(model.into(), "model"),
            context_budget: ContextBudget::default(),
            thinking_level: crate::thinking::ThinkingLevel::Default,
            text_verbosity: None,
            thinking_levels: crate::thinking::default_thinking_levels(),
            send_default_reasoning_summary: false,
        }
    }

    pub(super) fn for_model(model: impl Into<String>) -> Self {
        Self::new(crate::providers::OPENAI_CODEX_PROVIDER, model)
    }

    pub(crate) fn with_context_budget(mut self, budget: ContextBudget) -> Self {
        self.context_budget = budget;
        self
    }

    pub(crate) fn with_thinking_level(
        mut self,
        thinking_level: crate::thinking::ThinkingLevel,
    ) -> Self {
        self.thinking_level = thinking_level;
        self
    }

    pub(crate) fn with_text_verbosity(mut self, text_verbosity: Option<TextVerbosity>) -> Self {
        self.text_verbosity = text_verbosity;
        self
    }

    pub(crate) fn with_thinking_levels(
        mut self,
        thinking_levels: Vec<crate::thinking::ThinkingLevel>,
    ) -> Self {
        self.thinking_levels = crate::thinking::normalize_thinking_levels(&thinking_levels);
        self.thinking_level =
            crate::thinking::resolve_thinking_level(&self.thinking_levels, self.thinking_level);
        self
    }

    pub(crate) fn with_default_reasoning_summary(mut self, supported: bool) -> Self {
        self.send_default_reasoning_summary = supported;
        self
    }
}

impl AgentSession {
    pub(crate) fn new_with_prompt_dir_and_subagents(
        model: impl Into<String>,
        prompt_dir: Option<&Path>,
        instructions: &[InstructionFile],
        skills: &SkillDiscovery,
        subagents_section: Option<&str>,
        code_mode_enabled: bool,
    ) -> anyhow::Result<Self> {
        let system_prompt = build_system_prompt_with_prompt_dir_and_subagents(
            prompt_dir,
            instructions,
            skills,
            subagents_section,
            code_mode_enabled,
        )?;
        Ok(Self::from_system_prompt(
            system_prompt,
            AgentSessionConfig::for_model(model),
        ))
    }

    pub(super) fn from_system_prompt(system_prompt: String, config: AgentSessionConfig) -> Self {
        Self {
            provider_id: config.provider_id,
            model: config.model,
            system_prompt,
            context_budget: config.context_budget,
            thinking_level: config.thinking_level,
            text_verbosity: config.text_verbosity,
            thinking_levels: config.thinking_levels,
            send_default_reasoning_summary: config.send_default_reasoning_summary,
            reasoning_updates_enabled: false,
            replay_cache: Arc::new(Mutex::new(ConversationReplayCache::default())),
            completion_verification: CompletionVerificationSettings::default(),
            show_jev_debug_messages: false,
            skill_suggestion: None,
        }
    }

    pub(crate) fn with_reasoning_updates(mut self, enabled: bool) -> Self {
        self.reasoning_updates_enabled = enabled;
        self
    }

    pub(crate) fn with_completion_verification(
        mut self,
        settings: CompletionVerificationSettings,
    ) -> Self {
        self.completion_verification = settings;
        self
    }

    pub(crate) fn with_jev_debug_messages(mut self, enabled: bool) -> Self {
        self.show_jev_debug_messages = enabled;
        self
    }

    pub(crate) fn with_skill_suggestion(
        mut self,
        context: Option<skill_suggestion::SkillSuggestionContext>,
    ) -> Self {
        self.skill_suggestion = context;
        self
    }

    pub(super) fn reasoning_selection_payload(&self) -> serde_json::Value {
        json!({"provider": self.provider_id, "model": self.model, "effort": self.thinking_level})
    }

    pub(crate) fn with_config(mut self, config: AgentSessionConfig) -> Self {
        self.model = config.model;
        self.with_provider_id(config.provider_id)
            .with_thinking_level(config.thinking_level)
            .with_text_verbosity(config.text_verbosity)
            .with_thinking_levels(config.thinking_levels)
            .with_default_reasoning_summary(config.send_default_reasoning_summary)
            .with_context_budget(config.context_budget)
    }

    pub(crate) fn provider_id(&self) -> &str {
        &self.provider_id
    }

    pub(crate) fn model(&self) -> &str {
        &self.model
    }

    pub(crate) fn thinking_level(&self) -> crate::thinking::ThinkingLevel {
        self.thinking_level
    }

    pub(crate) fn with_appended_system_prompt(&self, section: &str) -> Self {
        let mut cloned = self.clone();
        let section = section.trim();
        if !section.is_empty() {
            cloned.system_prompt.push_str("\n\n");
            cloned.system_prompt.push_str(section);
        }
        cloned
    }

    pub fn with_provider_id(mut self, provider_id: impl Into<String>) -> Self {
        self.provider_id = normalize_agent_identifier(provider_id.into(), "provider");
        self.reasoning_updates_enabled = false;
        self
    }

    pub fn with_context_budget(mut self, budget: ContextBudget) -> Self {
        self.context_budget = budget;
        self
    }

    pub(crate) fn with_model(mut self, model: impl Into<String>) -> Self {
        self.model = normalize_agent_identifier(model.into(), "model");
        self.reasoning_updates_enabled = false;
        self
    }

    pub(crate) fn with_thinking_level(
        mut self,
        thinking_level: crate::thinking::ThinkingLevel,
    ) -> Self {
        self.thinking_level = thinking_level;
        self
    }

    pub(crate) fn with_text_verbosity(mut self, text_verbosity: Option<TextVerbosity>) -> Self {
        self.text_verbosity = text_verbosity;
        self
    }

    pub(crate) fn with_thinking_levels(
        mut self,
        thinking_levels: Vec<crate::thinking::ThinkingLevel>,
    ) -> Self {
        self.thinking_levels = crate::thinking::normalize_thinking_levels(&thinking_levels);
        self.thinking_level =
            crate::thinking::resolve_thinking_level(&self.thinking_levels, self.thinking_level);
        self
    }

    pub(crate) fn with_default_reasoning_summary(mut self, supported: bool) -> Self {
        self.send_default_reasoning_summary = supported;
        self
    }

    pub(crate) fn with_reasoning_override(
        mut self,
        thinking_level: crate::thinking::ThinkingLevel,
    ) -> Self {
        self.thinking_level =
            crate::thinking::resolve_thinking_level(&self.thinking_levels, thinking_level);
        self
    }

    pub(crate) fn with_provider_model_reasoning(
        self,
        provider_id: impl Into<String>,
        model: impl Into<String>,
        thinking_levels: Vec<crate::thinking::ThinkingLevel>,
        thinking_level: Option<crate::thinking::ThinkingLevel>,
    ) -> Self {
        let selected_thinking = thinking_level.unwrap_or(self.thinking_level);
        let send_default_reasoning_summary = thinking_levels.len() > 1;
        let effective_thinking =
            crate::thinking::resolve_thinking_level(&thinking_levels, selected_thinking);
        self.with_provider_id(provider_id)
            .with_model(model)
            .with_thinking_levels(thinking_levels)
            .with_default_reasoning_summary(send_default_reasoning_summary)
            .with_thinking_level(effective_thinking)
    }
}