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