use serde_json::{Value, json};
use talos_core::provider::ToolDefinition;
#[derive(Debug, Clone)]
pub struct SystemPrompt {
full_text: String,
cache_control_breakpoints: Vec<usize>,
}
impl SystemPrompt {
#[must_use]
pub fn full_text(&self) -> &str {
&self.full_text
}
#[must_use]
pub fn cache_control_breakpoints(&self) -> &[usize] {
&self.cache_control_breakpoints
}
#[must_use]
pub fn to_anthropic_format(&self) -> Value {
let mut blocks: Vec<Value> = Vec::new();
let text = &self.full_text;
let mut prev_pos = 0;
for &bp in &self.cache_control_breakpoints {
let end = bp.min(text.len());
if end > prev_pos {
let section_text = text[prev_pos..end].to_string();
blocks.push(json!({
"type": "text",
"text": section_text,
"cache_control": {"type": "ephemeral"}
}));
}
prev_pos = end;
}
if prev_pos < text.len() {
let remaining = text[prev_pos..].to_string();
blocks.push(json!({
"type": "text",
"text": remaining
}));
}
if blocks.is_empty() {
blocks.push(json!({
"type": "text",
"text": text
}));
}
json!({
"system": blocks
})
}
}
#[derive(Debug)]
pub struct PromptCache {
cache_hits: u64,
cache_checks: u64,
}
impl PromptCache {
#[must_use]
pub fn new() -> Self {
Self {
cache_hits: 0,
cache_checks: 0,
}
}
#[must_use]
pub fn build_system_prompt(
&self,
identity: &str,
tools: &[ToolDefinition],
context: &str,
) -> SystemPrompt {
let mut sorted_tools: Vec<&ToolDefinition> = tools.iter().collect();
sorted_tools.sort_by(|a, b| a.name.cmp(&b.name));
let identity_section = format!("# Identity\n{identity}\n");
let tools_section = if sorted_tools.is_empty() {
String::from("# Tools\nNo tools available.\n")
} else {
let mut section = String::from("# Tools\n");
for tool in &sorted_tools {
section.push_str(&tool.to_prompt_text());
section.push_str("\n\n");
}
section
};
let context_section = if context.is_empty() {
String::from("# Context\nNo context files loaded.\n")
} else {
format!("# Context\n{context}\n")
};
let static_prefix = format!("{identity_section}\n{tools_section}\n{context_section}");
let bp1 = identity_section.len();
let bp2 = bp1 + 1 + tools_section.len(); let bp3 = bp2 + 1 + context_section.len();
SystemPrompt {
full_text: static_prefix,
cache_control_breakpoints: vec![bp1, bp2, bp3],
}
}
pub fn track_cache_hit_rate(&mut self, hit: bool) {
self.cache_checks += 1;
if hit {
self.cache_hits += 1;
}
}
#[must_use]
pub fn cache_hit_rate(&self) -> f64 {
if self.cache_checks == 0 {
0.0
} else {
(self.cache_hits as f64 / self.cache_checks as f64) * 100.0
}
}
}
impl Default for PromptCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(warnings)]
mod tests {
use super::*;
#[test]
fn test_system_prompt_has_static_prefix() {
let cache = PromptCache::new();
let tools = vec![ToolDefinition::new(
"bash",
"Execute shell commands",
json!({}),
)];
let prompt = cache.build_system_prompt("You are an assistant.", &tools, "Context here.");
assert!(prompt.full_text().contains("# Identity"));
assert!(prompt.full_text().contains("You are an assistant."));
assert!(prompt.full_text().contains("# Tools"));
assert!(prompt.full_text().contains("# Context"));
assert!(prompt.full_text().contains("Context here."));
}
#[test]
fn test_system_prompt_static_prefix_is_consistent() {
let cache = PromptCache::new();
let tools = vec![ToolDefinition::new(
"bash",
"Execute shell commands",
json!({}),
)];
let prompt1 = cache.build_system_prompt("You are an assistant.", &tools, "Context here.");
let prompt2 = cache.build_system_prompt("You are an assistant.", &tools, "Context here.");
assert_eq!(prompt1.full_text(), prompt2.full_text());
assert_eq!(
prompt1.cache_control_breakpoints(),
prompt2.cache_control_breakpoints()
);
}
#[test]
fn test_cache_control_breakpoints_at_correct_positions() {
let cache = PromptCache::new();
let prompt = cache.build_system_prompt("Identity text.", &[], "");
let breakpoints = prompt.cache_control_breakpoints();
assert_eq!(breakpoints.len(), 3);
let bp1 = breakpoints[0];
assert!(prompt.full_text()[..bp1].contains("# Identity"));
assert!(prompt.full_text()[..bp1].contains("Identity text."));
let bp2 = breakpoints[1];
assert!(prompt.full_text()[..bp2].contains("# Tools"));
let bp3 = breakpoints[2];
assert!(prompt.full_text()[..bp3].contains("# Context"));
assert!(bp3 <= prompt.full_text().len());
}
#[test]
fn test_breakpoints_are_increasing() {
let cache = PromptCache::new();
let tools = vec![ToolDefinition::new("bash", "Execute commands", json!({}))];
let prompt = cache.build_system_prompt("Identity.", &tools, "Context.");
let bps = prompt.cache_control_breakpoints();
assert!(bps[0] < bps[1]);
assert!(bps[1] < bps[2]);
}
#[test]
fn test_tool_definitions_sorted_by_name() {
let cache = PromptCache::new();
let tools = vec![
ToolDefinition::new("write", "Write a file", json!({})),
ToolDefinition::new("bash", "Execute commands", json!({})),
ToolDefinition::new("read", "Read a file", json!({})),
];
let prompt = cache.build_system_prompt("Identity.", &tools, "");
let text = prompt.full_text();
let bash_pos = text.find("## bash").expect("bash should be present");
let read_pos = text.find("## read").expect("read should be present");
let write_pos = text.find("## write").expect("write should be present");
assert!(bash_pos < read_pos, "bash should come before read");
assert!(read_pos < write_pos, "read should come before write");
}
#[test]
fn test_empty_tools_list() {
let cache = PromptCache::new();
let prompt = cache.build_system_prompt("Identity.", &[], "");
assert!(prompt.full_text().contains("No tools available."));
assert_eq!(prompt.cache_control_breakpoints().len(), 3);
}
#[test]
fn test_empty_context() {
let cache = PromptCache::new();
let prompt = cache.build_system_prompt("Identity.", &[], "");
assert!(prompt.full_text().contains("No context files loaded."));
}
#[test]
fn test_to_anthropic_format_produces_valid_json() {
let cache = PromptCache::new();
let tools = vec![ToolDefinition::new("bash", "Execute commands", json!({}))];
let prompt = cache.build_system_prompt("Identity.", &tools, "Context.");
let anthropic = prompt.to_anthropic_format();
assert!(anthropic.get("system").is_some());
let system_blocks = anthropic["system"]
.as_array()
.expect("operation should succeed");
assert!(!system_blocks.is_empty());
}
#[test]
fn test_to_anthropic_format_has_cache_control_markers() {
let cache = PromptCache::new();
let prompt = cache.build_system_prompt("Identity.", &[], "");
let anthropic = prompt.to_anthropic_format();
let system_blocks = anthropic["system"]
.as_array()
.expect("operation should succeed");
let cached_blocks: Vec<_> = system_blocks
.iter()
.filter(|b| b.get("cache_control").is_some())
.collect();
assert!(
cached_blocks.len() >= 3,
"Expected at least 3 cached blocks, got {}",
cached_blocks.len()
);
for block in &cached_blocks {
let cc = block["cache_control"]
.as_object()
.expect("operation should succeed");
assert_eq!(
cc.get("type")
.expect("operation should succeed")
.as_str()
.expect("operation should succeed"),
"ephemeral"
);
}
}
#[test]
fn test_to_anthropic_format_last_block_uncached() {
let cache = PromptCache::new();
let prompt = cache.build_system_prompt("Identity.", &[], "Context.");
let anthropic = prompt.to_anthropic_format();
let system_blocks = anthropic["system"]
.as_array()
.expect("operation should succeed");
let last_block = system_blocks.last().expect("operation should succeed");
assert!(last_block.get("cache_control").is_some());
}
#[test]
fn test_to_anthropic_format_with_empty_prompt() {
let prompt = SystemPrompt {
full_text: String::new(),
cache_control_breakpoints: vec![],
};
let anthropic = prompt.to_anthropic_format();
let system_blocks = anthropic["system"]
.as_array()
.expect("operation should succeed");
assert_eq!(system_blocks.len(), 1);
assert_eq!(system_blocks[0]["text"], "");
assert!(system_blocks[0].get("cache_control").is_none());
}
#[test]
fn test_cache_hit_rate_initially_zero() {
let cache = PromptCache::new();
assert_eq!(cache.cache_hit_rate(), 0.0);
}
#[test]
fn test_cache_hit_rate_after_hits() {
let mut cache = PromptCache::new();
cache.track_cache_hit_rate(true);
cache.track_cache_hit_rate(true);
cache.track_cache_hit_rate(false);
cache.track_cache_hit_rate(true);
assert!((cache.cache_hit_rate() - 75.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_hit_rate_all_hits() {
let mut cache = PromptCache::new();
cache.track_cache_hit_rate(true);
cache.track_cache_hit_rate(true);
assert!((cache.cache_hit_rate() - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_hit_rate_all_misses() {
let mut cache = PromptCache::new();
cache.track_cache_hit_rate(false);
cache.track_cache_hit_rate(false);
assert!((cache.cache_hit_rate() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_hit_rate_single_hit() {
let mut cache = PromptCache::new();
cache.track_cache_hit_rate(true);
assert!((cache.cache_hit_rate() - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_hit_rate_single_miss() {
let mut cache = PromptCache::new();
cache.track_cache_hit_rate(false);
assert!((cache.cache_hit_rate() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_tool_definition_to_prompt_text() {
let tool = ToolDefinition::new(
"read_file",
"Read the contents of a file",
json!({
"type": "object",
"properties": {
"path": {"type": "string"}
}
}),
);
let text = tool.to_prompt_text();
assert!(text.contains("## read_file"));
assert!(text.contains("Read the contents of a file"));
assert!(text.contains("path"));
}
#[test]
fn test_full_prompt_with_all_sections() {
let cache = PromptCache::new();
let tools = vec![
ToolDefinition::new("bash", "Run shell commands", json!({"command": "string"})),
ToolDefinition::new("read", "Read files", json!({"path": "string"})),
ToolDefinition::new(
"write",
"Write files",
json!({"path": "string", "content": "string"}),
),
];
let identity = "You are Talos, a safety-first agent runtime.";
let context = "# AGENTS.md\nFollow the coding guide.";
let prompt = cache.build_system_prompt(identity, &tools, context);
let anthropic = prompt.to_anthropic_format();
assert!(prompt.full_text().contains("You are Talos"));
assert!(prompt.full_text().contains("## bash"));
assert!(prompt.full_text().contains("## read"));
assert!(prompt.full_text().contains("## write"));
assert!(prompt.full_text().contains("AGENTS.md"));
let system_blocks = anthropic["system"]
.as_array()
.expect("operation should succeed");
assert!(!system_blocks.is_empty());
let text = prompt.full_text();
assert!(
text.find("## bash").expect("operation should succeed")
< text.find("## read").expect("operation should succeed")
);
assert!(
text.find("## read").expect("operation should succeed")
< text.find("## write").expect("operation should succeed")
);
}
#[test]
fn test_prompt_cache_default_trait() {
let cache = PromptCache::default();
assert_eq!(cache.cache_hit_rate(), 0.0);
}
#[test]
fn test_system_prompt_clone() {
let cache = PromptCache::new();
let prompt = cache.build_system_prompt("Identity.", &[], "");
let cloned = prompt.clone();
assert_eq!(prompt.full_text(), cloned.full_text());
assert_eq!(
prompt.cache_control_breakpoints(),
cloned.cache_control_breakpoints()
);
}
#[test]
fn test_tool_definition_clone_and_eq() {
let tool1 = ToolDefinition::new("bash", "Run commands", json!({}));
let tool2 = tool1.clone();
assert_eq!(tool1, tool2);
}
}