claude_agent/context/
static_context.rs1use crate::mcp::make_mcp_name;
7use crate::types::{CacheTtl, SystemBlock, ToolDefinition};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, Default)]
11pub struct StaticContext {
12 pub system_prompt: String,
13 pub claude_md: String,
14 pub skill_index_summary: String,
15 pub rules_summary: String,
16 pub tool_definitions: Vec<ToolDefinition>,
17 pub mcp_tool_metadata: Vec<McpToolMeta>,
18}
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct McpToolMeta {
22 pub server: String,
23 pub name: String,
24 pub description: String,
25}
26
27impl StaticContext {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
33 self.system_prompt = prompt.into();
34 self
35 }
36
37 pub fn with_claude_md(mut self, content: impl Into<String>) -> Self {
38 self.claude_md = content.into();
39 self
40 }
41
42 pub fn with_skill_summary(mut self, summary: impl Into<String>) -> Self {
43 self.skill_index_summary = summary.into();
44 self
45 }
46
47 pub fn with_rules_summary(mut self, summary: impl Into<String>) -> Self {
48 self.rules_summary = summary.into();
49 self
50 }
51
52 pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
53 self.tool_definitions = tools;
54 self
55 }
56
57 pub fn with_mcp_tools(mut self, tools: Vec<McpToolMeta>) -> Self {
58 self.mcp_tool_metadata = tools;
59 self
60 }
61
62 pub fn to_system_blocks(&self) -> Vec<SystemBlock> {
68 let mut blocks = Vec::new();
69 let ttl = CacheTtl::OneHour;
70
71 if !self.system_prompt.is_empty() {
72 blocks.push(SystemBlock::cached_with_ttl(&self.system_prompt, ttl));
73 }
74
75 if !self.claude_md.is_empty() {
76 blocks.push(SystemBlock::cached_with_ttl(&self.claude_md, ttl));
77 }
78
79 if !self.skill_index_summary.is_empty() {
80 blocks.push(SystemBlock::cached_with_ttl(&self.skill_index_summary, ttl));
81 }
82
83 if !self.rules_summary.is_empty() {
84 blocks.push(SystemBlock::cached_with_ttl(&self.rules_summary, ttl));
85 }
86
87 if !self.mcp_tool_metadata.is_empty() {
88 blocks.push(SystemBlock::cached_with_ttl(self.format_mcp_summary(), ttl));
89 }
90
91 blocks
92 }
93
94 fn format_mcp_summary(&self) -> String {
95 let mut lines = vec!["# MCP Server Tools".to_string()];
96 for tool in &self.mcp_tool_metadata {
97 lines.push(format!(
98 "- {}: {}",
99 make_mcp_name(&tool.server, &tool.name),
100 tool.description
101 ));
102 }
103 lines.join("\n")
104 }
105
106 pub fn content_hash(&self) -> String {
107 use std::collections::hash_map::DefaultHasher;
108 use std::hash::{Hash, Hasher};
109
110 let mut hasher = DefaultHasher::new();
111 self.system_prompt.hash(&mut hasher);
112 self.claude_md.hash(&mut hasher);
113 self.skill_index_summary.hash(&mut hasher);
114 self.rules_summary.hash(&mut hasher);
115
116 for tool in &self.tool_definitions {
117 tool.name.hash(&mut hasher);
118 }
119
120 for mcp in &self.mcp_tool_metadata {
121 mcp.server.hash(&mut hasher);
122 mcp.name.hash(&mut hasher);
123 }
124
125 format!("{:016x}", hasher.finish())
126 }
127
128 pub fn estimate_tokens(&self) -> u64 {
129 let total_chars = self.system_prompt.len()
130 + self.claude_md.len()
131 + self.skill_index_summary.len()
132 + self.rules_summary.len()
133 + self
134 .mcp_tool_metadata
135 .iter()
136 .map(|t| t.description.len())
137 .sum::<usize>();
138
139 (total_chars / 4) as u64
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::types::CacheType;
147
148 #[test]
149 fn test_system_block_cached_with_ttl() {
150 let block = SystemBlock::cached_with_ttl("Hello", CacheTtl::OneHour);
151 assert!(block.cache_control.is_some());
152 let cache_ctrl = block.cache_control.unwrap();
153 assert_eq!(cache_ctrl.cache_type, CacheType::Ephemeral);
154 assert_eq!(cache_ctrl.ttl, Some(CacheTtl::OneHour));
155 assert_eq!(block.block_type, "text");
156 }
157
158 #[test]
159 fn test_static_context_blocks() {
160 let static_context = StaticContext::new()
161 .with_system_prompt("You are a helpful assistant")
162 .with_claude_md("# Project\nThis is a Rust project");
163
164 let blocks = static_context.to_system_blocks();
165 assert_eq!(blocks.len(), 2);
166 assert!(blocks[0].text.contains("helpful assistant"));
167 assert!(blocks[1].text.contains("Rust project"));
168 }
169
170 #[test]
171 fn test_content_hash_consistency() {
172 let ctx1 = StaticContext::new()
173 .with_system_prompt("Same prompt")
174 .with_claude_md("Same content");
175
176 let ctx2 = StaticContext::new()
177 .with_system_prompt("Same prompt")
178 .with_claude_md("Same content");
179
180 assert_eq!(ctx1.content_hash(), ctx2.content_hash());
181 }
182
183 #[test]
184 fn test_content_hash_different() {
185 let ctx1 = StaticContext::new().with_system_prompt("Prompt A");
186 let ctx2 = StaticContext::new().with_system_prompt("Prompt B");
187
188 assert_ne!(ctx1.content_hash(), ctx2.content_hash());
189 }
190}