use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use futures::stream;
use super::merge_system_prompt_with_contexts;
use super::system_sections::{strip_existing_skill_context, strip_existing_tool_guide_context};
use bamboo_agent_core::Message;
use bamboo_infrastructure::{LLMChunk, LLMError, LLMProvider, LLMStream};
#[derive(Clone)]
struct StaticResponseProvider {
response: String,
requested_models: Arc<Mutex<Vec<String>>>,
}
impl StaticResponseProvider {
fn new(response: impl Into<String>) -> Self {
Self {
response: response.into(),
requested_models: Arc::new(Mutex::new(Vec::new())),
}
}
}
#[async_trait]
impl LLMProvider for StaticResponseProvider {
async fn chat_stream(
&self,
_messages: &[Message],
_tools: &[bamboo_agent_core::tools::ToolSchema],
_max_output_tokens: Option<u32>,
model: &str,
) -> Result<LLMStream, LLMError> {
self.requested_models
.lock()
.expect("lock poisoned")
.push(model.to_string());
Ok(Box::pin(stream::iter(vec![
Ok(LLMChunk::Token(self.response.clone())),
Ok(LLMChunk::Done),
])))
}
}
#[test]
fn merge_system_prompt_with_contexts_appends_both_contexts() {
let merged = merge_system_prompt_with_contexts(
"You are a helpful assistant.",
"\n\n## Skill System\n\n### Available Skills\nDetails",
"## Tool Usage Guidelines\n\n### File Reading Tools\nDetails",
);
assert!(merged.starts_with("You are a helpful assistant."));
assert!(merged.contains("<!-- BAMBOO_SKILL_CONTEXT_START -->"));
assert!(merged.contains("## Skill System"));
assert!(merged.contains("<!-- BAMBOO_TOOL_GUIDE_START -->"));
assert!(merged.contains("## Tool Usage Guidelines"));
}
#[test]
fn merge_system_prompt_with_contexts_handles_empty_base_prompt() {
let merged = merge_system_prompt_with_contexts(
"",
"\n\n## Skill System\n\n### Available Skills",
"## Tool Usage Guidelines\n\n### File Reading Tools",
);
assert_eq!(
merged,
"<!-- BAMBOO_SKILL_CONTEXT_START -->\n## Skill System\n\n### Available Skills\n<!-- BAMBOO_SKILL_CONTEXT_END -->\n\n<!-- BAMBOO_TOOL_GUIDE_START -->\n## Tool Usage Guidelines\n\n### File Reading Tools\n<!-- BAMBOO_TOOL_GUIDE_END -->"
);
}
#[test]
fn strip_existing_skill_context_removes_previous_section() {
let stripped = strip_existing_skill_context(
"Base prompt\n\n<!-- BAMBOO_SKILL_CONTEXT_START -->\n## Skill System\n\n### Available Skills\nInstructions\n<!-- BAMBOO_SKILL_CONTEXT_END -->",
);
assert_eq!(stripped, "Base prompt");
}
#[test]
fn strip_existing_skill_context_does_not_remove_user_heading_without_markers() {
let original = "Base prompt\n\n## Skill System\nThis heading belongs to user prompt.";
let stripped = strip_existing_skill_context(original);
assert_eq!(stripped, original);
}
#[test]
fn strip_existing_tool_guide_context_removes_previous_section() {
let stripped = strip_existing_tool_guide_context(
"Base prompt\n\n<!-- BAMBOO_TOOL_GUIDE_START -->\n## Tool Usage Guidelines\n\n### File Reading Tools\nInstructions\n<!-- BAMBOO_TOOL_GUIDE_END -->",
);
assert_eq!(stripped, "Base prompt");
}
#[test]
fn strip_existing_tool_guide_context_does_not_remove_user_heading_without_markers() {
let original = "Base prompt\n\n## Tool Usage Guidelines\nUser custom section.";
let stripped = strip_existing_tool_guide_context(original);
assert_eq!(stripped, original);
}
#[tokio::test]
async fn inject_external_memory_includes_global_dream_fallback_and_session_note_when_project_unknown(
) {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
store
.write_dream_view("# Bamboo Dream Notebook\n\nDurable cross-session insight")
.await
.expect("save dream notebook");
store
.write_session_topic("session-dream-test", "default", "Session durable note")
.await
.expect("save session note");
let mut session = bamboo_agent_core::Session::new("session-dream-test", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("Global Dream Summary (fallback)"));
assert!(system_prompt.contains("Durable cross-session insight"));
assert!(system_prompt.contains("Session Memory Note"));
assert!(system_prompt.contains("Session durable note"));
assert!(!system_prompt.contains("### Project Durable Memory Index"));
assert!(system_prompt.contains("Use the `memory` tool for durable project/global knowledge"));
assert!(system_prompt.contains("prefer `memory` action=query first"));
}
#[tokio::test]
async fn inject_external_memory_includes_project_memory_index_and_omits_global_dream_fallback() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-alpha");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Project,
"Release freeze begins next week",
"Merge freeze begins on Tuesday for mobile release cut.",
&["release".to_string(), "freeze".to_string()],
Some("session-project-memory"),
"main-model",
false,
)
.await
.expect("save project memory");
store
.write_dream_view("# Bamboo Dream Notebook\n\nGlobal fallback that should not appear")
.await
.expect("save dream notebook");
let mut session = bamboo_agent_core::Session::new("session-project-memory", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("### Project Durable Memory Index"));
assert!(system_prompt.contains("Release freeze begins next week"));
assert!(!system_prompt.contains("### Global Dream Summary (fallback)"));
assert!(!system_prompt.contains("Global fallback that should not appear"));
assert!(system_prompt.contains("### Session Memory Note (markdown)"));
}
#[tokio::test]
async fn inject_external_memory_excludes_other_project_memory_index_content() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace_a = temp_dir.path().join("workspace-project-a");
let workspace_b = temp_dir.path().join("workspace-project-b");
std::fs::create_dir_all(&workspace_a).expect("workspace a dir");
std::fs::create_dir_all(&workspace_b).expect("workspace b dir");
let project_key_a = bamboo_memory::memory_store::project_key_from_path(&workspace_a);
let project_key_b = bamboo_memory::memory_store::project_key_from_path(&workspace_b);
store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key_a.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Project,
"Project A release rule",
"Only Project A changes may ship this week.",
&["release".to_string(), "project-a".to_string()],
Some("session-project-a"),
"main-model",
false,
)
.await
.expect("save project A memory");
store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key_b.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Project,
"Project B deployment rule",
"Project B uses a separate deployment checklist.",
&["deploy".to_string(), "project-b".to_string()],
Some("session-project-b"),
"main-model",
false,
)
.await
.expect("save project B memory");
let mut session = bamboo_agent_core::Session::new("session-project-a", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace_a.to_string_lossy().to_string(),
);
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("### Project Durable Memory Index"));
assert!(system_prompt.contains("Project A release rule"));
assert!(system_prompt.contains("Only Project A changes may ship this week."));
assert!(!system_prompt.contains("Project B deployment rule"));
assert!(!system_prompt.contains("Project B uses a separate deployment checklist."));
}
#[tokio::test]
async fn inject_external_memory_truncates_project_memory_index_and_adds_freshness_note() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-beta");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
let views_dir = store.resolver().views_dir(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
);
std::fs::create_dir_all(&views_dir).expect("views dir");
let large_view = format!(
"# Bamboo Memory Index (Project: {project_key})\n\n- `mem_old` Architectural note [project / active] updated 2026-03-01T00:00:00Z\n - {}\n{}",
"Older repo-state observation that needs verification.",
"x".repeat(4_000)
);
std::fs::write(
views_dir.join(bamboo_memory::memory_store::MEMORY_VIEW_FILE),
large_view,
)
.expect("write memory view");
let mut session =
bamboo_agent_core::Session::new("session-project-memory-truncated", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("### Project Durable Memory Index"));
assert!(system_prompt.contains("showing "));
assert!(
system_prompt.contains("Historical memory index entry")
|| system_prompt.contains("Older memory index entry")
);
}
#[tokio::test]
async fn inject_external_memory_truncates_multi_topic_content_and_is_idempotent() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
store
.write_session_topic("session-memory-many", "alpha", &"a".repeat(5_000))
.await
.expect("save alpha");
store
.write_session_topic("session-memory-many", "beta", &"b".repeat(5_000))
.await
.expect("save beta");
let mut session = bamboo_agent_core::Session::new("session-memory-many", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert_eq!(
system_prompt
.matches("## External Memory (Persistent)")
.count(),
1
);
assert!(system_prompt.contains("### Session Memory Topic: `alpha`"));
assert!(system_prompt.contains("### Session Memory Topic: `beta`"));
assert!(system_prompt.contains("showing "));
assert!(system_prompt.contains("use action=read topic=alpha"));
}
#[tokio::test]
async fn inject_external_memory_renders_relevant_memory_section_for_project_hits() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-recall-project");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Feedback,
"User prefers concise answers",
"Keep responses concise and avoid unnecessary recap.",
&["concise".to_string(), "style".to_string()],
Some("session-recall-project"),
"main-model",
false,
)
.await
.expect("save relevant project memory");
let mut session = bamboo_agent_core::Session::new("session-recall-project", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
session.add_message(bamboo_agent_core::Message::user(
"请记住我更喜欢 concise answers 并减少 recap",
));
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("### Relevant Durable Memories"));
assert!(system_prompt.contains("User prefers concise answers"));
assert!(system_prompt.contains("Summary: Keep responses concise"));
assert!(system_prompt.contains("[active][project]"));
}
#[tokio::test]
async fn inject_external_memory_adds_stale_guidance_for_old_relevant_memory_hits() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-recall-stale");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
let doc = store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Project,
"Release freeze policy",
"Mobile release freeze starts Tuesday and needs verification.",
&["release".to_string(), "freeze".to_string()],
Some("session-recall-stale"),
"main-model",
false,
)
.await
.expect("save stale-eligible project memory");
let raw = std::fs::read_to_string(&doc.path).expect("read stored memory doc");
let old_timestamp = "2026-03-01T00:00:00Z";
let rewritten = raw
.replace(&doc.frontmatter.updated_at, old_timestamp)
.replace(&doc.frontmatter.created_at, old_timestamp);
std::fs::write(&doc.path, rewritten).expect("rewrite timestamps");
store
.rebuild_scope(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
)
.await
.expect("rebuild scope after timestamp rewrite");
let mut session = bamboo_agent_core::Session::new("session-recall-stale", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
session.add_message(bamboo_agent_core::Message::user("release freeze policy"));
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("### Relevant Durable Memories"));
assert!(system_prompt.contains("Release freeze policy"));
assert!(
system_prompt.contains("Historical memory")
|| system_prompt.contains("Older historical memory")
);
assert!(system_prompt.contains("verify against current"));
}
#[tokio::test]
async fn inject_external_memory_omits_relevant_memory_section_when_no_match_exists() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-recall-none");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let mut session = bamboo_agent_core::Session::new("session-recall-none", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
session.add_message(bamboo_agent_core::Message::user(
"this query should not match anything relevant",
));
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(!system_prompt.contains("### Relevant Durable Memories"));
}
#[tokio::test]
async fn inject_external_memory_limits_relevant_memories_to_top_k() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-recall-topk");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
for idx in 0..4 {
store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Project,
&format!("Release freeze note {idx}"),
&format!("release freeze detail {idx}"),
&["release".to_string(), "freeze".to_string()],
Some("session-recall-topk"),
"main-model",
false,
)
.await
.expect("save project memory");
std::thread::sleep(std::time::Duration::from_millis(5));
}
let mut session = bamboo_agent_core::Session::new("session-recall-topk", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
session.add_message(bamboo_agent_core::Message::user("release freeze"));
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("### Relevant Durable Memories"));
assert_eq!(system_prompt.matches("Summary:").count(), 3);
}
#[tokio::test]
async fn inject_external_memory_uses_global_relevant_memory_fallback_only_when_project_has_no_hits()
{
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-recall-fallback");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Project,
"Unrelated project note",
"This should not match the fallback query.",
&["project".to_string()],
Some("session-recall-fallback"),
"main-model",
false,
)
.await
.expect("save unrelated project memory");
store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Global,
None,
bamboo_memory::memory_store::DurableMemoryType::Reference,
"Global release guidance",
"Use the release train checklist before shipping.",
&["release".to_string(), "checklist".to_string()],
Some("session-recall-fallback"),
"main-model",
false,
)
.await
.expect("save global fallback memory");
let mut session = bamboo_agent_core::Session::new("session-recall-fallback", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
session.add_message(bamboo_agent_core::Message::user("release checklist"));
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("### Relevant Durable Memories"));
assert!(system_prompt.contains("Global release guidance"));
assert!(system_prompt.contains("[active][global]"));
assert!(!system_prompt.contains("Unrelated project note (score"));
}
#[tokio::test]
async fn inject_external_memory_prefers_project_dream_over_global_fallback() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-project-dream");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
store
.write_project_dream_view(
project_key.as_str(),
"# Bamboo Dream Notebook\n\nProject dream context",
)
.await
.expect("write project dream");
store
.write_dream_view("# Bamboo Dream Notebook\n\nGlobal dream fallback")
.await
.expect("write global dream");
let mut session = bamboo_agent_core::Session::new("session-project-dream", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("### Project Dream Summary"));
assert!(system_prompt.contains("Project dream context"));
assert!(!system_prompt.contains("### Global Dream Summary (fallback)"));
assert!(!system_prompt.contains("Global dream fallback"));
}
#[tokio::test]
async fn inject_external_memory_uses_global_dream_fallback_when_project_dream_and_index_are_missing(
) {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-global-dream-fallback");
std::fs::create_dir_all(&workspace).expect("workspace dir");
store
.write_dream_view("# Bamboo Dream Notebook\n\nGlobal fallback dream")
.await
.expect("write global dream");
let mut session =
bamboo_agent_core::Session::new("session-global-dream-fallback", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags::default(),
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(system_prompt.contains("### Global Dream Summary (fallback)"));
assert!(system_prompt.contains("Global fallback dream"));
assert!(!system_prompt.contains("### Project Dream Summary"));
}
#[tokio::test]
async fn inject_external_memory_omits_project_index_when_project_prompt_injection_disabled() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-no-project-index");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Project,
"Project release rule",
"Use the strict release checklist.",
&["release".to_string()],
Some("session-no-project-index"),
"main-model",
false,
)
.await
.expect("save project memory");
let mut session = bamboo_agent_core::Session::new("session-no-project-index", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags {
project_prompt_injection: false,
..crate::runtime::config::PromptMemoryFlags::default()
},
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(!system_prompt.contains("### Project Durable Memory Index"));
assert!(!system_prompt.contains("Project release rule"));
let observability = session
.metadata
.get("runtime_prompt_memory_observability")
.and_then(|raw| {
serde_json::from_str::<bamboo_agent_core::PromptMemoryObservability>(raw).ok()
})
.expect("observability should be recorded");
assert!(!observability.project_prompt_injection_enabled);
assert!(!observability.relevant_recall_rerank_enabled);
assert_eq!(observability.project_memory_index_status, "disabled");
assert_eq!(observability.project_dream_status, "loaded");
assert_eq!(
observability.global_dream_fallback_status,
"skipped_project_memory_or_dream_present"
);
assert_eq!(observability.dream_source, "project");
}
#[tokio::test]
async fn inject_external_memory_omits_relevant_recall_and_uses_global_dream_when_project_first_disabled(
) {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-global-dream-mode");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Feedback,
"User prefers concise answers",
"Keep answers concise.",
&["concise".to_string()],
Some("session-global-dream-mode"),
"main-model",
false,
)
.await
.expect("save relevant project memory");
store
.write_project_dream_view(
project_key.as_str(),
"# Bamboo Dream Notebook\n\nProject dream context",
)
.await
.expect("write project dream");
store
.write_dream_view("# Bamboo Dream Notebook\n\nGlobal dream fallback")
.await
.expect("write global dream");
let mut session = bamboo_agent_core::Session::new("session-global-dream-mode", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
session.add_message(bamboo_agent_core::Message::user("concise answers"));
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags {
relevant_recall: false,
project_first_dream: false,
..crate::runtime::config::PromptMemoryFlags::default()
},
None,
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
assert!(!system_prompt.contains("### Relevant Durable Memories"));
assert!(system_prompt.contains("### Global Dream Summary (fallback)"));
assert!(system_prompt.contains("Global dream fallback"));
assert!(!system_prompt.contains("### Project Dream Summary"));
assert!(!system_prompt.contains("Project dream context"));
let observability = session
.metadata
.get("runtime_prompt_memory_observability")
.and_then(|raw| {
serde_json::from_str::<bamboo_agent_core::PromptMemoryObservability>(raw).ok()
})
.expect("observability should be recorded");
assert!(!observability.relevant_recall_enabled);
assert!(!observability.relevant_recall_rerank_enabled);
assert!(!observability.project_first_dream_enabled);
assert_eq!(observability.relevant_memory_status, "disabled");
assert_eq!(observability.global_dream_fallback_status, "forced_loaded");
assert_eq!(observability.dream_source, "global_fallback");
}
#[tokio::test]
async fn inject_external_memory_uses_model_rerank_for_relevant_memories_when_enabled() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let store = bamboo_memory::memory_store::MemoryStore::new(temp_dir.path());
let workspace = temp_dir.path().join("workspace-rerank-recall");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let project_key = bamboo_memory::memory_store::project_key_from_path(&workspace);
let lexical_first = store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Feedback,
"Release freeze checklist",
"Generic release freeze checklist for shipping work.",
&["release".to_string(), "freeze".to_string()],
Some("session-rerank-recall"),
"main-model",
false,
)
.await
.expect("save lexical-first memory");
let reranked_first = store
.write_memory(
bamboo_memory::memory_store::MemoryScope::Project,
Some(project_key.as_str()),
bamboo_memory::memory_store::DurableMemoryType::Feedback,
"Mobile launch blocker",
"This durable note captures the release freeze decision for the mobile app and should be preferred for mobile freeze requests.",
&["mobile".to_string(), "launch".to_string()],
Some("session-rerank-recall"),
"main-model",
false,
)
.await
.expect("save reranked-first memory");
let provider = StaticResponseProvider::new(format!(
"{{\"ids\":[\"{}\",\"{}\"]}}",
reranked_first.frontmatter.id, lexical_first.frontmatter.id
));
let requested_models = provider.requested_models.clone();
let runtime_context = super::PromptMemoryRuntimeContext {
llm: Arc::new(provider),
background_model_name: Some("rerank-fast-model".to_string()),
};
let mut session = bamboo_agent_core::Session::new("session-rerank-recall", "test-model");
session.add_message(bamboo_agent_core::Message::system("Base prompt"));
session.metadata.insert(
"workspace_path".to_string(),
workspace.to_string_lossy().to_string(),
);
session.add_message(bamboo_agent_core::Message::user(
"release freeze for mobile launch",
));
super::inject_external_memory_into_system_message_with_store(
&mut session,
&store,
crate::runtime::config::PromptMemoryFlags {
relevant_recall_rerank: true,
..crate::runtime::config::PromptMemoryFlags::default()
},
Some(&runtime_context),
)
.await;
let system_prompt = session
.messages
.iter()
.find(|message| matches!(message.role, bamboo_agent_core::Role::System))
.map(|message| message.content.clone())
.expect("system prompt should exist");
let reranked_pos = system_prompt
.find("Mobile launch blocker")
.expect("reranked memory should be rendered");
let lexical_pos = system_prompt
.find("Release freeze checklist")
.expect("lexical memory should be rendered");
assert!(reranked_pos < lexical_pos);
let observability = session
.metadata
.get("runtime_prompt_memory_observability")
.and_then(|raw| {
serde_json::from_str::<bamboo_agent_core::PromptMemoryObservability>(raw).ok()
})
.expect("observability should be recorded");
assert!(observability.relevant_recall_enabled);
assert!(observability.relevant_recall_rerank_enabled);
assert_eq!(observability.relevant_memory_status, "reranked");
assert_eq!(
requested_models.lock().expect("lock poisoned").as_slice(),
["rerank-fast-model"]
);
}