Skip to main content

ainl_memory/
query.rs

1//! Graph traversal and querying utilities.
2//!
3//! Two layers:
4//!
5//! 1. **Free functions** (e.g. [`recall_recent`], [`walk_from`], [`find_patterns`]) — take [`GraphStore`]
6//!    plus parameters such as `agent_id` where filtering applies.
7//! 2. **[`GraphQuery`]** — returned by [`SqliteGraphStore::query`]; holds `agent_id` and offers SQL-backed
8//!    filters (`episodes`, `lineage`, `by_tag`, `subgraph_edges`, [`GraphQuery::read_runtime_state`], …).
9//!
10//! Serialized node JSON uses top-level `agent_id` and nested `node_type` fields (e.g. `$.node_type.outcome`);
11//! edge rows use SQLite columns `from_id`, `to_id`, `label` (see [`crate::snapshot::SnapshotEdge`] for the
12//! export/import naming `source_id` / `target_id` / `edge_type`).
13
14use 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
29/// Walk the graph from a starting node, following edges with a specific label
30pub 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
70/// Recall recent episodes, optionally filtered by tool usage
71pub 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
94/// Find procedural patterns by name prefix
95pub 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
112/// Find semantic facts with confidence above a threshold
113pub 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
128/// Find persona traits sorted by strength
129pub 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
149// --- Semantic helpers ---
150
151pub 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
215// --- Episodic helpers ---
216
217pub 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
277// --- Procedural helpers ---
278
279pub 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
320// --- Persona helpers ---
321
322pub 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
356// --- GraphQuery (builder over SqliteGraphStore, v0.1.4+) ---
357
358/// Builder-style queries scoped to one `agent_id` (matches `json_extract(payload, '$.agent_id')`).
359pub 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    /// All directed edges whose **both** endpoints are nodes for this `agent_id` (same rule as [`SqliteGraphStore::export_graph`]).
491    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    /// Latest persisted [`RuntimeStateNode`] for this query's `agent_id`.
725    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}