Preserve completed command assessments

This commit is contained in:
2026-08-13 16:41:19 -05:00
parent 934cddc063
commit 5737f8342c
29 changed files with 864 additions and 94 deletions
+10 -5
View File
@@ -23,6 +23,7 @@ use crate::ai::agent::api::convert_from::{
convert_user_query_mode, ConversionParams, ConvertAPIMessageToClientOutputMessage,
MaybeAIAgentOutputMessage,
};
use crate::ai::agent::api::is_internal_command_completion_assessment;
use crate::ai::agent::conversation::{
update_todo_list_from_todo_op, AIConversation, AIConversationId, ServerAIConversationMetadata,
};
@@ -389,17 +390,21 @@ impl ConvertToExchanges for &api::Task {
let added_message_as_exchange_input = match message {
api::message::Message::UserQuery(user_query) => {
// Add user query as input
current_inputs.push(AIAgentInput::UserQuery {
if is_internal_command_completion_assessment(api_message) {
false
} else {
// Add user query as input
current_inputs.push(AIAgentInput::UserQuery {
query: user_query.query.clone(),
context: convert_input_context(user_query.context.as_ref()),
static_query_type: None,
referenced_attachments: HashMap::new(),
user_query_mode: convert_user_query_mode(user_query.mode.as_ref()),
running_command: None,
intended_agent: Some(user_query.intended_agent()),
});
true
intended_agent: Some(user_query.intended_agent()),
});
true
}
}
api::message::Message::SystemQuery(query) => {
let Some(query_type) = &query.r#type else {
@@ -4,7 +4,7 @@ use chrono::Utc;
use warp_multi_agent_api as api;
use crate::ai::agent::api::convert_conversation::*;
use crate::ai::agent::api::ServerConversationToken;
use crate::ai::agent::api::{mark_internal_command_completion_assessment, ServerConversationToken};
use crate::ai::agent::conversation::{
AIAgentHarness, AIConversationId, ServerAIConversationMetadata,
};
@@ -2129,6 +2129,61 @@ fn test_create_then_edit_then_create_version_tracking() {
);
}
#[test]
fn test_internal_command_completion_assessment_restores_output_without_visible_input() {
let assessment_text =
"[Completed command: cargo test]\n[Final terminal output:\ntest result: ok\n]";
let mut hidden_assessment = api::Message {
id: "msg_assessment".to_string(),
task_id: "task1".to_string(),
request_id: "req1".to_string(),
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: assessment_text.to_string(),
..Default::default()
})),
..Default::default()
};
mark_internal_command_completion_assessment(&mut hidden_assessment);
let provider_history =
crate::ai::bedrock::request_translator::convert_proto_message(&hidden_assessment)
.expect("hidden assessment should remain in provider history");
assert_eq!(
provider_history.role,
crate::ai::provider::types::MessageRole::User
);
assert!(matches!(
provider_history.content,
crate::ai::provider::types::MessageContent::Text(text) if text == assessment_text
));
let task = api::Task {
id: "task1".to_string(),
messages: vec![
hidden_assessment,
api::Message {
id: "msg_output".to_string(),
task_id: "task1".to_string(),
request_id: "req1".to_string(),
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "The command completed successfully.".to_string(),
},
)),
..Default::default()
},
],
..Default::default()
};
let exchanges = task.into_exchanges();
assert_eq!(exchanges.len(), 1);
assert!(exchanges[0].input.is_empty());
assert_eq!(
exchanges[0].format_output_for_copy(None),
"The command completed successfully."
);
}
/// Verify that a `SystemQuery::HandoffRehydration` message does not produce
/// a displayed input when restoring a conversation. It must be treated as
/// hidden, so the exchange should have zero user-visible inputs.
+4
View File
@@ -16,6 +16,7 @@ use warp_multi_agent_api as api;
use crate::ai::agent::api::convert_conversation::{
convert_input_context, convert_tool_call_result_to_input,
};
use crate::ai::agent::api::is_internal_command_completion_assessment;
use crate::ai::agent::comment::CodeReview;
use crate::ai::agent::task::TaskId;
use crate::ai::agent::todos::AIAgentTodoList;
@@ -957,6 +958,9 @@ pub fn user_inputs_from_messages(messages: &[api::Message]) -> Vec<AIAgentInput>
let Some(inner) = &m.message else { continue };
match inner {
api::message::Message::UserQuery(uq) => {
if is_internal_command_completion_assessment(m) {
continue;
}
let context = convert_input_context(uq.context.as_ref());
let referenced_attachments = uq
.referenced_attachments
+35 -4
View File
@@ -7,13 +7,14 @@ use warp_multi_agent_api as api;
use warp_util::local_or_remote_path::LocalOrRemotePath;
use super::{
convert_api_question, ConversionParams, ConvertAPIMessageToClientOutputMessage,
MaybeAIAgentOutputMessage,
convert_api_question, user_inputs_from_messages, ConversionParams,
ConvertAPIMessageToClientOutputMessage, MaybeAIAgentOutputMessage,
};
use crate::ai::agent::api::mark_internal_command_completion_assessment;
use crate::ai::agent::task::TaskId;
use crate::ai::agent::{
runtime_activity, AIAgentActionType, AIAgentOutputMessageType, LifecycleEventType,
StartAgentExecutionMode,
runtime_activity, AIAgentActionType, AIAgentInput, AIAgentOutputMessageType,
LifecycleEventType, StartAgentExecutionMode,
};
fn start_agent_tool_call_message(
@@ -617,6 +618,36 @@ fn converts_local_start_agent_v2_with_harness_type() {
assert_eq!(lifecycle_subscription, None);
}
#[test]
fn internal_command_completion_assessment_is_not_restored_as_shared_user_input() {
let mut hidden_assessment = api::Message {
id: "hidden-assessment".to_string(),
task_id: "task".to_string(),
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "[Completed command: cargo test]".to_string(),
..Default::default()
})),
..Default::default()
};
mark_internal_command_completion_assessment(&mut hidden_assessment);
let visible_query = api::Message {
id: "visible-query".to_string(),
task_id: "task".to_string(),
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "What changed?".to_string(),
..Default::default()
})),
..Default::default()
};
let inputs = user_inputs_from_messages(&[hidden_assessment, visible_query]);
assert_eq!(inputs.len(), 1);
assert!(matches!(
&inputs[0],
AIAgentInput::UserQuery { query, .. } if query == "What changed?"
));
}
#[test]
fn transfer_control_tool_call_converts_to_action_message() {
let task_id = TaskId::new("task".to_string());
+37
View File
@@ -348,6 +348,43 @@ fn convert_input_to_user_input(
}
))
}
AIAgentInput::CommandCompletionAssessment {
prompt,
completed_command:
RunningCommand {
command,
block_id,
grid_contents: output,
cursor,
requested_command_id,
is_alt_screen_active,
},
..
} => Ok(
api::request::input::user_inputs::user_input::Input::CliAgentUserQuery(
api::request::input::CliAgentUserQuery {
user_query: Some(api::request::input::UserQuery {
query: prompt,
referenced_attachments: Default::default(),
mode: Some(UserQueryMode::Normal.into()),
intended_agent: api::AgentType::Primary.into(),
}),
running_command: Some(api::RunningShellCommand {
command,
snapshot: Some(api::LongRunningShellCommandSnapshot {
output,
cursor,
command_id: block_id.as_str().to_owned(),
is_alt_screen_active,
is_preempted: false,
}),
}),
run_shell_command_tool_call_id: requested_command_id
.map(|id| id.to_string())
.unwrap_or_default(),
},
),
),
AIAgentInput::ActionResult { result, .. } => result.try_into(),
AIAgentInput::MessagesReceivedFromAgents { messages } => Ok(
api::request::input::user_inputs::user_input::Input::MessagesReceivedFromAgents(
+82 -2
View File
@@ -1,11 +1,13 @@
use std::sync::Arc;
use chrono::{DateTime, Utc};
use galaxy_core::command::ExitCode;
use warp_multi_agent_api as api;
use crate::ai::agent::task::TaskId;
use crate::ai::agent::{
AIAgentActionResult, AIAgentActionResultType, AIAgentContext, ImageContext,
TransferShellCommandControlToUserResult,
AIAgentActionResult, AIAgentActionResultType, AIAgentContext, AIAgentInput, ImageContext,
RunningCommand, TransferShellCommandControlToUserResult, UserQueryMode,
};
use crate::terminal::model::block::BlockId;
@@ -132,6 +134,84 @@ fn git_context_deserializes_legacy_string_pull_request_number() {
assert_eq!(pull_request.number, 42);
}
#[test]
fn command_completion_assessment_converts_to_primary_cli_query() {
let block_id = BlockId::from("completed-block".to_string());
let converted = super::convert_input(vec![AIAgentInput::CommandCompletionAssessment {
prompt: "Summarize whether the command succeeded.".to_string(),
context: Arc::from([AIAgentContext::SelectedText("root context".to_string())]),
completed_command: RunningCommand {
command: "cargo test -p galaxy".to_string(),
block_id: block_id.clone(),
grid_contents: "test result: ok".to_string(),
cursor: "cursor".to_string(),
requested_command_id: Some("run-call".to_string().into()),
is_alt_screen_active: true,
},
}])
.unwrap();
let Some(api::request::input::Type::UserInputs(inputs)) = converted.r#type else {
panic!("expected user inputs");
};
let Some(api::request::input::user_inputs::user_input::Input::CliAgentUserQuery(query)) =
inputs.inputs[0].input.as_ref()
else {
panic!("expected CLI agent query");
};
let user_query = query
.user_query
.as_ref()
.expect("expected assessment prompt");
assert_eq!(user_query.query, "Summarize whether the command succeeded.");
assert_eq!(user_query.intended_agent(), api::AgentType::Primary);
let command = query
.running_command
.as_ref()
.expect("expected completed command");
assert_eq!(command.command, "cargo test -p galaxy");
let snapshot = command.snapshot.as_ref().expect("expected final snapshot");
assert_eq!(snapshot.command_id, block_id.as_str());
assert_eq!(snapshot.output, "test result: ok");
assert_eq!(snapshot.cursor, "cursor");
assert!(snapshot.is_alt_screen_active);
assert_eq!(query.run_shell_command_tool_call_id, "run-call");
let active = super::convert_input(vec![AIAgentInput::UserQuery {
query: "Keep monitoring.".to_string(),
context: Arc::from([]),
static_query_type: None,
referenced_attachments: Default::default(),
user_query_mode: UserQueryMode::Normal,
running_command: Some(RunningCommand {
command: "cargo test -p galaxy".to_string(),
block_id,
grid_contents: "still running".to_string(),
cursor: String::new(),
requested_command_id: None,
is_alt_screen_active: false,
}),
intended_agent: None,
}])
.unwrap();
let Some(api::request::input::Type::UserInputs(inputs)) = active.r#type else {
panic!("expected active user inputs");
};
let Some(api::request::input::user_inputs::user_input::Input::CliAgentUserQuery(query)) =
inputs.inputs[0].input.as_ref()
else {
panic!("expected active CLI agent query");
};
assert_eq!(
query
.user_query
.as_ref()
.expect("expected active monitor prompt")
.intended_agent(),
api::AgentType::Cli
);
}
#[test]
fn transfer_control_snapshot_result_converts_to_tool_call_result_input() {
let block_id = BlockId::default();
+7 -1
View File
@@ -7,7 +7,7 @@ use warp_multi_agent_api as api;
use super::convert_to::convert_input;
use super::{ConvertToAPITypeError, RequestParams, ResponseStream, StreamEvent};
use crate::ai::agent::redaction;
use crate::ai::agent::{redaction, AIAgentInput};
use crate::ai::openai::translator as openai_translator;
use crate::ai::provider::ProviderConfig;
use crate::server::server_api::AIApiError;
@@ -97,6 +97,10 @@ pub async fn generate_multi_agent_output(
);
}
let emit_user_query_message = !params
.input
.iter()
.any(|input| matches!(input, AIAgentInput::CommandCompletionAssessment { .. }));
let mut request = api::Request {
task_context: Some(api::request::TaskContext {
tasks: params.tasks,
@@ -189,6 +193,7 @@ pub async fn generate_multi_agent_output(
progressive_summary: params.progressive_summary.clone(),
messages_sent: params.messages_sent.clone(),
global_rules: params.global_rules.clone(),
emit_user_query_message,
};
match openai_translator::execute(translator_request, &mut request).await {
@@ -223,6 +228,7 @@ pub async fn generate_multi_agent_output(
bedrock_progressive_summary: params.progressive_summary.clone(),
bedrock_messages_sent: params.messages_sent.clone(),
global_rules: params.global_rules.clone(),
emit_user_query_message,
};
match crate::ai::bedrock::translator::execute(translator_request, &mut request).await {