ACP Wrap up
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user