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.
|
||||
|
||||
@@ -901,6 +901,50 @@ pub struct OpenAIProviderConfig {
|
||||
|
||||
impl settings_value::SettingsValue for OpenAIProviderConfig {}
|
||||
|
||||
/// Cached metadata and runtime session options for an ACP agent.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
|
||||
pub struct AcpAgentSettings {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub version: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub icon_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub config_options: Vec<AcpConfigOptionSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
|
||||
pub struct AcpConfigOptionSettings {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub category: Option<String>,
|
||||
pub kind: String,
|
||||
#[serde(default)]
|
||||
pub current_value: serde_json::Value,
|
||||
#[serde(default)]
|
||||
pub options: Vec<AcpConfigValueSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
|
||||
pub struct AcpConfigValueSettings {
|
||||
pub value: serde_json::Value,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl settings_value::SettingsValue for AcpAgentSettings {}
|
||||
|
||||
// Nested ACP discovery data is intentionally persisted as one setting so refreshes are atomic.
|
||||
|
||||
define_settings_group!(AISettings, settings: [
|
||||
// If `false`, all AI features are disabled.
|
||||
is_any_ai_enabled: IsAnyAIEnabled {
|
||||
@@ -1270,6 +1314,17 @@ define_settings_group!(AISettings, settings: [
|
||||
description: "Arguments passed to the local Agent Client Protocol agent executable.",
|
||||
feature_flag: FeatureFlag::AgentClientProtocol,
|
||||
}
|
||||
// Cached ACP registry and runtime discovery data. Values are refreshed when the agent is queried.
|
||||
acp_agents: AcpAgents {
|
||||
type: Vec<AcpAgentSettings>,
|
||||
default: Vec::new(),
|
||||
supported_platforms: SupportedPlatforms::OR(SupportedPlatforms::MAC.into(), SupportedPlatforms::LINUX.into()),
|
||||
sync_to_cloud: SyncToCloud::Never,
|
||||
private: false,
|
||||
toml_path: "ai.acp.agents",
|
||||
description: "Cached ACP agent metadata, capabilities, and session configuration options.",
|
||||
feature_flag: FeatureFlag::AgentClientProtocol,
|
||||
}
|
||||
// Whether to use locally loaded AWS credentials for Bedrock-enabled requests.
|
||||
bedrock_enabled: BedrockEnabled {
|
||||
type: bool,
|
||||
|
||||
@@ -7824,6 +7824,17 @@ impl SettingsWidget for ACPSettingsWidget {
|
||||
is_enabled,
|
||||
app,
|
||||
));
|
||||
let discovered = settings.acp_agents.value();
|
||||
if let Some(agent) = discovered
|
||||
.iter()
|
||||
.find(|agent| agent.id.eq_ignore_ascii_case(settings.acp_agent_id.value()))
|
||||
{
|
||||
column.add_child(render_ai_setting_description(
|
||||
&format!("Discovered {} ACP configuration option(s) for {}. Options are refreshed from the running agent and cached in settings.toml.", agent.config_options.len(), agent.name),
|
||||
is_enabled,
|
||||
app,
|
||||
));
|
||||
}
|
||||
column.finish()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user