Skip to main content

lean_ctx/tools/registered/
ctx_transcript_compact.rs

1use 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        // Best-effort offload of the raw older turns into session memory so the
67        // recall tools (and the autonomy consolidation pipeline) can recover
68        // them. Skipped when no session is bound (e.g. one-shot CLI).
69        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}