worker: separate subworkers from workspace workers

This commit is contained in:
2026-08-05 03:36:59 +09:00
parent 50726e4cf3
commit ba009c0a20
35 changed files with 747 additions and 261 deletions
+30 -9
View File
@@ -83,7 +83,9 @@ pub struct FeatureConfigPartial {
#[serde(default)]
pub web: Option<FeatureFlagConfigPartial>,
#[serde(default)]
pub workers: Option<FeatureFlagConfigPartial>,
pub sub_worker: Option<FeatureFlagConfigPartial>,
#[serde(default)]
pub worker: Option<FeatureFlagConfigPartial>,
#[serde(default)]
pub objective: Option<FeatureFlagConfigPartial>,
#[serde(default)]
@@ -100,7 +102,12 @@ impl FeatureConfigPartial {
task: merge_option(self.task, other.task, FeatureFlagConfigPartial::merge),
memory: merge_option(self.memory, other.memory, MemoryFeatureConfigPartial::merge),
web: merge_option(self.web, other.web, FeatureFlagConfigPartial::merge),
workers: merge_option(self.workers, other.workers, FeatureFlagConfigPartial::merge),
sub_worker: merge_option(
self.sub_worker,
other.sub_worker,
FeatureFlagConfigPartial::merge,
),
worker: merge_option(self.worker, other.worker, FeatureFlagConfigPartial::merge),
objective: merge_option(
self.objective,
other.objective,
@@ -179,8 +186,12 @@ impl From<FeatureConfigPartial> for FeatureConfig {
.map(MemoryFeatureConfig::from)
.unwrap_or_default(),
web: value.web.map(FeatureFlagConfig::from).unwrap_or_default(),
workers: value
.workers
sub_worker: value
.sub_worker
.map(FeatureFlagConfig::from)
.unwrap_or_default(),
worker: value
.worker
.map(FeatureFlagConfig::from)
.unwrap_or_default(),
objective: value
@@ -267,7 +278,8 @@ impl From<FeatureConfig> for FeatureConfigPartial {
task: Some(value.task.into()),
memory: Some(value.memory.into()),
web: Some(value.web.into()),
workers: Some(value.workers.into()),
sub_worker: Some(value.sub_worker.into()),
worker: Some(value.worker.into()),
objective: Some(value.objective.into()),
manage_workdir: Some(value.manage_workdir.into()),
ticket: Some(value.ticket.into()),
@@ -417,6 +429,15 @@ pub(crate) fn reject_removed_manifest_fields(s: &str) -> Result<(), toml::de::Er
"unknown field in manifest: memory.extract_worker_max_input_tokens (removed)",
));
}
if value
.get("feature")
.and_then(toml::Value::as_table)
.is_some_and(|table| table.contains_key("workers"))
{
return Err(toml::de::Error::custom(
"unknown field in manifest: feature.workers (removed; use feature.sub_worker)",
));
}
Ok(())
}
@@ -424,8 +445,8 @@ impl WorkerManifestConfig {
/// Parse a partial manifest from a TOML string. Unknown top-level or
/// nested fields emit a `tracing::warn!` and are ignored; use
/// `tracing_subscriber` with `WARN` enabled to surface them to the
/// operator. Removed fields that must not be silently ignored (currently
/// `compaction.prune_protected_turns`) are rejected before deserialization.
/// operator. Removed fields with an explicit replacement (including
/// `feature.workers`) are rejected before deserialization.
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
reject_removed_manifest_fields(s)?;
let de = toml::Deserializer::parse(s)?;
@@ -1814,7 +1835,7 @@ worker_max_turns = 7
assert!(!manifest.feature.task.enabled);
assert!(!manifest.feature.memory.enabled);
assert!(!manifest.feature.web.enabled);
assert!(!manifest.feature.workers.enabled);
assert!(!manifest.feature.sub_worker.enabled);
assert!(!manifest.feature.objective.enabled);
assert!(!manifest.feature.manage_workdir.enabled);
assert!(!manifest.feature.ticket.enabled);
@@ -1949,7 +1970,7 @@ enabled = true
assert!(manifest.feature.ticket.orchestration_control);
assert!(manifest.feature.objective.enabled);
assert!(manifest.feature.web.enabled);
assert!(!manifest.feature.workers.enabled);
assert!(!manifest.feature.sub_worker.enabled);
}
#[test]
+6 -3
View File
@@ -111,7 +111,9 @@ pub struct FeatureConfig {
#[serde(default)]
pub web: FeatureFlagConfig,
#[serde(default)]
pub workers: FeatureFlagConfig,
pub sub_worker: FeatureFlagConfig,
#[serde(default)]
pub worker: FeatureFlagConfig,
#[serde(default)]
pub objective: FeatureFlagConfig,
#[serde(default)]
@@ -128,7 +130,8 @@ impl Default for FeatureConfig {
task: FeatureFlagConfig::disabled(),
memory: MemoryFeatureConfig::disabled(),
web: FeatureFlagConfig::disabled(),
workers: FeatureFlagConfig::disabled(),
sub_worker: FeatureFlagConfig::disabled(),
worker: FeatureFlagConfig::disabled(),
objective: FeatureFlagConfig::disabled(),
manage_workdir: FeatureFlagConfig::disabled(),
ticket: TicketFeatureConfig::default(),
@@ -405,7 +408,7 @@ pub struct MemoryConfig {
/// system-prompt section. `None` ⇒ enabled.
#[serde(default)]
pub inject_summary: Option<bool>,
/// Language used by memory extraction / consolidation workers for durable
/// Language used by memory extraction / consolidation sub_worker for durable
/// memory text. Free-form so workspaces can use names like
/// `English`, `Japanese`, or locale tags. `None` ⇒
/// [`defaults::MEMORY_LANGUAGE`].
+19 -12
View File
@@ -436,7 +436,7 @@ impl ProfileResolver {
}
}
/// Resolve a registry/default selector against an already-discovered
/// registry. Callers such as SpawnWorker use this to bind discovery to the
/// registry. Callers such as SubWorkerSpawn use this to bind discovery to the
/// Worker's cwd instead of the process current directory.
pub fn resolve_from_registry(
&self,
@@ -936,7 +936,7 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
value["feature"]["task"] = serde_json::json!({ "enabled": false });
value["feature"]["memory"] = serde_json::json!({ "enabled": true, "staging": true });
value["feature"]["web"] = serde_json::json!({ "enabled": false });
value["feature"]["workers"] = serde_json::json!({ "enabled": false });
value["feature"]["sub_worker"] = serde_json::json!({ "enabled": false });
value["feature"]["objective"] = serde_json::json!({ "enabled": false });
value["feature"]["ticket"] = serde_json::json!({ "enabled": false, "thread": false });
Some(value)
@@ -962,7 +962,8 @@ fn builtin_base_profile_artifact() -> serde_json::Value {
"task": { "enabled": true },
"memory": { "enabled": true },
"web": { "enabled": true },
"workers": { "enabled": true },
"sub_worker": { "enabled": true },
"worker": { "enabled": false },
"objective": { "enabled": true },
"ticket": { "enabled": true, "authoring": true, "thread": true }
},
@@ -990,14 +991,15 @@ fn apply_role_profile(
task: bool,
memory: bool,
web: bool,
workers: bool,
sub_worker: bool,
) {
value["slug"] = serde_json::Value::String(slug.to_string());
value["description"] = serde_json::Value::String(description.to_string());
value["feature"]["task"] = serde_json::json!({ "enabled": task });
value["feature"]["memory"] = serde_json::json!({ "enabled": memory });
value["feature"]["web"] = serde_json::json!({ "enabled": web });
value["feature"]["workers"] = serde_json::json!({ "enabled": workers });
value["feature"]["sub_worker"] = serde_json::json!({ "enabled": sub_worker });
value["feature"]["worker"] = serde_json::json!({ "enabled": slug == "orchestrator" });
value["feature"]["manage_workdir"] = serde_json::json!({ "enabled": slug == "orchestrator" });
let ticket = match slug {
"companion" => serde_json::json!({ "enabled": true, "authoring": true, "thread": true }),
@@ -1456,7 +1458,8 @@ mod tests {
let companion = resolve("companion");
assert!(companion.feature.task.enabled);
assert!(companion.feature.workers.enabled);
assert!(companion.feature.sub_worker.enabled);
assert!(!companion.feature.worker.enabled);
assert!(companion.scope.allow.is_empty());
assert!(companion.scope.deny.is_empty());
assert!(companion.delegation_scope.allow.is_empty());
@@ -1487,7 +1490,8 @@ mod tests {
let intake = resolve("intake");
assert!(intake.feature.task.enabled);
assert!(!intake.feature.workers.enabled);
assert!(!intake.feature.sub_worker.enabled);
assert!(!intake.feature.worker.enabled);
assert!(intake.feature.ticket.enabled);
assert!(intake.feature.ticket.enabled);
assert!(intake.feature.ticket.authoring);
@@ -1504,7 +1508,8 @@ mod tests {
let orchestrator = resolve("orchestrator");
assert!(orchestrator.feature.task.enabled);
assert!(!orchestrator.feature.workers.enabled);
assert!(!orchestrator.feature.sub_worker.enabled);
assert!(orchestrator.feature.worker.enabled);
assert!(orchestrator.feature.ticket.enabled);
assert!(orchestrator.feature.ticket.enabled);
assert!(!orchestrator.feature.ticket.authoring);
@@ -1524,7 +1529,8 @@ mod tests {
let coder = resolve("coder");
assert!(coder.feature.task.enabled);
assert!(!coder.feature.workers.enabled);
assert!(!coder.feature.sub_worker.enabled);
assert!(!coder.feature.worker.enabled);
assert!(coder.scope.allow.is_empty());
assert!(coder.delegation_scope.allow.is_empty());
assert_eq!(coder.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
@@ -1540,7 +1546,8 @@ mod tests {
assert!(!coder.feature.ticket.orchestration_control);
let reviewer = resolve("reviewer");
assert!(reviewer.feature.task.enabled);
assert!(!reviewer.feature.workers.enabled);
assert!(!reviewer.feature.sub_worker.enabled);
assert!(!reviewer.feature.worker.enabled);
assert!(reviewer.feature.ticket.enabled);
assert!(reviewer.feature.ticket.enabled);
assert!(!reviewer.feature.ticket.authoring);
@@ -1692,7 +1699,7 @@ enabled = false
[feature.web]
enabled = true
[feature.workers]
[feature.sub_worker]
enabled = true
[feature.ticket]
@@ -1716,7 +1723,7 @@ orchestration_control = false
assert!(resolved.manifest.feature.task.enabled);
assert!(!resolved.manifest.feature.memory.enabled);
assert!(resolved.manifest.feature.web.enabled);
assert!(resolved.manifest.feature.workers.enabled);
assert!(resolved.manifest.feature.sub_worker.enabled);
assert!(resolved.manifest.feature.ticket.enabled);
assert!(!resolved.manifest.feature.ticket.authoring);
assert!(!resolved.manifest.feature.ticket.thread);
+1 -1
View File
@@ -331,7 +331,7 @@ impl Scope {
/// Build a new [`Scope`] equal to `self` with `extra_deny` appended
/// to the deny set. Used by dynamic-scope shrink paths
/// (e.g. SpawnWorker-style delegation that strips Write from the
/// (e.g. SubWorkerSpawn-style delegation that strips Write from the
/// spawner without touching its allow rules).
pub fn with_added_deny_rules(
&self,