Skip to content

Commit 9cf4e09

Browse files
committed
feat(subagent): support explicit parent model inheritance
- interpret model_id=inherit as an explicit parent-session model override - apply the override to fresh, forked, and resumed subagent sessions - document model selection, configured model discovery, and model-slot semantics - cover inheritance parsing and subagent model-resolution paths
1 parent 547209c commit 9cf4e09

5 files changed

Lines changed: 239 additions & 21 deletions

File tree

src/crates/assembly/core/src/agentic/coordination/coordinator.rs

Lines changed: 182 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ pub(crate) struct SubagentExecutionRequest {
254254
pub(crate) model_binding_policy: SessionModelBindingPolicy,
255255
pub(crate) workspace_path: Option<String>,
256256
pub(crate) model_id: Option<String>,
257+
/// Explicitly select the current parent session's model instead of a
258+
/// configured subagent default.
259+
pub(crate) inherit_parent_model: bool,
257260
pub(crate) subagent_parent_info: SubagentParentInfo,
258261
pub(crate) context: HashMap<String, String>,
259262
/// Execution policy for the child subagent session being launched.
@@ -6416,11 +6419,18 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
64166419
async fn resolve_fresh_subagent_model_id(
64176420
&self,
64186421
explicit_model_id: Option<&str>,
6422+
inherit_parent_model: bool,
64196423
agent_type: &str,
64206424
workspace_path: &str,
64216425
parent_session_id: &str,
64226426
) -> BitFunResult<String> {
64236427
let defaults = Self::agent_model_defaults().await;
6428+
if inherit_parent_model {
6429+
return normalize_model_selection(
6430+
&self.parent_model_selection(parent_session_id, &defaults)?,
6431+
)
6432+
.await;
6433+
}
64246434
let registry = get_agent_registry();
64256435
let configured_selection = registry
64266436
.get_explicit_subagent_model_selection(agent_type, Some(Path::new(workspace_path)))
@@ -6459,6 +6469,13 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
64596469
.map(str::trim)
64606470
.filter(|model_id| !model_id.is_empty())
64616471
.map(str::to_string);
6472+
let inherit_parent_model = request.inherit_parent_model;
6473+
if inherit_parent_model && model_id.is_some() {
6474+
return Err(BitFunError::Validation(
6475+
"A subagent model request cannot specify both a model ID and parent inheritance"
6476+
.to_string(),
6477+
));
6478+
}
64626479
let created_by = Some(format!(
64636480
"session-{}",
64646481
request.subagent_parent_info.session_id
@@ -6492,8 +6509,20 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
64926509
background_allow_review_follow_up,
64936510
)
64946511
.await?;
6495-
if let Some(model_id) = model_id.as_deref() {
6496-
let model_id = normalize_model_selection(model_id).await?;
6512+
let requested_model_id = if inherit_parent_model {
6513+
let defaults = Self::agent_model_defaults().await;
6514+
Some(
6515+
normalize_model_selection(
6516+
&self.parent_model_selection(&parent_session_id, &defaults)?,
6517+
)
6518+
.await?,
6519+
)
6520+
} else if let Some(model_id) = model_id.as_deref() {
6521+
Some(normalize_model_selection(model_id).await?)
6522+
} else {
6523+
None
6524+
};
6525+
if let Some(model_id) = requested_model_id {
64976526
let session_id = session.session_id.clone();
64986527
self.session_manager
64996528
.update_session_model_id(&session_id, &model_id)
@@ -6560,7 +6589,7 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
65606589
request.model_binding_policy,
65616590
SessionModelBindingPolicy::ApprovedImmutable
65626591
) {
6563-
if model_id.is_some() {
6592+
if model_id.is_some() || inherit_parent_model {
65646593
return Err(BitFunError::Validation(
65656594
"An approved immutable subagent model cannot be overridden".to_string(),
65666595
));
@@ -6577,6 +6606,7 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
65776606
} else {
65786607
self.resolve_fresh_subagent_model_id(
65796608
model_id.as_deref(),
6609+
inherit_parent_model,
65806610
&agent_type,
65816611
&workspace_path,
65826612
&request.subagent_parent_info.session_id,
@@ -6649,8 +6679,9 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
66496679
.await?;
66506680
}
66516681
let defaults = Self::agent_model_defaults().await;
6652-
let parent_model_id = if model_id.is_none()
6653-
&& matches!(&defaults.subagents.fork, SubagentModelSelection::Inherit)
6682+
let parent_model_id = if inherit_parent_model
6683+
|| (model_id.is_none()
6684+
&& matches!(&defaults.subagents.fork, SubagentModelSelection::Inherit))
66546685
{
66556686
Some(
66566687
trimmed_model_id(snapshot.session_model_id.as_deref())
@@ -6665,11 +6696,19 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
66656696
} else {
66666697
None
66676698
};
6668-
let model_selection = resolve_subagent_model_selection(
6669-
model_id.as_deref(),
6670-
&defaults.subagents.fork,
6671-
parent_model_id.as_deref(),
6672-
)?;
6699+
let model_selection = if inherit_parent_model {
6700+
parent_model_id.ok_or_else(|| {
6701+
BitFunError::Validation(
6702+
"Fork parent session has no model selection".to_string(),
6703+
)
6704+
})?
6705+
} else {
6706+
resolve_subagent_model_selection(
6707+
model_id.as_deref(),
6708+
&defaults.subagents.fork,
6709+
parent_model_id.as_deref(),
6710+
)?
6711+
};
66736712
let resolved_model_id = normalize_model_selection(&model_selection).await?;
66746713
let mut session_config = snapshot.build_child_session_config(None);
66756714
session_config.model_id = Some(resolved_model_id);
@@ -9237,6 +9276,7 @@ mod tests {
92379276
model_binding_policy: SessionModelBindingPolicy::Mutable,
92389277
workspace_path: Some(workspace_path.to_string_lossy().into_owned()),
92399278
model_id: None,
9279+
inherit_parent_model: false,
92409280
subagent_parent_info: SubagentParentInfo {
92419281
session_id: "parent-session".to_string(),
92429282
dialog_turn_id: "parent-turn".to_string(),
@@ -9289,6 +9329,7 @@ mod tests {
92899329
model_binding_policy: SessionModelBindingPolicy::Mutable,
92909330
workspace_path: None,
92919331
model_id: None,
9332+
inherit_parent_model: false,
92929333
subagent_parent_info: SubagentParentInfo {
92939334
session_id: "parent-session".to_string(),
92949335
dialog_turn_id: "parent-turn".to_string(),
@@ -9383,6 +9424,7 @@ mod tests {
93839424
model_binding_policy: SessionModelBindingPolicy::Mutable,
93849425
workspace_path: None,
93859426
model_id: Some("fast".to_string()),
9427+
inherit_parent_model: false,
93869428
subagent_parent_info: SubagentParentInfo {
93879429
session_id: parent_session.session_id.clone(),
93889430
dialog_turn_id: "parent-turn".to_string(),
@@ -9457,6 +9499,7 @@ mod tests {
94579499
model_binding_policy: SessionModelBindingPolicy::Mutable,
94589500
workspace_path: None,
94599501
model_id: None,
9502+
inherit_parent_model: false,
94609503
subagent_parent_info: SubagentParentInfo {
94619504
session_id: parent_session.session_id.clone(),
94629505
dialog_turn_id: "parent-turn".to_string(),
@@ -9570,6 +9613,7 @@ mod tests {
95709613
model_binding_policy: SessionModelBindingPolicy::Mutable,
95719614
workspace_path: None,
95729615
model_id: None,
9616+
inherit_parent_model: false,
95739617
subagent_parent_info: SubagentParentInfo {
95749618
session_id: parent_session.session_id.clone(),
95759619
dialog_turn_id: "parent-turn".to_string(),
@@ -10309,7 +10353,51 @@ mod tests {
1030910353
}
1031010354

1031110355
#[tokio::test]
10312-
async fn reused_subagent_send_input_updates_requested_model() {
10356+
async fn fresh_subagent_request_can_explicitly_inherit_parent_model() {
10357+
let (coordinator, session_manager) = test_coordinator();
10358+
let workspace_path = std::env::temp_dir().join(format!(
10359+
"bitfun-fresh-subagent-inherit-test-{}",
10360+
uuid::Uuid::new_v4()
10361+
));
10362+
std::fs::create_dir_all(&workspace_path).expect("workspace dir should exist");
10363+
struct TempWorkspaceGuard(std::path::PathBuf);
10364+
impl Drop for TempWorkspaceGuard {
10365+
fn drop(&mut self) {
10366+
let _ = std::fs::remove_dir_all(&self.0);
10367+
}
10368+
}
10369+
let _workspace_guard = TempWorkspaceGuard(workspace_path.clone());
10370+
let parent_session = session_manager
10371+
.create_session(
10372+
"Parent".to_string(),
10373+
"agentic".to_string(),
10374+
SessionConfig {
10375+
model_id: Some("primary".to_string()),
10376+
workspace_path: Some(workspace_path.to_string_lossy().into_owned()),
10377+
..Default::default()
10378+
},
10379+
)
10380+
.await
10381+
.expect("parent session should be created");
10382+
10383+
let model_id = coordinator
10384+
.resolve_fresh_subagent_model_id(
10385+
None,
10386+
true,
10387+
"Explore",
10388+
workspace_path
10389+
.to_str()
10390+
.expect("workspace path should be UTF-8"),
10391+
&parent_session.session_id,
10392+
)
10393+
.await
10394+
.expect("fresh subagent request should inherit the parent model");
10395+
10396+
assert_eq!(model_id, "primary");
10397+
}
10398+
10399+
#[tokio::test]
10400+
async fn reused_subagent_send_input_updates_requested_and_inherited_model() {
1031310401
let (coordinator, session_manager) = test_coordinator();
1031410402
let workspace_path = std::env::temp_dir().join(format!(
1031510403
"bitfun-reused-subagent-model-test-{}",
@@ -10324,6 +10412,19 @@ mod tests {
1032410412
}
1032510413
let _workspace_guard = TempWorkspaceGuard(workspace_path.clone());
1032610414

10415+
let parent_session = session_manager
10416+
.create_session(
10417+
"Parent".to_string(),
10418+
"agentic".to_string(),
10419+
SessionConfig {
10420+
model_id: Some("primary".to_string()),
10421+
workspace_path: Some(workspace_path.to_string_lossy().into_owned()),
10422+
..Default::default()
10423+
},
10424+
)
10425+
.await
10426+
.expect("parent session should be created");
10427+
1032710428
let subagent_session = coordinator
1032810429
.create_hidden_agent_session(
1032910430
None,
@@ -10334,7 +10435,7 @@ mod tests {
1033410435
workspace_path: Some(workspace_path.to_string_lossy().into_owned()),
1033510436
..Default::default()
1033610437
},
10337-
Some("session-parent-session".to_string()),
10438+
Some(format!("session-{}", parent_session.session_id)),
1033810439
SessionKind::Subagent,
1033910440
)
1034010441
.await
@@ -10350,8 +10451,9 @@ mod tests {
1035010451
model_binding_policy: SessionModelBindingPolicy::Mutable,
1035110452
workspace_path: None,
1035210453
model_id: Some("fast".to_string()),
10454+
inherit_parent_model: false,
1035310455
subagent_parent_info: SubagentParentInfo {
10354-
session_id: "parent-session".to_string(),
10456+
session_id: parent_session.session_id.clone(),
1035510457
dialog_turn_id: "parent-turn".to_string(),
1035610458
tool_call_id: "task-tool".to_string(),
1035710459
},
@@ -10375,6 +10477,43 @@ mod tests {
1037510477
.as_deref(),
1037610478
Some("fast")
1037710479
);
10480+
10481+
let inherit_request = SubagentExecutionRequest {
10482+
task_description: "Continue with the parent model".to_string(),
10483+
context_mode: SubagentContextMode::Fresh,
10484+
target_session_id: Some(subagent_session.session_id.clone()),
10485+
subagent_type: None,
10486+
logical_subagent_type: None,
10487+
continuation_policy: SessionContinuationPolicy::Reusable,
10488+
model_binding_policy: SessionModelBindingPolicy::Mutable,
10489+
workspace_path: None,
10490+
model_id: None,
10491+
inherit_parent_model: true,
10492+
subagent_parent_info: SubagentParentInfo {
10493+
session_id: parent_session.session_id.clone(),
10494+
dialog_turn_id: "parent-turn".to_string(),
10495+
tool_call_id: "task-tool".to_string(),
10496+
},
10497+
context: HashMap::new(),
10498+
delegation_policy: DelegationPolicy::top_level().spawn_child(),
10499+
external_generation_lease: None,
10500+
};
10501+
10502+
let prepared = coordinator
10503+
.prepare_subagent_execution_request(inherit_request)
10504+
.await
10505+
.expect("send_input request should inherit the parent model");
10506+
10507+
assert_eq!(prepared.session_config.model_id.as_deref(), Some("primary"));
10508+
assert_eq!(
10509+
session_manager
10510+
.get_session(&subagent_session.session_id)
10511+
.expect("subagent session should remain available")
10512+
.config
10513+
.model_id
10514+
.as_deref(),
10515+
Some("primary")
10516+
);
1037810517
}
1037910518

1038010519
#[tokio::test]
@@ -10396,7 +10535,7 @@ mod tests {
1039610535
"Parent".to_string(),
1039710536
"agentic".to_string(),
1039810537
SessionConfig {
10399-
model_id: Some("parent-model".to_string()),
10538+
model_id: Some("primary".to_string()),
1040010539
workspace_path: Some(workspace_path.to_string_lossy().into_owned()),
1040110540
..Default::default()
1040210541
},
@@ -10420,6 +10559,7 @@ mod tests {
1042010559
model_binding_policy: SessionModelBindingPolicy::Mutable,
1042110560
workspace_path: None,
1042210561
model_id: Some("fast".to_string()),
10562+
inherit_parent_model: false,
1042310563
subagent_parent_info: SubagentParentInfo {
1042410564
session_id: parent_session.session_id.clone(),
1042510565
dialog_turn_id: "parent-turn".to_string(),
@@ -10449,6 +10589,34 @@ mod tests {
1044910589
.as_deref(),
1045010590
Some("fast")
1045110591
);
10592+
10593+
let inherit_request = SubagentExecutionRequest {
10594+
task_description: "Fork with the parent model".to_string(),
10595+
context_mode: SubagentContextMode::Fork,
10596+
target_session_id: None,
10597+
subagent_type: None,
10598+
logical_subagent_type: None,
10599+
continuation_policy: SessionContinuationPolicy::Reusable,
10600+
model_binding_policy: SessionModelBindingPolicy::Mutable,
10601+
workspace_path: None,
10602+
model_id: None,
10603+
inherit_parent_model: true,
10604+
subagent_parent_info: SubagentParentInfo {
10605+
session_id: parent_session.session_id.clone(),
10606+
dialog_turn_id: "parent-turn".to_string(),
10607+
tool_call_id: "task-tool".to_string(),
10608+
},
10609+
context: HashMap::new(),
10610+
delegation_policy: DelegationPolicy::top_level().spawn_child(),
10611+
external_generation_lease: None,
10612+
};
10613+
10614+
let prepared = coordinator
10615+
.prepare_subagent_execution_request(inherit_request)
10616+
.await
10617+
.expect("fork request should inherit the parent model");
10618+
10619+
assert_eq!(prepared.session_config.model_id.as_deref(), Some("primary"));
1045210620
}
1045310621

1045410622
#[tokio::test]

0 commit comments

Comments
 (0)