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| { let update_result = history_model.update(ctx, |history_model, ctx| {
match history_model.update_conversation_for_new_request_input( history_model.update_conversation_for_new_request_input(
request_input, request_input,
response_stream_id.clone(), response_stream_id.clone(),
self.terminal_surface_id, self.terminal_surface_id,
ctx, ctx,
) { )
Ok(_) => { });
history_model.update_conversation_status( if let Err(error) = update_result {
self.terminal_surface_id, let message = format!("failed to create AI response exchange: {error:?}");
conversation_data.id, self.cleanup_failed_response_stream_setup(
ConversationStatus::InProgress, conversation_data.id,
ctx, &response_stream_id,
); &response_stream,
} message.clone(),
Err(e) => { ctx,
log::warn!("Failed to push new exchange to AI conversation: {e:?}"); );
} 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 provider_projection_target = if provider_configs.is_some() {
let (task_id, exchange_id) = history_model let target = history_model
.as_ref(ctx) .as_ref(ctx)
.conversation(&conversation_data.id) .conversation(&conversation_data.id)
.and_then(|conversation| { .and_then(|conversation| {
conversation.provider_projection_target(&response_stream_id) conversation.provider_projection_target(&response_stream_id)
}) });
.ok_or_else(|| { let Some((task_id, exchange_id)) = target else {
anyhow!( let message =
"direct-provider response stream does not have exactly one projection target" "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 { Some(ProviderProjectionTarget {
task_id, task_id,
exchange_id, exchange_id,
@@ -4864,20 +4879,43 @@ impl BlocklistAIController {
conversation_data.id, conversation_data.id,
response_stream_id.as_str() response_stream_id.as_str()
)); ));
let root_task_id = history_model let Some(root_task_id) = history_model
.as_ref(ctx) .as_ref(ctx)
.conversation(&conversation_data.id) .conversation(&conversation_data.id)
.expect("conversation exists while starting provider run") .map(|conversation| conversation.get_root_task_id().clone())
.get_root_task_id() else {
.clone(); 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 { let slot = ActiveProviderRunSlot {
stream_id: response_stream_id.clone(), stream_id: response_stream_id.clone(),
response_stream, response_stream,
did_input_contain_user_query: input_contains_user_query, did_input_contain_user_query: input_contains_user_query,
run_id: provider_run_id, run_id: provider_run_id,
root_task_id, root_task_id,
projection_target: provider_projection_target projection_target,
.expect("provider projection target was validated"),
run: None, run: None,
checkpoint: None, checkpoint: None,
turn_control: None, turn_control: None,
@@ -4964,6 +5002,35 @@ impl BlocklistAIController {
Ok((conversation_data.id, response_stream_id)) 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( fn provider_generation_is_terminalizing_for_follow_up(
&self, &self,
conversation_id: AIConversationId, conversation_id: AIConversationId,
+1 -1
View File
@@ -601,10 +601,10 @@ impl ProviderRun {
} }
} }
ProviderRunState::AwaitingTools { batch } => { ProviderRunState::AwaitingTools { batch } => {
self.validate_post_model_phase()?;
if batch.calls.is_empty() { if batch.calls.is_empty() {
return Err(invalid("pending tool batch is empty".to_string())); return Err(invalid("pending tool batch is empty".to_string()));
} }
self.validate_post_model_phase()?;
let mut call_ids = HashSet::new(); let mut call_ids = HashSet::new();
for pending in &batch.calls { for pending in &batch.calls {
if pending.call.id.is_empty() { if pending.call.id.is_empty() {