microclaw_engine/internal/storage/db/
tool_cache.rs1use super::*;
2
3#[derive(Debug, Clone)]
5pub struct CachedToolResult {
6 pub tool_name: String,
7 pub content: String,
8 pub is_error: bool,
9 pub metadata_json: Option<String>,
10 pub created_at: String,
11 pub expires_at: String,
12}
13
14#[derive(Debug, Clone)]
16pub struct ToolArtifactMeta {
17 pub artifact_id: String,
18 pub chat_id: i64,
19 pub tool_name: String,
20 pub total_chars: i64,
21 pub created_at: String,
22 pub expires_at: String,
23}
24
25impl Database {
26 pub fn get_cached_tool_result(
30 &self,
31 cache_key: &str,
32 now: &str,
33 ) -> Result<Option<CachedToolResult>, MicroClawError> {
34 let conn = self.lock_conn();
35 let row = conn
36 .query_row(
37 "SELECT tool_name, result_content, is_error, metadata_json, created_at, expires_at
38 FROM tool_result_cache
39 WHERE cache_key = ?1 AND expires_at > ?2",
40 params![cache_key, now],
41 |r| {
42 Ok(CachedToolResult {
43 tool_name: r.get(0)?,
44 content: r.get(1)?,
45 is_error: r.get::<_, i64>(2)? != 0,
46 metadata_json: r.get::<_, Option<String>>(3)?,
47 created_at: r.get(4)?,
48 expires_at: r.get(5)?,
49 })
50 },
51 )
52 .optional()?;
53 Ok(row)
54 }
55
56 pub fn put_cached_tool_result(
60 &self,
61 cache_key: &str,
62 tool_name: &str,
63 content: &str,
64 is_error: bool,
65 metadata_json: Option<&str>,
66 expires_at: &str,
67 ) -> Result<(), MicroClawError> {
68 let conn = self.lock_conn();
69 let now = chrono::Utc::now().to_rfc3339();
70 conn.execute(
71 "INSERT INTO tool_result_cache
72 (cache_key, tool_name, result_content, is_error, metadata_json, created_at, expires_at)
73 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
74 ON CONFLICT(cache_key) DO UPDATE SET
75 tool_name = excluded.tool_name,
76 result_content = excluded.result_content,
77 is_error = excluded.is_error,
78 metadata_json = excluded.metadata_json,
79 created_at = excluded.created_at,
80 expires_at = excluded.expires_at",
81 params![cache_key, tool_name, content, is_error as i64, metadata_json, now, expires_at],
82 )?;
83 Ok(())
84 }
85
86 pub fn prune_tool_result_cache(&self, now: &str) -> Result<usize, MicroClawError> {
88 let conn = self.lock_conn();
89 let n = conn.execute(
90 "DELETE FROM tool_result_cache WHERE expires_at <= ?1",
91 params![now],
92 )?;
93 Ok(n)
94 }
95
96 pub fn save_tool_artifact(
100 &self,
101 artifact_id: &str,
102 chat_id: i64,
103 tool_name: &str,
104 content: &str,
105 expires_at: &str,
106 ) -> Result<(), MicroClawError> {
107 let conn = self.lock_conn();
108 let now = chrono::Utc::now().to_rfc3339();
109 let total_chars = content.chars().count() as i64;
110 conn.execute(
111 "INSERT INTO tool_result_artifacts
112 (artifact_id, chat_id, tool_name, content, total_chars, created_at, expires_at)
113 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
114 params![
115 artifact_id,
116 chat_id,
117 tool_name,
118 content,
119 total_chars,
120 now,
121 expires_at
122 ],
123 )?;
124 Ok(())
125 }
126
127 pub fn get_tool_artifact_slice(
132 &self,
133 artifact_id: &str,
134 offset: usize,
135 length: usize,
136 now: &str,
137 ) -> Result<Option<(ToolArtifactMeta, String)>, MicroClawError> {
138 let conn = self.lock_conn();
139 let row = conn
140 .query_row(
141 "SELECT artifact_id, chat_id, tool_name, content, total_chars, created_at, expires_at
142 FROM tool_result_artifacts
143 WHERE artifact_id = ?1 AND expires_at > ?2",
144 params![artifact_id, now],
145 |r| {
146 let content: String = r.get(3)?;
147 Ok((
148 ToolArtifactMeta {
149 artifact_id: r.get(0)?,
150 chat_id: r.get(1)?,
151 tool_name: r.get(2)?,
152 total_chars: r.get(4)?,
153 created_at: r.get(5)?,
154 expires_at: r.get(6)?,
155 },
156 content,
157 ))
158 },
159 )
160 .optional()?;
161 let Some((meta, content)) = row else {
162 return Ok(None);
163 };
164 let slice: String = content.chars().skip(offset).take(length).collect();
165 Ok(Some((meta, slice)))
166 }
167
168 pub fn prune_tool_artifacts(&self, now: &str) -> Result<usize, MicroClawError> {
170 let conn = self.lock_conn();
171 let n = conn.execute(
172 "DELETE FROM tool_result_artifacts WHERE expires_at <= ?1",
173 params![now],
174 )?;
175 Ok(n)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use crate::internal::storage::db::test_support::*;
182
183 #[test]
184 fn test_tool_artifact_save_and_slice() {
185 let (db, dir) = test_db();
186 let now = chrono::Utc::now();
187 let expires = (now + chrono::Duration::hours(1)).to_rfc3339();
188 let body: String = "0123456789".repeat(20); db.save_tool_artifact("art_x", 42, "bash", &body, &expires)
190 .unwrap();
191
192 let (meta, slice) = db
194 .get_tool_artifact_slice("art_x", 0, 50, &now.to_rfc3339())
195 .unwrap()
196 .expect("artifact present");
197 assert_eq!(meta.chat_id, 42);
198 assert_eq!(meta.tool_name, "bash");
199 assert_eq!(meta.total_chars, 200);
200 assert_eq!(slice.chars().count(), 50);
201 assert!(body.starts_with(&slice));
202
203 let (_, tail) = db
205 .get_tool_artifact_slice("art_x", 190, 50, &now.to_rfc3339())
206 .unwrap()
207 .unwrap();
208 assert_eq!(tail.chars().count(), 10);
209
210 let future = (now + chrono::Duration::hours(2)).to_rfc3339();
212 let missing = db.get_tool_artifact_slice("art_x", 0, 10, &future).unwrap();
213 assert!(missing.is_none());
214
215 let pruned = db.prune_tool_artifacts(&future).unwrap();
217 assert_eq!(pruned, 1);
218 cleanup(&dir);
219 }
220}