ACP work
This commit is contained in:
@@ -21,11 +21,12 @@ use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::todos::AIAgentTodoList;
|
||||
use crate::ai::agent::util::parse_markdown_into_text_and_code_sections;
|
||||
use crate::ai::agent::{
|
||||
AIAgentAction, AIAgentActionType, AIAgentAttachment, AIAgentCitation, AIAgentInput,
|
||||
AIAgentOutputMessage, AIAgentText, AIAgentTodo, ArtifactCreatedData, CloneRepositoryURL,
|
||||
MessageId, RunAgentsAgentRunConfig, RunAgentsExecutionMode, RunAgentsRequest,
|
||||
StartAgentExecutionMode, SubagentCall, SubagentType, SuggestedAgentModeWorkflow, SuggestedRule,
|
||||
Suggestions, SummarizationType, TodoOperation, UserQueryMode, WebFetchStatus, WebSearchStatus,
|
||||
runtime_activity, AIAgentAction, AIAgentActionType, AIAgentAttachment, AIAgentCitation,
|
||||
AIAgentInput, AIAgentOutputMessage, AIAgentText, AIAgentTodo, ArtifactCreatedData,
|
||||
CloneRepositoryURL, MessageId, RunAgentsAgentRunConfig, RunAgentsExecutionMode,
|
||||
RunAgentsRequest, StartAgentExecutionMode, SubagentCall, SubagentType,
|
||||
SuggestedAgentModeWorkflow, SuggestedRule, Suggestions, SummarizationType, TodoOperation,
|
||||
UserQueryMode, WebFetchStatus, WebSearchStatus,
|
||||
};
|
||||
use crate::ai::artifact_download::sanitized_basename;
|
||||
use crate::ai::document::ai_document_model::{AIDocumentId, AIDocumentVersion};
|
||||
@@ -272,10 +273,18 @@ impl ConvertAPIMessageToClientOutputMessage for api::Message {
|
||||
.collect::<Result<Vec<AIAgentCitation>, UnknownCitationTypeError>>()?;
|
||||
|
||||
match message {
|
||||
api::message::Message::AgentOutput(output) => Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::text(MessageId::new(self.id), output.into())
|
||||
.with_citations(citations),
|
||||
)),
|
||||
api::message::Message::AgentOutput(output) => {
|
||||
let message = if let Some(activity) =
|
||||
runtime_activity::decode(&self.server_message_data)
|
||||
{
|
||||
AIAgentOutputMessage::runtime_activity(MessageId::new(self.id), activity)
|
||||
} else {
|
||||
AIAgentOutputMessage::text(MessageId::new(self.id), output.into())
|
||||
};
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
message.with_citations(citations),
|
||||
))
|
||||
}
|
||||
api::message::Message::AgentReasoning(reasoning) => {
|
||||
let duration = reasoning
|
||||
.finished_duration
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use ai::agent::action::AskUserQuestionType;
|
||||
use ai::skills::{SkillPathOrigin, SkillReference};
|
||||
use galaxy_agent_core::{RuntimeActivity, RuntimeActivityStatus};
|
||||
use warp_multi_agent_api as api;
|
||||
use warp_util::local_or_remote_path::LocalOrRemotePath;
|
||||
|
||||
@@ -11,7 +12,8 @@ use super::{
|
||||
};
|
||||
use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::{
|
||||
AIAgentActionType, AIAgentOutputMessageType, LifecycleEventType, StartAgentExecutionMode,
|
||||
runtime_activity, AIAgentActionType, AIAgentOutputMessageType, LifecycleEventType,
|
||||
StartAgentExecutionMode,
|
||||
};
|
||||
|
||||
fn start_agent_tool_call_message(
|
||||
@@ -665,3 +667,45 @@ fn transfer_control_tool_call_converts_to_action_message() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_runtime_activity_converts_to_display_only_output() {
|
||||
let activity = RuntimeActivity {
|
||||
id: "tool-1".to_owned(),
|
||||
title: "List directories".to_owned(),
|
||||
status: Some(RuntimeActivityStatus::Completed),
|
||||
output: Some("payments\nrecords_v2".to_owned()),
|
||||
};
|
||||
let task_id = TaskId::new("task".to_owned());
|
||||
let message = api::Message {
|
||||
fetched_memories: Vec::new(),
|
||||
id: "message".to_owned(),
|
||||
task_id: "task".to_owned(),
|
||||
server_message_data: runtime_activity::encode(&activity).expect("metadata"),
|
||||
citations: Vec::new(),
|
||||
message: Some(api::message::Message::AgentOutput(
|
||||
api::message::AgentOutput {
|
||||
text: "fallback text".to_owned(),
|
||||
},
|
||||
)),
|
||||
request_id: "request".to_owned(),
|
||||
timestamp: None,
|
||||
};
|
||||
|
||||
let converted = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion");
|
||||
|
||||
let MaybeAIAgentOutputMessage::Message(output) = converted else {
|
||||
panic!("expected display output");
|
||||
};
|
||||
assert_eq!(
|
||||
output.message,
|
||||
AIAgentOutputMessageType::RuntimeActivity(activity)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ pub async fn generate_multi_agent_output(
|
||||
let err = Arc::new(crate::server::server_api::AIApiError::Stream {
|
||||
stream_type: "none",
|
||||
source: anyhow::anyhow!(
|
||||
"No AI provider configured. Enable Bedrock or OpenAI/LiteLLM in settings."
|
||||
"No AI runtime configured. Enable an agent runtime or model provider in settings."
|
||||
),
|
||||
});
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
|
||||
@@ -705,6 +705,27 @@ impl AIConversation {
|
||||
&self.agent_backend
|
||||
}
|
||||
|
||||
/// Updates the backend of a conversation that has not produced agent output.
|
||||
///
|
||||
/// Provider failures without output are safe to retry through a newly enabled runtime. Once
|
||||
/// any exchange has produced output, the backend remains stable so provider-native and
|
||||
/// ACP-owned histories are never mixed.
|
||||
pub(crate) fn set_agent_backend_if_no_output(&mut self, agent_backend: AgentBackend) -> bool {
|
||||
let can_change_backend = self.all_exchanges().iter().all(|exchange| {
|
||||
matches!(
|
||||
&exchange.output_status,
|
||||
AIAgentOutputStatus::Finished {
|
||||
finished_output: FinishedAIAgentOutput::Error { output: None, .. }
|
||||
}
|
||||
)
|
||||
});
|
||||
if !can_change_backend && self.agent_backend != agent_backend {
|
||||
return false;
|
||||
}
|
||||
self.agent_backend = agent_backend;
|
||||
true
|
||||
}
|
||||
|
||||
/// Records a resumable ACP session ID.
|
||||
///
|
||||
/// Returns `false` when called for a native provider conversation.
|
||||
|
||||
@@ -106,6 +106,15 @@ fn restored_conversation_with_queries(queries: &[&str]) -> AIConversation {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_backend_does_not_change_after_successful_output() {
|
||||
let mut conversation = restored_conversation_with_queries(&["Review this repository"]);
|
||||
|
||||
assert!(!conversation
|
||||
.set_agent_backend_if_no_output(AgentBackend::Acp(AcpConversationData::default())));
|
||||
assert_eq!(conversation.agent_backend(), &AgentBackend::Provider);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_user_query_returns_latest_non_empty_user_query() {
|
||||
let conversation =
|
||||
|
||||
@@ -7,6 +7,7 @@ pub(crate) mod comment;
|
||||
pub(crate) mod icons;
|
||||
pub(crate) mod linearization;
|
||||
pub(crate) mod redaction;
|
||||
pub(crate) mod runtime_activity;
|
||||
pub(crate) mod task;
|
||||
mod task_store;
|
||||
pub(super) mod telemetry;
|
||||
@@ -27,6 +28,7 @@ use ai::skills::ParsedSkill;
|
||||
use chrono::{DateTime, Local, TimeDelta};
|
||||
use comment::ReviewComment;
|
||||
use derivative::Derivative;
|
||||
use galaxy_agent_core::RuntimeActivity;
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use markdown_parser::{parse_markdown, FormattedTable, FormattedText, FormattedTextInline};
|
||||
@@ -619,6 +621,13 @@ impl AIAgentOutput {
|
||||
}
|
||||
}
|
||||
}
|
||||
AIAgentOutputMessageType::RuntimeActivity(activity) => {
|
||||
result.push(activity.title.clone());
|
||||
if let Some(output) = &activity.output {
|
||||
result.push(output.clone());
|
||||
}
|
||||
last_was_action = true;
|
||||
}
|
||||
AIAgentOutputMessageType::TodoOperation(operation) => {
|
||||
result.push(format!("{operation}"));
|
||||
last_was_action = false;
|
||||
@@ -1805,6 +1814,10 @@ pub enum AIAgentOutputMessageType {
|
||||
token_count: Option<u32>,
|
||||
},
|
||||
Subagent(SubagentCall),
|
||||
/// Display-only activity executed and owned by an external agent runtime.
|
||||
/// Unlike [`AIAgentOutputMessageType::Action`], Galaxy must never dispatch
|
||||
/// this activity through its action executor.
|
||||
RuntimeActivity(RuntimeActivity),
|
||||
Action(AIAgentAction),
|
||||
TodoOperation(TodoOperation),
|
||||
WebSearch(WebSearchStatus),
|
||||
@@ -1972,6 +1985,12 @@ impl Display for AIAgentOutputMessage {
|
||||
}
|
||||
}
|
||||
AIAgentOutputMessageType::Action(action) => write!(f, "Action: {action}")?,
|
||||
AIAgentOutputMessageType::RuntimeActivity(activity) => {
|
||||
write!(f, "Runtime activity: {}", activity.title)?;
|
||||
if let Some(output) = &activity.output {
|
||||
write!(f, "\n{output}")?;
|
||||
}
|
||||
}
|
||||
AIAgentOutputMessageType::TodoOperation(todo) => write!(f, "Todo: {todo}")?,
|
||||
AIAgentOutputMessageType::Subagent(subagent) => write!(f, "Subagent: {subagent}")?,
|
||||
AIAgentOutputMessageType::WebSearch(status) => match status {
|
||||
@@ -2044,6 +2063,14 @@ impl AIAgentOutputMessage {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runtime_activity(id: MessageId, activity: RuntimeActivity) -> Self {
|
||||
Self {
|
||||
id,
|
||||
message: AIAgentOutputMessageType::RuntimeActivity(activity),
|
||||
citations: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(id: MessageId, text: AIAgentText) -> Self {
|
||||
Self {
|
||||
id,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
use galaxy_agent_core::RuntimeActivity;
|
||||
|
||||
const SERVER_MESSAGE_DATA_PREFIX: &str = "galaxy:runtime-activity:v1:";
|
||||
|
||||
pub(crate) fn encode(activity: &RuntimeActivity) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(activity).map(|json| format!("{SERVER_MESSAGE_DATA_PREFIX}{json}"))
|
||||
}
|
||||
|
||||
pub(crate) fn decode(server_message_data: &str) -> Option<RuntimeActivity> {
|
||||
let json = server_message_data.strip_prefix(SERVER_MESSAGE_DATA_PREFIX)?;
|
||||
serde_json::from_str(json).ok()
|
||||
}
|
||||
Reference in New Issue
Block a user