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