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
+2 -3
View File
@@ -400,10 +400,10 @@ impl CLISubagentController {
}
fn advance_completed_subagent(&mut self, block_id: &BlockId, ctx: &mut ModelContext<Self>) {
let Some((task_id, completion)) = self
let Some(completion) = self
.active_subagents_by_block
.get(block_id)
.and_then(|state| Some((state.task_id.clone()?, state.completion.as_ref()?.clone())))
.and_then(|state| state.completion.as_ref().cloned())
else {
return;
};
@@ -433,7 +433,6 @@ impl CLISubagentController {
let sent = self.controller.update(ctx, |controller, ctx| {
controller.send_command_completion_assessment(
completion.conversation_id,
task_id,
completion.prompt,
completion.completed_command,
ctx,
+1
View File
@@ -1375,6 +1375,7 @@ impl AIAgentInput {
app,
)),
AIAgentInput::UserQuery { .. }
| AIAgentInput::CommandCompletionAssessment { .. }
| AIAgentInput::AutoCodeDiffQuery { .. }
| AIAgentInput::ResumeConversation { .. }
| AIAgentInput::InitProjectRules { .. }
@@ -3687,6 +3687,7 @@ pub(super) fn query_prefix_highlight_len(
match input {
AIAgentInput::InvokeSkill { skill, .. } => Some(1 + skill.name.len()),
AIAgentInput::UserQuery { .. }
| AIAgentInput::CommandCompletionAssessment { .. }
| AIAgentInput::AutoCodeDiffQuery { .. }
| AIAgentInput::ResumeConversation { .. }
| AIAgentInput::InitProjectRules { .. }
+17 -12
View File
@@ -1597,7 +1597,7 @@ impl BlocklistAIController {
);
}
/// Sends one non-preemptive final assessment to a completed CLI-monitor task.
/// Sends one non-preemptive final assessment to the root task after a CLI monitor completes.
///
/// This deliberately bypasses `send_query`: command completion must not cancel
/// another conversation, drain unrelated action results, or replace a request
@@ -1605,8 +1605,7 @@ impl BlocklistAIController {
pub fn send_command_completion_assessment(
&mut self,
conversation_id: AIConversationId,
task_id: TaskId,
query: String,
prompt: String,
completed_command: RunningCommand,
ctx: &mut ModelContext<Self>,
) -> bool {
@@ -1621,6 +1620,16 @@ impl BlocklistAIController {
return false;
}
let Some(root_task_id) = BlocklistAIHistoryModel::as_ref(ctx)
.conversation(&conversation_id)
.map(|conversation| conversation.get_root_task_id().clone())
else {
log::warn!(
"Cannot send command completion assessment for missing conversation \
{conversation_id:?}"
);
return false;
};
let context = input_context_for_request(
false,
self.context_model.as_ref(ctx),
@@ -1629,16 +1638,12 @@ impl BlocklistAIController {
ctx,
);
let request_input = RequestInput::for_task(
vec![AIAgentInput::UserQuery {
query,
vec![AIAgentInput::CommandCompletionAssessment {
prompt,
context,
static_query_type: None,
referenced_attachments: HashMap::new(),
user_query_mode: UserQueryMode::Normal,
running_command: Some(completed_command),
intended_agent: None,
completed_command,
}],
task_id,
root_task_id,
&self.active_session,
self.get_current_response_initiator(),
conversation_id,
@@ -4553,7 +4558,7 @@ impl BlocklistAIController {
.as_ref()
.map(|stream_cancellation| format!("{:?}", stream_cancellation.reason)),
"queued_tools": remote_action_summaries(&actions_to_queue),
"proposed_tools": remote_action_summaries(&proposed_actions),
"proposed_tools": remote_action_summaries(proposed_actions),
}),
},
);
@@ -376,6 +376,7 @@ impl ResponseStream {
}
#[cfg(not(target_family = "wasm"))]
#[allow(clippy::too_many_arguments)]
fn log_llm_request_started(
ctx: &mut ModelContext<Self>,
stream_id: &ResponseStreamId,
+123 -2
View File
@@ -21,8 +21,10 @@ use crate::ai::agent::conversation::{
ServerAIConversationMetadata,
};
use crate::ai::agent::{
AIAgentExchange, AIAgentExchangeId, AIAgentInput, AIAgentOutputStatus, FinishedAIAgentOutput,
RenderableAIError, Shared, TransientNetworkErrorKind, UserQueryMode,
AIAgentExchange, AIAgentExchangeId, AIAgentInput, AIAgentOutput, AIAgentOutputMessage,
AIAgentOutputMessageType, AIAgentOutputStatus, AIAgentText, AIAgentTextSection,
AgentOutputText, FinishedAIAgentOutput, MessageId, RenderableAIError, RunningCommand, Shared,
TransientNetworkErrorKind, UserQueryMode,
};
use crate::ai::ambient_agents::{
conversation_output_status_from_conversation, AmbientAgentTaskId, AmbientConversationStatus,
@@ -270,6 +272,125 @@ fn deactivating_cli_subtask_clears_activity_without_deleting_task() {
});
}
#[test]
fn completed_command_assessment_survives_cli_subtask_deactivation_on_root() {
App::test((), |mut app| async move {
initialize_history_persistence_for_tests(&mut app);
let terminal_view_id = EntityId::new();
let history_model = app.add_singleton_model(|_| BlocklistAIHistoryModel::new_for_test());
let block_id = BlockId::new();
let assessment_output = "The command completed successfully.";
history_model.update(&mut app, |model, ctx| {
let conversation_id =
model.start_new_conversation(terminal_view_id, false, false, false, ctx);
let cli_task_id = model
.create_cli_subagent_task_for_conversation(
block_id.clone(),
conversation_id,
terminal_view_id,
ctx,
)
.expect("CLI subtask should be created");
let monitor_exchange =
create_exchange_with_query("Check the command status.", Local::now(), None);
let monitor_exchange_id = monitor_exchange.id;
let conversation = model
.conversation_mut(&conversation_id)
.expect("conversation should exist");
conversation
.append_task_exchange_for_test(
&cli_task_id,
monitor_exchange,
terminal_view_id,
ctx,
)
.expect("monitor exchange should be appended to the CLI task");
let now = Local::now();
let assessment_exchange = AIAgentExchange {
id: AIAgentExchangeId::new(),
input: vec![AIAgentInput::CommandCompletionAssessment {
prompt: "Assess the completed command.".to_string(),
context: Arc::from([]),
completed_command: RunningCommand {
command: "cargo test -p galaxy".to_string(),
block_id: block_id.clone(),
grid_contents: "test result: ok".to_string(),
cursor: String::new(),
requested_command_id: None,
is_alt_screen_active: false,
},
}],
output_status: AIAgentOutputStatus::Finished {
finished_output: FinishedAIAgentOutput::Success {
output: Shared::new(AIAgentOutput {
messages: vec![AIAgentOutputMessage {
id: MessageId::new("assessment-output".to_string()),
message: AIAgentOutputMessageType::Text(AIAgentText {
sections: vec![AIAgentTextSection::PlainText {
text: AgentOutputText::from(assessment_output.to_string()),
}],
}),
citations: vec![],
}],
..Default::default()
}),
},
},
added_message_ids: HashSet::new(),
start_time: now,
finish_time: Some(now),
time_to_first_token_ms: None,
working_directory: None,
model_id: LLMId::from("test-model"),
request_cost: None,
coding_model_id: LLMId::from("test-coding-model"),
cli_agent_model_id: LLMId::from("test-cli-agent-model"),
computer_use_model_id: LLMId::from("test-computer-use-model"),
response_initiator: None,
};
let assessment_exchange_id = assessment_exchange.id;
model
.conversation_mut(&conversation_id)
.expect("conversation should exist")
.append_root_exchange_for_test(assessment_exchange);
model
.deactivate_cli_subagent_task_for_conversation(&block_id, conversation_id)
.expect("CLI subtask should deactivate");
let conversation = model
.conversation(&conversation_id)
.expect("conversation should still exist");
assert!(!conversation.has_active_subagent());
let cli_task = conversation
.get_task(&cli_task_id)
.expect("CLI task should be retained after deactivation");
assert_eq!(cli_task.exchanges_len(), 1);
assert_eq!(
cli_task.last_exchange().map(|exchange| exchange.id),
Some(monitor_exchange_id)
);
let root_exchange = conversation
.latest_visible_exchange()
.expect("root assessment output should remain visible");
assert_eq!(root_exchange.id, assessment_exchange_id);
assert!(matches!(
root_exchange.input.as_slice(),
[AIAgentInput::CommandCompletionAssessment { .. }]
));
assert!(root_exchange.input[0].display_query().is_none());
assert_eq!(
root_exchange.format_output_for_copy(None),
assessment_output
);
});
});
}
#[test]
fn monitoring_a_different_block_preserves_completed_cli_task_history() {
App::test((), |mut app| async move {
+2 -1
View File
@@ -77,7 +77,8 @@ impl TryFrom<&AIAgentInput> for PersistedAIInputType {
AIAgentInput::PassiveSuggestionResult { suggestion: PassiveSuggestionResultType::CodeDiff { .. }, .. } => Err(anyhow!(
"PassiveSuggestionResult::CodeDiff is not persisted as a query."
)),
AIAgentInput::ActionResult { .. }
AIAgentInput::CommandCompletionAssessment { .. }
| AIAgentInput::ActionResult { .. }
| AIAgentInput::ResumeConversation { .. }
| AIAgentInput::InitProjectRules { .. }
| AIAgentInput::CreateEnvironment { .. }