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:\n\
24             keep system + fresh tail verbatim, replace older turns with a recoverable\n\
25             summary, offload raw turns into session memory (indexed for recall).\n\
26             Returns JSON {messages, stats}. tool_call/tool_result pairs never split.",
27            json!({
28                "type": "object",
29                "properties": {
30                    "messages": {
31                        "type": "array",
32                        "items": { "type": "object" },
33                        "description": "OpenAI message array to compact"
34                    },
35                    "fresh_tail_tokens": {
36                        "type": "integer",
37                        "description": "Recent tokens to keep verbatim"
38                    },
39                    "protect_min_messages": {
40                        "type": "integer",
41                        "description": "Minimum recent messages to keep"
42                    },
43                    "focus_topic": {
44                        "type": "string",
45                        "description": "Topic to prioritise in summary"
46                    }
47                },
48                "required": ["messages"]
49            }),
50        )
51    }
52
53    fn handle(
54        &self,
55        args: &Map<String, Value>,
56        ctx: &ToolContext,
57    ) -> Result<ToolOutput, ErrorData> {
58        let messages = args
59            .get("messages")
60            .and_then(Value::as_array)
61            .ok_or_else(|| ErrorData::invalid_params("messages (array) is required", None))?;
62        let fresh_tail = get_usize(args, "fresh_tail_tokens").unwrap_or(DEFAULT_FRESH_TAIL_TOKENS);
63        let protect_min =
64            get_usize(args, "protect_min_messages").unwrap_or(DEFAULT_PROTECT_MIN_MESSAGES);
65        let focus = get_str(args, "focus_topic");
66
67        let result = compact_messages(messages.clone(), fresh_tail, protect_min, focus.as_deref());
68
69        // Best-effort offload of the raw older turns into session memory so the
70        // recall tools (and the autonomy consolidation pipeline) can recover
71        // them. Skipped when no session is bound (e.g. one-shot CLI).
72        let offload_target = if result.did_compact && !result.summarized.is_empty() {
73            ctx.session.as_ref()
74        } else {
75            None
76        };
77        if let Some(session_handle) = offload_target {
78            let digest = serialize_transcript(&result.summarized, OFFLOAD_MAX_CHARS);
79            if !digest.is_empty() {
80                let mut session = session_handle.blocking_write();
81                let _ = crate::tools::ctx_session::handle(
82                    &mut session,
83                    &[],
84                    "finding",
85                    Some(&digest),
86                    None,
87                    crate::tools::ctx_session::SessionToolOptions {
88                        format: None,
89                        path: None,
90                        write: false,
91                        privacy: None,
92                        terse: Some(true),
93                        agent_id: None,
94                    },
95                );
96            }
97        }
98
99        let saved = result
100            .original_tokens
101            .saturating_sub(result.compacted_tokens);
102        Ok(ToolOutput {
103            text: render_result(&result),
104            original_tokens: result.original_tokens,
105            saved_tokens: saved,
106            mode: Some("transcript_compact".to_string()),
107            path: None,
108            changed: false,
109            shell_outcome: None,
110            content_blocks: None,
111        })
112    }
113}