2026-07-21 06:42:37 +07:00
|
|
|
//! TUI agent turn interface adapter — resolves LLM provider configuration,
|
|
|
|
|
//! builds the tool context, and spawns the agent turn on a background task.
|
2026-07-20 10:55:09 +07:00
|
|
|
|
2026-07-20 16:29:32 +07:00
|
|
|
use std::sync::atomic::Ordering;
|
|
|
|
|
use tracing::info;
|
2026-07-20 10:55:09 +07:00
|
|
|
|
|
|
|
|
use zesdex_domain::core::ChatMessage;
|
2026-07-21 06:24:20 +07:00
|
|
|
use zesdex_domain::agent::AgentTurnParams;
|
2026-07-21 06:42:37 +07:00
|
|
|
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;
|
|
|
|
|
|
2026-07-20 10:55:09 +07:00
|
|
|
use crate::state::AppStateRest;
|
|
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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<String> {
|
|
|
|
|
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`.
|
2026-07-20 15:53:20 +07:00
|
|
|
#[tracing::instrument(skip(state))]
|
2026-07-20 10:55:09 +07:00
|
|
|
pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) {
|
2026-07-21 06:42:37 +07:00
|
|
|
// Only one turn at a time — compare_exchange is lock-free
|
2026-07-20 16:29:32 +07:00
|
|
|
if state
|
|
|
|
|
.turn_in_flight_flag
|
2026-07-20 13:52:20 +07:00
|
|
|
.compare_exchange(false, true, Ordering::SeqCst, Ordering::Relaxed)
|
|
|
|
|
.is_err()
|
|
|
|
|
{
|
2026-07-21 06:42:37 +07:00
|
|
|
return;
|
2026-07-20 10:55:09 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
// ── Resolve provider configuration ─────────────────────────────────
|
2026-07-20 14:30:36 +07:00
|
|
|
let provider_name = &state.settings.provider;
|
2026-07-21 06:42:37 +07:00
|
|
|
let api_key = resolve_api_key(state, provider_name);
|
2026-07-20 14:30:36 +07:00
|
|
|
let model = state.settings.model.clone();
|
2026-07-21 06:42:37 +07:00
|
|
|
let api_base = resolve_api_base(state, provider_name);
|
2026-07-20 14:30:36 +07:00
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
// ── Build message list ─────────────────────────────────────────────
|
2026-07-20 10:55:09 +07:00
|
|
|
let mut messages: Vec<ChatMessage> = 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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
info!("Spawning agent turn (model: {model})");
|
2026-07-20 10:55:09 +07:00
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
// ── Assemble dependencies (composition root) ──────────────────────
|
2026-07-20 16:29:32 +07:00
|
|
|
let params = AgentTurnParams {
|
|
|
|
|
messages,
|
2026-07-21 06:24:20 +07:00
|
|
|
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(),
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
let client = std::sync::Arc::new(LlmClient::new(api_key, model, api_base));
|
2026-07-20 16:29:32 +07:00
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
let tool_ctx = ToolCtx::builder()
|
2026-07-21 06:24:20 +07:00
|
|
|
.session_dir(session_dir)
|
|
|
|
|
.workspaces(workspace_roots)
|
|
|
|
|
.turn_events(turn_events)
|
|
|
|
|
.build();
|
|
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
let tool_executor =
|
|
|
|
|
std::sync::Arc::new(InfrastructureToolExecutor::new(tool_ctx));
|
2026-07-21 06:24:20 +07:00
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
let tools = all_tools();
|
|
|
|
|
let defs = tool_defs(&tools);
|
2026-07-21 06:24:20 +07:00
|
|
|
|
2026-07-21 06:42:37 +07:00
|
|
|
let turn_service = AgentTurnServiceImpl::new(client, tool_executor, defs);
|
2026-07-21 06:24:20 +07:00
|
|
|
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
let _ = turn_service.run_turn(params).await;
|
|
|
|
|
});
|
2026-07-20 10:55:09 +07:00
|
|
|
}
|
2026-07-21 07:00:15 +07:00
|
|
|
|
|
|
|
|
#[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);
|
|
|
|
|
}
|
|
|
|
|
}
|