bamboo_server_tools/
compact.rs1use async_trait::async_trait;
2use bamboo_agent_core::tools::{Tool, ToolError, ToolExecutionContext, ToolResult};
3use serde::Deserialize;
4use serde_json::json;
5
6pub struct CompactContextTool;
12
13#[derive(Debug, Deserialize)]
14struct CompactContextArgs {
15 #[serde(default)]
16 instructions: Option<String>,
17}
18
19#[async_trait]
20impl Tool for CompactContextTool {
21 fn name(&self) -> &str {
22 "compact_context"
23 }
24
25 fn description(&self) -> &str {
26 "Manually compress conversation history to free up context window space. \
27 Use at natural task boundaries (after finishing a feature, before starting a new topic). \
28 Optionally provide custom instructions to control what the summary focuses on."
29 }
30
31 fn parameters_schema(&self) -> serde_json::Value {
32 json!({
33 "type": "object",
34 "properties": {
35 "instructions": {
36 "type": "string",
37 "description": "Optional custom instructions for what to focus on in the summary. \
38 Examples: 'Preserve variable names and function signatures', \
39 'Focus on open issues and blockers', 'Keep only active task status'"
40 }
41 }
42 })
43 }
44
45 async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
46 let parsed: CompactContextArgs = serde_json::from_value(args.clone()).map_err(|e| {
47 ToolError::Execution(format!("invalid arguments for compact_context: {e}"))
48 })?;
49
50 let instructions_note = parsed
51 .instructions
52 .as_deref()
53 .filter(|s| !s.is_empty())
54 .map(|i| format!(" with instructions: {i}"))
55 .unwrap_or_default();
56
57 Ok(ToolResult {
58 success: true,
59 result: format!(
60 "Context compression requested{}. Compression will be applied before the next turn.",
61 instructions_note
62 ),
63 display_preference: Some("Collapsible".to_string()),
64 images: Vec::new(),
65 })
66 }
67
68 async fn execute_with_context(
69 &self,
70 args: serde_json::Value,
71 _ctx: ToolExecutionContext<'_>,
72 ) -> Result<ToolResult, ToolError> {
73 self.execute(args).await
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[tokio::test]
82 async fn execute_without_instructions() {
83 let tool = CompactContextTool;
84 let result = tool
85 .execute(serde_json::json!({}))
86 .await
87 .expect("execute should succeed");
88
89 assert!(result.success);
90 assert_eq!(
91 result.result,
92 "Context compression requested. Compression will be applied before the next turn."
93 );
94 assert_eq!(result.display_preference.as_deref(), Some("Collapsible"));
95 }
96
97 #[tokio::test]
98 async fn execute_with_instructions() {
99 let tool = CompactContextTool;
100 let result = tool
101 .execute(serde_json::json!({
102 "instructions": "Preserve all function signatures"
103 }))
104 .await
105 .expect("execute should succeed");
106
107 assert!(result.success);
108 assert!(result
109 .result
110 .contains("with instructions: Preserve all function signatures"));
111 }
112
113 #[tokio::test]
114 async fn execute_with_empty_instructions() {
115 let tool = CompactContextTool;
116 let result = tool
117 .execute(serde_json::json!({
118 "instructions": ""
119 }))
120 .await
121 .expect("execute should succeed");
122
123 assert!(result.success);
124 assert!(!result.result.contains("with instructions"));
125 }
126
127 #[tokio::test]
128 async fn execute_with_null_instructions() {
129 let tool = CompactContextTool;
130 let result = tool
131 .execute(serde_json::json!({
132 "instructions": null
133 }))
134 .await
135 .expect("execute should succeed");
136
137 assert!(result.success);
138 assert!(!result.result.contains("with instructions"));
139 }
140
141 #[tokio::test]
142 async fn execute_with_invalid_args_returns_error() {
143 let tool = CompactContextTool;
144 let result = tool.execute(serde_json::json!("not an object")).await;
145
146 assert!(result.is_err());
147 match result.unwrap_err() {
148 ToolError::Execution(msg) => assert!(msg.contains("invalid arguments")),
149 other => panic!("expected Execution error, got: {other:?}"),
150 }
151 }
152
153 #[tokio::test]
154 async fn execute_with_context_delegates_to_execute() {
155 let tool = CompactContextTool;
156 let result = tool
157 .execute_with_context(
158 serde_json::json!({"instructions": "keep task status"}),
159 ToolExecutionContext::none("call_123"),
160 )
161 .await
162 .expect("execute_with_context should succeed");
163
164 assert!(result.success);
165 assert!(result.result.contains("keep task status"));
166 }
167
168 #[test]
169 fn tool_name_is_compact_context() {
170 let tool = CompactContextTool;
171 assert_eq!(tool.name(), "compact_context");
172 }
173
174 #[test]
175 fn parameters_schema_has_instructions_field() {
176 let tool = CompactContextTool;
177 let schema = tool.parameters_schema();
178 let props = schema
179 .get("properties")
180 .expect("schema should have properties");
181 assert!(
182 props.get("instructions").is_some(),
183 "should have instructions property"
184 );
185 assert!(
187 schema.get("required").is_none(),
188 "instructions should be optional"
189 );
190 }
191}