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() {
|
if !self.profile.enabled && self.workspace_settings.is_some() {
|
||||||
return Err("disabled Memory feature must not carry Workspace settings");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -918,6 +924,12 @@ impl Default for CompactionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WorkerManifest {
|
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.
|
/// Parse a manifest from a TOML string.
|
||||||
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
|
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
|
||||||
config::reject_removed_manifest_fields(s)?;
|
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.
|
// 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,
|
/// Register the builtin file-manipulation tools, optional memory tools,
|
||||||
/// and the Worker-orchestration tools (SubWorkerSpawn + comm) on the Worker's
|
/// and the Worker-orchestration tools (SubWorkerSpawn + comm) on the Worker's
|
||||||
/// Engine. Returns the WorkdirSession handle used to attach a `WorkerFsView` to
|
/// 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_filesystem = worker.local_working_directory().cloned();
|
||||||
let local_workspace_root = local_filesystem.as_ref().map(|local| local.root.clone());
|
let local_workspace_root = local_filesystem.as_ref().map(|local| local.root.clone());
|
||||||
let task_feature = worker.task_feature();
|
let task_feature = worker.task_feature();
|
||||||
let memory_config = feature_config.memory.clone();
|
|
||||||
let web_config = worker.manifest().web.clone();
|
let web_config = worker.manifest().web.clone();
|
||||||
let mcp_config = worker.manifest().mcp.clone();
|
let mcp_config = worker.manifest().mcp.clone();
|
||||||
let spawner_name = worker.manifest().worker.name.clone();
|
let spawner_name = worker.manifest().worker.name.clone();
|
||||||
@@ -1031,46 +993,41 @@ where
|
|||||||
let worker_enabled = feature_config.worker.enabled;
|
let worker_enabled = feature_config.worker.enabled;
|
||||||
let sub_worker_enabled = feature_config.sub_worker.enabled;
|
let sub_worker_enabled = feature_config.sub_worker.enabled;
|
||||||
let mut feature_registry = FeatureRegistryBuilder::new();
|
let mut feature_registry = FeatureRegistryBuilder::new();
|
||||||
add_memory_tools_if_configured(&mut feature_registry, &memory_config, || {
|
let memory_install_plan = crate::feature::builtin::memory::MemoryFeatureInstallPlan::prepare(
|
||||||
let workspace_client = worker.workspace_client_handle();
|
worker.manifest(),
|
||||||
if !workspace_client.is_available() || workspace_client.workspace_id().is_none() {
|
worker.workspace_client_handle(),
|
||||||
return Err(std::io::Error::new(
|
worker.prompts().load_full(),
|
||||||
std::io::ErrorKind::InvalidInput,
|
)
|
||||||
"Memory tools require Backend Workspace API authority",
|
.await?;
|
||||||
));
|
let memory_prompt_contribution = memory_install_plan.as_ref().map(|plan| {
|
||||||
}
|
(
|
||||||
Ok(crate::feature::builtin::memory::MemoryToolsFeature::new(
|
plan.resident_summary.clone(),
|
||||||
workspace_client,
|
plan.system_prompt_override.clone(),
|
||||||
memory_config.profile.staging_tools,
|
)
|
||||||
))
|
});
|
||||||
})?;
|
let memory_lifecycle_config = memory_install_plan
|
||||||
add_memory_lifecycle_if_configured(
|
.as_ref()
|
||||||
&mut feature_registry,
|
.map(|plan| plan.resolved_config.clone());
|
||||||
memory_config.clone(),
|
if let Some(plan) = memory_install_plan {
|
||||||
worker.manifest_lifecycle_features_enabled(),
|
feature_registry.add_module(plan.module);
|
||||||
|config| {
|
}
|
||||||
let workspace_client = worker.workspace_client_handle();
|
if let Some(memory_config) = memory_lifecycle_config
|
||||||
if !workspace_client.is_available() || workspace_client.workspace_id().is_none() {
|
&& let Some(memory_lifecycle) =
|
||||||
return Err(std::io::Error::new(
|
crate::feature::builtin::memory_lifecycle::MemoryLifecycleFeature::from_resolved_config(
|
||||||
std::io::ErrorKind::InvalidInput,
|
worker.manifest_lifecycle_features_enabled(),
|
||||||
"Memory extraction requires Backend Workspace API authority",
|
memory_config,
|
||||||
));
|
worker.committed_session_capture_handle(),
|
||||||
}
|
worker.session_extension_handle(),
|
||||||
Ok(
|
worker.workspace_client_handle(),
|
||||||
crate::feature::builtin::memory_lifecycle::MemoryLifecycleFeature::new(
|
spawner_manifest.clone(),
|
||||||
config,
|
worker.llm_client_handle(),
|
||||||
worker.committed_session_capture_handle(),
|
prompts.clone(),
|
||||||
worker.session_extension_handle(),
|
spawner_workspace_context.clone(),
|
||||||
workspace_client,
|
worker.working_event_sender(),
|
||||||
spawner_manifest.clone(),
|
)?
|
||||||
worker.llm_client_handle(),
|
{
|
||||||
prompts.clone(),
|
feature_registry.add_module(memory_lifecycle);
|
||||||
spawner_workspace_context.clone(),
|
}
|
||||||
worker.working_event_sender(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
if sub_worker_enabled && !worker_enabled {
|
if sub_worker_enabled && !worker_enabled {
|
||||||
feature_registry.add_module(
|
feature_registry.add_module(
|
||||||
crate::feature::builtin::manage_worker::sub_worker_control_feature(
|
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 {
|
if let Some(tracker) = tracker {
|
||||||
worker.attach_tracker(tracker);
|
worker.attach_tracker(tracker);
|
||||||
}
|
}
|
||||||
@@ -2158,117 +2118,6 @@ mod tests {
|
|||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tokio::net::UnixListener;
|
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]
|
#[test]
|
||||||
fn image_attachment_gate_requires_vision_and_supported_openai_scheme() {
|
fn image_attachment_gate_requires_vision_and_supported_openai_scheme() {
|
||||||
let openai = manifest::ModelManifest {
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct MemoryToolsFeature {
|
pub struct MemoryToolsFeature {
|
||||||
tools: Vec<ToolDefinition>,
|
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> {
|
fn tool_names(definitions: Vec<ToolDefinition>) -> Vec<String> {
|
||||||
let mut names = definitions
|
let mut names = definitions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -410,6 +550,76 @@ mod tests {
|
|||||||
.input_schema
|
.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]
|
#[test]
|
||||||
fn memory_feature_owns_normal_and_staging_tool_surfaces() {
|
fn memory_feature_owns_normal_and_staging_tool_surfaces() {
|
||||||
let normal = MemoryToolsFeature::new(test_client(), false);
|
let normal = MemoryToolsFeature::new(test_client(), false);
|
||||||
|
|||||||
@@ -64,6 +64,44 @@ struct MemoryLifecycleTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryLifecycleFeature {
|
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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
config: manifest::ResolvedMemoryFeatureConfig,
|
config: manifest::ResolvedMemoryFeatureConfig,
|
||||||
@@ -1452,8 +1490,12 @@ permission = "write"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let controller_source = include_str!("../../controller.rs");
|
let controller_source = include_str!("../../controller.rs");
|
||||||
assert!(controller_source.contains("add_memory_lifecycle_if_configured"));
|
assert!(controller_source.contains("MemoryFeatureInstallPlan::prepare"));
|
||||||
assert!(controller_source.contains("MemoryLifecycleFeature::new"));
|
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");
|
let lifecycle_source = include_str!("memory_lifecycle.rs");
|
||||||
assert!(lifecycle_source.contains("request_memory_staging_consolidation"));
|
assert!(lifecycle_source.contains("request_memory_staging_consolidation"));
|
||||||
let internal_worker_source = include_str!("../../internal_worker.rs");
|
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
|
/// [`Self::from_manifest`], or defaults to the builtin pack when a
|
||||||
/// Worker is constructed through lower-level paths that have no loader.
|
/// Worker is constructed through lower-level paths that have no loader.
|
||||||
prompts: Arc<ArcSwap<PromptCatalog>>,
|
prompts: Arc<ArcSwap<PromptCatalog>>,
|
||||||
/// When true (default), the system-prompt assembler may append resident
|
/// Test/internal policy gate for installed resident prompt contributions.
|
||||||
/// context from the workspace Memory document. Internal disposable
|
|
||||||
/// workers disable this so resident memory exposure is opt-in per Worker.
|
|
||||||
inject_resident_summary: bool,
|
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
|
/// Typed user submissions in submit order. K-th entry corresponds to
|
||||||
/// the K-th `Item::user_message` in `worker.history()` (modulo seed
|
/// the K-th `Item::user_message` in `worker.history()` (modulo seed
|
||||||
/// history loaded via `AnnotatedSegmentStart.history`, whose original segments
|
/// 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,
|
runtime_ticket_role: None,
|
||||||
prompts,
|
prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
|
feature_resident_summary: None,
|
||||||
|
feature_system_prompt_override: None,
|
||||||
user_segments: Vec::new(),
|
user_segments: Vec::new(),
|
||||||
sink: SegmentLogSink::new(),
|
sink: SegmentLogSink::new(),
|
||||||
history_persistence_wired: false,
|
history_persistence_wired: false,
|
||||||
@@ -1487,11 +1491,20 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
|||||||
self.inject_resident_summary = enabled;
|
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) {
|
pub fn set_resident_summary_injection(&mut self, enabled: bool) {
|
||||||
self.inject_resident_summary = enabled;
|
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>> {
|
pub fn prompts(&self) -> Arc<ArcSwap<PromptCatalog>> {
|
||||||
Arc::clone(&self.prompts)
|
Arc::clone(&self.prompts)
|
||||||
}
|
}
|
||||||
@@ -1625,25 +1638,6 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
|||||||
self.worker_observation_provider.clone()
|
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
|
/// 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.
|
/// 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 {
|
let Some(template) = self.system_prompt_template.take() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let is_memory_consolidation = self.manifest.profile.as_ref().is_some_and(|snapshot| {
|
if let Some(rendered) = self.feature_system_prompt_override.take() {
|
||||||
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)?;
|
|
||||||
self.engine
|
self.engine
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("worker present")
|
.expect("worker present")
|
||||||
@@ -2515,20 +2490,10 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let inject_summary = self.inject_resident_summary
|
let resident_summary = self
|
||||||
&& self.manifest.feature.memory.profile.enabled
|
.inject_resident_summary
|
||||||
&& self.manifest.feature.memory.profile.resident.inject_summary;
|
.then(|| self.feature_resident_summary.clone())
|
||||||
let resident_summary: Option<String> = if inject_summary {
|
.flatten();
|
||||||
match self.resident_summary_from_workspace_authority().await {
|
|
||||||
Ok(summary) => summary,
|
|
||||||
Err(error) => {
|
|
||||||
tracing::debug!(%error, "resident memory summary unavailable");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let worker_language = worker_language(&self.manifest.engine);
|
let worker_language = worker_language(&self.manifest.engine);
|
||||||
let scope_snapshot = self.scope.snapshot();
|
let scope_snapshot = self.scope.snapshot();
|
||||||
let cwd_for_prompt = self
|
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 {
|
fn worker_language(cfg: &manifest::EngineManifest) -> &str {
|
||||||
let language = cfg.language.trim();
|
let language = cfg.language.trim();
|
||||||
if language.is_empty() {
|
if language.is_empty() {
|
||||||
@@ -4623,7 +4577,6 @@ where
|
|||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
model_client: Option<Box<dyn LlmClient>>,
|
model_client: Option<Box<dyn LlmClient>>,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
validate_workspace_memory_snapshot(&manifest.worker.name, &manifest, &workspace_context)?;
|
|
||||||
let common = prepare_worker_common_with_context_and_model_client(
|
let common = prepare_worker_common_with_context_and_model_client(
|
||||||
&manifest,
|
&manifest,
|
||||||
&loader,
|
&loader,
|
||||||
@@ -4708,6 +4661,8 @@ where
|
|||||||
runtime_ticket_role: None,
|
runtime_ticket_role: None,
|
||||||
prompts: common.prompts,
|
prompts: common.prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
|
feature_resident_summary: None,
|
||||||
|
feature_system_prompt_override: None,
|
||||||
user_segments: Vec::new(),
|
user_segments: Vec::new(),
|
||||||
sink: SegmentLogSink::new(),
|
sink: SegmentLogSink::new(),
|
||||||
history_persistence_wired: false,
|
history_persistence_wired: false,
|
||||||
@@ -4790,6 +4745,8 @@ where
|
|||||||
runtime_ticket_role: None,
|
runtime_ticket_role: None,
|
||||||
prompts: common.prompts,
|
prompts: common.prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
|
feature_resident_summary: None,
|
||||||
|
feature_system_prompt_override: None,
|
||||||
user_segments: Vec::new(),
|
user_segments: Vec::new(),
|
||||||
sink: SegmentLogSink::new(),
|
sink: SegmentLogSink::new(),
|
||||||
history_persistence_wired: false,
|
history_persistence_wired: false,
|
||||||
@@ -4836,7 +4793,6 @@ where
|
|||||||
workspace_context: WorkerWorkspaceContext,
|
workspace_context: WorkerWorkspaceContext,
|
||||||
filesystem_authority: WorkerFilesystemAuthority,
|
filesystem_authority: WorkerFilesystemAuthority,
|
||||||
) -> Result<Self, WorkerError> {
|
) -> Result<Self, WorkerError> {
|
||||||
validate_workspace_memory_snapshot(&manifest.worker.name, &manifest, &workspace_context)?;
|
|
||||||
let common = prepare_worker_common_with_context(
|
let common = prepare_worker_common_with_context(
|
||||||
&manifest,
|
&manifest,
|
||||||
&loader,
|
&loader,
|
||||||
@@ -4907,6 +4863,8 @@ where
|
|||||||
runtime_ticket_role: None,
|
runtime_ticket_role: None,
|
||||||
prompts: common.prompts,
|
prompts: common.prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
|
feature_resident_summary: None,
|
||||||
|
feature_system_prompt_override: None,
|
||||||
user_segments: Vec::new(),
|
user_segments: Vec::new(),
|
||||||
sink: SegmentLogSink::new(),
|
sink: SegmentLogSink::new(),
|
||||||
history_persistence_wired: false,
|
history_persistence_wired: false,
|
||||||
@@ -5280,6 +5238,8 @@ where
|
|||||||
runtime_ticket_role: None,
|
runtime_ticket_role: None,
|
||||||
prompts: common.prompts,
|
prompts: common.prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
|
feature_resident_summary: None,
|
||||||
|
feature_system_prompt_override: None,
|
||||||
user_segments: state.user_segments,
|
user_segments: state.user_segments,
|
||||||
// Seed the mirror with the entries we just replayed so a
|
// Seed the mirror with the entries we just replayed so a
|
||||||
// late-attaching client sees the full prefix without an
|
// late-attaching client sees the full prefix without an
|
||||||
@@ -5428,54 +5388,8 @@ fn worker_metadata_for_manifest(
|
|||||||
metadata
|
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 {
|
fn should_persist_resolved_manifest_snapshot(manifest: &WorkerManifest) -> bool {
|
||||||
manifest.profile.is_some()
|
manifest.requires_persisted_execution_snapshot()
|
||||||
|| manifest.plugins.has_resolved_plan()
|
|
||||||
|| manifest.feature.memory.workspace_settings.is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_manifest_from_worker_metadata_snapshot(
|
fn restore_manifest_from_worker_metadata_snapshot(
|
||||||
@@ -6565,7 +6479,7 @@ permission = "write"
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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(
|
let mut manifest = WorkerManifest::from_toml(
|
||||||
r#"
|
r#"
|
||||||
[worker]
|
[worker]
|
||||||
@@ -6614,42 +6528,6 @@ permission = "read"
|
|||||||
language: "Japanese".to_string(),
|
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]
|
#[test]
|
||||||
@@ -8230,6 +8108,12 @@ mod build_summary_prompt_tests {
|
|||||||
)
|
)
|
||||||
.unwrap(),
|
.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();
|
worker.ensure_system_prompt_materialized().await.unwrap();
|
||||||
let prompt = worker.engine().get_system_prompt().unwrap();
|
let prompt = worker.engine().get_system_prompt().unwrap();
|
||||||
assert!(prompt.contains("`language`: `Japanese`"));
|
assert!(prompt.contains("`language`: `Japanese`"));
|
||||||
@@ -8267,15 +8151,7 @@ mod build_summary_prompt_tests {
|
|||||||
}
|
}
|
||||||
let scope = Scope::writable(&cwd).unwrap();
|
let scope = Scope::writable(&cwd).unwrap();
|
||||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||||
let workspace_context = if memory_config
|
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
|
||||||
.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 mut worker = Worker::new(
|
let mut worker = Worker::new(
|
||||||
manifest,
|
manifest,
|
||||||
Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient),
|
Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient),
|
||||||
@@ -8287,6 +8163,16 @@ mod build_summary_prompt_tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
worker.set_resident_memory_injection(gates.summary);
|
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(
|
let template = SystemPromptTemplate::parse(
|
||||||
"default",
|
"default",
|
||||||
crate::prompt::source::PromptCatalogSource::builtins_only(),
|
crate::prompt::source::PromptCatalogSource::builtins_only(),
|
||||||
@@ -8313,45 +8199,6 @@ mod build_summary_prompt_tests {
|
|||||||
Some(doc.to_string())
|
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]
|
#[tokio::test]
|
||||||
async fn resident_summary_body_is_injected_without_frontmatter() {
|
async fn resident_summary_body_is_injected_without_frontmatter() {
|
||||||
let rendered = render_system_prompt_with_summary(
|
let rendered = render_system_prompt_with_summary(
|
||||||
@@ -8367,7 +8214,7 @@ mod build_summary_prompt_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn resident_summary_injection_can_be_disabled_by_manifest() {
|
async fn resident_summary_injection_can_be_disabled_by_memory_feature() {
|
||||||
let mut memory = manifest::ResolvedMemoryFeatureConfig::default();
|
let mut memory = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
memory.profile.resident.inject_summary = false;
|
memory.profile.resident.inject_summary = false;
|
||||||
let rendered = render_system_prompt_with_summary(
|
let rendered = render_system_prompt_with_summary(
|
||||||
|
|||||||
+2
-2
@@ -227,8 +227,8 @@ permission = "write"
|
|||||||
# Profile由来の設定を `profile` に、Backend由来のWorkspace設定snapshotを
|
# Profile由来の設定を `profile` に、Backend由来のWorkspace設定snapshotを
|
||||||
# `workspace_settings` に分離して保存する。`workspace_settings` はBackendだけが
|
# `workspace_settings` に分離して保存する。`workspace_settings` はBackendだけが
|
||||||
# bindする信頼済み入力で、Profile・Browser・model入力から指定できない。
|
# bindする信頼済み入力で、Profile・Browser・model入力から指定できない。
|
||||||
# `profile.enabled = false` の場合、Memory tools、resident injection、extract、
|
# `profile.enabled = false` の場合、Memory tools、Feature prompt contributionによる
|
||||||
# consolidation requestをすべて無効にし、snapshotも保持しない。
|
# resident injection、extract、consolidation requestをすべて無効にし、snapshotも保持しない。
|
||||||
#
|
#
|
||||||
# [feature.memory.profile]
|
# [feature.memory.profile]
|
||||||
# enabled = true
|
# enabled = true
|
||||||
|
|||||||
Reference in New Issue
Block a user