Document process monitoring handoff
Add ACP discovery and configuration support
This commit is contained in:
@@ -7,6 +7,22 @@ pub(crate) fn acp_model_id(agent_id: &str) -> String {
|
||||
format!("acp:{}", agent_id.trim().to_ascii_lowercase())
|
||||
}
|
||||
|
||||
pub(crate) fn acp_selection_model_id(
|
||||
agent_id: &str,
|
||||
values: &std::collections::BTreeMap<String, serde_json::Value>,
|
||||
) -> String {
|
||||
let suffix = values
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{key}={value}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
if suffix.is_empty() {
|
||||
acp_model_id(agent_id)
|
||||
} else {
|
||||
format!("{}:{suffix}", acp_model_id(agent_id))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn acp_launch_fingerprint(
|
||||
agent_id: &str,
|
||||
custom_command: &str,
|
||||
|
||||
@@ -12,8 +12,8 @@ mod runtime_model;
|
||||
mod transport;
|
||||
|
||||
pub(crate) use launch::{
|
||||
acp_launch_fingerprint, acp_model_id, resolve_acp_launch, validate_acp_dispatch,
|
||||
validate_acp_launch_identity,
|
||||
acp_launch_fingerprint, acp_model_id, acp_selection_model_id, resolve_acp_launch,
|
||||
validate_acp_dispatch, validate_acp_launch_identity,
|
||||
};
|
||||
pub(crate) use permissions::resolve_acp_permissions;
|
||||
pub(crate) use runtime_model::AcpRuntimeModel;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use crate::settings::{
|
||||
AISettings, AcpAgentSettings, AcpConfigOptionSettings, AcpConfigValueSettings,
|
||||
};
|
||||
use galaxy_acp::{AcpLaunchConfig, AcpManagerConfig, AcpSessionManager};
|
||||
use galaxyui::{Entity, ModelContext, SingletonEntity};
|
||||
|
||||
@@ -33,6 +36,70 @@ impl AcpRuntimeModel {
|
||||
});
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_config_options(
|
||||
options: Vec<galaxy_acp::SessionConfigOption>,
|
||||
) -> Vec<AcpConfigOptionSettings> {
|
||||
options
|
||||
.into_iter()
|
||||
.map(|option| {
|
||||
let kind = match &option.kind {
|
||||
galaxy_acp::SessionConfigOptionType::Select => "select",
|
||||
galaxy_acp::SessionConfigOptionType::Boolean => "boolean",
|
||||
}
|
||||
.to_owned();
|
||||
let current_value = serde_json::to_value(&option.current_value).unwrap_or_default();
|
||||
let values = option
|
||||
.options
|
||||
.into_iter()
|
||||
.map(|value| AcpConfigValueSettings {
|
||||
value: serde_json::to_value(value.value).unwrap_or_default(),
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
})
|
||||
.collect();
|
||||
AcpConfigOptionSettings {
|
||||
id: option.id,
|
||||
name: option.name,
|
||||
description: option.description,
|
||||
category: option
|
||||
.category
|
||||
.map(|category| format!("{category:?}").to_lowercase()),
|
||||
kind,
|
||||
current_value,
|
||||
options: values,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn upsert_agent_settings(
|
||||
settings: &mut AISettings,
|
||||
agent_id: &str,
|
||||
options: Vec<galaxy_acp::SessionConfigOption>,
|
||||
) -> Result<(), String> {
|
||||
let config_options = Self::normalize_config_options(options);
|
||||
let mut agents = settings.acp_agents.value().clone();
|
||||
if let Some(agent) = agents
|
||||
.iter_mut()
|
||||
.find(|agent| agent.id.eq_ignore_ascii_case(agent_id))
|
||||
{
|
||||
agent.config_options = config_options;
|
||||
} else {
|
||||
agents.push(AcpAgentSettings {
|
||||
id: agent_id.to_owned(),
|
||||
name: agent_id.to_owned(),
|
||||
version: None,
|
||||
description: None,
|
||||
icon_url: None,
|
||||
capabilities: Vec::new(),
|
||||
config_options,
|
||||
});
|
||||
}
|
||||
settings
|
||||
.acp_agents
|
||||
.set_value(agents, &mut settings.context())
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for AcpRuntimeModel {
|
||||
|
||||
@@ -8,11 +8,11 @@ use futures::stream::FusedStream as _;
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use galaxy_acp::{
|
||||
AcpEvent, AcpPermissionPolicy, AcpRuntimeError, AcpSessionHandle, AcpSessionManager,
|
||||
AcpSteeringOutcome, AcpTurnRequest, ContentBlock, McpServer, McpServerStdio, SessionId,
|
||||
TextContent,
|
||||
AcpSteeringOutcome, AcpTurnRequest, ContentBlock, McpServer, McpServerStdio,
|
||||
SessionConfigOptionValue, SessionId, TextContent,
|
||||
};
|
||||
|
||||
use super::launch::acp_model_id;
|
||||
use super::launch::acp_selection_model_id;
|
||||
use super::prompt::{prompt_content, GalaxyTerminalTools};
|
||||
use super::response_translator::AcpResponseTranslator;
|
||||
use crate::ai::agent::api::{self, RequestParams};
|
||||
@@ -25,6 +25,7 @@ pub(crate) struct AcpSessionMetadata {
|
||||
pub(crate) session_id: Option<String>,
|
||||
pub(crate) can_load: bool,
|
||||
pub(crate) can_steer: bool,
|
||||
pub(crate) config_options: Vec<galaxy_acp::SessionConfigOption>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -121,6 +122,15 @@ pub(crate) async fn acp_output_stream(
|
||||
mcp_servers.push(server);
|
||||
}
|
||||
let request = AcpTurnRequest {
|
||||
config_values: backend
|
||||
.config_values
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| {
|
||||
serde_json::from_value::<SessionConfigOptionValue>(value)
|
||||
.ok()
|
||||
.map(|value| (key, value))
|
||||
})
|
||||
.collect(),
|
||||
conversation_key: conversation_id,
|
||||
session_id: backend.session_id.map(SessionId::from),
|
||||
cwd,
|
||||
@@ -222,6 +232,7 @@ pub(crate) async fn acp_output_stream(
|
||||
session_id,
|
||||
can_load,
|
||||
can_steer,
|
||||
..
|
||||
} = &event
|
||||
{
|
||||
if let Ok(mut metadata) = session_metadata.lock() {
|
||||
@@ -230,6 +241,11 @@ pub(crate) async fn acp_output_stream(
|
||||
metadata.can_steer = *can_steer;
|
||||
}
|
||||
}
|
||||
if let AcpEvent::ConfigOptions { options } = &event {
|
||||
if let Ok(mut metadata) = session_metadata.lock() {
|
||||
metadata.config_options = options.clone();
|
||||
}
|
||||
}
|
||||
match translator.translate(event) {
|
||||
Ok(response_events) => {
|
||||
for response_event in response_events {
|
||||
@@ -272,7 +288,7 @@ fn response_translator(
|
||||
task_id,
|
||||
params.tasks.is_empty(),
|
||||
user_query,
|
||||
acp_model_id(&backend.agent_id),
|
||||
acp_selection_model_id(&backend.agent_id, &backend.config_values),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1220,6 +1220,22 @@ impl BlocklistAIHistoryModel {
|
||||
agent_id: agent_id.to_string(),
|
||||
launch_fingerprint,
|
||||
session_id: None,
|
||||
config_values: settings
|
||||
.acp_agents
|
||||
.value()
|
||||
.iter()
|
||||
.find(|agent| agent.id.eq_ignore_ascii_case(agent_id))
|
||||
.and_then(|agent| {
|
||||
agent
|
||||
.config_options
|
||||
.iter()
|
||||
.find(|option| option.category.as_deref() == Some("model"))
|
||||
})
|
||||
.map(|option| {
|
||||
std::iter::once((option.id.clone(), option.current_value.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
} else {
|
||||
AgentBackend::Provider
|
||||
|
||||
+54
-1
@@ -21,7 +21,7 @@ use crate::auth::auth_manager::{AuthManager, AuthManagerEvent};
|
||||
use crate::auth::AuthStateProvider;
|
||||
use crate::network::{NetworkStatus, NetworkStatusEvent, NetworkStatusKind};
|
||||
use crate::server::server_api::ServerApiProvider;
|
||||
use crate::settings::{BedrockModelConfig, OpenAIModelConfig};
|
||||
use crate::settings::{AcpAgentSettings, BedrockModelConfig, OpenAIModelConfig};
|
||||
use crate::user_config::{WarpConfig, WarpConfigUpdateEvent};
|
||||
use crate::workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent};
|
||||
use crate::{report_error, AISettings};
|
||||
@@ -922,6 +922,7 @@ impl LLMPreferences {
|
||||
self.openai_provider_routing.clear();
|
||||
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
self.inject_acp_models(ctx);
|
||||
if !*settings.openai_enabled.value() {
|
||||
return;
|
||||
}
|
||||
@@ -1034,6 +1035,58 @@ impl LLMPreferences {
|
||||
log::info!("[openai/litellm] Injected {total_injected} model(s) into available choices");
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn inject_acp_models(&mut self, ctx: &AppContext) {
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
for agent in settings.acp_agents.value() {
|
||||
let model_option = agent
|
||||
.config_options
|
||||
.iter()
|
||||
.find(|option| option.category.as_deref() == Some("model"));
|
||||
let Some(model_option) = model_option else { continue };
|
||||
let secondary = agent.config_options.iter().filter(|option| {
|
||||
matches!(option.category.as_deref(), Some("mode") | Some("thought_level"))
|
||||
});
|
||||
for value in &model_option.options {
|
||||
|
||||
let suffix = secondary
|
||||
.clone()
|
||||
.filter_map(|option| option.options.first().map(|v| v.name.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let display_name = if suffix.is_empty() {
|
||||
value.name.clone()
|
||||
} else {
|
||||
format!("{} ({})", value.name, suffix.join(", "))
|
||||
};
|
||||
let id = format!("acp:{}:{}={}", agent.id, model_option.id, value.value);
|
||||
let info = LLMInfo {
|
||||
id: LLMId::from(id.as_str()),
|
||||
display_name,
|
||||
base_model_name: value.name.clone(),
|
||||
reasoning_level: None,
|
||||
usage_metadata: LLMUsageMetadata {
|
||||
request_multiplier: 1,
|
||||
credit_multiplier: None,
|
||||
},
|
||||
description: Some(agent.name.clone()),
|
||||
disable_reason: None,
|
||||
vision_supported: false,
|
||||
spec: None,
|
||||
provider: LLMProvider::Unknown,
|
||||
host_configs: HashMap::new(),
|
||||
discount_percentage: None,
|
||||
context_window: LLMContextWindow::default(),
|
||||
};
|
||||
self.models_by_feature.agent_mode.choices.push(info.clone());
|
||||
self.models_by_feature.coding.choices.push(info.clone());
|
||||
if let Some(ref mut cli) = self.models_by_feature.cli_agent {
|
||||
cli.choices.push(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures the default model ID in each feature's choices still points to
|
||||
/// an existing entry. If the default was removed (e.g. provider disabled),
|
||||
/// switch to the first remaining choice.
|
||||
|
||||
Reference in New Issue
Block a user