lean_ctx/tools/registered/
ctx_transcript_compact.rs1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str, get_usize};
6use crate::tool_defs::tool_def;
7use crate::tools::ctx_transcript_compact::{compact_messages, render_result, serialize_transcript};
8
9const DEFAULT_FRESH_TAIL_TOKENS: usize = 4_000;
10const DEFAULT_PROTECT_MIN_MESSAGES: usize = 6;
11const OFFLOAD_MAX_CHARS: usize = 8_000;
12
13pub struct CtxTranscriptCompactTool;
14
15impl McpTool for CtxTranscriptCompactTool {
16 fn name(&self) -> &'static str {
17 "ctx_transcript_compact"
18 }
19
20 fn tool_def(&self) -> Tool {
21 tool_def(
22 "ctx_transcript_compact",
23 "Compact an OpenAI-format message array deterministically: keep system + a fresh tail verbatim, replace older turns with a recoverable summary, and offload the raw turns into lean-ctx session memory (indexed for ctx_search/ctx_knowledge recall). Built for the Hermes context-engine plugin. Returns JSON {messages, stats}; tool_call/tool_result pairs are never split.",
24 json!({
25 "type": "object",
26 "properties": {
27 "messages": {
28 "type": "array",
29 "items": { "type": "object" },
30 "description": "OpenAI-format message array to compact"
31 },
32 "fresh_tail_tokens": {
33 "type": "integer",
34 "description": "Recent tokens to keep verbatim (default 4000)"
35 },
36 "protect_min_messages": {
37 "type": "integer",
38 "description": "Minimum recent messages to keep verbatim (default 6)"
39 },
40 "focus_topic": {
41 "type": "string",
42 "description": "Optional topic to prioritise in the summary"
43 }
44 },
45 "required": ["messages"]
46 }),
47 )
48 }
49
50 fn handle(
51 &self,
52 args: &Map<String, Value>,
53 ctx: &ToolContext,
54 ) -> Result<ToolOutput, ErrorData> {
55 let messages = args
56 .get("messages")
57 .and_then(Value::as_array)
58 .ok_or_else(|| ErrorData::invalid_params("messages (array) is required", None))?;
59 let fresh_tail = get_usize(args, "fresh_tail_tokens").unwrap_or(DEFAULT_FRESH_TAIL_TOKENS);
60 let protect_min =
61 get_usize(args, "protect_min_messages").unwrap_or(DEFAULT_PROTECT_MIN_MESSAGES);
62 let focus = get_str(args, "focus_topic");
63
64 let result = compact_messages(messages.clone(), fresh_tail, protect_min, focus.as_deref());
65
66 let offload_target = if result.did_compact && !result.summarized.is_empty() {
70 ctx.session.as_ref()
71 } else {
72 None
73 };
74 if let Some(session_handle) = offload_target {
75 let digest = serialize_transcript(&result.summarized, OFFLOAD_MAX_CHARS);
76 if !digest.is_empty() {
77 let mut session = session_handle.blocking_write();
78 let _ = crate::tools::ctx_session::handle(
79 &mut session,
80 &[],
81 "finding",
82 Some(&digest),
83 None,
84 crate::tools::ctx_session::SessionToolOptions {
85 format: None,
86 path: None,
87 write: false,
88 privacy: None,
89 terse: Some(true),
90 },
91 );
92 }
93 }
94
95 let saved = result
96 .original_tokens
97 .saturating_sub(result.compacted_tokens);
98 Ok(ToolOutput {
99 text: render_result(&result),
100 original_tokens: result.original_tokens,
101 saved_tokens: saved,
102 mode: Some("transcript_compact".to_string()),
103 path: None,
104 changed: false,
105 shell_outcome: None,
106 })
107 }
108}