//! Turn-spawning logic: `spawn_turn` and the `TurnCtx` bundle passed to //! the background thread that runs `run_agent_turn`. use crate::app::state::rest::AppStateRest; use crate::app::state::runtime::TurnEvent; use super::turn::run_agent_turn; /// Context bundle passed to `run_agent_turn` on its background thread. pub(super) struct TurnCtx { pub(super) client: crate::service::provider::LlmClient, pub(super) tdefs: Vec, pub(super) tools: Vec>, pub(super) ctx: crate::tool::ToolCtx, pub(super) context_window: usize, pub(super) workspace_roots: Vec, pub(super) edit_log_session_dir: std::path::PathBuf, pub(super) session_id: String, pub(super) db: Option>>, pub(super) temperature: f32, pub(super) max_tokens: Option, pub(super) abort_flag: std::sync::Arc, /// Snapshot of `SessionRuntime.hive_mind_converged` taken at the start /// of this turn — whether a hive-mind convergence already completed /// earlier in this session. pub(super) hive_mind_converged: bool, } /// Spawn a background thread that runs one full LLM turn. /// /// Flow: check that no turn is currently in-flight → bail if so → /// collect messages and config from state → determine API key (from /// settings, env var, or default) → resolve generation params from /// the current effort level → collect all tools (built-in + MCP) → /// build `TurnCtx` → spawn a thread running `run_agent_turn` → /// on any error, push a `TurnEvent::Error` → clear the in-flight flag /// when the thread exits. /// /// Why: runs on a plain OS thread so the async event loop stays responsive. /// /// Return: nothing; results flow through `state.turn_events`. pub(super) fn spawn_turn(state: &AppStateRest) { let in_flight = if let Ok(guard) = state.turn_in_flight.lock() { *guard } else { return; }; if in_flight { return; } let messages = state .session_runtime .as_ref() .map(|rt| rt.messages.clone()) .unwrap_or_default(); if messages.is_empty() { return; } let mut api_key = crate::service::provider::resolve_api_key( &state.settings, &state.app_config, ); let model = state.settings.model.clone(); let base_url = state .app_config .providers .get(&state.settings.provider) .map(|p| p.api_base.clone()); let context_window = state .app_config .model_roles .values() .find(|role| { role.provider == state.settings.provider && role.model == state.settings.model }) .and_then(|role| role.context_window) .unwrap_or(state.app_config.default_context_window) as usize; // The selected provider has no entry in app_config at all (e.g. the // Claude-settings auto-detection that registers "claude" found nothing // this run). Without this check, LlmClient::new silently falls back to // the zen default base URL while keeping this provider's model name — // a mismatched request that reaches a real server and comes back as a // confusing "Missing API key" 401 from an unrelated provider, instead // of the actual problem: the configured provider doesn't exist. if base_url.is_none() { if let Ok(mut q) = state.turn_events.lock() { q.push_back(TurnEvent::Error(format!( "Provider '{}' is not configured — no matching entry found. \ Pick a different provider in Settings, or configure it.", state.settings.provider ))); } return; } if api_key.is_empty() { api_key = crate::service::provider::DEFAULT_API_KEY.to_string(); } let (temperature, max_tokens) = crate::app::mode::effort::generation_params( state.misc.effort_level, state.settings.max_tokens, ); let mut tools = crate::tool::all_tools(); tools.extend(state.mcp_manager.as_tools()); let tool_defs = crate::tool::tool_defs(&tools); let ctx = state.tool_ctx(); let edit_session_dir = state.session_dir.clone(); let session_id = state.session_id.clone(); let turn_events = state.turn_events.clone(); let in_flight_flag = state.turn_in_flight.clone(); let workspace_roots: Vec = ctx.workspaces.clone(); let abort_flag = state.abort_flag.clone(); abort_flag.store(false, std::sync::atomic::Ordering::SeqCst); let hive_mind_converged = state .session_runtime .as_ref() .is_some_and(|rt| rt.hive_mind_converged); *in_flight_flag.lock().unwrap_or_else(|e| { tracing::error!("[spawn_turn] in_flight_flag mutex poisoned: {}", e); e.into_inner() }) = true; let events_q = turn_events.clone(); std::thread::spawn(move || { let db = crate::model::msglog::open_or_create(&edit_session_dir) .ok() .map(|c| std::sync::Arc::new(std::sync::Mutex::new(c))); let tc = TurnCtx { client: crate::service::provider::LlmClient::new(api_key, model.clone(), base_url), tdefs: tool_defs, tools, ctx, context_window, workspace_roots, edit_log_session_dir: edit_session_dir, session_id, db, temperature, max_tokens, abort_flag, hive_mind_converged, }; let result = run_agent_turn(&tc, &messages, &events_q); if let Err(e) = result { if let Ok(mut q) = events_q.lock() { q.push_back(TurnEvent::Error(e.to_string())); } } if let Ok(mut flag) = in_flight_flag.lock() { *flag = false; } }); }