use crate::{
prompt::{render_section, EffectivePrompt, PromptSection, PromptStability},
provider::{
ConversationMessage, PromptContentBlock, ProviderNativeWebSearchRequest,
ProviderPromptCache, ProviderPromptFrame, ProviderTurnRequest,
},
tool::ToolSpec,
};
pub fn build_provider_prompt_frame(effective_prompt: &EffectivePrompt) -> ProviderPromptFrame {
let (system_blocks, context_blocks) = build_prompt_content_blocks(
&effective_prompt.system_sections,
&effective_prompt.context_sections,
);
ProviderPromptFrame::structured(
effective_prompt.rendered_system_prompt.clone(),
system_blocks,
context_blocks,
Some(prompt_cache_from_effective_prompt(effective_prompt)),
)
}
pub fn build_provider_turn_request(
effective_prompt: &EffectivePrompt,
available_tools: Vec<ToolSpec>,
native_web_search: Option<ProviderNativeWebSearchRequest>,
) -> ProviderTurnRequest {
let (system_blocks, context_blocks) = build_prompt_content_blocks(
&effective_prompt.system_sections,
&effective_prompt.context_sections,
);
ProviderTurnRequest {
prompt_frame: ProviderPromptFrame::structured(
effective_prompt.rendered_system_prompt.clone(),
system_blocks,
context_blocks.clone(),
Some(prompt_cache_from_effective_prompt(effective_prompt)),
),
conversation: vec![ConversationMessage::UserBlocks(context_blocks)],
tools: available_tools,
native_web_search,
}
}
pub fn build_continuation_request(
prompt_frame: ProviderPromptFrame,
conversation: Vec<ConversationMessage>,
available_tools: Vec<ToolSpec>,
native_web_search: Option<ProviderNativeWebSearchRequest>,
) -> ProviderTurnRequest {
ProviderTurnRequest {
prompt_frame,
conversation,
tools: available_tools,
native_web_search,
}
}
fn prompt_cache_from_effective_prompt(effective_prompt: &EffectivePrompt) -> ProviderPromptCache {
ProviderPromptCache {
agent_id: effective_prompt.cache_identity.agent_id.clone(),
prompt_cache_key: effective_prompt.cache_identity.prompt_cache_key.clone(),
context_fingerprint: effective_prompt.cache_identity.context_fingerprint.clone(),
working_memory_revision: effective_prompt.cache_identity.working_memory_revision,
compression_epoch: effective_prompt.cache_identity.compression_epoch,
}
}
fn build_prompt_content_blocks(
system_sections: &[PromptSection],
context_sections: &[PromptSection],
) -> (Vec<PromptContentBlock>, Vec<PromptContentBlock>) {
let mut system_blocks = section_slice_to_prompt_blocks(system_sections);
let mut context_blocks = section_slice_to_prompt_blocks(context_sections);
mark_last_cache_breakpoint(
&mut system_blocks,
&mut context_blocks,
PromptStability::Stable,
);
mark_last_cache_breakpoint(
&mut system_blocks,
&mut context_blocks,
PromptStability::AgentScoped,
);
(system_blocks, context_blocks)
}
fn section_slice_to_prompt_blocks(sections: &[PromptSection]) -> Vec<PromptContentBlock> {
sections
.iter()
.enumerate()
.map(|(index, section)| section_to_prompt_block(section, index + 1 != sections.len()))
.collect()
}
fn section_to_prompt_block(
section: &PromptSection,
preserve_section_delimiter: bool,
) -> PromptContentBlock {
let mut text = render_section(section);
if preserve_section_delimiter {
text.push_str("\n\n");
}
PromptContentBlock {
text,
stability: section.stability,
cache_breakpoint: false,
}
}
fn mark_last_cache_breakpoint(
system_blocks: &mut [PromptContentBlock],
context_blocks: &mut [PromptContentBlock],
stability: PromptStability,
) {
if let Some(block) = context_blocks
.iter_mut()
.rev()
.find(|block| block.stability == stability)
{
block.cache_breakpoint = true;
return;
}
if let Some(block) = system_blocks
.iter_mut()
.rev()
.find(|block| block.stability == stability)
{
block.cache_breakpoint = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
prompt::{PromptCacheIdentity, PromptSection, PromptStability},
system::ExecutionProfile,
system::ExecutionSnapshot,
types::{
AgentIdentityView, AgentKind, AgentOwnership, AgentProfilePreset, AgentRegistryStatus,
AgentVisibility, LoadedAgentsMd,
},
};
use std::path::PathBuf;
#[test]
fn build_provider_turn_request_preserves_supplied_tool_list() {
let tools = vec![
ToolSpec {
name: "Enqueue".to_string(),
description: "Enqueue a message".to_string(),
input_schema: serde_json::json!({}),
freeform_grammar: None,
},
ToolSpec {
name: "SpawnAgent".to_string(),
description: "Spawn an agent".to_string(),
input_schema: serde_json::json!({}),
freeform_grammar: None,
},
ToolSpec {
name: "Sleep".to_string(),
description: "Sleep".to_string(),
input_schema: serde_json::json!({}),
freeform_grammar: None,
},
];
let request = build_provider_turn_request(&fixture_prompt(), tools.clone(), None);
assert_eq!(
request
.tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>(),
tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn build_provider_turn_request_constructs_valid_request() {
let effective_prompt = fixture_prompt();
let tools = vec![ToolSpec {
name: "Sleep".to_string(),
description: "Sleep".to_string(),
input_schema: serde_json::json!({}),
freeform_grammar: None,
}];
let request = build_provider_turn_request(&effective_prompt, tools, None);
assert_eq!(request.prompt_frame.system_prompt, "system prompt");
assert_eq!(request.prompt_frame.system_blocks.len(), 0);
assert_eq!(request.conversation.len(), 1);
assert_eq!(request.tools.len(), 1);
assert_eq!(
request
.prompt_frame
.cache
.as_ref()
.map(|cache| cache.prompt_cache_key.as_str()),
Some("default")
);
assert_eq!(
request
.prompt_frame
.cache
.as_ref()
.map(|cache| cache.working_memory_revision),
Some(4)
);
assert_eq!(
request
.prompt_frame
.cache
.as_ref()
.map(|cache| cache.context_fingerprint.as_str()),
Some("fingerprint-default")
);
assert_eq!(
request
.prompt_frame
.cache
.as_ref()
.map(|cache| cache.compression_epoch),
Some(2)
);
}
fn fixture_prompt() -> EffectivePrompt {
EffectivePrompt {
identity: AgentIdentityView {
agent_id: "default".into(),
kind: AgentKind::Default,
visibility: AgentVisibility::Public,
ownership: AgentOwnership::SelfOwned,
profile_preset: AgentProfilePreset::PublicNamed,
status: AgentRegistryStatus::Active,
is_default_agent: true,
parent_agent_id: None,
lineage_parent_agent_id: None,
delegated_from_task_id: None,
},
agent_home: PathBuf::from("/tmp/agent-home"),
execution: ExecutionSnapshot {
profile: ExecutionProfile::default(),
policy: ExecutionProfile::default().policy_snapshot(),
attached_workspaces: vec![],
workspace_id: None,
workspace_anchor: PathBuf::from("/tmp/agent-home"),
execution_root: PathBuf::from("/tmp/agent-home"),
cwd: PathBuf::from("/tmp/agent-home"),
execution_root_id: None,
projection_kind: None,
access_mode: None,
worktree_root: None,
},
loaded_agents_md: LoadedAgentsMd::default(),
cache_identity: PromptCacheIdentity {
agent_id: "default".into(),
prompt_cache_key: "default".into(),
context_fingerprint: "fingerprint-default".into(),
working_memory_revision: 4,
compression_epoch: 2,
},
system_sections: vec![],
context_sections: vec![PromptSection {
name: "test".to_string(),
id: "test".to_string(),
content: "test content".to_string(),
stability: PromptStability::AgentScoped,
}],
rendered_system_prompt: "system prompt".to_string(),
rendered_context_attachment: "context attachment".to_string(),
}
}
#[test]
fn build_provider_turn_request_marks_last_stable_and_agent_scoped_breakpoints() {
let effective_prompt = EffectivePrompt {
identity: AgentIdentityView {
agent_id: "default".into(),
kind: AgentKind::Default,
visibility: AgentVisibility::Public,
ownership: AgentOwnership::SelfOwned,
profile_preset: AgentProfilePreset::PublicNamed,
status: AgentRegistryStatus::Active,
is_default_agent: true,
parent_agent_id: None,
lineage_parent_agent_id: None,
delegated_from_task_id: None,
},
agent_home: PathBuf::from("/tmp/agent-home"),
execution: ExecutionSnapshot {
profile: ExecutionProfile::default(),
policy: ExecutionProfile::default().policy_snapshot(),
attached_workspaces: vec![],
workspace_id: None,
workspace_anchor: PathBuf::from("/tmp/agent-home"),
execution_root: PathBuf::from("/tmp/agent-home"),
cwd: PathBuf::from("/tmp/agent-home"),
execution_root_id: None,
projection_kind: None,
access_mode: None,
worktree_root: None,
},
loaded_agents_md: LoadedAgentsMd::default(),
cache_identity: PromptCacheIdentity {
agent_id: "default".into(),
prompt_cache_key: "default".into(),
context_fingerprint: "fingerprint-default".into(),
working_memory_revision: 7,
compression_epoch: 3,
},
system_sections: vec![
PromptSection {
name: "identity".to_string(),
id: "identity".to_string(),
content: "stable system".to_string(),
stability: PromptStability::Stable,
},
PromptSection {
name: "policy".to_string(),
id: "policy".to_string(),
content: "agent system".to_string(),
stability: PromptStability::AgentScoped,
},
],
context_sections: vec![
PromptSection {
name: "memory".to_string(),
id: "memory".to_string(),
content: "stable context".to_string(),
stability: PromptStability::Stable,
},
PromptSection {
name: "working_memory".to_string(),
id: "working_memory".to_string(),
content: "agent context".to_string(),
stability: PromptStability::AgentScoped,
},
PromptSection {
name: "current_input".to_string(),
id: "current_input".to_string(),
content: "turn input".to_string(),
stability: PromptStability::TurnScoped,
},
],
rendered_system_prompt: "system prompt".to_string(),
rendered_context_attachment: "context attachment".to_string(),
};
let request = build_provider_turn_request(&effective_prompt, Vec::new(), None);
let system_blocks = request.prompt_frame.system_blocks;
assert_eq!(system_blocks[0].text, "## identity\nstable system\n\n");
assert_eq!(system_blocks[1].text, "## policy\nagent system");
assert_eq!(
system_blocks
.iter()
.filter(|block| block.cache_breakpoint)
.count(),
0
);
let ConversationMessage::UserBlocks(context_blocks) = &request.conversation[0] else {
panic!("expected user blocks");
};
assert_eq!(context_blocks[0].text, "## memory\nstable context\n\n");
assert_eq!(
context_blocks[1].text,
"## working_memory\nagent context\n\n"
);
assert_eq!(context_blocks[2].text, "## current_input\nturn input");
let flagged = context_blocks
.iter()
.filter(|block| block.cache_breakpoint)
.map(|block| block.text.as_str())
.collect::<Vec<_>>();
assert_eq!(flagged.len(), 2);
assert!(flagged.iter().any(|text| text.contains("stable context")));
assert!(flagged.iter().any(|text| text.contains("agent context")));
}
#[test]
fn build_continuation_request_preserves_conversation_history() {
let conversation = vec![
ConversationMessage::UserText("First message".to_string()),
ConversationMessage::AssistantBlocks(vec![]),
];
let tools = vec![ToolSpec {
name: "Sleep".to_string(),
description: "Sleep".to_string(),
input_schema: serde_json::json!({}),
freeform_grammar: None,
}];
let prompt_frame = ProviderPromptFrame::structured(
"system",
vec![PromptContentBlock {
text: "structured system".to_string(),
stability: PromptStability::Stable,
cache_breakpoint: true,
}],
vec![PromptContentBlock {
text: "structured context".to_string(),
stability: PromptStability::AgentScoped,
cache_breakpoint: true,
}],
Some(ProviderPromptCache {
agent_id: "default".to_string(),
prompt_cache_key: "default".to_string(),
context_fingerprint: "fingerprint-default".to_string(),
working_memory_revision: 4,
compression_epoch: 2,
}),
);
let request = build_continuation_request(prompt_frame, conversation, tools, None);
assert_eq!(request.prompt_frame.system_prompt, "system");
assert_eq!(request.prompt_frame.system_blocks.len(), 1);
assert_eq!(request.prompt_frame.context_blocks.len(), 1);
assert!(request.prompt_frame.system_blocks[0].cache_breakpoint);
assert!(request.prompt_frame.context_blocks[0].cache_breakpoint);
assert_eq!(
request
.prompt_frame
.cache
.as_ref()
.map(|cache| cache.compression_epoch),
Some(2)
);
assert_eq!(request.conversation.len(), 2);
assert_eq!(request.tools.len(), 1);
}
}