worker: add typed feature services for coder spawn
This commit is contained in:
@@ -20,7 +20,7 @@ use crate::{
|
||||
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
|
||||
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConfig, MemoryFeatureConfig, ScopeConfig,
|
||||
SessionConfig, SkillsConfig, TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig,
|
||||
ToolPermissionRule, WebConfig, WorkerManifest, WorkerMeta,
|
||||
ToolPermissionRule, WebConfig, WorkerFeatureConfig, WorkerManifest, WorkerMeta,
|
||||
};
|
||||
|
||||
/// Partial-form Worker manifest. Every field is optional; one or more
|
||||
@@ -89,7 +89,7 @@ pub struct FeatureConfigPartial {
|
||||
#[serde(default)]
|
||||
pub flow: Option<FeatureFlagConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub worker: Option<FeatureFlagConfigPartial>,
|
||||
pub worker: Option<WorkerFeatureConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub objective: Option<FeatureFlagConfigPartial>,
|
||||
#[serde(default)]
|
||||
@@ -97,6 +97,8 @@ pub struct FeatureConfigPartial {
|
||||
#[serde(default)]
|
||||
pub ticket: Option<TicketFeatureConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub orchestration: Option<FeatureFlagConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub plugins: Option<FeatureFlagConfigPartial>,
|
||||
}
|
||||
|
||||
@@ -113,7 +115,7 @@ impl FeatureConfigPartial {
|
||||
FeatureFlagConfigPartial::merge,
|
||||
),
|
||||
flow: merge_option(self.flow, other.flow, FeatureFlagConfigPartial::merge),
|
||||
worker: merge_option(self.worker, other.worker, FeatureFlagConfigPartial::merge),
|
||||
worker: merge_option(self.worker, other.worker, WorkerFeatureConfigPartial::merge),
|
||||
objective: merge_option(
|
||||
self.objective,
|
||||
other.objective,
|
||||
@@ -125,6 +127,11 @@ impl FeatureConfigPartial {
|
||||
FeatureFlagConfigPartial::merge,
|
||||
),
|
||||
ticket: merge_option(self.ticket, other.ticket, TicketFeatureConfigPartial::merge),
|
||||
orchestration: merge_option(
|
||||
self.orchestration,
|
||||
other.orchestration,
|
||||
FeatureFlagConfigPartial::merge,
|
||||
),
|
||||
plugins: merge_option(self.plugins, other.plugins, FeatureFlagConfigPartial::merge),
|
||||
}
|
||||
}
|
||||
@@ -144,6 +151,32 @@ impl FeatureFlagConfigPartial {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct WorkerFeatureConfigPartial {
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub direct_spawn: Option<bool>,
|
||||
}
|
||||
|
||||
impl WorkerFeatureConfigPartial {
|
||||
fn merge(self, other: Self) -> Self {
|
||||
Self {
|
||||
enabled: other.enabled.or(self.enabled),
|
||||
direct_spawn: other.direct_spawn.or(self.direct_spawn),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WorkerFeatureConfigPartial> for WorkerFeatureConfig {
|
||||
fn from(value: WorkerFeatureConfigPartial) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled.unwrap_or(false),
|
||||
direct_spawn: value.direct_spawn.unwrap_or(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct MemoryFeatureConfigPartial {
|
||||
#[serde(default)]
|
||||
@@ -168,7 +201,7 @@ pub struct TicketFeatureConfigPartial {
|
||||
pub authoring: Option<bool>,
|
||||
pub thread: Option<bool>,
|
||||
pub intake: Option<bool>,
|
||||
pub orchestration_control: Option<bool>,
|
||||
pub workflow: Option<bool>,
|
||||
}
|
||||
|
||||
impl TicketFeatureConfigPartial {
|
||||
@@ -178,7 +211,7 @@ impl TicketFeatureConfigPartial {
|
||||
authoring: other.authoring.or(self.authoring),
|
||||
thread: other.thread.or(self.thread),
|
||||
intake: other.intake.or(self.intake),
|
||||
orchestration_control: other.orchestration_control.or(self.orchestration_control),
|
||||
workflow: other.workflow.or(self.workflow),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +233,7 @@ impl From<FeatureConfigPartial> for FeatureConfig {
|
||||
flow: value.flow.map(FeatureFlagConfig::from).unwrap_or_default(),
|
||||
worker: value
|
||||
.worker
|
||||
.map(FeatureFlagConfig::from)
|
||||
.map(WorkerFeatureConfig::from)
|
||||
.unwrap_or_default(),
|
||||
objective: value
|
||||
.objective
|
||||
@@ -214,6 +247,10 @@ impl From<FeatureConfigPartial> for FeatureConfig {
|
||||
.ticket
|
||||
.map(TicketFeatureConfig::from)
|
||||
.unwrap_or_default(),
|
||||
orchestration: value
|
||||
.orchestration
|
||||
.map(FeatureFlagConfig::from)
|
||||
.unwrap_or_default(),
|
||||
plugins: value
|
||||
.plugins
|
||||
.map(FeatureFlagConfig::from)
|
||||
@@ -238,6 +275,15 @@ impl From<FeatureFlagConfig> for FeatureFlagConfigPartial {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WorkerFeatureConfig> for WorkerFeatureConfigPartial {
|
||||
fn from(value: WorkerFeatureConfig) -> Self {
|
||||
Self {
|
||||
enabled: Some(value.enabled),
|
||||
direct_spawn: Some(value.direct_spawn),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MemoryFeatureConfigPartial> for MemoryFeatureConfig {
|
||||
fn from(value: MemoryFeatureConfigPartial) -> Self {
|
||||
Self {
|
||||
@@ -263,7 +309,7 @@ impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
|
||||
authoring: value.authoring.unwrap_or_default(),
|
||||
thread: value.thread.unwrap_or_default(),
|
||||
intake: value.intake.unwrap_or_default(),
|
||||
orchestration_control: value.orchestration_control.unwrap_or_default(),
|
||||
workflow: value.workflow.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,7 +321,7 @@ impl From<TicketFeatureConfig> for TicketFeatureConfigPartial {
|
||||
authoring: Some(value.authoring),
|
||||
thread: Some(value.thread),
|
||||
intake: Some(value.intake),
|
||||
orchestration_control: Some(value.orchestration_control),
|
||||
workflow: Some(value.workflow),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,6 +339,7 @@ impl From<FeatureConfig> for FeatureConfigPartial {
|
||||
objective: Some(value.objective.into()),
|
||||
manage_workdir: Some(value.manage_workdir.into()),
|
||||
ticket: Some(value.ticket.into()),
|
||||
orchestration: Some(value.orchestration.into()),
|
||||
plugins: Some(value.plugins.into()),
|
||||
}
|
||||
}
|
||||
@@ -1866,7 +1913,10 @@ enabled = true
|
||||
authoring = false
|
||||
thread = false
|
||||
intake = false
|
||||
orchestration_control = false
|
||||
workflow = false
|
||||
|
||||
[feature.orchestration]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1900,7 +1950,8 @@ orchestration_control = false
|
||||
assert!(!manifest.feature.ticket.authoring);
|
||||
assert!(!manifest.feature.ticket.thread);
|
||||
assert!(!manifest.feature.ticket.intake);
|
||||
assert!(!manifest.feature.ticket.orchestration_control);
|
||||
assert!(!manifest.feature.ticket.workflow);
|
||||
assert!(!manifest.feature.orchestration.enabled);
|
||||
assert!(!manifest.feature.memory.enabled);
|
||||
assert!(!manifest.feature.memory.staging);
|
||||
assert!(!manifest.feature.objective.enabled);
|
||||
@@ -1921,7 +1972,10 @@ enabled = true
|
||||
authoring = false
|
||||
thread = false
|
||||
intake = false
|
||||
orchestration_control = false
|
||||
workflow = false
|
||||
|
||||
[feature.orchestration]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1929,7 +1983,10 @@ orchestration_control = false
|
||||
r#"
|
||||
[feature.ticket]
|
||||
thread = true
|
||||
orchestration_control = true
|
||||
workflow = true
|
||||
|
||||
[feature.orchestration]
|
||||
enabled = true
|
||||
|
||||
[feature.memory]
|
||||
staging = true
|
||||
@@ -1977,7 +2034,8 @@ enabled = true
|
||||
assert!(!manifest.feature.ticket.authoring);
|
||||
assert!(manifest.feature.ticket.thread);
|
||||
assert!(!manifest.feature.ticket.intake);
|
||||
assert!(manifest.feature.ticket.orchestration_control);
|
||||
assert!(manifest.feature.ticket.workflow);
|
||||
assert!(manifest.feature.orchestration.enabled);
|
||||
assert!(manifest.feature.objective.enabled);
|
||||
assert!(manifest.feature.web.enabled);
|
||||
assert!(!manifest.feature.sub_worker.enabled);
|
||||
|
||||
@@ -117,7 +117,7 @@ pub struct FeatureConfig {
|
||||
#[serde(default)]
|
||||
pub flow: FeatureFlagConfig,
|
||||
#[serde(default)]
|
||||
pub worker: FeatureFlagConfig,
|
||||
pub worker: WorkerFeatureConfig,
|
||||
#[serde(default)]
|
||||
pub objective: FeatureFlagConfig,
|
||||
#[serde(default)]
|
||||
@@ -125,6 +125,8 @@ pub struct FeatureConfig {
|
||||
#[serde(default)]
|
||||
pub ticket: TicketFeatureConfig,
|
||||
#[serde(default)]
|
||||
pub orchestration: FeatureFlagConfig,
|
||||
#[serde(default)]
|
||||
pub plugins: FeatureFlagConfig,
|
||||
}
|
||||
|
||||
@@ -137,10 +139,11 @@ impl Default for FeatureConfig {
|
||||
image: FeatureFlagConfig::disabled(),
|
||||
sub_worker: FeatureFlagConfig::disabled(),
|
||||
flow: FeatureFlagConfig::disabled(),
|
||||
worker: FeatureFlagConfig::disabled(),
|
||||
worker: WorkerFeatureConfig::disabled(),
|
||||
objective: FeatureFlagConfig::disabled(),
|
||||
manage_workdir: FeatureFlagConfig::disabled(),
|
||||
ticket: TicketFeatureConfig::default(),
|
||||
orchestration: FeatureFlagConfig::disabled(),
|
||||
plugins: FeatureFlagConfig::disabled(),
|
||||
}
|
||||
}
|
||||
@@ -168,6 +171,42 @@ impl Default for FeatureFlagConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WorkerFeatureConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Exposes the generic WorkerSpawn tool. Stateful lifecycle services remain
|
||||
/// available to dependent semantic features when this is false.
|
||||
#[serde(default = "default_true")]
|
||||
pub direct_spawn: bool,
|
||||
}
|
||||
|
||||
impl WorkerFeatureConfig {
|
||||
pub const fn disabled() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
direct_spawn: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn enabled() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
direct_spawn: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WorkerFeatureConfig {
|
||||
fn default() -> Self {
|
||||
Self::disabled()
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MemoryFeatureConfig {
|
||||
#[serde(default)]
|
||||
@@ -210,7 +249,7 @@ pub struct TicketFeatureConfig {
|
||||
#[serde(default)]
|
||||
pub intake: bool,
|
||||
#[serde(default)]
|
||||
pub orchestration_control: bool,
|
||||
pub workflow: bool,
|
||||
}
|
||||
|
||||
/// External Agent Skills (`SKILL.md`) ingest configuration. Skills are
|
||||
|
||||
@@ -1002,16 +1002,19 @@ fn apply_role_profile(
|
||||
value["feature"]["image"] = serde_json::json!({ "enabled": true });
|
||||
value["feature"]["sub_worker"] = serde_json::json!({ "enabled": sub_worker });
|
||||
value["feature"]["flow"] = serde_json::json!({ "enabled": slug == "coder" });
|
||||
value["feature"]["worker"] =
|
||||
serde_json::json!({ "enabled": matches!(slug, "companion" | "orchestrator") });
|
||||
value["feature"]["worker"] = serde_json::json!({
|
||||
"enabled": matches!(slug, "companion" | "orchestrator"),
|
||||
"direct_spawn": slug != "orchestrator"
|
||||
});
|
||||
value["feature"]["manage_workdir"] = serde_json::json!({ "enabled": slug == "orchestrator" });
|
||||
value["feature"]["orchestration"] = serde_json::json!({ "enabled": slug == "orchestrator" });
|
||||
let ticket = match slug {
|
||||
"companion" => serde_json::json!({ "enabled": true, "authoring": true, "thread": true }),
|
||||
"intake" => {
|
||||
serde_json::json!({ "enabled": true, "authoring": true, "thread": true, "intake": true })
|
||||
}
|
||||
"orchestrator" => {
|
||||
serde_json::json!({ "enabled": true, "thread": true, "orchestration_control": true })
|
||||
serde_json::json!({ "enabled": true, "thread": true, "workflow": true })
|
||||
}
|
||||
"coder" => serde_json::json!({ "enabled": true, "thread": true }),
|
||||
"reviewer" => serde_json::json!({ "enabled": true, "thread": true }),
|
||||
@@ -1474,7 +1477,7 @@ mod tests {
|
||||
assert!(companion.feature.ticket.thread);
|
||||
assert!(companion.feature.objective.enabled);
|
||||
assert!(!companion.feature.ticket.intake);
|
||||
assert!(!companion.feature.ticket.orchestration_control);
|
||||
assert!(!companion.feature.orchestration.enabled);
|
||||
assert_eq!(
|
||||
companion.compaction.as_ref().unwrap().threshold,
|
||||
Some(240000)
|
||||
@@ -1503,7 +1506,7 @@ mod tests {
|
||||
assert!(intake.feature.objective.enabled);
|
||||
assert!(!intake.feature.manage_workdir.enabled);
|
||||
assert!(intake.feature.ticket.intake);
|
||||
assert!(!intake.feature.ticket.orchestration_control);
|
||||
assert!(!intake.feature.orchestration.enabled);
|
||||
assert!(intake.scope.allow.is_empty());
|
||||
assert!(intake.delegation_scope.allow.is_empty());
|
||||
assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
||||
@@ -1514,6 +1517,7 @@ mod tests {
|
||||
assert!(orchestrator.feature.task.enabled);
|
||||
assert!(!orchestrator.feature.sub_worker.enabled);
|
||||
assert!(orchestrator.feature.worker.enabled);
|
||||
assert!(!orchestrator.feature.worker.direct_spawn);
|
||||
assert!(orchestrator.feature.ticket.enabled);
|
||||
assert!(orchestrator.feature.ticket.enabled);
|
||||
assert!(!orchestrator.feature.ticket.authoring);
|
||||
@@ -1521,7 +1525,8 @@ mod tests {
|
||||
assert!(orchestrator.feature.objective.enabled);
|
||||
assert!(orchestrator.feature.manage_workdir.enabled);
|
||||
assert!(!orchestrator.feature.ticket.intake);
|
||||
assert!(orchestrator.feature.ticket.orchestration_control);
|
||||
assert!(orchestrator.feature.ticket.workflow);
|
||||
assert!(orchestrator.feature.orchestration.enabled);
|
||||
assert!(orchestrator.scope.allow.is_empty());
|
||||
assert!(orchestrator.delegation_scope.allow.is_empty());
|
||||
assert_eq!(
|
||||
@@ -1548,7 +1553,7 @@ mod tests {
|
||||
assert!(coder.feature.objective.enabled);
|
||||
assert!(!coder.feature.manage_workdir.enabled);
|
||||
assert!(!coder.feature.ticket.intake);
|
||||
assert!(!coder.feature.ticket.orchestration_control);
|
||||
assert!(!coder.feature.orchestration.enabled);
|
||||
let reviewer = resolve("reviewer");
|
||||
assert!(reviewer.feature.task.enabled);
|
||||
assert!(!reviewer.feature.sub_worker.enabled);
|
||||
@@ -1561,7 +1566,7 @@ mod tests {
|
||||
assert!(reviewer.feature.objective.enabled);
|
||||
assert!(!reviewer.feature.manage_workdir.enabled);
|
||||
assert!(!reviewer.feature.ticket.intake);
|
||||
assert!(!reviewer.feature.ticket.orchestration_control);
|
||||
assert!(!reviewer.feature.orchestration.enabled);
|
||||
assert!(reviewer.scope.allow.is_empty());
|
||||
assert!(reviewer.delegation_scope.allow.is_empty());
|
||||
assert_eq!(reviewer.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
||||
@@ -1574,7 +1579,7 @@ mod tests {
|
||||
let prompt = include_str!("../../../resources/prompts/role/orchestrator.md");
|
||||
|
||||
assert!(prompt.contains("assigned Coder owns its review/fix loop"));
|
||||
assert!(prompt.contains("spawn the implementation Coder with `WorkerSpawn.ticket_id`"));
|
||||
assert!(prompt.contains("then use `SpawnTicketCoder`"));
|
||||
assert!(prompt.contains("verify its current assignment names that Coder"));
|
||||
assert!(prompt.contains("never route implementation to an unassigned Coder"));
|
||||
assert!(prompt.contains(
|
||||
@@ -1728,7 +1733,8 @@ enabled = true
|
||||
authoring = false
|
||||
thread = false
|
||||
intake = false
|
||||
orchestration_control = false
|
||||
[feature.orchestration]
|
||||
enabled = false
|
||||
"#,
|
||||
);
|
||||
let workspace = tmp.path().join("workspace");
|
||||
@@ -1749,7 +1755,7 @@ orchestration_control = false
|
||||
assert!(!resolved.manifest.feature.ticket.authoring);
|
||||
assert!(!resolved.manifest.feature.ticket.thread);
|
||||
assert!(!resolved.manifest.feature.ticket.intake);
|
||||
assert!(!resolved.manifest.feature.ticket.orchestration_control);
|
||||
assert!(!resolved.manifest.feature.orchestration.enabled);
|
||||
assert_eq!(
|
||||
resolved.manifest.delegation_scope.allow[0].target,
|
||||
workspace
|
||||
@@ -1836,7 +1842,7 @@ worker_context_max_tokens = 68000
|
||||
assert!(resolved.manifest.feature.ticket.authoring);
|
||||
assert!(resolved.manifest.feature.ticket.thread);
|
||||
assert!(!resolved.manifest.feature.ticket.intake);
|
||||
assert!(!resolved.manifest.feature.ticket.orchestration_control);
|
||||
assert!(!resolved.manifest.feature.orchestration.enabled);
|
||||
assert_eq!(
|
||||
resolved.profile.as_ref().unwrap().name.as_deref(),
|
||||
Some("companion")
|
||||
|
||||
Reference in New Issue
Block a user