Fix child agent startup routing and terminal failure status
This commit is contained in:
@@ -3445,7 +3445,7 @@ impl AIConversation {
|
||||
};
|
||||
|
||||
let current_comment_state = self.code_review.as_ref().cloned();
|
||||
task.add_messages(
|
||||
let add_result = task.add_messages(
|
||||
messages,
|
||||
exchange_id,
|
||||
TaskMessageContext {
|
||||
@@ -3459,9 +3459,11 @@ impl AIConversation {
|
||||
// to mimic the normal conversation flow. (If this is not a shared session, the
|
||||
// exchange inputs will already be populated).
|
||||
self.is_viewing_shared_session,
|
||||
)?;
|
||||
);
|
||||
|
||||
// Keep the task available for failure reporting even if message projection fails.
|
||||
self.task_store.insert(task);
|
||||
add_result?;
|
||||
if !added_exchanges
|
||||
.iter()
|
||||
.any(|new_exchange_info| new_exchange_info.exchange_id == exchange_id)
|
||||
|
||||
@@ -3206,6 +3206,27 @@ impl BlocklistAIController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Starts a newly created child on its root task. Shell initialization may still be
|
||||
/// finishing when the bootstrap event arrives; it is not a command to monitor.
|
||||
pub(crate) fn send_initial_child_agent_query(
|
||||
&mut self,
|
||||
query: String,
|
||||
conversation_id: AIConversationId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.send_user_query_in_conversation_internal(
|
||||
query,
|
||||
conversation_id,
|
||||
None,
|
||||
RunningCommandDetection::Skip,
|
||||
HashMap::new(),
|
||||
EntrypointType::AgentInitiated,
|
||||
false,
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -8339,8 +8360,11 @@ impl BlocklistAIController {
|
||||
});
|
||||
}
|
||||
},
|
||||
// Failed outcomes are finalized by the projected InternalError event.
|
||||
ProviderRunOutcome::Failed(_) => {}
|
||||
ProviderRunOutcome::Failed(failure) => {
|
||||
// Projection itself can fail before an exchange can display InternalError.
|
||||
// The durable outcome must still release parents waiting for this child.
|
||||
self.finalize_failed_provider_conversation(conversation_id, failure.message, ctx);
|
||||
}
|
||||
ProviderRunOutcome::Cancelled { .. } => {
|
||||
let cancellation_reason =
|
||||
self.active_provider_runs[&conversation_id].cancellation_reason;
|
||||
@@ -8373,6 +8397,28 @@ impl BlocklistAIController {
|
||||
self.cleanup_active_provider_run(conversation_id, &stream_id, &response_stream, ctx);
|
||||
}
|
||||
|
||||
fn finalize_failed_provider_conversation(
|
||||
&self,
|
||||
conversation_id: AIConversationId,
|
||||
message: String,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| {
|
||||
if history_model
|
||||
.conversation_status(&conversation_id)
|
||||
.is_some_and(|status| status != &ConversationStatus::Error)
|
||||
{
|
||||
history_model.update_conversation_status_with_error(
|
||||
self.terminal_surface_id,
|
||||
conversation_id,
|
||||
ConversationStatus::Error,
|
||||
Some(RenderableAIError::other(message, false)),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn fail_provider_startup(
|
||||
&mut self,
|
||||
conversation_id: AIConversationId,
|
||||
@@ -8391,7 +8437,7 @@ impl BlocklistAIController {
|
||||
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| {
|
||||
history_model.mark_response_stream_completed_with_error(
|
||||
RenderableAIError::Other {
|
||||
error_message: message,
|
||||
error_message: message.clone(),
|
||||
will_attempt_resume: false,
|
||||
waiting_for_network: false,
|
||||
is_user_error: false,
|
||||
@@ -8403,6 +8449,7 @@ impl BlocklistAIController {
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
self.finalize_failed_provider_conversation(conversation_id, message, ctx);
|
||||
self.cleanup_active_provider_run(conversation_id, &stream_id, &response_stream, ctx);
|
||||
}
|
||||
|
||||
@@ -10361,7 +10408,7 @@ fn get_running_command_for_conversation(
|
||||
conversation_id: AIConversationId,
|
||||
) -> Option<RunningCommand> {
|
||||
let active_block = terminal_model.block_list().active_block();
|
||||
if !active_block.is_active_and_long_running() {
|
||||
if !active_block.bootstrap_stage().is_done() || !active_block.is_active_and_long_running() {
|
||||
return None;
|
||||
}
|
||||
if active_block.is_agent_monitoring()
|
||||
|
||||
@@ -35,6 +35,8 @@ use crate::ai::remote_logging::RemoteLogLevel;
|
||||
use crate::ai::runtime::ProviderRunProjection;
|
||||
use crate::persistence::model::{AcpConversationData, AgentBackend};
|
||||
use crate::terminal::model::block::{BlockId, BlockState};
|
||||
use crate::terminal::model::bootstrap::BootstrapStage;
|
||||
use crate::terminal::model::test_utils::TestBlockBuilder;
|
||||
use crate::test_util::settings::initialize_history_persistence_for_tests;
|
||||
use crate::test_util::terminal::{add_window_with_terminal, initialize_app_for_terminal_view};
|
||||
|
||||
@@ -3812,3 +3814,79 @@ fn progressive_summary_keeps_retained_tool_results_paired() {
|
||||
let budget = super::estimated_history_tokens(&messages[2..]);
|
||||
assert_eq!(super::progressive_summary_split_point(&messages, budget), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_bootstrap_is_never_a_command_monitor_target() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app_for_terminal_view(&mut app);
|
||||
let terminal = add_window_with_terminal(&mut app, None);
|
||||
terminal.update(&mut app, |terminal, _ctx| {
|
||||
let mut model = terminal.model.lock();
|
||||
let conversation_id = AIConversationId::new();
|
||||
for stage in [
|
||||
BootstrapStage::WarpInput,
|
||||
BootstrapStage::ScriptExecution,
|
||||
BootstrapStage::Bootstrapped,
|
||||
BootstrapStage::PostBootstrapPrecmd,
|
||||
] {
|
||||
*model.block_list_mut().active_block_mut() =
|
||||
TestBlockBuilder::new().with_bootstrap_stage(stage).build();
|
||||
model.simulate_long_running_block("sleep 100", "shell startup output");
|
||||
assert!(model
|
||||
.block_list()
|
||||
.active_block()
|
||||
.is_active_and_long_running());
|
||||
assert_eq!(
|
||||
super::get_running_command_for_conversation(&model, conversation_id).is_some(),
|
||||
stage == BootstrapStage::PostBootstrapPrecmd,
|
||||
"bootstrap stage {stage:?}",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_child_without_an_exchange_reaches_terminal_error() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app_for_terminal_view(&mut app);
|
||||
let terminal = add_window_with_terminal(&mut app, None);
|
||||
terminal.update(&mut app, |terminal, ctx| {
|
||||
let terminal_id = terminal.id();
|
||||
let children = BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
|
||||
let parent = history.start_new_conversation(terminal_id, false, false, false, ctx);
|
||||
["stakeholder", "architect", "developer"].map(|name| {
|
||||
history.start_new_child_conversation(
|
||||
terminal_id,
|
||||
name.to_owned(),
|
||||
parent,
|
||||
None,
|
||||
ctx,
|
||||
)
|
||||
})
|
||||
});
|
||||
terminal.ai_controller().update(ctx, |controller, ctx| {
|
||||
for child in [children[0], children[2]] {
|
||||
controller.finalize_failed_provider_conversation(
|
||||
child,
|
||||
"projection failed: ExchangeNotFound".to_owned(),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
});
|
||||
let history = BlocklistAIHistoryModel::as_ref(ctx);
|
||||
for child in [children[0], children[2]] {
|
||||
let conversation = history.conversation(&child).unwrap();
|
||||
assert_eq!(conversation.status(), &ConversationStatus::Error);
|
||||
assert!(conversation
|
||||
.status_error_message()
|
||||
.unwrap()
|
||||
.contains("ExchangeNotFound"));
|
||||
}
|
||||
assert_eq!(
|
||||
history.conversation(&children[1]).unwrap().status(),
|
||||
&ConversationStatus::InProgress
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16279,7 +16279,7 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
self.ai_controller.update(ctx, |controller, ctx| {
|
||||
controller.send_agent_query_in_conversation(prompt, conversation_id, ctx);
|
||||
controller.send_initial_child_agent_query(prompt, conversation_id, ctx);
|
||||
});
|
||||
}
|
||||
pub fn has_pending_command_or_awaiting_completion(&self, ctx: &AppContext) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user