Add ACP agent backend and terminal controls

This commit is contained in:
2026-07-30 07:25:11 -05:00
parent dbfa8bcd48
commit ad24374f6d
84 changed files with 12151 additions and 157 deletions
+18
View File
@@ -92,6 +92,9 @@ impl TryFrom<ServerConversationToken>
#[derive(Debug, Clone)]
pub struct RequestParams {
/// Galaxy terminal view that originated the request. This is retained
/// locally so ACP-provided Galaxy tools can be pinned to the exact pane.
pub terminal_view_id: Option<EntityId>,
pub input: Vec<AIAgentInput>,
pub conversation_token: Option<ServerConversationToken>,
pub forked_from_conversation_token: Option<ServerConversationToken>,
@@ -182,6 +185,7 @@ impl RequestParams {
#[cfg(test)]
pub fn new_for_test() -> Self {
Self {
terminal_view_id: None,
input: vec![],
conversation_token: None,
forked_from_conversation_token: None,
@@ -222,6 +226,19 @@ impl RequestParams {
}
}
/// Applies the request's configured secret-redaction policy to text immediately
/// before it is handed to an external model or agent.
pub(crate) fn redact_text_for_model(&self, text: &mut String) {
if self.should_redact_secrets {
super::redaction::redact_secrets(text);
}
}
#[cfg(test)]
pub(crate) fn enable_secret_redaction_for_test(&mut self) {
self.should_redact_secrets = true;
}
pub fn new(
terminal_view_id: Option<EntityId>,
session_context: SessionContext,
@@ -372,6 +389,7 @@ impl RequestParams {
.context_window_limit_for_request(app);
Self {
terminal_view_id,
input: request_input.all_inputs().cloned().collect(),
conversation_token: conversation.server_conversation_token,
forked_from_conversation_token: conversation.forked_from_conversation_token,
@@ -71,6 +71,7 @@ pub fn convert_conversation_data_to_ai_conversation(
let agent_conversation_data = match restoration_mode {
RestorationMode::Fork => AgentConversationData {
agent_backend: Default::default(),
server_conversation_token: None,
conversation_usage_metadata: usage_metadata,
reverted_action_ids: None,
@@ -93,6 +94,7 @@ pub fn convert_conversation_data_to_ai_conversation(
messages_summarized_up_to: 0,
},
RestorationMode::Continue => AgentConversationData {
agent_backend: Default::default(),
server_conversation_token: Some(
metadata.server_conversation_token.as_str().to_string(),
),
+1
View File
@@ -12,6 +12,7 @@ fn request_params_with_ask_user_question_enabled(ask_user_question_enabled: bool
let model = LLMId::from("test-model");
RequestParams {
terminal_view_id: None,
input: vec![],
conversation_token: None,
forked_from_conversation_token: None,
+46 -4
View File
@@ -62,8 +62,8 @@ use crate::ai::skills::SkillDescriptor;
use crate::code_review::CodeReviewTelemetryEvent;
use crate::notebooks::NotebookId;
use crate::persistence::model::{
AgentConversationData, ContextWindowSegment, ConversationUsageMetadata, ModelTokenUsage,
PersistedAutoexecuteMode, ToolUsageMetadata,
AcpConversationData, AgentBackend, AgentConversationData, ContextWindowSegment,
ConversationUsageMetadata, ModelTokenUsage, PersistedAutoexecuteMode, ToolUsageMetadata,
};
use crate::persistence::ModelEvent;
use crate::server::ids::ServerId;
@@ -226,6 +226,9 @@ pub struct AIConversation {
/// credits spent, token usage, and tool usage.
conversation_usage_metadata: ConversationUsageMetadata,
/// Runtime responsible for executing this conversation.
agent_backend: AgentBackend,
/// The server-generated unique "token" for this conversation.
///
/// This must be roundtripped to the server when sending follow-ups within a given conversation.
@@ -351,6 +354,18 @@ pub(crate) fn artifact_from_fork_proto(
impl AIConversation {
pub fn new(is_viewing_shared_session: bool, is_cli_agent_transcript: bool) -> Self {
Self::new_with_agent_backend(
is_viewing_shared_session,
is_cli_agent_transcript,
AgentBackend::default(),
)
}
pub fn new_with_agent_backend(
is_viewing_shared_session: bool,
is_cli_agent_transcript: bool,
agent_backend: AgentBackend,
) -> Self {
let root_task = Task::new_optimistic_root();
Self {
id: AIConversationId::new(),
@@ -364,6 +379,7 @@ impl AIConversation {
status_error: None,
has_opened_code_review: false,
conversation_usage_metadata: ConversationUsageMetadata::default(),
agent_backend,
server_conversation_token: None,
task_id: None,
forked_from_server_conversation_token: None,
@@ -522,6 +538,7 @@ impl AIConversation {
};
let (
agent_backend,
server_conversation_token,
forked_from_server_conversation_token,
conversation_usage_metadata,
@@ -571,6 +588,7 @@ impl AIConversation {
AIConversationAutoexecuteMode::default()
};
(
data.agent_backend,
server_conversation_token,
forked_from_server_conversation_token,
conversation_usage_metadata,
@@ -590,6 +608,7 @@ impl AIConversation {
)
} else {
(
AgentBackend::default(),
None,
None,
ConversationUsageMetadata::default(),
@@ -643,6 +662,7 @@ impl AIConversation {
code_review: None,
has_opened_code_review: false,
conversation_usage_metadata,
agent_backend,
server_conversation_token,
task_id: run_id.as_deref().and_then(|id| id.parse().ok()),
forked_from_server_conversation_token,
@@ -681,6 +701,25 @@ impl AIConversation {
self.id
}
pub fn agent_backend(&self) -> &AgentBackend {
&self.agent_backend
}
/// Records a resumable ACP session ID.
///
/// Returns `false` when called for a native provider conversation.
pub fn set_acp_session_id(&mut self, session_id: impl Into<String>) -> bool {
let AgentBackend::Acp(AcpConversationData {
session_id: current_session_id,
..
}) = &mut self.agent_backend
else {
return false;
};
*current_session_id = Some(session_id.into());
true
}
pub fn current_context_tokens(&self) -> u32 {
self.current_context_tokens
}
@@ -2179,8 +2218,10 @@ impl AIConversation {
});
}
self.server_conversation_token =
Some(ServerConversationToken::new(init_event.conversation_id));
if matches!(self.agent_backend, AgentBackend::Provider) {
self.server_conversation_token =
Some(ServerConversationToken::new(init_event.conversation_id));
}
let run_id = Some(init_event.run_id).filter(|s| !s.is_empty());
self.task_id = run_id.as_deref().and_then(|id| id.parse().ok());
Ok(())
@@ -3782,6 +3823,7 @@ impl AIConversation {
.filter_map(|task| task.source_for_persistence())
.collect(),
conversation_data: AgentConversationData {
agent_backend: self.agent_backend.clone(),
server_conversation_token: self
.server_conversation_token
.clone()
+52 -1
View File
@@ -8,7 +8,7 @@ use super::{
ConversationStatus, RestoreConversationError,
};
use crate::ai::artifacts::Artifact;
use crate::persistence::model::AgentConversationData;
use crate::persistence::model::{AcpConversationData, AgentBackend, AgentConversationData};
fn restored_conversation(conversation_data: Option<AgentConversationData>) -> AIConversation {
AIConversation::new_restored(
@@ -206,6 +206,57 @@ fn restored_conversation_uses_persisted_remote_child_marker() {
assert!(conversation.is_remote_child());
}
#[test]
fn new_conversations_default_to_provider_backend() {
let conversation = AIConversation::new(false, false);
assert_eq!(conversation.agent_backend(), &AgentBackend::Provider);
}
#[test]
fn acp_session_id_is_updated_only_for_acp_conversations() {
let mut acp_conversation = AIConversation::new_with_agent_backend(
false,
false,
AgentBackend::Acp(AcpConversationData {
agent_id: "codex-acp".to_string(),
launch_fingerprint: "launch-123".to_string(),
session_id: None,
}),
);
assert!(acp_conversation.set_acp_session_id("session-123"));
assert_eq!(
acp_conversation.agent_backend(),
&AgentBackend::Acp(AcpConversationData {
agent_id: "codex-acp".to_string(),
launch_fingerprint: "launch-123".to_string(),
session_id: Some("session-123".to_string()),
})
);
let mut provider_conversation = AIConversation::new(false, false);
assert!(!provider_conversation.set_acp_session_id("ignored"));
assert_eq!(
provider_conversation.agent_backend(),
&AgentBackend::Provider
);
}
#[test]
fn restored_conversation_uses_persisted_acp_backend() {
let backend = AgentBackend::Acp(AcpConversationData {
agent_id: "codex-acp".to_string(),
launch_fingerprint: "launch-123".to_string(),
session_id: Some("session-123".to_string()),
});
let conversation = restored_conversation(Some(AgentConversationData {
agent_backend: backend.clone(),
..Default::default()
}));
assert_eq!(conversation.agent_backend(), &backend);
}
#[test]
fn child_conversation_detection_uses_parent_agent_id() {
let conversation_data: AgentConversationData = serde_json::from_str(
+6
View File
@@ -33,6 +33,7 @@ pub(crate) fn redact_inputs(inputs: &mut [AIAgentInput]) {
query,
context,
referenced_attachments,
running_command,
..
} => {
redact_secrets(query);
@@ -40,6 +41,11 @@ pub(crate) fn redact_inputs(inputs: &mut [AIAgentInput]) {
referenced_attachments
.values_mut()
.for_each(redact_attachment);
if let Some(running_command) = running_command {
redact_secrets(&mut running_command.command);
redact_secrets(&mut running_command.grid_contents);
redact_secrets(&mut running_command.cursor);
}
}
AIAgentInput::AutoCodeDiffQuery { query, context, .. } => {
redact_secrets(query);