diff --git a/crates/aionui-ai-agent/src/manager/acp/agent_session_flow.rs b/crates/aionui-ai-agent/src/manager/acp/agent_session_flow.rs index 44c7386f..4eaadced 100644 --- a/crates/aionui-ai-agent/src/manager/acp/agent_session_flow.rs +++ b/crates/aionui-ai-agent/src/manager/acp/agent_session_flow.rs @@ -9,7 +9,10 @@ use crate::protocol::events::{ use crate::protocol::send_error::AgentSendError; use crate::shared_kernel::SessionId as DomainSessionId; use crate::types::SendMessageData; -use agent_client_protocol::schema::{ContentBlock, LoadSessionRequest, PromptRequest, SessionId, StopReason}; +use agent_client_protocol::schema::{ + ContentBlock, LoadSessionRequest, PromptRequest, SessionConfigKind, SessionConfigOptionCategory, + SessionConfigSelectOptions, SessionId, SessionModelState, StopReason, +}; use aionui_api_types::SlashCommandItem; use serde_json::Value; use tokio::sync::broadcast::error::TryRecvError; @@ -28,6 +31,61 @@ pub(super) enum PromptOutcome { WarningTip { session_id: String, tips: TipsEventData }, } +/// Per the ACP spec, `configOptions` with `category: "model"` is the +/// stable, recommended way for agents to advertise available models. +/// Some agents (e.g. OpenCode) use this exclusively and omit the +/// legacy `models` field. When `models` is absent, derive a +/// `SessionModelState` from the "model" config option so the frontend +/// can still render the model selector. +fn derive_models_from_config_options( + config_options: &[agent_client_protocol::schema::SessionConfigOption], +) -> Option { + use agent_client_protocol::schema::{ModelId as AcpModelId, ModelInfo, SessionConfigSelectGroup}; + for opt in config_options { + if opt.category.as_ref() != Some(&SessionConfigOptionCategory::Model) { + continue; + } + if let SessionConfigKind::Select(ref select) = opt.kind { + let current_model_id = AcpModelId::new(select.current_value.0.as_ref()); + let available: Vec = match &select.options { + SessionConfigSelectOptions::Ungrouped(options) => options + .iter() + .map(|o| ModelInfo::new(AcpModelId::new(o.value.0.as_ref()), o.name.clone())) + .collect(), + SessionConfigSelectOptions::Grouped(groups) => groups + .iter() + .flat_map(|g: &SessionConfigSelectGroup| &g.options) + .map(|o| ModelInfo::new(AcpModelId::new(o.value.0.as_ref()), o.name.clone())) + .collect(), + // `SessionConfigSelectOptions` is non-exhaustive; future variants + // are treated as having no model entries. + _ => Vec::new(), + }; + if !available.is_empty() { + return Some(SessionModelState::new(current_model_id, available)); + } + } + } + None +} + +/// Apply advertised models to the session, preferring the dedicated +/// `models` field and falling back to `configOptions` (category +/// "model") when absent. +fn apply_advertised_models( + session: &mut crate::manager::acp::session::AcpSession, + models: Option, + config_options: Option<&[agent_client_protocol::schema::SessionConfigOption]>, +) { + if let Some(models) = models { + session.apply_advertised_models(models); + } else if let Some(config_options) = config_options + && let Some(models) = derive_models_from_config_options(config_options) + { + session.apply_advertised_models(models); + } +} + impl AcpAgentManager { /// Establish a fresh ACP session (session/new) and apply desired /// mode/model/config via reconcile. Does NOT send a prompt and @@ -42,9 +100,11 @@ impl AcpAgentManager { { let mut session = self.session.write().await; - if let Some(models) = session_response.models { - session.apply_advertised_models(models); - } + apply_advertised_models( + &mut session, + session_response.models, + session_response.config_options.as_deref(), + ); if let Some(modes) = session_response.modes { session.apply_advertised_modes(modes); } @@ -134,9 +194,11 @@ impl AcpAgentManager { { let mut session = self.session.write().await; - if let Some(models) = new_response.models { - session.apply_advertised_models(models); - } + apply_advertised_models( + &mut session, + new_response.models, + new_response.config_options.as_deref(), + ); if let Some(modes) = new_response.modes { session.apply_advertised_modes(modes); } @@ -185,9 +247,11 @@ impl AcpAgentManager { { let mut session = self.session.write().await; - if let Some(models) = load_response.models { - session.apply_advertised_models(models); - } + apply_advertised_models( + &mut session, + load_response.models, + load_response.config_options.as_deref(), + ); if let Some(mut modes) = load_response.modes { if let Some(db_current) = preloaded_mode { modes.current_mode_id = db_current.into();