//! `AgentRebuildSpec` — the canonical recipe for constructing an
//! [`kigi_agent::Agent`] for a given session.
//!
//! INVARIANT: This is the **only** place in the shell crate that calls
//! [`kigi_agent::AgentBuilder::new`]. Both initial session spawn
//! ([`crate::session::acp_session::spawn_session_actor`]) and zero-turn
//! harness rebuild
//! ([`crate::session::acp_session::SessionActor::handle_rebuild_agent_for_definition`])
//! go through [`AgentRebuildSpec::build_agent`].
//!
//! ## Why this exists
//!
//! [`kigi_agent::Agent`] owns an [`kigi_tools::bridge::ToolBridge`]
//! that carries session-scoped channels (notification handle, terminal/fs
//! backends, subagent senders, scheduler set, plugin registry, attribution
//! callback). The Agent is therefore session-bound — it cannot be shared
//! across sessions and cannot be re-rendered from outside its session
//! context. To rebuild it (e.g. when the user picks a model with a
//! different `agent_type` before sending any user message), we need to
//! retain every input that the original `AgentBuilder` chain consumed.
//! `AgentRebuildSpec` is exactly that retained bag of inputs.
//!
//! ## WHEN ADDING A NEW [`kigi_agent::AgentBuilder`]`::with_*` KNOB
//!
//! 1. Add the corresponding field to [`AgentRebuildSpec`].
//! 2. Pass it through in [`AgentRebuildSpec::build_agent`]. The destructure
//!    pattern at the top of `build_agent` forces every field to be used —
//!    drift is a compile error (`#[deny(unused_variables)]`).
//! 3. Populate the field at the call site in `spawn_session_actor`.
//!
//! ## Why some fields are channel senders
//!
//! Several `ToolBridge` resources (e.g. `UserQuestionSender`,
//! `SubagentBackendResource`) are backed by the `tx` half of channels
//! whose `rx` halves are owned by long-lived coordinator tasks spawned
//! in `spawn_session_actor`. The subagent channels are wrapped in a
//! `ChannelBackend` behind `SubagentBackendResource`. On rebuild, we
//! must reuse the **same** senders so the existing coordinator keeps
//! receiving requests; we cannot mint a fresh channel without orphaning
//! the running coordinator.
use kigi_agent::config::AgentDefinition;
use kigi_agent::error::AgentBuildError;
use kigi_agent::prompt::context::PromptAudience;
use kigi_agent::prompt::skills::SkillsConfig;
use kigi_agent::{Agent, AgentBuilder, CompactionPolicy, ReminderPolicy};
use kigi_tools::computer::types::{AsyncFileSystem, TerminalBackend};
use kigi_tools::implementations::kigi::ask_user_question::types::UserQuestionRequest;
use kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig;
use kigi_tools::implementations::kigi::task::types::{
    MonitorEventBuffer, SubagentEvent, TaskModelValidator,
};
use kigi_tools::implementations::kigi::web_fetch::WebFetchConfig;
use kigi_tools::implementations::lsp::LspBackend;
use kigi_tools::implementations::web_search::WebSearchConfig;
use kigi_tools::notification::ToolNotificationHandle;
use kigi_tools::types::SharedApiKeyProvider;
use kigi_tools::types::compat::CompatConfig;
use kigi_tools::types::memory_backend::MemoryBackend;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender;
/// Shell-resolved per-tool `ToolConfig.params` JSON maps, bundled into one
/// named struct so the spawn telescopes carry a single argument instead of
/// adjacent identically-typed positionals that a caller could transpose.
#[derive(Debug, Clone, Default)]
pub(crate) struct ResolvedToolParamsJson {
    /// `[toolset.bash]` overrides for the bash tool(s).
    pub bash: Option<serde_json::Map<String, serde_json::Value>>,
    /// `[toolset.ask_user_question]` timeout policy for the ask tool.
    pub ask_user_question: Option<serde_json::Map<String, serde_json::Value>>,
}
/// Cached recipe for building a session-scoped [`Agent`].
///
/// See module docs for the invariant: this is the only construction
/// site for `Agent` in the shell crate. Cloning is intentionally not
/// derived — the spec lives behind an [`Arc`] and is shared by clone of
/// that `Arc`.
pub(crate) struct AgentRebuildSpec {
    pub working_directory: PathBuf,
    pub terminal_backend: Arc<dyn TerminalBackend>,
    pub fs_backend: Arc<dyn AsyncFileSystem>,
    pub tools_notification_handle: ToolNotificationHandle,
    pub bridge_state_path: PathBuf,
    pub session_env: Arc<HashMap<String, String>>,
    pub models_manager: crate::agent::models::ModelsManager,
    pub compaction_policy: CompactionPolicy,
    pub reminder_policy: ReminderPolicy,
    pub memory_enabled: bool,
    pub memory_global_path: Option<String>,
    pub memory_workspace_path: Option<String>,
    pub memory_backend: Option<Arc<dyn MemoryBackend>>,
    pub web_search_config: WebSearchConfig,
    pub backend_search: bool,
    pub web_fetch_config: WebFetchConfig,
    pub app_builder_deployer_config: AppBuilderDeployerConfig,
    pub write_file_enabled: bool,
    pub subagents_enabled: bool,
    pub subagent_toggle: HashMap<String, bool>,
    pub ask_user_question_enabled: bool,
    pub persona_summaries: Vec<String>,
    pub prompt_audience: PromptAudience,
    pub role_instructions: Option<String>,
    pub persona_instructions: Option<String>,
    pub skills_config: SkillsConfig,
    /// Resolved vendor-compat config (from `Config::compat_resolved`), threaded
    /// into skills / rules / AGENTS.md discovery via the builder.
    pub compat: CompatConfig,
    pub context_window_tokens: u64,
    pub prompt_working_directory: Option<String>,
    pub lsp: Option<Arc<dyn LspBackend>>,
    pub plugin_registry: Option<Arc<kigi_agent::plugins::PluginRegistry>>,
    pub api_key_provider: Option<SharedApiKeyProvider>,
    pub attribution_callback: Option<kigi_tools::SharedAttributionCallback>,
    pub tool_params_json: ResolvedToolParamsJson,
    pub subagent_event_tx: Option<UnboundedSender<SubagentEvent>>,
    pub monitor_event_buffer: Option<MonitorEventBuffer>,
    pub user_question_tx: UnboundedSender<UserQuestionRequest>,
    pub subagent_depth: u32,
    pub session_id_str: String,
    pub respect_gitignore: bool,
    pub path_not_found_hints: bool,
    pub mcp_state: Arc<tokio::sync::Mutex<crate::session::mcp_servers::McpState>>,
    pub is_non_interactive: bool,
    pub system_prompt_label: String,
    pub owner_session_id: Option<String>,
    pub parent_scheduler_handle:
        Option<kigi_tools::implementations::kigi::scheduler::types::SchedulerHandle>,
}
impl AgentRebuildSpec {
    /// Build a fresh [`Agent`] from this spec and an [`AgentDefinition`].
    ///
    /// This is the canonical construction path; see module docs for the
    /// invariant. The destructure pattern below is intentional —
    /// `#[deny(unused_variables)]` ensures any newly added spec field is
    /// used here, otherwise compilation fails.
    #[deny(unused_variables)]
    pub async fn build_agent(
        self: &Arc<Self>,
        definition: AgentDefinition,
    ) -> Result<Agent, AgentBuildError> {
        self.build_agent_inner(definition, None, None).await
    }
    /// Build an agent with optional one-shot overrides for initial spawn.
    ///
    /// `persisted_skill_names`: restored into the `SkillManager` before
    /// `seed()` to prevent duplicate system-reminder injection on resume.
    ///
    /// `preloaded_skills`: parent-discovered skills passed to
    /// `AgentBuilder::with_preloaded_skills()` to bypass filesystem
    /// discovery in subagents.
    ///
    /// Both are consumed once — the rebuild path (`build_agent`) passes
    /// `None` for both so zero-turn model switches get fresh discovery.
    pub async fn build_agent_with_initial_overrides(
        self: &Arc<Self>,
        definition: AgentDefinition,
        persisted_skill_names: Option<std::collections::HashSet<String>>,
        preloaded_skills: Option<Vec<kigi_tools::implementations::skills::types::SkillInfo>>,
    ) -> Result<Agent, AgentBuildError> {
        self.build_agent_inner(definition, persisted_skill_names, preloaded_skills)
            .await
    }
    #[deny(unused_variables)]
    async fn build_agent_inner(
        self: &Arc<Self>,
        definition: AgentDefinition,
        persisted_skill_names: Option<std::collections::HashSet<String>>,
        preloaded_skills: Option<Vec<kigi_tools::implementations::skills::types::SkillInfo>>,
    ) -> Result<Agent, AgentBuildError> {
        let Self {
            working_directory,
            terminal_backend,
            fs_backend,
            tools_notification_handle,
            bridge_state_path,
            session_env,
            models_manager,
            compaction_policy,
            reminder_policy,
            memory_enabled,
            memory_global_path,
            memory_workspace_path,
            memory_backend,
            web_search_config,
            backend_search,
            web_fetch_config,
            app_builder_deployer_config,
            write_file_enabled,
            subagents_enabled,
            subagent_toggle,
            ask_user_question_enabled,
            persona_summaries,
            prompt_audience,
            role_instructions,
            persona_instructions,
            skills_config,
            compat,
            context_window_tokens,
            prompt_working_directory,
            lsp,
            plugin_registry,
            api_key_provider,
            attribution_callback,
            tool_params_json,
            subagent_event_tx,
            monitor_event_buffer,
            user_question_tx,
            subagent_depth,
            session_id_str,
            respect_gitignore,
            path_not_found_hints,
            mcp_state,
            is_non_interactive,
            system_prompt_label,
            owner_session_id,
            parent_scheduler_handle,
        } = self.as_ref();
        let _ = mcp_state;
        #[allow(unused_variables)]
        let is_cursor_template =
            crate::session::is_cursor_system_template(&definition.system_prompt);
        let mut builder = AgentBuilder::new(
            working_directory.clone(),
            terminal_backend.clone(),
            tools_notification_handle.clone(),
        )
        .from_definition(definition)
        .with_compaction_policy(compaction_policy.clone())
        .with_reminder_policy(reminder_policy.clone())
        .with_memory_enabled(*memory_enabled)
        .with_memory_paths(memory_global_path.clone(), memory_workspace_path.clone())
        .with_is_non_interactive(*is_non_interactive)
        .with_system_prompt_label(system_prompt_label.clone())
        .with_session_env(session_env.clone())
        .with_state_path(bridge_state_path.clone())
        .with_web_search_config(web_search_config.clone())
        .with_backend_search(*backend_search)
        .with_app_builder_deployer_config(app_builder_deployer_config.clone())
        .with_web_fetch_config(web_fetch_config.clone())
        .with_write_file_enabled(*write_file_enabled)
        .with_fs(fs_backend.clone())
        .with_subagents_enabled(*subagents_enabled)
        .with_subagent_toggle(subagent_toggle.clone())
        .with_task_model_slugs(
            models_manager
                .available()
                .keys()
                .map(|model_id| model_id.0.to_string())
                .collect::<Vec<_>>(),
        )
        .with_ask_user_question_enabled(*ask_user_question_enabled)
        .with_persona_summaries(persona_summaries.clone())
        .with_prompt_audience(*prompt_audience)
        .with_role_instructions(role_instructions.clone())
        .with_persona_instructions(persona_instructions.clone())
        .with_skills_config(skills_config.clone())
        .with_compat_config(*compat)
        .with_context_window(*context_window_tokens)
        .with_mcp_max_output_bytes(
            crate::util::config::resolve_max_mcp_output_bytes_for_cwd(working_directory),
        );
        if let Some(owner_session_id) = owner_session_id.clone() {
            builder = builder.with_owner_session_id(owner_session_id);
        }
        if let Some(handle) = parent_scheduler_handle.clone() {
            builder = builder.with_parent_scheduler_handle(handle);
        }
        if let Some(memory_backend) = memory_backend.clone() {
            builder = builder.with_memory_backend(memory_backend);
        }
        if let Some(lsp) = lsp.clone() {
            builder = builder.with_lsp(lsp);
        }
        if let Some(plugin_registry) = plugin_registry.clone() {
            builder = builder.with_plugin_registry(plugin_registry);
        }
        if let Some(api_key_provider) = api_key_provider.clone() {
            builder = builder.with_api_key_provider(api_key_provider);
        }
        if let Some(attribution_callback) = attribution_callback.clone() {
            builder = builder.with_attribution_callback(attribution_callback);
        }
        if let Some(bash_params_json) = tool_params_json.bash.clone() {
            builder = builder.with_bash_params(bash_params_json);
        }
        if let Some(ask_user_question_params_json) = tool_params_json.ask_user_question.clone() {
            builder = builder.with_ask_user_question_params(ask_user_question_params_json);
        }
        if let Some(prompt_working_directory) = prompt_working_directory.clone() {
            builder = builder.with_prompt_working_directory(prompt_working_directory);
        }
        if let Some(names) = persisted_skill_names {
            builder = builder.with_persisted_announced_skill_names(names);
        }
        if let Some(skills) = preloaded_skills {
            builder = builder.with_preloaded_skills(skills);
        }
        let agent = builder.build().await?;
        let model_validator = models_manager.clone();
        agent
            .tool_bridge()
            .update_resource(TaskModelValidator::new(move |requested| {
                model_validator.task_model_error(requested)
            }))
            .await;
        if let Some(event_tx) = subagent_event_tx.clone() {
            use kigi_tools::implementations::kigi::task::backend::{
                ChannelBackend, SubagentBackendResource,
            };
            use kigi_tools::implementations::kigi::task::types::{
                SessionIdResource, SubagentDepthCounter, SubagentEventSender,
            };
            let backend = SubagentBackendResource(Arc::new(ChannelBackend::new(event_tx.clone())));
            agent.tool_bridge().update_resource(backend).await;
            agent
                .tool_bridge()
                .update_resource(SubagentDepthCounter(*subagent_depth))
                .await;
            agent
                .tool_bridge()
                .update_resource(SessionIdResource(session_id_str.clone()))
                .await;
            agent
                .tool_bridge()
                .update_resource(SubagentEventSender(event_tx))
                .await;
            if let Some(buffer) = monitor_event_buffer.clone() {
                agent.tool_bridge().update_resource(buffer).await;
            }
        }
        agent
            .tool_bridge()
            .update_resource(kigi_tools::types::resources::RespectGitignore(
                *respect_gitignore,
            ))
            .await;
        agent
            .tool_bridge()
            .update_resource(kigi_tools::types::resources::PathNotFoundHints(
                *path_not_found_hints,
            ))
            .await;
        {
            use kigi_tools::implementations::kigi::ask_user_question::UserQuestionSender;
            agent
                .tool_bridge()
                .update_resource(UserQuestionSender(user_question_tx.clone()))
                .await;
        }
        Ok(agent)
    }
}
/// Build a stub [`AgentRebuildSpec`] for unit tests.
///
/// Every field is set to a minimal default suitable for test `SessionActor`
/// literals and focused `build_agent` tests.
#[cfg(test)]
pub(crate) fn test_rebuild_spec_default() -> Arc<AgentRebuildSpec> {
    let (uq_tx, _uq_rx) = tokio::sync::mpsc::unbounded_channel();
    Arc::new(AgentRebuildSpec {
        working_directory: std::env::temp_dir(),
        terminal_backend: Arc::new(
            kigi_tools::computer::local::LocalTerminalBackend::new_local(
                kigi_tools::computer::local::SearchShadowConfig::default(),
            ),
        ),
        fs_backend: Arc::new(kigi_tools::computer::local::LocalFs),
        tools_notification_handle: ToolNotificationHandle::noop(),
        bridge_state_path: std::env::temp_dir().join("test_tool_state.json"),
        session_env: Arc::new(HashMap::new()),
        models_manager: crate::agent::models::ModelsManager::default(),
        compaction_policy: CompactionPolicy::default(),
        reminder_policy: ReminderPolicy::default(),
        memory_enabled: false,
        memory_global_path: None,
        memory_workspace_path: None,
        memory_backend: None,
        web_search_config: WebSearchConfig::default(),
        backend_search: false,
        web_fetch_config: WebFetchConfig::Disabled,
        app_builder_deployer_config: AppBuilderDeployerConfig::default(),
        write_file_enabled: true,
        subagents_enabled: false,
        subagent_toggle: HashMap::new(),
        ask_user_question_enabled: true,
        persona_summaries: vec![],
        prompt_audience: PromptAudience::Primary,
        role_instructions: None,
        persona_instructions: None,
        skills_config: SkillsConfig::default(),
        compat: CompatConfig::default(),
        context_window_tokens: 256_000,
        prompt_working_directory: None,
        lsp: None,
        plugin_registry: None,
        api_key_provider: None,
        attribution_callback: None,
        tool_params_json: ResolvedToolParamsJson::default(),
        subagent_event_tx: None,
        monitor_event_buffer: None,
        user_question_tx: uq_tx,
        subagent_depth: 0,
        session_id_str: "test-session".to_string(),
        respect_gitignore: false,
        path_not_found_hints: false,
        mcp_state: Arc::new(tokio::sync::Mutex::new(
            crate::session::mcp_servers::McpState::new(vec![]),
        )),
        is_non_interactive: false,
        system_prompt_label: kigi_agent::DEFAULT_SYSTEM_PROMPT_LABEL.to_string(),
        owner_session_id: Some("test-session".to_string()),
        parent_scheduler_handle: None,
    })
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::agent::config::{EndpointsConfig, ModelEntry};
    fn model_entry(internal_id: &str) -> ModelEntry {
        ModelEntry::fallback(internal_id, &EndpointsConfig::default())
    }
    fn task_description(agent: &Agent) -> String {
        let toolset = agent.tool_bridge().toolset();
        let task_name = toolset
            .tool_name_for_kind(kigi_tools::types::tool::ToolKind::Task)
            .expect("Kigi Task tool should be present");
        toolset
            .tool_definitions()
            .into_iter()
            .find(|definition| definition.function.name == task_name)
            .and_then(|definition| definition.function.description)
            .expect("Kigi Task description should be present")
    }
    #[tokio::test(flavor = "current_thread")]
    async fn rebuild_projects_fresh_public_model_keys_into_task_description() {
        tokio::task::LocalSet::new()
            .run_until(async {
                let mut spec = test_rebuild_spec_default();
                Arc::get_mut(&mut spec)
                    .expect("test rebuild spec should be uniquely owned")
                    .subagents_enabled = true;
                let models_manager = spec.models_manager.clone();
                models_manager
                    .insert_test_entry("zeta-public", model_entry("internal-zeta"));
                models_manager
                    .insert_test_entry("alpha-public", model_entry("internal-alpha"));
                let mut hidden = model_entry("internal-hidden");
                hidden.info.hidden = true;
                models_manager.insert_test_entry("private-hidden-model", hidden);
                let mut unselectable = model_entry("internal-unselectable");
                unselectable.info.user_selectable = false;
                models_manager
                    .insert_test_entry("private-unselectable-model", unselectable);
                let first = spec
                    .build_agent(AgentDefinition::default_kigi())
                    .await
                    .expect("first agent build should succeed");
                let first_description = task_description(&first);
                assert!(
                    first_description
                    .contains("If the user explicitly asks for the model of a subagent/task, you may ONLY use model slugs from this list:\n\
                         - alpha-public\n\
                         - zeta-public")
                );
                assert!(! first_description.contains("private-hidden-model"));
                assert!(! first_description.contains("private-unselectable-model"));
                assert!(! first_description.contains("internal-alpha"));
                let validator = first
                    .tool_bridge()
                    .toolset()
                    .get_resource_cloned::<TaskModelValidator>()
                    .await
                    .expect("Task model validator should be registered");
                assert!(validator.error_for("alpha-public").is_none());
                assert!(validator.error_for("private-hidden-model").is_some());
                models_manager
                    .insert_test_entry("beta-public", model_entry("internal-beta"));
                assert!(validator.error_for("beta-public").is_none());
                let rebuilt = spec
                    .build_agent(AgentDefinition::default_kigi())
                    .await
                    .expect("rebuilt agent should succeed");
                let rebuilt_description = task_description(&rebuilt);
                assert!(
                    rebuilt_description
                    .contains("If the user explicitly asks for the model of a subagent/task, you may ONLY use model slugs from this list:\n\
                         - alpha-public\n\
                         - beta-public\n\
                         - zeta-public")
                );
            })
            .await;
    }
}
