Prevent provider follow-up setup panics

This commit is contained in:
Ryan Ward
2026-08-19 11:11:28 -05:00
parent 87080c8086
commit 11c466c26d
2 changed files with 96 additions and 29 deletions
+95 -28
View File
@@ -4800,39 +4800,54 @@ impl BlocklistAIController {
}
}
history_model.update(ctx, |history_model, ctx| {
match history_model.update_conversation_for_new_request_input(
let update_result = history_model.update(ctx, |history_model, ctx| {
history_model.update_conversation_for_new_request_input(
request_input,
response_stream_id.clone(),
self.terminal_surface_id,
ctx,
) {
Ok(_) => {
history_model.update_conversation_status(
self.terminal_surface_id,
conversation_data.id,
ConversationStatus::InProgress,
ctx,
);
}
Err(e) => {
log::warn!("Failed to push new exchange to AI conversation: {e:?}");
}
}
)
});
if let Err(error) = update_result {
let message = format!("failed to create AI response exchange: {error:?}");
self.cleanup_failed_response_stream_setup(
conversation_data.id,
&response_stream_id,
&response_stream,
message.clone(),
ctx,
);
return Err(anyhow!(message));
}
history_model.update(ctx, |history_model, ctx| {
history_model.update_conversation_status(
self.terminal_surface_id,
conversation_data.id,
ConversationStatus::InProgress,
ctx,
);
});
let provider_projection_target = if provider_configs.is_some() {
let (task_id, exchange_id) = history_model
let target = history_model
.as_ref(ctx)
.conversation(&conversation_data.id)
.and_then(|conversation| {
conversation.provider_projection_target(&response_stream_id)
})
.ok_or_else(|| {
anyhow!(
"direct-provider response stream does not have exactly one projection target"
)
})?;
});
let Some((task_id, exchange_id)) = target else {
let message =
"direct-provider response stream does not have exactly one projection target"
.to_owned();
self.cleanup_failed_response_stream_setup(
conversation_data.id,
&response_stream_id,
&response_stream,
message.clone(),
ctx,
);
return Err(anyhow!(message));
};
Some(ProviderProjectionTarget {
task_id,
exchange_id,
@@ -4864,20 +4879,43 @@ impl BlocklistAIController {
conversation_data.id,
response_stream_id.as_str()
));
let root_task_id = history_model
let Some(root_task_id) = history_model
.as_ref(ctx)
.conversation(&conversation_data.id)
.expect("conversation exists while starting provider run")
.get_root_task_id()
.clone();
.map(|conversation| conversation.get_root_task_id().clone())
else {
let message = format!(
"conversation {:?} disappeared while starting provider run",
conversation_data.id
);
self.cleanup_failed_response_stream_setup(
conversation_data.id,
&response_stream_id,
&response_stream,
message.clone(),
ctx,
);
return Err(anyhow!(message));
};
let Some(projection_target) = provider_projection_target else {
let message =
"direct-provider response stream is missing its projection target".to_owned();
self.cleanup_failed_response_stream_setup(
conversation_data.id,
&response_stream_id,
&response_stream,
message.clone(),
ctx,
);
return Err(anyhow!(message));
};
let slot = ActiveProviderRunSlot {
stream_id: response_stream_id.clone(),
response_stream,
did_input_contain_user_query: input_contains_user_query,
run_id: provider_run_id,
root_task_id,
projection_target: provider_projection_target
.expect("provider projection target was validated"),
projection_target,
run: None,
checkpoint: None,
turn_control: None,
@@ -4964,6 +5002,35 @@ impl BlocklistAIController {
Ok((conversation_data.id, response_stream_id))
}
fn cleanup_failed_response_stream_setup(
&self,
conversation_id: AIConversationId,
stream_id: &ResponseStreamId,
response_stream: &ModelHandle<ResponseStream>,
message: String,
ctx: &mut ModelContext<Self>,
) {
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| {
history_model.mark_response_stream_completed_with_error(
RenderableAIError::Other {
error_message: message,
will_attempt_resume: false,
waiting_for_network: false,
is_user_error: false,
},
false,
stream_id,
conversation_id,
self.terminal_surface_id,
ctx,
);
if let Some(conversation) = history_model.conversation_mut(&conversation_id) {
conversation.cleanup_completed_response_stream(stream_id);
}
});
ctx.unsubscribe_from_model(response_stream);
}
fn provider_generation_is_terminalizing_for_follow_up(
&self,
conversation_id: AIConversationId,
+1 -1
View File
@@ -601,10 +601,10 @@ impl ProviderRun {
}
}
ProviderRunState::AwaitingTools { batch } => {
self.validate_post_model_phase()?;
if batch.calls.is_empty() {
return Err(invalid("pending tool batch is empty".to_string()));
}
self.validate_post_model_phase()?;
let mut call_ids = HashSet::new();
for pending in &batch.calls {
if pending.call.id.is_empty() {