Skip to main content

ainl_memory/
query.rs

1//! Graph traversal and querying utilities.
2//!
3//! Higher-level query functions built on top of GraphStore.
4
5use crate::node::{
6    AinlMemoryNode, AinlNodeType, EpisodicNode, PersonaLayer, PersonaNode, ProcedureType,
7    ProceduralNode, 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
20/// Walk the graph from a starting node, following edges with a specific label
21pub 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
61/// Recall recent episodes, optionally filtered by tool usage
62pub 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
85/// Find procedural patterns by name prefix
86pub 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
103/// Find semantic facts with confidence above a threshold
104pub 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
119/// Find persona traits sorted by strength
120pub 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
140// --- Semantic helpers ---
141
142pub 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(store: &dyn GraphStore, node_id: Uuid) -> Result<Vec<SemanticNode>, String> {
162    let Some(node) = store.read_node(node_id)? else {
163        return Ok(Vec::new());
164    };
165    let contradiction_ids: Vec<String> = match &node.node_type {
166        AinlNodeType::Semantic { semantic } => semantic.contradiction_ids.clone(),
167        _ => return Ok(Vec::new()),
168    };
169    let mut out = Vec::new();
170    for cid in contradiction_ids {
171        if let Ok(uuid) = Uuid::parse_str(&cid) {
172            if let Some(n) = store.read_node(uuid)? {
173                if let AinlNodeType::Semantic { semantic } = &n.node_type {
174                    out.push(semantic.clone());
175                }
176            }
177        }
178    }
179    Ok(out)
180}
181
182pub fn count_by_topic_cluster(
183    store: &dyn GraphStore,
184    agent_id: &str,
185) -> Result<HashMap<String, usize>, String> {
186    let mut counts: HashMap<String, usize> = HashMap::new();
187    for node in store.find_by_type("semantic")? {
188        if !node_matches_agent(&node, agent_id) {
189            continue;
190        }
191        if let AinlNodeType::Semantic { semantic } = &node.node_type {
192            if let Some(cluster) = semantic.topic_cluster.as_deref() {
193                if cluster.is_empty() {
194                    continue;
195                }
196                *counts.entry(cluster.to_string()).or_insert(0) += 1;
197            }
198        }
199    }
200    Ok(counts)
201}
202
203// --- Episodic helpers ---
204
205pub fn recall_flagged_episodes(
206    store: &dyn GraphStore,
207    agent_id: &str,
208    limit: usize,
209) -> Result<Vec<EpisodicNode>, String> {
210    let mut out: Vec<(i64, EpisodicNode)> = Vec::new();
211    for node in store.find_by_type("episode")? {
212        if !node_matches_agent(&node, agent_id) {
213            continue;
214        }
215        if let AinlNodeType::Episode { episodic } = &node.node_type {
216            if episodic.flagged {
217                out.push((episodic.timestamp, episodic.clone()));
218            }
219        }
220    }
221    out.sort_by(|a, b| b.0.cmp(&a.0));
222    out.truncate(limit);
223    Ok(out.into_iter().map(|(_, e)| e).collect())
224}
225
226pub fn recall_episodes_by_conversation(
227    store: &dyn GraphStore,
228    conversation_id: &str,
229) -> Result<Vec<EpisodicNode>, String> {
230    let mut out: Vec<(u32, EpisodicNode)> = Vec::new();
231    for node in store.find_by_type("episode")? {
232        if let AinlNodeType::Episode { episodic } = &node.node_type {
233            if episodic.conversation_id == conversation_id {
234                out.push((episodic.turn_index, episodic.clone()));
235            }
236        }
237    }
238    out.sort_by(|a, b| a.0.cmp(&b.0));
239    Ok(out.into_iter().map(|(_, e)| e).collect())
240}
241
242pub fn recall_episodes_with_signal(
243    store: &dyn GraphStore,
244    agent_id: &str,
245    signal_type: &str,
246) -> Result<Vec<EpisodicNode>, String> {
247    let mut out = Vec::new();
248    for node in store.find_by_type("episode")? {
249        if !node_matches_agent(&node, agent_id) {
250            continue;
251        }
252        if let AinlNodeType::Episode { episodic } = &node.node_type {
253            if episodic
254                .persona_signals_emitted
255                .iter()
256                .any(|s| s == signal_type)
257            {
258                out.push(episodic.clone());
259            }
260        }
261    }
262    Ok(out)
263}
264
265// --- Procedural helpers ---
266
267pub fn recall_by_procedure_type(
268    store: &dyn GraphStore,
269    agent_id: &str,
270    procedure_type: ProcedureType,
271) -> Result<Vec<ProceduralNode>, String> {
272    let mut out = Vec::new();
273    for node in store.find_by_type("procedural")? {
274        if !node_matches_agent(&node, agent_id) {
275            continue;
276        }
277        if let AinlNodeType::Procedural { procedural } = &node.node_type {
278            if procedural.procedure_type == procedure_type {
279                out.push(procedural.clone());
280            }
281        }
282    }
283    Ok(out)
284}
285
286pub fn recall_low_success_procedures(
287    store: &dyn GraphStore,
288    agent_id: &str,
289    threshold: f32,
290) -> Result<Vec<ProceduralNode>, String> {
291    let mut out = Vec::new();
292    for node in store.find_by_type("procedural")? {
293        if !node_matches_agent(&node, agent_id) {
294            continue;
295        }
296        if let AinlNodeType::Procedural { procedural } = &node.node_type {
297            let total = procedural.success_count.saturating_add(procedural.failure_count);
298            if total > 0 && procedural.success_rate < threshold {
299                out.push(procedural.clone());
300            }
301        }
302    }
303    Ok(out)
304}
305
306// --- Persona helpers ---
307
308pub fn recall_strength_history(store: &dyn GraphStore, node_id: Uuid) -> Result<Vec<StrengthEvent>, String> {
309    let Some(node) = store.read_node(node_id)? else {
310        return Ok(Vec::new());
311    };
312    let mut events = match &node.node_type {
313        AinlNodeType::Persona { persona } => persona.evolution_log.clone(),
314        _ => return Ok(Vec::new()),
315    };
316    events.sort_by_key(|e| e.timestamp);
317    Ok(events)
318}
319
320pub fn recall_delta_by_relevance(
321    store: &dyn GraphStore,
322    agent_id: &str,
323    min_relevance: f32,
324) -> Result<Vec<PersonaNode>, String> {
325    let mut out = Vec::new();
326    for node in store.find_by_type("persona")? {
327        if !node_matches_agent(&node, agent_id) {
328            continue;
329        }
330        if let AinlNodeType::Persona { persona } = &node.node_type {
331            if persona.layer == PersonaLayer::Delta && persona.relevance_score >= min_relevance {
332                out.push(persona.clone());
333            }
334        }
335    }
336    Ok(out)
337}
338
339// --- GraphQuery (builder over SqliteGraphStore, v0.1.4+) ---
340
341/// Builder-style queries scoped to one `agent_id` (matches `json_extract(payload, '$.agent_id')`).
342pub struct GraphQuery<'a> {
343    store: &'a SqliteGraphStore,
344    agent_id: String,
345}
346
347impl SqliteGraphStore {
348    pub fn query<'a>(&'a self, agent_id: &str) -> GraphQuery<'a> {
349        GraphQuery {
350            store: self,
351            agent_id: agent_id.to_string(),
352        }
353    }
354}
355
356fn load_nodes_from_payload_rows(
357    rows: impl Iterator<Item = Result<String, rusqlite::Error>>,
358) -> Result<Vec<AinlMemoryNode>, String> {
359    let mut out = Vec::new();
360    for row in rows {
361        let payload = row.map_err(|e| e.to_string())?;
362        let node: AinlMemoryNode = serde_json::from_str(&payload).map_err(|e| e.to_string())?;
363        out.push(node);
364    }
365    Ok(out)
366}
367
368impl<'a> GraphQuery<'a> {
369    fn conn(&self) -> &rusqlite::Connection {
370        self.store.conn()
371    }
372
373    pub fn episodes(&self) -> Result<Vec<AinlMemoryNode>, String> {
374        let mut stmt = self
375            .conn()
376            .prepare(
377                "SELECT payload FROM ainl_graph_nodes
378                 WHERE node_type = 'episode'
379                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1",
380            )
381            .map_err(|e| e.to_string())?;
382        let rows = stmt
383            .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
384            .map_err(|e| e.to_string())?;
385        load_nodes_from_payload_rows(rows)
386    }
387
388    pub fn semantic_nodes(&self) -> Result<Vec<AinlMemoryNode>, String> {
389        let mut stmt = self
390            .conn()
391            .prepare(
392                "SELECT payload FROM ainl_graph_nodes
393                 WHERE node_type = 'semantic'
394                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1",
395            )
396            .map_err(|e| e.to_string())?;
397        let rows = stmt
398            .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
399            .map_err(|e| e.to_string())?;
400        load_nodes_from_payload_rows(rows)
401    }
402
403    pub fn procedural_nodes(&self) -> Result<Vec<AinlMemoryNode>, String> {
404        let mut stmt = self
405            .conn()
406            .prepare(
407                "SELECT payload FROM ainl_graph_nodes
408                 WHERE node_type = 'procedural'
409                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1",
410            )
411            .map_err(|e| e.to_string())?;
412        let rows = stmt
413            .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
414            .map_err(|e| e.to_string())?;
415        load_nodes_from_payload_rows(rows)
416    }
417
418    pub fn persona_nodes(&self) -> Result<Vec<AinlMemoryNode>, String> {
419        let mut stmt = self
420            .conn()
421            .prepare(
422                "SELECT payload FROM ainl_graph_nodes
423                 WHERE node_type = 'persona'
424                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1",
425            )
426            .map_err(|e| e.to_string())?;
427        let rows = stmt
428            .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
429            .map_err(|e| e.to_string())?;
430        load_nodes_from_payload_rows(rows)
431    }
432
433    pub fn recent_episodes(&self, limit: usize) -> Result<Vec<AinlMemoryNode>, String> {
434        let mut stmt = self
435            .conn()
436            .prepare(
437                "SELECT payload FROM ainl_graph_nodes
438                 WHERE node_type = 'episode'
439                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
440                 ORDER BY timestamp DESC
441                 LIMIT ?2",
442            )
443            .map_err(|e| e.to_string())?;
444        let rows = stmt
445            .query_map(params![&self.agent_id, limit as i64], |row| row.get::<_, String>(0))
446            .map_err(|e| e.to_string())?;
447        load_nodes_from_payload_rows(rows)
448    }
449
450    pub fn since(&self, ts: DateTime<Utc>, node_type: &str) -> Result<Vec<AinlMemoryNode>, String> {
451        let col = node_type.to_ascii_lowercase();
452        let since_ts = ts.timestamp();
453        let mut stmt = self
454            .conn()
455            .prepare(
456                "SELECT payload FROM ainl_graph_nodes
457                 WHERE node_type = ?1
458                   AND timestamp >= ?2
459                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?3
460                 ORDER BY timestamp ASC",
461            )
462            .map_err(|e| e.to_string())?;
463        let rows = stmt
464            .query_map(params![&col, since_ts, &self.agent_id], |row| row.get::<_, String>(0))
465            .map_err(|e| e.to_string())?;
466        load_nodes_from_payload_rows(rows)
467    }
468
469    /// All directed edges whose **both** endpoints are nodes for this `agent_id` (same rule as [`SqliteGraphStore::export_graph`]).
470    pub fn subgraph_edges(&self) -> Result<Vec<SnapshotEdge>, String> {
471        self.store.agent_subgraph_edges(&self.agent_id)
472    }
473
474    pub fn neighbors(&self, node_id: Uuid, edge_type: &str) -> Result<Vec<AinlMemoryNode>, String> {
475        let mut stmt = self
476            .conn()
477            .prepare(
478                "SELECT to_id FROM ainl_graph_edges
479                 WHERE from_id = ?1 AND label = ?2",
480            )
481            .map_err(|e| e.to_string())?;
482        let ids: Vec<String> = stmt
483            .query_map(params![node_id.to_string(), edge_type], |row| row.get(0))
484            .map_err(|e| e.to_string())?
485            .collect::<Result<Vec<_>, _>>()
486            .map_err(|e| e.to_string())?;
487        let mut out = Vec::new();
488        for sid in ids {
489            let id = Uuid::parse_str(&sid).map_err(|e| e.to_string())?;
490            if let Some(n) = self.store.read_node(id)? {
491                out.push(n);
492            }
493        }
494        Ok(out)
495    }
496
497    pub fn lineage(&self, node_id: Uuid) -> Result<Vec<AinlMemoryNode>, String> {
498        let mut visited: HashSet<Uuid> = HashSet::new();
499        let mut out = Vec::new();
500        let mut queue: VecDeque<(Uuid, u32)> = VecDeque::new();
501        visited.insert(node_id);
502        queue.push_back((node_id, 0));
503
504        while let Some((nid, depth)) = queue.pop_front() {
505            if depth >= 20 {
506                continue;
507            }
508            let mut stmt = self
509                .conn()
510                .prepare(
511                    "SELECT to_id FROM ainl_graph_edges
512                     WHERE from_id = ?1 AND label IN ('DERIVED_FROM', 'CAUSED_PATCH')",
513                )
514                .map_err(|e| e.to_string())?;
515            let targets: Vec<String> = stmt
516                .query_map(params![nid.to_string()], |row| row.get(0))
517                .map_err(|e| e.to_string())?
518                .collect::<Result<Vec<_>, _>>()
519                .map_err(|e| e.to_string())?;
520            for sid in targets {
521                let tid = Uuid::parse_str(&sid).map_err(|e| e.to_string())?;
522                if visited.insert(tid) {
523                    if let Some(n) = self.store.read_node(tid)? {
524                        out.push(n.clone());
525                        queue.push_back((tid, depth + 1));
526                    }
527                }
528            }
529        }
530
531        Ok(out)
532    }
533
534    pub fn by_tag(&self, tag: &str) -> Result<Vec<AinlMemoryNode>, String> {
535        let mut stmt = self
536            .conn()
537            .prepare(
538                "SELECT DISTINCT n.payload FROM ainl_graph_nodes n
539                 WHERE COALESCE(json_extract(n.payload, '$.agent_id'), '') = ?1
540                   AND (
541                     EXISTS (
542                       SELECT 1 FROM json_each(n.payload, '$.node_type.persona_signals_emitted') j
543                       WHERE j.value = ?2
544                     )
545                     OR EXISTS (
546                       SELECT 1 FROM json_each(n.payload, '$.node_type.tags') j
547                       WHERE j.value = ?2
548                     )
549                   )",
550            )
551            .map_err(|e| e.to_string())?;
552        let rows = stmt
553            .query_map(params![&self.agent_id, tag], |row| row.get::<_, String>(0))
554            .map_err(|e| e.to_string())?;
555        load_nodes_from_payload_rows(rows)
556    }
557
558    pub fn by_topic_cluster(&self, cluster: &str) -> Result<Vec<AinlMemoryNode>, String> {
559        let like = format!("%{cluster}%");
560        let mut stmt = self
561            .conn()
562            .prepare(
563                "SELECT payload FROM ainl_graph_nodes
564                 WHERE node_type = 'semantic'
565                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
566                   AND json_extract(payload, '$.node_type.topic_cluster') LIKE ?2 ESCAPE '\\'",
567            )
568            .map_err(|e| e.to_string())?;
569        let rows = stmt
570            .query_map(params![&self.agent_id, like], |row| row.get::<_, String>(0))
571            .map_err(|e| e.to_string())?;
572        load_nodes_from_payload_rows(rows)
573    }
574
575    pub fn pattern_by_name(&self, name: &str) -> Result<Option<AinlMemoryNode>, String> {
576        let mut stmt = self
577            .conn()
578            .prepare(
579                "SELECT payload FROM ainl_graph_nodes
580                 WHERE node_type = 'procedural'
581                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
582                   AND (
583                     json_extract(payload, '$.node_type.pattern_name') = ?2
584                     OR json_extract(payload, '$.node_type.label') = ?2
585                   )
586                 ORDER BY timestamp DESC
587                 LIMIT 1",
588            )
589            .map_err(|e| e.to_string())?;
590        let row = stmt
591            .query_row(params![&self.agent_id, name], |row| row.get::<_, String>(0))
592            .optional()
593            .map_err(|e| e.to_string())?;
594        match row {
595            Some(payload) => {
596                let node: AinlMemoryNode = serde_json::from_str(&payload).map_err(|e| e.to_string())?;
597                Ok(Some(node))
598            }
599            None => Ok(None),
600        }
601    }
602
603    pub fn active_patches(&self) -> Result<Vec<AinlMemoryNode>, String> {
604        let mut stmt = self
605            .conn()
606            .prepare(
607                "SELECT payload FROM ainl_graph_nodes
608                 WHERE node_type = 'procedural'
609                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
610                   AND (
611                     json_extract(payload, '$.node_type.retired') IS NULL
612                     OR json_extract(payload, '$.node_type.retired') = 0
613                     OR CAST(json_extract(payload, '$.node_type.retired') AS TEXT) = 'false'
614                   )",
615            )
616            .map_err(|e| e.to_string())?;
617        let rows = stmt
618            .query_map(params![&self.agent_id], |row| row.get::<_, String>(0))
619            .map_err(|e| e.to_string())?;
620        load_nodes_from_payload_rows(rows)
621    }
622
623    pub fn successful_episodes(&self, limit: usize) -> Result<Vec<AinlMemoryNode>, String> {
624        let mut stmt = self
625            .conn()
626            .prepare(
627                "SELECT payload FROM ainl_graph_nodes
628                 WHERE node_type = 'episode'
629                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
630                   AND json_extract(payload, '$.node_type.outcome') = 'success'
631                 ORDER BY timestamp DESC
632                 LIMIT ?2",
633            )
634            .map_err(|e| e.to_string())?;
635        let rows = stmt
636            .query_map(params![&self.agent_id, limit as i64], |row| row.get::<_, String>(0))
637            .map_err(|e| e.to_string())?;
638        load_nodes_from_payload_rows(rows)
639    }
640
641    pub fn episodes_with_tool(
642        &self,
643        tool_name: &str,
644        limit: usize,
645    ) -> 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 (
653                     EXISTS (
654                       SELECT 1 FROM json_each(json_extract(payload, '$.node_type.tools_invoked')) e
655                       WHERE e.value = ?2
656                     )
657                     OR EXISTS (
658                       SELECT 1 FROM json_each(json_extract(payload, '$.node_type.tool_calls')) e
659                       WHERE e.value = ?2
660                     )
661                   )
662                 ORDER BY timestamp DESC
663                 LIMIT ?3",
664            )
665            .map_err(|e| e.to_string())?;
666        let rows = stmt
667            .query_map(
668                params![&self.agent_id, tool_name, limit as i64],
669                |row| row.get::<_, String>(0),
670            )
671            .map_err(|e| e.to_string())?;
672        load_nodes_from_payload_rows(rows)
673    }
674
675    pub fn evolved_persona(&self) -> Result<Option<AinlMemoryNode>, String> {
676        let mut stmt = self
677            .conn()
678            .prepare(
679                "SELECT payload FROM ainl_graph_nodes
680                 WHERE node_type = 'persona'
681                   AND COALESCE(json_extract(payload, '$.agent_id'), '') = ?1
682                   AND json_extract(payload, '$.node_type.trait_name') = 'axis_evolution_snapshot'
683                 ORDER BY timestamp DESC
684                 LIMIT 1",
685            )
686            .map_err(|e| e.to_string())?;
687        let row = stmt
688            .query_row(params![&self.agent_id], |row| row.get::<_, String>(0))
689            .optional()
690            .map_err(|e| e.to_string())?;
691        match row {
692            Some(payload) => {
693                let node: AinlMemoryNode = serde_json::from_str(&payload).map_err(|e| e.to_string())?;
694                Ok(Some(node))
695            }
696            None => Ok(None),
697        }
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704    use crate::node::AinlMemoryNode;
705    use crate::store::SqliteGraphStore;
706
707    #[test]
708    fn test_recall_recent_with_tool_filter() {
709        let temp_dir = std::env::temp_dir();
710        let db_path = temp_dir.join("ainl_query_test_recall.db");
711        let _ = std::fs::remove_file(&db_path);
712
713        let store = SqliteGraphStore::open(&db_path).expect("Failed to open store");
714
715        let now = chrono::Utc::now().timestamp();
716
717        let node1 = AinlMemoryNode::new_episode(
718            uuid::Uuid::new_v4(),
719            now,
720            vec!["file_read".to_string()],
721            None,
722            None,
723        );
724
725        let node2 = AinlMemoryNode::new_episode(
726            uuid::Uuid::new_v4(),
727            now + 1,
728            vec!["agent_delegate".to_string()],
729            Some("agent-B".to_string()),
730            None,
731        );
732
733        store.write_node(&node1).expect("Write failed");
734        store.write_node(&node2).expect("Write failed");
735
736        let delegations = recall_recent(&store, now - 100, 10, Some("agent_delegate"))
737            .expect("Query failed");
738
739        assert_eq!(delegations.len(), 1);
740    }
741
742    #[test]
743    fn test_find_high_confidence_facts() {
744        let temp_dir = std::env::temp_dir();
745        let db_path = temp_dir.join("ainl_query_test_facts.db");
746        let _ = std::fs::remove_file(&db_path);
747
748        let store = SqliteGraphStore::open(&db_path).expect("Failed to open store");
749
750        let turn_id = uuid::Uuid::new_v4();
751
752        let fact1 = AinlMemoryNode::new_fact("User prefers Rust".to_string(), 0.95, turn_id);
753        let fact2 = AinlMemoryNode::new_fact("User dislikes Python".to_string(), 0.45, turn_id);
754
755        store.write_node(&fact1).expect("Write failed");
756        store.write_node(&fact2).expect("Write failed");
757
758        let high_conf = find_high_confidence_facts(&store, 0.7).expect("Query failed");
759
760        assert_eq!(high_conf.len(), 1);
761    }
762
763    #[test]
764    fn test_query_active_patches() {
765        let path = std::env::temp_dir().join(format!("ainl_query_active_patch_{}.db", uuid::Uuid::new_v4()));
766        let _ = std::fs::remove_file(&path);
767        let store = SqliteGraphStore::open(&path).expect("open");
768        let ag = "agent-active-patch";
769        let mut p1 = AinlMemoryNode::new_pattern("pat_one".into(), vec![1, 2]);
770        p1.agent_id = ag.into();
771        let mut p2 = AinlMemoryNode::new_pattern("pat_two".into(), vec![3, 4]);
772        p2.agent_id = ag.into();
773        store.write_node(&p1).expect("w1");
774        store.write_node(&p2).expect("w2");
775
776        let conn = store.conn();
777        let payload2: String = conn
778            .query_row(
779                "SELECT payload FROM ainl_graph_nodes WHERE id = ?1",
780                [p2.id.to_string()],
781                |row| row.get(0),
782            )
783            .unwrap();
784        let mut v: serde_json::Value = serde_json::from_str(&payload2).unwrap();
785        v["node_type"]["retired"] = serde_json::json!(true);
786        conn.execute(
787            "UPDATE ainl_graph_nodes SET payload = ?1 WHERE id = ?2",
788            rusqlite::params![v.to_string(), p2.id.to_string()],
789        )
790        .unwrap();
791
792        let active = store.query(ag).active_patches().expect("q");
793        assert_eq!(active.len(), 1);
794        assert_eq!(active[0].id, p1.id);
795    }
796}