1mod adapters;
16mod engine;
17mod hooks;
18mod runtime;
19
20pub use adapters::{AdapterRegistry, PatchAdapter};
21
22pub use ainl_graph_extractor::{run_extraction_pass, ExtractionReport, GraphExtractorTask};
23pub use ainl_persona::axes::default_axis_map;
24pub use ainl_persona::{
25 EvolutionEngine, MemoryNodeType, PersonaAxis, PersonaSnapshot, RawSignal, EVOLUTION_TRAIT_NAME,
26 INGEST_SCORE_EPSILON,
27};
28
29pub use engine::{
30 AinlGraphArtifact, MemoryContext, PatchDispatchResult, PatchSkipReason, TurnInput, TurnOutcome,
31 TurnOutput, EMIT_TO_EDGE,
32};
33pub use hooks::{NoOpHooks, TurnHooks};
34pub use runtime::AinlRuntime;
35
36use ainl_memory::{GraphMemory, GraphStore};
37use serde::{Deserialize, Serialize};
38
39#[derive(Serialize, Deserialize, Debug, Clone)]
41#[serde(default)]
42pub struct RuntimeConfig {
43 pub agent_id: String,
45 pub max_delegation_depth: u32,
47 pub enable_graph_memory: bool,
48 pub max_steps: u32,
50 pub extraction_interval: u32,
52}
53
54impl Default for RuntimeConfig {
55 fn default() -> Self {
56 Self {
57 agent_id: String::new(),
58 max_delegation_depth: 10,
59 enable_graph_memory: true,
60 max_steps: 1000,
61 extraction_interval: 10,
62 }
63 }
64}
65
66pub struct RuntimeContext {
68 _config: RuntimeConfig,
69 memory: Option<GraphMemory>,
70 extractor: Option<GraphExtractorTask>,
71}
72
73impl RuntimeContext {
74 pub fn new(config: RuntimeConfig, memory: Option<GraphMemory>) -> Self {
76 Self {
77 _config: config,
78 memory,
79 extractor: None,
80 }
81 }
82
83 pub fn record_delegation(
85 &self,
86 delegated_to: String,
87 trace_event: Option<serde_json::Value>,
88 ) -> Result<uuid::Uuid, String> {
89 if let Some(ref memory) = self.memory {
90 memory.write_episode(
91 vec!["agent_delegate".to_string()],
92 Some(delegated_to),
93 trace_event,
94 )
95 } else {
96 Err("Memory not initialized".to_string())
97 }
98 }
99
100 pub fn record_tool_execution(
102 &self,
103 tool_name: String,
104 trace_event: Option<serde_json::Value>,
105 ) -> Result<uuid::Uuid, String> {
106 if let Some(ref memory) = self.memory {
107 memory.write_episode(vec![tool_name], None, trace_event)
108 } else {
109 Err("Memory not initialized".to_string())
110 }
111 }
112
113 pub fn record_episode(
115 &self,
116 tool_calls: Vec<String>,
117 delegation_to: Option<String>,
118 trace_event: Option<serde_json::Value>,
119 ) -> Result<uuid::Uuid, String> {
120 if let Some(ref memory) = self.memory {
121 memory.write_episode(tool_calls, delegation_to, trace_event)
122 } else {
123 Err("Memory not initialized".to_string())
124 }
125 }
126
127 pub fn store(&self) -> Option<&dyn GraphStore> {
129 self.memory.as_ref().map(|m| m.store())
130 }
131
132 pub fn run_graph_extraction_pass(&mut self) -> Result<ExtractionReport, String> {
134 if self._config.agent_id.is_empty() {
135 return Err("RuntimeConfig.agent_id is required for graph extraction".to_string());
136 }
137 let memory = self
138 .memory
139 .as_ref()
140 .ok_or_else(|| "Graph memory is required for graph extraction".to_string())?;
141
142 self.extractor
143 .get_or_insert_with(|| GraphExtractorTask::new(&self._config.agent_id));
144 let store = memory.sqlite_store();
145 let report = self
146 .extractor
147 .as_mut()
148 .expect("get_or_insert_with always leaves Some")
149 .run_pass(store)?;
150
151 tracing::info!(
152 agent_id = %report.agent_id,
153 signals_extracted = report.signals_extracted,
154 signals_applied = report.signals_applied,
155 semantic_nodes_updated = report.semantic_nodes_updated,
156 "ainl-graph-extractor pass completed"
157 );
158 Ok(report)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use ainl_memory::{AinlMemoryNode, SqliteGraphStore};
166 use std::path::PathBuf;
167 use uuid::Uuid;
168
169 #[test]
170 fn test_runtime_config_default() {
171 let config = RuntimeConfig::default();
172 assert_eq!(config.max_delegation_depth, 10);
173 assert!(config.enable_graph_memory);
174 assert!(config.agent_id.is_empty());
175 assert_eq!(config.max_steps, 1000);
176 assert_eq!(config.extraction_interval, 10);
177 }
178
179 #[test]
180 fn extraction_pass_requires_agent_id() {
181 let dir = tempfile::tempdir().unwrap();
182 let db = dir.path().join("t.db");
183 let mem = GraphMemory::new(&db).unwrap();
184 let mut ctx = RuntimeContext::new(RuntimeConfig::default(), Some(mem));
185 let err = ctx.run_graph_extraction_pass().unwrap_err();
186 assert!(err.contains("agent_id"));
187 }
188
189 #[test]
190 fn extraction_pass_runs_with_memory_and_agent() {
191 let dir = tempfile::tempdir().unwrap();
192 let db: PathBuf = dir.path().join("t.db");
193 let mem = GraphMemory::new(&db).unwrap();
194 let cfg = RuntimeConfig {
195 agent_id: "agent-test".into(),
196 ..RuntimeConfig::default()
197 };
198 let mut ctx = RuntimeContext::new(cfg, Some(mem));
199 ctx.record_tool_execution("noop".into(), None).unwrap();
200 let report = ctx.run_graph_extraction_pass().expect("extraction");
201 assert_eq!(report.agent_id, "agent-test");
202 }
203
204 #[test]
205 fn ainl_runtime_run_turn_smoke() {
206 let dir = tempfile::tempdir().unwrap();
207 let db = dir.path().join("rt.db");
208 let _ = std::fs::remove_file(&db);
209 let store = SqliteGraphStore::open(&db).unwrap();
210 let ag = "rt-agent";
211 let mut ep = AinlMemoryNode::new_episode(Uuid::new_v4(), 3_000_000_000, vec![], None, None);
212 ep.agent_id = ag.into();
213 store.write_node(&ep).unwrap();
214
215 let cfg = RuntimeConfig {
216 agent_id: ag.into(),
217 extraction_interval: 1,
218 max_steps: 50,
219 ..RuntimeConfig::default()
220 };
221 let mut rt = AinlRuntime::new(cfg, store);
222 let art = rt.load_artifact().expect("load");
223 assert!(art.validation.is_valid);
224
225 let out = rt
226 .run_turn(TurnInput {
227 user_message: "hello".into(),
228 tools_invoked: vec!["noop".into()],
229 trace_event: None,
230 depth: 0,
231 ..Default::default()
232 })
233 .expect("turn");
234 assert!(matches!(out.outcome, TurnOutcome::Success));
235 assert_ne!(out.episode_id, Uuid::nil());
236 assert!(out.extraction_report.is_some());
237 }
238}