1use crate::node::{
6 AinlMemoryNode, AinlNodeType, EpisodicNode, PersonaLayer, PersonaNode, ProceduralNode,
7 ProcedureType, RuntimeStateNode, SemanticNode, StrengthEvent,
8};
9use crate::snapshot::SnapshotEdge;
10use crate::store::{GraphStore, SqliteGraphStore};
11use chrono::{DateTime, Utc};
12use rusqlite::{params, OptionalExtension};
13use std::collections::{HashMap, HashSet, VecDeque};
14use uuid::Uuid;
15
16fn node_matches_agent(node: &AinlMemoryNode, agent_id: &str) -> bool {
17 node.agent_id.is_empty() || node.agent_id == agent_id
18}
19
20pub fn walk_from(
22 store: &dyn GraphStore,
23 start_id: Uuid,
24 edge_label: &str,
25 max_depth: usize,
26) -> Result<Vec<AinlMemoryNode>, String> {
27 let mut visited = std::collections::HashSet::new();
28 let mut result = Vec::new();
29 let mut current_level = vec![start_id];
30
31 for _ in 0..max_depth {
32 if current_level.is_empty() {
33 break;
34 }
35
36 let mut next_level = Vec::new();
37
38 for node_id in current_level {
39 if visited.contains(&node_id) {
40 continue;
41 }
42 visited.insert(node_id);
43
44 if let Some(node) = store.read_node(node_id)? {
45 result.push(node.clone());
46
47 for next_node in store.walk_edges(node_id, edge_label)? {
48 if !visited.contains(&next_node.id) {
49 next_level.push(next_node.id);
50 }
51 }
52 }
53 }
54
55 current_level = next_level;
56 }
57
58 Ok(result)
59}
60
61pub fn recall_recent(
63 store: &dyn GraphStore,
64 since_timestamp: i64,
65 limit: usize,
66 tool_filter: Option<&str>,
67) -> Result<Vec<AinlMemoryNode>, String> {
68 let episodes = store.query_episodes_since(since_timestamp, limit)?;
69
70 if let Some(tool_name) = tool_filter {
71 Ok(episodes
72 .into_iter()
73 .filter(|node| match &node.node_type {
74 AinlNodeType::Episode { episodic } => {
75 episodic.effective_tools().contains(&tool_name.to_string())
76 }
77 _ => false,
78 })
79 .collect())
80 } else {
81 Ok(episodes)
82 }
83}
84
85pub fn find_patterns(
87 store: &dyn GraphStore,
88 name_prefix: &str,
89) -> Result<Vec<AinlMemoryNode>, String> {
90 let all_procedural = store.find_by_type("procedural")?;
91
92 Ok(all_procedural
93 .into_iter()
94 .filter(|node| match &node.node_type {
95 AinlNodeType::Procedural { procedural } => {
96 procedural.pattern_name.starts_with(name_prefix)
97 }
98 _ => false,
99 })
100 .collect())
101}
102
103pub fn find_high_confidence_facts(
105 store: &dyn GraphStore,
106 min_confidence: f32,
107) -> Result<Vec<AinlMemoryNode>, String> {
108 let all_semantic = store.find_by_type("semantic")?;
109
110 Ok(all_semantic
111 .into_iter()
112 .filter(|node| match &node.node_type {
113 AinlNodeType::Semantic { semantic } => semantic.confidence >= min_confidence,
114 _ => false,
115 })
116 .collect())
117}
118
119pub fn find_strong_traits(store: &dyn GraphStore) -> Result<Vec<AinlMemoryNode>, String> {
121 let mut all_persona = store.find_by_type("persona")?;
122
123 all_persona.sort_by(|a, b| {
124 let strength_a = match &a.node_type {
125 AinlNodeType::Persona { persona } => persona.strength,
126 _ => 0.0,
127 };
128 let strength_b = match &b.node_type {
129 AinlNodeType::Persona { persona } => persona.strength,
130 _ => 0.0,
131 };
132 strength_b
133 .partial_cmp(&strength_a)
134 .unwrap_or(std::cmp::Ordering::Equal)
135 });
136
137 Ok(all_persona)
138}
139
140pub fn recall_by_topic_cluster(
143 store: &dyn GraphStore,
144 agent_id: &str,
145 cluster: &str,
146) -> Result<Vec<SemanticNode>, String> {
147 let mut out = Vec::new();
148 for node in store.find_by_type("semantic")? {
149 if !node_matches_agent(&node, agent_id) {
150 continue;
151 }
152 if let AinlNodeType::Semantic { semantic } = &node.node_type {
153 if semantic.topic_cluster.as_deref() == Some(cluster) {
154 out.push(semantic.clone());
155 }
156 }
157 }
158 Ok(out)
159}
160
161pub fn recall_contradictions(
162 store: &dyn GraphStore,
163 node_id: Uuid,
164) -> Result<Vec<SemanticNode>, String> {
165 let Some(node) = store.read_node(node_id)? else {
166 return Ok(Vec::new());
167 };
168 let contradiction_ids: Vec<String> = match &node.node_type {
169 AinlNodeType::Semantic { semantic } => semantic.contradiction_ids.clone(),
170 _ => return Ok(Vec::new()),
171 };
172 let mut out = Vec::new();
173 for cid in contradiction_ids {
174 if let Ok(uuid) = Uuid::parse_str(&cid) {
175 if let Some(n) = store.read_node(uuid)? {
176 if let AinlNodeType::Semantic { semantic } = &n.node_type {
177 out.push(semantic.clone());
178 }
179 }
180 }
181 }
182 Ok(out)
183}
184
185pub fn count_by_topic_cluster(
186 store: &dyn GraphStore,
187 agent_id: &str,
188) -> Result<HashMap<String, usize>, String> {
189 let mut counts: HashMap<String, usize> = HashMap::new();
190 for node in store.find_by_type("semantic")? {
191 if !node_matches_agent(&node, agent_id) {
192 continue;
193 }
194 if let AinlNodeType::Semantic { semantic } = &node.node_type {
195 if let Some(cluster) = semantic.topic_cluster.as_deref() {
196 if cluster.is_empty() {
197 continue;
198 }
199 *counts.entry(cluster.to_string()).or_insert(0) += 1;
200 }
201 }
202 }
203 Ok(counts)
204}
205
206pub fn recall_flagged_episodes(
209 store: &dyn GraphStore,
210 agent_id: &str,
211 limit: usize,
212) -> Result<Vec<EpisodicNode>, String> {
213 let mut out: Vec<(i64, EpisodicNode)> = Vec::new();
214 for node in store.find_by_type("episode")? {
215 if !node_matches_agent(&node, agent_id) {
216 continue;
217 }
218 if let AinlNodeType::Episode { episodic } = &node.node_type {
219 if episodic.flagged {
220 out.push((episodic.timestamp, episodic.clone()));
221 }
222 }
223 }
224 out.sort_by(|a, b| b.0.cmp(&a.0));
225 out.truncate(limit);
226 Ok(out.into_iter().map(|(_, e)| e).collect())
227}
228
229pub fn recall_episodes_by_conversation(
230 store: &dyn GraphStore,
231 conversation_id: &str,
232) -> Result<Vec<EpisodicNode>, String> {
233 let mut out: Vec<(u32, EpisodicNode)> = Vec::new();
234 for node in store.find_by_type("episode")? {
235 if let AinlNodeType::Episode { episodic } = &node.node_type {
236 if episodic.conversation_id == conversation_id {
237 out.push((episodic.turn_index, episodic.clone()));
238 }
239 }
240 }
241 out.sort_by(|a, b| a.0.cmp(&b.0));
242 Ok(out.into_iter().map(|(_, e)| e).collect())
243}
244
245pub fn recall_episodes_with_signal(
246 store: &dyn GraphStore,
247 agent_id: &str,
248 signal_type: &str,
249) -> Result<Vec<EpisodicNode>, String> {
250 let mut out = Vec::new();
251 for node in store.find_by_type("episode")? {
252 if !node_matches_agent(&node, agent_id) {
253 continue;
254 }
255 if let AinlNodeType::Episode { episodic } = &node.node_type {
256 if episodic
257 .persona_signals_emitted
258 .iter()
259 .any(|s| s == signal_type)
260 {
261 out.push(episodic.clone());
262 }
263 }
264 }
265 Ok(out)
266}
267
268pub fn recall_by_procedure_type(
271 store: &dyn GraphStore,
272 agent_id: &str,
273 procedure_type: ProcedureType,
274) -> Result<Vec<ProceduralNode>, String> {
275 let mut out = Vec::new();
276 for node in store.find_by_type("procedural")? {
277 if !node_matches_agent(&node, agent_id) {
278 continue;
279 }
280 if let AinlNodeType::Procedural { procedural } = &node.node_type {
281 if procedural.procedure_type == procedure_type {
282 out.push(procedural.clone());
283 }
284 }
285 }
286 Ok(out)
287}
288
289pub fn recall_low_success_procedures(
290 store: &dyn GraphStore,
291 agent_id: &str,
292 threshold: f32,
293) -> Result<Vec<ProceduralNode>, String> {
294 let mut out = Vec::new();
295 for node in store.find_by_type("procedural")? {
296 if !node_matches_agent(&node, agent_id) {
297 continue;
298 }
299 if let AinlNodeType::Procedural { procedural } = &node.node_type {
300 let total = procedural
301 .success_count
302 .saturating_add(procedural.failure_count);
303 if total > 0 && procedural.success_rate < threshold {
304 out.push(procedural.clone());
305 }
306 }
307 }
308 Ok(out)
309}
310
311pub fn recall_strength_history(
314 store: &dyn GraphStore,
315 node_id: Uuid,
316) -> Result<Vec<StrengthEvent>, String> {
317 let Some(node) = store.read_node(node_id)? else {
318 return Ok(Vec::new());
319 };
320 let mut events = match &node.node_type {
321 AinlNodeType::Persona { persona } => persona.evolution_log.clone(),
322 _ => return Ok(Vec::new()),
323 };
324 events.sort_by_key(|e| e.timestamp);
325 Ok(events)
326}
327
328pub fn recall_delta_by_relevance(
329 store: &dyn GraphStore,
330 agent_id: &str,
331 min_relevance: f32,
332) -> Result<Vec<PersonaNode>, String> {
333 let mut out = Vec::new();
334 for node in store.find_by_type("persona")? {
335 if !node_matches_agent(&node, agent_id) {
336 continue;
337 }
338 if let AinlNodeType::Persona { persona } = &node.node_type {
339 if persona.layer == PersonaLayer::Delta && persona.relevance_score >= min_relevance {
340 out.push(persona.clone());
341 }
342 }
343 }
344 Ok(out)
345}
346
347pub struct GraphQuery<'a> {
351 store: &'a SqliteGraphStore,
352 agent_id: String,
353}
354
355impl SqliteGraphStore {
356 pub fn query<'a>(&'a self, agent_id: &str) -> GraphQuery<'a> {
357 GraphQuery {
358 store: self,
359 agent_id: agent_id.to_string(),
360 }
361 }
362}
363
364fn load_nodes_from_payload_rows(
365 rows: impl Iterator<Item = Result<String, rusqlite::Error>>,
366) -> Result<Vec<AinlMemoryNode>, String> {
367 let mut out = Vec::new();
368 for row in rows {
369 let payload = row.map_err(|e| e.to_string())?;
370 let node: AinlMemoryNode = serde_json::from_str(&payload).map_err(|e| e.to_string())?;
371 out.push(node);
372 }
373 Ok(out)
374}
375
376impl<'a> GraphQuery<'a> {
377 fn conn(&self) -> &rusqlite::Connection {
378 self.store.conn()
379 }
380
381 pub fn episodes(&self) -> Result<Vec<AinlMemoryNode>, String> {
382 let mut stmt = self
383 .conn()
384 .prepare(
385 "SELECT payload FROM ainl_graph_nodes
386 WHERE node_type = 'episode'
387 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1",
388 )
389 .map_err(|e| e.to_string())?;
390 let rows = stmt
391 .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
392 .map_err(|e| e.to_string())?;
393 load_nodes_from_payload_rows(rows)
394 }
395
396 pub fn semantic_nodes(&self) -> Result<Vec<AinlMemoryNode>, String> {
397 let mut stmt = self
398 .conn()
399 .prepare(
400 "SELECT payload FROM ainl_graph_nodes
401 WHERE node_type = 'semantic'
402 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1",
403 )
404 .map_err(|e| e.to_string())?;
405 let rows = stmt
406 .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
407 .map_err(|e| e.to_string())?;
408 load_nodes_from_payload_rows(rows)
409 }
410
411 pub fn procedural_nodes(&self) -> Result<Vec<AinlMemoryNode>, String> {
412 let mut stmt = self
413 .conn()
414 .prepare(
415 "SELECT payload FROM ainl_graph_nodes
416 WHERE node_type = 'procedural'
417 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1",
418 )
419 .map_err(|e| e.to_string())?;
420 let rows = stmt
421 .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
422 .map_err(|e| e.to_string())?;
423 load_nodes_from_payload_rows(rows)
424 }
425
426 pub fn persona_nodes(&self) -> Result<Vec<AinlMemoryNode>, String> {
427 let mut stmt = self
428 .conn()
429 .prepare(
430 "SELECT payload FROM ainl_graph_nodes
431 WHERE node_type = 'persona'
432 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1",
433 )
434 .map_err(|e| e.to_string())?;
435 let rows = stmt
436 .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
437 .map_err(|e| e.to_string())?;
438 load_nodes_from_payload_rows(rows)
439 }
440
441 pub fn recent_episodes(&self, limit: usize) -> Result<Vec<AinlMemoryNode>, String> {
442 let mut stmt = self
443 .conn()
444 .prepare(
445 "SELECT payload FROM ainl_graph_nodes
446 WHERE node_type = 'episode'
447 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
448 ORDER BY timestamp DESC
449 LIMIT ?2",
450 )
451 .map_err(|e| e.to_string())?;
452 let rows = stmt
453 .query_map(params![&self.agent_id, limit as i64], |row| {
454 row.get::<_, String>(0)
455 })
456 .map_err(|e| e.to_string())?;
457 load_nodes_from_payload_rows(rows)
458 }
459
460 pub fn since(&self, ts: DateTime<Utc>, node_type: &str) -> Result<Vec<AinlMemoryNode>, String> {
461 let col = node_type.to_ascii_lowercase();
462 let since_ts = ts.timestamp();
463 let mut stmt = self
464 .conn()
465 .prepare(
466 "SELECT payload FROM ainl_graph_nodes
467 WHERE node_type = ?1
468 AND timestamp >= ?2
469 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?3
470 ORDER BY timestamp ASC",
471 )
472 .map_err(|e| e.to_string())?;
473 let rows = stmt
474 .query_map(params![&col, since_ts, &self.agent_id], |row| {
475 row.get::<_, String>(0)
476 })
477 .map_err(|e| e.to_string())?;
478 load_nodes_from_payload_rows(rows)
479 }
480
481 pub fn subgraph_edges(&self) -> Result<Vec<SnapshotEdge>, String> {
483 self.store.agent_subgraph_edges(&self.agent_id)
484 }
485
486 pub fn neighbors(&self, node_id: Uuid, edge_type: &str) -> Result<Vec<AinlMemoryNode>, String> {
487 let mut stmt = self
488 .conn()
489 .prepare(
490 "SELECT to_id FROM ainl_graph_edges
491 WHERE from_id = ?1 AND label = ?2",
492 )
493 .map_err(|e| e.to_string())?;
494 let ids: Vec<String> = stmt
495 .query_map(params![node_id.to_string(), edge_type], |row| row.get(0))
496 .map_err(|e| e.to_string())?
497 .collect::<Result<Vec<_>, _>>()
498 .map_err(|e| e.to_string())?;
499 let mut out = Vec::new();
500 for sid in ids {
501 let id = Uuid::parse_str(&sid).map_err(|e| e.to_string())?;
502 if let Some(n) = self.store.read_node(id)? {
503 out.push(n);
504 }
505 }
506 Ok(out)
507 }
508
509 pub fn lineage(&self, node_id: Uuid) -> Result<Vec<AinlMemoryNode>, String> {
510 let mut visited: HashSet<Uuid> = HashSet::new();
511 let mut out = Vec::new();
512 let mut queue: VecDeque<(Uuid, u32)> = VecDeque::new();
513 visited.insert(node_id);
514 queue.push_back((node_id, 0));
515
516 while let Some((nid, depth)) = queue.pop_front() {
517 if depth >= 20 {
518 continue;
519 }
520 let mut stmt = self
521 .conn()
522 .prepare(
523 "SELECT to_id FROM ainl_graph_edges
524 WHERE from_id = ?1 AND label IN ('DERIVED_FROM', 'CAUSED_PATCH')",
525 )
526 .map_err(|e| e.to_string())?;
527 let targets: Vec<String> = stmt
528 .query_map(params![nid.to_string()], |row| row.get(0))
529 .map_err(|e| e.to_string())?
530 .collect::<Result<Vec<_>, _>>()
531 .map_err(|e| e.to_string())?;
532 for sid in targets {
533 let tid = Uuid::parse_str(&sid).map_err(|e| e.to_string())?;
534 if visited.insert(tid) {
535 if let Some(n) = self.store.read_node(tid)? {
536 out.push(n.clone());
537 queue.push_back((tid, depth + 1));
538 }
539 }
540 }
541 }
542
543 Ok(out)
544 }
545
546 pub fn by_tag(&self, tag: &str) -> Result<Vec<AinlMemoryNode>, String> {
547 let mut stmt = self
548 .conn()
549 .prepare(
550 "SELECT DISTINCT n.payload FROM ainl_graph_nodes n
551 WHERE COALESCE(json_extract(n.payload, '$.agent_id'), '') = ?1
552 AND (
553 EXISTS (
554 SELECT 1 FROM json_each(n.payload, '$.node_type.persona_signals_emitted') j
555 WHERE j.value = ?2
556 )
557 OR EXISTS (
558 SELECT 1 FROM json_each(n.payload, '$.node_type.tags') j
559 WHERE j.value = ?2
560 )
561 )",
562 )
563 .map_err(|e| e.to_string())?;
564 let rows = stmt
565 .query_map(params![&self.agent_id, tag], |row| row.get::<_, String>(0))
566 .map_err(|e| e.to_string())?;
567 load_nodes_from_payload_rows(rows)
568 }
569
570 pub fn by_topic_cluster(&self, cluster: &str) -> Result<Vec<AinlMemoryNode>, String> {
571 let like = format!("%{cluster}%");
572 let mut stmt = self
573 .conn()
574 .prepare(
575 "SELECT payload FROM ainl_graph_nodes
576 WHERE node_type = 'semantic'
577 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
578 AND json_extract(payload, '$.node_type.topic_cluster') LIKE ?2 ESCAPE '\\'",
579 )
580 .map_err(|e| e.to_string())?;
581 let rows = stmt
582 .query_map(params![&self.agent_id, like], |row| row.get::<_, String>(0))
583 .map_err(|e| e.to_string())?;
584 load_nodes_from_payload_rows(rows)
585 }
586
587 pub fn pattern_by_name(&self, name: &str) -> Result<Option<AinlMemoryNode>, String> {
588 let mut stmt = self
589 .conn()
590 .prepare(
591 "SELECT payload FROM ainl_graph_nodes
592 WHERE node_type = 'procedural'
593 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
594 AND (
595 json_extract(payload, '$.node_type.pattern_name') = ?2
596 OR json_extract(payload, '$.node_type.label') = ?2
597 )
598 ORDER BY timestamp DESC
599 LIMIT 1",
600 )
601 .map_err(|e| e.to_string())?;
602 let row = stmt
603 .query_row(params![&self.agent_id, name], |row| row.get::<_, String>(0))
604 .optional()
605 .map_err(|e| e.to_string())?;
606 match row {
607 Some(payload) => {
608 let node: AinlMemoryNode =
609 serde_json::from_str(&payload).map_err(|e| e.to_string())?;
610 Ok(Some(node))
611 }
612 None => Ok(None),
613 }
614 }
615
616 pub fn active_patches(&self) -> Result<Vec<AinlMemoryNode>, String> {
617 let mut stmt = self
618 .conn()
619 .prepare(
620 "SELECT payload FROM ainl_graph_nodes
621 WHERE node_type = 'procedural'
622 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
623 AND (
624 json_extract(payload, '$.node_type.retired') IS NULL
625 OR json_extract(payload, '$.node_type.retired') = 0
626 OR CAST(json_extract(payload, '$.node_type.retired') AS TEXT) = 'false'
627 )",
628 )
629 .map_err(|e| e.to_string())?;
630 let rows = stmt
631 .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
632 .map_err(|e| e.to_string())?;
633 load_nodes_from_payload_rows(rows)
634 }
635
636 pub fn successful_episodes(&self, limit: usize) -> Result<Vec<AinlMemoryNode>, String> {
637 let mut stmt = self
638 .conn()
639 .prepare(
640 "SELECT payload FROM ainl_graph_nodes
641 WHERE node_type = 'episode'
642 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
643 AND json_extract(payload, '$.node_type.outcome') = 'success'
644 ORDER BY timestamp DESC
645 LIMIT ?2",
646 )
647 .map_err(|e| e.to_string())?;
648 let rows = stmt
649 .query_map(params![&self.agent_id, limit as i64], |row| {
650 row.get::<_, String>(0)
651 })
652 .map_err(|e| e.to_string())?;
653 load_nodes_from_payload_rows(rows)
654 }
655
656 pub fn episodes_with_tool(
657 &self,
658 tool_name: &str,
659 limit: usize,
660 ) -> Result<Vec<AinlMemoryNode>, String> {
661 let mut stmt = self
662 .conn()
663 .prepare(
664 "SELECT payload FROM ainl_graph_nodes
665 WHERE node_type = 'episode'
666 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
667 AND (
668 EXISTS (
669 SELECT 1 FROM json_each(json_extract(payload, '$.node_type.tools_invoked')) e
670 WHERE e.value = ?2
671 )
672 OR EXISTS (
673 SELECT 1 FROM json_each(json_extract(payload, '$.node_type.tool_calls')) e
674 WHERE e.value = ?2
675 )
676 )
677 ORDER BY timestamp DESC
678 LIMIT ?3",
679 )
680 .map_err(|e| e.to_string())?;
681 let rows = stmt
682 .query_map(params![&self.agent_id, tool_name, limit as i64], |row| {
683 row.get::<_, String>(0)
684 })
685 .map_err(|e| e.to_string())?;
686 load_nodes_from_payload_rows(rows)
687 }
688
689 pub fn evolved_persona(&self) -> Result<Option<AinlMemoryNode>, String> {
690 let mut stmt = self
691 .conn()
692 .prepare(
693 "SELECT payload FROM ainl_graph_nodes
694 WHERE node_type = 'persona'
695 AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
696 AND json_extract(payload, '$.node_type.trait_name') = 'axis_evolution_snapshot'
697 ORDER BY timestamp DESC
698 LIMIT 1",
699 )
700 .map_err(|e| e.to_string())?;
701 let row = stmt
702 .query_row(params![&self.agent_id], |row| row.get::<_, String>(0))
703 .optional()
704 .map_err(|e| e.to_string())?;
705 match row {
706 Some(payload) => {
707 let node: AinlMemoryNode =
708 serde_json::from_str(&payload).map_err(|e| e.to_string())?;
709 Ok(Some(node))
710 }
711 None => Ok(None),
712 }
713 }
714
715 pub fn read_runtime_state(&self) -> Result<Option<RuntimeStateNode>, String> {
717 self.store.read_runtime_state(&self.agent_id)
718 }
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724 use crate::node::AinlMemoryNode;
725 use crate::store::SqliteGraphStore;
726
727 #[test]
728 fn test_recall_recent_with_tool_filter() {
729 let temp_dir = std::env::temp_dir();
730 let db_path = temp_dir.join("ainl_query_test_recall.db");
731 let _ = std::fs::remove_file(&db_path);
732
733 let store = SqliteGraphStore::open(&db_path).expect("Failed to open store");
734
735 let now = chrono::Utc::now().timestamp();
736
737 let node1 = AinlMemoryNode::new_episode(
738 uuid::Uuid::new_v4(),
739 now,
740 vec!["file_read".to_string()],
741 None,
742 None,
743 );
744
745 let node2 = AinlMemoryNode::new_episode(
746 uuid::Uuid::new_v4(),
747 now + 1,
748 vec!["agent_delegate".to_string()],
749 Some("agent-B".to_string()),
750 None,
751 );
752
753 store.write_node(&node1).expect("Write failed");
754 store.write_node(&node2).expect("Write failed");
755
756 let delegations =
757 recall_recent(&store, now - 100, 10, Some("agent_delegate")).expect("Query failed");
758
759 assert_eq!(delegations.len(), 1);
760 }
761
762 #[test]
763 fn test_find_high_confidence_facts() {
764 let temp_dir = std::env::temp_dir();
765 let db_path = temp_dir.join("ainl_query_test_facts.db");
766 let _ = std::fs::remove_file(&db_path);
767
768 let store = SqliteGraphStore::open(&db_path).expect("Failed to open store");
769
770 let turn_id = uuid::Uuid::new_v4();
771
772 let fact1 = AinlMemoryNode::new_fact("User prefers Rust".to_string(), 0.95, turn_id);
773 let fact2 = AinlMemoryNode::new_fact("User dislikes Python".to_string(), 0.45, turn_id);
774
775 store.write_node(&fact1).expect("Write failed");
776 store.write_node(&fact2).expect("Write failed");
777
778 let high_conf = find_high_confidence_facts(&store, 0.7).expect("Query failed");
779
780 assert_eq!(high_conf.len(), 1);
781 }
782
783 #[test]
784 fn test_query_active_patches() {
785 let path = std::env::temp_dir().join(format!(
786 "ainl_query_active_patch_{}.db",
787 uuid::Uuid::new_v4()
788 ));
789 let _ = std::fs::remove_file(&path);
790 let store = SqliteGraphStore::open(&path).expect("open");
791 let ag = "agent-active-patch";
792 let mut p1 = AinlMemoryNode::new_pattern("pat_one".into(), vec![1, 2]);
793 p1.agent_id = ag.into();
794 let mut p2 = AinlMemoryNode::new_pattern("pat_two".into(), vec![3, 4]);
795 p2.agent_id = ag.into();
796 store.write_node(&p1).expect("w1");
797 store.write_node(&p2).expect("w2");
798
799 let conn = store.conn();
800 let payload2: String = conn
801 .query_row(
802 "SELECT payload FROM ainl_graph_nodes WHERE id = ?1",
803 [p2.id.to_string()],
804 |row| row.get(0),
805 )
806 .unwrap();
807 let mut v: serde_json::Value = serde_json::from_str(&payload2).unwrap();
808 v["node_type"]["retired"] = serde_json::json!(true);
809 conn.execute(
810 "UPDATE ainl_graph_nodes SET payload = ?1 WHERE id = ?2",
811 rusqlite::params![v.to_string(), p2.id.to_string()],
812 )
813 .unwrap();
814
815 let active = store.query(ag).active_patches().expect("q");
816 assert_eq!(active.len(), 1);
817 assert_eq!(active[0].id, p1.id);
818 }
819}