//! TUI agent turn interface adapter — resolves LLM provider configuration, //! builds the tool context, and spawns the agent turn on a background task. use std::sync::atomic::Ordering; use tracing::info; use zesdex_domain::core::ChatMessage; use zesdex_domain::agent::AgentTurnParams; use zesdex_infrastructure::llm::provider::LlmClient; use zesdex_infrastructure::tools::executor::InfrastructureToolExecutor; use zesdex_infrastructure::tools::{all_tools, tool_defs, ToolCtx}; use zesdex_application::agent::turn_service::AgentTurnServiceImpl; use zesdex_application::agent::AgentTurnService; use crate::state::AppStateRest; // --------------------------------------------------------------------------- // Provider resolution // --------------------------------------------------------------------------- /// Resolve the API key from settings or environment for the given provider. fn resolve_api_key(state: &AppStateRest, provider_name: &str) -> String { if let Some(key) = state.settings.api_keys.get(provider_name) { return key.clone(); } if let Some(ref cfg) = state.app_config.providers.get(provider_name) { if let Some(ref default_key) = cfg.default_api_key { if !default_key.is_empty() { return default_key.clone(); } } if let Some(ref env_name) = cfg.api_key_env { if let Ok(val) = std::env::var(env_name) { return val; } } } String::new() } /// Resolve the API base URL from the provider config. fn resolve_api_base(state: &AppStateRest, provider_name: &str) -> Option { state .app_config .providers .get(provider_name) .map(|cfg| cfg.api_base.clone()) } // --------------------------------------------------------------------------- // Turn spawning // --------------------------------------------------------------------------- /// Spawn an agent turn on a background Tokio task. /// /// Flow: /// 1. Compare-exchange the in-flight flag (no-op if already running). /// 2. Resolve provider (API key, model, base URL) from settings. /// 3. Build messages including the user's input text. /// 4. Construct `AgentTurnParams` with the turn-event queue and abort flag. /// 5. Create `LlmClient`, `ToolCtx`, and `InfrastructureToolExecutor`. /// 6. Assemble `AgentTurnServiceImpl` and spawn it via `tokio::spawn`. #[tracing::instrument(skip(state))] pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) { // Only one turn at a time — compare_exchange is lock-free if state .turn_in_flight_flag .compare_exchange(false, true, Ordering::SeqCst, Ordering::Relaxed) .is_err() { return; } let turn_events = state.turn_events.clone(); let in_flight = state.turn_in_flight_flag.clone(); let abort = state.abort_flag.clone(); let session_dir = state.session_dir.clone(); let workspace_roots = state.workspace_roots.clone(); // ── Resolve provider configuration ───────────────────────────────── let provider_name = &state.settings.provider; let api_key = resolve_api_key(state, provider_name); let model = state.settings.model.clone(); let api_base = resolve_api_base(state, provider_name); // ── Build message list ───────────────────────────────────────────── let mut messages: Vec = state .session_runtime .as_ref() .map(|rt| rt.messages.clone()) .unwrap_or_default(); messages.push(ChatMessage::user(text)); if let Some(ref mut rt) = state.session_runtime { rt.messages = messages.clone(); } info!("Spawning agent turn (model: {model})"); // ── Assemble dependencies (composition root) ────────────────────── let params = AgentTurnParams { messages, session_dir: session_dir.clone(), workspace_roots: workspace_roots.clone(), turn_events: turn_events.clone(), in_flight: in_flight.clone(), abort: abort.clone(), api_key: api_key.clone(), model: model.clone(), api_base: api_base.clone(), }; let client = std::sync::Arc::new(LlmClient::new(api_key, model, api_base)); let tool_ctx = ToolCtx::builder() .session_dir(session_dir) .workspaces(workspace_roots) .turn_events(turn_events) .build(); let tool_executor = std::sync::Arc::new(InfrastructureToolExecutor::new(tool_ctx)); let tools = all_tools(); let defs = tool_defs(&tools); let turn_service = AgentTurnServiceImpl::new(client, tool_executor, defs); tokio::spawn(async move { let _ = turn_service.run_turn(params).await; }); } #[cfg(test)] mod tests { use super::*; #[test] fn test_spawn_agent_turn_with_tokio_runtime() { let rt = tokio::runtime::Runtime::new().unwrap(); let _guard = rt.enter(); let temp_dir = std::env::temp_dir().join(format!("zesdex_test_{}", uuid::Uuid::new_v4())); let session_dir = temp_dir.join("session"); let memory_dir = temp_dir.join("memory"); std::fs::create_dir_all(&session_dir).unwrap(); std::fs::create_dir_all(&memory_dir).unwrap(); let workspace_roots = vec![temp_dir.clone()]; let mut state = AppStateRest::new(workspace_roots, &session_dir, memory_dir); spawn_agent_turn(&mut state, "hello".to_string()); assert!(state.turn_in_flight()); let _ = std::fs::remove_dir_all(&temp_dir); } }