use rmcp::ErrorData;
use rmcp::model::Tool;
use serde_json::{Map, Value, json};
use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str, get_usize};
use crate::tool_defs::tool_def;
use crate::tools::ctx_transcript_compact::{compact_messages, render_result, serialize_transcript};
const DEFAULT_FRESH_TAIL_TOKENS: usize = 4_000;
const DEFAULT_PROTECT_MIN_MESSAGES: usize = 6;
const OFFLOAD_MAX_CHARS: usize = 8_000;
pub struct CtxTranscriptCompactTool;
impl McpTool for CtxTranscriptCompactTool {
fn name(&self) -> &'static str {
"ctx_transcript_compact"
}
fn tool_def(&self) -> Tool {
tool_def(
"ctx_transcript_compact",
"Compact an OpenAI-format message array deterministically:\n\
keep system + fresh tail verbatim, replace older turns with a recoverable\n\
summary, offload raw turns into session memory (indexed for recall).\n\
Returns JSON {messages, stats}. tool_call/tool_result pairs never split.",
json!({
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": { "type": "object" },
"description": "OpenAI message array to compact"
},
"fresh_tail_tokens": {
"type": "integer",
"description": "Recent tokens to keep verbatim"
},
"protect_min_messages": {
"type": "integer",
"description": "Minimum recent messages to keep"
},
"focus_topic": {
"type": "string",
"description": "Topic to prioritise in summary"
}
},
"required": ["messages"]
}),
)
}
fn handle(
&self,
args: &Map<String, Value>,
ctx: &ToolContext,
) -> Result<ToolOutput, ErrorData> {
let messages = args
.get("messages")
.and_then(Value::as_array)
.ok_or_else(|| ErrorData::invalid_params("messages (array) is required", None))?;
let fresh_tail = get_usize(args, "fresh_tail_tokens").unwrap_or(DEFAULT_FRESH_TAIL_TOKENS);
let protect_min =
get_usize(args, "protect_min_messages").unwrap_or(DEFAULT_PROTECT_MIN_MESSAGES);
let focus = get_str(args, "focus_topic");
let result = compact_messages(messages.clone(), fresh_tail, protect_min, focus.as_deref());
let offload_target = if result.did_compact && !result.summarized.is_empty() {
ctx.session.as_ref()
} else {
None
};
if let Some(session_handle) = offload_target {
let digest = serialize_transcript(&result.summarized, OFFLOAD_MAX_CHARS);
if !digest.is_empty() {
let mut session = session_handle.blocking_write();
let _ = crate::tools::ctx_session::handle(
&mut session,
&[],
"finding",
Some(&digest),
None,
crate::tools::ctx_session::SessionToolOptions {
format: None,
path: None,
write: false,
privacy: None,
terse: Some(true),
},
);
}
}
let saved = result
.original_tokens
.saturating_sub(result.compacted_tokens);
Ok(ToolOutput {
text: render_result(&result),
original_tokens: result.original_tokens,
saved_tokens: saved,
mode: Some("transcript_compact".to_string()),
path: None,
changed: false,
shell_outcome: None,
})
}
}