Yield orchestrators for child groups and correct subscription context catalogs

This commit is contained in:
2026-09-14 22:32:18 -05:00
parent 506dc5b225
commit 72cd38b54e
27 changed files with 741 additions and 115 deletions
@@ -34,6 +34,7 @@ use crate::ai::agent::{
};
use crate::ai::auth_secret_types::auth_secret_types_for_harness;
use crate::ai::blocklist::inline_action::orchestration_controls::OrchestrationEditState;
use crate::ai::blocklist::orchestration_events::OrchestrationEventService;
use crate::ai::blocklist::{BlocklistAIHistoryModel, BlocklistAIPermissions};
use crate::ai::cloud_agent_settings::CloudAgentSettings;
use crate::ai::document::plan_publication::{
@@ -180,6 +181,9 @@ impl RunAgentsExecutor {
"RunAgents cancellation detached {detached_dispatches} pending child dispatch(es) for action {action_id}"
);
if self.pending.remove(&action_key).is_some() {
OrchestrationEventService::handle(ctx).update(ctx, |service, ctx| {
service.seal_local_group(conversation_id, &action_id.to_string(), Vec::new(), ctx);
});
ctx.emit(RunAgentsExecutorEvent::SpawningFinished {
conversation_id,
action_id: action_id.clone(),
@@ -423,15 +427,30 @@ impl RunAgentsExecutor {
}),
);
if matches!(run_execution_mode, RunAgentsExecutionMode::Local) {
OrchestrationEventService::handle(ctx).update(ctx, |service, ctx| {
service.begin_local_group(parent_conversation_id, &action_id.to_string(), ctx);
});
}
let mut slots: Vec<ChildSlot> = Vec::with_capacity(agent_run_configs.len());
let wait_policy = match &run_execution_mode {
RunAgentsExecutionMode::Local => StartAgentWaitPolicy::Completion,
RunAgentsExecutionMode::Local => StartAgentWaitPolicy::LocalStartup,
RunAgentsExecutionMode::Remote { .. } => StartAgentWaitPolicy::Startup,
};
for cfg in &agent_run_configs {
let normalized_name = normalize_agent_name(&cfg.name)
.expect("validated RunAgents requests have non-empty agent names");
if let Some(child_conversation_id) = recovery_children.remove(&normalized_name) {
if matches!(run_execution_mode, RunAgentsExecutionMode::Local) {
OrchestrationEventService::handle(ctx).update(ctx, |service, ctx| {
service.register_local_group_child(
parent_conversation_id,
&action_id.to_string(),
child_conversation_id,
ctx,
);
});
}
let dispatch = self.start_agent_executor.update(ctx, |executor, exec_ctx| {
executor.reattach(
action_id.clone(),
@@ -509,7 +528,7 @@ impl RunAgentsExecutor {
}),
);
let dispatch = self.start_agent_executor.update(ctx, |executor, exec_ctx| {
executor.dispatch(
let mut dispatch = executor.dispatch(
action_id.clone(),
cfg.name.clone(),
prompt,
@@ -518,7 +537,11 @@ impl RunAgentsExecutor {
parent_conversation_id,
parent_run_id.clone(),
exec_ctx,
)
);
if matches!(run_execution_mode, RunAgentsExecutionMode::Local) {
executor.yield_after_local_start(&mut dispatch);
}
dispatch
});
slots.push(ChildSlot::Pending(dispatch));
}
@@ -583,6 +606,16 @@ impl RunAgentsExecutor {
})
.collect();
me.record_launched_agents(parent_conversation_id_for_result, &agents);
if matches!(run_execution_mode_for_aggr, RunAgentsExecutionMode::Local) {
let errors = agents.iter().filter_map(|agent| match &agent.kind {
RunAgentsAgentOutcomeKind::Failed { error } => Some(format!("{}: {error}", agent.name)),
RunAgentsAgentOutcomeKind::Launched { .. } | RunAgentsAgentOutcomeKind::Completed { .. } => None,
}).collect();
OrchestrationEventService::handle(ctx).update(ctx, |service, ctx| {
service.seal_local_group(parent_conversation_id_for_result, &action_id_for_aggr.to_string(), errors, ctx);
});
}
#[cfg(not(target_family = "wasm"))]
log_run_agents_event(
ctx,
@@ -838,7 +871,9 @@ async fn resolve_child_slot_with_timeout(
let request_id = dispatch.request_id;
let outcome = match dispatch.wait_policy {
StartAgentWaitPolicy::Completion => dispatch.receiver.recv().await.ok(),
StartAgentWaitPolicy::Completion | StartAgentWaitPolicy::LocalStartup => {
dispatch.receiver.recv().await.ok()
}
StartAgentWaitPolicy::Startup => {
let timeout = warpui::r#async::Timer::after(spawn_timeout);
match futures::future::select(Box::pin(dispatch.receiver.recv()), Box::pin(timeout))
@@ -458,12 +458,12 @@ fn recovered_run_agents_reattaches_existing_child_and_launches_only_missing_chil
assert_eq!(agents.len(), 2);
assert!(matches!(
&agents[0].kind,
RunAgentsAgentOutcomeKind::Completed { agent_id, .. }
RunAgentsAgentOutcomeKind::Launched { agent_id }
if agent_id == &existing_child_id.to_string()
));
assert!(matches!(
&agents[1].kind,
RunAgentsAgentOutcomeKind::Completed { agent_id, .. }
RunAgentsAgentOutcomeKind::Launched { agent_id }
if agent_id == &missing_child_id.to_string()
));
});
@@ -698,6 +698,7 @@ fn initialize_run_agents_test(app: &mut App, mode: ExecutionMode) -> RunAgentsTe
let global_resource_handles = GlobalResourceHandles::mock(app);
app.add_singleton_model(|_| GlobalResourceHandlesProvider::new(global_resource_handles));
let history = app.add_singleton_model(|_| BlocklistAIHistoryModel::new(vec![], &[]));
app.add_singleton_model(OrchestrationEventService::new);
app.add_singleton_model(|_| CLIAgentSessionsModel::new());
app.add_singleton_model(|_| ActiveAgentViewsModel::new());
app.add_singleton_model(AgentNotificationsModel::new);
@@ -18,6 +18,7 @@ use crate::ai::agent::{
StartAgentExecutionMode, StartAgentResult,
};
use crate::ai::blocklist::orchestration_event_streamer::OrchestrationEventStreamer;
use crate::ai::blocklist::orchestration_events::OrchestrationEventService;
use crate::ai::blocklist::{BlocklistAIHistoryEvent, BlocklistAIHistoryModel};
use crate::ai::llms::{LLMId, LLMPreferences};
use crate::ai::local_harness_setup::local_harness_product_disabled_message;
@@ -42,6 +43,8 @@ pub enum StartAgentOutcome {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StartAgentWaitPolicy {
Startup,
/// A local child has been linked; its persistent group owns completion.
LocalStartup,
Completion,
}
@@ -227,6 +230,20 @@ impl StartAgentExecutor {
}
};
if let Some(pending) = self.pending.get(&request_id) {
if matches!(pending.wait_policy, StartAgentWaitPolicy::LocalStartup) {
let parent = pending.parent_conversation_id;
let group_id = pending.action_id.to_string();
OrchestrationEventService::handle(ctx).update(ctx, |service, ctx| {
service.register_local_group_child(
parent,
&group_id,
child_conversation_id,
ctx,
);
});
}
}
if let Some(event) = child_link_event {
ctx.emit(event);
}
@@ -373,6 +390,14 @@ impl StartAgentExecutor {
| ConversationStatus::Blocked { .. }
| ConversationStatus::WaitingForEvents => {}
},
Some(StartAgentWaitPolicy::LocalStartup) => {
// Membership is durable before launch acknowledgement reaches the parent.
if let Some(pending) = self.pending.remove(&request_id) {
let _ = pending.sender.try_send(StartAgentOutcome::Started {
agent_id: child_conversation_id.to_string(),
});
}
}
Some(StartAgentWaitPolicy::Startup) => {
if let Some(error_msg) = start_agent_startup_error_message_for_status(
conversation.status(),
@@ -683,6 +708,15 @@ impl StartAgentExecutor {
})
}
pub(super) fn yield_after_local_start(&mut self, dispatch: &mut StartAgentDispatch) {
if matches!(dispatch.wait_policy, StartAgentWaitPolicy::Completion) {
dispatch.wait_policy = StartAgentWaitPolicy::LocalStartup;
if let Some(pending) = self.pending.get_mut(&dispatch.request_id) {
pending.wait_policy = StartAgentWaitPolicy::LocalStartup;
}
}
}
/// Dispatch a pre-validated StartAgent request. Returns a receiver
/// for the resulting [`StartAgentOutcome`].
#[allow(clippy::too_many_arguments)]
@@ -559,7 +559,7 @@ fn execute_resolves_error_when_request_linkage_happens_after_child_already_faile
assert!(matches!(
result,
AIAgentActionResultType::StartAgent(StartAgentResult::Error { error, version })
if error == "'codex' CLI not found on your machine."
if error.contains("'codex' CLI not found on your machine.") && error.contains("send_message_to_agent")
&& version == StartAgentVersion::V1
));
@@ -906,7 +906,7 @@ fn direct_provider_nonterminal_states_remain_pending_until_cancelled() {
assert!(matches!(
state.dispatch.receiver.try_recv(),
Ok(StartAgentOutcome::Error(error))
if error == "Child agent was cancelled by the user."
if error.contains("Child agent was cancelled by the user.") && error.contains("send_message_to_agent")
));
state.executor.read(&app, |executor, _| {
assert!(executor.pending.is_empty());
@@ -933,7 +933,7 @@ fn direct_provider_error_preserves_child_for_inspection() {
assert!(matches!(
state.dispatch.receiver.try_recv(),
Ok(StartAgentOutcome::Error(error)) if error == "Child execution failed"
Ok(StartAgentOutcome::Error(error)) if error.contains("Child execution failed") && error.contains("send_message_to_agent")
));
state.captured_cleanup.read(&app, |captured, _| {
assert!(captured.0.is_empty());
@@ -1369,7 +1369,7 @@ fn execute_returns_detailed_error_when_child_startup_fails_before_initialization
assert!(matches!(
result,
AIAgentActionResultType::StartAgent(StartAgentResult::Error { error, version })
if error == "Failed to resolve child agent skills: review-comments"
if error.contains("Failed to resolve child agent skills: review-comments") && error.contains("send_message_to_agent")
&& version == StartAgentVersion::V1
));
});
@@ -1739,7 +1739,7 @@ fn parallel_pendings_each_resolve_independently_via_recorded_child_id() {
assert!(matches!(
result_b,
AIAgentActionResultType::StartAgent(StartAgentResult::Error { error, .. })
if error == "Agent B init failed"
if error.contains("Agent B init failed") && error.contains("send_message_to_agent")
));
executor.read(&app, |executor, _| {