use bamboo_agent_core::{
ContextBlock, ContextBlockPriority, ContextBlockStability, ContextBlockType, Message, Session,
};
#[derive(Debug, Clone, Default)]
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 render_context_block_message(block: &ContextBlock) -> Message {
block.render_runtime_context_message()
}
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> {
let durable = session
.metadata
.get(bamboo_skills::ACTIVE_WORKFLOW_SNAPSHOT_METADATA_KEY)
.and_then(|raw| serde_json::from_str::<bamboo_skills::DurableWorkflowActivation>(raw).ok())
.filter(|durable| {
durable.active.status == bamboo_skills::WorkflowActivationStatus::Active
})?;
let entry = durable.snapshot.skills.get(&durable.active.id)?;
if durable.snapshot.skills.len() != 1
|| entry.revision != durable.active.revision
|| entry.catalog_entry.source != durable.active.source
|| entry.catalog_entry.kind != bamboo_skills::WorkflowKind::Instruction
{
return None;
}
let dynamic = durable
.active
.dynamic_context
.iter()
.map(|block| {
serde_json::json!({
"provider_id": block.provider_id,
"provenance": block.provenance,
"status": block.status,
"content": block.content,
"diagnostic": block.diagnostic,
})
})
.collect::<Vec<_>>();
Some(
ContextBlock::new(
ContextBlockType::WorkflowRuntime,
ContextBlockPriority::Critical,
ContextBlockStability::SessionStable,
format!("Active Workflow: {}@{}", durable.active.id, durable.active.revision),
format!(
"workflow_id: {}\nsource: {:?}\nrevision: {}\nargs: {}\ncontext_fingerprint: {}\n\n### Instructions\n{}\n\n### Dynamic Context\n{}",
durable.active.id,
durable.active.source,
durable.active.revision,
durable.active.args,
durable
.active
.context_fingerprint
.as_deref()
.unwrap_or("unavailable"),
entry.definition.prompt,
serde_json::to_string(&dynamic).unwrap_or_else(|_| "[]".to_string()),
),
)
.with_metadata(Some(serde_json::json!({
"workflow_id": durable.active.id,
"source": durable.active.source,
"revision": durable.active.revision,
"context_fingerprint": durable.active.context_fingerprint,
}))),
)
}
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_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_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 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 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 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."));
}
}