use async_trait::async_trait;
use bamboo_agent_core::tools::{Tool, ToolError, ToolExecutionContext, ToolResult};
use serde::Deserialize;
use serde_json::json;
pub struct CompactContextTool;
#[derive(Debug, Deserialize)]
struct CompactContextArgs {
#[serde(default)]
instructions: Option<String>,
}
#[async_trait]
impl Tool for CompactContextTool {
fn name(&self) -> &str {
"compact_context"
}
fn description(&self) -> &str {
"Manually compress conversation history to free up context window space. \
Use at natural task boundaries (after finishing a feature, before starting a new topic). \
Optionally provide custom instructions to control what the summary focuses on."
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"instructions": {
"type": "string",
"description": "Optional custom instructions for what to focus on in the summary. \
Examples: 'Preserve variable names and function signatures', \
'Focus on open issues and blockers', 'Keep only active task status'"
}
}
})
}
async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
let parsed: CompactContextArgs = serde_json::from_value(args.clone()).map_err(|e| {
ToolError::Execution(format!("invalid arguments for compact_context: {e}"))
})?;
let instructions_note = parsed
.instructions
.as_deref()
.filter(|s| !s.is_empty())
.map(|i| format!(" with instructions: {i}"))
.unwrap_or_default();
Ok(ToolResult {
success: true,
result: format!(
"Context compression requested{}. Compression will be applied before the next turn.",
instructions_note
),
display_preference: Some("Collapsible".to_string()),
})
}
async fn execute_with_context(
&self,
args: serde_json::Value,
_ctx: ToolExecutionContext<'_>,
) -> Result<ToolResult, ToolError> {
self.execute(args).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn execute_without_instructions() {
let tool = CompactContextTool;
let result = tool
.execute(serde_json::json!({}))
.await
.expect("execute should succeed");
assert!(result.success);
assert_eq!(
result.result,
"Context compression requested. Compression will be applied before the next turn."
);
assert_eq!(result.display_preference.as_deref(), Some("Collapsible"));
}
#[tokio::test]
async fn execute_with_instructions() {
let tool = CompactContextTool;
let result = tool
.execute(serde_json::json!({
"instructions": "Preserve all function signatures"
}))
.await
.expect("execute should succeed");
assert!(result.success);
assert!(result
.result
.contains("with instructions: Preserve all function signatures"));
}
#[tokio::test]
async fn execute_with_empty_instructions() {
let tool = CompactContextTool;
let result = tool
.execute(serde_json::json!({
"instructions": ""
}))
.await
.expect("execute should succeed");
assert!(result.success);
assert!(!result.result.contains("with instructions"));
}
#[tokio::test]
async fn execute_with_null_instructions() {
let tool = CompactContextTool;
let result = tool
.execute(serde_json::json!({
"instructions": null
}))
.await
.expect("execute should succeed");
assert!(result.success);
assert!(!result.result.contains("with instructions"));
}
#[tokio::test]
async fn execute_with_invalid_args_returns_error() {
let tool = CompactContextTool;
let result = tool.execute(serde_json::json!("not an object")).await;
assert!(result.is_err());
match result.unwrap_err() {
ToolError::Execution(msg) => assert!(msg.contains("invalid arguments")),
other => panic!("expected Execution error, got: {other:?}"),
}
}
#[tokio::test]
async fn execute_with_context_delegates_to_execute() {
let tool = CompactContextTool;
let result = tool
.execute_with_context(
serde_json::json!({"instructions": "keep task status"}),
ToolExecutionContext::none("call_123"),
)
.await
.expect("execute_with_context should succeed");
assert!(result.success);
assert!(result.result.contains("keep task status"));
}
#[test]
fn tool_name_is_compact_context() {
let tool = CompactContextTool;
assert_eq!(tool.name(), "compact_context");
}
#[test]
fn parameters_schema_has_instructions_field() {
let tool = CompactContextTool;
let schema = tool.parameters_schema();
let props = schema
.get("properties")
.expect("schema should have properties");
assert!(
props.get("instructions").is_some(),
"should have instructions property"
);
assert!(
schema.get("required").is_none(),
"instructions should be optional"
);
}
}