use bamboo_agent_core::{
ContextBlock, ContextBlockPriority, ContextBlockStability, ContextBlockType, Message, Session,
};
#[derive(Debug, Clone, Default)]
#[cfg(test)]
pub struct PromptEnvelope {
pub stable_instructions: String,
pub stable_prefix_messages: Vec<Message>,
pub dynamic_context_messages: Vec<Message>,
}
#[derive(Debug, Clone)]
pub struct StablePromptFrame {
pub stable_instructions: String,
pub stable_prefix_messages: Vec<Message>,
}
impl StablePromptFrame {
pub fn new(
stable_instructions: impl Into<String>,
stable_prefix_messages: Vec<Message>,
) -> Self {
Self {
stable_instructions: stable_instructions.into(),
stable_prefix_messages,
}
}
}
pub(crate) fn build_session_identity_context_block(session: &Session) -> ContextBlock {
let encoded_session_id =
serde_json::to_string(&session.id).expect("Session ID string is JSON serializable");
ContextBlock::new(
ContextBlockType::SessionIdentity,
ContextBlockPriority::Critical,
ContextBlockStability::SessionStable,
"Current Session Identity",
format!(
"Current Session ID: {encoded_session_id}\nTools authorize the caller from trusted runtime context, not from this prompt value."
),
)
}
const ROOT_ORCHESTRATION_GUIDANCE: &str = "Root delegation mode (explicitly selected for this Session):\n\
Own task boundaries, ordering, progress, integration, and the final response. Delegate independently verifiable work when useful; keep the root available to coordinate.\n\
For uncertain or multi-step work, delegate a read-only Plan to a child and review its plan and evidence before assigning implementation.\n\
Give each child a narrow objective, in-scope files or systems, non-goals, expected evidence, and a time or budget bound. Shared background is context, not permission to widen scope.\n\
Inspect authoritative child progress and results when needed. Correct or revise the same child's assignment, then confirm the child received the change.\n\
If a finding exceeds the assignment, pause that expansion and seek the user's approval or a separate focused task before proceeding. Never infer broader authority from a child report.\n\
Verify child evidence, resolve conflicting results, and report completed work, remaining work, and risks with clear provenance.";
const ULTRA_ROOT_GUIDANCE: &str = "Ultra Root thinking mode: increase task-wide reasoning through independent child planning and narrow delegated execution, followed by Root verification and synthesis. Use the existing read-only Plan for planning. Keep required user goals, constraints and acceptance criteria intact. This product policy is independent of each model call's reasoning effort; it does not claim a native provider Ultra budget.";
pub(crate) fn build_root_orchestration_context_block(session: &Session) -> Option<ContextBlock> {
(session.root_orchestration_prompt_enabled() || session.root_orchestration_only_enabled()).then(
|| {
let guidance = if session.root_orchestration_only_enabled() {
format!("{ULTRA_ROOT_GUIDANCE}\n\n{ROOT_ORCHESTRATION_GUIDANCE}")
} else {
ROOT_ORCHESTRATION_GUIDANCE.to_string()
};
ContextBlock::new(
ContextBlockType::RootOrchestration,
ContextBlockPriority::Critical,
ContextBlockStability::SessionStable,
"Root Delegation Mode",
guidance,
)
},
)
}
#[cfg(test)]
#[test]
fn root_orchestration_guidance_is_bounded_and_schema_independent() {
assert!(ROOT_ORCHESTRATION_GUIDANCE.len() <= 1_200);
assert!(ULTRA_ROOT_GUIDANCE.len() + ROOT_ORCHESTRATION_GUIDANCE.len() <= 1_800);
for concept in [
"Plan", "progress", "Correct", "scope", "approval", "evidence",
] {
assert!(
ROOT_ORCHESTRATION_GUIDANCE.contains(concept),
"missing {concept}"
);
}
assert!(!ROOT_ORCHESTRATION_GUIDANCE.contains("\"action\""));
assert!(!ROOT_ORCHESTRATION_GUIDANCE.contains("\"intent\""));
}
#[cfg(test)]
#[test]
fn orchestration_only_root_receives_guidance_without_prompt_only_selection() {
let mut root = Session::new("mode-only-root", "model");
assert!(!root.root_orchestration_prompt_enabled());
assert!(build_root_orchestration_context_block(&root).is_none());
root.set_root_orchestration_only(true)
.expect("ordinary Root may select orchestration-only mode");
let restored: Session =
serde_json::from_str(&serde_json::to_string(&root).expect("serialize selected Root"))
.expect("restore selected Root");
assert!(!restored.root_orchestration_prompt_enabled());
let guidance = build_root_orchestration_context_block(&restored)
.expect("durable orchestration-only mode supplies delegation guidance");
assert_eq!(guidance.block_type, ContextBlockType::RootOrchestration);
assert!(guidance.content.contains("delegate a read-only Plan"));
assert!(guidance.content.contains("Ultra Root thinking mode"));
assert!(guidance.content.contains("independent of each model call"));
let child = Session::new_child_of("child", &restored, "model", "worker");
assert!(build_root_orchestration_context_block(&child).is_none());
let mut disabled = restored;
disabled
.set_root_orchestration_only(false)
.expect("Root may disable orchestration-only mode");
assert!(build_root_orchestration_context_block(&disabled).is_none());
disabled.set_root_orchestration_prompt_enabled(true);
let prompt_only = build_root_orchestration_context_block(&disabled).unwrap();
assert!(!prompt_only.content.contains("Ultra Root thinking mode"));
}
pub(crate) fn build_workspace_context_block(session: &Session) -> Option<ContextBlock> {
let workspace = super::prompt_setup::workspace_context_from_session(session)?;
let project = session
.metadata
.get(crate::project_context::PROJECT_CONTEXT_RENDERED_KEY)
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let content = project
.into_iter()
.chain(std::iter::once(workspace.trim()))
.collect::<Vec<_>>()
.join("\n\n");
Some(ContextBlock::new(
ContextBlockType::Workspace,
ContextBlockPriority::High,
ContextBlockStability::RoundDynamic,
"Project & Workspace",
content,
))
}
pub(crate) fn build_instruction_overlay_context_block(session: &Session) -> Option<ContextBlock> {
let workspace = session.workspace_path_meta()?;
let content =
crate::runtime::context::instruction::build_instruction_prompt_context(workspace.trim())?;
Some(ContextBlock::new(
ContextBlockType::InstructionOverlay,
ContextBlockPriority::Critical,
ContextBlockStability::RoundDynamic,
"Project Instructions",
content,
))
}
#[cfg(test)]
pub(crate) fn render_context_block_message(block: &ContextBlock) -> Message {
block.render_runtime_context_message()
}
#[cfg(test)]
pub(crate) fn assemble_prompt_envelope(
stable: StablePromptFrame,
dynamic_blocks: Vec<ContextBlock>,
) -> PromptEnvelope {
let dynamic_context_messages: Vec<Message> = dynamic_blocks
.iter()
.map(render_context_block_message)
.collect();
PromptEnvelope {
stable_instructions: stable.stable_instructions,
stable_prefix_messages: stable.stable_prefix_messages,
dynamic_context_messages,
}
}
pub(crate) fn build_task_list_context_block(session: &Session) -> Option<ContextBlock> {
let content = session.format_task_list_for_prompt();
let trimmed = content.trim();
if trimmed.is_empty() {
return None;
}
let title = session
.task_list
.as_ref()
.map(|task_list| format!("Current Task List: {}", task_list.title.trim()))
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "Current Task List".to_string());
Some(ContextBlock::new(
ContextBlockType::TaskSnapshot,
ContextBlockPriority::High,
ContextBlockStability::RoundDynamic,
title,
trimmed,
))
}
pub(crate) fn build_active_workflow_context_block(session: &Session) -> Option<ContextBlock> {
super::legacy_instruction::active_context_block(session)
}
pub(crate) fn build_goal_context_block(goal: Option<&str>) -> Option<ContextBlock> {
let objective = goal.map(str::trim).filter(|value| !value.is_empty())?;
Some(ContextBlock::new(
ContextBlockType::GoalState,
ContextBlockPriority::Critical,
ContextBlockStability::RoundDynamic,
"Session Goal",
crate::runtime::runner::prompt_context::render_goal_section(objective),
))
}
pub(crate) fn build_agent_hook_context_block(session: &Session) -> Option<ContextBlock> {
let mut contexts = session
.agent_runtime_state
.as_ref()
.map(|state| state.hook_contexts.clone())
.unwrap_or_default();
let plugin_contexts: Vec<String> = session
.metadata
.get("runtime.plugin_hook_contexts")
.and_then(|value| serde_json::from_str(value).ok())
.unwrap_or_default();
contexts.extend(plugin_contexts);
let content = contexts
.iter()
.map(|text| text.trim())
.filter(|text| !text.is_empty())
.collect::<Vec<_>>()
.join("\n\n---\n\n");
if content.is_empty() {
return None;
}
Some(ContextBlock::new(
ContextBlockType::AgentHookContext,
ContextBlockPriority::High,
ContextBlockStability::RoundDynamic,
"Session Hook Context",
content,
))
}
pub(crate) fn build_plan_mode_context_block(session: &Session) -> Option<ContextBlock> {
let text = crate::runtime::runner::prompt_context::render_plan_mode_section(session)?;
let trimmed = text.trim();
if trimmed.is_empty() {
return None;
}
Some(ContextBlock::new(
ContextBlockType::PlanModeState,
ContextBlockPriority::High,
ContextBlockStability::RoundDynamic,
"Plan Mode State",
trimmed,
))
}
pub(crate) fn build_plan_runtime_context_block(
session: &Session,
app_data_dir: Option<&std::path::Path>,
) -> Option<ContextBlock> {
let text =
crate::runtime::runner::prompt_context::render_plan_runtime_section(session, app_data_dir)?;
let trimmed = text.trim();
if trimmed.is_empty() {
return None;
}
Some(ContextBlock::new(
ContextBlockType::PlanRuntimeState,
ContextBlockPriority::High,
ContextBlockStability::RoundDynamic,
"Durable Plan Execution Context",
trimmed,
))
}
pub(crate) fn build_external_memory_context_block(session: &Session) -> Option<ContextBlock> {
let content = crate::runtime::runner::prompt_context::render_external_memory_section(session)?;
let trimmed = content.trim();
if trimmed.is_empty() {
return None;
}
Some(ContextBlock::new(
ContextBlockType::ExternalMemory,
ContextBlockPriority::Medium,
ContextBlockStability::RoundDynamic,
"External Memory (Persistent)",
trimmed,
))
}
pub(crate) fn build_project_resources_context_block(session: &Session) -> Option<ContextBlock> {
let content = session
.metadata
.get(crate::project_context::PROJECT_RESOURCES_RENDERED_KEY)?;
let trimmed = content.trim();
if trimmed.is_empty() {
return None;
}
Some(ContextBlock::new(
ContextBlockType::ProjectResources,
ContextBlockPriority::Medium,
ContextBlockStability::RoundDynamic,
"Project Shared Resources (redacted)",
trimmed,
))
}
fn history_boundary_content(
archive_boundaries: usize,
archived_messages: usize,
retained_recent_user_turns: usize,
) -> String {
format!(
"Earlier Session messages are stored exactly but omitted from the active model context.\n\
Archive boundaries: {archive_boundaries}. Archived messages: {archived_messages}. \
Recent complete user turns retained at the latest boundary: {retained_recent_user_turns}.\n\
Use session_history_current with search_current to locate exact evidence, then \
read_around or read_current for bounded raw history. Raw Session history is the \
transcript authority. Memory is selective and may be stale; do not guess."
)
}
fn history_boundary_block(
archive_boundaries: usize,
archived_messages: usize,
retained_recent_user_turns: usize,
) -> ContextBlock {
ContextBlock::new(
ContextBlockType::HistoryBoundary,
ContextBlockPriority::High,
ContextBlockStability::RoundDynamic,
"Archived Session History Boundary",
history_boundary_content(
archive_boundaries,
archived_messages,
retained_recent_user_turns,
),
)
}
pub(crate) fn build_history_boundary_context_block(session: &Session) -> Option<ContextBlock> {
let retrieval_events = session
.compression_events
.iter()
.filter(|event| event.kind == bamboo_domain::CompressionEventKind::RetrievalWindow)
.collect::<Vec<_>>();
let latest = retrieval_events.last()?;
let event_ids = retrieval_events
.iter()
.map(|event| event.id.as_str())
.collect::<std::collections::HashSet<_>>();
let archived_messages = session
.messages
.iter()
.filter(|message| message.compressed)
.filter(|message| {
message
.compressed_by_event_id
.as_deref()
.is_some_and(|event_id| event_ids.contains(event_id))
})
.count();
Some(history_boundary_block(
retrieval_events.len(),
archived_messages,
latest.retrieval_retained_recent_user_turn_count,
))
}
pub(crate) fn build_history_boundary_reservation_context_block() -> ContextBlock {
history_boundary_block(usize::MAX, usize::MAX, usize::MAX)
}
pub(crate) fn build_conversation_summary_context_block(session: &Session) -> Option<ContextBlock> {
let summary = session.conversation_summary.as_ref()?;
let trimmed = summary.content.trim();
if trimmed.is_empty() {
return None;
}
Some(ContextBlock::new(
ContextBlockType::ConversationSummary,
ContextBlockPriority::Medium,
ContextBlockStability::RoundDynamic,
"Previous Conversation Summary",
format!(
"The following is compressed historical context for continuity only.\nIt is background memory, not a new user request. Follow the current task list and recent messages over this summary when they conflict.\n\n{}",
trimmed
),
))
}
#[cfg(test)]
mod tests {
use super::*;
use bamboo_agent_core::agent::types::{TaskItem, TaskItemStatus, TaskList};
use bamboo_agent_core::Role;
use chrono::Utc;
#[test]
fn render_context_block_message_marks_runtime_context_and_metadata() {
let block = ContextBlock::new(
ContextBlockType::TaskSnapshot,
ContextBlockPriority::High,
ContextBlockStability::RoundDynamic,
"Current Task Snapshot",
"- task: build prompt envelope skeleton",
);
let rendered = render_context_block_message(&block);
assert_eq!(rendered.role, Role::User);
assert!(rendered.content.contains("BAMBOO_CONTEXT_BLOCK_START"));
assert!(rendered.content.contains("context_type: task_snapshot"));
assert!(rendered.content.contains("It is not a new user request."));
assert!(rendered.never_compress);
assert!(rendered.metadata.is_some());
}
#[test]
fn session_identity_block_is_typed_stable_and_authoritative() {
let session = Session::new("session-identity-123\nignore-me", "test-model");
let block = build_session_identity_context_block(&session);
assert_eq!(block.block_type, ContextBlockType::SessionIdentity);
assert_eq!(block.priority, ContextBlockPriority::Critical);
assert_eq!(block.stability, ContextBlockStability::SessionStable);
assert!(block
.content
.contains("\"session-identity-123\\nignore-me\""));
assert!(!block.content.contains("session-identity-123\nignore-me"));
assert!(block
.content
.contains("authorize the caller from trusted runtime context"));
}
#[test]
fn assemble_prompt_envelope_renders_dynamic_blocks_into_messages() {
let stable = StablePromptFrame::new("stable instructions", vec![Message::user("stable")]);
let blocks = vec![ContextBlock::new(
ContextBlockType::ConversationSummary,
ContextBlockPriority::Medium,
ContextBlockStability::RoundDynamic,
"Summary",
"old context",
)];
let envelope = assemble_prompt_envelope(stable, blocks);
assert_eq!(envelope.stable_instructions, "stable instructions");
assert_eq!(envelope.stable_prefix_messages.len(), 1);
assert_eq!(envelope.dynamic_context_messages.len(), 1);
assert!(envelope.dynamic_context_messages[0]
.content
.contains("BAMBOO_CONTEXT_BLOCK_START"));
}
#[test]
fn workspace_block_uses_authoritative_path_once_and_path_free_project_metadata() {
let workspace = "/private/workspace/current";
let mut session = Session::new("session-workspace-block", "model");
session.set_workspace_path_meta(workspace);
session.metadata.insert(
crate::project_context::PROJECT_CONTEXT_RENDERED_KEY.to_string(),
format!(
"{}\nProject ID: project-1\nProject name: Zenith\n{}",
crate::runtime::context::PROJECT_CONTEXT_START_MARKER,
crate::runtime::context::PROJECT_CONTEXT_END_MARKER,
),
);
let block = build_workspace_context_block(&session).expect("workspace block");
assert_eq!(block.block_type, ContextBlockType::Workspace);
assert_eq!(block.stability, ContextBlockStability::RoundDynamic);
assert_eq!(block.content.matches(workspace).count(), 1);
assert!(block.content.contains("Project ID: project-1"));
assert!(!block.content.contains("Project path:"));
assert!(!block.content.contains("Project home"));
}
#[test]
fn build_task_list_context_block_uses_formatted_prompt_content() {
let mut session = Session::new("session-task-block", "model");
session.task_list = Some(TaskList {
session_id: session.id.clone(),
title: "Agent Tasks".to_string(),
items: vec![TaskItem {
id: "task-1".to_string(),
description: "Implement prompt envelope".to_string(),
status: TaskItemStatus::InProgress,
..TaskItem::default()
}],
created_at: Utc::now(),
updated_at: Utc::now(),
});
let block = build_task_list_context_block(&session).expect("task block should exist");
assert_eq!(block.block_type, ContextBlockType::TaskSnapshot);
assert_eq!(block.priority, ContextBlockPriority::High);
assert!(block.content.contains("Current Task List"));
assert!(block.content.contains("Implement prompt envelope"));
}
#[test]
fn build_external_memory_context_block_reads_session_field() {
let mut session = Session::new("session-external-memory-block", "model");
session.metadata.insert(
crate::runtime::runner::prompt_context::EXTERNAL_MEMORY_RENDERED_KEY.to_string(),
"## External Memory (Persistent)\n\nSession note body".to_string(),
);
let block = build_external_memory_context_block(&session)
.expect("external memory block should exist");
assert_eq!(block.block_type, ContextBlockType::ExternalMemory);
assert_eq!(block.priority, ContextBlockPriority::Medium);
assert!(block.content.contains("## External Memory (Persistent)"));
assert!(block.content.contains("Session note body"));
assert!(build_external_memory_context_block(&Session::new("s2", "model")).is_none());
}
#[test]
fn project_resources_context_block_is_round_dynamic_and_redacted() {
let mut session = Session::new("project-resources", "model");
session.metadata.insert(
crate::project_context::PROJECT_RESOURCES_RENDERED_KEY.to_string(),
"Project ID: project-1\nResource revision: 3\n- Skills: status=available, items=2"
.to_string(),
);
let block =
build_project_resources_context_block(&session).expect("project resource block");
assert_eq!(block.block_type, ContextBlockType::ProjectResources);
assert_eq!(block.stability, ContextBlockStability::RoundDynamic);
assert!(block.content.contains("Resource revision: 3"));
assert!(!block.content.contains("secret"));
}
#[test]
fn build_agent_hook_context_block_reads_runtime_state_as_volatile_context() {
let mut session = Session::new("session-hook-block", "model");
let mut state = bamboo_domain::AgentRuntimeState::new("run-hook");
state.hook_contexts = vec!["first hook context".to_string(), "second".to_string()];
session.agent_runtime_state = Some(state);
let block = build_agent_hook_context_block(&session).expect("hook block should exist");
assert_eq!(block.block_type, ContextBlockType::AgentHookContext);
assert_eq!(block.priority, ContextBlockPriority::High);
assert_eq!(block.stability, ContextBlockStability::RoundDynamic);
assert!(block.content.contains("first hook context"));
assert!(block.content.contains("second"));
assert!(build_agent_hook_context_block(&Session::new("empty", "model")).is_none());
}
#[test]
fn build_plan_mode_context_block_renders_from_session_state() {
use bamboo_domain::session::runtime_state::{
AgentRuntimeState, PlanModeState, PlanModeStatus,
};
let mut session = Session::new("session-plan-mode-block", "model");
session.agent_runtime_state = Some(AgentRuntimeState::new("run-1"));
session.agent_runtime_state.as_mut().unwrap().plan_mode = Some(PlanModeState {
entered_at: chrono::Utc::now(),
pre_permission_mode: "default".to_string(),
plan_file_path: None,
status: PlanModeStatus::Exploring,
});
let block = build_plan_mode_context_block(&session).expect("plan mode block should exist");
assert_eq!(block.block_type, ContextBlockType::PlanModeState);
assert_eq!(block.priority, ContextBlockPriority::High);
assert!(block.content.contains("PLAN MODE ACTIVE"));
assert!(build_plan_mode_context_block(&Session::new("s2", "model")).is_none());
}
#[test]
fn build_plan_runtime_context_block_renders_from_session_state() {
use bamboo_domain::session::runtime_state::{
AgentRuntimeState, PlanModeState, PlanModeStatus,
};
let mut session = Session::new("session-plan-runtime-block", "model");
session.agent_runtime_state = Some(AgentRuntimeState::new("run-1"));
session.agent_runtime_state.as_mut().unwrap().plan_mode = Some(PlanModeState {
entered_at: chrono::Utc::now(),
pre_permission_mode: "default".to_string(),
plan_file_path: None,
status: PlanModeStatus::Designing,
});
let block = build_plan_runtime_context_block(&session, None)
.expect("plan runtime block should exist");
assert_eq!(block.block_type, ContextBlockType::PlanRuntimeState);
assert_eq!(block.priority, ContextBlockPriority::High);
assert!(block.content.contains("DURABLE PLAN EXECUTION CONTEXT"));
assert!(build_plan_runtime_context_block(&Session::new("s2", "model"), None).is_none());
}
#[test]
fn build_conversation_summary_context_block_wraps_summary_content() {
let mut session = Session::new("session-summary-block", "model");
session.conversation_summary = Some(bamboo_agent_core::ConversationSummary::new(
"Older work was compressed.",
3,
120,
));
let block =
build_conversation_summary_context_block(&session).expect("summary block should exist");
assert_eq!(block.block_type, ContextBlockType::ConversationSummary);
assert_eq!(block.priority, ContextBlockPriority::Medium);
assert!(block.content.contains("compressed historical context"));
assert!(block.content.contains("Older work was compressed."));
}
#[test]
fn history_boundary_rehydrates_without_raw_history_content() {
let mut session = Session::new("session-history-boundary", "model");
let mut event = bamboo_domain::CompressionEvent::new(
1,
1,
82.0,
58.0,
0,
bamboo_domain::CompressionTriggerType::Auto,
0.0,
None,
0,
);
event.kind = bamboo_domain::CompressionEventKind::RetrievalWindow;
event.retrieval_retained_recent_user_turn_count = 3;
let event_id = event.id.clone();
let mut archived = Message::user("private archived transcript body");
archived.compressed = true;
archived.compressed_by_event_id = Some(event_id);
session.messages.push(archived);
session.compression_events.push(event);
let block = build_history_boundary_context_block(&session).expect("history boundary");
assert_eq!(block.block_type, ContextBlockType::HistoryBoundary);
assert_eq!(block.stability, ContextBlockStability::RoundDynamic);
assert!(block.content.contains("Archived messages: 1"));
assert!(block.content.contains("session_history_current"));
assert!(block
.content
.contains("Memory is selective and may be stale"));
assert!(!block.content.contains("private archived transcript body"));
let bytes = serde_json::to_vec(&session).unwrap();
let restored: Session = serde_json::from_slice(&bytes).unwrap();
assert_eq!(
build_history_boundary_context_block(&restored)
.expect("boundary after reload")
.content,
block.content
);
assert!(
build_history_boundary_reservation_context_block()
.content
.len()
>= block.content.len()
);
}
#[test]
fn history_boundary_is_absent_without_retrieval_event() {
assert!(build_history_boundary_context_block(&Session::new("plain", "model")).is_none());
}
}