bamboo_server/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 })
65 }
66
67 async fn execute_with_context(
68 &self,
69 args: serde_json::Value,
70 _ctx: ToolExecutionContext<'_>,
71 ) -> Result<ToolResult, ToolError> {
72 self.execute(args).await
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[tokio::test]
81 async fn execute_without_instructions() {
82 let tool = CompactContextTool;
83 let result = tool
84 .execute(serde_json::json!({}))
85 .await
86 .expect("execute should succeed");
87
88 assert!(result.success);
89 assert_eq!(
90 result.result,
91 "Context compression requested. Compression will be applied before the next turn."
92 );
93 assert_eq!(result.display_preference.as_deref(), Some("Collapsible"));
94 }
95
96 #[tokio::test]
97 async fn execute_with_instructions() {
98 let tool = CompactContextTool;
99 let result = tool
100 .execute(serde_json::json!({
101 "instructions": "Preserve all function signatures"
102 }))
103 .await
104 .expect("execute should succeed");
105
106 assert!(result.success);
107 assert!(result
108 .result
109 .contains("with instructions: Preserve all function signatures"));
110 }
111
112 #[tokio::test]
113 async fn execute_with_empty_instructions() {
114 let tool = CompactContextTool;
115 let result = tool
116 .execute(serde_json::json!({
117 "instructions": ""
118 }))
119 .await
120 .expect("execute should succeed");
121
122 assert!(result.success);
123 assert!(!result.result.contains("with instructions"));
124 }
125
126 #[tokio::test]
127 async fn execute_with_null_instructions() {
128 let tool = CompactContextTool;
129 let result = tool
130 .execute(serde_json::json!({
131 "instructions": null
132 }))
133 .await
134 .expect("execute should succeed");
135
136 assert!(result.success);
137 assert!(!result.result.contains("with instructions"));
138 }
139
140 #[tokio::test]
141 async fn execute_with_invalid_args_returns_error() {
142 let tool = CompactContextTool;
143 let result = tool.execute(serde_json::json!("not an object")).await;
144
145 assert!(result.is_err());
146 match result.unwrap_err() {
147 ToolError::Execution(msg) => assert!(msg.contains("invalid arguments")),
148 other => panic!("expected Execution error, got: {other:?}"),
149 }
150 }
151
152 #[tokio::test]
153 async fn execute_with_context_delegates_to_execute() {
154 let tool = CompactContextTool;
155 let result = tool
156 .execute_with_context(
157 serde_json::json!({"instructions": "keep task status"}),
158 ToolExecutionContext::none("call_123"),
159 )
160 .await
161 .expect("execute_with_context should succeed");
162
163 assert!(result.success);
164 assert!(result.result.contains("keep task status"));
165 }
166
167 #[test]
168 fn tool_name_is_compact_context() {
169 let tool = CompactContextTool;
170 assert_eq!(tool.name(), "compact_context");
171 }
172
173 #[test]
174 fn parameters_schema_has_instructions_field() {
175 let tool = CompactContextTool;
176 let schema = tool.parameters_schema();
177 let props = schema
178 .get("properties")
179 .expect("schema should have properties");
180 assert!(
181 props.get("instructions").is_some(),
182 "should have instructions property"
183 );
184 assert!(
186 schema.get("required").is_none(),
187 "instructions should be optional"
188 );
189 }
190}