ACP Wrap up

This commit is contained in:
2026-08-05 08:10:41 -05:00
parent 2015498831
commit 993abb96df
23 changed files with 1459 additions and 1194 deletions
+25 -1
View File
@@ -188,13 +188,37 @@ impl BlocklistAIContextModel {
);
ctx.subscribe_to_model(&LLMPreferences::handle(ctx), |me, _, event, ctx| {
if let LLMPreferencesEvent::UpdatedActiveAgentModeLLM = event {
if matches!(
event,
LLMPreferencesEvent::UpdatedActiveAgentModeLLM
| LLMPreferencesEvent::UpdatedAvailableLLMs
) {
let llm_prefs = LLMPreferences::as_ref(ctx);
let vision_supported =
llm_prefs.vision_supported(ctx, Some(me.terminal_surface_id));
#[cfg(not(target_family = "wasm"))]
let desired_backend =
llm_prefs.agent_backend_for_active_model(Some(me.terminal_surface_id), ctx);
if !vision_supported {
me.clear_pending_images(ctx);
}
// ACP and provider histories have different owners. When the
// selected model crosses that boundary, make the next prompt a
// fresh conversation instead of silently sending it through
// the backend that owned the existing conversation.
#[cfg(not(target_family = "wasm"))]
{
let selected_backend = me
.selected_conversation(ctx)
.map(|conversation| conversation.agent_backend().clone());
if selected_backend.is_some_and(|backend| backend != desired_backend) {
me.set_pending_query_state_for_new_conversation(
AgentViewEntryOrigin::ConversationSelector,
ctx,
);
}
}
}
});
+9 -6
View File
@@ -818,7 +818,7 @@ impl BlocklistAIController {
if can_attempt_live_steering {
if let Some((stream_id, model_id)) = self
.in_flight_response_streams
.try_steer_acp_stream_for_conversation(conversation_id, query.clone(), ctx)
.try_steer_runtime_for_conversation(conversation_id, query.clone(), ctx)
{
ctx.emit(BlocklistAIControllerEvent::SentRequest {
contains_user_query: true,
@@ -3446,7 +3446,7 @@ impl BlocklistAIController {
Ok(api::StreamEvent::Response(event)) => {
// If this controller is part of a shared session, forward the entire response event to viewers first.
if FeatureFlag::AgentSharedSessions.is_enabled()
&& !response_stream.as_ref(ctx).is_acp()
&& response_stream.as_ref(ctx).supports_shared_session_sync()
{
let mut model = self.terminal_model.lock();
if model.shared_session_status().is_sharer() {
@@ -3527,7 +3527,9 @@ impl BlocklistAIController {
// After the stream finishes, persist the full message
// history (input + assistant response) from the Arc back
// into the conversation for the next request cycle.
let new_history = (!response_stream.as_ref(ctx).is_acp())
let new_history = response_stream
.as_ref(ctx)
.host_manages_history()
.then(|| response_stream.as_ref(ctx).messages_sent().clone())
.and_then(|messages_sent| {
messages_sent.lock().ok().and_then(|sent| {
@@ -3632,9 +3634,10 @@ impl BlocklistAIController {
const MAX_ERROR_RETRIES: usize = 2;
let retry_count =
self.error_retry_counts.entry(conversation_id).or_insert(0);
let should_corrective_retry = !response_stream.as_ref(ctx).is_acp()
&& is_corrective_retry_candidate
&& *retry_count < MAX_ERROR_RETRIES;
let should_corrective_retry =
response_stream.as_ref(ctx).allows_corrective_retries()
&& is_corrective_retry_candidate
&& *retry_count < MAX_ERROR_RETRIES;
if should_corrective_retry {
*retry_count += 1;
@@ -52,11 +52,11 @@ impl PendingResponseStreams {
.collect()
}
/// Attempts to inject a plain-text follow-up into the active ACP turn.
/// Attempts to inject a plain-text follow-up into an active steerable runtime.
///
/// Returning `None` leaves the caller free to use the normal
/// cancel-and-queue path without dropping the user's message.
pub fn try_steer_acp_stream_for_conversation(
pub fn try_steer_runtime_for_conversation(
&self,
conversation_id: AIConversationId,
display_text: String,
@@ -71,7 +71,7 @@ impl PendingResponseStreams {
let model_id = stream.as_ref(app).llm_id().clone();
stream
.as_ref(app)
.try_steer_acp(display_text)
.try_steer_runtime(display_text)
.then(|| (stream_id.clone(), model_id))
}
@@ -11,6 +11,7 @@ use ::local_control::remote_command::is_potential_remote_ssh_command;
use anyhow::anyhow;
use chrono::{DateTime, Local, TimeDelta};
use futures::channel::oneshot;
use galaxy_agent_core::RuntimeCapabilities;
#[cfg(not(target_family = "wasm"))]
use galaxy_agent_core::TurnCommand;
#[cfg(not(target_family = "wasm"))]
@@ -122,7 +123,7 @@ struct AcpRequestControl {
/// received yet, ensuring we don't retry after the AI has started executing actions.
pub struct ResponseStream {
id: ResponseStreamId,
agent_backend: AgentBackend,
runtime_capabilities: RuntimeCapabilities,
#[cfg(not(target_family = "wasm"))]
acp_session_metadata: Arc<Mutex<AcpSessionMetadata>>,
#[cfg(not(target_family = "wasm"))]
@@ -193,7 +194,7 @@ impl ResponseStream {
let (cancellation_tx, _rx) = oneshot::channel();
Self {
id,
agent_backend: AgentBackend::Provider,
runtime_capabilities: RuntimeCapabilities::provider(),
#[cfg(not(target_family = "wasm"))]
acp_session_metadata: Arc::new(Mutex::new(AcpSessionMetadata::default())),
#[cfg(not(target_family = "wasm"))]
@@ -439,6 +440,10 @@ impl ResponseStream {
let start_time = Local::now();
let request_id = Uuid::new_v4();
let runtime_capabilities = match &agent_backend {
AgentBackend::Provider => RuntimeCapabilities::provider(),
AgentBackend::Acp(_) => RuntimeCapabilities::session_runtime(),
};
#[cfg(not(target_family = "wasm"))]
let acp_session_metadata = Arc::new(Mutex::new(AcpSessionMetadata::default()));
#[cfg(not(target_family = "wasm"))]
@@ -489,7 +494,7 @@ impl ResponseStream {
}
Self {
id: ResponseStreamId(Uuid::new_v4().to_string()),
agent_backend,
runtime_capabilities,
#[cfg(not(target_family = "wasm"))]
acp_session_metadata,
#[cfg(not(target_family = "wasm"))]
@@ -516,13 +521,22 @@ impl ResponseStream {
&self.id
}
pub fn is_acp(&self) -> bool {
matches!(self.agent_backend, AgentBackend::Acp(_))
pub fn supports_shared_session_sync(&self) -> bool {
self.runtime_capabilities.shared_session_sync
}
pub fn host_manages_history(&self) -> bool {
self.runtime_capabilities.host_managed_history
}
pub fn allows_corrective_retries(&self) -> bool {
self.runtime_capabilities.corrective_retries
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn acp_session_metadata(&self) -> Option<AcpSessionMetadata> {
self.is_acp()
self.runtime_capabilities
.session_resume
.then(|| {
self.acp_session_metadata
.lock()
@@ -532,10 +546,10 @@ impl ResponseStream {
.flatten()
}
pub(super) fn try_steer_acp(&self, display_text: String) -> bool {
pub(super) fn try_steer_runtime(&self, display_text: String) -> bool {
#[cfg(not(target_family = "wasm"))]
{
if !self.is_acp()
if !self.runtime_capabilities.steering
|| self.current_request_id.is_none()
|| !self
.acp_session_metadata()
@@ -637,7 +651,10 @@ impl ResponseStream {
&self,
error: &Arc<crate::server::server_api::AIApiError>,
) -> bool {
if self.is_acp() || self.coding_model_fallback_attempted || self.has_received_client_actions
if !self.runtime_capabilities.model_selection
|| !self.runtime_capabilities.request_retries
|| self.coding_model_fallback_attempted
|| self.has_received_client_actions
{
return false;
}
@@ -820,7 +837,7 @@ impl ResponseStream {
let is_online = NetworkStatus::as_ref(ctx).is_online();
match recovery_action(
self.has_received_client_actions,
e.is_recoverable() && !self.is_acp(),
e.is_recoverable() && self.runtime_capabilities.request_retries,
self.retry_count < MAX_RETRIES,
self.can_attempt_resume_on_error,
is_online,
@@ -893,7 +910,7 @@ impl ResponseStream {
let is_online = NetworkStatus::as_ref(ctx).is_online();
match recovery_action(
self.has_received_client_actions,
unexpected_eof.is_recoverable() && !self.is_acp(),
unexpected_eof.is_recoverable() && self.runtime_capabilities.request_retries,
self.retry_count < MAX_RETRIES,
self.can_attempt_resume_on_error,
is_online,
+17 -8
View File
@@ -1183,6 +1183,7 @@ impl BlocklistAIHistoryModel {
}
fn configured_agent_backend(
terminal_surface_id: EntityId,
is_viewing_shared_session: bool,
is_cli_agent_transcript: bool,
ctx: &AppContext,
@@ -1200,6 +1201,11 @@ impl BlocklistAIHistoryModel {
return AgentBackend::Provider;
}
#[cfg(not(target_family = "wasm"))]
if let Some(llm_preferences) = ctx.try_get_singleton_model_as_ref::<LLMPreferences>() {
return llm_preferences.agent_backend_for_active_model(Some(terminal_surface_id), ctx);
}
let configured_agent_id = settings.acp_agent_id.value().trim();
let agent_id = if configured_agent_id.is_empty() {
"codex"
@@ -1224,12 +1230,6 @@ impl BlocklistAIHistoryModel {
.iter()
.find(|agent| agent.id.eq_ignore_ascii_case(agent_id))
.map(|agent| {
#[cfg(not(target_family = "wasm"))]
if let Some(selection) =
LLMPreferences::as_ref(ctx).selected_acp_config_for_agent(&agent.name, ctx)
{
return selection;
}
crate::ai::acp::AcpRuntimeModel::current_config_values(&agent.config_options)
})
.unwrap_or_default(),
@@ -1249,7 +1249,12 @@ impl BlocklistAIHistoryModel {
let Some(conversation) = self.conversation(&conversation_id) else {
return;
};
let Some(terminal_surface_id) = self.terminal_surface_id_for_conversation(&conversation_id)
else {
return;
};
let agent_backend = Self::configured_agent_backend(
terminal_surface_id,
conversation.is_viewing_shared_session(),
conversation.is_cli_agent_transcript(),
ctx,
@@ -1277,8 +1282,12 @@ impl BlocklistAIHistoryModel {
is_cli_agent_transcript: bool,
ctx: &mut ModelContext<Self>,
) -> AIConversationId {
let agent_backend =
Self::configured_agent_backend(is_viewing_shared_session, is_cli_agent_transcript, ctx);
let agent_backend = Self::configured_agent_backend(
terminal_surface_id,
is_viewing_shared_session,
is_cli_agent_transcript,
ctx,
);
let mut new_conversation = AIConversation::new_with_agent_backend(
is_viewing_shared_session,
is_cli_agent_transcript,