// SPDX-License-Identifier: MIT
// Copyright (c) 2025-2026 SenWeaverCoding
// Licensed under the MIT License.
use crate::agent::dispatcher::{
    NativeToolDispatcher, ParsedToolCall, ToolDispatcher, ToolExecutionResult, XmlToolDispatcher,
};
use crate::agent::memory_loader::{DefaultMemoryLoader, MemoryLoader};
use crate::agent::prompt::{PromptContext, SystemPromptBuilder};
use crate::config::Config;
use crate::error::AgentError;
use crate::i18n::ToolDescriptions;
use crate::memory::{self, Memory, MemoryCategory};
use crate::observability::{self, Observer, ObserverEvent};
use crate::providers::{self, ChatMessage, ConversationMessage, Provider, ToolCall};
use crate::runtime;
use crate::security::SecurityPolicy;
use crate::tools::{self, Tool, ToolSpec};
use anyhow::Result;
use chrono::{Datelike, Timelike};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use uuid::Uuid;

pub(crate) const TURN_EVENT_DRAIN_BUFFER: usize = 1024;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GuardrailApprovalOutcome {
    Approved,
    Denied,
    Cancelled,
}

#[derive(Debug, Clone)]
pub enum TurnEvent {

    Chunk { delta: String },

    StreamReset,

    Thinking { delta: String },

    ToolCall {
        name: String,
        args: serde_json::Value,
        tool_call_id: Option<String>,
    },

    ToolResult {
        name: String,
        output: String,
        success: bool,
        tool_call_id: Option<String>,
    },

    PlanProgressCommitted {
        plan_path: String,
        title: String,
        todos_json: String,
    },

    Error { message: String },

    FileEdit {
        path: String,
        additions: i32,
        deletions: i32,
        diff: Option<String>,
        edit_batch_id: Option<String>,
    },

    StatusUpdate { action: String, detail: String },

    ProgressTick {
        iteration: usize,
        max_iterations: usize,
        tokens_used: u64,
    },

    CommandPreview {
        tool_name: String,
        args: serde_json::Value,
        estimated_duration_ms: Option<u64>,
    },

    Cancelling { reason: String },

    ContextCompressed {
        tokens_before: usize,
        tokens_after: usize,
    },

    PermissionRequest {
        request_id: String,
        tool_name: String,
        input: serde_json::Value,
        description: Option<String>,
    },

    SubagentChunk {
        task_id: String,
        agent_id: String,
        kind: SubagentChunkKind,
        delta: String,
    },

    PiiSanitized {
        report: crate::services::governance::pii_sanitizer::SanitizationReport,
    },

    ProviderRetry {
        attempt: u32,
        max_attempts: u32,
        wait_ms: u64,
        class: String,
        provider: String,
        model: String,
        message: String,
    },

    WorkerSpawned {
        parent_tool_use_id: String,
        worker_id: String,
        title: String,
        model: String,
    },

    WorkerStatus {
        worker_id: String,
        status: String,
        detail: Option<String>,
    },

    WorkerProgress {
        worker_id: String,
        action: String,
        detail: String,
    },

    WorkerCompleted {
        worker_id: String,
        success: bool,
        summary: String,
    },

    WorkerStopped {
        worker_id: String,
        reason: String,
    },

    ParentResumed {
        reason: String,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubagentChunkKind {

    Chunk,

    Thinking,

    ToolCall,

    ToolResult,

    Status,
}

#[derive(Debug, Clone, PartialEq)]
pub enum ConfigChange {

    None,

    Soft,

    Hard,
}

pub struct Agent {
    provider: Box<dyn Provider>,
    tools: Vec<Box<dyn Tool>>,
    tool_index: std::collections::HashMap<String, usize>,

    tool_specs: std::sync::Arc<Vec<ToolSpec>>,
    memory: Arc<dyn Memory>,
    observer: Arc<dyn Observer>,
    prompt_builder: SystemPromptBuilder,
    tool_dispatcher: Box<dyn ToolDispatcher>,
    memory_loader: Box<dyn MemoryLoader>,
    config: crate::config::AgentConfig,
    model_name: String,
    temperature: f64,
    workspace_dir: std::path::PathBuf,
    identity_config: crate::config::IdentityConfig,
    skills: Vec<crate::skills::Skill>,
    skills_prompt_mode: crate::config::SkillsPromptInjectionMode,
    auto_save: bool,
    memory_session_id: Option<String>,
    history: Vec<ConversationMessage>,
    classification_config: crate::config::QueryClassificationConfig,
    available_hints: Vec<String>,
    route_model_by_hint: HashMap<String, String>,
    allowed_tools: Option<Vec<String>>,
    response_cache: Option<Arc<crate::memory::response_cache::ResponseCache>>,
    tool_descriptions: Option<ToolDescriptions>,

    security_summary: Option<String>,

    autonomy_level: crate::security::AutonomyLevel,

    activated_tools: Option<Arc<parking_lot::Mutex<crate::tools::ActivatedToolSet>>>,

    surface: crate::tools::ToolSurfaceBaseline,

    user_profile_config: crate::agent::user::profile::UserProfileConfig,
    skill_evolution_config: crate::agent::skill_evolution::SkillEvolutionConfig,
    prompt_optimizer_config: crate::agent::prompt::optimizer::PromptOptimizerConfig,

    rbac_engine: Option<std::sync::Arc<crate::security::rbac::RbacEngine>>,
    rbac_identity: Option<crate::security::rbac::CallerIdentity>,
    experience_replay: Option<crate::agent::reward::experience::ExperienceReplay>,
    plan_mode_config: crate::agent::plan_mode::PlanModeConfig,
    intent_analysis_config: crate::agent::intent::IntentAnalysisConfig,

    mode_tool_filter: Option<std::collections::HashSet<String>>,

    mode_filter_dirty: bool,

    current_coding_mode: Option<crate::agent::coding_mode::CodingMode>,

    baseline_max_tool_iterations: usize,

    cancelled: Arc<std::sync::atomic::AtomicBool>,

    cancel_signal: Arc<arc_swap::ArcSwap<tokio_util::sync::CancellationToken>>,

    shared_config: crate::config::live::LiveConfig,

    cached_provider: String,

    cached_api_key: crate::security::secret_string::SecretString,
    cached_api_url: String,

    last_usage: Option<crate::providers::TokenUsage>,

    desktop_security_policy: Option<Arc<SecurityPolicy>>,

    plan_execution_armed: parking_lot::Mutex<Option<String>>,

    hook_runner: Option<std::sync::Arc<crate::hooks::HotHookRunner>>,

    cached_tools_signature: u64,

    plan_mode_flag: crate::tools::PlanModeFlag,

    last_turn_interrupted: bool,
}

pub struct AgentBuilder {
    provider: Option<Box<dyn Provider>>,
    tools: Option<Vec<Box<dyn Tool>>>,
    memory: Option<Arc<dyn Memory>>,
    observer: Option<Arc<dyn Observer>>,
    prompt_builder: Option<SystemPromptBuilder>,
    tool_dispatcher: Option<Box<dyn ToolDispatcher>>,
    memory_loader: Option<Box<dyn MemoryLoader>>,
    config: Option<crate::config::AgentConfig>,
    model_name: Option<String>,
    temperature: Option<f64>,
    workspace_dir: Option<std::path::PathBuf>,
    identity_config: Option<crate::config::IdentityConfig>,
    skills: Option<Vec<crate::skills::Skill>>,
    skills_prompt_mode: Option<crate::config::SkillsPromptInjectionMode>,
    auto_save: Option<bool>,
    memory_session_id: Option<String>,
    classification_config: Option<crate::config::QueryClassificationConfig>,
    available_hints: Option<Vec<String>>,
    route_model_by_hint: Option<HashMap<String, String>>,
    allowed_tools: Option<Vec<String>>,
    denied_tools: Option<Vec<String>>,
    response_cache: Option<Arc<crate::memory::response_cache::ResponseCache>>,
    tool_descriptions: Option<ToolDescriptions>,
    security_summary: Option<String>,
    autonomy_level: Option<crate::security::AutonomyLevel>,
    activated_tools: Option<Arc<parking_lot::Mutex<crate::tools::ActivatedToolSet>>>,
    surface: Option<crate::tools::ToolSurfaceBaseline>,
    user_profile_config: Option<crate::agent::user::profile::UserProfileConfig>,
    skill_evolution_config: Option<crate::agent::skill_evolution::SkillEvolutionConfig>,
    prompt_optimizer_config: Option<crate::agent::prompt::optimizer::PromptOptimizerConfig>,
    rbac_engine: Option<std::sync::Arc<crate::security::rbac::RbacEngine>>,
    rbac_identity: Option<crate::security::rbac::CallerIdentity>,
    experience_replay: Option<crate::agent::reward::experience::ExperienceReplay>,
    plan_mode_config: Option<crate::agent::plan_mode::PlanModeConfig>,
    intent_analysis_config: Option<crate::agent::intent::IntentAnalysisConfig>,
    shared_config: Option<crate::config::live::LiveConfig>,

    cached_provider: Option<String>,
    cached_api_key: Option<String>,
    cached_api_url: Option<String>,
    desktop_security_policy: Option<Arc<SecurityPolicy>>,
    hook_runner: Option<std::sync::Arc<crate::hooks::HotHookRunner>>,
    plan_mode_flag: Option<crate::tools::PlanModeFlag>,
}

impl AgentBuilder {
    pub fn new() -> Self {
        Self {
            provider: None,
            tools: None,
            memory: None,
            observer: None,
            prompt_builder: None,
            tool_dispatcher: None,
            memory_loader: None,
            config: None,
            model_name: None,
            temperature: None,
            workspace_dir: None,
            identity_config: None,
            skills: None,
            skills_prompt_mode: None,
            auto_save: None,
            memory_session_id: None,
            classification_config: None,
            available_hints: None,
            route_model_by_hint: None,
            allowed_tools: None,
            denied_tools: None,
            response_cache: None,
            tool_descriptions: None,
            security_summary: None,
            autonomy_level: None,
            activated_tools: None,
            surface: None,
            user_profile_config: None,
            skill_evolution_config: None,
            prompt_optimizer_config: None,
            rbac_engine: None,
            rbac_identity: None,
            experience_replay: None,
            plan_mode_config: None,
            intent_analysis_config: None,
            shared_config: None,
            cached_provider: None,
            cached_api_key: None,
            cached_api_url: None,
            desktop_security_policy: None,
            hook_runner: None,
            plan_mode_flag: None,
        }
    }

    pub fn plan_mode_flag(mut self, flag: crate::tools::PlanModeFlag) -> Self {
        self.plan_mode_flag = Some(flag);
        self
    }

    pub fn provider(mut self, provider: Box<dyn Provider>) -> Self {
        self.provider = Some(provider);
        self
    }

    pub fn tools(mut self, tools: Vec<Box<dyn Tool>>) -> Self {
        self.tools = Some(tools);
        self
    }

    pub fn memory(mut self, memory: Arc<dyn Memory>) -> Self {
        self.memory = Some(memory);
        self
    }

    pub fn observer(mut self, observer: Arc<dyn Observer>) -> Self {
        self.observer = Some(observer);
        self
    }

    pub fn prompt_builder(mut self, prompt_builder: SystemPromptBuilder) -> Self {
        self.prompt_builder = Some(prompt_builder);
        self
    }

    pub fn tool_dispatcher(mut self, tool_dispatcher: Box<dyn ToolDispatcher>) -> Self {
        self.tool_dispatcher = Some(tool_dispatcher);
        self
    }

    pub fn memory_loader(mut self, memory_loader: Box<dyn MemoryLoader>) -> Self {
        self.memory_loader = Some(memory_loader);
        self
    }

    pub fn config(mut self, config: crate::config::AgentConfig) -> Self {
        self.config = Some(config);
        self
    }

    pub fn model_name(mut self, model_name: String) -> Self {
        self.model_name = Some(model_name);
        self
    }

    pub fn temperature(mut self, temperature: f64) -> Self {
        self.temperature = Some(temperature);
        self
    }

    pub fn workspace_dir(mut self, workspace_dir: std::path::PathBuf) -> Self {
        self.workspace_dir = Some(workspace_dir);
        self
    }

    pub fn identity_config(mut self, identity_config: crate::config::IdentityConfig) -> Self {
        self.identity_config = Some(identity_config);
        self
    }

    pub fn skills(mut self, skills: Vec<crate::skills::Skill>) -> Self {
        self.skills = Some(skills);
        self
    }

    pub fn skills_prompt_mode(
        mut self,
        skills_prompt_mode: crate::config::SkillsPromptInjectionMode,
    ) -> Self {
        self.skills_prompt_mode = Some(skills_prompt_mode);
        self
    }

    pub fn auto_save(mut self, auto_save: bool) -> Self {
        self.auto_save = Some(auto_save);
        self
    }

    pub fn memory_session_id(mut self, memory_session_id: Option<String>) -> Self {
        self.memory_session_id = memory_session_id;
        self
    }

    pub fn classification_config(
        mut self,
        classification_config: crate::config::QueryClassificationConfig,
    ) -> Self {
        self.classification_config = Some(classification_config);
        self
    }

    pub fn available_hints(mut self, available_hints: Vec<String>) -> Self {
        self.available_hints = Some(available_hints);
        self
    }

    pub fn route_model_by_hint(mut self, route_model_by_hint: HashMap<String, String>) -> Self {
        self.route_model_by_hint = Some(route_model_by_hint);
        self
    }

    pub fn allowed_tools(mut self, allowed_tools: Option<Vec<String>>) -> Self {
        self.allowed_tools = allowed_tools;
        self
    }

    pub fn denied_tools(mut self, denied_tools: Option<Vec<String>>) -> Self {
        self.denied_tools = denied_tools;
        self
    }

    pub fn response_cache(
        mut self,
        cache: Option<Arc<crate::memory::response_cache::ResponseCache>>,
    ) -> Self {
        self.response_cache = cache;
        self
    }

    pub fn tool_descriptions(mut self, tool_descriptions: Option<ToolDescriptions>) -> Self {
        self.tool_descriptions = tool_descriptions;
        self
    }

    pub fn security_summary(mut self, summary: Option<String>) -> Self {
        self.security_summary = summary;
        self
    }

    pub fn autonomy_level(mut self, level: crate::security::AutonomyLevel) -> Self {
        self.autonomy_level = Some(level);
        self
    }

    pub fn surface(mut self, surface: crate::tools::ToolSurfaceBaseline) -> Self {
        self.surface = Some(surface);
        self
    }

    pub fn activated_tools(
        mut self,
        activated: Option<Arc<parking_lot::Mutex<tools::ActivatedToolSet>>>,
    ) -> Self {
        self.activated_tools = activated;
        self
    }

    pub fn user_profile_config(
        mut self,
        cfg: crate::agent::user::profile::UserProfileConfig,
    ) -> Self {
        self.user_profile_config = Some(cfg);
        self
    }

    pub fn skill_evolution_config(
        mut self,
        cfg: crate::agent::skill_evolution::SkillEvolutionConfig,
    ) -> Self {
        self.skill_evolution_config = Some(cfg);
        self
    }

    pub fn prompt_optimizer_config(
        mut self,
        cfg: crate::agent::prompt::optimizer::PromptOptimizerConfig,
    ) -> Self {
        self.prompt_optimizer_config = Some(cfg);
        self
    }

    pub fn build(self) -> Result<Agent> {
        let mut tools = self
            .tools
            .ok_or_else(|| anyhow::anyhow!("tools are required"))?;
        let allowed = self.allowed_tools.clone();
        if let Some(ref allow_list) = allowed {
            tools.retain(|t| allow_list.iter().any(|name| name == t.name()));
        }
        let denied = self.denied_tools.clone();
        if let Some(ref deny_list) = denied {
            tools.retain(|t| !deny_list.iter().any(|name| name == t.name()));
        }
        let tool_specs_vec: Vec<ToolSpec> = tools.iter().map(|tool| tool.spec()).collect();
        let tool_specs =
            std::sync::Arc::new(crate::tools::dedupe_tool_specs(&tool_specs_vec));
        let tool_index: std::collections::HashMap<String, usize> = tools
            .iter()
            .enumerate()
            .map(|(i, tool)| (tool.name().to_string(), i))
            .collect();

        let baseline_max_iter = self
            .config
            .as_ref()
            .map(|c| c.max_tool_iterations)
            .unwrap_or_default();

        Ok(Agent {
            provider: self
                .provider
                .ok_or_else(|| anyhow::anyhow!("provider is required"))?,
            tools,
            tool_index,
            tool_specs,
            memory: self
                .memory
                .ok_or_else(|| anyhow::anyhow!("memory is required"))?,
            observer: self
                .observer
                .ok_or_else(|| anyhow::anyhow!("observer is required"))?,
            prompt_builder: self
                .prompt_builder
                .unwrap_or_else(SystemPromptBuilder::with_defaults),
            tool_dispatcher: self
                .tool_dispatcher
                .ok_or_else(|| anyhow::anyhow!("tool_dispatcher is required"))?,
            memory_loader: self
                .memory_loader
                .unwrap_or_else(|| Box::new(DefaultMemoryLoader::default())),
            config: self.config.unwrap_or_default(),
            model_name: self
                .model_name
                .ok_or_else(|| anyhow::anyhow!(
                    "no_model_configured: AgentBuilder.model_name is required; 请先在提供商设置页添加至少一个模型 (please add at least one model in Provider settings)"
                ))?,
            temperature: self.temperature.unwrap_or(0.7),
            workspace_dir: self
                .workspace_dir
                .unwrap_or_else(|| std::path::PathBuf::from(".")),
            identity_config: self.identity_config.unwrap_or_default(),
            skills: self.skills.unwrap_or_default(),
            skills_prompt_mode: self.skills_prompt_mode.unwrap_or_default(),
            auto_save: self.auto_save.unwrap_or(false),
            memory_session_id: self.memory_session_id,
            history: Vec::new(),
            classification_config: self.classification_config.unwrap_or_default(),
            available_hints: self.available_hints.unwrap_or_default(),
            route_model_by_hint: self.route_model_by_hint.unwrap_or_default(),
            allowed_tools: allowed,
            response_cache: self.response_cache,
            tool_descriptions: self.tool_descriptions,
            security_summary: self.security_summary,
            autonomy_level: self
                .autonomy_level
                .unwrap_or(crate::security::AutonomyLevel::Supervised),
            activated_tools: self.activated_tools,
            surface: self
                .surface
                .unwrap_or(crate::tools::ToolSurfaceBaseline::Both),
            user_profile_config: self.user_profile_config.unwrap_or_default(),
            skill_evolution_config: self.skill_evolution_config.unwrap_or_default(),
            prompt_optimizer_config: self.prompt_optimizer_config.unwrap_or_default(),
            rbac_engine: self.rbac_engine,
            rbac_identity: self.rbac_identity,
            experience_replay: self.experience_replay,
            plan_mode_config: self.plan_mode_config.unwrap_or_default(),
            intent_analysis_config: self.intent_analysis_config.unwrap_or_default(),
            mode_tool_filter: None,
            mode_filter_dirty: false,
            current_coding_mode: None,
            baseline_max_tool_iterations: baseline_max_iter,
            cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
            cancel_signal: Arc::new(arc_swap::ArcSwap::from_pointee(
                tokio_util::sync::CancellationToken::new(),
            )),
            shared_config: self
                .shared_config
                .unwrap_or_else(crate::config::live::LiveConfig::default),
            cached_provider: self.cached_provider.unwrap_or_default(),
            cached_api_key: crate::security::secret_string::SecretString::new(
                self.cached_api_key.unwrap_or_default(),
            ),
            cached_api_url: self.cached_api_url.unwrap_or_default(),
            last_usage: None,
            desktop_security_policy: self.desktop_security_policy,
            plan_execution_armed: parking_lot::Mutex::new(None),
            hook_runner: self.hook_runner,
            cached_tools_signature: 0,
            plan_mode_flag: self.plan_mode_flag.unwrap_or_default(),
            last_turn_interrupted: false,
        })
    }

    pub fn rbac_session(
        mut self,
        engine: Option<Arc<crate::security::rbac::RbacEngine>>,
        identity: Option<crate::security::rbac::CallerIdentity>,
    ) -> Self {
        self.rbac_engine = engine;
        self.rbac_identity = identity;
        self
    }

    pub fn experience_replay(
        mut self,
        replay: Option<crate::agent::reward::experience::ExperienceReplay>,
    ) -> Self {
        self.experience_replay = replay;
        self
    }

    pub fn plan_mode_config(mut self, cfg: crate::agent::plan_mode::PlanModeConfig) -> Self {
        self.plan_mode_config = Some(cfg);
        self
    }

    pub fn intent_analysis_config(
        mut self,
        cfg: crate::agent::intent::IntentAnalysisConfig,
    ) -> Self {
        self.intent_analysis_config = Some(cfg);
        self
    }

    pub fn shared_config(mut self, config: crate::config::live::LiveConfig) -> Self {
        self.shared_config = Some(config);
        self
    }

    pub fn cached_provider_config(
        mut self,
        provider: String,
        api_key: String,
        api_url: String,
    ) -> Self {
        self.cached_provider = Some(provider);
        self.cached_api_key = Some(api_key);
        self.cached_api_url = Some(api_url);
        self
    }

    pub fn desktop_security_policy(mut self, policy: Option<Arc<SecurityPolicy>>) -> Self {
        self.desktop_security_policy = policy;
        self
    }

    pub fn hook_runner(
        mut self,
        runner: Option<std::sync::Arc<crate::hooks::HotHookRunner>>,
    ) -> Self {
        self.hook_runner = runner;
        self
    }
}

impl Agent {
    pub fn builder() -> AgentBuilder {
        AgentBuilder::new()
    }

    #[must_use]
    pub fn inline_edit_runner(&self) -> Option<std::sync::Arc<crate::inline_edit::InlineEditRunner>> {
        let config = self.shared_config.load();
        crate::inline_edit::service::default_runner(&config)
    }

    #[must_use]
    pub fn last_usage(&self) -> Option<&crate::providers::TokenUsage> {
        self.last_usage.as_ref()
    }

    pub async fn turn_streamed(
        &mut self,
        user_message: &str,
        event_tx: tokio::sync::mpsc::Sender<TurnEvent>,
    ) -> Result<String, AgentError> {
        self.sync_tools_from_config_if_changed().await;

        let user_message_owned = user_message.to_string();
        let user_message_for_turn = if let Some(ref runner) = self.hook_runner {
            match runner.run_before_prompt_build(user_message_owned.clone()).await {
                crate::hooks::HookResult::Continue(rewritten) => rewritten,
                crate::hooks::HookResult::Cancel(reason) => {
                    let banner = format!("[Cancelled by hook: {reason}]");
                    let _ = event_tx
                        .send(TurnEvent::Cancelling {
                            reason: format!("hook:beforeSubmitPrompt:{reason}"),
                        })
                        .await;
                    return Ok(banner);
                }
            }
        } else {
            user_message_owned
        };
        let user_message: &str = user_message_for_turn.as_str();

        crate::agent::model_switch::scope_model_switch(
            self.turn_streamed_inner(user_message, event_tx),
        )
        .await
    }

    async fn turn_streamed_inner(
        &mut self,
        user_message: &str,
        event_tx: tokio::sync::mpsc::Sender<TurnEvent>,
    ) -> Result<String, AgentError> {
        let mut _turn_metrics = crate::agent::executor_core::TurnMetricsGuard::start();

        let _ = event_tx.try_send(TurnEvent::ProgressTick {
            iteration: 0,
            max_iterations: self.config.max_tool_iterations,
            tokens_used: 0,
        });

        self.apply_turn_preamble(user_message, &event_tx).await?;
        self.apply_gui_model_switch(&event_tx).await;
        self.apply_bootstrap_model_override(&event_tx).await;
        let mut effective_model = self.classify_model(user_message);

        let armed_plan_path: Option<String> = self.plan_execution_armed.lock().take();

        let failed_turn_rollback_len = self.history.len().saturating_sub(1);

        let mut history_chat = self.tool_dispatcher.to_provider_messages(&self.history);

        let mut plan_exec_focus_base_len: usize = 0;
        let plan_exec_full_history: Option<Vec<crate::providers::traits::ChatMessage>> =
            if armed_plan_path.is_some() {
                let full = history_chat.clone();
                history_chat = Self::focus_history_for_plan_execution(history_chat);
                plan_exec_focus_base_len = history_chat.len();
                Some(full)
            } else {
                None
            };

        let cancel = self.cancel_signal.load_full().as_ref().clone();
        let live_cfg = self.shared_config.load_ref();
        let multimodal = live_cfg.multimodal.clone();
        let pacing = live_cfg.pacing.clone();
        let dedup_exempt = live_cfg.agent.tool_call_dedup_exempt.clone();
        drop(live_cfg);
        let excluded_tools: Vec<String> = Vec::new();

        let hook_runner_arc = self.hook_runner.as_ref().and_then(|h| h.current());
        let hook_runner_ref = hook_runner_arc.as_deref();

        let final_text = loop {
            let provider_name = self.cached_provider.clone();
            let gui_hooks: Arc<GuiHooksFromAgent> = Arc::new(GuiHooksFromAgent::from_agent(self));

            let loop_result = {
                let policy = crate::agent::loop_::policy::PolicyBundle::gui(
                    self.provider.as_ref(),
                    &self.tools,
                    self.observer.as_ref(),
                    provider_name.as_str(),
                    effective_model.as_str(),
                    &multimodal,
                    &pacing,
                    &excluded_tools,
                    &dedup_exempt,
                )
                .with_temperature(self.temperature)
                .with_max_iterations(self.config.max_tool_iterations)
                .with_cancellation(Some(cancel.clone()))
                .with_event_sink(crate::agent::event_sink::EventSink::turn(event_tx.clone()))
                .with_activated_tools(self.activated_tools.as_ref())
                .with_hooks(hook_runner_ref)
                .with_rbac(self.rbac_engine.as_ref(), self.rbac_identity.as_ref())
                .with_model_switch_callback(Some(crate::agent::loop_::get_model_switch_state()))
                .with_response_cache_hook(Some(gui_hooks.clone()))
                .with_memory_session_hook(Some(gui_hooks.clone()))
                .with_model_classifier_hook(Some(gui_hooks.clone()))
                .with_turn_preamble_hook(Some(gui_hooks.clone()))
                .with_gui_model_switch_hook(Some(gui_hooks.clone()))
                .with_iteration_context_budget_hook(Some(gui_hooks.clone()))
                .with_experience_recorder_hook(Some(gui_hooks.clone()))
                .with_plan_mode_nudge_hook(Some(gui_hooks.clone()))
                .with_plan_mode_flag(Some(&self.plan_mode_flag))
                .with_plan_execution_path(armed_plan_path.as_deref());

                crate::agent::loop_::unified::UnifiedLoop::new(policy)
                    .run(&mut history_chat)
                    .await
            };

            match loop_result {
                Ok(text) => break text,
                Err(err) => {
                    if crate::agent::model_switch::is_model_switch_requested(&err).is_some() {
                        tracing::info!(
                            target: "runtime_model_switch",
                            "GUI turn received model switch request mid-turn; applying switch and retrying instead of failing"
                        );
                        self.apply_gui_model_switch(&event_tx).await;
                        effective_model = self.model_name.clone();
                        continue;
                    }
                    let msg = err.to_string();
                    let downcast = err.downcast_ref::<AgentError>();
                    let is_cancelled = matches!(downcast, Some(AgentError::TurnCancelled))
                        || crate::agent::error_classify::classify_turn_error_code(&msg)
                            == "CANCELLED";
                    if is_cancelled {
                        let _ = event_tx
                            .send(TurnEvent::Cancelling {
                                reason: msg.clone(),
                            })
                            .await;
                        self.commit_interrupted_turn_history(
                            plan_exec_full_history.clone(),
                            history_chat.clone(),
                            plan_exec_focus_base_len,
                        );
                        self.last_turn_interrupted = true;
                        return Err(AgentError::TurnCancelled);
                    }
                    let is_transport_interruption =
                        matches!(downcast, Some(AgentError::StreamInterrupted(_)));
                    let _ = event_tx
                        .send(TurnEvent::Error {
                            message: msg.clone(),
                        })
                        .await;

                    if !is_transport_interruption {
                        self.record_failed_turn_reinforcement(&msg);
                    }
                    self.rollback_failed_turn_history(failed_turn_rollback_len);
                    return Err(AgentError::ToolDispatchFailed(msg));
                }
            }
        };

        if let Some(mut full) = plan_exec_full_history {
            full.push(crate::providers::traits::ChatMessage::assistant(
                final_text.clone(),
            ));
            Self::replace_history_from_flat(&mut self.history, full);
        } else {
            Self::replace_history_from_flat(&mut self.history, history_chat);
        }
        self.trim_history();

        crate::evolution::record_provider_model(
            Some(self.cached_provider.as_str()),
            Some(effective_model.as_str()),
        );
        crate::evolution::set_response_text(&final_text);

        _turn_metrics.mark_ok();
        Ok(final_text)
    }

    fn commit_interrupted_turn_history(
        &mut self,
        plan_exec_full_history: Option<Vec<crate::providers::traits::ChatMessage>>,
        partial_history: Vec<crate::providers::traits::ChatMessage>,
        plan_exec_focus_base_len: usize,
    ) {
        match plan_exec_full_history {
            Some(mut full) => {
                if partial_history.len() > plan_exec_focus_base_len {
                    full.extend(
                        partial_history[plan_exec_focus_base_len..]
                            .iter()
                            .cloned(),
                    );
                }
                Self::replace_history_from_flat(&mut self.history, full);
            }
            None => Self::replace_history_from_flat(&mut self.history, partial_history),
        }
        crate::agent::dangling_tool_repair::close_orphan_user_turns(&mut self.history);
    }

    fn rollback_failed_turn_history(&mut self, rollback_len: usize) {
        if self.history.len() > rollback_len {
            let dropped = self.history.len() - rollback_len;
            self.history.truncate(rollback_len);
            tracing::warn!(
                target: "agent.turn",
                dropped,
                rollback_len,
                "turn failed; rolled back the failed user turn from history so it does not pollute subsequent context (consistent with legacy truncate semantics; the failure is still surfaced to the session via the returned error)"
            );
        }
    }

    fn replace_history_from_flat(
        history: &mut Vec<ConversationMessage>,
        flat: Vec<ChatMessage>,
    ) {
        let mut out: Vec<ConversationMessage> = Vec::with_capacity(flat.len());
        let mut tool_batch: Vec<crate::providers::traits::ToolResultMessage> = Vec::new();
        for msg in flat {
            if msg.role != "tool" && !tool_batch.is_empty() {
                out.push(ConversationMessage::ToolResults(std::mem::take(
                    &mut tool_batch,
                )));
            }
            match msg.role.as_str() {
                "tool" => {
                    if let Ok(v) = serde_json::from_str::<serde_json::Value>(&msg.content) {
                        if let (Some(id), Some(content_v)) = (
                            v.get("tool_call_id").and_then(|x| x.as_str()),
                            v.get("content"),
                        ) {
                            let content_str = match content_v {
                                serde_json::Value::String(s) => s.clone(),
                                other => other.to_string(),
                            };
                            tool_batch.push(crate::providers::traits::ToolResultMessage {
                                tool_call_id: id.to_string(),
                                content: content_str,
                            });
                            continue;
                        }
                    }
                    out.push(ConversationMessage::Chat(msg));
                }
                "assistant" => {
                    if let Ok(v) =
                        serde_json::from_str::<serde_json::Value>(msg.content.trim())
                    {
                        if let Some(tc_arr) =
                            v.get("tool_calls").and_then(|x| x.as_array())
                        {
                            if !tc_arr.is_empty() {
                                let text = v
                                    .get("content")
                                    .and_then(|x| x.as_str())
                                    .filter(|s| !s.is_empty())
                                    .map(String::from);
                                let reasoning = v
                                    .get("reasoning_content")
                                    .and_then(|x| x.as_str())
                                    .filter(|s| !s.is_empty())
                                    .map(String::from);
                                let tool_calls: Vec<
                                    crate::providers::traits::ToolCall,
                                > = tc_arr
                                    .iter()
                                    .filter_map(|tc| {
                                        serde_json::from_value(tc.clone()).ok()
                                    })
                                    .collect();
                                out.push(ConversationMessage::AssistantToolCalls {
                                    text,
                                    tool_calls,
                                    reasoning_content: reasoning,
                                });
                                continue;
                            }
                        }
                    }
                    out.push(ConversationMessage::Chat(msg));
                }
                _ => {
                    out.push(ConversationMessage::Chat(msg));
                }
            }
        }
        if !tool_batch.is_empty() {
            out.push(ConversationMessage::ToolResults(tool_batch));
        }
        crate::agent::dangling_tool_repair::drop_payloadless_assistant_messages(&mut out);
        *history = out;
    }

    pub fn history(&self) -> &[ConversationMessage] {
        &self.history
    }

    pub fn clear_history(&mut self) {
        self.history.clear();
    }

    pub fn set_mode_tool_filter(&mut self, filter: Option<std::collections::HashSet<String>>) {

        self.mode_tool_filter = filter;
        self.mode_filter_dirty = true;
    }

    pub fn current_coding_mode(&self) -> Option<crate::agent::coding_mode::CodingMode> {
        self.current_coding_mode
    }

    pub fn set_coding_mode(&mut self, mode: crate::agent::coding_mode::CodingMode) {
        let prev = self.current_coding_mode;
        self.current_coding_mode = Some(mode);

        let filter = mode
            .allowed_tools()
            .map(|set| set.into_iter().map(String::from).collect());
        self.set_mode_tool_filter(filter);

        if let Some(prev_mode) = prev {
            if prev_mode != mode {
                let contract = mode.system_prompt_injection();
                let body = format!(
                    "[Mode Switch] Now operating in {} mode.\n{}",
                    mode.label(),
                    contract.trim_start()
                );
                self.history
                    .push(ConversationMessage::Chat(ChatMessage::system(body)));

                if matches!(mode, crate::agent::coding_mode::CodingMode::Plan) {
                    let reset_body = "[Plan-Mode Reset] Disregard any prior \"Step N completed\", \"开始执行 Step N\", \"Starting step …\", \"executing\", \"已完成\" or other execution-voice framing inherited from earlier turns or other modes. You are now ONLY drafting/refining a plan document; no step has been executed yet, no work is in progress, and the user has not clicked Build. Speak strictly in planning voice (\"will\", \"propose\", \"draft\", \"would\"); never claim any todo is finished, never narrate progress. If the user has not asked you anything new, simply wait  -  do not start a fake execution recap.";
                    self.history.push(ConversationMessage::Chat(
                        ChatMessage::system(reset_body.to_string()),
                    ));
                }

                tracing::info!(
                    target: "agent.mode",
                    from = %prev_mode,
                    to = %mode,
                    "coding mode switched mid-turn  -  contract pushed to history"
                );
            }
        }
    }

    pub fn arm_plan_execution(&self, plan_path: impl Into<String>) {
        *self.plan_execution_armed.lock() = Some(plan_path.into());
    }

    pub fn cancel_token(&self) -> Arc<std::sync::atomic::AtomicBool> {
        Arc::clone(&self.cancelled)
    }

    pub fn cancel_signal(&self) -> tokio_util::sync::CancellationToken {
        self.cancel_signal.load_full().as_ref().clone()
    }

    pub fn cancel_signal_handle(
        &self,
    ) -> Arc<arc_swap::ArcSwap<tokio_util::sync::CancellationToken>> {
        Arc::clone(&self.cancel_signal)
    }

    pub fn request_cancel(&self) {
        self.cancelled
            .store(true, std::sync::atomic::Ordering::Relaxed);
        self.cancel_signal.load_full().cancel();
    }

    pub fn reset_cancel(&self) {
        self.cancelled
            .store(false, std::sync::atomic::Ordering::Relaxed);
        let token = self.cancel_signal.load_full();
        if token.is_cancelled() {
            self.cancel_signal
                .store(Arc::new(tokio_util::sync::CancellationToken::new()));
        }
    }

    pub fn set_max_iterations_override(&mut self, max: usize) {
        self.config.max_tool_iterations = if max == 0 {
            self.baseline_max_tool_iterations
        } else {
            max
        };
    }

    fn apply_mode_filter(&mut self) {
        if !self.mode_filter_dirty {
            return;
        }
        let specs: Vec<ToolSpec> = if let Some(ref allowed) = self.mode_tool_filter {
            self.tools
                .iter()
                .filter(|t| allowed.contains(t.name()))
                .map(|t| t.spec())
                .collect()
        } else {
            self.tools.iter().map(|t| t.spec()).collect()
        };
        self.tool_specs = std::sync::Arc::new(crate::tools::dedupe_tool_specs(&specs));
        self.rebuild_tool_index();
        self.mode_filter_dirty = false;
    }

    fn sync_config_from_store(&mut self) -> ConfigChange {
        let config = self.shared_config.load();

        self.temperature = config.default_temperature;
        let resolved_model = providers::resolve_default_model(&config);
        let new_model_opt: Option<String> = match resolved_model {
            Ok(m) => Some(m),
            Err(e) => {
                tracing::warn!(target = "config", "{}", e);
                None
            }
        };

        self.config.max_history_messages = config.agent.max_history_messages;

        let new_provider = config
            .default_provider
            .clone()
            .unwrap_or_else(|| "openrouter".to_string());
        let new_api_key = config.api_key.clone().unwrap_or_default();
        let new_api_url = config.api_url.clone().unwrap_or_default();

        let provider_changed = new_provider != self.cached_provider;
        let api_key_changed = !self.cached_api_key.constant_time_eq(&new_api_key);
        let api_url_changed = new_api_url != self.cached_api_url;
        let model_changed = match new_model_opt.as_ref() {
            Some(m) => m != &self.model_name,
            None => false,
        };

        if provider_changed || api_key_changed || api_url_changed {
            tracing::info!(
                "Provider config changed: provider={}->{}, api_key={}, api_url={}",
                self.cached_provider,
                new_provider,
                if api_key_changed {
                    "(changed)"
                } else {
                    "(unchanged)"
                },
                if api_url_changed {
                    "(changed)"
                } else {
                    "(unchanged)"
                }
            );
            self.cached_provider = new_provider.clone();
            self.cached_api_key = crate::security::secret_string::SecretString::new(new_api_key);
            self.cached_api_url = new_api_url.clone();

            if let Some(new_model) = new_model_opt.clone() {
                if model_changed {
                    self.model_name = new_model;
                }
            }

            tracing::debug!(
                "Config synced (hard): provider={}, model={}, temperature={}",
                self.cached_provider,
                self.model_name,
                self.temperature
            );
            ConfigChange::Hard
        } else if model_changed
            || (self.temperature - config.default_temperature).abs() > f64::EPSILON
        {
            if let Some(new_model) = new_model_opt {
                self.model_name = new_model;
            }
            tracing::debug!(
                "Config synced (soft): model={}, temperature={}",
                self.model_name,
                self.temperature
            );
            ConfigChange::Soft
        } else {
            ConfigChange::None
        }
    }

    pub async fn reload_provider(&mut self) -> Result<()> {

        let config = self.shared_config.load_ref();

        let provider_name_raw = config
            .default_provider
            .clone()
            .unwrap_or_else(|| "openrouter".to_string());
        let provider_name = providers::resolve_runtime_provider_name(&provider_name_raw, &config);
        let api_key = config.api_key.clone().unwrap_or_default();
        let api_url = config.api_url.clone().unwrap_or_default();
        let model_name = providers::resolve_default_model(&config)?;
        let reliability = config.reliability.clone();
        let model_routes = config.model_routes.clone();
        let provider_runtime_options = providers::provider_runtime_options_from_config(&config);

        let provider_name_for_blocking = provider_name;
        let api_key_for_blocking = api_key.clone();
        let api_url_for_blocking = api_url.clone();
        let model_name_for_blocking = model_name.clone();
        let reliability_for_blocking = reliability;
        let model_routes_for_blocking = model_routes;
        let options_for_blocking = provider_runtime_options;
        let new_provider = tokio::task::spawn_blocking(move || {
            providers::create_routed_provider_with_options(
                &provider_name_for_blocking,
                if api_key_for_blocking.is_empty() {
                    None
                } else {
                    Some(api_key_for_blocking.as_str())
                },
                if api_url_for_blocking.is_empty() {
                    None
                } else {
                    Some(api_url_for_blocking.as_str())
                },
                &reliability_for_blocking,
                &model_routes_for_blocking,
                &model_name_for_blocking,
                &options_for_blocking,
            )
        })
        .await
        .map_err(|e| anyhow::anyhow!("Provider reload task failed: {}", e))?
        .map_err(|e| anyhow::anyhow!("Failed to create provider: {}", e))?;

        self.provider = new_provider;
        let model_actually_changed = model_name != self.model_name;
        self.model_name = model_name;

        self.cached_provider = provider_name_raw;
        self.cached_api_key = crate::security::secret_string::SecretString::new(api_key);
        self.cached_api_url = api_url;

        if model_actually_changed {
            self.refresh_history_system_prompt();
        }

        tracing::info!("Provider reloaded successfully");
        Ok(())
    }

    pub fn signal_runtime_model_switch(&mut self, provider: String, model: String) {
        let trimmed_provider = provider.trim();
        let trimmed_model = model.trim();
        if !trimmed_provider.is_empty() {
            self.cached_provider = trimmed_provider.to_string();
        }
        if !trimmed_model.is_empty() && trimmed_model != self.model_name {
            tracing::info!(
                target = "runtime_model_switch",
                old_model = %self.model_name,
                new_model = %trimmed_model,
                provider = %trimmed_provider,
                "UI-initiated model switch: updating in-memory model_name"
            );
            self.model_name = trimmed_model.to_string();
            self.refresh_history_system_prompt();
        }
    }

    pub async fn apply_runtime_config_now(&mut self) -> Result<()> {
        let config = self.shared_config.load_ref();
        let new_provider = config
            .default_provider
            .clone()
            .unwrap_or_else(|| "openrouter".to_string());
        let new_api_key = config.api_key.clone().unwrap_or_default();
        let new_api_url = config.api_url.clone().unwrap_or_default();
        let new_model = providers::resolve_default_model(&config).ok();
        drop(config);

        let model_unchanged = match new_model.as_deref() {
            Some(m) => m == self.model_name,
            None => true,
        };
        let unchanged = new_provider == self.cached_provider
            && self.cached_api_key.constant_time_eq(&new_api_key)
            && new_api_url == self.cached_api_url
            && model_unchanged;
        if unchanged {
            return Ok(());
        }

        self.reload_provider().await?;
        self.refresh_history_system_prompt();
        Ok(())
    }

    fn refresh_history_system_prompt(&mut self) {
        if self.history.is_empty() {
            return;
        }
        let new_prompt = match self.build_system_prompt() {
            Ok(s) => s,
            Err(e) => {
                tracing::warn!(
                    target = "runtime_model_switch",
                    error = %e,
                    "failed to rebuild system prompt after model switch"
                );
                return;
            }
        };
        if let Some(ConversationMessage::Chat(chat)) = self.history.first_mut() {
            if chat.role == "system" {
                chat.content = new_prompt;
            }
        }
    }

    pub async fn execute_tool(
        &self,
        name: &str,
        arguments: serde_json::Value,
    ) -> crate::agent::dispatcher::ToolExecutionResult {
        let parsed = crate::agent::dispatcher::ParsedToolCall {
            name: name.to_string(),
            arguments,
            tool_call_id: Some(uuid::Uuid::new_v4().to_string()),
            parse_error: false,
        };
        self.execute_tool_call(&parsed).await
    }

    pub fn set_memory_session_id(&mut self, session_id: Option<String>) {
        self.memory_session_id = session_id;
    }

    pub fn current_workspace_dir(&self) -> &std::path::Path {
        &self.workspace_dir
    }

    pub fn set_session_workspace_dir(&mut self, path: std::path::PathBuf) {
        if path.as_os_str().is_empty() {
            return;
        }
        tracing::info!(path = %path.display(), "agent: using per-session workspace directory");
        if let Some(ref p) = self.desktop_security_policy {
            p.retarget_session_workspace_root(path.clone());
            self.security_summary = Some(p.prompt_summary());
        }
        crate::security::register_workspace_root(&path);
        self.workspace_dir = path.clone();

        crate::agent::token::optimizer::ensure_workspace_optimizer(path.clone());

        self.reload_skills_for_workspace(&path);
    }

    pub fn reload_skills_for_workspace(&mut self, workspace_dir: &std::path::Path) {
        let config = self.shared_config.load();
        let new_skills =
            crate::skills::load_skills_with_config(workspace_dir, config.as_ref());

        self.tools
            .retain(|t| !t.name().contains('.') && t.name() != "read_skill");
        if let Some(ref policy) = self.desktop_security_policy {
            crate::tools::register_skill_tools(
                &mut self.tools,
                &new_skills,
                std::sync::Arc::clone(policy),
            );
        }

        if matches!(
            config.skills.prompt_injection_mode,
            crate::config::SkillsPromptInjectionMode::Compact
        ) {
            self.tools.push(Box::new(crate::tools::ReadSkillTool::new(
                workspace_dir.to_path_buf(),
                config.skills.open_skills_enabled,
                config.skills.open_skills_dir.clone(),
                config.skills.disabled_skills.clone(),
            )));
        }

        let specs: Vec<crate::tools::ToolSpec> =
            self.tools.iter().map(|t| t.spec()).collect();
        self.tool_specs = std::sync::Arc::new(crate::tools::dedupe_tool_specs(&specs));
        self.rebuild_tool_index();
        self.mode_filter_dirty = true;
        self.skills = new_skills;
    }

    pub fn set_rbac_session(
        &mut self,
        engine: Option<Arc<crate::security::rbac::RbacEngine>>,
        identity: Option<crate::security::rbac::CallerIdentity>,
    ) {
        self.rbac_engine = engine;
        self.rbac_identity = identity;
    }

    pub fn set_hook_runner(
        &mut self,
        runner: Option<std::sync::Arc<crate::hooks::HotHookRunner>>,
    ) {
        self.hook_runner = runner;
    }

    fn compute_tools_signature(config: &crate::config::Config) -> u64 {
        use std::hash::{Hash, Hasher};
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        config.mcp.enabled.hash(&mut hasher);
        config.mcp.deferred_loading.hash(&mut hasher);
        for s in &config.mcp.servers {
            s.name.hash(&mut hasher);
            s.command.hash(&mut hasher);
            s.url.hash(&mut hasher);
            s.enabled.hash(&mut hasher);
            for a in &s.args {
                a.hash(&mut hasher);
            }

            let mut env_pairs: Vec<(&String, &String)> = s.env.iter().collect();
            env_pairs.sort_unstable();
            for (k, v) in env_pairs {
                k.hash(&mut hasher);
                v.hash(&mut hasher);
            }
            let mut hdr_pairs: Vec<(&String, &String)> = s.headers.iter().collect();
            hdr_pairs.sort_unstable();
            for (k, v) in hdr_pairs {
                k.hash(&mut hasher);
                v.hash(&mut hasher);
            }
        }
        for t in &config.custom_tools.tools {
            t.name.hash(&mut hasher);
            t.description.hash(&mut hasher);
        }
        config.web_search.enabled.hash(&mut hasher);
        config.web_fetch.enabled.hash(&mut hasher);

        let mut agent_keys: Vec<&String> = config.agents.keys().collect();
        agent_keys.sort();
        for k in agent_keys {
            k.hash(&mut hasher);
            if let Some(cfg) = config.agents.get(k) {
                cfg.provider.hash(&mut hasher);
                cfg.model.hash(&mut hasher);
                cfg.agentic.hash(&mut hasher);
                cfg.max_depth.hash(&mut hasher);
            }
        }

        for t in &config.custom_tools.tools {
            t.enabled.hash(&mut hasher);
            t.command.hash(&mut hasher);
            for a in &t.args {
                a.hash(&mut hasher);
            }
            t.cwd.hash(&mut hasher);
            t.timeout_secs.hash(&mut hasher);
            t.schema.to_string().hash(&mut hasher);
        }

        let mut disabled = config.skills.disabled_skills.clone();
        disabled.sort();
        for d in &disabled {
            d.hash(&mut hasher);
        }
        format!("{:?}", config.skills.prompt_injection_mode).hash(&mut hasher);
        config.skills.allow_scripts.hash(&mut hasher);
        config.skills.open_skills_enabled.hash(&mut hasher);
        config.skills.open_skills_dir.hash(&mut hasher);

        config.lsp.enabled.hash(&mut hasher);
        for entry in &config.lsp.servers {
            entry.id.hash(&mut hasher);
            entry.language_id.hash(&mut hasher);
            entry.enabled.hash(&mut hasher);
            entry.managed.hash(&mut hasher);
            entry.command.hash(&mut hasher);
            for a in &entry.args {
                a.hash(&mut hasher);
            }

            serde_json::to_string(&entry.install_state)
                .unwrap_or_default()
                .hash(&mut hasher);
        }
        hasher.finish()
    }

    fn refresh_delegate_agents_from_config(&mut self, config: &crate::config::Config) {
        for tool in &self.tools {
            if let Some(any_ref) = tool.as_any() {
                if let Some(dt) = any_ref.downcast_ref::<crate::tools::DelegateTool>() {
                    dt.refresh_agents(config.agents.clone());
                    tracing::info!(
                        target: "agent.delegate",
                        agents = config.agents.len(),
                        "DelegateTool subagent table refreshed from live config"
                    );
                    break;
                }
            }
        }
    }

    pub async fn sync_tools_from_config_if_changed(&mut self) {
        let config_arc = self.shared_config.load_ref();
        let signature = Self::compute_tools_signature(&config_arc);
        if signature == self.cached_tools_signature {
            return;
        }
        tracing::info!(
            target: "agent.tools",
            old = self.cached_tools_signature,
            new = signature,
            "tools signature changed; reloading MCP / custom / web tools"
        );
        self.reload_mcp_tools_inner(&config_arc).await;
        self.reload_custom_tools_inner(&config_arc);
        self.reload_web_tools_inner(&config_arc);
        self.refresh_delegate_agents_from_config(&config_arc);

        let workspace_for_skills = self
            .desktop_security_policy
            .as_ref()
            .map(|p| p.workspace_root_handle().read().clone())
            .filter(|p| !p.as_os_str().is_empty())
            .unwrap_or_else(|| config_arc.workspace_dir.clone());
        self.reload_skills_for_workspace(&workspace_for_skills);

        let specs: Vec<crate::tools::ToolSpec> =
            self.tools.iter().map(|t| t.spec()).collect();
        self.tool_specs = std::sync::Arc::new(crate::tools::dedupe_tool_specs(&specs));
        self.rebuild_tool_index();
        self.mode_filter_dirty = true;
        self.cached_tools_signature = signature;
    }

    fn rebuild_tool_index(&mut self) {
        self.tool_index = self
            .tools
            .iter()
            .enumerate()
            .map(|(i, t)| (t.name().to_string(), i))
            .collect();
    }

    async fn reload_mcp_tools_inner(&mut self, config: &crate::config::Config) {

        self.tools.retain(|t| {
            let name = t.name();
            !(name == "tool_search"
                || (name.contains("__") && name.split_once("__").map_or(false, |(h, _)| !h.is_empty()))
                || name.starts_with("mcp_"))
        });
        if !config.mcp.enabled || config.mcp.servers.is_empty() {
            return;
        }
        match crate::tools::McpRegistry::connect_all(&config.mcp.servers).await {
            Ok(registry) => {
                let registry = std::sync::Arc::new(registry);
                if config.mcp.deferred_loading {
                    let deferred_set =
                        crate::tools::DeferredMcpToolSet::from_registry(std::sync::Arc::clone(
                            &registry,
                        ))
                        .await;
                    let activated = std::sync::Arc::new(parking_lot::Mutex::new(
                        crate::tools::ActivatedToolSet::new(),
                    ));
                    self.activated_tools = Some(std::sync::Arc::clone(&activated));
                    self.tools.push(Box::new(crate::tools::ToolSearchTool::new(
                        deferred_set,
                        activated,
                    )));
                } else {
                    let names = registry.tool_names();
                    let mut registered = 0usize;
                    for name in names {
                        if let Some(def) = registry.get_tool_def(&name).await {
                            let wrapper: std::sync::Arc<dyn crate::tools::Tool> =
                                std::sync::Arc::new(crate::tools::McpToolWrapper::new(
                                    name,
                                    def,
                                    std::sync::Arc::clone(&registry),
                                ));
                            self.tools
                                .push(Box::new(crate::tools::ArcToolRef(wrapper)));
                            registered += 1;
                        }
                    }
                    tracing::info!(
                        registered,
                        servers = registry.server_count(),
                        "MCP reload: tools registered"
                    );
                }
            }
            Err(e) => {
                tracing::error!("MCP reload failed to initialise registry: {e:#}");
            }
        }
    }

    fn reload_custom_tools_inner(&mut self, config: &crate::config::Config) {

        self.tools.retain(|t| !t.name().starts_with("custom_"));

        let workspace_root = match self.desktop_security_policy.as_ref() {
            Some(policy) => policy.workspace_root_handle(),
            None => std::sync::Arc::new(parking_lot::RwLock::new(config.workspace_dir.clone())),
        };

        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
        for tool in self.tools.iter() {
            seen.insert(tool.name().to_string());
        }
        for def in &config.custom_tools.tools {
            if !def.enabled {
                continue;
            }
            let validation_errors = def.validate();
            if !validation_errors.is_empty() {
                tracing::warn!(
                    target: "agent.custom_tools",
                    name = %def.name,
                    errors = ?validation_errors,
                    "custom_tools: skipping invalid entry on hot reload"
                );
                continue;
            }
            let registered = format!("custom_{}", def.name.trim());
            if !seen.insert(registered.clone()) {
                tracing::warn!(
                    target: "agent.custom_tools",
                    name = %registered,
                    "custom_tools: duplicate tool name on hot reload, skipping"
                );
                continue;
            }
            self.tools
                .push(Box::new(crate::tools::custom_tool::CustomTool::from_def(
                    def,
                    std::sync::Arc::clone(&workspace_root),
                )));
        }
    }

    fn reload_web_tools_inner(&mut self, config: &crate::config::Config) {

        let want_web_search = config.web_search.enabled;
        let has_web_search = self.tools.iter().any(|t| t.name() == "web_search_tool");
        if want_web_search && !has_web_search {
            self.tools.push(Box::new(
                crate::tools::WebSearchTool::new_with_config(
                    config.web_search.provider.clone(),
                    config.web_search.brave_api_key.clone(),
                    config.web_search.searxng_instance_url.clone(),
                    config.web_search.max_results,
                    config.web_search.timeout_secs,
                    config.config_path.clone(),
                    config.secrets.encrypt,
                ),
            ));
        } else if !want_web_search && has_web_search {
            self.tools.retain(|t| t.name() != "web_search_tool");
        }

        let want_web_fetch = config.web_fetch.enabled;
        let has_web_fetch = self.tools.iter().any(|t| t.name() == "web_fetch");
        if want_web_fetch && !has_web_fetch {
            if let Some(ref policy) = self.desktop_security_policy {
                self.tools.push(Box::new(crate::tools::WebFetchTool::new(
                    std::sync::Arc::clone(policy),
                    config.web_fetch.allowed_domains.clone(),
                    config.web_fetch.blocked_domains.clone(),
                    config.web_fetch.max_response_size,
                    config.web_fetch.timeout_secs,
                    config.web_fetch.firecrawl.clone(),
                    config.web_fetch.allowed_private_hosts.clone(),
                )));
            }
        } else if !want_web_fetch && has_web_fetch {
            self.tools.retain(|t| t.name() != "web_fetch");
        }
    }

    pub fn add_node_tools_from_registry(
        &mut self,
        registry: std::sync::Arc<crate::gateway::nodes::NodeRegistry>,
    ) {
        for (node_id, _, cap) in registry.all_capabilities() {
            let prefixed = crate::tools::NodeTool::tool_name(&node_id, &cap.name);
            if let Some(ref allow) = self.allowed_tools {
                if !allow.iter().any(|a| a == &prefixed) {
                    continue;
                }
            }
            self.tools.push(Box::new(crate::tools::NodeTool::new(
                node_id.clone(),
                cap.name.clone(),
                cap.description.clone(),
                cap.parameters.clone(),
                std::sync::Arc::clone(&registry),
            )));
        }
        let specs: Vec<ToolSpec> = self.tools.iter().map(|t| t.spec()).collect();
        self.tool_specs = std::sync::Arc::new(crate::tools::dedupe_tool_specs(&specs));
        self.rebuild_tool_index();
    }

    pub fn seed_history(&mut self, messages: &[ChatMessage]) {
        if self.history.is_empty() {
            if let Ok(sys) = self.build_system_prompt() {
                self.history
                    .push(ConversationMessage::Chat(ChatMessage::system(sys)));
            }
        }
        let mirrored =
            crate::providers::sanitize::mirror_tool_ids_in_chat_messages(messages.to_vec());
        let cleaned =
            crate::providers::sanitize::clean_empty_assistant_tool_calls_in_chat_messages(mirrored);
        let mut expanded =
            super::sqlite_gateway_hydrate::hydrate_gateway_sqlite_messages(&cleaned);
        Self::repair_orphan_tool_result_messages(&mut expanded);
        crate::agent::dangling_tool_repair::drop_payloadless_assistant_messages(&mut expanded);
        if crate::agent::dangling_tool_repair::tail_signals_interrupted_turn(&expanded) {
            self.last_turn_interrupted = true;
        }
        self.activate_deferred_tools_from_history(&expanded);
        self.history.extend(expanded);
    }

    fn focus_history_for_plan_execution(
        history: Vec<crate::providers::traits::ChatMessage>,
    ) -> Vec<crate::providers::traits::ChatMessage> {
        let Some(last_user_idx) = history.iter().rposition(|m| m.role == "user") else {
            return history;
        };
        let first_system_idx = history.iter().position(|m| m.role == "system");
        let dropped = history
            .len()
            .saturating_sub(1 + usize::from(first_system_idx.is_some()));
        if dropped == 0 {
            return history;
        }
        let chars_before: usize = history.iter().map(|m| m.content.len()).sum();
        let system_chars = first_system_idx.map(|i| history[i].content.len()).unwrap_or(0);
        let trigger_chars = history[last_user_idx].content.len();
        let mut focused: Vec<crate::providers::traits::ChatMessage> = Vec::with_capacity(2);
        if let Some(i) = first_system_idx {
            focused.push(history[i].clone());
        }
        focused.push(history[last_user_idx].clone());
        let chars_after: usize = focused.iter().map(|m| m.content.len()).sum();
        tracing::info!(
            target: "agent.plan_execution",
            dropped,
            chars_before,
            chars_after,
            system_chars,
            trigger_chars,
            "plan execution armed: focusing context on the base system prompt + plan trigger \
             only, dropping the entire conversation backlog (including accumulated system \
             reminders / summaries) so the model executes the plan on a clean, minimal context \
             instead of being overwhelmed or answering stale questions"
        );
        focused
    }

    fn activate_deferred_tools_from_history(&self, history: &[ConversationMessage]) {
        if self.activated_tools.is_none() {
            return;
        }
        let surface = self.surface;
        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
        let mut builtin_pending: Vec<(String, crate::tools::ToolSpec)> = Vec::new();
        let mut mcp_pending: Vec<String> = Vec::new();
        for msg in history {
            if let ConversationMessage::AssistantToolCalls { tool_calls, .. } = msg {
                for tc in tool_calls {
                    if !seen.insert(tc.name.clone()) {
                        continue;
                    }
                    if tc.name.contains("__") {
                        mcp_pending.push(tc.name.clone());
                        continue;
                    }
                    let entry = crate::tools::handler::tier::classify(&tc.name, surface);
                    if !matches!(
                        entry.tier,
                        crate::tools::handler::tier::BuiltinToolTier::OnDemand
                    ) {
                        continue;
                    }
                    if let Some(spec) = self
                        .tools
                        .iter()
                        .find(|t| t.name() == tc.name)
                        .map(|t| t.spec())
                    {
                        builtin_pending.push((tc.name.clone(), spec));
                    }
                }
            }
        }
        if builtin_pending.is_empty() && mcp_pending.is_empty() {
            return;
        }
        let mut activated_now: Vec<String> = Vec::new();
        if !builtin_pending.is_empty() {
            if let Some(ref activated_arc) = self.activated_tools {
                let mut guard = activated_arc.lock();
                for (name, spec) in builtin_pending {
                    if guard.is_activated(&name) {
                        continue;
                    }
                    guard.activate_spec(name.clone(), spec);
                    activated_now.push(name);
                }
            }
        }
        if !mcp_pending.is_empty() {
            if let Some(tool_search_tool) = self
                .tool_index
                .get("tool_search")
                .map(|&i| &self.tools[i])
            {
                if let Some(any_ref) = tool_search_tool.as_any() {
                    if let Some(ts) =
                        any_ref.downcast_ref::<crate::tools::ToolSearchTool>()
                    {
                        for name in mcp_pending {
                            if ts.activate_from_history(&name) {
                                activated_now.push(name);
                            }
                        }
                    }
                }
            }
        }
        if !activated_now.is_empty() {
            tracing::info!(
                target: "agent.tool_search.replay",
                count = activated_now.len(),
                names = ?activated_now,
                "replayed deferred tool activations from hydrated history"
            );
        }
    }

    pub async fn from_config(
        config: &Config,
        denied_tools: Option<Vec<String>>,
        shared_config: Option<crate::config::live::LiveConfig>,
    ) -> Result<Self> {
        if crate::services::governance::credential_vault::try_get_credential_vault().is_none() {
            let anchor = if config.workspace_dir.as_os_str().is_empty() {
                std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
            } else {
                config.workspace_dir.clone()
            };
            if let Err(err) = crate::services::governance::credential_vault::init_credential_vault(&anchor) {
                tracing::warn!(error = %err, "credential vault initialisation failed for agent session");
            }
        }
        let observer: Arc<dyn Observer> =
            Arc::from(observability::create_observer(&config.observability));
        let runtime: Arc<dyn runtime::RuntimeAdapter> =
            Arc::from(runtime::create_runtime(&config.runtime)?);
        let security = Arc::new(SecurityPolicy::from_config(
            &config.autonomy,
            &config.workspace_dir,
        ));

        let memory: Arc<dyn Memory> = Arc::from(
            memory::create_memory_with_storage_and_routes_async(
                config.memory.clone(),
                config.embedding_routes.clone(),
                Some(config.storage.provider.config.clone()),
                config.workspace_dir.clone(),
                config.api_key.clone(),
            )
            .await?,
        );

        let composio_key = if config.composio.enabled {
            config.composio.api_key.as_deref()
        } else {
            None
        };
        let composio_entity_id = if config.composio.enabled {
            Some(config.composio.entity_id.as_str())
        } else {
            None
        };

        let (
            mut tools,
            delegate_handle,
            _reaction_handle,
            _channel_map_handle,
            _ask_user_handle,
            _escalate_handle,
            plan_mode_flag,
        ) = tools::all_tools_with_runtime(
            Arc::new(config.clone()),
            &security,
            runtime,
            memory.clone(),
            composio_key,
            composio_entity_id,
            &config.browser,
            &config.http_request,
            &config.web_fetch,
            &config.workspace_dir,
            &config.agents,
            config.api_key.as_deref(),
            config,
            None,
        );

        let mut activated_tools: Option<Arc<parking_lot::Mutex<tools::ActivatedToolSet>>> = None;
        let (builtin_deferred_enabled, mcp_deferred_enabled) =
            crate::tools::deferred_loading_effective(config);
        if config.mcp.enabled && !config.mcp.servers.is_empty() {
            tracing::info!(
                "Initializing MCP client  -  {} server(s) configured",
                config.mcp.servers.len()
            );
            match tools::McpRegistry::connect_all(&config.mcp.servers).await {
                Ok(registry) => {
                    let registry = std::sync::Arc::new(registry);
                    if mcp_deferred_enabled {
                        let deferred_set = tools::DeferredMcpToolSet::from_registry(
                            std::sync::Arc::clone(&registry),
                        )
                        .await;
                        tracing::info!(
                            "MCP deferred: {} tool stub(s) from {} server(s)",
                            deferred_set.len(),
                            registry.server_count()
                        );
                        let activated =
                            Arc::new(parking_lot::Mutex::new(tools::ActivatedToolSet::new()));
                        activated_tools = Some(Arc::clone(&activated));
                        tools.push(Box::new(tools::ToolSearchTool::new(
                            deferred_set,
                            activated,
                        )));
                    } else {
                        let names = registry.tool_names();
                        let mut registered = 0usize;
                        for name in names {
                            if let Some(def) = registry.get_tool_def(&name).await {
                                let wrapper: std::sync::Arc<dyn tools::Tool> =
                                    std::sync::Arc::new(tools::McpToolWrapper::new(
                                        name,
                                        def,
                                        std::sync::Arc::clone(&registry),
                                    ));
                                if let Some(ref handle) = delegate_handle {
                                    handle.write().push(std::sync::Arc::clone(&wrapper));
                                }
                                tools.push(Box::new(tools::ArcToolRef(wrapper)));
                                registered += 1;
                            }
                        }
                        tracing::info!(
                            "MCP: {} tool(s) registered from {} server(s)",
                            registered,
                            registry.server_count()
                        );
                    }
                }
                Err(e) => {
                    tracing::error!("MCP registry failed to initialize: {e:#}");
                }
            }
        }

        if builtin_deferred_enabled {
            let mut deferred_section_unused = String::new();
            let workspace_key = crate::session::workspace_key_from_path(
                &config.workspace_dir,
                "default",
            );
            let options = crate::tools::BuiltinDeferredRegistrationOptions {
                workspace_key: workspace_key.clone(),
                allowlist: Vec::new(),
                gate: None,
                config: Some(config),
            };
            let deferred_builtin_set =
                crate::tools::apply_builtin_deferred_registration_with_options(
                    &mut tools,
                    &mut deferred_section_unused,
                    crate::tools::ToolSurfaceBaseline::Both,
                    &mut activated_tools,
                    options,
                );

            if let (Some(activated_handle), Some(svc)) =
                (activated_tools.as_ref(), crate::services::try_get_services())
            {
                match svc.tool_activation_store.load(&workspace_key).await {
                    Ok(names) => {
                        if !names.is_empty() {
                            let mut guard = activated_handle.lock();
                            for name in &names {
                                if guard.is_activated(name) {
                                    continue;
                                }
                                if let Some(spec) = deferred_builtin_set.tool_spec(name) {
                                    guard.activate_spec(name.clone(), spec);
                                }
                            }
                        }
                    }
                    Err(e) => {
                        tracing::warn!(
                            target: "tool_activation_store",
                            workspace_key = %workspace_key,
                            error = %e,
                            "failed to preload activated tools"
                        );
                    }
                }
            }
        }

        let provider_name_raw = config.default_provider.as_deref().unwrap_or("openrouter");
        let provider_name =
            providers::resolve_runtime_provider_name(provider_name_raw, config);

        let model_name = providers::resolve_default_model(config)?;

        let provider_runtime_options = providers::provider_runtime_options_from_config(config);

        let provider: Box<dyn Provider> = providers::create_routed_provider_with_options_async(
            provider_name.clone(),
            config.api_key.clone(),
            config.api_url.clone(),
            config.reliability.clone(),
            config.model_routes.clone(),
            model_name.clone(),
            provider_runtime_options.clone(),
        )
        .await?;

        let dispatcher_choice = config.agent.tool_dispatcher.as_str();
        let tool_dispatcher: Box<dyn ToolDispatcher> = match dispatcher_choice {
            "native" => Box::new(NativeToolDispatcher),
            "xml" => Box::new(XmlToolDispatcher),
            _ if provider.supports_native_tools() => Box::new(NativeToolDispatcher),
            _ => Box::new(XmlToolDispatcher),
        };

        let route_model_by_hint: HashMap<String, String> = config
            .model_routes
            .iter()
            .map(|route| (route.hint.clone(), route.model.clone()))
            .collect();
        let available_hints: Vec<String> = route_model_by_hint.keys().cloned().collect();

        let response_cache = if config.memory.response_cache_enabled {
            let cache_ws = config.workspace_dir.clone();
            let cache_ttl = config.memory.response_cache_ttl_minutes;
            let cache_max = config.memory.response_cache_max_entries;
            let cache_hot = config.memory.response_cache_hot_entries;
            tokio::task::spawn_blocking(move || {
                crate::memory::response_cache::ResponseCache::with_hot_cache(
                    &cache_ws, cache_ttl, cache_max, cache_hot,
                )
            })
            .await
            .ok()
            .and_then(|r| r.ok())
            .map(Arc::new)
        } else {
            None
        };

        crate::agent::token::optimizer::ensure_global_optimizer_from_config(config);

        crate::token_saver::set_enabled(config.token_saver.enabled);
        crate::token_saver::set_global(config.token_saver.to_runtime_ctx());
        crate::guardrails::ensure_global_guardrails(config.guardrails.clone());

        let experience_replay = if config.experience.enabled {
            Some(crate::agent::reward::experience::ExperienceReplay::new(
                &config.experience,
            ))
        } else {
            None
        };

        if let Some(ref deny_list) = denied_tools {
            let deny_set: std::collections::HashSet<_> = deny_list.iter().cloned().collect();
            tools.retain(|t| !deny_set.contains(t.name()));
        }

        let loaded_skills = {
            let skills_workspace = config.workspace_dir.clone();
            let skills_config = config.clone();
            tokio::task::spawn_blocking(move || {
                crate::skills::load_skills_with_config(&skills_workspace, &skills_config)
            })
            .await
            .unwrap_or_else(|e| {
                tracing::warn!(error = %e, "skill loading task failed; continuing without skills");
                Vec::new()
            })
        };

        Agent::builder()
            .provider(provider)
            .tools(tools)
            .memory(memory)
            .observer(observer)
            .response_cache(response_cache)
            .tool_dispatcher(tool_dispatcher)
            .memory_loader(Box::new(DefaultMemoryLoader::new(
                5,
                config.memory.min_relevance_score,
            )))
            .prompt_builder(SystemPromptBuilder::with_defaults())
            .config(config.agent.clone())
            .model_name(model_name)
            .temperature(config.default_temperature)
            .workspace_dir(config.workspace_dir.clone())
            .classification_config(config.query_classification.clone())
            .available_hints(available_hints)
            .route_model_by_hint(route_model_by_hint)
            .identity_config(config.identity.clone())
            .skills(loaded_skills)
            .skills_prompt_mode(config.skills.prompt_injection_mode)
            .auto_save(config.memory.auto_save)
            .security_summary(Some(security.prompt_summary()))
            .autonomy_level(config.autonomy.level)
            .activated_tools(activated_tools)
            .surface(crate::tools::ToolSurfaceBaseline::Both)
            .user_profile_config(config.user_profile.clone())
            .skill_evolution_config(config.skill_evolution.clone())
            .prompt_optimizer_config(config.prompt_optimizer.clone())
            .experience_replay(experience_replay)
            .plan_mode_config(config.plan_mode.clone())
            .intent_analysis_config(config.intent_analysis.clone())
            .shared_config(shared_config.unwrap_or_else(crate::config::live::LiveConfig::default))
            .cached_provider_config(
                provider_name_raw.to_string(),
                config.api_key.clone().unwrap_or_default(),
                config.api_url.clone().unwrap_or_default(),
            )
            .desktop_security_policy(Some(Arc::clone(&security)))
            .plan_mode_flag(plan_mode_flag)
            .build()
    }

    fn trim_history(&mut self) {
        let max_messages = self.config.max_history_messages;

        const MAX_CHARS: usize = 400_000;

        let mut system_messages = Vec::new();
        let mut other_messages = Vec::new();

        for msg in self.history.drain(..) {
            match &msg {
                ConversationMessage::Chat(chat) if chat.role == "system" => {
                    system_messages.push(msg);
                }
                _ => other_messages.push(msg),
            }
        }

        if other_messages.len() > max_messages {
            let drop_count = other_messages.len() - max_messages;
            other_messages.drain(0..drop_count);
        }

        let mut total_chars: usize = system_messages.iter().map(|m| Self::msg_char_len(m)).sum();
        let mut start = 0;
        for (i, msg) in other_messages.iter().enumerate() {
            total_chars += Self::msg_char_len(msg);
            if total_chars > MAX_CHARS && i > 0 {
                start = i;
                break;
            }
        }
        if start > 0 {
            other_messages.drain(0..start);
        }

        self.history = system_messages;
        self.history.extend(other_messages);
        Self::repair_orphan_tool_result_messages(&mut self.history);
    }

    fn repair_orphan_tool_result_messages(history: &mut Vec<ConversationMessage>) {
        Self::upgrade_native_json_assistants_in_place(history);
        Self::collapse_empty_assistant_tool_calls(history);

        let mut out = Vec::with_capacity(history.len());
        let mut recovered_tool_result_batches = 0usize;
        let mut recovered_chat_tool_rows = 0usize;
        for msg in history.drain(..) {
            match &msg {
                ConversationMessage::ToolResults(rows) => {
                    let preceded = out
                        .last()
                        .is_some_and(|p| matches!(p, ConversationMessage::AssistantToolCalls { .. }));
                    if preceded {
                        out.push(msg);
                    } else {
                        recovered_tool_result_batches += 1;
                        tracing::debug!(
                            target: "agent.history_repair",
                            batch_len = rows.len(),
                            "recovered orphaned ToolResults as synthetic user transcript (missing assistant preamble)"
                        );
                        out.push(Self::recover_tool_results_batch_as_user(rows));
                    }
                }
                ConversationMessage::Chat(c) if c.role == "tool" => {
                    let preceded = out.last().is_some_and(|p| match p {
                        ConversationMessage::AssistantToolCalls { .. } => true,
                        ConversationMessage::Chat(pc) => pc.role == "tool",
                        _ => false,
                    });
                    if preceded {
                        out.push(msg);
                    } else {
                        recovered_chat_tool_rows += 1;
                        tracing::debug!(
                            target: "agent.history_repair",
                            "recovered orphaned Chat(role=tool) as synthetic user transcript (missing assistant preamble)"
                        );
                        out.push(Self::recover_chat_tool_as_user(c));
                    }
                }
                _ => out.push(msg),
            }
        }
        if recovered_tool_result_batches > 0 || recovered_chat_tool_rows > 0 {
            tracing::info!(
                target: "agent.history_repair",
                tool_result_batches = recovered_tool_result_batches,
                chat_tool_rows = recovered_chat_tool_rows,
                "recovered orphaned tool transcript rows as synthetic user transcript"
            );
        }
        *history = out;
        crate::agent::dangling_tool_repair::ensure_assistant_tool_replies_inplace(history);
    }

    fn collapse_empty_assistant_tool_calls(history: &mut [ConversationMessage]) {
        for m in history.iter_mut() {
            let collapsed = match &*m {
                ConversationMessage::AssistantToolCalls {
                    text,
                    tool_calls,
                    reasoning_content,
                } if tool_calls.is_empty() => Some(Self::collapsed_native_style_assistant_blob(
                    text.as_deref(),
                    reasoning_content.as_deref(),
                )),
                _ => None,
            };
            if let Some(content) = collapsed {
                *m = ConversationMessage::Chat(ChatMessage::assistant(content));
            }
        }
    }

    fn collapsed_native_style_assistant_blob(
        text: Option<&str>,
        reasoning_content: Option<&str>,
    ) -> String {
        let mut map = serde_json::Map::new();
        if let Some(t) = text.filter(|s| !s.is_empty()) {
            map.insert(
                "content".to_string(),
                serde_json::Value::String(t.to_string()),
            );
        }
        if let Some(r) = reasoning_content.filter(|s| !s.is_empty()) {
            map.insert(
                "reasoning_content".to_string(),
                serde_json::Value::String(r.to_string()),
            );
        }
        if map.is_empty() {
            return String::new();
        }
        serde_json::Value::Object(map).to_string()
    }

    fn recover_tool_results_batch_as_user(
        rows: &[crate::providers::ToolResultMessage],
    ) -> ConversationMessage {
        let mut buf = String::from(
            "[Recovered tool batch; assistant tool_calls preamble was missing in transcript]\n\n",
        );
        use std::fmt::Write as _;
        for tr in rows {
            let _ = writeln!(
                &mut buf,
                "### {}\n{}",
                tr.tool_call_id,
                tr.content.trim_end()
            );
            let _ = writeln!(&mut buf);
        }
        ConversationMessage::Chat(ChatMessage::user(buf))
    }

    fn recover_chat_tool_as_user(tool: &ChatMessage) -> ConversationMessage {
        let fallback_body = tool.content.clone();
        let parsed = serde_json::from_str::<serde_json::Value>(&tool.content).ok();
        let id = parsed
            .as_ref()
            .and_then(|v| v.get("tool_call_id"))
            .and_then(serde_json::Value::as_str)
            .unwrap_or("unknown")
            .to_string();
        let body = parsed
            .as_ref()
            .and_then(|v| v.get("content"))
            .map(|c| match c {
                serde_json::Value::String(s) => s.clone(),
                other => other.to_string(),
            })
            .unwrap_or(fallback_body);
        let msg = format!(
            "[Recovered tool output; assistant tool_calls preamble was missing in transcript]\n\
             tool_call_id={id}\n\
             {body}",
        );
        ConversationMessage::Chat(ChatMessage::user(msg))
    }

    fn upgrade_native_json_assistants_in_place(history: &mut [ConversationMessage]) {
        for m in history.iter_mut() {
            if let ConversationMessage::Chat(c) = m {
                if c.role != "assistant" {
                    continue;
                }
                if let Some(up) = Self::try_chat_as_native_assistant_tool_calls(c) {
                    *m = up;
                }
            }
        }
    }

    fn try_chat_as_native_assistant_tool_calls(chat: &ChatMessage) -> Option<ConversationMessage> {
        let v: serde_json::Value = serde_json::from_str(chat.content.trim()).ok()?;
        if !v.is_object() {
            return None;
        }
        let tc_val = v.get("tool_calls")?;
        let tool_calls: Vec<ToolCall> = serde_json::from_value(tc_val.clone()).ok()?;
        if tool_calls.is_empty() {
            return None;
        }
        let text = match v.get("content") {
            None | Some(serde_json::Value::Null) => None,
            Some(serde_json::Value::String(s)) => {
                if s.is_empty() {
                    None
                } else {
                    Some(s.clone())
                }
            }
            Some(_) => return None,
        };
        let reasoning_content = v
            .get("reasoning_content")
            .and_then(serde_json::Value::as_str)
            .filter(|s| !s.is_empty())
            .map(str::to_string);
        Some(ConversationMessage::AssistantToolCalls {
            text,
            tool_calls,
            reasoning_content,
        })
    }

    fn msg_char_len(msg: &ConversationMessage) -> usize {
        match msg {
            ConversationMessage::Chat(chat) => chat.content.len(),
            _ => 200,
        }
    }

    fn build_system_prompt(&self) -> Result<String> {
        let live_cfg = self.shared_config.load();
        let coding_mode_label = self.current_coding_mode.map(|m| m.label());
        let allowed_tool_names = self.current_coding_mode.and_then(|m| m.allowed_tools());

        let instructions = self.tool_dispatcher.prompt_instructions(&self.tools);
        let ctx = PromptContext {
            workspace_dir: &self.workspace_dir,
            model_name: &self.model_name,
            tools: &self.tools,
            allowed_tool_names: allowed_tool_names.clone(),
            skills: &self.skills,
            skills_prompt_mode: self.skills_prompt_mode,
            identity_config: Some(&self.identity_config),
            dispatcher_instructions: &instructions,
            tool_descriptions: self.tool_descriptions.as_ref(),
            security_summary: self.security_summary.clone(),
            autonomy_level: self.autonomy_level,
            global_directives: live_cfg.agent.global_directives.as_slice(),
            coding_mode_label,
        };
        let mut prompt = self.prompt_builder.build(&ctx)?;

        let user_profile = crate::agent::user::profile::UserProfile::new(
            &self.workspace_dir,
            self.user_profile_config.clone(),
        );
        if let Some(profile_text) = user_profile.prompt_injection() {
            prompt.push_str(&profile_text);
        }

        let skill_engine =
            crate::agent::skill_evolution::SkillEvolutionEngine::new(&self.skill_evolution_config);
        if let Some(skill_text) = skill_engine.prompt_injection() {
            prompt.push_str(&skill_text);
        }

        let prompt_optimizer =
            crate::agent::prompt::optimizer::PromptOptimizer::new(&self.prompt_optimizer_config);
        if let Some(po_text) = prompt_optimizer.prompt_injection() {
            prompt.push_str(&po_text);
        }

        if let Some(ref replay) = self.experience_replay {
            if let Some(exp_text) = replay.prompt_injection(None) {
                prompt.push_str(&exp_text);
            }
        }

        if self.plan_mode_config.enabled {
            prompt.push_str(
                "\n\n## Plan Mode\n\n\
                 You are operating in plan mode. For complex multi-step tasks, \
                 create a structured plan before executing. Break tasks into \
                 ordered steps, track progress, and update status as you work.\n\
                 - Mark items [x] completed, [!] failed, or [-] skipped.\n\
                 - Auto-activate planning for queries requiring more than \
                 ",
            );
            prompt.push_str(&self.plan_mode_config.auto_activate_threshold.to_string());
            prompt.push_str(" tool calls.\n");
        }

        if let Some(ref mode) = self.current_coding_mode {
            prompt.push_str(&mode.system_prompt_injection());
        }

        if live_cfg.buddy.enabled {
            let companion = crate::buddy::companion::Companion::new(live_cfg.buddy.clone());
            if companion.is_enabled() {
                prompt.push_str("\n\n");
                prompt.push_str(&crate::buddy::prompt::buddy_system_prompt(
                    &live_cfg.buddy.name,
                    &live_cfg.buddy.personality,
                    companion.mood(),
                ));
            }
        }

        if let Some(theme) = crate::util::get_runtime_var("SEN_THEME") {
            let theme = theme.trim();
            if !theme.is_empty()
                && theme != crate::constants::output_styles::STYLE_DEFAULT
            {
                if let Some(style) = crate::constants::output_styles::builtin_output_styles()
                    .into_iter()
                    .find(|s| s.name == theme)
                {
                    if !style.system_prompt_addition.is_empty() {
                        prompt.push_str("\n\n");
                        prompt.push_str(&style.system_prompt_addition);
                    }
                }
            }
        }

        let max_chars = live_cfg.agent.max_system_prompt_chars;
        if max_chars > 0 && prompt.len() > max_chars {
            let mut take = max_chars.saturating_sub(160);
            while take > 0 && !prompt.is_char_boundary(take) {
                take -= 1;
            }
            if take > 0 {
                let truncated = &prompt[..take];
                prompt = format!(
                    "{truncated}\n\n[system prompt truncated to {max_chars} chars to preserve token budget]\n"
                );
            }
        }

        Ok(prompt)
    }

    async fn execute_tool_call(&self, call: &ParsedToolCall) -> ToolExecutionResult {
        let start = Instant::now();

        if self.cancel_signal.load_full().is_cancelled()
            || self.cancelled.load(std::sync::atomic::Ordering::Relaxed)
        {
            return ToolExecutionResult {
                name: call.name.clone(),
                output: "[Cancelled by user]".to_string(),
                success: false,
                tool_call_id: call.tool_call_id.clone(),
            };
        }

        if let (Some(engine), Some(identity)) = (&self.rbac_engine, &self.rbac_identity) {
            let auth = engine.authorize_tool(identity, &call.name);
            if !auth.allowed {
                let reason = auth.reason.unwrap_or_else(|| "access denied".to_string());
                return ToolExecutionResult {
                    name: call.name.clone(),
                    output: format!("RBAC denied: {reason}"),
                    success: false,
                    tool_call_id: call.tool_call_id.clone(),
                };
            }
        } else if self.rbac_engine.is_some() != self.rbac_identity.is_some() {
            tracing::error!(
                tool = call.name,
                "RBAC partially configured (engine={}, identity={}); denying tool execution (fail-closed)",
                self.rbac_engine.is_some(),
                self.rbac_identity.is_some(),
            );
            return ToolExecutionResult {
                name: call.name.clone(),
                output: format!(
                    "RBAC denied: access control is partially configured (engine={}, identity={}); refusing to run tool '{}' without a complete RBAC setup",
                    self.rbac_engine.is_some(),
                    self.rbac_identity.is_some(),
                    call.name
                ),
                success: false,
                tool_call_id: call.tool_call_id.clone(),
            };
        }

        let effective_coding_mode = crate::agent::coding_mode::scoped_coding_mode()
            .or_else(|| {
                let svc = crate::services::try_get_services()?;
                let session = crate::session::current_session_context()?;
                svc.session_coding_mode(&format!("gw_{}", session.session_id))
                    .or_else(|| svc.session_coding_mode(&session.session_id))
            })
            .or(self.current_coding_mode)
            .or_else(|| Some(crate::agent::coding_mode::active_coding_mode()));

        if let Some(mode) = effective_coding_mode {
            if let Some(allowed) = mode.allowed_tools() {
                if !allowed.contains(call.name.as_str()) {
                    let mut listed: Vec<&str> = allowed.iter().copied().collect();
                    listed.sort_unstable();
                    let preview: String = listed
                        .iter()
                        .take(12)
                        .copied()
                        .collect::<Vec<_>>()
                        .join(", ");
                    let extra = if listed.len() > 12 {
                        format!(", ... ({} more)", listed.len() - 12)
                    } else {
                        String::new()
                    };
                    let label = mode.label();
                    let hint = if matches!(mode, crate::agent::coding_mode::CodingMode::Plan) {
                        " To produce or update the plan document call \
                         `update_plan(action=\"set\"|\"add\"|\"save\", ...)`. \
                         When planning is finished call `exit_plan_mode` so \
                         the user can press the golden Build button (or \
                         reply '同意'/'Build'/'execute') to switch to Agent \
                         mode for execution."
                    } else {
                        ""
                    };
                    let denial_message = format!(
                        "Tool '{}' is not permitted in {} mode.{} \
                         Allowed tools: {}{}",
                        call.name, label, hint, preview, extra
                    );
                    crate::agent::mode::effects::record_mode_intercept(
                        crate::agent::mode::effects::ModeInterceptReason::ToolNotAllowed,
                        &crate::agent::mode::effects::ModeInterceptContext {
                            mode,
                            channel: Some("desktop"),
                            provider: Some(self.cached_provider.as_str()),
                            model: None,
                            turn_id: None,
                            tool: Some(call.name.as_str()),
                            tool_call_id: call.tool_call_id.as_deref(),
                            iteration: None,
                            message: Some(&denial_message),
                        },
                    );
                    return ToolExecutionResult {
                        name: call.name.clone(),
                        output: denial_message,
                        success: false,
                        tool_call_id: call.tool_call_id.clone(),
                    };
                }
            }
            if let Some(reason) =
                crate::agent::mode::effects::mode_blocks_tool(mode, call.name.as_str())
            {
                crate::agent::mode::effects::record_mode_intercept(
                    crate::agent::mode::effects::ModeInterceptReason::ReadOnlyPolicy,
                    &crate::agent::mode::effects::ModeInterceptContext {
                        mode,
                        channel: Some("desktop"),
                        provider: Some(self.cached_provider.as_str()),
                        model: None,
                        turn_id: None,
                        tool: Some(call.name.as_str()),
                        tool_call_id: call.tool_call_id.as_deref(),
                        iteration: None,
                        message: Some(&reason),
                    },
                );
                return ToolExecutionResult {
                    name: call.name.clone(),
                    output: reason,
                    success: false,
                    tool_call_id: call.tool_call_id.clone(),
                };
            }
        }

        let coding_label = effective_coding_mode.map(|m| m.label().to_string());
        let coding_label_lc = coding_label.as_deref().map(str::to_ascii_lowercase);
        let perm_mode_lc = crate::gateway::ws::desktop::active_permission_mode();
        let tool_lc = call.name.to_ascii_lowercase();
        let guardrail_ctx = crate::guardrails::GuardrailContext {
            coding_mode: coding_label_lc.as_deref(),
            permission_mode: Some(&perm_mode_lc),
            tool_name: Some(&tool_lc),
        };
        let mode_auto_approved = effective_coding_mode
            .map(crate::agent::mode::effects::mode_auto_approves)
            .unwrap_or(false);
        match crate::guardrails::evaluate_tool_guardrails(&call.name, Some(&guardrail_ctx)) {
            crate::guardrails::GuardrailDecision::Allow => {}
            crate::guardrails::GuardrailDecision::Deny(reason) => {
                return ToolExecutionResult {
                    name: call.name.clone(),
                    output: format!("Blocked by guardrails: {reason}"),
                    success: false,
                    tool_call_id: call.tool_call_id.clone(),
                };
            }
            crate::guardrails::GuardrailDecision::RequireApproval(reason) => {
                let outcome = if mode_auto_approved {
                    GuardrailApprovalOutcome::Approved
                } else {
                    self.request_guardrail_approval(&call.name, &call.arguments, &reason)
                        .await
                };
                match outcome {
                    GuardrailApprovalOutcome::Approved => {}
                    GuardrailApprovalOutcome::Denied => {
                        return ToolExecutionResult {
                            name: call.name.clone(),
                            output: format!(
                                "Blocked by guardrails: approval required but not granted ({reason})"
                            ),
                            success: false,
                            tool_call_id: call.tool_call_id.clone(),
                        };
                    }
                    GuardrailApprovalOutcome::Cancelled => {
                        return ToolExecutionResult {
                            name: call.name.clone(),
                            output: "[Cancelled by user]".to_string(),
                            success: false,
                            tool_call_id: call.tool_call_id.clone(),
                        };
                    }
                }
            }
        }

        let mut hook_call_name = call.name.clone();
        let mut hook_call_args = call.arguments.clone();
        if let Some(ref runner) = self.hook_runner {
            match runner
                .run_before_tool_call(hook_call_name.clone(), hook_call_args.clone())
                .await
            {
                crate::hooks::HookResult::Continue((n, a)) => {
                    hook_call_name = n;
                    hook_call_args = a;
                }
                crate::hooks::HookResult::Cancel(reason) => {
                    let result = crate::tools::ToolResult {
                        success: false,
                        output: format!("Cancelled by hook: {reason}"),
                        error: Some(reason.clone()),
                    };
                    runner
                        .fire_after_tool_call(&call.name, &result, start.elapsed())
                        .await;
                    return ToolExecutionResult {
                        name: call.name.clone(),
                        output: format!("Cancelled by hook: {reason}"),
                        success: false,
                        tool_call_id: call.tool_call_id.clone(),
                    };
                }
            }
        }
        let effective_call = if hook_call_name == call.name && hook_call_args == call.arguments {
            None
        } else {
            Some(ParsedToolCall {
                name: hook_call_name,
                arguments: hook_call_args,
                tool_call_id: call.tool_call_id.clone(),
                parse_error: call.parse_error,
            })
        };
        let dispatch_call: &ParsedToolCall = effective_call.as_ref().unwrap_or(call);

        if effective_call.is_some() {
            let dispatch_tool_lc = dispatch_call.name.to_ascii_lowercase();
            let renamed_ctx = crate::guardrails::GuardrailContext {
                coding_mode: coding_label_lc.as_deref(),
                permission_mode: Some(&perm_mode_lc),
                tool_name: Some(&dispatch_tool_lc),
            };
            match crate::guardrails::evaluate_tool_guardrails(
                &dispatch_call.name,
                Some(&renamed_ctx),
            ) {
                crate::guardrails::GuardrailDecision::Allow => {}
                crate::guardrails::GuardrailDecision::Deny(reason) => {
                    return ToolExecutionResult {
                        name: call.name.clone(),
                        output: format!(
                            "Blocked by guardrails after hook modification of '{}': {reason}",
                            dispatch_call.name
                        ),
                        success: false,
                        tool_call_id: call.tool_call_id.clone(),
                    };
                }
                crate::guardrails::GuardrailDecision::RequireApproval(reason) => {
                    let outcome = if mode_auto_approved {
                        GuardrailApprovalOutcome::Approved
                    } else {
                        self.request_guardrail_approval(
                            &dispatch_call.name,
                            &dispatch_call.arguments,
                            &reason,
                        )
                        .await
                    };
                    match outcome {
                        GuardrailApprovalOutcome::Approved => {}
                        GuardrailApprovalOutcome::Denied => {
                            return ToolExecutionResult {
                                name: call.name.clone(),
                                output: format!(
                                    "Blocked by guardrails after hook modification of '{}': approval required but not granted ({reason})",
                                    dispatch_call.name
                                ),
                                success: false,
                                tool_call_id: call.tool_call_id.clone(),
                            };
                        }
                        GuardrailApprovalOutcome::Cancelled => {
                            return ToolExecutionResult {
                                name: call.name.clone(),
                                output: "[Cancelled by user]".to_string(),
                                success: false,
                                tool_call_id: call.tool_call_id.clone(),
                            };
                        }
                    }
                }
            }
        }

        {
            let web_search_enabled = crate::services::try_get_services()
                .map(|svc| svc.config().web_search.enabled)
                .unwrap_or(true);
            match crate::agent::web_search_url_guard::evaluate_browser_or_web_fetch_call(
                dispatch_call.name.as_str(),
                &dispatch_call.arguments,
                web_search_enabled,
            ) {
                crate::agent::web_search_url_guard::GuardDecision::Allow => {}
                crate::agent::web_search_url_guard::GuardDecision::AllowWithFallbackTrace => {
                    tracing::info!(
                        tool = %dispatch_call.name,
                        "Permitting search-engine URL as fallback; web_search recently failed"
                    );
                }
                crate::agent::web_search_url_guard::GuardDecision::Refuse(refusal) => {
                    tracing::warn!(
                        tool = %dispatch_call.name,
                        "Blocked search-engine URL misuse; web_search has not been tried yet"
                    );
                    self.observer.record_event(&ObserverEvent::ToolCall {
                        tool: call.name.clone(),
                        duration: start.elapsed(),
                        success: false,
                    });
                    if let Some(ref runner) = self.hook_runner {
                        let hook_result = crate::tools::ToolResult {
                            success: false,
                            output: refusal.clone(),
                            error: Some(refusal.clone()),
                        };
                        runner
                            .fire_after_tool_call(&call.name, &hook_result, start.elapsed())
                            .await;
                    }
                    return ToolExecutionResult {
                        name: call.name.clone(),
                        output: refusal,
                        success: false,
                        tool_call_id: call.tool_call_id.clone(),
                    };
                }
            }
        }

        async fn run_tool(
            tool: &dyn Tool,
            call: &ParsedToolCall,
            observer: &Arc<dyn Observer>,
        ) -> (String, bool) {
            let start = Instant::now();
            match crate::agent::loop_::execute_tool_panic_safe(
                tool,
                &call.name,
                call.arguments.clone(),
            )
            .await
            {
                Ok(r) => {
                    observer.record_event(&ObserverEvent::ToolCall {
                        tool: call.name.clone(),
                        duration: start.elapsed(),
                        success: r.success,
                    });
                    if r.success {
                        let scrubbed = crate::agent::profile::pii_sanitize::scrub_credentials(&r.output);
                        let fallback = scrubbed.clone();
                        let call_name_owned = call.name.clone();
                        let out = tokio::task::spawn_blocking(move || {
                            crate::agent::token::optimizer::compress_output(
                                &call_name_owned,
                                &scrubbed,
                            )
                        })
                        .await
                        .unwrap_or_else(|join_err| {
                            tracing::warn!(
                                tool = %call.name,
                                error = %join_err,
                                "tool output compression task failed; falling back to uncompressed output"
                            );
                            fallback
                        });
                        (out, true)
                    } else {
                        let reason = r.error.unwrap_or(r.output);
                        (
                            format!("Error: {}", crate::agent::profile::pii_sanitize::scrub_credentials(&reason)),
                            false,
                        )
                    }
                }
                Err(e) => {
                    observer.record_event(&ObserverEvent::ToolCall {
                        tool: call.name.clone(),
                        duration: start.elapsed(),
                        success: false,
                    });
                    (format!("Error executing {}: {e}", call.name), false)
                }
            }
        }

        tracing::debug!(
            target: "agent.tool",
            tool = %dispatch_call.name,
            id = ?dispatch_call.tool_call_id,
            "tool execution start"
        );
        let cancel_handle = self.cancel_signal.load_full().as_ref().clone();
        let (output, success) = if let Some(tool) = self
            .tool_index
            .get(dispatch_call.name.as_str())
            .map(|&i| &self.tools[i])
        {
            tokio::select! {
                biased;
                _ = cancel_handle.cancelled() => {
                    ("[Cancelled by user]".to_string(), false)
                }
                res = run_tool(tool.as_ref(), dispatch_call, &self.observer) => res,
            }
        } else if let Some(activated_arc) = self.activated_tools.as_ref() {
            let activated_opt = activated_arc.lock().get_resolved(&dispatch_call.name);
            if let Some(tool) = activated_opt {
                tokio::select! {
                    biased;
                    _ = cancel_handle.cancelled() => {
                        ("[Cancelled by user]".to_string(), false)
                    }
                    res = run_tool(tool.as_ref(), dispatch_call, &self.observer) => res,
                }
            } else {
                (format!("Unknown tool: {}", dispatch_call.name), false)
            }
        } else {
            (format!("Unknown tool: {}", dispatch_call.name), false)
        };

        let wall_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
        tracing::debug!(
            target: "agent.tool",
            tool = %dispatch_call.name,
            id = ?dispatch_call.tool_call_id,
            success,
            wall_ms,
            "tool execution end"
        );
        crate::agent::profile::runtime_hooks::publish_tool_event(&dispatch_call.name, success, wall_ms);

        if crate::agent::web_search_url_guard::is_web_search_tool_name(&dispatch_call.name) {
            if success {
                crate::agent::web_search_url_guard::record_web_search_success();
            } else {
                crate::agent::web_search_url_guard::record_web_search_failure();
            }
        }

        if let Some(ref runner) = self.hook_runner {
            let hook_result = crate::tools::ToolResult {
                success,
                output: output.clone(),
                error: if success { None } else { Some(output.clone()) },
            };
            runner
                .fire_after_tool_call(&call.name, &hook_result, start.elapsed())
                .await;
        }

        ToolExecutionResult {
            name: call.name.clone(),
            output,
            success,
            tool_call_id: call.tool_call_id.clone(),
        }
    }

    fn build_user_envelope(user_message: &str, context: &str) -> String {
        let now = chrono::Local::now();
        let (year, month, day) = (now.year(), now.month(), now.day());
        let (hour, minute, second) = (now.hour(), now.minute(), now.second());
        let tz = now.format("%Z");
        let date_str =
            format!("{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02} {tz}");

        let current_request = format!(
            "[CURRENT REQUEST - this is the user's latest message and the primary task to act \
             on right now. Judge from the conversation context whether it asks to continue an \
             earlier or interrupted request, or is a new request; act accordingly, and do not \
             blindly resume an earlier request when the user has moved on.]\n{user_message}"
        );

        if context.is_empty() {
            format!("[CURRENT DATE & TIME: {date_str}]\n\n{current_request}")
        } else {
            format!("[CURRENT DATE & TIME: {date_str}]\n\n{context}\n\n{current_request}")
        }
    }

    fn classify_model(&self, user_message: &str) -> String {
        if let Some(decision) =
            super::classifier::classify_with_decision(&self.classification_config, user_message)
        {
            if self.available_hints.contains(&decision.hint) {
                let resolved_model = self
                    .route_model_by_hint
                    .get(&decision.hint)
                    .map(String::as_str)
                    .unwrap_or("unknown");
                tracing::info!(
                    target: "query_classification",
                    hint = decision.hint.as_str(),
                    model = resolved_model,
                    rule_priority = decision.priority,
                    message_length = user_message.len(),
                    "Classified message route"
                );
                return format!("route:{}", decision.hint);
            }
        }

        if let Some(ref ac) = self.config.auto_classify {
            let tier = super::eval::estimate_complexity(user_message);
            if let Some(hint) = ac.hint_for(tier) {
                if self.available_hints.contains(&hint.to_string()) {
                    tracing::info!(
                        target: "query_classification",
                        hint = hint,
                        complexity = ?tier,
                        message_length = user_message.len(),
                        "Auto-classified by complexity"
                    );
                    return format!("route:{hint}");
                }
            }
        }

        self.model_name.clone()
    }

    pub async fn turn(&mut self, user_message: &str) -> Result<String, AgentError> {
        use futures_util::FutureExt as _;

        let (tx, mut rx) = tokio::sync::mpsc::channel::<TurnEvent>(TURN_EVENT_DRAIN_BUFFER);
        let drain = crate::runtime::spawn_supervised("agent.agent.drain", async move {
            while rx.recv().await.is_some() {

            }
        })
        .into_inner();
        let pre_turn_history_len = self.history.len();
        let caught =
            std::panic::AssertUnwindSafe(self.turn_streamed(user_message, tx))
                .catch_unwind()
                .await;

        let _ = drain.await;
        match caught {
            Ok(result) => result,
            Err(panic) => {
                let msg = crate::util::describe_panic(panic.as_ref());
                tracing::error!(
                    target: "agent",
                    panic = %msg,
                    "agent.turn panicked; isolated to this turn"
                );
                self.rollback_failed_turn_history(pre_turn_history_len);
                Err(AgentError::ToolDispatchFailed(format!(
                    "turn panicked: {msg}"
                )))
            }
        }
    }

    async fn apply_turn_preamble(
        &mut self,
        user_message: &str,
        event_tx: &tokio::sync::mpsc::Sender<TurnEvent>,
    ) -> Result<()> {

        self.apply_mode_filter();

        let config_change = self.sync_config_from_store();

        if config_change == ConfigChange::Hard {
            if let Err(e) = self.reload_provider().await {
                let msg = e.to_string();
                if !crate::agent::error_classify::is_no_model_error(&msg) {
                    let _ = event_tx
                        .send(TurnEvent::Error {
                            message: format!("Failed to reload provider: {msg}"),
                        })
                        .await;
                }
                return Err(e);
            }
        }

        if self.history.is_empty() {
            let system_prompt = self.build_system_prompt()?;
            self.history
                .push(ConversationMessage::Chat(ChatMessage::system(
                    system_prompt,
                )));
        } else {
            self.refresh_history_system_prompt();
        }

        let plan_armed = self.plan_execution_armed.lock().is_some();
        let is_design_trigger = user_message
            .trim_start()
            .starts_with(crate::agent::designer::pipeline::DESIGN_TASK_PREFIX);

        let context = if plan_armed || is_design_trigger {
            String::new()
        } else {
            self.memory_loader
                .load_context(
                    self.memory.as_ref(),
                    user_message,
                    self.memory_session_id.as_deref(),
                )
                .await
                .unwrap_or_default()
        };

        if self.auto_save && !plan_armed && !is_design_trigger {
            let autosave_key = format!("user_msg_{}", Uuid::new_v4());
            let memory = Arc::clone(&self.memory);
            let user_message_owned = user_message.to_string();
            let memory_session_id = self.memory_session_id.clone();
            let _ = crate::runtime::spawn_supervised("agent.memory.autosave", async move {
                let _ = memory
                    .store(
                        &autosave_key,
                        &user_message_owned,
                        MemoryCategory::Conversation,
                        memory_session_id.as_deref(),
                    )
                    .await;
            });
        }

        crate::agent::dangling_tool_repair::drop_payloadless_assistant_messages(&mut self.history);
        crate::agent::dangling_tool_repair::close_orphan_user_turns(&mut self.history);

        let was_interrupted = std::mem::take(&mut self.last_turn_interrupted);
        if was_interrupted {
            crate::agent::dangling_tool_repair::note_interrupted_turn(&mut self.history);
        }

        let mut enriched = Self::build_user_envelope(user_message, &context);

        if let Some(svc) = crate::services::try_get_services() {
            let mode = crate::agent::coding_mode::active_coding_mode();
            if let Some(reminder) = crate::agent::mode::effects::pre_turn_reminder(mode) {
                enriched.push_str("\n\n");
                enriched.push_str(reminder);
            }
            if !plan_armed {
                let cfg = svc.config();
                if let Some(web_reminder) =
                    crate::agent::mode::effects::web_research_disabled_reminder(
                        mode,
                        cfg.web_search.enabled,
                        cfg.web_fetch.enabled,
                    )
                {
                    enriched.push_str("\n\n");
                    enriched.push_str(web_reminder);
                }
                if let Some(web_active) = crate::agent::mode::effects::web_research_active_reminder(
                    mode,
                    cfg.web_search.enabled,
                    cfg.web_fetch.enabled,
                ) {
                    enriched.push_str("\n\n");
                    enriched.push_str(web_active);
                }
            }
        }

        if self.intent_analysis_config.enabled && !plan_armed && !is_design_trigger {
            let analysis = crate::agent::intent::analyze_intent(user_message);
            if analysis.is_confident(self.intent_analysis_config.min_confidence) {
                tracing::debug!(
                    target: "agent.intent",
                    intent = analysis.intent.as_str(),
                    confidence = analysis.confidence,
                    "intent analysis result for turn"
                );
                if self.intent_analysis_config.enrich_preamble {
                    if let Some(note) = analysis.intent_note() {
                        enriched.push_str("\n\n");
                        enriched.push_str(note);
                    }
                }
                if self.intent_analysis_config.enforce_plan_threshold
                    && self.plan_mode_config.enabled
                    && matches!(
                        analysis.complexity,
                        crate::agent::eval::ComplexityTier::Complex
                    )
                {
                    enriched.push_str(&format!(
                        "\n\nThis task likely requires more than {} steps. Create a structured \
                         plan with the todo tool before executing, and update progress as you go.",
                        self.plan_mode_config.auto_activate_threshold
                    ));
                }
            }
        }

        self.history
            .push(ConversationMessage::Chat(ChatMessage::user(enriched)));

        self.history = crate::agent::dangling_tool_repair::repair_dangling_tool_calls(
            std::mem::take(&mut self.history),
        );
        Ok(())
    }

    async fn apply_gui_model_switch(&mut self, event_tx: &tokio::sync::mpsc::Sender<TurnEvent>) {
        let switch_state = crate::agent::loop_::get_model_switch_state();
        let switch_opt = switch_state.lock().clone();

        tracing::debug!(
            "Model switch state check: {:?}, current model={}, provider={}",
            switch_opt,
            self.model_name,
            self.cached_provider
        );

        let Some((provider, model)) = switch_opt else {
            return;
        };
        tracing::info!(
            "Model switch detected in turn_streamed: provider={}, model={}",
            provider,
            model
        );
        let old_model = self.model_name.clone();
        self.model_name = model.clone();
        if old_model != self.model_name && !old_model.is_empty() && !self.model_name.is_empty() {
            let old_marker = format!("| Model: {old_model}");
            let new_marker = format!("| Model: {}", self.model_name);
            for entry in self.history.iter_mut() {
                if let ConversationMessage::Chat(msg) = entry {
                    if msg.role == "system" && msg.content.contains(&old_marker) {
                        msg.content = msg.content.replacen(&old_marker, &new_marker, 1);
                        break;
                    }
                }
            }
        }

        let (new_api_key, new_api_url) = {
            let config = self.shared_config.load();
            (
                config.api_key.clone().unwrap_or_default(),
                config.api_url.clone().unwrap_or_default(),
            )
        };

        let need_reload = provider != self.cached_provider
            || !self.cached_api_key.constant_time_eq(&new_api_key)
            || new_api_url != self.cached_api_url;

        self.cached_provider = provider.clone();
        self.cached_api_key = crate::security::secret_string::SecretString::new(new_api_key);
        self.cached_api_url = new_api_url;

        if need_reload {
            tracing::info!("Model switch requires provider reload, reloading...");
            if let Err(e) = self.reload_provider().await {
                tracing::error!("Failed to reload provider during model switch: {}", e);
                let _ = event_tx
                    .send(TurnEvent::Error {
                        message: format!("Provider reload failed: {}", e),
                    })
                    .await;
            }
        }

        crate::agent::loop_::clear_model_switch_request();
    }

    async fn request_guardrail_approval(
        &self,
        tool_name: &str,
        arguments: &serde_json::Value,
        reason: &str,
    ) -> GuardrailApprovalOutcome {
        use crate::session::SessionEventKind;

        let bus = crate::gateway::ws::gateway_approval_bus();
        if bus.receiver_count() == 0 {
            tracing::warn!(
                tool = tool_name,
                reason,
                "guardrail requires approval but no approval surface is connected; denying"
            );
            return GuardrailApprovalOutcome::Denied;
        }

        let request_id = format!("guardrail_{}", uuid::Uuid::new_v4().simple());
        let mut rx = bus.subscribe();
        let sink = crate::gateway::ws::gateway_approval_sink_handle();
        sink.emit_kind(SessionEventKind::ApprovalRequested {
            id: request_id.clone(),
            tool_name: tool_name.to_string(),
            arguments: serde_json::json!({
                "kind": "guardrail_approval",
                "reason": reason,
                "input": arguments,
            }),
            issued_at: chrono::Utc::now(),
        });
        crate::approval::register_pending_gateway_approval(request_id.clone());

        const GUARDRAIL_APPROVAL_TIMEOUT_MS: u64 = 300_000;
        let cancel_token = self.cancel_signal();
        let deadline = std::time::Instant::now()
            + std::time::Duration::from_millis(GUARDRAIL_APPROVAL_TIMEOUT_MS);
        loop {
            let remaining = deadline.saturating_duration_since(std::time::Instant::now());
            if remaining.is_zero() {
                tracing::warn!(
                    tool = tool_name,
                    request_id = %request_id,
                    "guardrail approval timed out; denying"
                );
                return GuardrailApprovalOutcome::Denied;
            }
            let received = tokio::select! {
                () = cancel_token.cancelled() => {
                    tracing::warn!(
                        tool = tool_name,
                        request_id = %request_id,
                        "guardrail approval cancelled by user; cancelling turn"
                    );
                    return GuardrailApprovalOutcome::Cancelled;
                }
                outcome = tokio::time::timeout(remaining, rx.recv()) => outcome,
            };
            match received {
                Ok(Ok(evt)) => {
                    if let SessionEventKind::ApprovalResponded { id, decision, .. } = &evt.kind {
                        if id == &request_id {
                            return if matches!(
                                decision.to_ascii_lowercase().as_str(),
                                "yes" | "y" | "always" | "a"
                            ) {
                                GuardrailApprovalOutcome::Approved
                            } else {
                                GuardrailApprovalOutcome::Denied
                            };
                        }
                    }
                }
                Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
                Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) | Err(_) => {
                    return GuardrailApprovalOutcome::Denied;
                }
            }
        }
    }

    fn record_failed_turn_reinforcement(&self, error_message: &str) {
        let Some(engine) = crate::agent::reward::reinforcement::global_reinforcement_engine()
        else {
            return;
        };
        let record = crate::agent::reward::reinforcement::TurnRecord {
            turn_index: engine.total_turns(),
            timestamp: chrono::Utc::now(),
            reward: -1.0,
            model_used: self.model_name.clone(),
            temperature_used: self.temperature,
            query_category: "turn_error".to_string(),
            tools_used: Vec::new(),
            response_length: error_message.len(),
        };
        let _ = engine.record_turn(record);
    }

    async fn apply_bootstrap_model_override(
        &mut self,
        event_tx: &tokio::sync::mpsc::Sender<TurnEvent>,
    ) {
        let Some(bs) = crate::bootstrap::try_get_state() else {
            return;
        };
        let Some(requested) = bs.read(|s| s.main_loop_model_override.clone()) else {
            return;
        };
        let resolved = {
            let config = self.shared_config.load();
            crate::agent::loop_::resolve_model_override_target(&requested, &config)
        };
        let Some((provider_override, target_model)) = resolved else {
            let _ = event_tx
                .send(TurnEvent::StatusUpdate {
                    action: "model_override".to_string(),
                    detail: "未找到可用的 fast 模型配置：请在 model_routes 中配置 hint=\"fast\" 的路由，或设置 agent_runtime.fast_apply_model。".to_string(),
                })
                .await;
            bs.write(|s| s.main_loop_model_override = None);
            return;
        };
        let target_provider = provider_override.unwrap_or_else(|| self.cached_provider.clone());
        if target_model == self.model_name && target_provider == self.cached_provider {
            return;
        }
        {
            let switch_state = crate::agent::loop_::get_model_switch_state();
            *switch_state.lock() = Some((target_provider, target_model));
        }
        self.apply_gui_model_switch(event_tx).await;
    }

    pub async fn run_single(&mut self, message: &str) -> Result<String> {
        self.turn(message).await.map_err(|e| e.into())
    }

    pub async fn run_interactive(&mut self) -> Result<()> {
        println!("🦀 SenWeaverCoding Interactive Mode");
        println!("Type /quit to exit.\n");

        let (tx, mut rx) = tokio::sync::mpsc::channel(32);
        let cli = crate::channels::CliChannel::new();

        let listen_handle =
            crate::runtime::spawn_supervised("agent.repl.cli_listener", async move {
                let _ = crate::channels::Channel::listen(&cli, tx).await;
            })
            .into_inner();

        while let Some(msg) = rx.recv().await {
            let response = match self.turn(&msg.content).await {
                Ok(resp) => resp,
                Err(e) => {
                    eprintln!("\nError: {e}\n");
                    continue;
                }
            };
            println!("\n{response}\n");
        }

        listen_handle.abort();
        Ok(())
    }
}

pub(crate) struct GuiHooksFromAgent {
    response_cache: Option<Arc<crate::memory::response_cache::ResponseCache>>,
    memory: Arc<dyn Memory>,
    memory_session_id: Option<String>,
    auto_save: bool,
    classification_config: crate::config::QueryClassificationConfig,
    available_hints: Vec<String>,
    route_model_by_hint: HashMap<String, String>,
    auto_classify: Option<crate::agent::eval::AutoClassifyConfig>,
    default_model: String,
    temperature: f64,
    experience_replay: Option<crate::agent::reward::experience::ExperienceReplay>,
    observer: Arc<dyn Observer>,
    cached_provider: String,
}

impl GuiHooksFromAgent {
    pub fn from_agent(agent: &Agent) -> Self {
        Self {
            response_cache: agent.response_cache.clone(),
            memory: agent.memory.clone(),
            memory_session_id: agent.memory_session_id.clone(),
            auto_save: agent.auto_save,
            classification_config: agent.classification_config.clone(),
            available_hints: agent.available_hints.clone(),
            route_model_by_hint: agent.route_model_by_hint.clone(),
            auto_classify: agent.config.auto_classify.clone(),
            default_model: agent.model_name.clone(),
            temperature: agent.temperature,
            experience_replay: agent.experience_replay.clone(),
            observer: agent.observer.clone(),
            cached_provider: agent.cached_provider.clone(),
        }
    }
}

#[async_trait::async_trait]
impl crate::agent::loop_::traits::ResponseCacheHook for GuiHooksFromAgent {
    fn build_key(&self, messages: &[ChatMessage], model: &str) -> Option<String> {
        if self.temperature != 0.0 {
            return None;
        }
        self.response_cache.as_ref().map(|_| {
            let last_user = messages
                .iter()
                .rfind(|m| m.role == "user")
                .map(|m| m.content.as_str())
                .unwrap_or("");
            let system = messages
                .iter()
                .find(|m| m.role == "system")
                .map(|m| m.content.as_str());
            crate::memory::response_cache::ResponseCache::cache_key(model, system, last_user)
        })
    }

    async fn try_hit(&self, key: &str, _user_message: &str) -> Option<String> {
        let cache = self.response_cache.as_ref()?;
        let provider_label = self.cached_provider.clone();
        let model_label = self.default_model.clone();
        let cache = Arc::clone(cache);
        let key_owned = key.to_string();
        let lookup = tokio::task::spawn_blocking(move || cache.get(&key_owned)).await;
        match lookup {
            Ok(Ok(Some(cached))) => {
                self.observer.record_event(&ObserverEvent::CacheHit {
                    cache_type: "response".into(),
                    tokens_saved: 0,
                });
                self.observer.record_metric(
                    &crate::observability::traits::ObserverMetric::ResponseCacheOutcome {
                        provider: provider_label,
                        model: model_label,
                        hit: true,
                    },
                );
                Some(cached)
            }
            _ => {
                self.observer.record_event(&ObserverEvent::CacheMiss {
                    cache_type: "response".into(),
                });
                self.observer.record_metric(
                    &crate::observability::traits::ObserverMetric::ResponseCacheOutcome {
                        provider: provider_label,
                        model: model_label,
                        hit: false,
                    },
                );
                None
            }
        }
    }

    async fn write_back(&self, key: &str, model: &str, response: &str, output_tokens: u32) {
        if let Some(cache) = &self.response_cache {
            let cache = Arc::clone(cache);
            let key = key.to_string();
            let model = model.to_string();
            let response = response.to_string();
            let _ = tokio::task::spawn_blocking(move || {
                cache.put(&key, &model, &response, output_tokens)
            })
            .await;
        }
    }
}

#[async_trait::async_trait]
impl crate::agent::loop_::traits::MemorySessionHook for GuiHooksFromAgent {
    async fn on_turn_start(&self, user_message: &str) {
        if !self.auto_save {
            return;
        }
        let key = format!("user_msg_{}", Uuid::new_v4());
        let _ = self
            .memory
            .store(
                &key,
                user_message,
                MemoryCategory::Conversation,
                self.memory_session_id.as_deref(),
            )
            .await;
    }

    async fn on_turn_end(&self, assistant_message: &str, _tools_used: &[String]) {
        if !self.auto_save {
            return;
        }
        let key = format!("assistant_msg_{}", Uuid::new_v4());
        let _ = self
            .memory
            .store(
                &key,
                assistant_message,
                MemoryCategory::Conversation,
                self.memory_session_id.as_deref(),
            )
            .await;
    }
}

impl crate::agent::loop_::traits::ModelClassifierHook for GuiHooksFromAgent {
    fn classify(&self, user_message: &str) -> Option<String> {
        if let Some(decision) = crate::agent::classifier::classify_with_decision(
            &self.classification_config,
            user_message,
        ) {
            if self.available_hints.contains(&decision.hint) {
                let resolved_model = self
                    .route_model_by_hint
                    .get(&decision.hint)
                    .map(String::as_str)
                    .unwrap_or("unknown");
                tracing::info!(
                    target: "query_classification",
                    hint = decision.hint.as_str(),
                    model = resolved_model,
                    rule_priority = decision.priority,
                    message_length = user_message.len(),
                    "Classified message route via hook"
                );
                return Some(format!("route:{}", decision.hint));
            }
        }
        if let Some(ref ac) = self.auto_classify {
            let tier = crate::agent::eval::estimate_complexity(user_message);
            if let Some(hint) = ac.hint_for(tier) {
                if self.available_hints.contains(&hint.to_string()) {
                    tracing::info!(
                        target: "query_classification",
                        hint = hint,
                        complexity = ?tier,
                        message_length = user_message.len(),
                        "Auto-classified by complexity via hook"
                    );
                    return Some(format!("route:{hint}"));
                }
            }
        }
        None
    }
}

#[async_trait::async_trait]
impl crate::agent::loop_::traits::TurnPreambleHook for GuiHooksFromAgent {
    async fn apply(
        &self,
        _user_message: &str,
        _event_tx: &tokio::sync::mpsc::Sender<TurnEvent>,
    ) -> Result<()> {
        Ok(())
    }
}

#[async_trait::async_trait]
impl crate::agent::loop_::traits::GuiModelSwitchHook for GuiHooksFromAgent {
    async fn poll(&self, _event_tx: &tokio::sync::mpsc::Sender<TurnEvent>) -> Option<String> {
        None
    }
}

#[async_trait::async_trait]
impl crate::agent::loop_::traits::IterationContextBudgetHook for GuiHooksFromAgent {
    async fn prepare(
        &self,
        _iteration: usize,
        _event_tx: &tokio::sync::mpsc::Sender<TurnEvent>,
    ) {
    }
}

#[async_trait::async_trait]
impl crate::agent::loop_::traits::ExperienceRecorderHook for GuiHooksFromAgent {
    async fn record(&self, summary: &crate::agent::loop_::traits::TurnExperienceSummary) {
        let Some(ref replay) = self.experience_replay else {
            return;
        };
        if !replay.collection_enabled() {
            return;
        }
        let refs: Vec<(&str, bool)> = summary
            .tool_results
            .iter()
            .map(|(n, s)| (n.as_str(), *s))
            .collect();
        let dims = crate::agent::self_assess::eval::heuristic_eval(
            &summary.user_query,
            &summary.assistant_response,
            &refs,
        );
        let reward = (dims.aggregate() * 2.0) - 1.0;
        let query_category =
            crate::agent::classifier::classify(&self.classification_config, &summary.user_query)
                .unwrap_or_else(|| "general".to_string());
        let experience = crate::agent::reward::experience::Experience {
            id: uuid::Uuid::new_v4().to_string(),
            session_id: self.memory_session_id.clone().unwrap_or_default(),
            timestamp: chrono::Utc::now(),
            user_query: summary.user_query.clone(),
            assistant_response: summary.assistant_response.clone(),
            tools_used: summary.tools_used.clone(),
            model: self.default_model.clone(),
            reward,
            query_category,
            replay_count: 0,
        };
        replay.store(experience);
    }
}

#[async_trait::async_trait]
impl crate::agent::loop_::traits::PlanModeNudgeHook for GuiHooksFromAgent {
    async fn try_inject(
        &self,
        _iteration: usize,
        _history: &mut Vec<ChatMessage>,
        _event_tx: &tokio::sync::mpsc::Sender<TurnEvent>,
    ) -> bool {
        false
    }
}

pub async fn run(
    config: Config,
    message: Option<String>,
    provider_override: Option<String>,
    model_override: Option<String>,
    temperature: f64,
) -> Result<()> {
    let start = Instant::now();

    let mut effective_config = config;
    if let Some(p) = provider_override {
        effective_config.default_provider = Some(p);
    }
    if let Some(m) = model_override {
        effective_config.default_model = Some(m);
    }
    effective_config.default_temperature = temperature;

    let mut agent = Agent::from_config(&effective_config, None, None).await?;

    let provider_name = effective_config
        .default_provider
        .as_deref()
        .unwrap_or("openrouter")
        .to_string();
    let model_name = providers::resolve_default_model(&effective_config)?;

    agent.observer.record_event(&ObserverEvent::AgentStart {
        provider: provider_name.clone(),
        model: model_name.clone(),
    });

    if let Some(msg) = message {
        let response = agent.run_single(&msg).await?;
        println!("{response}");
    } else {
        agent.run_interactive().await?;
    }

    agent.observer.record_event(&ObserverEvent::AgentEnd {
        provider: provider_name,
        model: model_name,
        duration: start.elapsed(),
        tokens_used: None,
        cost_usd: None,
    });

    Ok(())
}
