fix: inherit parent profile for default subworkers
This commit is contained in:
@@ -45,9 +45,9 @@ use protocol::Method;
|
|||||||
struct SubWorkerSpawnInput {
|
struct SubWorkerSpawnInput {
|
||||||
/// Identifier for the spawned Internal SubWorker. Must be unique among this Worker's direct children.
|
/// Identifier for the spawned Internal SubWorker. Must be unique among this Worker's direct children.
|
||||||
name: String,
|
name: String,
|
||||||
/// Profile selector for child role configuration. Omit or use `default`
|
/// Profile selector for child role configuration. Omit or use `inherit`
|
||||||
/// for the effective child default profile, use `inherit` to derive
|
/// to derive reusable config from the spawner, use `default` for the
|
||||||
/// reusable config from the spawner, or use a registry selector such as
|
/// effective registry default profile, or use a registry selector such as
|
||||||
/// `project:coder`, `project:reviewer`, `builtin:companion`, or an
|
/// `project:coder`, `project:reviewer`, `builtin:companion`, or an
|
||||||
/// unambiguous profile slug. Raw/path selectors are rejected.
|
/// unambiguous profile slug. Raw/path selectors are rejected.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -204,7 +204,7 @@ enum SpawnProfileSelector {
|
|||||||
|
|
||||||
fn parse_spawn_profile_selector(raw: Option<&str>) -> Result<SpawnProfileSelector, String> {
|
fn parse_spawn_profile_selector(raw: Option<&str>) -> Result<SpawnProfileSelector, String> {
|
||||||
let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else {
|
let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else {
|
||||||
return Ok(SpawnProfileSelector::Default);
|
return Ok(SpawnProfileSelector::Inherit);
|
||||||
};
|
};
|
||||||
if raw == "default" {
|
if raw == "default" {
|
||||||
return Ok(SpawnProfileSelector::Default);
|
return Ok(SpawnProfileSelector::Default);
|
||||||
@@ -2298,7 +2298,7 @@ max_tokens = 3333
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn omitted_profile_resolves_effective_registry_default() {
|
fn explicit_default_profile_resolves_effective_registry_default() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
let project = tmp.path().join("project");
|
let project = tmp.path().join("project");
|
||||||
let delegated = tmp.path().join("delegated");
|
let delegated = tmp.path().join("delegated");
|
||||||
@@ -2322,7 +2322,7 @@ max_tokens = 3333
|
|||||||
"child-default",
|
"child-default",
|
||||||
None,
|
None,
|
||||||
&scope,
|
&scope,
|
||||||
SpawnProfileSelector::Default,
|
parse_spawn_profile_selector(Some("default")).unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(config.worker.name.as_deref(), Some("child-default"));
|
assert_eq!(config.worker.name.as_deref(), Some("child-default"));
|
||||||
@@ -2373,6 +2373,44 @@ max_tokens = 3333
|
|||||||
assert!(config.scope.deny.is_empty());
|
assert!(config.scope.deny.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn omitted_profile_inherits_parent_ticket_features_instead_of_registry_default() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let mut parent = parent_manifest(tmp.path(), None);
|
||||||
|
let scope = vec![abs_rule(tmp.path(), Permission::Read)];
|
||||||
|
let available = AvailableProfiles {
|
||||||
|
registry: Some(
|
||||||
|
ProfileDiscovery::with_sources(None, None)
|
||||||
|
.discover()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
diagnostic: None,
|
||||||
|
};
|
||||||
|
let input: SubWorkerSpawnInput = serde_json::from_value(serde_json::json!({
|
||||||
|
"name": "child", "task": "inspect", "scope": []
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
for enabled in [false, true] {
|
||||||
|
parent.feature.ticket.enabled = enabled;
|
||||||
|
parent.feature.ticket.thread = enabled;
|
||||||
|
let config = child_config_from_profile(
|
||||||
|
&parent,
|
||||||
|
&available,
|
||||||
|
tmp.path(),
|
||||||
|
"child",
|
||||||
|
None,
|
||||||
|
&scope,
|
||||||
|
parse_spawn_profile_selector(input.profile.as_deref()).unwrap(),
|
||||||
|
);
|
||||||
|
assert_eq!(config.model.model_id.as_deref(), Some("parent-model"));
|
||||||
|
assert_eq!(config.engine.language.as_deref(), Some("Parentish"));
|
||||||
|
let ticket = config.feature.ticket.unwrap();
|
||||||
|
assert_eq!(ticket.enabled, Some(enabled));
|
||||||
|
assert_eq!(ticket.thread, Some(enabled));
|
||||||
|
assert_eq!(config.scope.allow, scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inherit_copies_reusable_parent_fields_and_replaces_runtime_authority() {
|
fn inherit_copies_reusable_parent_fields_and_replaces_runtime_authority() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
@@ -2581,14 +2619,16 @@ max_tokens = 3333
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spawn_profile_selector_accepts_default_inherit_and_registry() {
|
fn spawn_profile_selector_accepts_default_inherit_and_registry() {
|
||||||
|
for raw in [None, Some(""), Some(" "), Some("inherit")] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_spawn_profile_selector(None).unwrap(),
|
parse_spawn_profile_selector(raw).unwrap(),
|
||||||
SpawnProfileSelector::Default
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_spawn_profile_selector(Some("inherit")).unwrap(),
|
|
||||||
SpawnProfileSelector::Inherit
|
SpawnProfileSelector::Inherit
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
parse_spawn_profile_selector(Some("default")).unwrap(),
|
||||||
|
SpawnProfileSelector::Default
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_spawn_profile_selector(Some("project:reviewer")).unwrap(),
|
parse_spawn_profile_selector(Some("project:reviewer")).unwrap(),
|
||||||
SpawnProfileSelector::Registry(ProfileSelector::source_named(
|
SpawnProfileSelector::Registry(ProfileSelector::source_named(
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ Spawn a parent-owned Internal SubWorker session to split context for a delegated
|
|||||||
|
|
||||||
Optional `cwd`: when provided, the spawned SubWorker's tool default working directory only. It must be a Workdir-relative existing directory covered by the child's readable scope, and it does not change workspace/Profile/memory/Ticket roots or grant authority. `name` must be unique among this Worker's direct children.
|
Optional `cwd`: when provided, the spawned SubWorker's tool default working directory only. It must be a Workdir-relative existing directory covered by the child's readable scope, and it does not change workspace/Profile/memory/Ticket roots or grant authority. `name` must be unique among this Worker's direct children.
|
||||||
|
|
||||||
Profile selection: `profile` may be omitted or set to `default` to use the effective child default profile, set to `inherit` to derive reusable child configuration from this Worker, or set to one of the registry selectors below. Raw/path profile selectors are not accepted by SubWorkerSpawn. `scope` is the child's only filesystem capability and replaces profile scope. `command` is a separate explicit grant, defaults to false, and is accepted only with a writable scope; writable scope alone does not grant command execution.
|
Profile selection: omit `profile` or set it to `inherit` to derive reusable child configuration from this Worker. Set it to `default` to use the effective registry default profile, or to one of the registry selectors below. Raw/path profile selectors are not accepted by SubWorkerSpawn. `scope` is the child's only filesystem capability and replaces profile scope. `command` is a separate explicit grant, defaults to false, and is accepted only with a writable scope; writable scope alone does not grant command execution.
|
||||||
|
|
||||||
Default profile: {{ default_profile }}
|
Default when omitted: inherit.
|
||||||
|
Registry default (`profile = "default"`): {{ default_profile }}
|
||||||
Special selector: inherit — derive reusable model/worker/tool policy from the spawner while replacing worker.name and scope.
|
Special selector: inherit — derive reusable model/worker/tool policy from the spawner while replacing worker.name and scope.
|
||||||
Available registry profiles:
|
Available registry profiles:
|
||||||
{{ available_profiles }}{% if profile_diagnostic %}
|
{{ available_profiles }}{% if profile_diagnostic %}
|
||||||
|
|||||||
Reference in New Issue
Block a user