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