codetether_agent/tool/
session_recall.rs1use super::{Tool, ToolResult};
19use crate::provider::Provider;
20use crate::rlm::router::AutoProcessContext;
21use crate::rlm::{RlmConfig, RlmRouter};
22use crate::session::Session;
23use crate::session::helper::error::messages_to_rlm_context;
24use anyhow::Result;
25use async_trait::async_trait;
26use serde_json::{Value, json};
27use std::sync::Arc;
28
29const DEFAULT_SESSION_LIMIT: usize = 3;
33
34pub struct SessionRecallTool {
36 provider: Arc<dyn Provider>,
37 model: String,
38 config: RlmConfig,
39}
40
41impl SessionRecallTool {
42 pub fn new(provider: Arc<dyn Provider>, model: String, config: RlmConfig) -> Self {
43 Self {
44 provider,
45 model,
46 config,
47 }
48 }
49}
50
51#[async_trait]
52impl Tool for SessionRecallTool {
53 fn id(&self) -> &str {
54 "session_recall"
55 }
56
57 fn name(&self) -> &str {
58 "SessionRecall"
59 }
60
61 fn description(&self) -> &str {
62 "RECALL FROM YOUR OWN PAST SESSIONS. Call this whenever:\n\
63 - You see `[AUTO CONTEXT COMPRESSION]` in the transcript and need \
64 specifics the summary dropped (exact paths, prior tool output, \
65 earlier user instructions, numbers).\n\
66 - The user references something from earlier (\"like I said before\", \
67 \"the task I gave you yesterday\", \"that file we edited\") and you \
68 cannot find it in your active context.\n\
69 - You feel uncertain about prior decisions in this workspace.\n\
70 Do NOT ask the user to repeat themselves — call this tool first. \
71 It runs RLM over `.codetether-agent/sessions/` for the current \
72 workspace. Pass a natural-language `query` describing what you \
73 need to remember. Optionally pin `session_id` or widen `limit` \
74 (recent sessions to include, default 3). Distinct from the \
75 `memory` tool: `memory` is curated notes you wrote; \
76 `session_recall` is the raw conversation archive."
77 }
78
79 fn parameters(&self) -> Value {
80 json!({
81 "type": "object",
82 "properties": {
83 "query": {
84 "type": "string",
85 "description": "Natural-language question about past session content"
86 },
87 "session_id": {
88 "type": "string",
89 "description": "Specific session UUID to recall from. \
90 When omitted, searches the most recent \
91 sessions for the current workspace."
92 },
93 "limit": {
94 "type": "integer",
95 "description": "How many recent sessions to include when \
96 session_id is not set (default 3).",
97 "default": 3
98 }
99 },
100 "required": ["query"]
101 })
102 }
103
104 async fn execute(&self, args: Value) -> Result<ToolResult> {
105 let query = match args["query"].as_str() {
106 Some(q) if !q.trim().is_empty() => q.to_string(),
107 _ => return Ok(ToolResult::error("query is required")),
108 };
109 let session_id = args["session_id"].as_str().map(str::to_string);
110 let limit = args["limit"]
111 .as_u64()
112 .map(|n| n as usize)
113 .unwrap_or(DEFAULT_SESSION_LIMIT)
114 .max(1);
115
116 let (context, sources) = match build_recall_context(session_id, limit).await {
117 Ok(ok) => ok,
118 Err(e) => return Ok(ToolResult::error(format!("recall load failed: {e}"))),
119 };
120
121 if context.trim().is_empty() {
122 return Ok(ToolResult::success(
123 "No prior session history found for this workspace.".to_string(),
124 ));
125 }
126
127 run_recall(
128 &context,
129 &sources,
130 &query,
131 Arc::clone(&self.provider),
132 &self.model,
133 &self.config,
134 )
135 .await
136 }
137}
138
139async fn build_recall_context(
144 session_id: Option<String>,
145 limit: usize,
146) -> Result<(String, Vec<String>)> {
147 let sessions = match session_id {
148 Some(id) => vec![Session::load(&id).await?],
149 None => load_recent_for_cwd(limit).await?,
150 };
151
152 let mut ctx = String::new();
153 let mut sources = Vec::with_capacity(sessions.len());
154 for s in &sessions {
155 let label = format!("{} ({})", s.title.as_deref().unwrap_or("<untitled>"), &s.id);
156 sources.push(label.clone());
157 ctx.push_str(&format!(
158 "\n===== SESSION {label} — updated {} =====\n",
159 s.updated_at
160 ));
161 ctx.push_str(&messages_to_rlm_context(&s.messages));
162 }
163 Ok((ctx, sources))
164}
165
166async fn load_recent_for_cwd(limit: usize) -> Result<Vec<Session>> {
169 let cwd = std::env::current_dir().ok();
170 let summaries = match cwd.as_deref() {
171 Some(dir) => crate::session::listing::list_sessions_for_directory(dir).await?,
172 None => crate::session::list_sessions().await?,
173 };
174
175 let mut loaded = Vec::new();
176 for s in summaries.into_iter().take(limit) {
177 match Session::load(&s.id).await {
178 Ok(sess) => loaded.push(sess),
179 Err(e) => tracing::warn!(session_id = %s.id, error = %e, "session_recall: load failed"),
180 }
181 }
182 Ok(loaded)
183}
184
185async fn run_recall(
188 context: &str,
189 sources: &[String],
190 query: &str,
191 provider: Arc<dyn Provider>,
192 model: &str,
193 config: &RlmConfig,
194) -> Result<ToolResult> {
195 let auto_ctx = AutoProcessContext {
196 tool_id: "session_recall",
197 tool_args: json!({ "query": query }),
198 session_id: "session-recall-tool",
199 abort: None,
200 on_progress: None,
201 provider,
202 model: model.to_string(),
203 bus: None,
204 trace_id: None,
205 subcall_provider: None,
206 subcall_model: None,
207 };
208
209 let framed = format!(
213 "Recall task: {query}\n\n\
214 Use the session transcript below to answer the recall task. \
215 Quote short passages verbatim when useful; otherwise summarise.\n\n\
216 {context}"
217 );
218
219 match RlmRouter::auto_process(&framed, auto_ctx, config).await {
220 Ok(result) => Ok(ToolResult::success(format!(
221 "Recalled from {} session(s): {}\n\
222 (RLM: {} → {} tokens, {} iterations)\n\n{}",
223 sources.len(),
224 sources.join(", "),
225 result.stats.input_tokens,
226 result.stats.output_tokens,
227 result.stats.iterations,
228 result.processed
229 ))),
230 Err(e) => Ok(ToolResult::error(format!("RLM recall failed: {e}"))),
231 }
232}