1mod adapters;
29mod engine;
30mod graph_cell;
31mod hooks;
32mod runtime;
33
34pub use adapters::{AdapterRegistry, GraphPatchAdapter, GraphPatchHostDispatch, PatchAdapter};
35
36pub use ainl_semantic_tagger::infer_topic_tags;
37
38pub use ainl_graph_extractor::{run_extraction_pass, ExtractionReport, GraphExtractorTask};
39pub use ainl_persona::axes::default_axis_map;
40pub use ainl_persona::{
41 EvolutionEngine, MemoryNodeType, PersonaAxis, PersonaSnapshot, RawSignal, EVOLUTION_TRAIT_NAME,
42 INGEST_SCORE_EPSILON,
43};
44
45pub use engine::{
46 AinlGraphArtifact, AinlRuntimeError, MemoryContext, PatchDispatchContext, PatchDispatchResult,
47 PatchSkipReason, TurnInput, TurnOutcome, TurnPhase, TurnResult, TurnStatus, TurnWarning,
48 EMIT_TO_EDGE,
49};
50pub use graph_cell::SqliteStoreRef;
51pub use hooks::{NoOpHooks, TurnHooks};
52#[cfg(feature = "async")]
53pub use hooks::{NoOpAsyncHooks, TurnHooksAsync};
54pub use runtime::AinlRuntime;
55
56use ainl_memory::{GraphMemory, GraphStore};
57use serde::{Deserialize, Serialize};
58
59#[derive(Serialize, Deserialize, Debug, Clone)]
61#[serde(default)]
62pub struct RuntimeConfig {
63 pub agent_id: String,
65 pub max_delegation_depth: u32,
67 pub enable_graph_memory: bool,
68 pub max_steps: u32,
70 pub extraction_interval: u32,
72}
73
74impl Default for RuntimeConfig {
75 fn default() -> Self {
76 Self {
77 agent_id: String::new(),
78 max_delegation_depth: 8,
79 enable_graph_memory: true,
80 max_steps: 1000,
81 extraction_interval: 10,
82 }
83 }
84}
85
86pub struct RuntimeContext {
88 _config: RuntimeConfig,
89 memory: Option<GraphMemory>,
90 extractor: Option<GraphExtractorTask>,
91}
92
93impl RuntimeContext {
94 pub fn new(config: RuntimeConfig, memory: Option<GraphMemory>) -> Self {
96 Self {
97 _config: config,
98 memory,
99 extractor: None,
100 }
101 }
102
103 pub fn record_delegation(
105 &self,
106 delegated_to: String,
107 trace_event: Option<serde_json::Value>,
108 ) -> Result<uuid::Uuid, String> {
109 if let Some(ref memory) = self.memory {
110 memory.write_episode(
111 vec!["agent_delegate".to_string()],
112 Some(delegated_to),
113 trace_event,
114 )
115 } else {
116 Err("Memory not initialized".to_string())
117 }
118 }
119
120 pub fn record_tool_execution(
122 &self,
123 tool_name: String,
124 trace_event: Option<serde_json::Value>,
125 ) -> Result<uuid::Uuid, String> {
126 if let Some(ref memory) = self.memory {
127 memory.write_episode(vec![tool_name], None, trace_event)
128 } else {
129 Err("Memory not initialized".to_string())
130 }
131 }
132
133 pub fn record_episode(
135 &self,
136 tool_calls: Vec<String>,
137 delegation_to: Option<String>,
138 trace_event: Option<serde_json::Value>,
139 ) -> Result<uuid::Uuid, String> {
140 if let Some(ref memory) = self.memory {
141 memory.write_episode(tool_calls, delegation_to, trace_event)
142 } else {
143 Err("Memory not initialized".to_string())
144 }
145 }
146
147 pub fn store(&self) -> Option<&dyn GraphStore> {
149 self.memory.as_ref().map(|m| m.store())
150 }
151
152 pub fn run_graph_extraction_pass(&mut self) -> Result<ExtractionReport, String> {
154 if self._config.agent_id.is_empty() {
155 return Err("RuntimeConfig.agent_id is required for graph extraction".to_string());
156 }
157 let memory = self
158 .memory
159 .as_ref()
160 .ok_or_else(|| "Graph memory is required for graph extraction".to_string())?;
161
162 self.extractor
163 .get_or_insert_with(|| GraphExtractorTask::new(&self._config.agent_id));
164 let store = memory.sqlite_store();
165 let report = self
166 .extractor
167 .as_mut()
168 .expect("get_or_insert_with always leaves Some")
169 .run_pass(store);
170
171 if report.has_errors() {
172 tracing::warn!(
173 agent_id = %report.agent_id,
174 extract_error = ?report.extract_error,
175 pattern_error = ?report.pattern_error,
176 persona_error = ?report.persona_error,
177 "ainl-graph-extractor pass completed with phase errors"
178 );
179 } else {
180 tracing::info!(
181 agent_id = %report.agent_id,
182 signals_extracted = report.signals_extracted,
183 signals_applied = report.signals_applied,
184 semantic_nodes_updated = report.semantic_nodes_updated,
185 "ainl-graph-extractor pass completed"
186 );
187 }
188 Ok(report)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::TurnStatus;
196 use ainl_memory::{AinlMemoryNode, SqliteGraphStore};
197 use std::path::PathBuf;
198 use uuid::Uuid;
199
200 #[test]
201 fn ainl_runtime_error_helpers() {
202 let e = AinlRuntimeError::DelegationDepthExceeded { depth: 2, max: 8 };
203 assert!(e.is_delegation_depth_exceeded());
204 assert_eq!(e.delegation_depth_exceeded(), Some((2, 8)));
205 assert!(e.message_str().is_none());
206
207 let m = AinlRuntimeError::Message("graph validation failed".into());
208 assert!(!m.is_delegation_depth_exceeded());
209 assert!(m.delegation_depth_exceeded().is_none());
210 assert_eq!(m.message_str(), Some("graph validation failed"));
211
212 let from_str: AinlRuntimeError = "via from".to_string().into();
213 assert_eq!(from_str.message_str(), Some("via from"));
214 }
215
216 #[test]
217 fn test_runtime_config_default() {
218 let config = RuntimeConfig::default();
219 assert_eq!(config.max_delegation_depth, 8);
220 assert!(config.enable_graph_memory);
221 assert!(config.agent_id.is_empty());
222 assert_eq!(config.max_steps, 1000);
223 assert_eq!(config.extraction_interval, 10);
224 }
225
226 #[test]
227 fn extraction_pass_requires_agent_id() {
228 let dir = tempfile::tempdir().unwrap();
229 let db = dir.path().join("t.db");
230 let mem = GraphMemory::new(&db).unwrap();
231 let mut ctx = RuntimeContext::new(RuntimeConfig::default(), Some(mem));
232 let err = ctx.run_graph_extraction_pass().unwrap_err();
233 assert!(err.contains("agent_id"));
234 }
235
236 #[test]
237 fn extraction_pass_runs_with_memory_and_agent() {
238 let dir = tempfile::tempdir().unwrap();
239 let db: PathBuf = dir.path().join("t.db");
240 let mem = GraphMemory::new(&db).unwrap();
241 let cfg = RuntimeConfig {
242 agent_id: "agent-test".into(),
243 ..RuntimeConfig::default()
244 };
245 let mut ctx = RuntimeContext::new(cfg, Some(mem));
246 ctx.record_tool_execution("noop".into(), None).unwrap();
247 let report = ctx.run_graph_extraction_pass().expect("extraction");
248 assert_eq!(report.agent_id, "agent-test");
249 }
250
251 #[test]
252 fn ainl_runtime_run_turn_smoke() {
253 let dir = tempfile::tempdir().unwrap();
254 let db = dir.path().join("rt.db");
255 let _ = std::fs::remove_file(&db);
256 let store = SqliteGraphStore::open(&db).unwrap();
257 let ag = "rt-agent";
258 let mut ep = AinlMemoryNode::new_episode(Uuid::new_v4(), 3_000_000_000, vec![], None, None);
259 ep.agent_id = ag.into();
260 store.write_node(&ep).unwrap();
261
262 let cfg = RuntimeConfig {
263 agent_id: ag.into(),
264 extraction_interval: 1,
265 max_steps: 50,
266 ..RuntimeConfig::default()
267 };
268 let mut rt = AinlRuntime::new(cfg, store);
269 let art = rt.load_artifact().expect("load");
270 assert!(art.validation.is_valid);
271
272 let out = rt
273 .run_turn(TurnInput {
274 user_message: "hello".into(),
275 tools_invoked: vec!["noop".into()],
276 trace_event: None,
277 depth: 0,
278 ..Default::default()
279 })
280 .expect("turn");
281 assert!(out.is_complete());
282 assert_eq!(out.turn_status(), TurnStatus::Ok);
283 assert_ne!(out.result().episode_id, Uuid::nil());
284 assert!(out.result().extraction_report.is_some());
285 }
286}