Improve orchestration agent model selection
This commit is contained in:
@@ -166,6 +166,7 @@ fn convert_run_agents(
|
||||
name: config.name,
|
||||
prompt: config.prompt,
|
||||
title: config.title,
|
||||
model_id: String::new(),
|
||||
})
|
||||
.collect(),
|
||||
plan_id,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -47,7 +47,7 @@ pub struct HarnessAvailability {
|
||||
fn default_harnesses() -> Vec<HarnessAvailability> {
|
||||
vec![HarnessAvailability {
|
||||
harness: Harness::Oz,
|
||||
display_name: "Warp".to_string(),
|
||||
display_name: harness_display::display_name(Harness::Oz).to_string(),
|
||||
enabled: true,
|
||||
available_models: vec![],
|
||||
}]
|
||||
@@ -144,6 +144,9 @@ impl HarnessAvailabilityModel {
|
||||
}
|
||||
|
||||
pub fn display_name_for(&self, harness: Harness) -> &str {
|
||||
if harness == Harness::Oz {
|
||||
return harness_display::display_name(harness);
|
||||
}
|
||||
self.harnesses
|
||||
.iter()
|
||||
.find(|h| h.harness == harness)
|
||||
|
||||
@@ -15,3 +15,4 @@ pub(crate) use provider_run_coordinator::{
|
||||
pub(crate) use rig::{
|
||||
prepare_provider_run, provider_runtime_for_request, PreparedProviderRun, ProviderActionContext,
|
||||
};
|
||||
pub(crate) use rig_request::OrchestrationModelOption;
|
||||
|
||||
@@ -13,8 +13,9 @@ use uuid::Uuid;
|
||||
use warp_multi_agent_api::ToolType;
|
||||
|
||||
use super::rig_request::{
|
||||
prepare_bedrock_rig_turn_for_mode, prepare_rig_turn, prepare_rig_turn_for_mode, MCPToolTarget,
|
||||
PreparedRigTurn, RigRequestMode,
|
||||
add_orchestration_model_options, prepare_bedrock_rig_turn_for_mode, prepare_rig_turn,
|
||||
prepare_rig_turn_for_mode, MCPToolTarget, OrchestrationModelOption, PreparedRigTurn,
|
||||
RigRequestMode,
|
||||
};
|
||||
use super::rig_tool::action_from_tool_call;
|
||||
use super::ProviderRunProfile;
|
||||
@@ -76,6 +77,7 @@ pub(crate) async fn prepare_provider_run(
|
||||
base_provider_config: crate::ai::provider::ProviderConfig,
|
||||
cli_provider_config: crate::ai::provider::ProviderConfig,
|
||||
mut params: RequestParams,
|
||||
orchestration_models: Vec<OrchestrationModelOption>,
|
||||
) -> anyhow::Result<PreparedProviderRun> {
|
||||
let (supported_tools, supported_cli_agent_tools) =
|
||||
crate::ai::agent::api::prepare_direct_provider_params(&mut params);
|
||||
@@ -112,7 +114,7 @@ pub(crate) async fn prepare_provider_run(
|
||||
}
|
||||
};
|
||||
|
||||
let (base_runtime, prepared) = prepare_provider_profile(
|
||||
let (base_runtime, mut prepared) = prepare_provider_profile(
|
||||
base_provider_config,
|
||||
params,
|
||||
supported_tools.clone(),
|
||||
@@ -120,6 +122,7 @@ pub(crate) async fn prepare_provider_run(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
add_orchestration_model_options(&mut prepared.request.tools, &orchestration_models);
|
||||
let (cli_runtime, cli_prepared) = prepare_provider_profile(
|
||||
cli_provider_config,
|
||||
cli_params,
|
||||
|
||||
@@ -33,6 +33,91 @@ pub(crate) struct PreparedRigTurn {
|
||||
pub mcp_tool_aliases: HashMap<String, MCPToolTarget>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct OrchestrationModelOption {
|
||||
pub id: String,
|
||||
pub display_name: String,
|
||||
pub provider: String,
|
||||
pub quality: Option<f32>,
|
||||
pub cost: Option<f32>,
|
||||
pub credit_multiplier: Option<f32>,
|
||||
}
|
||||
|
||||
pub(crate) fn add_orchestration_model_options(
|
||||
tools: &mut [ToolDefinition],
|
||||
models: &[OrchestrationModelOption],
|
||||
) {
|
||||
if models.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(tool) = tools.iter_mut().find(|tool| tool.name == "run_agents") else {
|
||||
return;
|
||||
};
|
||||
|
||||
let catalog = models
|
||||
.iter()
|
||||
.map(|model| {
|
||||
let mut details = vec![
|
||||
format!("id={:?}", model.id),
|
||||
format!("name={:?}", model.display_name),
|
||||
format!("provider={:?}", model.provider),
|
||||
];
|
||||
if let Some(quality) = model.quality {
|
||||
details.push(format!("quality_score={quality:.2}"));
|
||||
}
|
||||
if let Some(cost) = model.cost {
|
||||
details.push(format!("cost_score={cost:.2}"));
|
||||
}
|
||||
if let Some(multiplier) = model.credit_multiplier {
|
||||
details.push(format!("credit_multiplier={multiplier:.2}x"));
|
||||
}
|
||||
format!("- {}", details.join(", "))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let description = format!(
|
||||
"Required model for this child. Select exactly one available model ID. Prioritize the model best suited to the child's task and most likely to succeed. Among similarly capable models, prefer the lower-cost option; do not sacrifice material capability merely to choose the cheapest model. Cost scores represent relative consumption, with higher values costing more.\nAvailable models:\n{catalog}"
|
||||
);
|
||||
let model_ids = models
|
||||
.iter()
|
||||
.map(|model| serde_json::Value::String(model.id.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let Some(agent_items) = tool
|
||||
.input_schema
|
||||
.get_mut("properties")
|
||||
.and_then(|properties| properties.get_mut("agent_run_configs"))
|
||||
.and_then(|configs| configs.get_mut("items"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(properties) = agent_items
|
||||
.get_mut("properties")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
properties.insert(
|
||||
"model_id".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "string",
|
||||
"enum": model_ids,
|
||||
"description": description,
|
||||
}),
|
||||
);
|
||||
let Some(required) = agent_items
|
||||
.get_mut("required")
|
||||
.and_then(serde_json::Value::as_array_mut)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !required.iter().any(|field| field == "model_id") {
|
||||
required.push(serde_json::Value::String("model_id".to_string()));
|
||||
}
|
||||
|
||||
tool.description = "Start one or more child agents. Assign each child the best-fit, cost-effective model from its required model_id choices, prioritizing capability and likelihood of success over price. Use independent, uniquely named tasks and do not launch duplicate agents for follow-up work. After launch, call wait_for_events when you need child-agent results instead of repeating their work yourself.".to_string();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct MCPToolTarget {
|
||||
pub server_id: Option<Uuid>,
|
||||
|
||||
@@ -8,8 +8,8 @@ use galaxy_util::local_or_remote_path::LocalOrRemotePath;
|
||||
use warp_multi_agent_api::ToolType;
|
||||
|
||||
use super::{
|
||||
input_messages, prepare_bedrock_rig_turn, prepare_rig_turn, prepare_rig_turn_for_mode,
|
||||
tool_definitions, RigRequestMode,
|
||||
add_orchestration_model_options, input_messages, prepare_bedrock_rig_turn, prepare_rig_turn,
|
||||
prepare_rig_turn_for_mode, tool_definitions, OrchestrationModelOption, RigRequestMode,
|
||||
};
|
||||
use crate::ai::agent::api::RequestParams;
|
||||
use crate::ai::agent::task::TaskId;
|
||||
@@ -631,6 +631,51 @@ fn modern_and_legacy_orchestration_tools_follow_subagent_capabilities() {
|
||||
.any(|tool| matches!(tool.name.as_str(), "run_agents" | "start_agent")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_agents_tool_requires_a_best_fit_model_from_the_available_catalog() {
|
||||
let (mut tools, _) = tool_definitions(&[ToolType::Subagent], None);
|
||||
add_orchestration_model_options(
|
||||
&mut tools,
|
||||
&[
|
||||
OrchestrationModelOption {
|
||||
id: "strong-model".to_string(),
|
||||
display_name: "Strong Model".to_string(),
|
||||
provider: "Provider A".to_string(),
|
||||
quality: Some(0.95),
|
||||
cost: Some(0.8),
|
||||
credit_multiplier: Some(2.0),
|
||||
},
|
||||
OrchestrationModelOption {
|
||||
id: "efficient-model".to_string(),
|
||||
display_name: "Efficient Model".to_string(),
|
||||
provider: "Provider B".to_string(),
|
||||
quality: Some(0.9),
|
||||
cost: Some(0.3),
|
||||
credit_multiplier: Some(0.5),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
let run_agents = tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "run_agents")
|
||||
.expect("run_agents tool");
|
||||
let model = &run_agents.input_schema["properties"]["agent_run_configs"]["items"]["properties"]
|
||||
["model_id"];
|
||||
assert_eq!(
|
||||
model["enum"],
|
||||
serde_json::json!(["strong-model", "efficient-model"])
|
||||
);
|
||||
assert!(model["description"]
|
||||
.as_str()
|
||||
.is_some_and(|description| description.contains("Prioritize the model best suited")));
|
||||
assert!(
|
||||
run_agents.input_schema["properties"]["agent_run_configs"]["items"]["required"]
|
||||
.as_array()
|
||||
.is_some_and(|required| required.iter().any(|field| field == "model_id"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn grouped_mcp_tool_names_use_the_installation_id_not_the_display_name() {
|
||||
|
||||
@@ -31,6 +31,7 @@ async fn missing_cli_provider_route_falls_back_to_base_provider_profile() {
|
||||
ProviderConfig::OpenAI(openai_config("base-provider-model")),
|
||||
ProviderConfig::None,
|
||||
params,
|
||||
Vec::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -181,6 +181,7 @@ pub(crate) fn action_from_tool_call(
|
||||
name: required_nonempty_string(config, "name")?,
|
||||
prompt: required_nonempty_string(config, "prompt")?,
|
||||
title: optional_string(config, "title")?.unwrap_or_default(),
|
||||
model_id: optional_string(config, "model_id")?.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, String>>()?,
|
||||
|
||||
@@ -444,7 +444,8 @@ fn run_agents_calls_decode_to_local_domain_requests_with_safe_defaults() {
|
||||
{
|
||||
"name": "runtime",
|
||||
"prompt": "Inspect runtime behavior",
|
||||
"title": "Runtime investigator"
|
||||
"title": "Runtime investigator",
|
||||
"model_id": "strong-model"
|
||||
},
|
||||
{
|
||||
"name": "tests",
|
||||
@@ -476,9 +477,11 @@ fn run_agents_calls_decode_to_local_domain_requests_with_safe_defaults() {
|
||||
"Inspect runtime behavior"
|
||||
);
|
||||
assert_eq!(request.agent_run_configs[0].title, "Runtime investigator");
|
||||
assert_eq!(request.agent_run_configs[0].model_id, "strong-model");
|
||||
assert_eq!(request.agent_run_configs[1].name, "tests");
|
||||
assert_eq!(request.agent_run_configs[1].prompt, "Design focused tests");
|
||||
assert!(request.agent_run_configs[1].title.is_empty());
|
||||
assert!(request.agent_run_configs[1].model_id.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user