refactor: unify Memory feature configuration authority
This commit is contained in:
+132
-76
@@ -18,8 +18,9 @@ use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
|||||||
use crate::plugin::PluginConfig;
|
use crate::plugin::PluginConfig;
|
||||||
use crate::{
|
use crate::{
|
||||||
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
|
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
|
||||||
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConfig, MemoryFeatureConfig,
|
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryExtractionProfileConfig,
|
||||||
MergeRequestFeatureConfig, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureConfig,
|
MemoryFeatureProfileConfig, MemoryResidentProfileConfig, MergeRequestFeatureConfig,
|
||||||
|
ResolvedMemoryFeatureConfig, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureConfig,
|
||||||
ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerFeatureConfig,
|
ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerFeatureConfig,
|
||||||
WorkerManifest, WorkerMeta,
|
WorkerManifest, WorkerMeta,
|
||||||
};
|
};
|
||||||
@@ -67,9 +68,6 @@ pub struct WorkerManifestConfig {
|
|||||||
/// First-class web tool opt-in. See [`WebConfig`].
|
/// First-class web tool opt-in. See [`WebConfig`].
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub web: Option<WebConfig>,
|
pub web: Option<WebConfig>,
|
||||||
/// Memory subsystem opt-in. See [`MemoryConfig`].
|
|
||||||
#[serde(default)]
|
|
||||||
pub memory: Option<MemoryConfig>,
|
|
||||||
/// External Agent Skills directories. See [`crate::SkillsConfig`].
|
/// External Agent Skills directories. See [`crate::SkillsConfig`].
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub skills: Option<SkillsConfig>,
|
pub skills: Option<SkillsConfig>,
|
||||||
@@ -193,18 +191,72 @@ impl From<WorkerFeatureConfigPartial> for WorkerFeatureConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct MemoryFeatureConfigPartial {
|
pub struct MemoryFeatureConfigPartial {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub staging: Option<bool>,
|
pub staging_tools: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub resident: Option<MemoryResidentProfileConfigPartial>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub extraction: Option<MemoryExtractionProfileConfigPartial>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct MemoryResidentProfileConfigPartial {
|
||||||
|
#[serde(default)]
|
||||||
|
pub inject_summary: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct MemoryExtractionProfileConfigPartial {
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub model: Option<ModelManifest>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub threshold: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub worker_max_turns: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryFeatureConfigPartial {
|
impl MemoryFeatureConfigPartial {
|
||||||
fn merge(self, other: Self) -> Self {
|
fn merge(self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: other.enabled.or(self.enabled),
|
enabled: other.enabled.or(self.enabled),
|
||||||
staging: other.staging.or(self.staging),
|
staging_tools: other.staging_tools.or(self.staging_tools),
|
||||||
|
resident: merge_option(
|
||||||
|
self.resident,
|
||||||
|
other.resident,
|
||||||
|
MemoryResidentProfileConfigPartial::merge,
|
||||||
|
),
|
||||||
|
extraction: merge_option(
|
||||||
|
self.extraction,
|
||||||
|
other.extraction,
|
||||||
|
MemoryExtractionProfileConfigPartial::merge,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryResidentProfileConfigPartial {
|
||||||
|
fn merge(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
inject_summary: other.inject_summary.or(self.inject_summary),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryExtractionProfileConfigPartial {
|
||||||
|
fn merge(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: other.enabled.or(self.enabled),
|
||||||
|
model: other.model.or(self.model),
|
||||||
|
threshold: other.threshold.or(self.threshold),
|
||||||
|
worker_max_turns: other.worker_max_turns.or(self.worker_max_turns),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +311,7 @@ impl From<FeatureConfigPartial> for FeatureConfig {
|
|||||||
task: value.task.map(FeatureFlagConfig::from).unwrap_or_default(),
|
task: value.task.map(FeatureFlagConfig::from).unwrap_or_default(),
|
||||||
memory: value
|
memory: value
|
||||||
.memory
|
.memory
|
||||||
.map(MemoryFeatureConfig::from)
|
.map(ResolvedMemoryFeatureConfig::from)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
web: value.web.map(FeatureFlagConfig::from).unwrap_or_default(),
|
web: value.web.map(FeatureFlagConfig::from).unwrap_or_default(),
|
||||||
image: value.image.map(FeatureFlagConfig::from).unwrap_or_default(),
|
image: value.image.map(FeatureFlagConfig::from).unwrap_or_default(),
|
||||||
@@ -329,20 +381,45 @@ impl From<WorkerFeatureConfig> for WorkerFeatureConfigPartial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MemoryFeatureConfigPartial> for MemoryFeatureConfig {
|
impl From<MemoryFeatureConfigPartial> for ResolvedMemoryFeatureConfig {
|
||||||
fn from(value: MemoryFeatureConfigPartial) -> Self {
|
fn from(value: MemoryFeatureConfigPartial) -> Self {
|
||||||
|
let resident = value.resident.unwrap_or_default();
|
||||||
|
let extraction = value.extraction.unwrap_or_default();
|
||||||
Self {
|
Self {
|
||||||
enabled: value.enabled.unwrap_or_default(),
|
profile: MemoryFeatureProfileConfig {
|
||||||
staging: value.staging.unwrap_or_default(),
|
enabled: value.enabled.unwrap_or_default(),
|
||||||
|
staging_tools: value.staging_tools.unwrap_or_default(),
|
||||||
|
resident: MemoryResidentProfileConfig {
|
||||||
|
inject_summary: resident.inject_summary.unwrap_or(true),
|
||||||
|
},
|
||||||
|
extraction: MemoryExtractionProfileConfig {
|
||||||
|
enabled: extraction.enabled.unwrap_or(true),
|
||||||
|
model: extraction.model,
|
||||||
|
threshold: extraction.threshold.or(Some(50_000)),
|
||||||
|
worker_max_turns: extraction
|
||||||
|
.worker_max_turns
|
||||||
|
.or(defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workspace_settings: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MemoryFeatureConfig> for MemoryFeatureConfigPartial {
|
impl From<ResolvedMemoryFeatureConfig> for MemoryFeatureConfigPartial {
|
||||||
fn from(value: MemoryFeatureConfig) -> Self {
|
fn from(value: ResolvedMemoryFeatureConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: Some(value.enabled),
|
enabled: Some(value.profile.enabled),
|
||||||
staging: Some(value.staging),
|
staging_tools: Some(value.profile.staging_tools),
|
||||||
|
resident: Some(MemoryResidentProfileConfigPartial {
|
||||||
|
inject_summary: Some(value.profile.resident.inject_summary),
|
||||||
|
}),
|
||||||
|
extraction: Some(MemoryExtractionProfileConfigPartial {
|
||||||
|
enabled: Some(value.profile.extraction.enabled),
|
||||||
|
model: value.profile.extraction.model,
|
||||||
|
threshold: value.profile.extraction.threshold,
|
||||||
|
worker_max_turns: value.profile.extraction.worker_max_turns,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -543,13 +620,9 @@ pub(crate) fn reject_removed_manifest_fields(s: &str) -> Result<(), toml::de::Er
|
|||||||
(removed; use compaction.prune_protected_tokens)",
|
(removed; use compaction.prune_protected_tokens)",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if value
|
if value.get("memory").is_some() {
|
||||||
.get("memory")
|
|
||||||
.and_then(toml::Value::as_table)
|
|
||||||
.is_some_and(|table| table.contains_key("extract_worker_max_input_tokens"))
|
|
||||||
{
|
|
||||||
return Err(toml::de::Error::custom(
|
return Err(toml::de::Error::custom(
|
||||||
"unknown field in manifest: memory.extract_worker_max_input_tokens (removed)",
|
"unknown field in manifest: memory (removed; configure feature.memory)",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if value
|
if value
|
||||||
@@ -633,11 +706,6 @@ impl WorkerManifestConfig {
|
|||||||
for rule in &mut self.delegation_scope.deny {
|
for rule in &mut self.delegation_scope.deny {
|
||||||
rule.target = join_if_relative(base, &rule.target);
|
rule.target = join_if_relative(base, &rule.target);
|
||||||
}
|
}
|
||||||
if let Some(ref mut memory) = self.memory
|
|
||||||
&& let Some(ref mut root) = memory.workspace_root
|
|
||||||
{
|
|
||||||
*root = join_if_relative(base, root);
|
|
||||||
}
|
|
||||||
if let Some(ref mut compaction) = self.compaction
|
if let Some(ref mut compaction) = self.compaction
|
||||||
&& let Some(ref mut cp) = compaction.model
|
&& let Some(ref mut cp) = compaction.model
|
||||||
{
|
{
|
||||||
@@ -682,7 +750,6 @@ impl WorkerManifestConfig {
|
|||||||
CompactionConfigPartial::merge,
|
CompactionConfigPartial::merge,
|
||||||
),
|
),
|
||||||
web: merge_option(self.web, upper.web, WebConfig::merge),
|
web: merge_option(self.web, upper.web, WebConfig::merge),
|
||||||
memory: merge_option(self.memory, upper.memory, MemoryConfig::merge),
|
|
||||||
skills: merge_option(self.skills, upper.skills, SkillsConfig::merge),
|
skills: merge_option(self.skills, upper.skills, SkillsConfig::merge),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -754,32 +821,6 @@ impl crate::WebFetchConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryConfig {
|
|
||||||
fn merge(self, upper: Self) -> Self {
|
|
||||||
Self {
|
|
||||||
workspace_root: upper.workspace_root.or(self.workspace_root),
|
|
||||||
query_result_limit: upper.query_result_limit.or(self.query_result_limit),
|
|
||||||
query_excerpt_lines: upper.query_excerpt_lines.or(self.query_excerpt_lines),
|
|
||||||
inject_summary: upper.inject_summary.or(self.inject_summary),
|
|
||||||
workspace_id: upper.workspace_id.or(self.workspace_id),
|
|
||||||
settings_revision: upper.settings_revision.or(self.settings_revision),
|
|
||||||
language: upper.language.or(self.language),
|
|
||||||
extract_model: upper.extract_model.or(self.extract_model),
|
|
||||||
extract_threshold: upper.extract_threshold.or(self.extract_threshold),
|
|
||||||
extract_worker_max_turns: upper
|
|
||||||
.extract_worker_max_turns
|
|
||||||
.or(self.extract_worker_max_turns),
|
|
||||||
consolidation_model: upper.consolidation_model.or(self.consolidation_model),
|
|
||||||
consolidation_threshold_files: upper
|
|
||||||
.consolidation_threshold_files
|
|
||||||
.or(self.consolidation_threshold_files),
|
|
||||||
consolidation_threshold_bytes: upper
|
|
||||||
.consolidation_threshold_bytes
|
|
||||||
.or(self.consolidation_threshold_bytes),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkerMetaConfig {
|
impl WorkerMetaConfig {
|
||||||
fn merge(self, upper: Self) -> Self {
|
fn merge(self, upper: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -1223,7 +1264,6 @@ impl TryFrom<WorkerManifestConfig> for WorkerManifest {
|
|||||||
mcp: cfg.mcp,
|
mcp: cfg.mcp,
|
||||||
compaction,
|
compaction,
|
||||||
web: cfg.web,
|
web: cfg.web,
|
||||||
memory: cfg.memory,
|
|
||||||
skills: cfg.skills,
|
skills: cfg.skills,
|
||||||
profile: None,
|
profile: None,
|
||||||
})
|
})
|
||||||
@@ -1271,7 +1311,6 @@ mod tests {
|
|||||||
session: None,
|
session: None,
|
||||||
compaction: None,
|
compaction: None,
|
||||||
web: None,
|
web: None,
|
||||||
memory: None,
|
|
||||||
skills: None,
|
skills: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1846,29 +1885,46 @@ prune_protected_turns = 3
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_toml_rejects_removed_extract_worker_max_input_tokens_field() {
|
fn from_toml_accepts_memory_extraction_settings_only_under_feature_memory() {
|
||||||
let bad = r#"
|
let cfg = WorkerManifestConfig::from_toml(
|
||||||
[memory]
|
r#"
|
||||||
extract_worker_max_input_tokens = 30000
|
[feature.memory]
|
||||||
"#;
|
enabled = true
|
||||||
let err = WorkerManifestConfig::from_toml(bad).unwrap_err();
|
staging_tools = false
|
||||||
assert!(
|
|
||||||
err.to_string()
|
[feature.memory.resident]
|
||||||
.contains("memory.extract_worker_max_input_tokens"),
|
inject_summary = false
|
||||||
"unexpected error: {err}"
|
|
||||||
);
|
[feature.memory.extraction]
|
||||||
|
enabled = true
|
||||||
|
threshold = 42000
|
||||||
|
worker_max_turns = 2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let memory = cfg.feature.memory.unwrap();
|
||||||
|
assert_eq!(memory.enabled, Some(true));
|
||||||
|
assert_eq!(memory.staging_tools, Some(false));
|
||||||
|
assert_eq!(memory.resident.unwrap().inject_summary, Some(false));
|
||||||
|
let extraction = memory.extraction.unwrap();
|
||||||
|
assert_eq!(extraction.enabled, Some(true));
|
||||||
|
assert_eq!(extraction.threshold, Some(42_000));
|
||||||
|
assert_eq!(extraction.worker_max_turns, Some(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_toml_accepts_extract_worker_max_turns() {
|
fn from_toml_rejects_legacy_top_level_memory_authority() {
|
||||||
let cfg = WorkerManifestConfig::from_toml(
|
let err = WorkerManifestConfig::from_toml(
|
||||||
r#"
|
r#"
|
||||||
[memory]
|
[memory]
|
||||||
extract_worker_max_turns = 2
|
extract_worker_max_turns = 2
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap_err();
|
||||||
assert_eq!(cfg.memory.unwrap().extract_worker_max_turns, Some(2));
|
assert!(
|
||||||
|
err.to_string().contains("memory"),
|
||||||
|
"unexpected error: {err}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1948,7 +2004,7 @@ worker_max_turns = 7
|
|||||||
fn feature_flags_default_disabled_in_resolved_manifest() {
|
fn feature_flags_default_disabled_in_resolved_manifest() {
|
||||||
let manifest: WorkerManifest = minimal_valid().try_into().unwrap();
|
let manifest: WorkerManifest = minimal_valid().try_into().unwrap();
|
||||||
assert!(!manifest.feature.task.enabled);
|
assert!(!manifest.feature.task.enabled);
|
||||||
assert!(!manifest.feature.memory.enabled);
|
assert!(!manifest.feature.memory.profile.enabled);
|
||||||
assert!(!manifest.feature.web.enabled);
|
assert!(!manifest.feature.web.enabled);
|
||||||
assert!(!manifest.feature.sub_worker.enabled);
|
assert!(!manifest.feature.sub_worker.enabled);
|
||||||
assert!(!manifest.feature.objective.enabled);
|
assert!(!manifest.feature.objective.enabled);
|
||||||
@@ -2025,8 +2081,8 @@ enabled = false
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert!(!manifest.feature.orchestration.enabled);
|
assert!(!manifest.feature.orchestration.enabled);
|
||||||
assert!(!manifest.feature.memory.enabled);
|
assert!(!manifest.feature.memory.profile.enabled);
|
||||||
assert!(!manifest.feature.memory.staging);
|
assert!(!manifest.feature.memory.profile.staging_tools);
|
||||||
assert!(!manifest.feature.objective.enabled);
|
assert!(!manifest.feature.objective.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2074,7 +2130,7 @@ readiness_check = true
|
|||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
[feature.memory]
|
[feature.memory]
|
||||||
staging = true
|
staging_tools = true
|
||||||
|
|
||||||
[feature.manage_workdir]
|
[feature.manage_workdir]
|
||||||
enabled = true
|
enabled = true
|
||||||
@@ -2111,8 +2167,8 @@ enabled = true
|
|||||||
})
|
})
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(manifest.feature.memory.enabled);
|
assert!(manifest.feature.memory.profile.enabled);
|
||||||
assert!(manifest.feature.memory.staging);
|
assert!(manifest.feature.memory.profile.staging_tools);
|
||||||
assert!(manifest.feature.manage_workdir.enabled);
|
assert!(manifest.feature.manage_workdir.enabled);
|
||||||
assert!(manifest.feature.ticket.enabled);
|
assert!(manifest.feature.ticket.enabled);
|
||||||
assert!(!manifest.feature.ticket.authoring);
|
assert!(!manifest.feature.ticket.authoring);
|
||||||
|
|||||||
@@ -93,5 +93,5 @@ pub const COMPACT_RESULT_CONTEXT_MAX_TOKENS: u64 = 60_000;
|
|||||||
pub const COMPACT_DEFAULT_REFERENCE_COUNT: usize = 5;
|
pub const COMPACT_DEFAULT_REFERENCE_COUNT: usize = 5;
|
||||||
|
|
||||||
/// Optional maximum extract-worker tool-loop depth. `None` means unlimited.
|
/// Optional maximum extract-worker tool-loop depth. `None` means unlimited.
|
||||||
/// See [`crate::MemoryConfig::extract_worker_max_turns`].
|
/// See [`crate::MemoryExtractionProfileConfig::worker_max_turns`].
|
||||||
pub const MEMORY_EXTRACT_WORKER_MAX_TURNS: Option<u32> = Some(8);
|
pub const MEMORY_EXTRACT_WORKER_MAX_TURNS: Option<u32> = Some(8);
|
||||||
|
|||||||
+380
-147
@@ -47,6 +47,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
/// part of the manifest — it is the process's `std::env::current_dir()`
|
/// part of the manifest — it is the process's `std::env::current_dir()`
|
||||||
/// at construction time.
|
/// at construction time.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkerManifest {
|
pub struct WorkerManifest {
|
||||||
pub worker: WorkerMeta,
|
pub worker: WorkerMeta,
|
||||||
pub model: ModelManifest,
|
pub model: ModelManifest,
|
||||||
@@ -80,11 +81,6 @@ pub struct WorkerManifest {
|
|||||||
pub mcp: McpConfig,
|
pub mcp: McpConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub compaction: Option<CompactionConfig>,
|
pub compaction: Option<CompactionConfig>,
|
||||||
/// Memory subsystem configuration. Presence of `[memory]` configures memory
|
|
||||||
/// storage, extraction, consolidation, and resident injection, but memory
|
|
||||||
/// tools are surfaced only when `[feature.memory].enabled = true`.
|
|
||||||
#[serde(default)]
|
|
||||||
pub memory: Option<MemoryConfig>,
|
|
||||||
/// First-class web tools configuration. Network access remains fail-closed
|
/// First-class web tools configuration. Network access remains fail-closed
|
||||||
/// under this config; WebSearch/WebFetch schemas are surfaced only when
|
/// under this config; WebSearch/WebFetch schemas are surfaced only when
|
||||||
/// `[feature.web].enabled = true`.
|
/// `[feature.web].enabled = true`.
|
||||||
@@ -109,12 +105,12 @@ pub struct WorkerManifest {
|
|||||||
/// profile/config data only: they do not carry runtime Worker names, sockets,
|
/// profile/config data only: they do not carry runtime Worker names, sockets,
|
||||||
/// sessions, secrets, or resolved host state. Tool registration still applies
|
/// sessions, secrets, or resolved host state. Tool registration still applies
|
||||||
/// the normal scope, host-authority, backend, memory, and network checks.
|
/// the normal scope, host-authority, backend, memory, and network checks.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct FeatureConfig {
|
pub struct FeatureConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub task: FeatureFlagConfig,
|
pub task: FeatureFlagConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub memory: MemoryFeatureConfig,
|
pub memory: ResolvedMemoryFeatureConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub web: FeatureFlagConfig,
|
pub web: FeatureFlagConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -147,7 +143,7 @@ impl Default for FeatureConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
task: FeatureFlagConfig::disabled(),
|
task: FeatureFlagConfig::disabled(),
|
||||||
memory: MemoryFeatureConfig::disabled(),
|
memory: ResolvedMemoryFeatureConfig::default(),
|
||||||
web: FeatureFlagConfig::disabled(),
|
web: FeatureFlagConfig::disabled(),
|
||||||
image: FeatureFlagConfig::disabled(),
|
image: FeatureFlagConfig::disabled(),
|
||||||
sub_worker: FeatureFlagConfig::disabled(),
|
sub_worker: FeatureFlagConfig::disabled(),
|
||||||
@@ -222,34 +218,117 @@ const fn default_true() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct MemoryFeatureConfig {
|
#[serde(default, deny_unknown_fields)]
|
||||||
#[serde(default)]
|
pub struct MemoryFeatureProfileConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// Exposes Memory staging queue tools in addition to normal Memory CRUD/query tools.
|
/// Exposes Memory staging queue tools in addition to normal Memory CRUD/query tools.
|
||||||
#[serde(default)]
|
pub staging_tools: bool,
|
||||||
pub staging: bool,
|
pub resident: MemoryResidentProfileConfig,
|
||||||
|
pub extraction: MemoryExtractionProfileConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryFeatureConfig {
|
impl MemoryFeatureProfileConfig {
|
||||||
pub const fn disabled() -> Self {
|
pub fn disabled() -> Self {
|
||||||
Self {
|
Self::default()
|
||||||
enabled: false,
|
|
||||||
staging: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn enabled() -> Self {
|
pub fn enabled() -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
staging: false,
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MemoryFeatureConfig {
|
impl Default for MemoryFeatureProfileConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::disabled()
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
staging_tools: false,
|
||||||
|
resident: MemoryResidentProfileConfig::default(),
|
||||||
|
extraction: MemoryExtractionProfileConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct MemoryResidentProfileConfig {
|
||||||
|
pub inject_summary: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MemoryResidentProfileConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inject_summary: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct MemoryExtractionProfileConfig {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub model: Option<ModelManifest>,
|
||||||
|
pub threshold: Option<u64>,
|
||||||
|
pub worker_max_turns: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MemoryExtractionProfileConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
model: None,
|
||||||
|
threshold: Some(50_000),
|
||||||
|
worker_max_turns: defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Immutable Memory execution configuration persisted in a resolved Worker Manifest.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct ResolvedMemoryFeatureConfig {
|
||||||
|
pub profile: MemoryFeatureProfileConfig,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub workspace_settings: Option<WorkspaceMemorySettingsSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedMemoryFeatureConfig {
|
||||||
|
pub fn enabled(&self) -> bool {
|
||||||
|
self.profile.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind_workspace_settings(
|
||||||
|
&mut self,
|
||||||
|
settings: WorkspaceMemorySettingsSnapshot,
|
||||||
|
) -> Result<(), &'static str> {
|
||||||
|
if !self.profile.enabled {
|
||||||
|
if self.workspace_settings.is_some() {
|
||||||
|
return Err("disabled Memory feature must not carry Workspace settings");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if self.workspace_settings.is_some() {
|
||||||
|
return Err("memory Workspace settings are already bound");
|
||||||
|
}
|
||||||
|
self.workspace_settings = Some(settings);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workspace_settings(&self) -> Option<WorkspaceMemorySettingsSnapshot> {
|
||||||
|
self.workspace_settings.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_execution(&self) -> Result<(), &'static str> {
|
||||||
|
if self.profile.enabled && self.workspace_settings.is_none() {
|
||||||
|
return Err("enabled Memory feature requires trusted Workspace settings");
|
||||||
|
}
|
||||||
|
if !self.profile.enabled && self.workspace_settings.is_some() {
|
||||||
|
return Err("disabled Memory feature must not carry Workspace settings");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,98 +563,6 @@ pub struct WorkspaceMemorySettingsSnapshot {
|
|||||||
pub language: String,
|
pub language: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Memory subsystem configuration. Presence in the manifest enables
|
|
||||||
/// memory; `workspace_root` pins the memory workspace explicitly. When it
|
|
||||||
/// is absent, memory resolution searches upward from the Worker's pwd for a
|
|
||||||
/// `.yoi/memory` marker rather than treating `.yoi` project records alone
|
|
||||||
/// as a memory root.
|
|
||||||
///
|
|
||||||
/// All fields are `Option`; defaults are applied at the consumer
|
|
||||||
/// (`.unwrap_or(defaults::...)`). This keeps cascade `merge` simple
|
|
||||||
/// (`upper.x.or(self.x)`) without a separate partial/resolved split.
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
||||||
pub struct MemoryConfig {
|
|
||||||
/// Override for the memory workspace root. When `None`, consumers resolve
|
|
||||||
/// the root from their default path and ancestor `.yoi/memory` markers.
|
|
||||||
/// When set, must be an absolute path.
|
|
||||||
#[serde(default)]
|
|
||||||
pub workspace_root: Option<PathBuf>,
|
|
||||||
/// Maximum number of records returned by `MemoryQuery` /
|
|
||||||
/// `MemoryQuery` per call. `None` ⇒ tool default (20).
|
|
||||||
#[serde(default)]
|
|
||||||
pub query_result_limit: Option<usize>,
|
|
||||||
/// Lines of context before and after each match in query excerpts.
|
|
||||||
/// Ignored when the request omits `query`. `None` ⇒ tool default (3).
|
|
||||||
#[serde(default)]
|
|
||||||
pub query_excerpt_lines: Option<usize>,
|
|
||||||
/// Whether the body of `memory/summary.md` is exposed in the resident
|
|
||||||
/// system-prompt section. `None` ⇒ enabled.
|
|
||||||
#[serde(default)]
|
|
||||||
pub inject_summary: Option<bool>,
|
|
||||||
/// Workspace that owns the bound Memory settings revision.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub workspace_id: Option<String>,
|
|
||||||
/// Monotonic revision of the bound Workspace Memory settings.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub settings_revision: Option<u64>,
|
|
||||||
/// Language from the bound Workspace Memory settings revision.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub language: Option<String>,
|
|
||||||
/// Optional model for the extract worker. When `None`,
|
|
||||||
/// the main engine model is cloned via `clone_boxed()`. Lightweight
|
|
||||||
/// reasoning-capable models (Haiku / 4o-mini / Flash class) are
|
|
||||||
/// recommended.
|
|
||||||
#[serde(default)]
|
|
||||||
pub extract_model: Option<ModelManifest>,
|
|
||||||
/// Cumulative input-token threshold (since the last extract pointer)
|
|
||||||
/// that triggers an extract run. `None` disables the extract trigger
|
|
||||||
/// entirely; memory tools and resident injection still work, only
|
|
||||||
/// the auto-extract trigger is dormant.
|
|
||||||
#[serde(default)]
|
|
||||||
pub extract_threshold: Option<u64>,
|
|
||||||
/// Optional maximum extract-worker tool-loop depth. `None` leaves
|
|
||||||
/// the worker unlimited; the default bounds runaway short-context
|
|
||||||
/// loops. Falls through to
|
|
||||||
/// [`defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS`] when unset.
|
|
||||||
#[serde(default)]
|
|
||||||
pub extract_worker_max_turns: Option<u32>,
|
|
||||||
/// Optional model for the consolidation worker. When
|
|
||||||
/// `None`, the main engine model is cloned via `clone_boxed()`.
|
|
||||||
/// Reasoning-class models are recommended.
|
|
||||||
#[serde(default)]
|
|
||||||
pub consolidation_model: Option<ModelManifest>,
|
|
||||||
/// Consolidation trigger: file-count threshold of `_staging/`. The
|
|
||||||
/// consolidation run fires when the staging directory has at least
|
|
||||||
/// this many entries. Either threshold reaching its limit fires
|
|
||||||
/// consolidation (logical OR). `None` for both thresholds ⇒
|
|
||||||
/// consolidation disabled.
|
|
||||||
#[serde(default)]
|
|
||||||
pub consolidation_threshold_files: Option<usize>,
|
|
||||||
/// Consolidation trigger: byte-size threshold across all `_staging/`
|
|
||||||
/// entries. Either threshold reaching its limit fires consolidation.
|
|
||||||
/// `None` for both thresholds ⇒ consolidation disabled.
|
|
||||||
#[serde(default)]
|
|
||||||
pub consolidation_threshold_bytes: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemoryConfig {
|
|
||||||
/// Replace any untrusted manifest values with a trusted Workspace snapshot.
|
|
||||||
pub fn bind_workspace_settings(&mut self, snapshot: &WorkspaceMemorySettingsSnapshot) {
|
|
||||||
self.workspace_id = Some(snapshot.workspace_id.clone());
|
|
||||||
self.settings_revision = Some(snapshot.settings_revision);
|
|
||||||
self.language = Some(snapshot.language.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the complete bound Workspace settings snapshot, if every field is present.
|
|
||||||
pub fn workspace_settings(&self) -> Option<WorkspaceMemorySettingsSnapshot> {
|
|
||||||
Some(WorkspaceMemorySettingsSnapshot {
|
|
||||||
workspace_id: self.workspace_id.clone()?,
|
|
||||||
settings_revision: self.settings_revision?,
|
|
||||||
language: self.language.clone()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Worker metadata.
|
/// Worker metadata.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct WorkerMeta {
|
pub struct WorkerMeta {
|
||||||
@@ -941,6 +928,167 @@ impl WorkerManifest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RESOLVED_MANIFEST_SNAPSHOT_SCHEMA_VERSION: u64 = 2;
|
||||||
|
|
||||||
|
/// Serialize a resolved Worker Manifest for durable Worker-specific storage.
|
||||||
|
pub fn write_persisted_worker_manifest_snapshot(
|
||||||
|
manifest: &WorkerManifest,
|
||||||
|
) -> Result<serde_json::Value, serde_json::Error> {
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"schema_version": RESOLVED_MANIFEST_SNAPSHOT_SCHEMA_VERSION,
|
||||||
|
"manifest": serde_json::to_value(manifest)?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a durable resolved Worker Manifest through the versioned compatibility
|
||||||
|
/// boundary. Runtime code must not deserialize persisted snapshots directly.
|
||||||
|
pub fn read_persisted_worker_manifest_snapshot(
|
||||||
|
snapshot: serde_json::Value,
|
||||||
|
) -> Result<WorkerManifest, serde_json::Error> {
|
||||||
|
let object = snapshot.as_object().ok_or_else(|| {
|
||||||
|
serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"resolved Worker manifest snapshot must be an object",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if let Some(version) = object.get("schema_version") {
|
||||||
|
let version = version.as_u64().ok_or_else(|| {
|
||||||
|
serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"resolved Worker manifest snapshot schema_version must be an integer",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if version != RESOLVED_MANIFEST_SNAPSHOT_SCHEMA_VERSION {
|
||||||
|
return Err(serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
format!("unsupported resolved Worker manifest snapshot schema version {version}"),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if object.len() != 2 {
|
||||||
|
return Err(serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"resolved Worker manifest snapshot contains unknown fields",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let manifest = object.get("manifest").cloned().ok_or_else(|| {
|
||||||
|
serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"resolved Worker manifest snapshot is missing manifest",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
return serde_json::from_value(manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate_legacy_resolved_manifest_snapshot(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrate_legacy_resolved_manifest_snapshot(
|
||||||
|
mut snapshot: serde_json::Value,
|
||||||
|
) -> Result<WorkerManifest, serde_json::Error> {
|
||||||
|
let root = snapshot.as_object_mut().ok_or_else(|| {
|
||||||
|
serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"legacy resolved Worker manifest snapshot must be an object",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let legacy_memory = root.remove("memory");
|
||||||
|
let feature = root
|
||||||
|
.entry("feature")
|
||||||
|
.or_insert_with(|| serde_json::json!({}))
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"legacy resolved Worker manifest feature must be an object",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let legacy_feature_memory = feature
|
||||||
|
.remove("memory")
|
||||||
|
.unwrap_or_else(|| serde_json::json!({}));
|
||||||
|
let legacy_feature_memory = legacy_feature_memory.as_object().ok_or_else(|| {
|
||||||
|
serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"legacy resolved Worker manifest feature.memory must be an object",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if legacy_feature_memory
|
||||||
|
.keys()
|
||||||
|
.any(|key| !matches!(key.as_str(), "enabled" | "staging"))
|
||||||
|
{
|
||||||
|
return Err(serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"legacy resolved Worker manifest mixes old and new Memory configuration",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let enabled = legacy_feature_memory
|
||||||
|
.get("enabled")
|
||||||
|
.and_then(serde_json::Value::as_bool)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let staging_tools = legacy_feature_memory
|
||||||
|
.get("staging")
|
||||||
|
.and_then(serde_json::Value::as_bool)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let legacy_memory = legacy_memory.unwrap_or_else(|| serde_json::json!({}));
|
||||||
|
let legacy_memory = legacy_memory.as_object().ok_or_else(|| {
|
||||||
|
serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"legacy resolved Worker manifest memory must be an object",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let workspace_id = legacy_memory.get("workspace_id").cloned();
|
||||||
|
let settings_revision = legacy_memory.get("settings_revision").cloned();
|
||||||
|
let language = legacy_memory.get("language").cloned();
|
||||||
|
let workspace_settings = match (workspace_id, settings_revision, language) {
|
||||||
|
(Some(workspace_id), Some(settings_revision), Some(language)) => Some(serde_json::json!({
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"settings_revision": settings_revision,
|
||||||
|
"language": language,
|
||||||
|
})),
|
||||||
|
(None, None, None) => None,
|
||||||
|
_ => {
|
||||||
|
return Err(serde_json::Error::io(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"legacy resolved Worker manifest contains a partial Memory settings snapshot",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let extraction_threshold = legacy_memory
|
||||||
|
.get("extract_threshold")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(serde_json::Value::Null);
|
||||||
|
let extraction_enabled = !extraction_threshold.is_null();
|
||||||
|
let mut resolved = serde_json::json!({
|
||||||
|
"profile": {
|
||||||
|
"enabled": enabled,
|
||||||
|
"staging_tools": staging_tools,
|
||||||
|
"resident": {
|
||||||
|
"inject_summary": legacy_memory
|
||||||
|
.get("inject_summary")
|
||||||
|
.and_then(serde_json::Value::as_bool)
|
||||||
|
.unwrap_or(true),
|
||||||
|
},
|
||||||
|
"extraction": {
|
||||||
|
"enabled": extraction_enabled,
|
||||||
|
"model": legacy_memory.get("extract_model").cloned().unwrap_or(serde_json::Value::Null),
|
||||||
|
"threshold": extraction_threshold,
|
||||||
|
"worker_max_turns": legacy_memory
|
||||||
|
.get("extract_worker_max_turns")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(serde_json::Value::Null),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if let Some(workspace_settings) = workspace_settings {
|
||||||
|
resolved
|
||||||
|
.as_object_mut()
|
||||||
|
.expect("resolved Memory config is an object")
|
||||||
|
.insert("workspace_settings".to_string(), workspace_settings);
|
||||||
|
}
|
||||||
|
feature.insert("memory".to_string(), resolved);
|
||||||
|
serde_json::from_value(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1246,36 +1394,129 @@ model_id = "claude-sonnet-4-20250514"
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn omitted_memory_is_none() {
|
fn omitted_memory_feature_is_disabled() {
|
||||||
let manifest = WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap();
|
let manifest = WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap();
|
||||||
assert!(manifest.memory.is_none());
|
assert!(!manifest.feature.memory.profile.enabled);
|
||||||
|
assert!(manifest.feature.memory.workspace_settings.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_memory_section_enables_with_default_root() {
|
fn resolved_memory_feature_requires_nested_profile_and_trusted_snapshot() {
|
||||||
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\n");
|
let toml = format!(
|
||||||
|
"{MINIMAL_REQUIRED}\n\
|
||||||
|
[feature.memory.profile]\n\
|
||||||
|
enabled = true\n\
|
||||||
|
staging_tools = false\n\n\
|
||||||
|
[feature.memory.profile.resident]\n\
|
||||||
|
inject_summary = false\n\n\
|
||||||
|
[feature.memory.profile.extraction]\n\
|
||||||
|
enabled = true\n\
|
||||||
|
threshold = 42000\n\
|
||||||
|
worker_max_turns = 2\n\n\
|
||||||
|
[feature.memory.workspace_settings]\n\
|
||||||
|
workspace_id = \"workspace-1\"\n\
|
||||||
|
settings_revision = 7\n\
|
||||||
|
language = \"日本語\"\n"
|
||||||
|
);
|
||||||
let manifest = WorkerManifest::from_toml(&toml).unwrap();
|
let manifest = WorkerManifest::from_toml(&toml).unwrap();
|
||||||
let mem = manifest.memory.expect("memory section parsed");
|
assert!(manifest.feature.memory.profile.enabled);
|
||||||
assert!(mem.workspace_root.is_none());
|
assert!(!manifest.feature.memory.profile.resident.inject_summary);
|
||||||
assert_eq!(mem.inject_summary, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn memory_section_with_inject_summary_false() {
|
|
||||||
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\ninject_summary = false\n");
|
|
||||||
let manifest = WorkerManifest::from_toml(&toml).unwrap();
|
|
||||||
let mem = manifest.memory.unwrap();
|
|
||||||
assert_eq!(mem.inject_summary, Some(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn memory_section_with_explicit_root() {
|
|
||||||
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\nworkspace_root = \"/some/where\"\n");
|
|
||||||
let manifest = WorkerManifest::from_toml(&toml).unwrap();
|
|
||||||
let mem = manifest.memory.unwrap();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mem.workspace_root.unwrap(),
|
manifest.feature.memory.profile.extraction.threshold,
|
||||||
std::path::PathBuf::from("/some/where")
|
Some(42_000)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
manifest
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.workspace_settings()
|
||||||
|
.unwrap()
|
||||||
|
.language,
|
||||||
|
"日本語"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolved_memory_execution_validation_fails_closed() {
|
||||||
|
let snapshot = WorkspaceMemorySettingsSnapshot {
|
||||||
|
workspace_id: "workspace-1".to_string(),
|
||||||
|
settings_revision: 1,
|
||||||
|
language: "English".to_string(),
|
||||||
|
};
|
||||||
|
let mut enabled = ResolvedMemoryFeatureConfig::default();
|
||||||
|
enabled.profile.enabled = true;
|
||||||
|
assert!(enabled.validate_execution().is_err());
|
||||||
|
enabled.bind_workspace_settings(snapshot.clone()).unwrap();
|
||||||
|
assert!(enabled.validate_execution().is_ok());
|
||||||
|
|
||||||
|
let mut disabled = ResolvedMemoryFeatureConfig::default();
|
||||||
|
disabled.workspace_settings = Some(snapshot.clone());
|
||||||
|
assert!(disabled.validate_execution().is_err());
|
||||||
|
assert!(disabled.bind_workspace_settings(snapshot).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_manifest_rejects_legacy_top_level_memory_authority() {
|
||||||
|
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\nlanguage = \"Japanese\"\n");
|
||||||
|
assert!(WorkerManifest::from_toml(&toml).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persisted_manifest_adapter_migrates_legacy_memory_authority() {
|
||||||
|
let mut manifest =
|
||||||
|
serde_json::to_value(WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap()).unwrap();
|
||||||
|
manifest["feature"]["memory"] = serde_json::json!({
|
||||||
|
"enabled": true,
|
||||||
|
"staging": true,
|
||||||
|
});
|
||||||
|
manifest["memory"] = serde_json::json!({
|
||||||
|
"workspace_root": "/discarded",
|
||||||
|
"query_result_limit": 999,
|
||||||
|
"inject_summary": false,
|
||||||
|
"workspace_id": "workspace-1",
|
||||||
|
"settings_revision": 9,
|
||||||
|
"language": "Français",
|
||||||
|
"extract_threshold": 1234,
|
||||||
|
"extract_worker_max_turns": 3,
|
||||||
|
"consolidation_threshold_files": 99,
|
||||||
|
});
|
||||||
|
|
||||||
|
let migrated = read_persisted_worker_manifest_snapshot(manifest).unwrap();
|
||||||
|
assert!(migrated.feature.memory.profile.enabled);
|
||||||
|
assert!(migrated.feature.memory.profile.staging_tools);
|
||||||
|
assert!(!migrated.feature.memory.profile.resident.inject_summary);
|
||||||
|
assert_eq!(
|
||||||
|
migrated.feature.memory.profile.extraction.threshold,
|
||||||
|
Some(1234)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
migrated
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.workspace_settings()
|
||||||
|
.unwrap()
|
||||||
|
.language,
|
||||||
|
"Français"
|
||||||
|
);
|
||||||
|
let current = write_persisted_worker_manifest_snapshot(&migrated).unwrap();
|
||||||
|
assert_eq!(current["schema_version"], 2);
|
||||||
|
assert!(current["manifest"].get("memory").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persisted_manifest_adapter_rejects_mixed_or_future_authority() {
|
||||||
|
let manifest =
|
||||||
|
serde_json::to_value(WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap()).unwrap();
|
||||||
|
let mut mixed = manifest.clone();
|
||||||
|
mixed["feature"]["memory"] = serde_json::json!({ "enabled": true, "profile": {} });
|
||||||
|
mixed["memory"] = serde_json::json!({});
|
||||||
|
assert!(read_persisted_worker_manifest_snapshot(mixed).is_err());
|
||||||
|
assert!(
|
||||||
|
read_persisted_worker_manifest_snapshot(serde_json::json!({
|
||||||
|
"schema_version": 3,
|
||||||
|
"manifest": manifest,
|
||||||
|
}))
|
||||||
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1291,14 +1532,6 @@ model_id = "claude-sonnet-4-20250514"
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn memory_section_with_language() {
|
|
||||||
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\nlanguage = \"Japanese\"\n");
|
|
||||||
let manifest = WorkerManifest::from_toml(&toml).unwrap();
|
|
||||||
let mem = manifest.memory.unwrap();
|
|
||||||
assert_eq!(mem.language.as_deref(), Some("Japanese"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reject_unknown_scheme() {
|
fn reject_unknown_scheme() {
|
||||||
let toml =
|
let toml =
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ use crate::config::{
|
|||||||
use crate::model::{AuthRef, ModelManifest};
|
use crate::model::{AuthRef, ModelManifest};
|
||||||
use crate::plugin::PluginConfig;
|
use crate::plugin::PluginConfig;
|
||||||
use crate::{
|
use crate::{
|
||||||
EngineManifestConfig, McpConfig, McpStdioCwdPolicy, MemoryConfig, Permission, ResolveError,
|
EngineManifestConfig, McpConfig, McpStdioCwdPolicy, Permission, ResolveError, ScopeConfig,
|
||||||
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig,
|
ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig, WorkerMetaConfig,
|
||||||
WorkerMetaConfig, paths,
|
paths,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROFILE_FORMAT_V1: &str = "yoi.profile.v1";
|
const PROFILE_FORMAT_V1: &str = "yoi.profile.v1";
|
||||||
@@ -185,7 +185,7 @@ pub fn validate_profile_execution_target(
|
|||||||
if feature.manage_workdir.enabled {
|
if feature.manage_workdir.enabled {
|
||||||
requirements.insert(WorkspaceAuthorityRequirement::ManageWorkdir);
|
requirements.insert(WorkspaceAuthorityRequirement::ManageWorkdir);
|
||||||
}
|
}
|
||||||
if feature.memory.enabled || feature.memory.staging {
|
if feature.memory.profile.enabled || feature.memory.profile.staging_tools {
|
||||||
requirements.insert(WorkspaceAuthorityRequirement::Memory);
|
requirements.insert(WorkspaceAuthorityRequirement::Memory);
|
||||||
}
|
}
|
||||||
if feature.merge_request.show
|
if feature.merge_request.show
|
||||||
@@ -642,7 +642,6 @@ fn resolve_profile_value(
|
|||||||
mcp: profile.mcp,
|
mcp: profile.mcp,
|
||||||
compaction,
|
compaction,
|
||||||
web: profile.web,
|
web: profile.web,
|
||||||
memory: profile.memory.map(Into::into),
|
|
||||||
skills: profile.skills,
|
skills: profile.skills,
|
||||||
};
|
};
|
||||||
let config =
|
let config =
|
||||||
@@ -663,51 +662,6 @@ fn resolve_profile_value(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
struct ProfileMemoryConfig {
|
|
||||||
#[serde(default)]
|
|
||||||
workspace_root: Option<PathBuf>,
|
|
||||||
#[serde(default)]
|
|
||||||
query_result_limit: Option<usize>,
|
|
||||||
#[serde(default)]
|
|
||||||
query_excerpt_lines: Option<usize>,
|
|
||||||
#[serde(default)]
|
|
||||||
inject_summary: Option<bool>,
|
|
||||||
#[serde(default)]
|
|
||||||
extract_model: Option<ModelManifest>,
|
|
||||||
#[serde(default)]
|
|
||||||
extract_threshold: Option<u64>,
|
|
||||||
#[serde(default)]
|
|
||||||
extract_worker_max_turns: Option<u32>,
|
|
||||||
#[serde(default)]
|
|
||||||
consolidation_model: Option<ModelManifest>,
|
|
||||||
#[serde(default)]
|
|
||||||
consolidation_threshold_files: Option<usize>,
|
|
||||||
#[serde(default)]
|
|
||||||
consolidation_threshold_bytes: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ProfileMemoryConfig> for MemoryConfig {
|
|
||||||
fn from(profile: ProfileMemoryConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
workspace_root: profile.workspace_root,
|
|
||||||
query_result_limit: profile.query_result_limit,
|
|
||||||
query_excerpt_lines: profile.query_excerpt_lines,
|
|
||||||
inject_summary: profile.inject_summary,
|
|
||||||
workspace_id: None,
|
|
||||||
settings_revision: None,
|
|
||||||
language: None,
|
|
||||||
extract_model: profile.extract_model,
|
|
||||||
extract_threshold: profile.extract_threshold,
|
|
||||||
extract_worker_max_turns: profile.extract_worker_max_turns,
|
|
||||||
consolidation_model: profile.consolidation_model,
|
|
||||||
consolidation_threshold_files: profile.consolidation_threshold_files,
|
|
||||||
consolidation_threshold_bytes: profile.consolidation_threshold_bytes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct ProfileConfig {
|
struct ProfileConfig {
|
||||||
@@ -738,8 +692,6 @@ struct ProfileConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
web: Option<WebConfig>,
|
web: Option<WebConfig>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
memory: Option<ProfileMemoryConfig>,
|
|
||||||
#[serde(default)]
|
|
||||||
skills: Option<SkillsConfig>,
|
skills: Option<SkillsConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -940,12 +892,6 @@ fn validate_profile_paths(profile: &ProfileConfig) -> Result<(), ProfileError> {
|
|||||||
.map_err(|source| ProfileError::ProfileDeserialize { source })?;
|
.map_err(|source| ProfileError::ProfileDeserialize { source })?;
|
||||||
reject_absolute_auth_file(&model.auth, "compaction.model.auth.file")?;
|
reject_absolute_auth_file(&model.auth, "compaction.model.auth.file")?;
|
||||||
}
|
}
|
||||||
if let Some(memory) = &profile.memory
|
|
||||||
&& let Some(root) = &memory.workspace_root
|
|
||||||
&& root.is_absolute()
|
|
||||||
{
|
|
||||||
return Err(ProfileError::InvalidProfile("field `memory.workspace_root` is a resolved path and is not allowed in reusable Profiles".into()));
|
|
||||||
}
|
|
||||||
if let Some(skills) = &profile.skills {
|
if let Some(skills) = &profile.skills {
|
||||||
for dir in &skills.directories {
|
for dir in &skills.directories {
|
||||||
if dir.is_absolute() {
|
if dir.is_absolute() {
|
||||||
@@ -1299,7 +1245,9 @@ mod tests {
|
|||||||
("settings_revision", serde_json::json!(2)),
|
("settings_revision", serde_json::json!(2)),
|
||||||
("language", serde_json::json!("Japanese")),
|
("language", serde_json::json!("Japanese")),
|
||||||
] {
|
] {
|
||||||
let artifact = serde_json::json!({ "memory": { (field): value } });
|
let artifact = serde_json::json!({
|
||||||
|
"feature": { "memory": { (field): value } }
|
||||||
|
});
|
||||||
let error = resolve_profile_artifact_value(
|
let error = resolve_profile_artifact_value(
|
||||||
artifact,
|
artifact,
|
||||||
ProfileSource::Registry {
|
ProfileSource::Registry {
|
||||||
@@ -1351,7 +1299,7 @@ mod tests {
|
|||||||
assert!(resolved.manifest.delegation_scope.allow.iter().any(|rule| {
|
assert!(resolved.manifest.delegation_scope.allow.iter().any(|rule| {
|
||||||
rule.permission == protocol::Permission::Write && rule.target == tmp.path()
|
rule.permission == protocol::Permission::Write && rule.target == tmp.path()
|
||||||
}));
|
}));
|
||||||
assert!(!resolved.manifest.feature.memory.enabled);
|
assert!(!resolved.manifest.feature.memory.profile.enabled);
|
||||||
assert!(!resolved.manifest.feature.ticket.enabled);
|
assert!(!resolved.manifest.feature.ticket.enabled);
|
||||||
assert!(!resolved.manifest.feature.objective.enabled);
|
assert!(!resolved.manifest.feature.objective.enabled);
|
||||||
assert!(!resolved.manifest.feature.flow.enabled);
|
assert!(!resolved.manifest.feature.flow.enabled);
|
||||||
@@ -1630,7 +1578,7 @@ enabled = false
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resolved.manifest.worker.name, "runtime-worker");
|
assert_eq!(resolved.manifest.worker.name, "runtime-worker");
|
||||||
assert!(resolved.manifest.feature.task.enabled);
|
assert!(resolved.manifest.feature.task.enabled);
|
||||||
assert!(!resolved.manifest.feature.memory.enabled);
|
assert!(!resolved.manifest.feature.memory.profile.enabled);
|
||||||
assert!(resolved.manifest.feature.web.enabled);
|
assert!(resolved.manifest.feature.web.enabled);
|
||||||
assert!(resolved.manifest.feature.sub_worker.enabled);
|
assert!(resolved.manifest.feature.sub_worker.enabled);
|
||||||
assert!(resolved.manifest.feature.ticket.enabled);
|
assert!(resolved.manifest.feature.ticket.enabled);
|
||||||
|
|||||||
@@ -152,13 +152,10 @@ pub enum MemoryStagingAffectedMemoryOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct MemoryConsolidateStagingOperation {
|
pub struct MemoryConsolidateStagingOperation {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub threshold_files: Option<usize>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub threshold_bytes: Option<u64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -450,10 +447,21 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::extract::{CandidateKind, ExtractedCandidate};
|
use crate::extract::{CandidateKind, ExtractedCandidate};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn consolidation_operation_rejects_caller_owned_thresholds() {
|
||||||
|
let error =
|
||||||
|
serde_json::from_value::<MemoryConsolidateStagingOperation>(serde_json::json!({
|
||||||
|
"force": false,
|
||||||
|
"threshold_files": 1,
|
||||||
|
}))
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(error.to_string().contains("threshold_files"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn staging_list_read_close_records_reason_and_deletes_candidate() {
|
fn staging_list_read_close_records_reason_and_deletes_candidate() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
let layout = WorkspaceLayout::resolve(&manifest::MemoryConfig::default(), temp.path());
|
let layout = WorkspaceLayout::resolve(temp.path());
|
||||||
let source = SourceRef {
|
let source = SourceRef {
|
||||||
segment_id: "segment-1".into(),
|
segment_id: "segment-1".into(),
|
||||||
range: [0, 1],
|
range: [0, 1],
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ pub struct StagingEntry {
|
|||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub record: StagingRecord,
|
pub record: StagingRecord,
|
||||||
/// このファイルのバイト長。閾値判定 (`consolidation_threshold_bytes`)
|
/// このファイルのバイト長。Backendのconsolidation閾値判定に使用する。
|
||||||
/// に使う。
|
|
||||||
pub bytes: u64,
|
pub bytes: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,24 +70,12 @@ impl WorkspaceLayout {
|
|||||||
Self { root: root.into() }
|
Self { root: root.into() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a layout from a `MemoryConfig`.
|
/// Resolve a layout from the nearest Memory marker.
|
||||||
///
|
///
|
||||||
/// An explicit `memory.workspace_root` is honored exactly. Without an
|
/// Resolution searches `default_root` and its ancestors for the nearest
|
||||||
/// explicit root, resolution searches `default_root` and its ancestors for
|
/// `.yoi/memory` directory. This legacy local-storage helper owns its path
|
||||||
/// the nearest `.yoi/memory` directory. This keeps child worktrees that
|
/// policy directly; resolved Worker Manifests do not carry storage paths.
|
||||||
/// contain `.yoi` project records such as tickets from
|
pub fn resolve(default_root: &Path) -> Self {
|
||||||
/// becoming independent memory roots merely because they contain `.yoi`.
|
|
||||||
///
|
|
||||||
/// If no memory marker exists, this falls back to `default_root` because
|
|
||||||
/// existing call sites require a concrete layout. That fallback is a
|
|
||||||
/// no-marker compatibility path, not a `.yoi` marker interpretation; it
|
|
||||||
/// must not be used as evidence that `.yoi` alone enables repo-local
|
|
||||||
/// memory.
|
|
||||||
pub fn resolve(cfg: &manifest::MemoryConfig, default_root: &Path) -> Self {
|
|
||||||
if let Some(root) = &cfg.workspace_root {
|
|
||||||
return Self::new(root.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let root =
|
let root =
|
||||||
find_memory_marker_root(default_root).unwrap_or_else(|| default_root.to_path_buf());
|
find_memory_marker_root(default_root).unwrap_or_else(|| default_root.to_path_buf());
|
||||||
Self::new(root)
|
Self::new(root)
|
||||||
@@ -335,16 +323,6 @@ mod tests {
|
|||||||
assert!(matches!(err, LintError::InvalidPath(_)));
|
assert!(matches!(err, LintError::InvalidPath(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_uses_workspace_root_when_set() {
|
|
||||||
let cfg = manifest::MemoryConfig {
|
|
||||||
workspace_root: Some(PathBuf::from("/explicit")),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let layout = WorkspaceLayout::resolve(&cfg, Path::new("/fallback"));
|
|
||||||
assert_eq!(layout.root(), Path::new("/explicit"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_selects_nearest_ancestor_memory_marker_when_workspace_root_missing() {
|
fn resolve_selects_nearest_ancestor_memory_marker_when_workspace_root_missing() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
@@ -353,8 +331,7 @@ mod tests {
|
|||||||
std::fs::create_dir_all(workspace.join(".yoi/memory")).unwrap();
|
std::fs::create_dir_all(workspace.join(".yoi/memory")).unwrap();
|
||||||
std::fs::create_dir_all(&child).unwrap();
|
std::fs::create_dir_all(&child).unwrap();
|
||||||
|
|
||||||
let cfg = manifest::MemoryConfig::default();
|
let layout = WorkspaceLayout::resolve(&child);
|
||||||
let layout = WorkspaceLayout::resolve(&cfg, &child);
|
|
||||||
assert_eq!(layout.root(), workspace.as_path());
|
assert_eq!(layout.root(), workspace.as_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,8 +343,7 @@ mod tests {
|
|||||||
std::fs::create_dir_all(workspace.join(".yoi/memory")).unwrap();
|
std::fs::create_dir_all(workspace.join(".yoi/memory")).unwrap();
|
||||||
std::fs::create_dir_all(child.join(".yoi/tickets")).unwrap();
|
std::fs::create_dir_all(child.join(".yoi/tickets")).unwrap();
|
||||||
|
|
||||||
let cfg = manifest::MemoryConfig::default();
|
let layout = WorkspaceLayout::resolve(&child);
|
||||||
let layout = WorkspaceLayout::resolve(&cfg, &child);
|
|
||||||
assert_eq!(layout.root(), workspace.as_path());
|
assert_eq!(layout.root(), workspace.as_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +357,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(find_memory_marker_root(&child), None);
|
assert_eq!(find_memory_marker_root(&child), None);
|
||||||
|
|
||||||
let cfg = manifest::MemoryConfig::default();
|
let layout = WorkspaceLayout::resolve(&child);
|
||||||
let layout = WorkspaceLayout::resolve(&cfg, &child);
|
|
||||||
assert_eq!(layout.root(), child.as_path());
|
assert_eq!(layout.root(), child.as_path());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ worker_context_max_tokens = 100000
|
|||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
[feature.memory]
|
[feature.memory]
|
||||||
enabled = true
|
enabled = false
|
||||||
|
|
||||||
[feature.web]
|
[feature.web]
|
||||||
enabled = true
|
enabled = true
|
||||||
@@ -241,11 +241,6 @@ enabled = true
|
|||||||
authoring = true
|
authoring = true
|
||||||
thread = true
|
thread = true
|
||||||
|
|
||||||
[memory]
|
|
||||||
extract_threshold = 50000
|
|
||||||
consolidation_threshold_files = 5
|
|
||||||
consolidation_threshold_bytes = 50000
|
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
|
|||||||
@@ -759,8 +759,8 @@ fn migrate_worker_aggregate_document(
|
|||||||
.get_mut("resolved_manifest_snapshot")
|
.get_mut("resolved_manifest_snapshot")
|
||||||
.filter(|snapshot| !snapshot.is_null())
|
.filter(|snapshot| !snapshot.is_null())
|
||||||
{
|
{
|
||||||
let manifest: manifest::WorkerManifest =
|
let mut manifest = manifest::read_persisted_worker_manifest_snapshot(snapshot.clone())
|
||||||
serde_json::from_value(snapshot.clone()).map_err(|error| {
|
.map_err(|error| {
|
||||||
runtime_store_corrupt(
|
runtime_store_corrupt(
|
||||||
metadata_path,
|
metadata_path,
|
||||||
format!("decode Worker aggregate resolved manifest snapshot: {error}"),
|
format!("decode Worker aggregate resolved manifest snapshot: {error}"),
|
||||||
@@ -775,20 +775,14 @@ fn migrate_worker_aggregate_document(
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
snapshot
|
manifest.worker.name = expected_name.clone();
|
||||||
.as_object_mut()
|
*snapshot =
|
||||||
.and_then(|manifest| manifest.get_mut("worker"))
|
manifest::write_persisted_worker_manifest_snapshot(&manifest).map_err(|error| {
|
||||||
.and_then(serde_json::Value::as_object_mut)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
runtime_store_corrupt(
|
runtime_store_corrupt(
|
||||||
metadata_path,
|
metadata_path,
|
||||||
"Worker aggregate resolved manifest is missing worker metadata".to_string(),
|
format!("encode migrated Worker aggregate resolved manifest: {error}"),
|
||||||
)
|
)
|
||||||
})?
|
})?;
|
||||||
.insert(
|
|
||||||
"name".to_string(),
|
|
||||||
serde_json::Value::String(expected_name.clone()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
"worker_name".to_string(),
|
"worker_name".to_string(),
|
||||||
@@ -809,8 +803,8 @@ fn migrate_worker_aggregate_document(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(snapshot) = metadata.resolved_manifest_snapshot {
|
if let Some(snapshot) = metadata.resolved_manifest_snapshot {
|
||||||
let manifest: manifest::WorkerManifest =
|
let manifest =
|
||||||
serde_json::from_value(snapshot).map_err(|error| {
|
manifest::read_persisted_worker_manifest_snapshot(snapshot).map_err(|error| {
|
||||||
runtime_store_corrupt(
|
runtime_store_corrupt(
|
||||||
metadata_path,
|
metadata_path,
|
||||||
format!("decode migrated Worker aggregate resolved manifest: {error}"),
|
format!("decode migrated Worker aggregate resolved manifest: {error}"),
|
||||||
|
|||||||
@@ -746,9 +746,15 @@ fn bind_workspace_memory_settings(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
manifest
|
manifest
|
||||||
|
.feature
|
||||||
.memory
|
.memory
|
||||||
.get_or_insert_with(manifest::MemoryConfig::default)
|
.bind_workspace_settings(snapshot.clone())
|
||||||
.bind_workspace_settings(snapshot);
|
.map_err(str::to_string)?;
|
||||||
|
manifest
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.validate_execution()
|
||||||
|
.map_err(str::to_string)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,10 +765,18 @@ fn validate_worker_memory_settings(
|
|||||||
let Some(expected) = request.memory_settings.as_ref() else {
|
let Some(expected) = request.memory_settings.as_ref() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let actual = manifest
|
manifest
|
||||||
|
.feature
|
||||||
.memory
|
.memory
|
||||||
.as_ref()
|
.validate_execution()
|
||||||
.and_then(manifest::MemoryConfig::workspace_settings)
|
.map_err(str::to_string)?;
|
||||||
|
if !manifest.feature.memory.profile.enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let actual = manifest
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.workspace_settings()
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
"Workspace Worker restored without its bound Memory settings snapshot".to_string()
|
"Workspace Worker restored without its bound Memory settings snapshot".to_string()
|
||||||
})?;
|
})?;
|
||||||
@@ -3165,7 +3179,7 @@ mod tests {
|
|||||||
Some(session_store::WorkerActiveSegmentRef::pending_segment(
|
Some(session_store::WorkerActiveSegmentRef::pending_segment(
|
||||||
session_id,
|
session_id,
|
||||||
)),
|
)),
|
||||||
Some(serde_json::to_value(&manifest).unwrap()),
|
Some(manifest::write_persisted_worker_manifest_snapshot(&manifest).unwrap()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -901,27 +901,39 @@ pub(crate) fn wire_event_bridges_on_engine<C, St>(
|
|||||||
// per-item commit channel is wired at the top of this function.
|
// per-item commit channel is wired at the top of this function.
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_memory_lifecycle_if_configured<M>(
|
fn add_memory_tools_if_configured<M>(
|
||||||
registry: &mut FeatureRegistryBuilder,
|
registry: &mut FeatureRegistryBuilder,
|
||||||
config: Option<manifest::MemoryConfig>,
|
config: &manifest::ResolvedMemoryFeatureConfig,
|
||||||
workspace_bound: bool,
|
build: impl FnOnce() -> std::io::Result<M>,
|
||||||
build: impl FnOnce(manifest::MemoryConfig) -> std::io::Result<M>,
|
|
||||||
) -> std::io::Result<bool>
|
) -> std::io::Result<bool>
|
||||||
where
|
where
|
||||||
M: crate::feature::FeatureModule + 'static,
|
M: crate::feature::FeatureModule + 'static,
|
||||||
{
|
{
|
||||||
let Some(config) = config else {
|
if !config.profile.enabled {
|
||||||
return Ok(false);
|
|
||||||
};
|
|
||||||
if config.workspace_settings().is_none() {
|
|
||||||
if workspace_bound {
|
|
||||||
return Err(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::InvalidInput,
|
|
||||||
"Workspace-bound Memory requires a Backend-authored settings snapshot",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
config
|
||||||
|
.validate_execution()
|
||||||
|
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
|
||||||
|
registry.add_module(build()?);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_memory_lifecycle_if_configured<M>(
|
||||||
|
registry: &mut FeatureRegistryBuilder,
|
||||||
|
config: manifest::ResolvedMemoryFeatureConfig,
|
||||||
|
lifecycle_enabled: bool,
|
||||||
|
build: impl FnOnce(manifest::ResolvedMemoryFeatureConfig) -> std::io::Result<M>,
|
||||||
|
) -> std::io::Result<bool>
|
||||||
|
where
|
||||||
|
M: crate::feature::FeatureModule + 'static,
|
||||||
|
{
|
||||||
|
if !lifecycle_enabled || !config.profile.enabled || !config.profile.extraction.enabled {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
config
|
||||||
|
.validate_execution()
|
||||||
|
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
|
||||||
registry.add_module(build(config)?);
|
registry.add_module(build(config)?);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
@@ -962,7 +974,7 @@ where
|
|||||||
let local_filesystem = worker.local_working_directory().cloned();
|
let local_filesystem = worker.local_working_directory().cloned();
|
||||||
let local_workspace_root = local_filesystem.as_ref().map(|local| local.root.clone());
|
let local_workspace_root = local_filesystem.as_ref().map(|local| local.root.clone());
|
||||||
let task_feature = worker.task_feature();
|
let task_feature = worker.task_feature();
|
||||||
let memory_config = worker.manifest().memory.clone();
|
let memory_config = feature_config.memory.clone();
|
||||||
let web_config = worker.manifest().web.clone();
|
let web_config = worker.manifest().web.clone();
|
||||||
let mcp_config = worker.manifest().mcp.clone();
|
let mcp_config = worker.manifest().mcp.clone();
|
||||||
let spawner_name = worker.manifest().worker.name.clone();
|
let spawner_name = worker.manifest().worker.name.clone();
|
||||||
@@ -1019,13 +1031,23 @@ where
|
|||||||
let worker_enabled = feature_config.worker.enabled;
|
let worker_enabled = feature_config.worker.enabled;
|
||||||
let sub_worker_enabled = feature_config.sub_worker.enabled;
|
let sub_worker_enabled = feature_config.sub_worker.enabled;
|
||||||
let mut feature_registry = FeatureRegistryBuilder::new();
|
let mut feature_registry = FeatureRegistryBuilder::new();
|
||||||
|
add_memory_tools_if_configured(&mut feature_registry, &memory_config, || {
|
||||||
|
let workspace_client = worker.workspace_client_handle();
|
||||||
|
if !workspace_client.is_available() || workspace_client.workspace_id().is_none() {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"Memory tools require Backend Workspace API authority",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(crate::feature::builtin::memory::MemoryToolsFeature::new(
|
||||||
|
workspace_client,
|
||||||
|
memory_config.profile.staging_tools,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
add_memory_lifecycle_if_configured(
|
add_memory_lifecycle_if_configured(
|
||||||
&mut feature_registry,
|
&mut feature_registry,
|
||||||
worker
|
memory_config.clone(),
|
||||||
.manifest_lifecycle_features_enabled()
|
worker.manifest_lifecycle_features_enabled(),
|
||||||
.then(|| memory_config.clone())
|
|
||||||
.flatten(),
|
|
||||||
spawner_workspace_context.workspace_id().is_some(),
|
|
||||||
|config| {
|
|config| {
|
||||||
let workspace_client = worker.workspace_client_handle();
|
let workspace_client = worker.workspace_client_handle();
|
||||||
if !workspace_client.is_available() || workspace_client.workspace_id().is_none() {
|
if !workspace_client.is_available() || workspace_client.workspace_id().is_none() {
|
||||||
@@ -1202,38 +1224,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory tools require explicit feature exposure. Workspace memory access
|
|
||||||
// is authority-bound to the Backend Workspace API; the Worker must not
|
|
||||||
// register local filesystem memory tools even when it has local cwd/root
|
|
||||||
// authority for shell/file tools.
|
|
||||||
if feature_config.memory.enabled {
|
|
||||||
let _mem = memory_config.as_ref().ok_or_else(|| {
|
|
||||||
std::io::Error::new(
|
|
||||||
std::io::ErrorKind::InvalidInput,
|
|
||||||
"[feature.memory].enabled = true requires a [memory] configuration section",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
if workspace_client.is_available() && workspace_client.workspace_id().is_some() {
|
|
||||||
let definitions = if feature_config.memory.staging {
|
|
||||||
crate::feature::builtin::memory::workspace_http_memory_consolidation_tools(
|
|
||||||
workspace_client.clone(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
crate::feature::builtin::memory::workspace_http_memory_tools(
|
|
||||||
workspace_client.clone(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
for definition in definitions {
|
|
||||||
engine.register_tool(definition);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::InvalidInput,
|
|
||||||
"memory tools require Backend Workspace API authority",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut observation_providers: Vec<
|
let mut observation_providers: Vec<
|
||||||
Arc<dyn crate::feature::builtin::worker_observation::WorkerObservationProvider>,
|
Arc<dyn crate::feature::builtin::worker_observation::WorkerObservationProvider>,
|
||||||
> = Vec::new();
|
> = Vec::new();
|
||||||
@@ -2169,7 +2159,7 @@ mod tests {
|
|||||||
use tokio::net::UnixListener;
|
use tokio::net::UnixListener;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn memory_lifecycle_registration_requires_bound_workspace_memory_config() {
|
fn memory_feature_registration_requires_bound_workspace_memory_config() {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct TestMemoryLifecycleModule;
|
struct TestMemoryLifecycleModule;
|
||||||
|
|
||||||
@@ -2189,16 +2179,43 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut registry = FeatureRegistryBuilder::new();
|
||||||
|
let installed = add_memory_tools_if_configured::<TestMemoryLifecycleModule>(
|
||||||
|
&mut registry,
|
||||||
|
&manifest::ResolvedMemoryFeatureConfig::default(),
|
||||||
|
|| panic!("disabled Memory must not construct its tools Feature"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!installed);
|
||||||
|
|
||||||
|
let mut missing_snapshot = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
|
missing_snapshot.profile.enabled = true;
|
||||||
|
let error = add_memory_tools_if_configured::<TestMemoryLifecycleModule>(
|
||||||
|
&mut registry,
|
||||||
|
&missing_snapshot,
|
||||||
|
|| panic!("invalid Memory config must fail before tools Feature construction"),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.to_string()
|
||||||
|
.contains("requires trusted Workspace settings")
|
||||||
|
);
|
||||||
|
|
||||||
let mut registry = FeatureRegistryBuilder::new();
|
let mut registry = FeatureRegistryBuilder::new();
|
||||||
let configured = std::cell::Cell::new(false);
|
let configured = std::cell::Cell::new(false);
|
||||||
let mut memory_config = manifest::MemoryConfig::default();
|
let mut memory_config = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
memory_config.bind_workspace_settings(&manifest::WorkspaceMemorySettingsSnapshot {
|
memory_config.profile.enabled = true;
|
||||||
workspace_id: "workspace-1".to_string(),
|
memory_config.profile.extraction.enabled = true;
|
||||||
settings_revision: 1,
|
memory_config
|
||||||
language: "English".to_string(),
|
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
|
||||||
});
|
workspace_id: "workspace-1".to_string(),
|
||||||
|
settings_revision: 1,
|
||||||
|
language: "English".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
let installed =
|
let installed =
|
||||||
add_memory_lifecycle_if_configured(&mut registry, Some(memory_config), true, |_| {
|
add_memory_lifecycle_if_configured(&mut registry, memory_config, true, |_| {
|
||||||
configured.set(true);
|
configured.set(true);
|
||||||
Ok(TestMemoryLifecycleModule)
|
Ok(TestMemoryLifecycleModule)
|
||||||
})
|
})
|
||||||
@@ -2209,25 +2226,38 @@ mod tests {
|
|||||||
let mut registry = FeatureRegistryBuilder::new();
|
let mut registry = FeatureRegistryBuilder::new();
|
||||||
let installed = add_memory_lifecycle_if_configured::<TestMemoryLifecycleModule>(
|
let installed = add_memory_lifecycle_if_configured::<TestMemoryLifecycleModule>(
|
||||||
&mut registry,
|
&mut registry,
|
||||||
None,
|
manifest::ResolvedMemoryFeatureConfig::default(),
|
||||||
false,
|
true,
|
||||||
|_| panic!("disabled Memory must not construct its lifecycle Feature"),
|
|_| panic!("disabled Memory must not construct its lifecycle Feature"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!installed);
|
assert!(!installed);
|
||||||
|
|
||||||
|
let mut lifecycle_disabled = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
|
lifecycle_disabled.profile.enabled = true;
|
||||||
|
lifecycle_disabled.profile.extraction.enabled = true;
|
||||||
|
lifecycle_disabled
|
||||||
|
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
|
||||||
|
workspace_id: "workspace-1".to_string(),
|
||||||
|
settings_revision: 1,
|
||||||
|
language: "English".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
let installed = add_memory_lifecycle_if_configured::<TestMemoryLifecycleModule>(
|
let installed = add_memory_lifecycle_if_configured::<TestMemoryLifecycleModule>(
|
||||||
&mut registry,
|
&mut registry,
|
||||||
Some(manifest::MemoryConfig::default()),
|
lifecycle_disabled,
|
||||||
false,
|
false,
|
||||||
|_| panic!("Memory without a Backend-authored settings snapshot must stay disabled"),
|
|_| panic!("disabled lifecycle must not construct its Feature"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!installed);
|
assert!(!installed);
|
||||||
|
|
||||||
|
let mut missing_snapshot = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
|
missing_snapshot.profile.enabled = true;
|
||||||
|
missing_snapshot.profile.extraction.enabled = true;
|
||||||
let error = add_memory_lifecycle_if_configured::<TestMemoryLifecycleModule>(
|
let error = add_memory_lifecycle_if_configured::<TestMemoryLifecycleModule>(
|
||||||
&mut registry,
|
&mut registry,
|
||||||
Some(manifest::MemoryConfig::default()),
|
missing_snapshot,
|
||||||
true,
|
true,
|
||||||
|_| panic!("invalid Workspace Memory config must fail before Feature construction"),
|
|_| panic!("invalid Workspace Memory config must fail before Feature construction"),
|
||||||
)
|
)
|
||||||
@@ -2235,7 +2265,7 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
error
|
error
|
||||||
.to_string()
|
.to_string()
|
||||||
.contains("Backend-authored settings snapshot")
|
.contains("requires trusted Workspace settings")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ use schemars::JsonSchema;
|
|||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::feature::{
|
||||||
|
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution,
|
||||||
|
ToolDeclaration,
|
||||||
|
};
|
||||||
use crate::worker::{
|
use crate::worker::{
|
||||||
WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceRequestMethod,
|
WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceRequestMethod,
|
||||||
};
|
};
|
||||||
@@ -338,6 +342,44 @@ fn query_schema() -> serde_json::Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MemoryToolsFeature {
|
||||||
|
tools: Vec<ToolDefinition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryToolsFeature {
|
||||||
|
pub fn new(client: Arc<dyn WorkspaceClient>, staging_tools: bool) -> Self {
|
||||||
|
let tools = if staging_tools {
|
||||||
|
workspace_http_memory_consolidation_tools(client)
|
||||||
|
} else {
|
||||||
|
workspace_http_memory_tools(client)
|
||||||
|
};
|
||||||
|
Self { tools }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureModule for MemoryToolsFeature {
|
||||||
|
fn descriptor(&self) -> FeatureDescriptor {
|
||||||
|
let mut descriptor = FeatureDescriptor::builtin("memory", "Memory")
|
||||||
|
.with_description("Workspace Memory document, query, and staging tools.");
|
||||||
|
for tool in &self.tools {
|
||||||
|
let (meta, _) = tool();
|
||||||
|
descriptor = descriptor.with_tool(ToolDeclaration::new(meta.name, meta.description));
|
||||||
|
}
|
||||||
|
descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
|
||||||
|
for tool in &self.tools {
|
||||||
|
let (meta, _) = tool();
|
||||||
|
context
|
||||||
|
.tools()
|
||||||
|
.register(ToolContribution::new(meta.name, tool.clone()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -368,6 +410,20 @@ mod tests {
|
|||||||
.input_schema
|
.input_schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memory_feature_owns_normal_and_staging_tool_surfaces() {
|
||||||
|
let normal = MemoryToolsFeature::new(test_client(), false);
|
||||||
|
let normal_names = tool_names(normal.tools);
|
||||||
|
assert!(normal_names.contains(&"MemoryQuery".to_string()));
|
||||||
|
assert!(!normal_names.contains(&"MemoryStagingList".to_string()));
|
||||||
|
|
||||||
|
let staging = MemoryToolsFeature::new(test_client(), true);
|
||||||
|
assert_eq!(staging.descriptor().id.as_str(), "builtin:memory");
|
||||||
|
let staging_names = tool_names(staging.tools);
|
||||||
|
assert!(staging_names.contains(&"MemoryQuery".to_string()));
|
||||||
|
assert!(staging_names.contains(&"MemoryStagingList".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normal_workspace_memory_tools_do_not_include_staging_tools() {
|
fn normal_workspace_memory_tools_do_not_include_staging_tools() {
|
||||||
let names = tool_names(workspace_http_memory_tools(test_client()));
|
let names = tool_names(workspace_http_memory_tools(test_client()));
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ pub(crate) struct MemoryLifecycleFeature {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct MemoryLifecycleTask {
|
struct MemoryLifecycleTask {
|
||||||
config: manifest::MemoryConfig,
|
config: manifest::ResolvedMemoryFeatureConfig,
|
||||||
capture: CommittedSessionCaptureHandle,
|
capture: CommittedSessionCaptureHandle,
|
||||||
extensions: SessionExtensionHandle,
|
extensions: SessionExtensionHandle,
|
||||||
workspace_client: Arc<dyn WorkspaceClient>,
|
workspace_client: Arc<dyn WorkspaceClient>,
|
||||||
@@ -66,7 +66,7 @@ struct MemoryLifecycleTask {
|
|||||||
impl MemoryLifecycleFeature {
|
impl MemoryLifecycleFeature {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
config: manifest::MemoryConfig,
|
config: manifest::ResolvedMemoryFeatureConfig,
|
||||||
capture: CommittedSessionCaptureHandle,
|
capture: CommittedSessionCaptureHandle,
|
||||||
extensions: SessionExtensionHandle,
|
extensions: SessionExtensionHandle,
|
||||||
workspace_client: Arc<dyn WorkspaceClient>,
|
workspace_client: Arc<dyn WorkspaceClient>,
|
||||||
@@ -132,7 +132,9 @@ impl MemoryLifecycleTask {
|
|||||||
memory::audit::AuditWorker::MemoryExtract,
|
memory::audit::AuditWorker::MemoryExtract,
|
||||||
memory::audit::AuditTrigger::TokenThreshold,
|
memory::audit::AuditTrigger::TokenThreshold,
|
||||||
self.config
|
self.config
|
||||||
.extract_model
|
.profile
|
||||||
|
.extraction
|
||||||
|
.model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.or(Some(&self.manifest.model))
|
.or(Some(&self.manifest.model))
|
||||||
.map(model_audit_from_manifest),
|
.map(model_audit_from_manifest),
|
||||||
@@ -188,7 +190,9 @@ impl MemoryLifecycleTask {
|
|||||||
};
|
};
|
||||||
let Some(threshold) = self
|
let Some(threshold) = self
|
||||||
.config
|
.config
|
||||||
.extract_threshold
|
.profile
|
||||||
|
.extraction
|
||||||
|
.threshold
|
||||||
.filter(|threshold| *threshold > 0)
|
.filter(|threshold| *threshold > 0)
|
||||||
else {
|
else {
|
||||||
audit
|
audit
|
||||||
@@ -283,7 +287,7 @@ impl MemoryLifecycleTask {
|
|||||||
source,
|
source,
|
||||||
audit.run_id.to_string(),
|
audit.run_id.to_string(),
|
||||||
);
|
);
|
||||||
let client = if let Some(model) = self.config.extract_model.as_ref() {
|
let client = if let Some(model) = self.config.profile.extraction.model.as_ref() {
|
||||||
match crate::model_client::build_client(model) {
|
match crate::model_client::build_client(model) {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -321,7 +325,7 @@ impl MemoryLifecycleTask {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut manifest = self.manifest.clone();
|
let mut manifest = self.manifest.clone();
|
||||||
if let Some(model) = self.config.extract_model.clone() {
|
if let Some(model) = self.config.profile.extraction.model.clone() {
|
||||||
manifest.model = model;
|
manifest.model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +353,9 @@ impl MemoryLifecycleTask {
|
|||||||
cache_key: Some(capture.segment_id.clone()),
|
cache_key: Some(capture.segment_id.clone()),
|
||||||
max_turns: self
|
max_turns: self
|
||||||
.config
|
.config
|
||||||
.extract_worker_max_turns
|
.profile
|
||||||
|
.extraction
|
||||||
|
.worker_max_turns
|
||||||
.or(manifest::defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS),
|
.or(manifest::defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS),
|
||||||
engine_configurator: None,
|
engine_configurator: None,
|
||||||
features,
|
features,
|
||||||
@@ -493,36 +499,13 @@ impl MemoryLifecycleTask {
|
|||||||
let audit = WorkerAuditBase::new(
|
let audit = WorkerAuditBase::new(
|
||||||
memory::audit::AuditWorker::MemoryConsolidation,
|
memory::audit::AuditWorker::MemoryConsolidation,
|
||||||
memory::audit::AuditTrigger::StagingBacklog,
|
memory::audit::AuditTrigger::StagingBacklog,
|
||||||
self.config
|
Some(model_audit_from_manifest(&self.manifest.model)),
|
||||||
.consolidation_model
|
|
||||||
.as_ref()
|
|
||||||
.or(Some(&self.manifest.model))
|
|
||||||
.map(model_audit_from_manifest),
|
|
||||||
)
|
)
|
||||||
.with_memory_settings(&self.config);
|
.with_memory_settings(&self.config);
|
||||||
let Some((threshold_files, threshold_bytes)) = consolidation_thresholds(&self.config)
|
|
||||||
else {
|
|
||||||
audit
|
|
||||||
.emit(
|
|
||||||
self.workspace_client.as_ref(),
|
|
||||||
self.event_tx.as_ref(),
|
|
||||||
memory::audit::WorkerLifecycleStatus::Skipped,
|
|
||||||
"consolidation_threshold_disabled",
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
match self
|
match self
|
||||||
.workspace_client
|
.workspace_client
|
||||||
.request_memory_staging_consolidation(
|
.request_memory_staging_consolidation(
|
||||||
memory::backend::MemoryConsolidateStagingOperation {
|
memory::backend::MemoryConsolidateStagingOperation { force: false },
|
||||||
force: false,
|
|
||||||
threshold_files,
|
|
||||||
threshold_bytes,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -646,22 +629,6 @@ fn extract_pointer(
|
|||||||
Ok(pointer)
|
Ok(pointer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn consolidation_thresholds(
|
|
||||||
config: &manifest::MemoryConfig,
|
|
||||||
) -> Option<(Option<usize>, Option<u64>)> {
|
|
||||||
let threshold_files = config
|
|
||||||
.consolidation_threshold_files
|
|
||||||
.filter(|threshold| *threshold > 0);
|
|
||||||
let threshold_bytes = config
|
|
||||||
.consolidation_threshold_bytes
|
|
||||||
.filter(|threshold| *threshold > 0);
|
|
||||||
if threshold_files.is_none() && threshold_bytes.is_none() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some((threshold_files, threshold_bytes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extraction_run_eligible(exit: CommittedRunExit) -> bool {
|
fn extraction_run_eligible(exit: CommittedRunExit) -> bool {
|
||||||
exit == CommittedRunExit::Finished
|
exit == CommittedRunExit::Finished
|
||||||
}
|
}
|
||||||
@@ -688,12 +655,17 @@ fn tokens_since_pointer(
|
|||||||
fn extraction_threshold_reached(
|
fn extraction_threshold_reached(
|
||||||
capture: &CommittedSessionCapture,
|
capture: &CommittedSessionCapture,
|
||||||
pointer: Option<&memory::ExtractPointerPayload>,
|
pointer: Option<&memory::ExtractPointerPayload>,
|
||||||
config: &manifest::MemoryConfig,
|
config: &manifest::ResolvedMemoryFeatureConfig,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if capture.history.is_empty() {
|
if capture.history.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let Some(threshold) = config.extract_threshold.filter(|threshold| *threshold > 0) else {
|
let Some(threshold) = config
|
||||||
|
.profile
|
||||||
|
.extraction
|
||||||
|
.threshold
|
||||||
|
.filter(|threshold| *threshold > 0)
|
||||||
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
tokens_since_pointer(capture, pointer) >= threshold
|
tokens_since_pointer(capture, pointer) >= threshold
|
||||||
@@ -723,7 +695,7 @@ impl WorkerAuditBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_memory_settings(mut self, config: &manifest::MemoryConfig) -> Self {
|
fn with_memory_settings(mut self, config: &manifest::ResolvedMemoryFeatureConfig) -> Self {
|
||||||
self.memory_settings =
|
self.memory_settings =
|
||||||
config
|
config
|
||||||
.workspace_settings()
|
.workspace_settings()
|
||||||
@@ -1014,16 +986,18 @@ permission = "write"
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_config() -> manifest::MemoryConfig {
|
fn test_config() -> manifest::ResolvedMemoryFeatureConfig {
|
||||||
let mut config = manifest::MemoryConfig {
|
let mut config = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
extract_threshold: Some(1),
|
config.profile.enabled = true;
|
||||||
..Default::default()
|
config.profile.extraction.enabled = true;
|
||||||
};
|
config.profile.extraction.threshold = Some(1);
|
||||||
config.bind_workspace_settings(&manifest::WorkspaceMemorySettingsSnapshot {
|
config
|
||||||
workspace_id: "workspace-1".to_string(),
|
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
|
||||||
settings_revision: 1,
|
workspace_id: "workspace-1".to_string(),
|
||||||
language: "English".to_string(),
|
settings_revision: 1,
|
||||||
});
|
language: "English".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1282,30 +1256,30 @@ permission = "write"
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn lifecycle_task_requests_backend_consolidation_from_configured_threshold() {
|
async fn lifecycle_task_requests_backend_owned_consolidation_eligibility() {
|
||||||
let client = ScriptClient::new(Vec::new());
|
let client = ScriptClient::new(Vec::new());
|
||||||
let extension_writes = Arc::new(Mutex::new(Vec::new()));
|
let extension_writes = Arc::new(Mutex::new(Vec::new()));
|
||||||
let (event_tx, _) = broadcast::channel(16);
|
let (event_tx, _) = broadcast::channel(16);
|
||||||
let workspace_client = Arc::new(RecordingWorkspaceClient::default());
|
let workspace_client = Arc::new(RecordingWorkspaceClient::default());
|
||||||
let mut interrupted = capture(2, 250);
|
let mut interrupted = capture(2, 250);
|
||||||
interrupted.run_exit = CommittedRunExit::Interrupted;
|
interrupted.run_exit = CommittedRunExit::Interrupted;
|
||||||
let mut task = test_task(
|
let task = test_task(
|
||||||
interrupted,
|
interrupted,
|
||||||
Box::new(client),
|
Box::new(client),
|
||||||
extension_writes,
|
extension_writes,
|
||||||
event_tx,
|
event_tx,
|
||||||
workspace_client.clone(),
|
workspace_client.clone(),
|
||||||
);
|
);
|
||||||
task.config.consolidation_threshold_files = Some(3);
|
|
||||||
run_background_task(task).await;
|
run_background_task(task).await;
|
||||||
|
|
||||||
let requests = workspace_client.requests.lock().unwrap();
|
let requests = workspace_client.requests.lock().unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
requests.iter().any(|request| {
|
requests.iter().any(|request| {
|
||||||
request.path.contains("memory")
|
request.path.contains("memory")
|
||||||
&& request.body.as_deref().is_some_and(|body| {
|
&& request
|
||||||
body.contains("\"threshold_files\":3") && body.contains("\"force\":false")
|
.body
|
||||||
})
|
.as_deref()
|
||||||
|
.is_some_and(|body| body == "{\"force\":false}")
|
||||||
}),
|
}),
|
||||||
"recorded requests: {requests:?}"
|
"recorded requests: {requests:?}"
|
||||||
);
|
);
|
||||||
@@ -1349,17 +1323,6 @@ permission = "write"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn consolidation_thresholds_enable_backend_request_on_either_limit() {
|
|
||||||
let mut config = manifest::MemoryConfig::default();
|
|
||||||
assert_eq!(consolidation_thresholds(&config), None);
|
|
||||||
config.consolidation_threshold_files = Some(3);
|
|
||||||
assert_eq!(consolidation_thresholds(&config), Some((Some(3), None)));
|
|
||||||
config.consolidation_threshold_files = None;
|
|
||||||
config.consolidation_threshold_bytes = Some(4096);
|
|
||||||
assert_eq!(consolidation_thresholds(&config), Some((None, Some(4096))));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interrupted_parent_run_is_not_extraction_eligible() {
|
fn interrupted_parent_run_is_not_extraction_eligible() {
|
||||||
assert!(extraction_run_eligible(CommittedRunExit::Finished));
|
assert!(extraction_run_eligible(CommittedRunExit::Finished));
|
||||||
@@ -1412,8 +1375,8 @@ permission = "write"
|
|||||||
#[test]
|
#[test]
|
||||||
fn threshold_uses_committed_usage_after_pointer() {
|
fn threshold_uses_committed_usage_after_pointer() {
|
||||||
let capture = capture(2, 250);
|
let capture = capture(2, 250);
|
||||||
let mut config = manifest::MemoryConfig::default();
|
let mut config = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
config.extract_threshold = Some(1);
|
config.profile.extraction.threshold = Some(1);
|
||||||
assert!(extraction_threshold_reached(
|
assert!(extraction_threshold_reached(
|
||||||
&capture,
|
&capture,
|
||||||
Some(&memory::ExtractPointerPayload {
|
Some(&memory::ExtractPointerPayload {
|
||||||
@@ -1500,8 +1463,8 @@ permission = "write"
|
|||||||
#[test]
|
#[test]
|
||||||
fn empty_capture_never_schedules_extraction() {
|
fn empty_capture_never_schedules_extraction() {
|
||||||
let capture = capture(0, 500);
|
let capture = capture(0, 500);
|
||||||
let mut config = manifest::MemoryConfig::default();
|
let mut config = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
config.extract_threshold = Some(1);
|
config.profile.extraction.threshold = Some(1);
|
||||||
assert!(!extraction_threshold_reached(&capture, None, &config));
|
assert!(!extraction_threshold_reached(&capture, None, &config));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -425,6 +425,8 @@ impl Tool for SubWorkerSpawnTool {
|
|||||||
WorkerManifestConfig::resolution_defaults().merge(child_config),
|
WorkerManifestConfig::resolution_defaults().merge(child_config),
|
||||||
)
|
)
|
||||||
.map_err(|error| ToolError::ExecutionFailed(format!("resolve child manifest: {error}")))?;
|
.map_err(|error| ToolError::ExecutionFailed(format!("resolve child manifest: {error}")))?;
|
||||||
|
bind_child_memory_settings(&self.spawner_manifest, &mut child_manifest)
|
||||||
|
.map_err(ToolError::ExecutionFailed)?;
|
||||||
// Delegated children stay bound to their scoped session and cannot use
|
// Delegated children stay bound to their scoped session and cannot use
|
||||||
// Workspace attachment tools to replace it with parent-level authority.
|
// Workspace attachment tools to replace it with parent-level authority.
|
||||||
child_manifest.feature.manage_workdir.enabled = false;
|
child_manifest.feature.manage_workdir.enabled = false;
|
||||||
@@ -827,6 +829,33 @@ fn profile_error_with_available(error: ProfileError, available: &AvailableProfil
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bind_child_memory_settings(
|
||||||
|
parent: &manifest::WorkerManifest,
|
||||||
|
child: &mut manifest::WorkerManifest,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if !child.feature.memory.profile.enabled {
|
||||||
|
return child
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.validate_execution()
|
||||||
|
.map_err(str::to_string);
|
||||||
|
}
|
||||||
|
let workspace_settings = parent.feature.memory.workspace_settings().ok_or_else(|| {
|
||||||
|
"enabled child Memory feature requires the parent's trusted Workspace settings snapshot"
|
||||||
|
.to_string()
|
||||||
|
})?;
|
||||||
|
child
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.bind_workspace_settings(workspace_settings)
|
||||||
|
.map_err(str::to_string)?;
|
||||||
|
child
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.validate_execution()
|
||||||
|
.map_err(str::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfig {
|
fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfig {
|
||||||
WorkerManifestConfig {
|
WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
@@ -894,7 +923,6 @@ fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfi
|
|||||||
model: c.model.clone(),
|
model: c.model.clone(),
|
||||||
}),
|
}),
|
||||||
web: manifest.web.clone(),
|
web: manifest.web.clone(),
|
||||||
memory: manifest.memory.clone(),
|
|
||||||
skills: manifest.skills.clone(),
|
skills: manifest.skills.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1091,10 +1119,7 @@ enabled = true
|
|||||||
thread = true
|
thread = true
|
||||||
|
|
||||||
[feature.memory]
|
[feature.memory]
|
||||||
enabled = true
|
enabled = false
|
||||||
|
|
||||||
[memory]
|
|
||||||
extract_threshold = 4000
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1526,6 +1551,33 @@ extract_threshold = 4000
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn child_memory_inherits_only_the_parents_trusted_settings_snapshot() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let mut parent = parent_manifest(temp.path(), None);
|
||||||
|
parent.feature.memory.profile.enabled = true;
|
||||||
|
parent
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
|
||||||
|
workspace_id: "workspace-1".to_string(),
|
||||||
|
settings_revision: 4,
|
||||||
|
language: "日本語".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let mut child = parent.clone();
|
||||||
|
child.feature.memory.workspace_settings = None;
|
||||||
|
|
||||||
|
bind_child_memory_settings(&parent, &mut child).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
child.feature.memory.workspace_settings(),
|
||||||
|
parent.feature.memory.workspace_settings()
|
||||||
|
);
|
||||||
|
|
||||||
|
child.feature.memory.profile.enabled = false;
|
||||||
|
assert!(bind_child_memory_settings(&parent, &mut child).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
fn write_project_profile_registry(
|
fn write_project_profile_registry(
|
||||||
project: &Path,
|
project: &Path,
|
||||||
default: Option<&str>,
|
default: Option<&str>,
|
||||||
|
|||||||
+68
-64
@@ -2478,11 +2478,10 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
if is_memory_consolidation {
|
if is_memory_consolidation {
|
||||||
let memory_config = self.manifest.memory.as_ref().ok_or_else(|| {
|
let memory_config = &self.manifest.feature.memory;
|
||||||
WorkerError::InvalidState(
|
memory_config
|
||||||
"Memory consolidation Worker has no Memory configuration".to_string(),
|
.validate_execution()
|
||||||
)
|
.map_err(|message| WorkerError::InvalidState(message.to_string()))?;
|
||||||
})?;
|
|
||||||
let language = memory_language(memory_config)?;
|
let language = memory_language(memory_config)?;
|
||||||
let rendered = self
|
let rendered = self
|
||||||
.prompts
|
.prompts
|
||||||
@@ -2517,11 +2516,8 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let inject_summary = self.inject_resident_summary
|
let inject_summary = self.inject_resident_summary
|
||||||
&& self
|
&& self.manifest.feature.memory.profile.enabled
|
||||||
.manifest
|
&& self.manifest.feature.memory.profile.resident.inject_summary;
|
||||||
.memory
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|m| m.inject_summary.unwrap_or(true));
|
|
||||||
let resident_summary: Option<String> = if inject_summary {
|
let resident_summary: Option<String> = if inject_summary {
|
||||||
match self.resident_summary_from_workspace_authority().await {
|
match self.resident_summary_from_workspace_authority().await {
|
||||||
Ok(summary) => summary,
|
Ok(summary) => summary,
|
||||||
@@ -4552,7 +4548,7 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn memory_language(config: &manifest::MemoryConfig) -> Result<String, WorkerError> {
|
fn memory_language(config: &manifest::ResolvedMemoryFeatureConfig) -> Result<String, WorkerError> {
|
||||||
config
|
config
|
||||||
.workspace_settings()
|
.workspace_settings()
|
||||||
.map(|snapshot| snapshot.language)
|
.map(|snapshot| snapshot.language)
|
||||||
@@ -5426,7 +5422,8 @@ fn worker_metadata_for_manifest(
|
|||||||
metadata = metadata.with_workspace_root(local_workspace_root.to_path_buf());
|
metadata = metadata.with_workspace_root(local_workspace_root.to_path_buf());
|
||||||
}
|
}
|
||||||
if should_persist_resolved_manifest_snapshot(manifest) {
|
if should_persist_resolved_manifest_snapshot(manifest) {
|
||||||
metadata.resolved_manifest_snapshot = serde_json::to_value(manifest).ok();
|
metadata.resolved_manifest_snapshot =
|
||||||
|
manifest::write_persisted_worker_manifest_snapshot(manifest).ok();
|
||||||
}
|
}
|
||||||
metadata
|
metadata
|
||||||
}
|
}
|
||||||
@@ -5439,10 +5436,20 @@ fn validate_workspace_memory_snapshot(
|
|||||||
let Some(workspace_id) = workspace_context.workspace_id() else {
|
let Some(workspace_id) = workspace_context.workspace_id() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let snapshot = manifest
|
manifest
|
||||||
|
.feature
|
||||||
.memory
|
.memory
|
||||||
.as_ref()
|
.validate_execution()
|
||||||
.and_then(manifest::MemoryConfig::workspace_settings)
|
.map_err(|message| {
|
||||||
|
WorkerError::InvalidState(format!("Workspace Worker {worker_name}: {message}"))
|
||||||
|
})?;
|
||||||
|
if !manifest.feature.memory.profile.enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let snapshot = manifest
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.workspace_settings()
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
WorkerError::InvalidState(format!(
|
WorkerError::InvalidState(format!(
|
||||||
"Workspace Worker {worker_name} has no complete persisted Memory settings snapshot"
|
"Workspace Worker {worker_name} has no complete persisted Memory settings snapshot"
|
||||||
@@ -5468,11 +5475,7 @@ fn validate_workspace_memory_snapshot(
|
|||||||
fn should_persist_resolved_manifest_snapshot(manifest: &WorkerManifest) -> bool {
|
fn should_persist_resolved_manifest_snapshot(manifest: &WorkerManifest) -> bool {
|
||||||
manifest.profile.is_some()
|
manifest.profile.is_some()
|
||||||
|| manifest.plugins.has_resolved_plan()
|
|| manifest.plugins.has_resolved_plan()
|
||||||
|| manifest
|
|| manifest.feature.memory.workspace_settings.is_some()
|
||||||
.memory
|
|
||||||
.as_ref()
|
|
||||||
.and_then(manifest::MemoryConfig::workspace_settings)
|
|
||||||
.is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_manifest_from_worker_metadata_snapshot(
|
fn restore_manifest_from_worker_metadata_snapshot(
|
||||||
@@ -5481,12 +5484,14 @@ fn restore_manifest_from_worker_metadata_snapshot(
|
|||||||
fallback: WorkerManifest,
|
fallback: WorkerManifest,
|
||||||
) -> Result<WorkerManifest, WorkerError> {
|
) -> Result<WorkerManifest, WorkerError> {
|
||||||
match snapshot {
|
match snapshot {
|
||||||
Some(snapshot) => serde_json::from_value(snapshot).map_err(|source| {
|
Some(snapshot) => {
|
||||||
WorkerError::WorkerMetadataManifestSnapshot {
|
manifest::read_persisted_worker_manifest_snapshot(snapshot).map_err(|source| {
|
||||||
worker_name: worker_name.to_string(),
|
WorkerError::WorkerMetadataManifestSnapshot {
|
||||||
source,
|
worker_name: worker_name.to_string(),
|
||||||
}
|
source,
|
||||||
}),
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
None => Ok(fallback),
|
None => Ok(fallback),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6198,11 +6203,6 @@ fn prepare_worker_common_with_context_and_model_client(
|
|||||||
WorkerFilesystemAuthority::Local(LocalWorkingDirectory { root, cwd })
|
WorkerFilesystemAuthority::Local(LocalWorkingDirectory { root, cwd })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut scope_config = scope_config;
|
|
||||||
if let (Some(mem), Some(local)) = (manifest.memory.as_ref(), filesystem_authority.as_local()) {
|
|
||||||
let layout = memory::WorkspaceLayout::resolve(mem, &local.root);
|
|
||||||
scope_config.deny.extend(memory::deny_write_rules(&layout));
|
|
||||||
}
|
|
||||||
let scope = if scope_config.allow.is_empty() && filesystem_authority.as_local().is_none() {
|
let scope = if scope_config.allow.is_empty() && filesystem_authority.as_local().is_none() {
|
||||||
Scope::empty()
|
Scope::empty()
|
||||||
} else {
|
} else {
|
||||||
@@ -6292,8 +6292,7 @@ mod spawned_context_tests {
|
|||||||
std::fs::create_dir_all(&workspace_root).unwrap();
|
std::fs::create_dir_all(&workspace_root).unwrap();
|
||||||
std::fs::create_dir_all(&cwd).unwrap();
|
std::fs::create_dir_all(&cwd).unwrap();
|
||||||
|
|
||||||
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
let manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
||||||
manifest.memory = Some(manifest::MemoryConfig::default());
|
|
||||||
let common = prepare_worker_common_with_context(
|
let common = prepare_worker_common_with_context(
|
||||||
&manifest,
|
&manifest,
|
||||||
&PromptCatalogSource::builtins_only(),
|
&PromptCatalogSource::builtins_only(),
|
||||||
@@ -6327,8 +6326,7 @@ mod spawned_context_tests {
|
|||||||
let workspace_root = tmp.path().join("workspace-root");
|
let workspace_root = tmp.path().join("workspace-root");
|
||||||
let cwd = workspace_root.join("nested");
|
let cwd = workspace_root.join("nested");
|
||||||
std::fs::create_dir_all(&cwd).unwrap();
|
std::fs::create_dir_all(&cwd).unwrap();
|
||||||
let mut manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
let manifest = minimal_manifest_for_context_test(&workspace_root, &cwd);
|
||||||
manifest.memory = Some(manifest::MemoryConfig::default());
|
|
||||||
let loader = PromptCatalogSource::builtins_only();
|
let loader = PromptCatalogSource::builtins_only();
|
||||||
let workspace_id = WorkspaceId::new("ws-api-only").unwrap();
|
let workspace_id = WorkspaceId::new("ws-api-only").unwrap();
|
||||||
let common = prepare_worker_common_with_context(
|
let common = prepare_worker_common_with_context(
|
||||||
@@ -6535,7 +6533,7 @@ permission = "write"
|
|||||||
|
|
||||||
let restored = restore_manifest_from_worker_metadata_snapshot(
|
let restored = restore_manifest_from_worker_metadata_snapshot(
|
||||||
"restore-scope",
|
"restore-scope",
|
||||||
Some(serde_json::to_value(&saved).unwrap()),
|
Some(manifest::write_persisted_worker_manifest_snapshot(&saved).unwrap()),
|
||||||
current,
|
current,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -6590,24 +6588,26 @@ permission = "read"
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
manifest.memory = Some(manifest::MemoryConfig::default());
|
manifest.feature.memory.profile.enabled = true;
|
||||||
manifest.memory.as_mut().unwrap().bind_workspace_settings(
|
manifest
|
||||||
&manifest::WorkspaceMemorySettingsSnapshot {
|
.feature
|
||||||
|
.memory
|
||||||
|
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
|
||||||
workspace_id: "workspace-a".to_string(),
|
workspace_id: "workspace-a".to_string(),
|
||||||
settings_revision: 7,
|
settings_revision: 7,
|
||||||
language: "Japanese".to_string(),
|
language: "Japanese".to_string(),
|
||||||
},
|
})
|
||||||
);
|
.unwrap();
|
||||||
|
|
||||||
let metadata = worker_metadata_for_manifest(&manifest, None, None, None);
|
let metadata = worker_metadata_for_manifest(&manifest, None, None, None);
|
||||||
let restored: WorkerManifest = serde_json::from_value(
|
let restored = manifest::read_persisted_worker_manifest_snapshot(
|
||||||
metadata
|
metadata
|
||||||
.resolved_manifest_snapshot
|
.resolved_manifest_snapshot
|
||||||
.expect("Memory settings require a resolved manifest snapshot"),
|
.expect("Memory settings require a resolved manifest snapshot"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
restored.memory.unwrap().workspace_settings(),
|
restored.feature.memory.workspace_settings(),
|
||||||
Some(manifest::WorkspaceMemorySettingsSnapshot {
|
Some(manifest::WorkspaceMemorySettingsSnapshot {
|
||||||
workspace_id: "workspace-a".to_string(),
|
workspace_id: "workspace-a".to_string(),
|
||||||
settings_revision: 7,
|
settings_revision: 7,
|
||||||
@@ -6638,7 +6638,7 @@ permission = "read"
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut missing = manifest.clone();
|
let mut missing = manifest.clone();
|
||||||
missing.memory.as_mut().unwrap().settings_revision = None;
|
missing.feature.memory.workspace_settings = None;
|
||||||
assert!(
|
assert!(
|
||||||
validate_workspace_memory_snapshot(
|
validate_workspace_memory_snapshot(
|
||||||
"memory-snapshot",
|
"memory-snapshot",
|
||||||
@@ -6715,7 +6715,7 @@ permission = "read"
|
|||||||
let snapshot = metadata
|
let snapshot = metadata
|
||||||
.resolved_manifest_snapshot
|
.resolved_manifest_snapshot
|
||||||
.expect("plugin-resolved manifest should be snapshotted");
|
.expect("plugin-resolved manifest should be snapshotted");
|
||||||
let restored: WorkerManifest = serde_json::from_value(snapshot).unwrap();
|
let restored = manifest::read_persisted_worker_manifest_snapshot(snapshot).unwrap();
|
||||||
|
|
||||||
assert!(restored.profile.is_none());
|
assert!(restored.profile.is_none());
|
||||||
assert_eq!(restored.plugins.resolved.len(), 1);
|
assert_eq!(restored.plugins.resolved.len(), 1);
|
||||||
@@ -8203,13 +8203,16 @@ mod build_summary_prompt_tests {
|
|||||||
},
|
},
|
||||||
profile: None,
|
profile: None,
|
||||||
});
|
});
|
||||||
let mut memory = manifest::MemoryConfig::default();
|
let mut memory = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
memory.bind_workspace_settings(&manifest::WorkspaceMemorySettingsSnapshot {
|
memory.profile.enabled = true;
|
||||||
workspace_id: "workspace-test".to_string(),
|
memory
|
||||||
settings_revision: 3,
|
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
|
||||||
language: "Japanese".to_string(),
|
workspace_id: "workspace-test".to_string(),
|
||||||
});
|
settings_revision: 3,
|
||||||
manifest.memory = Some(memory);
|
language: "Japanese".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
manifest.feature.memory = memory;
|
||||||
let mut worker = Worker::new(
|
let mut worker = Worker::new(
|
||||||
manifest,
|
manifest,
|
||||||
Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient),
|
Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient),
|
||||||
@@ -8235,7 +8238,7 @@ mod build_summary_prompt_tests {
|
|||||||
|
|
||||||
async fn render_system_prompt_with_summary(
|
async fn render_system_prompt_with_summary(
|
||||||
summary_doc: Option<&str>,
|
summary_doc: Option<&str>,
|
||||||
memory_config: Option<manifest::MemoryConfig>,
|
memory_config: Option<manifest::ResolvedMemoryFeatureConfig>,
|
||||||
resident_injection: bool,
|
resident_injection: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
render_system_prompt_with_resident_sections(
|
render_system_prompt_with_resident_sections(
|
||||||
@@ -8249,7 +8252,7 @@ mod build_summary_prompt_tests {
|
|||||||
|
|
||||||
async fn render_system_prompt_with_resident_sections(
|
async fn render_system_prompt_with_resident_sections(
|
||||||
summary_doc: Option<&str>,
|
summary_doc: Option<&str>,
|
||||||
memory_config: Option<manifest::MemoryConfig>,
|
memory_config: Option<manifest::ResolvedMemoryFeatureConfig>,
|
||||||
gates: ResidentInjectionGates,
|
gates: ResidentInjectionGates,
|
||||||
_unused: bool,
|
_unused: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
@@ -8258,12 +8261,15 @@ mod build_summary_prompt_tests {
|
|||||||
let cwd = dir.path().join("workspace");
|
let cwd = dir.path().join("workspace");
|
||||||
std::fs::create_dir_all(&cwd).unwrap();
|
std::fs::create_dir_all(&cwd).unwrap();
|
||||||
let mut manifest = minimal_manifest();
|
let mut manifest = minimal_manifest();
|
||||||
manifest.memory = memory_config.clone();
|
manifest.feature.memory = memory_config.clone().unwrap_or_default();
|
||||||
|
if memory_config.is_some() {
|
||||||
|
manifest.feature.memory.profile.enabled = true;
|
||||||
|
}
|
||||||
let scope = Scope::writable(&cwd).unwrap();
|
let scope = Scope::writable(&cwd).unwrap();
|
||||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||||
let workspace_context = if memory_config
|
let workspace_context = if memory_config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|cfg| cfg.inject_summary.unwrap_or(true))
|
.is_some_and(|cfg| cfg.profile.resident.inject_summary)
|
||||||
&& gates.summary
|
&& gates.summary
|
||||||
{
|
{
|
||||||
stub_memory_backend_context(summary_doc.and_then(summary_content_for_backend))
|
stub_memory_backend_context(summary_doc.and_then(summary_content_for_backend))
|
||||||
@@ -8350,7 +8356,7 @@ mod build_summary_prompt_tests {
|
|||||||
async fn resident_summary_body_is_injected_without_frontmatter() {
|
async fn resident_summary_body_is_injected_without_frontmatter() {
|
||||||
let rendered = render_system_prompt_with_summary(
|
let rendered = render_system_prompt_with_summary(
|
||||||
Some(&summary_doc("summary body for resident prompt\n")),
|
Some(&summary_doc("summary body for resident prompt\n")),
|
||||||
Some(manifest::MemoryConfig::default()),
|
Some(manifest::ResolvedMemoryFeatureConfig::default()),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -8362,10 +8368,8 @@ mod build_summary_prompt_tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn resident_summary_injection_can_be_disabled_by_manifest() {
|
async fn resident_summary_injection_can_be_disabled_by_manifest() {
|
||||||
let memory = manifest::MemoryConfig {
|
let mut memory = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
inject_summary: Some(false),
|
memory.profile.resident.inject_summary = false;
|
||||||
..manifest::MemoryConfig::default()
|
|
||||||
};
|
|
||||||
let rendered = render_system_prompt_with_summary(
|
let rendered = render_system_prompt_with_summary(
|
||||||
Some(&summary_doc("disabled summary body\n")),
|
Some(&summary_doc("disabled summary body\n")),
|
||||||
Some(memory),
|
Some(memory),
|
||||||
@@ -8377,7 +8381,7 @@ mod build_summary_prompt_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn resident_summary_is_absent_without_memory_config() {
|
async fn resident_summary_is_absent_when_memory_feature_is_disabled() {
|
||||||
let rendered = render_system_prompt_with_summary(
|
let rendered = render_system_prompt_with_summary(
|
||||||
Some(&summary_doc("memory-disabled summary body\n")),
|
Some(&summary_doc("memory-disabled summary body\n")),
|
||||||
None,
|
None,
|
||||||
@@ -8392,7 +8396,7 @@ mod build_summary_prompt_tests {
|
|||||||
async fn malformed_resident_summary_does_not_fail_render() {
|
async fn malformed_resident_summary_does_not_fail_render() {
|
||||||
let rendered = render_system_prompt_with_summary(
|
let rendered = render_system_prompt_with_summary(
|
||||||
Some("---\nthis is not yaml: : :\n---\nbad summary body\n"),
|
Some("---\nthis is not yaml: : :\n---\nbad summary body\n"),
|
||||||
Some(manifest::MemoryConfig::default()),
|
Some(manifest::ResolvedMemoryFeatureConfig::default()),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -8405,7 +8409,7 @@ mod build_summary_prompt_tests {
|
|||||||
async fn resident_summary_gate_false_omits_only_summary() {
|
async fn resident_summary_gate_false_omits_only_summary() {
|
||||||
let prompt = render_system_prompt_with_resident_sections(
|
let prompt = render_system_prompt_with_resident_sections(
|
||||||
Some(&summary_doc("resident summary marker")),
|
Some(&summary_doc("resident summary marker")),
|
||||||
Some(manifest::MemoryConfig::default()),
|
Some(manifest::ResolvedMemoryFeatureConfig::default()),
|
||||||
ResidentInjectionGates { summary: false },
|
ResidentInjectionGates { summary: false },
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7993,17 +7993,15 @@ fn start_memory_staging_consolidation(
|
|||||||
total_bytes,
|
total_bytes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let reached_files = operation
|
const CONSOLIDATION_THRESHOLD_FILES: usize = 5;
|
||||||
.threshold_files
|
const CONSOLIDATION_THRESHOLD_BYTES: u64 = 50_000;
|
||||||
.is_some_and(|threshold| candidate_count >= threshold);
|
let reached_files = candidate_count >= CONSOLIDATION_THRESHOLD_FILES;
|
||||||
let reached_bytes = operation
|
let reached_bytes = total_bytes >= CONSOLIDATION_THRESHOLD_BYTES;
|
||||||
.threshold_bytes
|
|
||||||
.is_some_and(|threshold| total_bytes >= threshold);
|
|
||||||
if !operation.force && !reached_files && !reached_bytes {
|
if !operation.force && !reached_files && !reached_bytes {
|
||||||
return Ok(MemoryConsolidationOutput {
|
return Ok(MemoryConsolidationOutput {
|
||||||
status: "skipped_below_threshold".to_string(),
|
status: "skipped_below_threshold".to_string(),
|
||||||
summary: format!(
|
summary: format!(
|
||||||
"Memory staging backlog has {candidate_count} candidate(s), {total_bytes} byte(s), below configured threshold."
|
"Memory staging backlog has {candidate_count} candidate(s), {total_bytes} byte(s), below Backend policy threshold."
|
||||||
),
|
),
|
||||||
candidate_count,
|
candidate_count,
|
||||||
total_bytes,
|
total_bytes,
|
||||||
@@ -19921,11 +19919,7 @@ mod tests {
|
|||||||
|
|
||||||
let output = match start_memory_staging_consolidation(
|
let output = match start_memory_staging_consolidation(
|
||||||
api,
|
api,
|
||||||
MemoryConsolidateStagingOperation {
|
MemoryConsolidateStagingOperation { force: true },
|
||||||
force: true,
|
|
||||||
threshold_files: None,
|
|
||||||
threshold_bytes: None,
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
Ok(output) => output,
|
Ok(output) => output,
|
||||||
Err(_) => panic!("unexpected ApiError from memory consolidation trigger"),
|
Err(_) => panic!("unexpected ApiError from memory consolidation trigger"),
|
||||||
@@ -19956,6 +19950,15 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let below_threshold = start_memory_staging_consolidation(
|
||||||
|
api.clone(),
|
||||||
|
MemoryConsolidateStagingOperation { force: false },
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(below_threshold.status, "skipped_below_threshold");
|
||||||
|
assert_eq!(below_threshold.candidate_count, 1);
|
||||||
|
assert!(below_threshold.summary.contains("Backend policy threshold"));
|
||||||
|
|
||||||
let resolved_config_bundle = None;
|
let resolved_config_bundle = None;
|
||||||
let existing = api
|
let existing = api
|
||||||
.runtime
|
.runtime
|
||||||
@@ -20010,11 +20013,7 @@ mod tests {
|
|||||||
|
|
||||||
let second = match start_memory_staging_consolidation(
|
let second = match start_memory_staging_consolidation(
|
||||||
api.clone(),
|
api.clone(),
|
||||||
MemoryConsolidateStagingOperation {
|
MemoryConsolidateStagingOperation { force: true },
|
||||||
force: true,
|
|
||||||
threshold_files: None,
|
|
||||||
threshold_bytes: None,
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
Ok(output) => output,
|
Ok(output) => output,
|
||||||
Err(_) => panic!("unexpected ApiError from second memory consolidation trigger"),
|
Err(_) => panic!("unexpected ApiError from second memory consolidation trigger"),
|
||||||
|
|||||||
+25
-42
@@ -222,55 +222,38 @@ permission = "write"
|
|||||||
# # ref = "anthropic/claude-haiku-4-5"
|
# # ref = "anthropic/claude-haiku-4-5"
|
||||||
|
|
||||||
|
|
||||||
# ===== [memory] =============================================================
|
# ===== [feature.memory] ======================================================
|
||||||
# Memory subsystem の opt-in。
|
# Memory は `feature.memory` だけを入口にする。resolved Worker Manifest では
|
||||||
# - セクションが *ある* … memory tools (MemoryRead/Write/Edit) を登録、
|
# Profile由来の設定を `profile` に、Backend由来のWorkspace設定snapshotを
|
||||||
# `<workspace>/memory/` と `<workspace>/`
|
# `workspace_settings` に分離して保存する。`workspace_settings` はBackendだけが
|
||||||
# の通常 write を Worker 自体に対して deny する。
|
# bindする信頼済み入力で、Profile・Browser・model入力から指定できない。
|
||||||
# - セクションが *無い* … 何も起きない (legacy 動作)。
|
# `profile.enabled = false` の場合、Memory tools、resident injection、extract、
|
||||||
# `[memory]` だけ書いて中身を省略するのも有効 (全フィールド既定値で有効化)。
|
# consolidation requestをすべて無効にし、snapshotも保持しない。
|
||||||
# [memory]
|
|
||||||
#
|
#
|
||||||
# # 任意。デフォルト: Worker の pwd (構築時)。
|
# [feature.memory.profile]
|
||||||
# # 必ず絶対パス (相対なら manifest base 起点で resolve)。
|
# enabled = true
|
||||||
# workspace_root = "/abs/path/to/workspace"
|
# staging_tools = false
|
||||||
#
|
#
|
||||||
# # 任意。デフォルト: tool 側既定 = 20。
|
# [feature.memory.profile.resident]
|
||||||
# # MemoryQuery / MemoryQuery が 1 回に返す最大件数。
|
# inject_summary = true
|
||||||
# query_result_limit = 20
|
|
||||||
#
|
#
|
||||||
# # 任意。デフォルト: tool 側既定 = 3。
|
# [feature.memory.profile.extraction]
|
||||||
# # 各マッチ前後に表示するコンテキスト行数。`query` 省略時は無視。
|
# enabled = true
|
||||||
# query_excerpt_lines = 3
|
# threshold = 30000
|
||||||
|
# worker_max_turns = 8
|
||||||
#
|
#
|
||||||
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
|
# # 任意。省略時はmain modelをcloneする。
|
||||||
# # extract ワーカーのモデル ([model] と同じ形式)。
|
# # [feature.memory.profile.extraction.model]
|
||||||
# # Haiku / 4o-mini / Flash クラスの軽量 reasoning モデル推奨。
|
|
||||||
# # [memory.extract_model]
|
|
||||||
# # ref = "anthropic/claude-haiku-4-5"
|
# # ref = "anthropic/claude-haiku-4-5"
|
||||||
#
|
#
|
||||||
# # 任意。デフォルト: なし (extract 自動発火を完全停止)。
|
# # Backendがresolved Manifestへbindする。手書き/Profile入力では指定しない。
|
||||||
# # 前回 extract pointer 以降の累積入力 token がこの値を超えると extract 起動。
|
# # [feature.memory.workspace_settings]
|
||||||
# # ※ memory tools と resident injection は extract_threshold が None でも動く。
|
# # workspace_id = "workspace-id"
|
||||||
# extract_threshold = 30000
|
# # settings_revision = 1
|
||||||
|
# # language = "日本語"
|
||||||
#
|
#
|
||||||
# # 任意。デフォルト: 8 (`defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS`)。
|
# Query結果/抜粋の上限とconsolidation eligibility/thresholdはoperation/Backend
|
||||||
# # extract worker 自身の tool loop 上限。Rust config で None の場合のみ無制限。
|
# policyが所有し、通常Worker Manifestには含めない。legacy `[memory]` は拒否する。
|
||||||
# extract_worker_max_turns = 8
|
|
||||||
#
|
|
||||||
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
|
|
||||||
# # consolidation ワーカーのモデル。reasoning クラス推奨。
|
|
||||||
# # [memory.consolidation_model]
|
|
||||||
# # ref = "anthropic/claude-sonnet-4-6"
|
|
||||||
#
|
|
||||||
# # 任意。デフォルト: なし。
|
|
||||||
# # `_staging/` のエントリ数がこの値以上で consolidation 発火 (files / bytes は OR)。
|
|
||||||
# consolidation_threshold_files = 50
|
|
||||||
#
|
|
||||||
# # 任意。デフォルト: なし。
|
|
||||||
# # `_staging/` の総バイト数がこの値以上で consolidation 発火 (files / bytes は OR)。
|
|
||||||
# # files / bytes の両方が None だと consolidation 完全無効。
|
|
||||||
# consolidation_threshold_bytes = 1048576
|
|
||||||
|
|
||||||
|
|
||||||
# ===== [skills] =============================================================
|
# ===== [skills] =============================================================
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ compaction = {
|
|||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = true; };
|
task = { enabled = true; };
|
||||||
memory = { enabled = true; };
|
memory = {
|
||||||
|
enabled = true;
|
||||||
|
resident = { inject_summary = true; };
|
||||||
|
extraction = {
|
||||||
|
enabled = true;
|
||||||
|
threshold = 50000;
|
||||||
|
};
|
||||||
|
};
|
||||||
web = { enabled = true; };
|
web = { enabled = true; };
|
||||||
image = { enabled = true; };
|
image = { enabled = true; };
|
||||||
sub_worker = { enabled = false; };
|
sub_worker = { enabled = false; };
|
||||||
@@ -40,12 +47,6 @@ feature = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
memory = {
|
|
||||||
extract_threshold = 50000;
|
|
||||||
consolidation_threshold_files = 5;
|
|
||||||
consolidation_threshold_bytes = 50000;
|
|
||||||
};
|
|
||||||
|
|
||||||
web = {
|
web = {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
search = {
|
search = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import "./base.dcdl" // {
|
|||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = true; };
|
task = { enabled = true; };
|
||||||
memory = { enabled = false; staging = false; };
|
memory = { enabled = false; staging_tools = false; };
|
||||||
web = { enabled = true; };
|
web = { enabled = true; };
|
||||||
image = { enabled = true; };
|
image = { enabled = true; };
|
||||||
sub_worker = { enabled = true; };
|
sub_worker = { enabled = true; };
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "./base.dcdl" // {
|
|||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = false; };
|
task = { enabled = false; };
|
||||||
memory = { enabled = true; staging = true; };
|
memory = { enabled = true; staging_tools = true; };
|
||||||
web = { enabled = false; };
|
web = { enabled = false; };
|
||||||
sub_worker = { enabled = false; };
|
sub_worker = { enabled = false; };
|
||||||
worker = { enabled = false; };
|
worker = { enabled = false; };
|
||||||
|
|||||||
Reference in New Issue
Block a user