memory: gate staging tools by profile
This commit is contained in:
parent
a44673fa6b
commit
e997aa2fc6
|
|
@ -18,9 +18,9 @@ use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
|||
use crate::plugin::PluginConfig;
|
||||
use crate::{
|
||||
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
|
||||
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConfig, ScopeConfig, SessionConfig,
|
||||
SkillsConfig, TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule,
|
||||
WebConfig, WorkerManifest, WorkerMeta,
|
||||
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConfig, MemoryFeatureConfig, ScopeConfig,
|
||||
SessionConfig, SkillsConfig, TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig,
|
||||
ToolPermissionRule, WebConfig, WorkerManifest, WorkerMeta,
|
||||
};
|
||||
|
||||
/// Partial-form Worker manifest. Every field is optional; one or more
|
||||
|
|
@ -79,7 +79,7 @@ pub struct FeatureConfigPartial {
|
|||
#[serde(default)]
|
||||
pub task: Option<FeatureFlagConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub memory: Option<FeatureFlagConfigPartial>,
|
||||
pub memory: Option<MemoryFeatureConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub web: Option<FeatureFlagConfigPartial>,
|
||||
#[serde(default)]
|
||||
|
|
@ -94,7 +94,7 @@ impl FeatureConfigPartial {
|
|||
fn merge(self, other: Self) -> Self {
|
||||
Self {
|
||||
task: merge_option(self.task, other.task, FeatureFlagConfigPartial::merge),
|
||||
memory: merge_option(self.memory, other.memory, 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),
|
||||
ticket: merge_option(self.ticket, other.ticket, TicketFeatureConfigPartial::merge),
|
||||
|
|
@ -117,6 +117,23 @@ impl FeatureFlagConfigPartial {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct MemoryFeatureConfigPartial {
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub staging: Option<bool>,
|
||||
}
|
||||
|
||||
impl MemoryFeatureConfigPartial {
|
||||
fn merge(self, other: Self) -> Self {
|
||||
Self {
|
||||
enabled: other.enabled.or(self.enabled),
|
||||
staging: other.staging.or(self.staging),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct TicketFeatureConfigPartial {
|
||||
|
|
@ -145,7 +162,7 @@ impl From<FeatureConfigPartial> for FeatureConfig {
|
|||
task: value.task.map(FeatureFlagConfig::from).unwrap_or_default(),
|
||||
memory: value
|
||||
.memory
|
||||
.map(FeatureFlagConfig::from)
|
||||
.map(MemoryFeatureConfig::from)
|
||||
.unwrap_or_default(),
|
||||
web: value.web.map(FeatureFlagConfig::from).unwrap_or_default(),
|
||||
workers: value
|
||||
|
|
@ -180,6 +197,24 @@ impl From<FeatureFlagConfig> for FeatureFlagConfigPartial {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<MemoryFeatureConfigPartial> for MemoryFeatureConfig {
|
||||
fn from(value: MemoryFeatureConfigPartial) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled.unwrap_or_default(),
|
||||
staging: value.staging.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MemoryFeatureConfig> for MemoryFeatureConfigPartial {
|
||||
fn from(value: MemoryFeatureConfig) -> Self {
|
||||
Self {
|
||||
enabled: Some(value.enabled),
|
||||
staging: Some(value.staging),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
|
||||
fn from(value: TicketFeatureConfigPartial) -> Self {
|
||||
Self {
|
||||
|
|
@ -1806,6 +1841,7 @@ orchestration_control = false
|
|||
assert!(!manifest.feature.ticket.intake);
|
||||
assert!(!manifest.feature.ticket.orchestration_control);
|
||||
assert!(!manifest.feature.memory.enabled);
|
||||
assert!(!manifest.feature.memory.staging);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1830,6 +1866,9 @@ orchestration_control = false
|
|||
thread = true
|
||||
orchestration_control = true
|
||||
|
||||
[feature.memory]
|
||||
staging = true
|
||||
|
||||
[feature.web]
|
||||
enabled = true
|
||||
"#,
|
||||
|
|
@ -1861,6 +1900,7 @@ enabled = true
|
|||
.try_into()
|
||||
.unwrap();
|
||||
assert!(manifest.feature.memory.enabled);
|
||||
assert!(manifest.feature.memory.staging);
|
||||
assert!(manifest.feature.ticket.enabled);
|
||||
assert!(!manifest.feature.ticket.authoring);
|
||||
assert!(manifest.feature.ticket.thread);
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ pub struct FeatureConfig {
|
|||
#[serde(default)]
|
||||
pub task: FeatureFlagConfig,
|
||||
#[serde(default)]
|
||||
pub memory: FeatureFlagConfig,
|
||||
pub memory: MemoryFeatureConfig,
|
||||
#[serde(default)]
|
||||
pub web: FeatureFlagConfig,
|
||||
#[serde(default)]
|
||||
|
|
@ -122,7 +122,7 @@ impl Default for FeatureConfig {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
task: FeatureFlagConfig::disabled(),
|
||||
memory: FeatureFlagConfig::disabled(),
|
||||
memory: MemoryFeatureConfig::disabled(),
|
||||
web: FeatureFlagConfig::disabled(),
|
||||
workers: FeatureFlagConfig::disabled(),
|
||||
ticket: TicketFeatureConfig::default(),
|
||||
|
|
@ -153,6 +153,37 @@ impl Default for FeatureFlagConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MemoryFeatureConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Exposes Memory staging queue tools in addition to normal Memory CRUD/query tools.
|
||||
#[serde(default)]
|
||||
pub staging: bool,
|
||||
}
|
||||
|
||||
impl MemoryFeatureConfig {
|
||||
pub const fn disabled() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
staging: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn enabled() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
staging: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemoryFeatureConfig {
|
||||
fn default() -> Self {
|
||||
Self::disabled()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct TicketFeatureConfig {
|
||||
#[serde(default)]
|
||||
|
|
|
|||
|
|
@ -954,6 +954,17 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
|
|||
);
|
||||
Some(value)
|
||||
}
|
||||
"builtin:memory-consolidation" | "memory-consolidation" => {
|
||||
value["slug"] = serde_json::Value::String("memory-consolidation".to_string());
|
||||
value["description"] =
|
||||
serde_json::Value::String("Memory staging consolidation profile.".to_string());
|
||||
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"]["ticket"] = serde_json::json!({ "enabled": false, "thread": false });
|
||||
Some(value)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1425,6 +1436,29 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_memory_consolidation_profile_enables_staging_by_feature() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let resolved = ProfileResolver::new()
|
||||
.with_workspace_base(tmp.path())
|
||||
.resolve(
|
||||
&ProfileSelector::source_named(
|
||||
ProfileRegistrySource::Builtin,
|
||||
"memory-consolidation",
|
||||
),
|
||||
ProfileResolveOptions::with_worker_name("arbitrary-worker-name"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resolved.profile.as_ref().unwrap().name.as_deref(),
|
||||
Some("memory-consolidation")
|
||||
);
|
||||
assert_eq!(resolved.manifest.worker.name, "arbitrary-worker-name");
|
||||
assert!(resolved.manifest.feature.memory.enabled);
|
||||
assert!(resolved.manifest.feature.memory.staging);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_role_profiles_preserve_role_tool_policy() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -646,7 +646,7 @@ where
|
|||
base_url,
|
||||
} = workspace_client
|
||||
{
|
||||
let definitions = if spawner_name == "memory-consolidation" {
|
||||
let definitions = if feature_config.memory.staging {
|
||||
crate::feature::builtin::memory::workspace_http_memory_consolidation_tools(
|
||||
workspace_id,
|
||||
base_url,
|
||||
|
|
|
|||
|
|
@ -418,3 +418,44 @@ fn query_schema() -> serde_json::Value {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use llm_engine::tool::ToolDefinition;
|
||||
|
||||
fn tool_names(definitions: Vec<ToolDefinition>) -> Vec<String> {
|
||||
let mut names = definitions
|
||||
.into_iter()
|
||||
.map(|tool| tool().0.name)
|
||||
.collect::<Vec<_>>();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_workspace_memory_tools_do_not_include_staging_tools() {
|
||||
let names = tool_names(workspace_http_memory_tools(
|
||||
"workspace".to_string(),
|
||||
"http://backend".to_string(),
|
||||
));
|
||||
|
||||
assert!(names.contains(&"MemoryQuery".to_string()));
|
||||
assert!(!names.contains(&"MemoryStagingList".to_string()));
|
||||
assert!(!names.contains(&"MemoryStagingRead".to_string()));
|
||||
assert!(!names.contains(&"MemoryStagingClose".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consolidation_workspace_memory_tools_include_staging_tools() {
|
||||
let names = tool_names(workspace_http_memory_consolidation_tools(
|
||||
"workspace".to_string(),
|
||||
"http://backend".to_string(),
|
||||
));
|
||||
|
||||
assert!(names.contains(&"MemoryQuery".to_string()));
|
||||
assert!(names.contains(&"MemoryStagingList".to_string()));
|
||||
assert!(names.contains(&"MemoryStagingRead".to_string()));
|
||||
assert!(names.contains(&"MemoryStagingClose".to_string()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2892,7 +2892,14 @@ fn default_profile_source_archive(
|
|||
"builtin:default".to_string(),
|
||||
"profiles/default.dcdl".to_string(),
|
||||
);
|
||||
for slug in ["companion", "intake", "orchestrator", "coder", "reviewer"] {
|
||||
for slug in [
|
||||
"companion",
|
||||
"intake",
|
||||
"orchestrator",
|
||||
"coder",
|
||||
"reviewer",
|
||||
"memory-consolidation",
|
||||
] {
|
||||
entrypoints.insert(format!("builtin:{slug}"), format!("profiles/{slug}.dcdl"));
|
||||
}
|
||||
entrypoints.insert(selected, selected_path);
|
||||
|
|
@ -2922,9 +2929,20 @@ fn default_profile_source_archive(
|
|||
"profiles/reviewer.dcdl".to_string(),
|
||||
include_str!("../../../resources/profiles/reviewer.dcdl").to_string(),
|
||||
);
|
||||
sources.insert(
|
||||
"profiles/memory-consolidation.dcdl".to_string(),
|
||||
include_str!("../../../resources/profiles/memory-consolidation.dcdl").to_string(),
|
||||
);
|
||||
|
||||
let mut imports = BTreeMap::new();
|
||||
for slug in ["companion", "intake", "orchestrator", "coder", "reviewer"] {
|
||||
for slug in [
|
||||
"companion",
|
||||
"intake",
|
||||
"orchestrator",
|
||||
"coder",
|
||||
"reviewer",
|
||||
"memory-consolidation",
|
||||
] {
|
||||
imports.insert(
|
||||
format!("profiles/{slug}.dcdl\0./default.dcdl"),
|
||||
"profiles/default.dcdl".to_string(),
|
||||
|
|
@ -2950,6 +2968,7 @@ fn embedded_profile_path(profile: &ProfileSelector) -> Result<String, String> {
|
|||
"orchestrator" => Ok("profiles/orchestrator.dcdl".to_string()),
|
||||
"coder" => Ok("profiles/coder.dcdl".to_string()),
|
||||
"reviewer" => Ok("profiles/reviewer.dcdl".to_string()),
|
||||
"memory-consolidation" => Ok("profiles/memory-consolidation.dcdl".to_string()),
|
||||
other => Err(format!("unknown builtin profile selector: builtin:{other}")),
|
||||
},
|
||||
ProfileSelector::Named(name) => Err(format!("unknown named profile selector: {name}")),
|
||||
|
|
@ -3433,6 +3452,7 @@ mod tests {
|
|||
ProfileSelector::Builtin("builtin:orchestrator".to_string()),
|
||||
ProfileSelector::Builtin("builtin:coder".to_string()),
|
||||
ProfileSelector::Builtin("builtin:reviewer".to_string()),
|
||||
ProfileSelector::Builtin("builtin:memory-consolidation".to_string()),
|
||||
] {
|
||||
let bundle = default_embedded_config_bundle(
|
||||
&selector,
|
||||
|
|
@ -3469,6 +3489,10 @@ mod tests {
|
|||
.unwrap();
|
||||
assert_eq!(manifest.worker.name, "embedded-test-worker");
|
||||
assert_eq!(manifest.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
||||
if selector_key == "builtin:memory-consolidation" {
|
||||
assert!(manifest.feature.memory.enabled);
|
||||
assert!(manifest.feature.memory.staging);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
worker = { name = "memory-consolidation"; }
|
||||
import "./default.dcdl" // {
|
||||
slug = "memory-consolidation";
|
||||
description = "Memory staging consolidation profile.";
|
||||
scope = "workspace_read";
|
||||
|
||||
feature = {
|
||||
task = { enabled = false; };
|
||||
memory = { enabled = true; };
|
||||
memory = { enabled = true; staging = true; };
|
||||
web = { enabled = false; };
|
||||
workers = { enabled = false; };
|
||||
ticket = { enabled = false; thread = false; };
|
||||
};
|
||||
system_prompt = { file = "internal/memory_consolidation_system.md"; };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user