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