Improve orchestration agent model selection
This commit is contained in:
@@ -1247,7 +1247,7 @@ pub fn compose_run_agents_child_prompt(base_prompt: &str, per_agent_prompt: &str
|
||||
}
|
||||
}
|
||||
|
||||
/// Translates run-wide config into a per-child
|
||||
/// Translates shared config and the child's model selection into a per-child
|
||||
/// [`StartAgentExecutionMode`]. Returns `Err` for rejected
|
||||
/// combinations (e.g. OpenCode+Remote).
|
||||
///
|
||||
@@ -1262,12 +1262,15 @@ pub fn run_agents_to_start_agent_mode(
|
||||
run_auth_secret_name: Option<&str>,
|
||||
cfg: &RunAgentsAgentRunConfig,
|
||||
) -> Result<StartAgentExecutionMode, String> {
|
||||
let child_model_id = if cfg.model_id.trim().is_empty() {
|
||||
run_model_id.trim()
|
||||
} else {
|
||||
cfg.model_id.trim()
|
||||
};
|
||||
match run_execution_mode {
|
||||
RunAgentsExecutionMode::Local => {
|
||||
let trimmed = run_harness_type.trim();
|
||||
// Propagate run-wide model selection for local launches.
|
||||
let trimmed_model_id = run_model_id.trim();
|
||||
let model_id = (!trimmed_model_id.is_empty()).then(|| trimmed_model_id.to_string());
|
||||
let model_id = (!child_model_id.is_empty()).then(|| child_model_id.to_string());
|
||||
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("oz") {
|
||||
Ok(StartAgentExecutionMode::Local {
|
||||
harness_type: None,
|
||||
@@ -1299,7 +1302,7 @@ pub fn run_agents_to_start_agent_mode(
|
||||
Ok(StartAgentExecutionMode::Remote {
|
||||
environment_id: environment_id.clone(),
|
||||
skill_references: run_skills.to_vec(),
|
||||
model_id: run_model_id.to_string(),
|
||||
model_id: child_model_id.to_string(),
|
||||
computer_use_enabled: *computer_use_enabled,
|
||||
worker_host: worker_host.clone(),
|
||||
harness_type: run_harness_type.to_string(),
|
||||
|
||||
@@ -266,6 +266,7 @@ fn execute_denies_mixed_batch_containing_launched_agent() {
|
||||
name: "new-child".to_string(),
|
||||
prompt: "Do separate work".to_string(),
|
||||
title: String::new(),
|
||||
model_id: String::new(),
|
||||
});
|
||||
|
||||
let execution = state.executor.update(&mut app, |executor, ctx| {
|
||||
@@ -306,6 +307,7 @@ fn validate_request_rejects_blank_and_duplicate_agent_names() {
|
||||
name: " child ".to_string(),
|
||||
prompt: "Do separate work".to_string(),
|
||||
title: String::new(),
|
||||
model_id: String::new(),
|
||||
});
|
||||
assert_eq!(
|
||||
validate_request(&request),
|
||||
@@ -323,6 +325,7 @@ fn validate_request_allows_unique_sibling_names() {
|
||||
name: "second-child".to_string(),
|
||||
prompt: "Do separate work".to_string(),
|
||||
title: String::new(),
|
||||
model_id: String::new(),
|
||||
});
|
||||
|
||||
assert_eq!(validate_request(&request), Ok(()));
|
||||
@@ -384,6 +387,7 @@ fn recovered_run_agents_reattaches_existing_child_and_launches_only_missing_chil
|
||||
name: "missing-child".to_string(),
|
||||
prompt: "Do separate work".to_string(),
|
||||
title: String::new(),
|
||||
model_id: String::new(),
|
||||
});
|
||||
state.executor.update(&mut app, |executor, _| {
|
||||
executor
|
||||
@@ -454,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::Launched { agent_id }
|
||||
RunAgentsAgentOutcomeKind::Completed { agent_id, .. }
|
||||
if agent_id == &existing_child_id.to_string()
|
||||
));
|
||||
assert!(matches!(
|
||||
&agents[1].kind,
|
||||
RunAgentsAgentOutcomeKind::Launched { agent_id }
|
||||
RunAgentsAgentOutcomeKind::Completed { agent_id, .. }
|
||||
if agent_id == &missing_child_id.to_string()
|
||||
));
|
||||
});
|
||||
@@ -766,6 +770,7 @@ fn remote_run_agents_action(harness_type: &str) -> AIAgentAction {
|
||||
name: "child".to_string(),
|
||||
prompt: "Help".to_string(),
|
||||
title: String::new(),
|
||||
model_id: String::new(),
|
||||
}],
|
||||
plan_id: String::new(),
|
||||
harness_auth_secret_name: None,
|
||||
@@ -789,6 +794,7 @@ fn local_codex_run_agents_maps_to_local_harness_mode_when_flag_enabled() {
|
||||
name: "child".to_string(),
|
||||
prompt: "Investigate the failure".to_string(),
|
||||
title: String::new(),
|
||||
model_id: String::new(),
|
||||
};
|
||||
|
||||
let mode = run_agents_to_start_agent_mode(
|
||||
@@ -810,6 +816,34 @@ fn local_codex_run_agents_maps_to_local_harness_mode_when_flag_enabled() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_model_overrides_the_run_wide_model() {
|
||||
let cfg = RunAgentsAgentRunConfig {
|
||||
name: "child".to_string(),
|
||||
prompt: "Investigate".to_string(),
|
||||
title: String::new(),
|
||||
model_id: "child-model".to_string(),
|
||||
};
|
||||
|
||||
let mode = run_agents_to_start_agent_mode(
|
||||
&RunAgentsExecutionMode::Local,
|
||||
"oz",
|
||||
"fallback-model",
|
||||
&[],
|
||||
None,
|
||||
&cfg,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
mode,
|
||||
StartAgentExecutionMode::Local {
|
||||
harness_type: None,
|
||||
model_id: Some("child-model".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn persist_default_auth_secret(app: &mut App, harness_config_name: &str, secret_name: &str) {
|
||||
CloudAgentSettings::handle(app).update(app, |settings, ctx| {
|
||||
let mut secrets = settings.last_selected_auth_secret.value().clone();
|
||||
|
||||
@@ -483,6 +483,7 @@ fn agent_cfg() -> RunAgentsAgentRunConfig {
|
||||
name: "child".to_string(),
|
||||
prompt: "do X".to_string(),
|
||||
title: "Child".to_string(),
|
||||
model_id: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,10 +81,11 @@ use crate::ai::provider::ProviderConfig;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use crate::ai::remote_logging::{self, RemoteLogLevel, RemoteLogRecord};
|
||||
use crate::ai::runtime::{
|
||||
prepare_provider_run, provider_runtime_for_request, PreparedProviderRun, ProviderActionContext,
|
||||
ProviderRunBlock, ProviderRunCoordinator, ProviderRunProfile, ProviderRunProjection,
|
||||
ProviderRunResponseProjector, ProviderToolExecutionRef, ProviderToolLifecycleOutcome,
|
||||
RuntimeResponseConfig, BASE_PROVIDER_PROFILE, CLI_MONITOR_PROVIDER_PROFILE,
|
||||
prepare_provider_run, provider_runtime_for_request, OrchestrationModelOption,
|
||||
PreparedProviderRun, ProviderActionContext, ProviderRunBlock, ProviderRunCoordinator,
|
||||
ProviderRunProfile, ProviderRunProjection, ProviderRunResponseProjector,
|
||||
ProviderToolExecutionRef, ProviderToolLifecycleOutcome, RuntimeResponseConfig,
|
||||
BASE_PROVIDER_PROFILE, CLI_MONITOR_PROVIDER_PROFILE,
|
||||
};
|
||||
use crate::ai::AIRequestUsageModel;
|
||||
use crate::cloud_object::model::persistence::CloudModel;
|
||||
@@ -6296,10 +6297,27 @@ impl BlocklistAIController {
|
||||
request_params: api::RequestParams,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let orchestration_models = LLMPreferences::as_ref(ctx)
|
||||
.get_base_llm_choices_for_agent_mode(ctx)
|
||||
.filter(|model| model.disable_reason.is_none())
|
||||
.map(|model| OrchestrationModelOption {
|
||||
id: model.id.to_string(),
|
||||
display_name: model.display_name.clone(),
|
||||
provider: model.provider.display_name().to_string(),
|
||||
quality: model.spec.as_ref().map(|spec| spec.quality),
|
||||
cost: model.spec.as_ref().map(|spec| spec.cost),
|
||||
credit_multiplier: model.usage_metadata.credit_multiplier,
|
||||
})
|
||||
.collect();
|
||||
let _ = ctx.spawn(
|
||||
async move {
|
||||
prepare_provider_run(base_provider_config, cli_provider_config, request_params)
|
||||
.await
|
||||
prepare_provider_run(
|
||||
base_provider_config,
|
||||
cli_provider_config,
|
||||
request_params,
|
||||
orchestration_models,
|
||||
)
|
||||
.await
|
||||
},
|
||||
move |me, result, ctx| {
|
||||
me.handle_prepared_provider_run(conversation_id, stream_id, result, ctx);
|
||||
|
||||
@@ -430,6 +430,30 @@ pub fn populate_model_picker_for_harness<A: OrchestrationControlAction, V: View>
|
||||
harness_type: &str,
|
||||
is_local: bool,
|
||||
ctx: &mut ViewContext<V>,
|
||||
) {
|
||||
populate_model_picker_for_harness_with_action(
|
||||
dropdown,
|
||||
initial_model_id,
|
||||
harness_type,
|
||||
is_local,
|
||||
A::model_changed,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
/// Populates an orchestration model picker while allowing the caller to
|
||||
/// attach extra identity (for example, a child-agent name) to model actions.
|
||||
pub fn populate_model_picker_for_harness_with_action<
|
||||
A: OrchestrationControlAction,
|
||||
V: View,
|
||||
F: Fn(String) -> A + Clone,
|
||||
>(
|
||||
dropdown: &ViewHandle<FilterableDropdown<A>>,
|
||||
initial_model_id: &str,
|
||||
harness_type: &str,
|
||||
is_local: bool,
|
||||
model_changed: F,
|
||||
ctx: &mut ViewContext<V>,
|
||||
) {
|
||||
let initial_model_id = initial_model_id.to_string();
|
||||
let harness_type = harness_type.to_string();
|
||||
@@ -459,9 +483,7 @@ pub fn populate_model_picker_for_harness<A: OrchestrationControlAction, V: View>
|
||||
let items = available_model_menu_items(
|
||||
ordered_choices,
|
||||
move |llm| {
|
||||
DropdownAction::select_action_and_close(A::model_changed(
|
||||
llm.id.to_string(),
|
||||
))
|
||||
DropdownAction::select_action_and_close(model_changed(llm.id.to_string()))
|
||||
},
|
||||
None,
|
||||
None,
|
||||
@@ -476,21 +498,22 @@ pub fn populate_model_picker_for_harness<A: OrchestrationControlAction, V: View>
|
||||
}
|
||||
Some(Harness::Codex) if is_local => {
|
||||
// Local Codex: only "Default model" entry.
|
||||
let items = vec![default_model_menu_item::<A>()];
|
||||
let items = vec![default_model_menu_item(model_changed)];
|
||||
dropdown.set_rich_items(items, ctx_dropdown);
|
||||
dropdown.set_selected_by_name(DEFAULT_MODEL_LABEL, ctx_dropdown);
|
||||
}
|
||||
Some(harness) => {
|
||||
// Non-Oz harness: "Default model" at top, then server-provided
|
||||
// harness models.
|
||||
let mut items: Vec<MenuItem<DropdownAction>> = vec![default_model_menu_item::<A>()];
|
||||
let mut items: Vec<MenuItem<DropdownAction>> =
|
||||
vec![default_model_menu_item(model_changed.clone())];
|
||||
let availability = HarnessAvailabilityModel::as_ref(ctx_dropdown);
|
||||
if let Some(models) = availability.models_for(harness) {
|
||||
for model in models {
|
||||
let model_id = model.id.clone();
|
||||
let fields = MenuItemFields::new(&model.display_name)
|
||||
.with_on_select_action(DropdownAction::select_action_and_close(
|
||||
A::model_changed(model_id),
|
||||
model_changed(model_id),
|
||||
));
|
||||
items.push(MenuItem::Item(fields));
|
||||
}
|
||||
@@ -519,10 +542,12 @@ pub fn populate_model_picker_for_harness<A: OrchestrationControlAction, V: View>
|
||||
}
|
||||
|
||||
/// Creates a "Default model" menu item that emits an empty model_id.
|
||||
fn default_model_menu_item<A: OrchestrationControlAction>() -> MenuItem<DropdownAction> {
|
||||
fn default_model_menu_item<A: OrchestrationControlAction>(
|
||||
model_changed: impl Fn(String) -> A,
|
||||
) -> MenuItem<DropdownAction> {
|
||||
MenuItem::Item(
|
||||
MenuItemFields::new(DEFAULT_MODEL_LABEL).with_on_select_action(
|
||||
DropdownAction::select_action_and_close(A::model_changed(String::new())),
|
||||
DropdownAction::select_action_and_close(model_changed(String::new())),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -598,6 +623,7 @@ pub fn populate_harness_picker<A: OrchestrationControlAction, V: View>(
|
||||
let harnesses = availability.available_harnesses();
|
||||
|
||||
let resolve_entry_harness = |harness: Harness, display_name: &str| match harness {
|
||||
Harness::Unknown if display_name == "Warp" => Harness::Oz,
|
||||
Harness::Unknown => [
|
||||
Harness::Oz,
|
||||
Harness::Claude,
|
||||
@@ -644,11 +670,15 @@ pub fn populate_harness_picker<A: OrchestrationControlAction, V: View>(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Use the server-provided display_name for the label so stale
|
||||
// cache entries (where harness deserializes as Unknown) still
|
||||
// show the correct name.
|
||||
let mut fields = MenuItemFields::new(&entry.display_name)
|
||||
.with_icon(harness_display::icon_for(harness));
|
||||
// Oz is branded locally so stale server/cache values cannot
|
||||
// reintroduce the legacy product name.
|
||||
let display_name = if harness == Harness::Oz {
|
||||
harness_display::display_name(harness)
|
||||
} else {
|
||||
&entry.display_name
|
||||
};
|
||||
let mut fields =
|
||||
MenuItemFields::new(display_name).with_icon(harness_display::icon_for(harness));
|
||||
if let Some(color) = harness_display::brand_color(harness) {
|
||||
fields = fields.with_override_icon_color(Fill::from(color));
|
||||
}
|
||||
@@ -673,10 +703,10 @@ pub fn populate_harness_picker<A: OrchestrationControlAction, V: View>(
|
||||
// is Unknown but entry.display_name is still correct.
|
||||
if selected_name.is_none() {
|
||||
if harness_str.eq_ignore_ascii_case(&initial_harness) {
|
||||
selected_name = Some(entry.display_name.clone());
|
||||
selected_name = Some(display_name.to_string());
|
||||
} else if let Some(target_display) = target_display {
|
||||
if entry.display_name == target_display {
|
||||
selected_name = Some(entry.display_name.clone());
|
||||
if display_name == target_display {
|
||||
selected_name = Some(display_name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ use ai::skills::SkillReference;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use warpui::elements::{
|
||||
Border, ChildAnchor, ChildView, Container, CornerRadius, CrossAxisAlignment, Empty, Flex,
|
||||
MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius,
|
||||
Stack, Text, Wrap,
|
||||
Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Empty, Flex, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
|
||||
ParentOffsetBounds, Radius, Stack, Text, Wrap,
|
||||
};
|
||||
use warpui::keymap::FixedBinding;
|
||||
use warpui::{
|
||||
@@ -128,14 +128,16 @@ impl RunAgentsEditState {
|
||||
if matches!(req.execution_mode, RunAgentsExecutionMode::Local) {
|
||||
orch.sanitize_for_local_execution();
|
||||
}
|
||||
Self {
|
||||
let mut state = Self {
|
||||
orch,
|
||||
agent_run_configs: req.agent_run_configs.clone(),
|
||||
base_prompt: req.base_prompt.clone(),
|
||||
summary: req.summary.clone(),
|
||||
skills: req.skills.clone(),
|
||||
plan_id: req.plan_id.clone(),
|
||||
}
|
||||
};
|
||||
state.fill_missing_agent_models();
|
||||
state
|
||||
}
|
||||
|
||||
pub fn to_request(&self) -> RunAgentsRequest {
|
||||
@@ -151,6 +153,14 @@ impl RunAgentsEditState {
|
||||
harness_auth_secret_name: self.orch.auth_secret_name().map(str::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_missing_agent_models(&mut self) {
|
||||
for config in &mut self.agent_run_configs {
|
||||
if config.model_id.trim().is_empty() {
|
||||
config.model_id = self.orch.model_id.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OrchestrationControlAction for RunAgentsCardViewAction {
|
||||
@@ -197,6 +207,10 @@ pub enum RunAgentsCardViewAction {
|
||||
ModelChanged {
|
||||
model_id: String,
|
||||
},
|
||||
AgentModelChanged {
|
||||
agent_name: String,
|
||||
model_id: String,
|
||||
},
|
||||
HarnessChanged {
|
||||
harness_type: String,
|
||||
},
|
||||
@@ -225,6 +239,8 @@ struct RunAgentsChildState {
|
||||
conversation_id: Option<AIConversationId>,
|
||||
removed: bool,
|
||||
mouse_state: MouseStateHandle,
|
||||
model_picker:
|
||||
Option<ViewHandle<crate::view_components::FilterableDropdown<RunAgentsCardViewAction>>>,
|
||||
}
|
||||
|
||||
impl RunAgentsChildState {
|
||||
@@ -234,6 +250,7 @@ impl RunAgentsChildState {
|
||||
conversation_id: None,
|
||||
removed: false,
|
||||
mouse_state: MouseStateHandle::default(),
|
||||
model_picker: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,6 +418,7 @@ fn resolve_interactive_defaults(
|
||||
}
|
||||
}
|
||||
}
|
||||
state.fill_missing_agent_models();
|
||||
}
|
||||
impl RunAgentsCardView {
|
||||
pub fn new(
|
||||
@@ -537,6 +555,8 @@ impl RunAgentsCardView {
|
||||
// to the full confirmation UI.
|
||||
resolve_interactive_defaults(&mut me.state, &*me.block_model, ctx);
|
||||
oc::repopulate_all_pickers(&mut me.state.orch, &me.handles.pickers, ctx);
|
||||
me.reconcile_agent_model_selections(ctx);
|
||||
me.repopulate_agent_model_pickers(ctx);
|
||||
me.refresh_accept_button_state(ctx);
|
||||
me.maybe_auto_open_create_modal(ctx);
|
||||
if let Some(conversation_id) = me.block_model.conversation_id(ctx) {
|
||||
@@ -548,7 +568,7 @@ impl RunAgentsCardView {
|
||||
}
|
||||
});
|
||||
|
||||
// Repopulate the model picker when available Warp LLMs change.
|
||||
// Repopulate the model pickers when available Galaxy LLMs change.
|
||||
// Only relevant for Oz harness — non-Oz harnesses get their
|
||||
// model catalog from HarnessAvailabilityModel, not LLMPreferences.
|
||||
ctx.subscribe_to_model(&LLMPreferences::handle(ctx), |me, _, event, ctx| {
|
||||
@@ -563,6 +583,8 @@ impl RunAgentsCardView {
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
me.reconcile_agent_model_selections(ctx);
|
||||
me.repopulate_agent_model_pickers(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -582,6 +604,8 @@ impl RunAgentsCardView {
|
||||
ctx,
|
||||
);
|
||||
oc::repopulate_all_pickers(&mut me.state.orch, &me.handles.pickers, ctx);
|
||||
me.reconcile_agent_model_selections(ctx);
|
||||
me.repopulate_agent_model_pickers(ctx);
|
||||
me.refresh_accept_button_state(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
@@ -593,6 +617,8 @@ impl RunAgentsCardView {
|
||||
// Deleted events also force a repopulate so this card
|
||||
// stops surfacing the deleted secret as an option.
|
||||
oc::repopulate_all_pickers(&mut me.state.orch, &me.handles.pickers, ctx);
|
||||
me.reconcile_agent_model_selections(ctx);
|
||||
me.repopulate_agent_model_pickers(ctx);
|
||||
me.refresh_accept_button_state(ctx);
|
||||
me.maybe_auto_open_create_modal(ctx);
|
||||
ctx.notify();
|
||||
@@ -651,6 +677,7 @@ impl RunAgentsCardView {
|
||||
};
|
||||
|
||||
view.ensure_pickers(ctx);
|
||||
view.ensure_agent_model_pickers(ctx);
|
||||
view.refresh_accept_button_state(ctx);
|
||||
// No-ops if secrets are still in flight; the `AuthSecretsLoaded`
|
||||
// subscription will retry once they resolve.
|
||||
@@ -683,6 +710,7 @@ impl RunAgentsCardView {
|
||||
}
|
||||
}
|
||||
}
|
||||
new_state.fill_missing_agent_models();
|
||||
// Re-seed an Unset selection from persisted per-harness settings,
|
||||
// honoring an explicit `Inherit` choice for this harness.
|
||||
if matches!(
|
||||
@@ -699,10 +727,14 @@ impl RunAgentsCardView {
|
||||
|| self.state.orch.execution_mode != new_state.orch.execution_mode;
|
||||
self.state = new_state;
|
||||
self.sync_configured_children();
|
||||
self.ensure_agent_model_pickers(ctx);
|
||||
self.repopulate_agent_model_pickers(ctx);
|
||||
if harness_or_model_changed {
|
||||
// Repopulate pickers and re-arm auto-open for the newly-
|
||||
// streamed harness.
|
||||
oc::repopulate_all_pickers(&mut self.state.orch, &self.handles.pickers, ctx);
|
||||
self.reconcile_agent_model_selections(ctx);
|
||||
self.repopulate_agent_model_pickers(ctx);
|
||||
self.has_auto_opened_create_modal = false;
|
||||
}
|
||||
self.refresh_accept_button_state(ctx);
|
||||
@@ -715,6 +747,76 @@ impl RunAgentsCardView {
|
||||
sync_run_agents_children(&mut self.children, &self.state.agent_run_configs);
|
||||
}
|
||||
|
||||
fn reconcile_agent_model_selections(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let harness_type = self.state.orch.harness_type.clone();
|
||||
let is_local = !self.state.orch.execution_mode.is_remote();
|
||||
let fallback = if oc::is_model_in_filtered_choices(
|
||||
&self.state.orch.model_id,
|
||||
&harness_type,
|
||||
is_local,
|
||||
ctx,
|
||||
) {
|
||||
self.state.orch.model_id.clone()
|
||||
} else {
|
||||
oc::first_filtered_model_id(&harness_type, ctx).unwrap_or_default()
|
||||
};
|
||||
for config in &mut self.state.agent_run_configs {
|
||||
if !oc::is_model_in_filtered_choices(&config.model_id, &harness_type, is_local, ctx) {
|
||||
config.model_id = fallback.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_agent_model_pickers(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let appearance = Appearance::as_ref(ctx);
|
||||
let (styles, _) = oc::picker_styles(appearance);
|
||||
let harness_type = self.state.orch.harness_type.clone();
|
||||
let is_local = !self.state.orch.execution_mode.is_remote();
|
||||
for (config, child) in self.state.agent_run_configs.iter().zip(&mut self.children) {
|
||||
if child.model_picker.is_some() {
|
||||
continue;
|
||||
}
|
||||
let handle = oc::new_standard_filterable_picker_dropdown(&styles, ctx);
|
||||
Self::set_upward_filterable_menu_position(&handle, ctx);
|
||||
let agent_name = config.name.clone();
|
||||
oc::populate_model_picker_for_harness_with_action(
|
||||
&handle,
|
||||
&config.model_id,
|
||||
&harness_type,
|
||||
is_local,
|
||||
move |model_id| RunAgentsCardViewAction::AgentModelChanged {
|
||||
agent_name: agent_name.clone(),
|
||||
model_id,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
Self::subscribe_filterable_picker_close(&handle, ctx);
|
||||
child.model_picker = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
fn repopulate_agent_model_pickers(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let harness_type = self.state.orch.harness_type.clone();
|
||||
let is_local = !self.state.orch.execution_mode.is_remote();
|
||||
for (config, child) in self.state.agent_run_configs.iter().zip(&self.children) {
|
||||
let Some(handle) = &child.model_picker else {
|
||||
continue;
|
||||
};
|
||||
let agent_name = config.name.clone();
|
||||
oc::populate_model_picker_for_harness_with_action(
|
||||
handle,
|
||||
&config.model_id,
|
||||
&harness_type,
|
||||
is_local,
|
||||
move |model_id| RunAgentsCardViewAction::AgentModelChanged {
|
||||
agent_name: agent_name.clone(),
|
||||
model_id,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn link_child_conversation(&mut self, agent_name: &str, conversation_id: AIConversationId) {
|
||||
if !link_run_agents_child(&mut self.children, agent_name, conversation_id) {
|
||||
log::warn!(
|
||||
@@ -1207,7 +1309,8 @@ impl View for RunAgentsCardView {
|
||||
}
|
||||
|
||||
let is_blocked = matches!(status, Some(AIActionStatus::Blocked));
|
||||
let card = render_confirmation_card(&self.state, &self.handles, is_blocked, app);
|
||||
let card =
|
||||
render_confirmation_card(&self.state, &self.handles, &self.children, is_blocked, app);
|
||||
|
||||
let mut root_stack = Stack::new();
|
||||
root_stack.add_child(card);
|
||||
@@ -1292,6 +1395,20 @@ impl TypedActionView for RunAgentsCardView {
|
||||
self.refresh_accept_button_state(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
RunAgentsCardViewAction::AgentModelChanged {
|
||||
agent_name,
|
||||
model_id,
|
||||
} => {
|
||||
if let Some(config) = self
|
||||
.state
|
||||
.agent_run_configs
|
||||
.iter_mut()
|
||||
.find(|config| config.name == *agent_name)
|
||||
{
|
||||
config.model_id = model_id.clone();
|
||||
}
|
||||
ctx.notify();
|
||||
}
|
||||
RunAgentsCardViewAction::HarnessChanged { harness_type } => {
|
||||
let block_model = self.block_model.clone();
|
||||
oc::apply_harness_change(
|
||||
@@ -1302,6 +1419,8 @@ impl TypedActionView for RunAgentsCardView {
|
||||
|ctx| block_model.base_model(ctx).map(|id| id.to_string()),
|
||||
ctx,
|
||||
);
|
||||
self.reconcile_agent_model_selections(ctx);
|
||||
self.repopulate_agent_model_pickers(ctx);
|
||||
// Harness change resets per-harness selection state, so
|
||||
// give the new harness a fresh auto-open prompt.
|
||||
self.has_auto_opened_create_modal = false;
|
||||
@@ -1441,6 +1560,7 @@ fn diverged_orch_fields_against_config(
|
||||
fn render_confirmation_card(
|
||||
state: &RunAgentsEditState,
|
||||
handles: &RunAgentsCardHandles,
|
||||
children: &[RunAgentsChildState],
|
||||
is_blocked: bool,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
@@ -1448,7 +1568,7 @@ fn render_confirmation_card(
|
||||
let theme = appearance.theme();
|
||||
|
||||
let header = render_header(handles, app);
|
||||
let body = render_body(state, app);
|
||||
let body = render_body(state, children, app);
|
||||
|
||||
let mut content = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
@@ -1492,13 +1612,17 @@ fn render_header(handles: &RunAgentsCardHandles, app: &AppContext) -> Box<dyn El
|
||||
config.render(app)
|
||||
}
|
||||
|
||||
fn render_body(state: &RunAgentsEditState, app: &AppContext) -> Box<dyn Element> {
|
||||
fn render_body(
|
||||
state: &RunAgentsEditState,
|
||||
children: &[RunAgentsChildState],
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let mut column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
||||
|
||||
column.add_child(render_summary(state, appearance));
|
||||
column.add_child(render_agents_section(state, app));
|
||||
column.add_child(render_agents_section(state, children, app));
|
||||
|
||||
Container::new(column.finish())
|
||||
.with_horizontal_padding(16.)
|
||||
@@ -1532,7 +1656,11 @@ fn render_summary(state: &RunAgentsEditState, appearance: &Appearance) -> Box<dy
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_agents_section(state: &RunAgentsEditState, app: &AppContext) -> Box<dyn Element> {
|
||||
fn render_agents_section(
|
||||
state: &RunAgentsEditState,
|
||||
children: &[RunAgentsChildState],
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let label = Text::new(
|
||||
@@ -1543,22 +1671,56 @@ fn render_agents_section(state: &RunAgentsEditState, app: &AppContext) -> Box<dy
|
||||
.with_color(blended_colors::text_disabled(theme, theme.background()))
|
||||
.finish();
|
||||
|
||||
let pills_row = Wrap::row()
|
||||
let agent_cards = Wrap::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_spacing(4.)
|
||||
.with_run_spacing(4.)
|
||||
.with_spacing(12.)
|
||||
.with_run_spacing(8.)
|
||||
.with_children(
|
||||
state
|
||||
.agent_run_configs
|
||||
.iter()
|
||||
.map(|cfg| render_static_agent_pill(&cfg.name, app)),
|
||||
.zip(children)
|
||||
.map(|(config, child)| render_agent_model_card(config, child, app)),
|
||||
)
|
||||
.finish();
|
||||
|
||||
Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(Container::new(label).with_margin_bottom(6.).finish())
|
||||
.with_child(pills_row)
|
||||
.with_child(agent_cards)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_agent_model_card(
|
||||
config: &RunAgentsAgentRunConfig,
|
||||
child: &RunAgentsChildState,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let model_label = Text::new(
|
||||
"Model".to_string(),
|
||||
appearance.ui_font_family(),
|
||||
appearance.monospace_font_size() - 1.,
|
||||
)
|
||||
.with_color(blended_colors::text_disabled(
|
||||
appearance.theme(),
|
||||
appearance.theme().background(),
|
||||
))
|
||||
.finish();
|
||||
let picker = child
|
||||
.model_picker
|
||||
.as_ref()
|
||||
.map(|picker| ChildView::new(picker).finish())
|
||||
.unwrap_or_else(|| Empty::new().finish());
|
||||
let content = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(render_static_agent_pill(&config.name, app))
|
||||
.with_child(Container::new(model_label).with_margin_top(6.).finish())
|
||||
.with_child(picker)
|
||||
.finish();
|
||||
|
||||
ConstrainedBox::new(content)
|
||||
.with_width(oc::ORCHESTRATION_PICKER_MAX_WIDTH)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@@ -1865,7 +2027,6 @@ fn render_editor(
|
||||
handles: &RunAgentsCardHandles,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
use warpui::elements::ConstrainedBox;
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let mut column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
||||
@@ -1889,11 +2050,24 @@ fn render_editor(
|
||||
.with_margin_top(12.)
|
||||
.finish(),
|
||||
);
|
||||
column.add_child(oc::render_picker_row(
|
||||
&state.orch,
|
||||
&handles.pickers,
|
||||
appearance,
|
||||
));
|
||||
let harness_picker = handles
|
||||
.pickers
|
||||
.harness_picker
|
||||
.as_ref()
|
||||
.map(|picker| ChildView::new(picker).finish());
|
||||
column.add_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(oc::render_picker_column(
|
||||
"Agent harness",
|
||||
harness_picker,
|
||||
appearance,
|
||||
))
|
||||
.with_width(oc::ORCHESTRATION_PICKER_MAX_WIDTH)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(12.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
if let Some(reason) = oc::accept_disabled_reason_with_auth(&state.orch, app) {
|
||||
column.add_child(oc::render_validation_error(
|
||||
|
||||
@@ -37,6 +37,7 @@ fn make_request_with_skills(
|
||||
name: "child".to_string(),
|
||||
prompt: "do work".to_string(),
|
||||
title: "Child agent".to_string(),
|
||||
model_id: "auto".to_string(),
|
||||
}],
|
||||
plan_id: String::new(),
|
||||
harness_auth_secret_name: None,
|
||||
@@ -256,6 +257,21 @@ fn to_request_preserves_fields_but_normalizes_execution_to_local() {
|
||||
assert_eq!(round_tripped.plan_id, req.plan_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_child_without_model_inherits_the_run_model() {
|
||||
let mut request = make_request("oz", RunAgentsExecutionMode::Local);
|
||||
request.model_id = "fallback-model".to_string();
|
||||
request.agent_run_configs[0].model_id.clear();
|
||||
|
||||
let state = RunAgentsEditState::from_request(&request);
|
||||
|
||||
assert_eq!(state.agent_run_configs[0].model_id, "fallback-model");
|
||||
assert_eq!(
|
||||
state.to_request().agent_run_configs[0].model_id,
|
||||
"fallback-model"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_child_links_and_removal_survive_streaming_config_sync() {
|
||||
let first_id = AIConversationId::new();
|
||||
@@ -275,11 +291,13 @@ fn live_child_links_and_removal_survive_streaming_config_sync() {
|
||||
name: "gamma".to_string(),
|
||||
prompt: "new work".to_string(),
|
||||
title: String::new(),
|
||||
model_id: "model-gamma".to_string(),
|
||||
},
|
||||
RunAgentsAgentRunConfig {
|
||||
name: "alpha".to_string(),
|
||||
prompt: "updated work".to_string(),
|
||||
title: String::new(),
|
||||
model_id: "model-alpha".to_string(),
|
||||
},
|
||||
];
|
||||
sync_run_agents_children(&mut children, &configs);
|
||||
|
||||
Reference in New Issue
Block a user