oxi_agent/tools/
memory_reflect.rs1use crate::tools::typed::TypedTool;
4use async_trait::async_trait;
5use schemars::JsonSchema;
6use serde::Deserialize;
7use serde_json::{Value, json};
8
9use super::{AgentTool, AgentToolResult, ToolContext, ToolError};
10
11#[derive(Deserialize, JsonSchema)]
20pub struct MemoryReflectArgs {
21 summary: String,
22}
23
24#[allow(missing_docs)]
25pub struct MemoryReflectTool;
26
27#[async_trait]
28impl AgentTool for MemoryReflectTool {
29 fn name(&self) -> &str {
30 "memory_reflect"
31 }
32
33 fn label(&self) -> &str {
34 "Memory Reflect"
35 }
36
37 fn description(&self) -> &str {
38 "Save a session summary or reflection to long-term memory. \
39 A non-empty `summary` is required."
40 }
41
42 fn essential(&self) -> bool {
43 false
44 }
45
46 fn parameters_schema(&self) -> Value {
47 json!({
48 "type": "object",
49 "properties": {
50 "summary": {
51 "type": "string",
52 "description": "Session summary to persist to long-term memory."
53 }
54 },
55 "required": ["summary"]
56 })
57 }
58
59 async fn execute(
60 &self,
61 _tool_call_id: &str,
62 params: Value,
63 _signal: Option<tokio::sync::oneshot::Receiver<()>>,
64 ctx: &ToolContext,
65 ) -> Result<AgentToolResult, ToolError> {
66 let args: MemoryReflectArgs =
67 serde_json::from_value(params).map_err(|e| format!("invalid params: {e}"))?;
68 self.execute_typed(_tool_call_id, args, _signal, ctx).await
69 }
70}
71
72#[async_trait]
73impl TypedTool for MemoryReflectTool {
74 type Args = MemoryReflectArgs;
75
76 async fn execute_typed(
77 &self,
78 _tool_call_id: &str,
79 args: Self::Args,
80 _signal: Option<tokio::sync::oneshot::Receiver<()>>,
81 ctx: &ToolContext,
82 ) -> Result<AgentToolResult, ToolError> {
83 let backend = ctx.memory.as_ref().ok_or("Memory not configured")?;
84 let subject = ctx.session_id.as_deref().unwrap_or("default");
85 if args.summary.trim().is_empty() {
86 return Err("summary must not be empty".into());
87 }
88 backend.put(&args.summary, "summary", subject).await?;
89 Ok(AgentToolResult::success(format!(
90 "Reflected session summary to memory (subject: {}).",
91 subject
92 )))
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::tools::MemoryBackend;
100 use parking_lot::Mutex;
101 use std::future::Future;
102 use std::pin::Pin;
103 use std::sync::Arc;
104
105 #[derive(Debug)]
107 struct MockMemory {
108 puts: Mutex<Vec<(String, String, String)>>,
109 }
110
111 impl MockMemory {
112 fn new() -> Self {
113 Self {
114 puts: Mutex::new(vec![]),
115 }
116 }
117 }
118
119 impl MemoryBackend for MockMemory {
120 fn put<'a>(
121 &'a self,
122 content: &'a str,
123 kind: &'a str,
124 subject: &'a str,
125 ) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>> {
126 self.puts
127 .lock()
128 .push((content.into(), kind.into(), subject.into()));
129 Box::pin(async move { Ok("mem-1".to_string()) })
130 }
131
132 fn search<'a>(
133 &'a self,
134 _query: &'a str,
135 _k: usize,
136 ) -> Pin<
137 Box<dyn Future<Output = Result<Vec<crate::tools::MemoryItem>, ToolError>> + Send + 'a>,
138 > {
139 Box::pin(async move { Ok(vec![]) })
140 }
141
142 fn list<'a>(
143 &'a self,
144 _subject: &'a str,
145 ) -> Pin<
146 Box<dyn Future<Output = Result<Vec<crate::tools::MemoryItem>, ToolError>> + Send + 'a>,
147 > {
148 Box::pin(async move { Ok(vec![]) })
149 }
150
151 fn delete<'a>(
152 &'a self,
153 _id: &'a str,
154 ) -> Pin<Box<dyn Future<Output = Result<(), ToolError>> + Send + 'a>> {
155 Box::pin(async move { Ok(()) })
156 }
157 }
158
159 #[tokio::test]
160 async fn reflect_with_summary_persists() {
161 let mock = Arc::new(MockMemory::new());
162 let ctx = ToolContext::default()
163 .with_session("s1")
164 .with_memory(mock.clone());
165 let result = MemoryReflectTool
166 .execute("c1", json!({"summary": "did X"}), None, &ctx)
167 .await
168 .unwrap();
169 assert!(result.success);
170 assert!(result.output.contains("Reflected session summary"));
171 let puts = mock.puts.lock();
172 assert_eq!(puts.len(), 1);
173 assert_eq!(puts[0].0, "did X");
174 assert_eq!(puts[0].1, "summary");
175 assert_eq!(puts[0].2, "s1");
176 }
177
178 #[tokio::test]
179 async fn reflect_without_summary_errors() {
180 let mock = Arc::new(MockMemory::new());
181 let ctx = ToolContext::default().with_memory(mock.clone());
182 let err = MemoryReflectTool
183 .execute("c1", json!({}), None, &ctx)
184 .await
185 .unwrap_err();
186 assert!(err.contains("summary"));
187 assert!(mock.puts.lock().is_empty());
188 }
189
190 #[tokio::test]
191 async fn reflect_rejects_empty_summary() {
192 let mock = Arc::new(MockMemory::new());
193 let ctx = ToolContext::default().with_memory(mock.clone());
194 let err = MemoryReflectTool
195 .execute("c1", json!({"summary": " "}), None, &ctx)
196 .await
197 .unwrap_err();
198 assert!(err.contains("empty"));
199 assert!(mock.puts.lock().is_empty());
200 }
201
202 #[tokio::test]
203 async fn reflect_errors_when_memory_not_configured() {
204 let ctx = ToolContext::default();
205 let err = MemoryReflectTool
206 .execute("c1", json!({"summary": "x"}), None, &ctx)
207 .await
208 .unwrap_err();
209 assert_eq!(err, "Memory not configured");
210 }
211}