//! Subagent LLM provider — resolves provider/model from settings and wraps //! `LlmClient` in a higher-level API for subagent use. //! //! Flow: `resolve_subagent_provider` is called at startup to pick a provider //! + model → `SubagentProvider` wraps that pair around an `LlmClient` for use //! inside the subagent engine loop. use anyhow::Result; use tracing::instrument; use crate::llm::provider::LlmClient; use crate::tools::{tool_defs, Tool}; use zesdex_domain::core::ChatMessage; /// Provider wrapper for subagent LLM interactions. /// /// Provides two convenience methods (`chat`, `chat_with_tools`) that /// abstract away the raw `LlmClient` parameter plumbing so the engine /// loop only deals with messages and tools. /// /// The model identifier is already embedded in the `LlmClient` itself /// (its `model` field), so `SubagentProvider` does not duplicate it. pub struct SubagentProvider { client: LlmClient, } impl SubagentProvider { /// Wrap an existing `LlmClient` for higher-level use. #[instrument(skip(client))] pub fn new(client: LlmClient) -> Self { Self { client } } /// Send messages to the LLM without any tool definitions. /// /// Use this for a plain text-in/text-out conversation. #[tracing::instrument(skip(self, messages))] pub async fn chat( &self, messages: &[ChatMessage], ) -> Result<(ChatMessage, Option<(u64, u64)>)> { use zesdex_application::ports::ProviderService; self.client .chat(messages, None, Some(4096), None) .await } /// Send messages with available tool definitions. /// /// Automatically converts the `&[Box]` slice to /// `Vec` before passing to the underlying client. #[tracing::instrument(skip(self, messages, tools))] pub async fn chat_with_tools( &self, messages: &[ChatMessage], tools: &[Box], ) -> Result<(ChatMessage, Option<(u64, u64)>)> { let defs = tool_defs(tools); use zesdex_application::ports::ProviderService; self.client .chat(messages, Some(defs), Some(4096), None) .await } } /// Resolve subagent provider and model from settings. /// /// Flow: reads `settings.provider` and `settings.model` → if model is empty, /// falls back to the provider config's `default_model` → if that is also /// empty, uses the domain default model constant. #[instrument] pub fn resolve_subagent_provider( settings: &zesdex_domain::cms::Settings, app_config: &zesdex_domain::cms::AppConfig, ) -> (String, String) { let provider = settings.provider.clone(); let model = settings.model.clone(); // Use the default model from the provider config if available let model = if model.is_empty() { app_config .providers .get(&provider) .and_then(|p| p.default_model.clone()) .unwrap_or_else(|| zesdex_domain::agent::defaults::DEFAULT_MODEL.to_string()) } else { model }; (provider, model) }