refactor: install Memory prompt contributions through Feature
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user