Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
523 changes: 510 additions & 13 deletions src/crates/assembly/core/src/agentic/coordination/coordinator.rs

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions src/crates/assembly/core/src/service/config/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,48 @@ mod tests {
assert!(error.to_string().contains("agent_model_defaults.mode"));
}

#[tokio::test]
async fn enforced_subagent_default_flag_validates_and_is_never_dropped() {
// The legacy surface only validates while no runtime selections exist.
let mut config = AIConfig::default();
config.agent_model_defaults.subagents.enforce_default = true;
let value = serde_json::to_value(&config).unwrap();
assert_eq!(
value["agent_model_defaults"]["subagents"]["enforce_default"],
serde_json::json!(true)
);
AIConfigProvider
.validate_config(&value)
.await
.expect("a boolean enforcement flag has nothing to validate");
let round_tripped: AIConfig = serde_json::from_value(value).unwrap();
assert!(round_tripped.agent_model_defaults.subagents.enforce_default);

// The runtime surface keeps the flag through validation as well.
let mut selections =
openbitfun_config_contracts::model_pool::AgentModelSelections::default();
selections.subagents.enforce_default = true;
let mut config = AIConfig::default();
config.set_model_selections(selections).unwrap();
let value = serde_json::to_value(&config).unwrap();
assert_eq!(
value["agent_model_selections"]["subagents"]["enforce_default"],
serde_json::json!(true)
);
AIConfigProvider
.validate_config(&value)
.await
.expect("the runtime selections remain valid");
let round_tripped: AIConfig = serde_json::from_value(value).unwrap();
assert!(
round_tripped
.effective_model_selections()
.unwrap()
.subagents
.enforce_default
);
}

#[tokio::test]
async fn rejects_invalid_canonical_reasoning_actions() {
for (action, expected) in [
Expand Down
103 changes: 103 additions & 0 deletions src/crates/assembly/core/src/service/config/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ fn translate_legacy_model_selection_write(
SubagentModelSelection,
>(value)?))?
}
// A raw boolean is not expressible as a legacy model selection, so it is
// passed through unchanged instead of being projected through the enum.
".subagents.enforce_default" => {
if value.is_boolean() {
value
} else {
return Err(OpenBitFunError::validation(
"ai.agent_model_defaults.subagents.enforce_default must be a boolean",
));
}
}
_ if suffix.starts_with(".subagents.builtin.") => {
if value.is_null() {
value
Expand Down Expand Up @@ -1262,6 +1273,98 @@ mod tests {
);
}

#[tokio::test]
async fn legacy_subagent_enforce_default_write_lands_on_runtime_selections() {
let (service, dir) = test_service("subagent-enforce-default").await;
service
.add_ai_model(runtime_model("chosen", "fixture-key"))
.await
.unwrap();
let legacy_before: serde_json::Value = service
.get_config(Some("ai.agent_model_defaults"))
.await
.unwrap();
assert!(!service
.get_config::<bool>(Some("ai.agent_model_selections.subagents.enforce_default"))
.await
.unwrap());

assert!(service
.set_config_if_changed(
"ai.agent_model_defaults.subagents.enforce_default",
serde_json::json!(true),
)
.await
.unwrap());
assert!(service
.get_config::<bool>(Some("ai.agent_model_selections.subagents.enforce_default"))
.await
.unwrap());
let selections: openbitfun_config_contracts::model_pool::AgentModelSelections = service
.get_config(Some("ai.agent_model_selections"))
.await
.unwrap();
assert!(selections.subagents.enforce_default);
// The frozen legacy surface is read-only and must not be rewritten.
assert_eq!(
service
.get_config::<serde_json::Value>(Some("ai.agent_model_defaults"))
.await
.unwrap(),
legacy_before
);

// Whole-object writes carry the flag through the same translation.
service
.set_config(
"ai.agent_model_defaults.subagents",
serde_json::json!({
"default": {"kind": "fixed", "model_id": "chosen"},
"builtin": {},
"fork": {"kind": "inherit"},
"enforce_default": true,
}),
)
.await
.unwrap();
let selections: openbitfun_config_contracts::model_pool::AgentModelSelections = service
.get_config(Some("ai.agent_model_selections"))
.await
.unwrap();
assert!(selections.subagents.enforce_default);
assert_eq!(
selections.subagents.default,
openbitfun_config_contracts::model_pool::AgentModelSelection::Model {
model_ref: "chosen".into()
}
);

// The flag survives a restart and clearing it restores today's rules.
let restarted = restart_test_service(&dir, "subagent-enforce-default").await;
assert!(restarted
.get_config::<bool>(Some("ai.agent_model_selections.subagents.enforce_default"))
.await
.unwrap());
restarted
.set_config(
"ai.agent_model_defaults.subagents.enforce_default",
serde_json::json!(false),
)
.await
.unwrap();
assert!(!restarted
.get_config::<bool>(Some("ai.agent_model_selections.subagents.enforce_default"))
.await
.unwrap());
assert!(service
.set_config(
"ai.agent_model_defaults.subagents.enforce_default",
serde_json::json!("true"),
)
.await
.is_err());
}

fn realtime_voice_fixture() -> VoiceCallConfig {
VoiceCallConfig {
api_key: "fixture-realtime-voice-key".to_string(),
Expand Down
60 changes: 60 additions & 0 deletions src/crates/contracts/config-contracts/src/model_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ pub struct SubagentModelSelections {
pub default: AgentModelSelection,
pub builtin: BTreeMap<String, AgentModelSelection>,
pub fork: AgentModelSelection,
/// Mirrors `ai.agent_model_defaults.subagents.enforce_default`: pin every
/// internally created subagent to `default`. Missing data means `false`.
#[serde(default)]
pub enforce_default: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -163,6 +167,7 @@ impl Default for AgentModelSelections {
AgentModelSelection::Inherit,
)]),
fork: AgentModelSelection::Inherit,
enforce_default: false,
},
}
}
Expand All @@ -183,6 +188,7 @@ impl AgentModelSelections {
.map(|(id, choice)| (id.clone(), choice.clone().into()))
.collect(),
fork: legacy.subagents.fork.clone().into(),
enforce_default: legacy.subagents.enforce_default,
},
}
}
Expand Down Expand Up @@ -330,6 +336,60 @@ mod tests {
);
}

#[test]
fn subagent_enforce_default_is_absent_by_default_and_round_trips() {
// A document written before the field existed must deserialize to false.
let legacy = json!({
"mode": {"kind": "model", "model_ref": "primary"},
"subagents": {
"default": {"kind": "model", "model_ref": "fast"},
"builtin": {},
"fork": {"kind": "inherit"}
}
});
let selections: AgentModelSelections = serde_json::from_value(legacy.clone()).unwrap();
assert!(!selections.subagents.enforce_default);
assert_eq!(
serde_json::to_value(&selections).unwrap()["subagents"]["enforce_default"],
json!(false)
);

let mut explicit = selections.clone();
explicit.subagents.enforce_default = true;
let saved = serde_json::to_value(&explicit).unwrap();
assert_eq!(saved["subagents"]["enforce_default"], json!(true));
assert_eq!(
serde_json::from_value::<AgentModelSelections>(saved).unwrap(),
explicit
);

// An explicit `false` is still a valid document and keeps today's rules.
let mut disabled = legacy;
disabled["subagents"]["enforce_default"] = json!(false);
assert!(
!serde_json::from_value::<AgentModelSelections>(disabled)
.unwrap()
.subagents
.enforce_default
);
}

#[test]
fn from_legacy_carries_the_enforced_subagent_default_flag() {
for enforce_default in [false, true] {
let mut legacy = crate::types::AIConfig::default();
legacy.agent_model_defaults.subagents.enforce_default = enforce_default;
let migrated = AgentModelSelections::from_legacy(&legacy.agent_model_defaults);
assert_eq!(migrated.subagents.enforce_default, enforce_default);
assert_eq!(
migrated.subagents.default,
AgentModelSelection::Model {
model_ref: "fast".into()
}
);
}
}

#[test]
fn malformed_new_selection_is_retained_and_never_read_as_legacy() {
for malformed in [json!(null), json!({}), json!({"mode":{"kind":"future"}})] {
Expand Down
52 changes: 52 additions & 0 deletions src/crates/contracts/config-contracts/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,10 @@ pub struct SubagentModelDefaultsConfig {
pub builtin: HashMap<String, SubagentModelSelection>,
/// Default choice for a child created from the parent's context.
pub fork: SubagentModelSelection,
/// When true every internally created subagent uses `default`, ignoring
/// explicit requests, `fork` and per-builtin overrides. Absent means false.
#[serde(default)]
pub enforce_default: bool,
}

impl Default for SubagentModelDefaultsConfig {
Expand All @@ -1077,6 +1081,7 @@ impl Default for SubagentModelDefaultsConfig {
SubagentModelSelection::Inherit,
)]),
fork: SubagentModelSelection::Inherit,
enforce_default: false,
}
}
}
Expand Down Expand Up @@ -4137,6 +4142,53 @@ mod tests {
);
}

#[test]
fn subagent_enforce_default_defaults_off_and_round_trips() {
assert!(!super::SubagentModelDefaultsConfig::default().enforce_default);
assert!(
!AgentModelDefaultsConfig::default()
.subagents
.enforce_default
);

// Absent field (older configuration) keeps today's resolution rules.
for payload in [
serde_json::json!({}),
serde_json::json!({"subagents": {}}),
serde_json::json!({"subagents": {"default": {"kind": "inherit"}}}),
] {
let config: AgentModelDefaultsConfig = serde_json::from_value(payload).unwrap();
assert!(!config.subagents.enforce_default);
}

let config: AgentModelDefaultsConfig = serde_json::from_value(serde_json::json!({
"subagents": {"enforce_default": true}
}))
.unwrap();
assert!(config.subagents.enforce_default);
let saved = serde_json::to_value(&config).unwrap();
assert_eq!(
saved["subagents"]["enforce_default"],
serde_json::json!(true)
);
let reloaded: AgentModelDefaultsConfig = serde_json::from_value(saved).unwrap();
assert!(reloaded.subagents.enforce_default);

// A damaged value must stay an error instead of silently enforcing.
for invalid in [
serde_json::json!("true"),
serde_json::Value::Null,
serde_json::json!(1),
] {
assert!(
serde_json::from_value::<AgentModelDefaultsConfig>(serde_json::json!({
"subagents": {"enforce_default": invalid}
}))
.is_err()
);
}
}

#[test]
fn code_font_defaults_follow_frontend_and_preserve_explicit_legacy_fonts() {
let editor: EditorConfig = serde_json::from_str("{}").unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const ExecutionSettingsPage: React.FC = () => {
}
const hasSeededPageData = seed.loaded && Boolean(seededSelections);
const [subagentDefaultModel, setSubagentDefaultModel] = useState<AgentModelTargetSelection>(() => seededSelections?.subagents.default ?? { kind: 'inherit' });
const [enforceSubagentDefaultModel, setEnforceSubagentDefaultModel] = useState(() => seededSelections?.subagents.enforce_default === true);
const [modelPools, setModelPools] = useState<ModelPoolDefinition[]>(() => {
const document = seed.get<ModelPoolDocument | null>('ai.model_pools', null);
return document ? readableModelPools(document) : [];
Expand Down Expand Up @@ -224,6 +225,31 @@ const ExecutionSettingsPage: React.FC = () => {
}
};

const handleSubagentModelEnforcementChange = async (checked: boolean) => {
const scope = getActiveSurfaceScope();
const previousEnforcement = enforceSubagentDefaultModel;
setEnforceSubagentDefaultModel(checked);
setToolExecConfigLoading(true);
try {
if (modelSelectionsConfigPath() === 'ai.agent_model_selections') {
await configManager.setConfig('ai.agent_model_selections.subagents.enforce_default', checked);
} else {
await configManager.setConfig('ai.agent_model_defaults.subagents.enforce_default', checked);
}
if (!scope.isCurrent()) return;
notificationService.success(tTools('messages.saveSuccess'), { duration: 2000 });
} catch (error) {
if (!scope.isCurrent()) return;
log.error('Failed to save subagent model enforcement', error);
setEnforceSubagentDefaultModel(previousEnforcement);
notificationService.error(
`${tTools('messages.saveFailed')}: ${error instanceof Error ? error.message : String(error)}`
);
} finally {
if (scope.isCurrent()) setToolExecConfigLoading(false);
}
};

const handleSwarmMaxConcurrencyChange = async (input: number) => {
if (!Number.isFinite(input)) return;
const value = Math.min(SWARM_MAX_CONCURRENCY_LIMIT, Math.max(1, Math.round(input)));
Expand Down Expand Up @@ -332,6 +358,7 @@ const ExecutionSettingsPage: React.FC = () => {
supportsModelPools() ? configManager.getOptionalConfig<ModelPoolDocument>('ai.model_pools') : Promise.resolve(undefined),
]);
setSubagentDefaultModel(loadedSubagentDefaultModel.subagents.default);
setEnforceSubagentDefaultModel(loadedSubagentDefaultModel.subagents.enforce_default === true);
setModelPools(poolDocument ? readableModelPools(poolDocument) : []);
setConfiguredModels(loadedModels ?? []);
setEnableDeferredToolLoading(deferredToolLoadingEnabled ?? true);
Expand Down Expand Up @@ -512,6 +539,21 @@ const ExecutionSettingsPage: React.FC = () => {
/>
</div>
</ConfigPageRow>
<ConfigPageRow
label={tTools('config.subagentModelEnforcement')}
description={enforceSubagentDefaultModel && subagentDefaultModel.kind === 'inherit'
? tTools('config.subagentModelEnforcementInheritWarning')
: tTools('config.subagentModelEnforcementDesc')}
align="center"
>
<div className="openbitfun-runtime-settings__row-control" data-openbitfun-component="runtime-settings" data-openbitfun-part="control">
<Switch
checked={enforceSubagentDefaultModel}
onChange={(event) => void handleSubagentModelEnforcementChange(event.target.checked)}
disabled={toolExecConfigLoading}
/>
</div>
</ConfigPageRow>
<ConfigPageRow
label={tTools('config.subagentBatchPolicy.label')}
align="center"
Expand Down
Loading
Loading