refactor: install Memory prompt contributions through Feature

This commit is contained in:
2026-09-04 23:40:12 +09:00
parent 1e674d70c2
commit 12646b6ca0
6 changed files with 356 additions and 396 deletions
+12
View File
@@ -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<Self, toml::de::Error> {
config::reject_removed_manifest_fields(s)?;
+38 -189
View File
@@ -901,43 +901,6 @@ pub(crate) fn wire_event_bridges_on_engine<C, St>(
// per-item commit channel is wired at the top of this function.
}
fn add_memory_tools_if_configured<M>(
registry: &mut FeatureRegistryBuilder,
config: &manifest::ResolvedMemoryFeatureConfig,
build: impl FnOnce() -> std::io::Result<M>,
) -> std::io::Result<bool>
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<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)?);
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::<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 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::<TestMemoryLifecycleModule>(
&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::<TestMemoryLifecycleModule>(
&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::<TestMemoryLifecycleModule>(
&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 {
+210
View File
@@ -342,6 +342,113 @@ fn query_schema() -> serde_json::Value {
})
}
pub struct MemoryFeatureInstallPlan {
pub module: MemoryToolsFeature,
pub resident_summary: Option<String>,
pub system_prompt_override: Option<String>,
pub(crate) resolved_config: manifest::ResolvedMemoryFeatureConfig,
}
impl MemoryFeatureInstallPlan {
pub async fn prepare(
manifest: &manifest::WorkerManifest,
client: Arc<dyn WorkspaceClient>,
prompts: Arc<crate::prompt::catalog::PromptCatalog>,
) -> std::io::Result<Option<Self>> {
Self::prepare_resolved(
manifest.feature.memory.clone(),
client,
prompts,
manifest.profile.clone(),
)
.await
}
async fn prepare_resolved(
config: manifest::ResolvedMemoryFeatureConfig,
client: Arc<dyn WorkspaceClient>,
prompts: Arc<crate::prompt::catalog::PromptCatalog>,
profile: Option<manifest::ProfileManifestSnapshot>,
) -> std::io::Result<Option<Self>> {
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<ToolDefinition>,
@@ -392,6 +499,39 @@ mod tests {
))
}
fn resident_client(content: &str) -> Arc<dyn WorkspaceClient> {
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<ToolDefinition>) -> Vec<String> {
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);
@@ -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<dyn WorkspaceClient>,
manifest: WorkerManifest,
client: Box<dyn LlmClient>,
prompts: Arc<ArcSwap<PromptCatalog>>,
workspace_context: WorkerWorkspaceContext,
event_tx: Option<broadcast::Sender<Event>>,
) -> std::io::Result<Option<Self>> {
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");
+50 -203
View File
@@ -1228,10 +1228,12 @@ pub struct Worker<C: LlmClient, St: Store> {
/// [`Self::from_manifest`], or defaults to the builtin pack when a
/// Worker is constructed through lower-level paths that have no loader.
prompts: Arc<ArcSwap<PromptCatalog>>,
/// 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<String>,
/// Complete system prompt replacement installed by an enabled Feature.
feature_system_prompt_override: Option<String>,
/// 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<C: LlmClient + 'static, St: Store> Worker<C, St> {
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<C: LlmClient + 'static, St: Store> Worker<C, St> {
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<String>,
system_prompt_override: Option<String>,
) {
self.feature_resident_summary = resident_summary;
self.feature_system_prompt_override = system_prompt_override;
}
pub fn prompts(&self) -> Arc<ArcSwap<PromptCatalog>> {
Arc::clone(&self.prompts)
}
@@ -1625,25 +1638,6 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
self.worker_observation_provider.clone()
}
async fn resident_summary_from_workspace_authority(
&self,
) -> Result<Option<String>, 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<C: LlmClient + 'static, St: Store> Worker<C, St> {
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<C: LlmClient + 'static, St: Store> Worker<C, St> {
}
}
}
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<String> = 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<C: LlmClient + 'static, St: Store> Worker<C, St> {
}
}
fn memory_language(config: &manifest::ResolvedMemoryFeatureConfig) -> Result<String, WorkerError> {
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<Box<dyn LlmClient>>,
) -> Result<Self, WorkerError> {
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<Self, WorkerError> {
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<String>) -> 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(