Eliminate ~500 lines of duplicate code across 31 files by extracting shared functions, helpers, and consolidating repeated patterns. Highlights: - Toast helpers (toast_info/success/warning/error) on AppStateRest - push_event() helper for turn-event queue (19 callers consolidated) - log_write_edit_tool() shared fn (turn.rs + engine.rs ~50 lines saved) - resolve_api_key() shared fn (spawn.rs + provider.rs) - LSP call_positional() helper on LspClient - lsp_cursor_params() shared schema for 4 tool files -overlay_block() helper for consistent overlay title/border styling - cycle_selected_index(), path_not_found/a_directory() helpers - mark_dirty(), save_settings() on AppStateRest - Remove redundant Err(e) => Err(e) arms in LSP tools - Consolidate generate_workspace_tree (turn.rs → workspace.rs) - Simplify background-review wrapper args in auto/mod.rs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
157 lines
5.8 KiB
Rust
157 lines
5.8 KiB
Rust
//! 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<crate::dto::provider::request::ToolDef>,
|
|
pub(super) tools: Vec<Box<dyn crate::tool::Tool>>,
|
|
pub(super) ctx: crate::tool::ToolCtx,
|
|
pub(super) context_window: usize,
|
|
|
|
pub(super) workspace_roots: Vec<std::path::PathBuf>,
|
|
pub(super) edit_log_session_dir: std::path::PathBuf,
|
|
pub(super) session_id: String,
|
|
pub(super) db: Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>,
|
|
pub(super) temperature: f32,
|
|
pub(super) max_tokens: Option<u32>,
|
|
pub(super) abort_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
|
/// 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<std::path::PathBuf> = 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;
|
|
}
|
|
});
|
|
}
|