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, 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
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(
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
206// --- Episodic helpers ---
207
208pub 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
268// --- Procedural helpers ---
269
270pub 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
311// --- Persona helpers ---
312
313pub 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
347// --- GraphQuery (builder over SqliteGraphStore, v0.1.4+) ---
348
349/// Builder-style queries scoped to one `agent_id` (matches `json_extract(payload, '$.agent_id')`).
350pub 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    /// All directed edges whose **both** endpoints are nodes for this `agent_id` (same rule as [`SqliteGraphStore::export_graph`]).
482    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    /// Latest persisted [`RuntimeStateNode`] for this query's `agent_id`.
716    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}