pub(crate) mod convert_conversation; mod convert_from; mod convert_to; mod r#impl; use std::path::Path; use std::pin::Pin; use std::sync::Arc; pub use ai::agent::convert::ConvertToAPITypeError; use ai::api_keys::ApiKeyManager; pub use convert_from::{ user_inputs_from_messages, ConversionParams, ConvertAPIMessageToClientOutputMessage, MaybeAIAgentOutputMessage, MessageToAIAgentOutputMessageError, }; use futures_lite::Stream; use galaxy_core::channel::ChannelState; use galaxy_core::execution_mode::AppExecutionMode; use galaxy_core::features::FeatureFlag; use galaxy_core::user_preferences::GetUserPreferences; use galaxyui::{AppContext, EntityId, SingletonEntity as _}; use mcp::TemplatableMCPServerInfo; pub use r#impl::generate_multi_agent_output; use serde::Serialize; use super::{AIAgentInput, MCPContext, MCPServer, RequestMetadata, Suggestions}; use crate::ai::agent::conversation::AIConversationId; use crate::ai::ambient_agents::AmbientAgentTaskId; use crate::ai::blocklist::{BlocklistAIPermissions, RequestInput, SessionContext}; use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel; use crate::ai::execution_profiles::AIExecutionProfileAppExt; use crate::ai::llms::{LLMId, LLMPreferences}; use crate::ai::mcp::TemplatableMCPServerManager; use crate::server::server_api::AIApiError; use crate::settings::AISettings; use crate::terminal::safe_mode_settings::get_secret_obfuscation_mode; use crate::workspaces::user_workspaces::UserWorkspaces; /// Unique, server-generated conversation-scoped token to be roundtripped to the API when sending /// requests that follow-up within a given conversation. #[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)] pub struct ServerConversationToken(String); impl ServerConversationToken { pub fn new(id: String) -> Self { Self(id) } pub fn as_str(&self) -> &str { &self.0 } pub fn debug_link(&self) -> String { format!( "{}/debug/maa/{}", ChannelState::server_root_url(), self.as_str() ) } pub fn conversation_link(&self) -> String { format!( "{}/conversation/{}", ChannelState::server_root_url(), self.as_str() ) } } impl From for String { fn from(value: ServerConversationToken) -> Self { value.0 } } // Conversions between AI ServerConversationToken and protocol ServerConversationToken impl From for ServerConversationToken { fn from(token: session_sharing_protocol::common::ServerConversationToken) -> Self { Self(token.to_string()) } } impl TryFrom for session_sharing_protocol::common::ServerConversationToken { type Error = uuid::Error; fn try_from(token: ServerConversationToken) -> Result { token.as_str().parse() } } #[derive(Debug, Clone)] pub struct RequestParams { pub input: Vec, pub conversation_token: Option, pub forked_from_conversation_token: Option, pub ambient_agent_task_id: Option, pub tasks: Vec, pub existing_suggestions: Option, pub metadata: Option, pub session_context: SessionContext, pub model: LLMId, #[allow(unused)] pub coding_model: LLMId, pub cli_agent_model: LLMId, pub computer_use_model: LLMId, pub is_memory_enabled: bool, pub warp_drive_context_enabled: bool, pub context_window_limit: Option, pub mcp_context: Option, pub planning_enabled: bool, should_redact_secrets: bool, /// User-provided API keys for AI providers (BYO API Key). pub api_keys: Option, /// User-provided custom model providers (BYOK endpoints). pub custom_model_providers: Option, /// User-defined custom model routers referenced by the current selection. Mirrors /// `custom_model_providers`: the selected model's `config_key` indexes into this /// registry. `None` when no custom router is selected. pub custom_model_routers: Option, pub allow_use_of_warp_credits: bool, pub autonomy_level: warp_multi_agent_api::AutonomyLevel, pub isolation_level: warp_multi_agent_api::IsolationLevel, pub web_search_enabled: bool, pub computer_use_enabled: bool, pub ask_user_question_enabled: bool, pub research_agent_enabled: bool, pub orchestration_enabled: bool, pub supported_tools_override: Option>, /// The root task ID for the conversation — needed for direct Bedrock streaming /// since optimistic tasks don't appear in the proto task_context. pub root_task_id: Option, /// The conversation ID of the parent agent that spawned this child agent, if any. pub parent_agent_id: Option, /// The display name for this agent (e.g. "Agent 1"), assigned by the orchestrator. pub agent_name: Option, /// Full Bedrock conversation history for direct Bedrock calls. /// When present, the Bedrock path uses this instead of extracting from task_context. pub bedrock_message_history: Vec, /// Progressive summary of older conversation history. Prepended as the first /// message pair in the messages array sent to Bedrock. pub bedrock_progressive_summary: Option, /// Archived tool_use/tool_result pairs from previous summarization drains. /// Passed to the Bedrock translator so `recall_tool_history` can search archived /// results even after they've been summarized away from live history. pub bedrock_tool_result_archive: Vec, /// Populated by the Bedrock path after building the message list. /// Contains the full messages sent (old history + new input) so the controller /// can store them back into the conversation for the next request cycle. pub bedrock_messages_sent: std::sync::Arc>>, } pub type Event = Result>; #[cfg(not(target_family = "wasm"))] pub type ResponseStream = Pin + Send + 'static>>; // The WASM version of this type has no bound on `Send`, which is an unnecessary bound when // targeting wasm because the browser is single-threaded (and we don't leverage WebWorkers for async // execution in WoW). #[cfg(target_family = "wasm")] pub type ResponseStream = Pin>>; #[derive(Debug, Clone)] pub struct ConversationData { pub id: AIConversationId, pub tasks: Vec, pub server_conversation_token: Option, pub forked_from_conversation_token: Option, pub ambient_agent_task_id: Option, pub existing_suggestions: Option, } impl RequestParams { #[cfg(test)] pub fn new_for_test() -> Self { Self { input: vec![], conversation_token: None, forked_from_conversation_token: None, ambient_agent_task_id: None, tasks: vec![], existing_suggestions: None, metadata: None, session_context: SessionContext::new_for_test(), model: LLMId::from("test-model"), coding_model: LLMId::from("test-model"), cli_agent_model: LLMId::from("test-model"), computer_use_model: LLMId::from("test-model"), is_memory_enabled: false, warp_drive_context_enabled: false, context_window_limit: None, mcp_context: None, planning_enabled: false, should_redact_secrets: false, api_keys: None, custom_model_providers: None, custom_model_routers: None, allow_use_of_warp_credits: false, autonomy_level: Default::default(), isolation_level: Default::default(), web_search_enabled: false, computer_use_enabled: false, ask_user_question_enabled: false, research_agent_enabled: false, orchestration_enabled: false, supported_tools_override: None, parent_agent_id: None, agent_name: None, root_task_id: None, bedrock_message_history: vec![], bedrock_progressive_summary: None, bedrock_tool_result_archive: vec![], bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(vec![])), } } pub fn new( terminal_view_id: Option, session_context: SessionContext, request_input: &RequestInput, conversation: ConversationData, metadata: Option, app: &AppContext, ) -> Self { let ai_settings = AISettings::as_ref(app); let is_memory_enabled = ai_settings.is_memory_enabled(app); let warp_drive_context_enabled = ai_settings.is_warp_drive_context_enabled(app); // Build MCP context - either grouped by server or flat lists based on feature flag let mcp_context = if FeatureFlag::MCPGroupedServerContext.is_enabled() { // Group MCP tools and resources by server let templatable_manager = TemplatableMCPServerManager::as_ref(app); let mut active_servers: Vec<&TemplatableMCPServerInfo> = templatable_manager .get_active_templatable_servers() .values() .copied() .collect(); // If file-based MCP servers are enabled, add active servers in scope of // the user's current working directory if let Some(cwd) = session_context.current_working_directory() { active_servers.extend( templatable_manager .get_active_file_based_servers(Path::new(cwd), app) .values(), ); } // Include any ephemeral MCP servers started via the Oz CLI. active_servers.extend( templatable_manager .get_active_cli_spawned_servers() .values(), ); let servers: Vec = active_servers .into_iter() .map(|server| MCPServer { name: server.name().to_string(), description: server.description().unwrap_or_default().to_string(), id: server.installation_id().to_string(), resources: server.resources().to_vec(), tools: server.tools().to_vec(), }) .collect(); if servers.is_empty() { None } else { #[allow(deprecated)] Some(MCPContext { resources: vec![], tools: vec![], servers, }) } } else { // Flat lists of resources and tools let templatable_mcp_manager = TemplatableMCPServerManager::as_ref(app); let resources = templatable_mcp_manager .resources() .cloned() .collect::>(); let tools = templatable_mcp_manager.tools().cloned().collect::>(); #[allow(deprecated)] (!resources.is_empty() || !tools.is_empty()).then_some(MCPContext { resources, tools, servers: vec![], }) }; let should_redact_secrets = get_secret_obfuscation_mode(app).should_redact_secret(); let user_workspaces = UserWorkspaces::as_ref(app); let api_key_manager = ApiKeyManager::as_ref(app); let is_byo_enabled = user_workspaces.is_byo_api_key_enabled(app); #[cfg(not(target_family = "wasm"))] let geap_binding = crate::ai::geap_credentials::current_geap_policy(app).mint_binding(); #[cfg(target_family = "wasm")] let geap_binding: Option<::ai::api_keys::GeapMintBinding> = None; let api_keys = api_key_manager.api_keys_for_request( is_byo_enabled, user_workspaces.is_bedrock_enabled(app), geap_binding, ); let is_custom_inference_enabled = user_workspaces.is_custom_inference_enabled(app); let custom_model_providers = FeatureFlag::CustomInferenceEndpoints .is_enabled() .then(|| { api_key_manager.custom_model_providers_for_request(is_custom_inference_enabled) }) .flatten(); let custom_model_routers = FeatureFlag::CustomModelRouters.is_enabled().then(|| { LLMPreferences::as_ref(app).custom_model_routers_for_request( &request_input.model_id, &request_input.coding_model_id, ) }); let allow_use_of_warp_credits = *AISettings::as_ref(app).can_use_warp_credits_for_fallback; let app_execution_mode = AppExecutionMode::as_ref(app); let autonomy_level = if app_execution_mode.is_autonomous() { warp_multi_agent_api::AutonomyLevel::Unsupervised } else { warp_multi_agent_api::AutonomyLevel::Supervised }; let isolation_level = if app_execution_mode.is_sandboxed() { warp_multi_agent_api::IsolationLevel::Sandbox } else { warp_multi_agent_api::IsolationLevel::None }; let web_search_enabled = BlocklistAIPermissions::as_ref(app).get_web_search_enabled(app, terminal_view_id); let research_agent_enabled = app .private_user_preferences() .read_value("ResearchAgentEnabled") .ok() .flatten() .and_then(|s| s.parse().ok()) .unwrap_or_default(); let is_ambient_agent = conversation.ambient_agent_task_id.is_some(); let computer_use_enabled = FeatureFlag::AgentModeComputerUse.is_enabled() && BlocklistAIPermissions::as_ref(app) .get_computer_use_setting(app, terminal_view_id) .is_enabled() && computer_use::is_supported_on_current_platform() && (FeatureFlag::LocalComputerUse.is_enabled() || is_ambient_agent); let ask_user_question_enabled = BlocklistAIPermissions::as_ref(app) .get_ask_user_question_setting(app, terminal_view_id) != crate::ai::execution_profiles::AskUserQuestionPermission::Never; let orchestration_enabled = ai_settings.is_orchestration_enabled(app) && BlocklistAIPermissions::as_ref(app) .get_run_agents_setting(app, terminal_view_id) .is_enabled() && session_context .session_type() .as_ref() .is_none_or(|t| matches!(t, crate::terminal::model::session::SessionType::Local)); // Reconcile the persisted override against the active base model's // current `LLMContextWindow` instead of trusting whatever was stored // last. If the active model isn't configurable or has been removed // server-side, drop the override; otherwise clamp it to the model's // current `[min, max]` range. This closes the window between an // in-flight model metadata refresh and the next request. let context_window_limit = AIExecutionProfilesModel::as_ref(app) .active_profile(terminal_view_id, app) .data() .context_window_limit_for_request(app); Self { input: request_input.all_inputs().cloned().collect(), conversation_token: conversation.server_conversation_token, forked_from_conversation_token: conversation.forked_from_conversation_token, ambient_agent_task_id: conversation.ambient_agent_task_id, tasks: conversation.tasks, existing_suggestions: conversation.existing_suggestions, context_window_limit, metadata, session_context, model: request_input.model_id.clone(), coding_model: request_input.coding_model_id.clone(), cli_agent_model: request_input.cli_agent_model_id.clone(), computer_use_model: request_input.computer_use_model_id.clone(), is_memory_enabled, warp_drive_context_enabled, mcp_context, planning_enabled: true, should_redact_secrets, api_keys, custom_model_providers, custom_model_routers, allow_use_of_warp_credits, autonomy_level, isolation_level, web_search_enabled, computer_use_enabled, ask_user_question_enabled, research_agent_enabled, orchestration_enabled, supported_tools_override: request_input.supported_tools_override.clone(), root_task_id: request_input .input_messages .keys() .next() .map(|id| id.to_string()), parent_agent_id: None, agent_name: None, bedrock_message_history: Vec::new(), bedrock_progressive_summary: None, bedrock_tool_result_archive: Vec::new(), bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), } } }