Files
galaxy/app/src/ai/agent/api.rs
T

323 lines
12 KiB
Rust

pub(crate) mod convert_conversation;
mod convert_from;
mod convert_to;
mod r#impl;
pub use ai::agent::convert::ConvertToAPITypeError;
use ai::api_keys::ApiKeyManager;
pub use convert_from::{
user_inputs_from_messages, ConversionParams, ConvertAPIMessageToClientOutputMessage,
MaybeAIAgentOutputMessage, MessageToAIAgentOutputMessageError,
};
pub use r#impl::generate_multi_agent_output;
use futures_lite::Stream;
use serde::Serialize;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use warp_core::channel::ChannelState;
use warp_core::execution_mode::AppExecutionMode;
use warp_core::features::FeatureFlag;
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::{
ai::{blocklist::SessionContext, llms::LLMId},
server::server_api::AIApiError,
};
use super::{AIAgentInput, MCPContext, MCPServer, RequestMetadata, Suggestions};
use crate::ai::blocklist::{BlocklistAIPermissions, RequestInput};
use crate::ai::mcp::templatable_manager::TemplatableMCPServerInfo;
use crate::ai::mcp::TemplatableMCPServerManager;
use crate::settings::AISettings;
use crate::terminal::safe_mode_settings::get_secret_obfuscation_mode;
use crate::workspaces::user_workspaces::UserWorkspaces;
use warp_core::user_preferences::GetUserPreferences;
use warpui::{AppContext, EntityId, SingletonEntity as _};
/// 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<ServerConversationToken> for String {
fn from(value: ServerConversationToken) -> Self {
value.0
}
}
// Conversions between AI ServerConversationToken and protocol ServerConversationToken
impl From<session_sharing_protocol::common::ServerConversationToken> for ServerConversationToken {
fn from(token: session_sharing_protocol::common::ServerConversationToken) -> Self {
Self(token.to_string())
}
}
impl TryFrom<ServerConversationToken>
for session_sharing_protocol::common::ServerConversationToken
{
type Error = uuid::Error;
fn try_from(token: ServerConversationToken) -> Result<Self, Self::Error> {
token.as_str().parse()
}
}
#[derive(Debug, Clone)]
pub struct RequestParams {
pub input: Vec<AIAgentInput>,
pub conversation_token: Option<ServerConversationToken>,
pub forked_from_conversation_token: Option<ServerConversationToken>,
pub ambient_agent_task_id: Option<AmbientAgentTaskId>,
pub tasks: Vec<warp_multi_agent_api::Task>,
pub existing_suggestions: Option<Suggestions>,
pub metadata: Option<RequestMetadata>,
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 mcp_context: Option<MCPContext>,
pub planning_enabled: bool,
should_redact_secrets: bool,
/// User-provided API keys for AI providers (BYO API Key).
pub api_keys: Option<warp_multi_agent_api::request::settings::ApiKeys>,
pub allow_use_of_warp_credits_with_byok: 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<Vec<warp_multi_agent_api::ToolType>>,
/// 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<String>,
/// The conversation ID of the parent agent that spawned this child agent, if any.
pub parent_agent_id: Option<String>,
/// The display name for this agent (e.g. "Agent 1"), assigned by the orchestrator.
pub agent_name: Option<String>,
}
pub type Event = Result<warp_multi_agent_api::ResponseEvent, Arc<AIApiError>>;
#[cfg(not(target_family = "wasm"))]
pub type ResponseStream = Pin<Box<dyn Stream<Item = Event> + 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<Box<dyn Stream<Item = Event>>>;
#[derive(Debug, Clone)]
pub struct ConversationData {
pub id: AIConversationId,
pub tasks: Vec<warp_multi_agent_api::Task>,
pub server_conversation_token: Option<ServerConversationToken>,
pub forked_from_conversation_token: Option<ServerConversationToken>,
pub ambient_agent_task_id: Option<AmbientAgentTaskId>,
pub existing_suggestions: Option<Suggestions>,
}
impl RequestParams {
pub fn new(
terminal_view_id: Option<EntityId>,
session_context: SessionContext,
request_input: &RequestInput,
conversation: ConversationData,
metadata: Option<RequestMetadata>,
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<MCPServer> = 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::<Vec<_>>();
let tools = templatable_mcp_manager.tools().cloned().collect::<Vec<_>>();
#[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_keys = ApiKeyManager::as_ref(app).api_keys_for_request(
user_workspaces.is_byo_api_key_enabled(),
user_workspaces.is_bedrock_enabled(app),
);
let allow_use_of_warp_credits_with_byok =
*AISettings::as_ref(app).can_use_warp_credits_with_byok;
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)
&& session_context
.session_type()
.as_ref()
.is_none_or(|t| matches!(t, crate::terminal::model::session::SessionType::Local));
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,
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,
allow_use_of_warp_credits_with_byok,
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,
}
}
}