From 27692611471a0608f8b16b0787bfcfdd5e9a4289 Mon Sep 17 00:00:00 2001 From: Jerry Liu <953639086@qq.com> Date: Thu, 4 Jun 2026 20:21:30 +0800 Subject: [PATCH 1/2] fix(aionui-ai-agent): derive models from configOptions when models field is absent 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 session/new or session/load responses lack the models field but contain a "model" configOption, derive a SessionModelState from it so the frontend model selector renders correctly. Handles Ungrouped, Grouped, and non-exhaustive option variants. --- .../src/manager/acp/agent_session_flow.rs | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) 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 ee2a37057..7fb8662a5 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 @@ -6,7 +6,10 @@ use crate::protocol::events::{ }; 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::{ AgentErrorCode, AgentErrorOwnership, AgentErrorResolution, AgentErrorResolutionKind, AgentErrorResolutionTarget, }; @@ -27,6 +30,45 @@ pub(super) enum PromptOutcome { EmptyResponse { session_id: String, error: ErrorEventData }, } +/// 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 None; + } + return Some(SessionModelState::new(current_model_id, available)); + } + } + None +} + impl AcpAgentManager { /// Establish a fresh ACP session (session/new) and apply desired /// mode/model/config via reconcile. Does NOT send a prompt and @@ -43,6 +85,13 @@ impl AcpAgentManager { let mut session = self.session.write().await; if let Some(models) = session_response.models { session.apply_advertised_models(models); + } else if let Some(ref config_options) = session_response.config_options { + // `configOptions` with category "model" is the ACP-recommended + // way to advertise models; fall back to it when `models` is + // absent (e.g. OpenCode). + if let Some(models) = derive_models_from_config_options(config_options) { + session.apply_advertised_models(models); + } } if let Some(modes) = session_response.modes { session.apply_advertised_modes(modes); @@ -136,6 +185,10 @@ impl AcpAgentManager { let mut session = self.session.write().await; if let Some(models) = new_response.models { session.apply_advertised_models(models); + } else if let Some(ref config_options) = new_response.config_options { + if let Some(models) = derive_models_from_config_options(config_options) { + session.apply_advertised_models(models); + } } if let Some(modes) = new_response.modes { session.apply_advertised_modes(modes); @@ -189,6 +242,10 @@ impl AcpAgentManager { let mut session = self.session.write().await; if let Some(models) = load_response.models { session.apply_advertised_models(models); + } else if let Some(ref config_options) = load_response.config_options { + if let Some(models) = derive_models_from_config_options(config_options) { + session.apply_advertised_models(models); + } } if let Some(mut modes) = load_response.modes { if let Some(db_current) = preloaded_mode { From 9cb0e9035fb9a0c57069fbc17493985a42eacbb7 Mon Sep 17 00:00:00 2001 From: Jerry Liu <953639086@qq.com> Date: Thu, 4 Jun 2026 20:28:10 +0800 Subject: [PATCH 2/2] refactor(aionui-ai-agent): extract apply_advertised_models helper, fix clippy - Extract shared model-application logic into apply_advertised_models() to eliminate duplication across three session flows (new, resume, load) and prevent future drift. - Collapse nested if-let into && let to satisfy clippy::collapsible_if. - Continue scanning config_options when a model category yields an empty list, rather than returning None prematurely. --- .../src/manager/acp/agent_session_flow.rs | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) 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 7fb8662a5..b63e3859b 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 @@ -60,15 +60,31 @@ fn derive_models_from_config_options( // are treated as having no model entries. _ => Vec::new(), }; - if available.is_empty() { - return None; + if !available.is_empty() { + return Some(SessionModelState::new(current_model_id, available)); } - 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 @@ -83,16 +99,11 @@ impl AcpAgentManager { { let mut session = self.session.write().await; - if let Some(models) = session_response.models { - session.apply_advertised_models(models); - } else if let Some(ref config_options) = session_response.config_options { - // `configOptions` with category "model" is the ACP-recommended - // way to advertise models; fall back to it when `models` is - // absent (e.g. OpenCode). - if let Some(models) = derive_models_from_config_options(config_options) { - 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); } @@ -183,13 +194,11 @@ impl AcpAgentManager { { let mut session = self.session.write().await; - if let Some(models) = new_response.models { - session.apply_advertised_models(models); - } else if let Some(ref config_options) = new_response.config_options { - if let Some(models) = derive_models_from_config_options(config_options) { - 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); } @@ -240,13 +249,11 @@ impl AcpAgentManager { { let mut session = self.session.write().await; - if let Some(models) = load_response.models { - session.apply_advertised_models(models); - } else if let Some(ref config_options) = load_response.config_options { - if let Some(models) = derive_models_from_config_options(config_options) { - 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();