1use 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
20pub fn walk_from(
22 store: &dyn GraphStore,
23 start_id: Uuid,
24 edge_label: &str,
25 max_depth: usize,
26) -> Result<Vec<AinlMemoryNode>, String> {
27 let mut visited = std::collections::HashSet::new();
28 let mut result = Vec::new();
29 let mut current_level = vec![start_id];
30
31 for _ in 0..max_depth {
32 if current_level.is_empty() {
33 break;
34 }
35
36 let mut next_level = Vec::new();
37
38 for node_id in current_level {
39 if visited.contains(&node_id) {
40 continue;
41 }
42 visited.insert(node_id);
43
44 if let Some(node) = store.read_node(node_id)? {
45 result.push(node.clone());
46
47 for next_node in store.walk_edges(node_id, edge_label)? {
48 if !visited.contains(&next_node.id) {
49 next_level.push(next_node.id);
50 }
51 }
52 }
53 }
54
55 current_level = next_level;
56 }
57
58 Ok(result)
59}
60
61pub fn recall_recent(
63 store: &dyn GraphStore,
64 since_timestamp: i64,
65 limit: usize,
66 tool_filter: Option<&str>,
67) -> Result<Vec<AinlMemoryNode>, String> {
68 let episodes = store.query_episodes_since(since_timestamp, limit)?;
69
70 if let Some(tool_name) = tool_filter {
71 Ok(episodes
72 .into_iter()
73 .filter(|node| match &node.node_type {
74 AinlNodeType::Episode { episodic } => {
75 episodic.effective_tools().contains(&tool_name.to_string())
76 }
77 _ => false,
78 })
79 .collect())
80 } else {
81 Ok(episodes)
82 }
83}
84
85pub fn find_patterns(
87 store: &dyn GraphStore,
88 name_prefix: &str,
89) -> Result<Vec<AinlMemoryNode>, String> {
90 let all_procedural = store.find_by_type("procedural")?;
91
92 Ok(all_procedural
93 .into_iter()
94 .filter(|node| match &node.node_type {
95 AinlNodeType::Procedural { procedural } => {
96 procedural.pattern_name.starts_with(name_prefix)
97 }
98 _ => false,
99 })
100 .collect())
101}
102
103pub fn find_high_confidence_facts(
105 store: &dyn GraphStore,
106 min_confidence: f32,
107) -> Result<Vec<AinlMemoryNode>, String> {
108 let all_semantic = store.find_by_type("semantic")?;
109
110 Ok(all_semantic
111 .into_iter()
112 .filter(|node| match &node.node_type {
113 AinlNodeType::Semantic { semantic } => semantic.confidence >= min_confidence,
114 _ => false,
115 })
116 .collect())
117}
118
119pub fn find_strong_traits(store: &dyn GraphStore) -> Result<Vec<AinlMemoryNode>, String> {
121 let mut all_persona = store.find_by_type("persona")?;
122
123 all_persona.sort_by(|a, b| {
124 let strength_a = match &a.node_type {
125 AinlNodeType::Persona { persona } => persona.strength,
126 _ => 0.0,
127 };
128 let strength_b = match &b.node_type {
129 AinlNodeType::Persona { persona } => persona.strength,
130 _ => 0.0,
131 };
132 strength_b
133 .partial_cmp(&strength_a)
134 .unwrap_or(std::cmp::Ordering::Equal)
135 });
136
137 Ok(all_persona)
138}
139
140pub fn recall_by_topic_cluster(
143 store: &dyn GraphStore,
144 agent_id: &str,
145 cluster: &str,
146) -> Result<Vec<SemanticNode>, String> {
147 let mut out = Vec::new();
148 for node in store.find_by_type("semantic")? {
149 if !node_matches_agent(&node, agent_id) {
150 continue;
151 }
152 if let AinlNodeType::Semantic { semantic } = &node.node_type {
153 if semantic.topic_cluster.as_deref() == Some(cluster) {
154 out.push(semantic.clone());
155 }
156 }
157 }
158 Ok(out)
159}
160
161pub fn recall_contradictions(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
203pub 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
265pub 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
306pub 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
339pub 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 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}