diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index 11c6c2f7..0e892677 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -328,6 +328,12 @@ impl ResolvedMemoryFeatureConfig { if !self.profile.enabled && self.workspace_settings.is_some() { return Err("disabled Memory feature must not carry Workspace settings"); } + if let Some(settings) = &self.workspace_settings + && (settings.settings_revision == 0 + || !is_normalized_workspace_memory_language(&settings.language)) + { + return Err("Memory Workspace settings snapshot metadata is invalid"); + } Ok(()) } } @@ -918,6 +924,12 @@ impl Default for CompactionConfig { } impl WorkerManifest { + pub fn requires_persisted_execution_snapshot(&self) -> bool { + self.profile.is_some() + || self.plugins.has_resolved_plan() + || self.feature.memory.workspace_settings.is_some() + } + /// Parse a manifest from a TOML string. pub fn from_toml(s: &str) -> Result { config::reject_removed_manifest_fields(s)?; diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index fb93f785..4e431d3d 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -901,43 +901,6 @@ pub(crate) fn wire_event_bridges_on_engine( // per-item commit channel is wired at the top of this function. } -fn add_memory_tools_if_configured( - registry: &mut FeatureRegistryBuilder, - config: &manifest::ResolvedMemoryFeatureConfig, - build: impl FnOnce() -> std::io::Result, -) -> std::io::Result -where - M: crate::feature::FeatureModule + 'static, -{ - if !config.profile.enabled { - 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( - registry: &mut FeatureRegistryBuilder, - config: manifest::ResolvedMemoryFeatureConfig, - lifecycle_enabled: bool, - build: impl FnOnce(manifest::ResolvedMemoryFeatureConfig) -> std::io::Result, -) -> std::io::Result -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)?); - Ok(true) -} - /// Register the builtin file-manipulation tools, optional memory tools, /// and the Worker-orchestration tools (SubWorkerSpawn + comm) on the Worker's /// Engine. Returns the WorkdirSession handle used to attach a `WorkerFsView` to @@ -974,7 +937,6 @@ where let local_filesystem = worker.local_working_directory().cloned(); let local_workspace_root = local_filesystem.as_ref().map(|local| local.root.clone()); let task_feature = worker.task_feature(); - let memory_config = feature_config.memory.clone(); let web_config = worker.manifest().web.clone(); let mcp_config = worker.manifest().mcp.clone(); let spawner_name = worker.manifest().worker.name.clone(); @@ -1031,46 +993,41 @@ where let worker_enabled = feature_config.worker.enabled; let sub_worker_enabled = feature_config.sub_worker.enabled; 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( - &mut feature_registry, - memory_config.clone(), - worker.manifest_lifecycle_features_enabled(), - |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 extraction requires Backend Workspace API authority", - )); - } - Ok( - crate::feature::builtin::memory_lifecycle::MemoryLifecycleFeature::new( - config, - worker.committed_session_capture_handle(), - worker.session_extension_handle(), - workspace_client, - spawner_manifest.clone(), - worker.llm_client_handle(), - prompts.clone(), - spawner_workspace_context.clone(), - worker.working_event_sender(), - ), - ) - }, - )?; + let memory_install_plan = crate::feature::builtin::memory::MemoryFeatureInstallPlan::prepare( + worker.manifest(), + worker.workspace_client_handle(), + worker.prompts().load_full(), + ) + .await?; + let memory_prompt_contribution = memory_install_plan.as_ref().map(|plan| { + ( + plan.resident_summary.clone(), + plan.system_prompt_override.clone(), + ) + }); + let memory_lifecycle_config = memory_install_plan + .as_ref() + .map(|plan| plan.resolved_config.clone()); + if let Some(plan) = memory_install_plan { + feature_registry.add_module(plan.module); + } + if let Some(memory_config) = memory_lifecycle_config + && let Some(memory_lifecycle) = + crate::feature::builtin::memory_lifecycle::MemoryLifecycleFeature::from_resolved_config( + worker.manifest_lifecycle_features_enabled(), + memory_config, + worker.committed_session_capture_handle(), + worker.session_extension_handle(), + worker.workspace_client_handle(), + spawner_manifest.clone(), + worker.llm_client_handle(), + prompts.clone(), + spawner_workspace_context.clone(), + worker.working_event_sender(), + )? + { + feature_registry.add_module(memory_lifecycle); + } if sub_worker_enabled && !worker_enabled { feature_registry.add_module( crate::feature::builtin::manage_worker::sub_worker_control_feature( @@ -1279,6 +1236,9 @@ where ), )); } + if let Some((resident_summary, system_prompt_override)) = memory_prompt_contribution { + worker.install_system_prompt_contribution(resident_summary, system_prompt_override); + } if let Some(tracker) = tracker { worker.attach_tracker(tracker); } @@ -2158,117 +2118,6 @@ mod tests { use tempfile::TempDir; use tokio::net::UnixListener; - #[test] - fn memory_feature_registration_requires_bound_workspace_memory_config() { - #[derive(Clone)] - struct TestMemoryLifecycleModule; - - impl crate::feature::FeatureModule for TestMemoryLifecycleModule { - fn descriptor(&self) -> crate::feature::FeatureDescriptor { - crate::feature::FeatureDescriptor::builtin( - "test-memory-lifecycle", - "Test Memory Lifecycle", - ) - } - - fn install( - &self, - _context: &mut crate::feature::FeatureInstallContext<'_>, - ) -> Result<(), crate::feature::FeatureInstallError> { - Ok(()) - } - } - - let mut registry = FeatureRegistryBuilder::new(); - let installed = add_memory_tools_if_configured::( - &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::( - &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 configured = std::cell::Cell::new(false); - let mut memory_config = manifest::ResolvedMemoryFeatureConfig::default(); - memory_config.profile.enabled = true; - memory_config.profile.extraction.enabled = true; - memory_config - .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(&mut registry, memory_config, true, |_| { - configured.set(true); - Ok(TestMemoryLifecycleModule) - }) - .unwrap(); - assert!(installed); - assert!(configured.get()); - - let mut registry = FeatureRegistryBuilder::new(); - let installed = add_memory_lifecycle_if_configured::( - &mut registry, - manifest::ResolvedMemoryFeatureConfig::default(), - true, - |_| panic!("disabled Memory must not construct its lifecycle Feature"), - ) - .unwrap(); - 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::( - &mut registry, - lifecycle_disabled, - false, - |_| panic!("disabled lifecycle must not construct its Feature"), - ) - .unwrap(); - 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::( - &mut registry, - missing_snapshot, - true, - |_| panic!("invalid Workspace Memory config must fail before Feature construction"), - ) - .unwrap_err(); - assert!( - error - .to_string() - .contains("requires trusted Workspace settings") - ); - } - #[test] fn image_attachment_gate_requires_vision_and_supported_openai_scheme() { let openai = manifest::ModelManifest { diff --git a/crates/worker/src/feature/builtin/memory.rs b/crates/worker/src/feature/builtin/memory.rs index 62403d1a..e04054b0 100644 --- a/crates/worker/src/feature/builtin/memory.rs +++ b/crates/worker/src/feature/builtin/memory.rs @@ -342,6 +342,113 @@ fn query_schema() -> serde_json::Value { }) } +pub struct MemoryFeatureInstallPlan { + pub module: MemoryToolsFeature, + pub resident_summary: Option, + pub system_prompt_override: Option, + pub(crate) resolved_config: manifest::ResolvedMemoryFeatureConfig, +} + +impl MemoryFeatureInstallPlan { + pub async fn prepare( + manifest: &manifest::WorkerManifest, + client: Arc, + prompts: Arc, + ) -> std::io::Result> { + Self::prepare_resolved( + manifest.feature.memory.clone(), + client, + prompts, + manifest.profile.clone(), + ) + .await + } + + async fn prepare_resolved( + config: manifest::ResolvedMemoryFeatureConfig, + client: Arc, + prompts: Arc, + profile: Option, + ) -> std::io::Result> { + let memory_consolidation_worker = profile.as_ref().is_some_and(|snapshot| { + matches!( + &snapshot.source, + manifest::ProfileSource::Registry { + source: manifest::ProfileRegistrySource::Builtin, + name, + .. + } if name == "memory-consolidation" + ) + }); + config + .validate_execution() + .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; + if !config.profile.enabled { + return Ok(None); + } + let workspace_id = client.workspace_id().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Memory tools require Backend Workspace API authority", + ) + })?; + let settings = config + .workspace_settings() + .expect("validated enabled Memory config has Workspace settings"); + if settings.workspace_id != workspace_id { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "Memory settings belong to {} instead of {}", + settings.workspace_id, workspace_id + ), + )); + } + + let resident_summary = if config.profile.resident.inject_summary { + match client + .execute_memory_backend_operation( + memory::backend::MemoryBackendOperation::ResidentSummary( + memory::backend::MemoryResidentSummaryOperation::default(), + ), + ) + .await + { + Ok(memory::backend::MemoryBackendOperationResult::ToolOutput(output)) => { + output.content + } + Ok(other) => { + tracing::debug!(?other, "unexpected resident Memory Backend result"); + None + } + Err(error) => { + tracing::debug!(%error, "resident Memory summary unavailable"); + None + } + } + } else { + None + }; + let system_prompt_override = if memory_consolidation_worker { + let language = settings.language; + Some( + prompts + .memory_consolidation_system(&language) + .map_err(|error| std::io::Error::other(error.to_string()))?, + ) + } else { + None + }; + + Ok(Some(Self { + module: MemoryToolsFeature::new(client, config.profile.staging_tools), + resident_summary, + system_prompt_override, + resolved_config: config, + })) + } +} + #[derive(Clone)] pub struct MemoryToolsFeature { tools: Vec, @@ -392,6 +499,39 @@ mod tests { )) } + fn resident_client(content: &str) -> Arc { + use std::io::{Read, Write}; + use std::net::TcpListener; + + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let content = content.to_string(); + std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut request = [0_u8; 1024]; + let _ = stream.read(&mut request).unwrap(); + let body = serde_json::json!({ + "status": "ok", + "result": { + "kind": "tool_output", + "summary": "resident Memory summary collected", + "content": content, + } + }) + .to_string(); + let response = format!( + "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", + body.len(), + body + ); + stream.write_all(response.as_bytes()).unwrap(); + }); + Arc::new(crate::worker::TestWorkspaceHttpClient::new( + "workspace", + format!("http://{addr}"), + )) + } + fn tool_names(definitions: Vec) -> Vec { let mut names = definitions .into_iter() @@ -410,6 +550,76 @@ mod tests { .input_schema } + #[tokio::test] + async fn memory_install_plan_is_the_fail_closed_config_boundary() { + let prompts = crate::prompt::catalog::PromptCatalog::builtins_only().unwrap(); + let disabled = MemoryFeatureInstallPlan::prepare_resolved( + manifest::ResolvedMemoryFeatureConfig::default(), + test_client(), + prompts.clone(), + None, + ) + .await + .unwrap(); + assert!(disabled.is_none()); + + let mut enabled = manifest::ResolvedMemoryFeatureConfig::default(); + enabled.profile.enabled = true; + enabled.profile.resident.inject_summary = false; + assert!( + MemoryFeatureInstallPlan::prepare_resolved( + enabled.clone(), + test_client(), + prompts.clone(), + None, + ) + .await + .is_err() + ); + enabled + .bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot { + workspace_id: "workspace".to_string(), + settings_revision: 1, + language: "English".to_string(), + }) + .unwrap(); + let mut foreign = enabled.clone(); + foreign.workspace_settings.as_mut().unwrap().workspace_id = "other-workspace".to_string(); + assert!( + MemoryFeatureInstallPlan::prepare_resolved( + foreign, + test_client(), + prompts.clone(), + None, + ) + .await + .is_err() + ); + let plan = MemoryFeatureInstallPlan::prepare_resolved( + enabled.clone(), + test_client(), + prompts.clone(), + None, + ) + .await + .unwrap() + .unwrap(); + assert!(plan.resident_summary.is_none()); + assert!(plan.system_prompt_override.is_none()); + + enabled.profile.resident.inject_summary = true; + let plan = MemoryFeatureInstallPlan::prepare_resolved( + enabled, + resident_client("# Durable Memory"), + prompts, + None, + ) + .await + .unwrap() + .unwrap(); + assert_eq!(plan.resident_summary.as_deref(), Some("# Durable Memory")); + } + #[test] fn memory_feature_owns_normal_and_staging_tool_surfaces() { let normal = MemoryToolsFeature::new(test_client(), false); diff --git a/crates/worker/src/feature/builtin/memory_lifecycle.rs b/crates/worker/src/feature/builtin/memory_lifecycle.rs index 9c601263..f2d0caab 100644 --- a/crates/worker/src/feature/builtin/memory_lifecycle.rs +++ b/crates/worker/src/feature/builtin/memory_lifecycle.rs @@ -64,6 +64,44 @@ struct MemoryLifecycleTask { } impl MemoryLifecycleFeature { + #[allow(clippy::too_many_arguments)] + pub(crate) fn from_resolved_config( + lifecycle_enabled: bool, + config: manifest::ResolvedMemoryFeatureConfig, + capture: CommittedSessionCaptureHandle, + extensions: SessionExtensionHandle, + workspace_client: Arc, + manifest: WorkerManifest, + client: Box, + prompts: Arc>, + workspace_context: WorkerWorkspaceContext, + event_tx: Option>, + ) -> std::io::Result> { + if !lifecycle_enabled || !config.profile.enabled || !config.profile.extraction.enabled { + return Ok(None); + } + config + .validate_execution() + .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; + if !workspace_client.is_available() || workspace_client.workspace_id().is_none() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Memory extraction requires Backend Workspace API authority", + )); + } + Ok(Some(Self::new( + config, + capture, + extensions, + workspace_client, + manifest, + client, + prompts, + workspace_context, + event_tx, + ))) + } + #[allow(clippy::too_many_arguments)] pub(crate) fn new( config: manifest::ResolvedMemoryFeatureConfig, @@ -1452,8 +1490,12 @@ permission = "write" ); } let controller_source = include_str!("../../controller.rs"); - assert!(controller_source.contains("add_memory_lifecycle_if_configured")); - assert!(controller_source.contains("MemoryLifecycleFeature::new")); + assert!(controller_source.contains("MemoryFeatureInstallPlan::prepare")); + assert!(controller_source.contains("MemoryLifecycleFeature::from_resolved_config")); + let worker_production = worker_source.split("#[cfg(test)]").next().unwrap(); + let controller_production = controller_source.split("#[cfg(test)]").next().unwrap(); + assert!(!worker_production.contains(".feature.memory")); + assert!(!controller_production.contains(".feature.memory")); let lifecycle_source = include_str!("memory_lifecycle.rs"); assert!(lifecycle_source.contains("request_memory_staging_consolidation")); let internal_worker_source = include_str!("../../internal_worker.rs"); diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 76888eff..d4a3b85c 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -1228,10 +1228,12 @@ pub struct Worker { /// [`Self::from_manifest`], or defaults to the builtin pack when a /// Worker is constructed through lower-level paths that have no loader. prompts: Arc>, - /// When true (default), the system-prompt assembler may append resident - /// context from the workspace Memory document. Internal disposable - /// workers disable this so resident memory exposure is opt-in per Worker. + /// Test/internal policy gate for installed resident prompt contributions. inject_resident_summary: bool, + /// Materialized resident prompt context installed by an enabled Feature. + feature_resident_summary: Option, + /// Complete system prompt replacement installed by an enabled Feature. + feature_system_prompt_override: Option, /// Typed user submissions in submit order. K-th entry corresponds to /// the K-th `Item::user_message` in `worker.history()` (modulo seed /// history loaded via `AnnotatedSegmentStart.history`, whose original segments @@ -1450,6 +1452,8 @@ impl Worker { runtime_ticket_role: None, prompts, inject_resident_summary: true, + feature_resident_summary: None, + feature_system_prompt_override: None, user_segments: Vec::new(), sink: SegmentLogSink::new(), history_persistence_wired: false, @@ -1487,11 +1491,20 @@ impl Worker { self.inject_resident_summary = enabled; } - /// Toggle workspace Memory document resident injection in the system prompt. + /// Internal/test gate for installed resident prompt contributions. pub fn set_resident_summary_injection(&mut self, enabled: bool) { self.inject_resident_summary = enabled; } + pub(crate) fn install_system_prompt_contribution( + &mut self, + resident_summary: Option, + system_prompt_override: Option, + ) { + self.feature_resident_summary = resident_summary; + self.feature_system_prompt_override = system_prompt_override; + } + pub fn prompts(&self) -> Arc> { Arc::clone(&self.prompts) } @@ -1625,25 +1638,6 @@ impl Worker { self.worker_observation_provider.clone() } - async fn resident_summary_from_workspace_authority( - &self, - ) -> Result, WorkerError> { - let result = self - .workspace_client() - .execute_memory_backend_operation( - memory::backend::MemoryBackendOperation::ResidentSummary( - memory::backend::MemoryResidentSummaryOperation::default(), - ), - ) - .await?; - match result { - memory::backend::MemoryBackendOperationResult::ToolOutput(output) => Ok(output.content), - other => Err(WorkerError::FeatureInstall(format!( - "unexpected memory backend result for resident summary: {other:?}" - ))), - } - } - /// Activate an Agent Skill through the Workspace backend/client and commit /// the returned SKILL.md body to history before it can influence an LLM run. /// @@ -2467,26 +2461,7 @@ impl Worker { let Some(template) = self.system_prompt_template.take() else { return Ok(()); }; - let is_memory_consolidation = self.manifest.profile.as_ref().is_some_and(|snapshot| { - matches!( - &snapshot.source, - manifest::ProfileSource::Registry { - source: manifest::ProfileRegistrySource::Builtin, - name, - .. - } if name == "memory-consolidation" - ) - }); - if is_memory_consolidation { - let memory_config = &self.manifest.feature.memory; - memory_config - .validate_execution() - .map_err(|message| WorkerError::InvalidState(message.to_string()))?; - let language = memory_language(memory_config)?; - let rendered = self - .prompts - .load_full() - .memory_consolidation_system(&language)?; + if let Some(rendered) = self.feature_system_prompt_override.take() { self.engine .as_mut() .expect("worker present") @@ -2515,20 +2490,10 @@ impl Worker { } } } - let inject_summary = self.inject_resident_summary - && self.manifest.feature.memory.profile.enabled - && self.manifest.feature.memory.profile.resident.inject_summary; - let resident_summary: Option = if inject_summary { - match self.resident_summary_from_workspace_authority().await { - Ok(summary) => summary, - Err(error) => { - tracing::debug!(%error, "resident memory summary unavailable"); - None - } - } - } else { - None - }; + let resident_summary = self + .inject_resident_summary + .then(|| self.feature_resident_summary.clone()) + .flatten(); let worker_language = worker_language(&self.manifest.engine); let scope_snapshot = self.scope.snapshot(); let cwd_for_prompt = self @@ -4548,17 +4513,6 @@ impl Worker { } } -fn memory_language(config: &manifest::ResolvedMemoryFeatureConfig) -> Result { - config - .workspace_settings() - .map(|snapshot| snapshot.language) - .ok_or_else(|| { - WorkerError::InvalidState( - "Memory is enabled without a bound Workspace Memory settings snapshot".to_string(), - ) - }) -} - fn worker_language(cfg: &manifest::EngineManifest) -> &str { let language = cfg.language.trim(); if language.is_empty() { @@ -4623,7 +4577,6 @@ where filesystem_authority: WorkerFilesystemAuthority, model_client: Option>, ) -> Result { - validate_workspace_memory_snapshot(&manifest.worker.name, &manifest, &workspace_context)?; let common = prepare_worker_common_with_context_and_model_client( &manifest, &loader, @@ -4708,6 +4661,8 @@ where runtime_ticket_role: None, prompts: common.prompts, inject_resident_summary: true, + feature_resident_summary: None, + feature_system_prompt_override: None, user_segments: Vec::new(), sink: SegmentLogSink::new(), history_persistence_wired: false, @@ -4790,6 +4745,8 @@ where runtime_ticket_role: None, prompts: common.prompts, inject_resident_summary: true, + feature_resident_summary: None, + feature_system_prompt_override: None, user_segments: Vec::new(), sink: SegmentLogSink::new(), history_persistence_wired: false, @@ -4836,7 +4793,6 @@ where workspace_context: WorkerWorkspaceContext, filesystem_authority: WorkerFilesystemAuthority, ) -> Result { - validate_workspace_memory_snapshot(&manifest.worker.name, &manifest, &workspace_context)?; let common = prepare_worker_common_with_context( &manifest, &loader, @@ -4907,6 +4863,8 @@ where runtime_ticket_role: None, prompts: common.prompts, inject_resident_summary: true, + feature_resident_summary: None, + feature_system_prompt_override: None, user_segments: Vec::new(), sink: SegmentLogSink::new(), history_persistence_wired: false, @@ -5280,6 +5238,8 @@ where runtime_ticket_role: None, prompts: common.prompts, inject_resident_summary: true, + feature_resident_summary: None, + feature_system_prompt_override: None, user_segments: state.user_segments, // Seed the mirror with the entries we just replayed so a // late-attaching client sees the full prefix without an @@ -5428,54 +5388,8 @@ fn worker_metadata_for_manifest( metadata } -fn validate_workspace_memory_snapshot( - worker_name: &str, - manifest: &WorkerManifest, - workspace_context: &WorkerWorkspaceContext, -) -> Result<(), WorkerError> { - let Some(workspace_id) = workspace_context.workspace_id() else { - return Ok(()); - }; - manifest - .feature - .memory - .validate_execution() - .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(|| { - WorkerError::InvalidState(format!( - "Workspace Worker {worker_name} has no complete persisted Memory settings snapshot" - )) - })?; - if snapshot.workspace_id != workspace_id.as_str() { - return Err(WorkerError::InvalidState(format!( - "Workspace Worker {worker_name} Memory settings belong to {} instead of {}", - snapshot.workspace_id, - workspace_id.as_str() - ))); - } - if snapshot.settings_revision == 0 - || !manifest::is_normalized_workspace_memory_language(&snapshot.language) - { - return Err(WorkerError::InvalidState(format!( - "Workspace Worker {worker_name} has corrupt Memory settings snapshot metadata" - ))); - } - Ok(()) -} - fn should_persist_resolved_manifest_snapshot(manifest: &WorkerManifest) -> bool { - manifest.profile.is_some() - || manifest.plugins.has_resolved_plan() - || manifest.feature.memory.workspace_settings.is_some() + manifest.requires_persisted_execution_snapshot() } fn restore_manifest_from_worker_metadata_snapshot( @@ -6565,7 +6479,7 @@ permission = "write" } #[test] - fn workspace_memory_settings_snapshot_is_persisted_and_scope_checked() { + fn workspace_memory_settings_snapshot_is_persisted_through_versioned_adapter() { let mut manifest = WorkerManifest::from_toml( r#" [worker] @@ -6614,42 +6528,6 @@ permission = "read" language: "Japanese".to_string(), }) ); - assert!( - validate_workspace_memory_snapshot( - "memory-snapshot", - &manifest, - &WorkerWorkspaceContext::unavailable( - Some(WorkspaceId::new("workspace-a").unwrap()), - "test", - ) - ) - .is_ok() - ); - assert!( - validate_workspace_memory_snapshot( - "memory-snapshot", - &manifest, - &WorkerWorkspaceContext::unavailable( - Some(WorkspaceId::new("workspace-b").unwrap()), - "test", - ) - ) - .is_err() - ); - - let mut missing = manifest.clone(); - missing.feature.memory.workspace_settings = None; - assert!( - validate_workspace_memory_snapshot( - "memory-snapshot", - &missing, - &WorkerWorkspaceContext::unavailable( - Some(WorkspaceId::new("workspace-a").unwrap()), - "test", - ) - ) - .is_err() - ); } #[test] @@ -8230,6 +8108,12 @@ mod build_summary_prompt_tests { ) .unwrap(), ); + let prompt_override = worker + .prompts() + .load_full() + .memory_consolidation_system("Japanese") + .unwrap(); + worker.install_system_prompt_contribution(None, Some(prompt_override)); worker.ensure_system_prompt_materialized().await.unwrap(); let prompt = worker.engine().get_system_prompt().unwrap(); assert!(prompt.contains("`language`: `Japanese`")); @@ -8267,15 +8151,7 @@ mod build_summary_prompt_tests { } let scope = Scope::writable(&cwd).unwrap(); let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone()); - let workspace_context = if memory_config - .as_ref() - .is_some_and(|cfg| cfg.profile.resident.inject_summary) - && gates.summary - { - stub_memory_backend_context(summary_doc.and_then(summary_content_for_backend)) - } else { - WorkerWorkspaceContext::local_filesystem(None) - }; + let workspace_context = WorkerWorkspaceContext::local_filesystem(None); let mut worker = Worker::new( manifest, Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient), @@ -8287,6 +8163,16 @@ mod build_summary_prompt_tests { .await .unwrap(); worker.set_resident_memory_injection(gates.summary); + let resident_summary = if memory_config + .as_ref() + .is_some_and(|cfg| cfg.profile.resident.inject_summary) + && gates.summary + { + summary_doc.and_then(summary_content_for_backend) + } else { + None + }; + worker.install_system_prompt_contribution(resident_summary, None); let template = SystemPromptTemplate::parse( "default", crate::prompt::source::PromptCatalogSource::builtins_only(), @@ -8313,45 +8199,6 @@ mod build_summary_prompt_tests { Some(doc.to_string()) } - fn stub_memory_backend_context(content: Option) -> WorkerWorkspaceContext { - use std::io::{Read, Write}; - use std::net::TcpListener; - - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = listener.local_addr().unwrap(); - std::thread::spawn(move || { - let (mut stream, _) = listener.accept().unwrap(); - let mut buffer = [0_u8; 1024]; - let _ = stream.read(&mut buffer).unwrap(); - let body = serde_json::json!({ - "status": "ok", - "result": { - "kind": "tool_output", - "summary": if content.is_some() { - "resident memory summary collected" - } else { - "resident memory summary unavailable" - }, - "content": content, - } - }) - .to_string(); - let response = format!( - "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", - body.len(), - body - ); - stream.write_all(response.as_bytes()).unwrap(); - }); - WorkerWorkspaceContext::with_client( - Some(WorkspaceId::new("test-memory").unwrap()), - Arc::new(TestWorkspaceHttpClient::new( - "test-memory", - format!("http://{addr}"), - )), - ) - } - #[tokio::test] async fn resident_summary_body_is_injected_without_frontmatter() { let rendered = render_system_prompt_with_summary( @@ -8367,7 +8214,7 @@ mod build_summary_prompt_tests { } #[tokio::test] - async fn resident_summary_injection_can_be_disabled_by_manifest() { + async fn resident_summary_injection_can_be_disabled_by_memory_feature() { let mut memory = manifest::ResolvedMemoryFeatureConfig::default(); memory.profile.resident.inject_summary = false; let rendered = render_system_prompt_with_summary( diff --git a/docs/manifest.toml b/docs/manifest.toml index 2dc2386e..2becb8bc 100644 --- a/docs/manifest.toml +++ b/docs/manifest.toml @@ -227,8 +227,8 @@ permission = "write" # Profile由来の設定を `profile` に、Backend由来のWorkspace設定snapshotを # `workspace_settings` に分離して保存する。`workspace_settings` はBackendだけが # bindする信頼済み入力で、Profile・Browser・model入力から指定できない。 -# `profile.enabled = false` の場合、Memory tools、resident injection、extract、 -# consolidation requestをすべて無効にし、snapshotも保持しない。 +# `profile.enabled = false` の場合、Memory tools、Feature prompt contributionによる +# resident injection、extract、consolidation requestをすべて無効にし、snapshotも保持しない。 # # [feature.memory.profile] # enabled = true