Skip to main content

bamboo_server_tools/
compact.rs

1use async_trait::async_trait;
2use bamboo_agent_core::tools::{Tool, ToolError, ToolExecutionContext, ToolResult};
3use serde::Deserialize;
4use serde_json::json;
5
6/// Server-side tool for manually triggering conversation context compression.
7///
8/// Unlike automatic compression (which triggers based on token thresholds), this
9/// tool lets the model or user proactively compress history at natural task boundaries.
10/// It bypasses the exposure gate and always forces a compression cycle.
11pub 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        // No required fields — instructions is optional
186        assert!(
187            schema.get("required").is_none(),
188            "instructions should be optional"
189        );
190    }
191}