1use std::collections::HashMap;
4use std::error::Error;
5use std::fmt;
6
7use ainl_graph_extractor::ExtractionReport;
8use ainl_memory::{
9 AgentGraphSnapshot, AinlMemoryNode, AinlNodeType, GraphValidationReport, ProceduralNode,
10 SqliteGraphStore,
11};
12use ainl_persona::PersonaSnapshot;
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use uuid::Uuid;
16
17pub const EMIT_TO_EDGE: &str = "EMIT_TO";
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum AinlRuntimeError {
23 DelegationDepthExceeded {
25 depth: u32,
26 max: u32,
27 },
28 Message(String),
29 AsyncJoinError(String),
31 AsyncStoreError(String),
34}
35
36impl fmt::Display for AinlRuntimeError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 AinlRuntimeError::DelegationDepthExceeded { depth, max } => {
40 write!(f, "delegation depth exceeded (depth={depth}, max={max})")
41 }
42 AinlRuntimeError::Message(s) => f.write_str(s),
43 AinlRuntimeError::AsyncJoinError(s) => write!(f, "async join error: {s}"),
44 AinlRuntimeError::AsyncStoreError(s) => write!(f, "async store error: {s}"),
45 }
46 }
47}
48
49impl Error for AinlRuntimeError {}
50
51impl From<String> for AinlRuntimeError {
52 fn from(s: String) -> Self {
53 Self::Message(s)
54 }
55}
56
57impl AinlRuntimeError {
58 #[must_use]
60 pub fn message_str(&self) -> Option<&str> {
61 match self {
62 Self::Message(s) => Some(s.as_str()),
63 Self::DelegationDepthExceeded { .. } => None,
64 Self::AsyncJoinError(s) => Some(s.as_str()),
65 Self::AsyncStoreError(s) => Some(s.as_str()),
66 }
67 }
68
69 #[must_use]
70 pub fn is_delegation_depth_exceeded(&self) -> bool {
71 matches!(self, Self::DelegationDepthExceeded { .. })
72 }
73
74 #[must_use]
75 pub fn is_async_join_error(&self) -> bool {
76 matches!(self, Self::AsyncJoinError(_))
77 }
78
79 #[must_use]
80 pub fn is_async_store_error(&self) -> bool {
81 matches!(self, Self::AsyncStoreError(_))
82 }
83
84 #[must_use]
86 pub fn delegation_depth_exceeded(&self) -> Option<(u32, u32)> {
87 match self {
88 Self::DelegationDepthExceeded { depth, max } => Some((*depth, *max)),
89 Self::Message(_) | Self::AsyncJoinError(_) | Self::AsyncStoreError(_) => None,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy)]
100pub struct PatchDispatchContext<'a> {
101 pub patch_label: &'a str,
102 pub node: &'a AinlMemoryNode,
103 pub frame: &'a HashMap<String, serde_json::Value>,
104}
105
106impl<'a> PatchDispatchContext<'a> {
107 pub fn procedural(&self) -> Option<&'a ProceduralNode> {
108 match &self.node.node_type {
109 AinlNodeType::Procedural { procedural } => Some(procedural),
110 _ => None,
111 }
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct PatchDispatchResult {
118 pub label: String,
119 pub patch_version: u32,
120 pub fitness_before: f32,
121 pub fitness_after: f32,
122 pub dispatched: bool,
123 pub skip_reason: Option<PatchSkipReason>,
124 pub adapter_output: Option<serde_json::Value>,
126 pub adapter_name: Option<String>,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum PatchSkipReason {
132 MissingDeclaredRead(String),
133 Retired,
134 ZeroVersion,
135 NotProcedural,
137 PersistFailed(String),
139}
140
141impl fmt::Display for PatchSkipReason {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 PatchSkipReason::MissingDeclaredRead(s) => write!(f, "missing_declared_read:{s}"),
145 PatchSkipReason::Retired => write!(f, "retired"),
146 PatchSkipReason::ZeroVersion => write!(f, "zero_version"),
147 PatchSkipReason::NotProcedural => write!(f, "not_procedural"),
148 PatchSkipReason::PersistFailed(s) => write!(f, "persist_failed:{s}"),
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct AinlGraphArtifact {
156 pub agent_id: String,
157 pub snapshot: AgentGraphSnapshot,
158 pub validation: GraphValidationReport,
159}
160
161impl AinlGraphArtifact {
162 pub fn load(store: &SqliteGraphStore, agent_id: &str) -> Result<Self, String> {
164 let snapshot = store.export_graph(agent_id)?;
165 let validation = store.validate_graph(agent_id)?;
166 if !validation.is_valid {
167 let mut msg = String::from("graph validation failed: dangling edges");
168 for d in &validation.dangling_edge_details {
169 msg.push_str(&format!(
170 "; {} -> {} [{}]",
171 d.source_id, d.target_id, d.edge_type
172 ));
173 }
174 return Err(msg);
175 }
176 Ok(Self {
177 agent_id: agent_id.to_string(),
178 snapshot,
179 validation,
180 })
181 }
182
183 pub fn from_snapshot(snapshot: AgentGraphSnapshot) -> Self {
185 let agent_id = snapshot.agent_id.clone();
186 let node_count = snapshot.nodes.len();
187 let edge_count = snapshot.edges.len();
188 let validation = GraphValidationReport {
189 agent_id: agent_id.clone(),
190 node_count,
191 edge_count,
192 dangling_edges: Vec::new(),
193 dangling_edge_details: Vec::new(),
194 cross_agent_boundary_edges: 0,
195 orphan_nodes: Vec::new(),
196 is_valid: true,
197 };
198 Self {
199 agent_id,
200 snapshot,
201 validation,
202 }
203 }
204}
205
206#[derive(Debug, Clone, Default, Serialize, Deserialize)]
208#[serde(default)]
209pub struct TurnInput {
210 pub user_message: String,
211 pub tools_invoked: Vec<String>,
212 pub trace_event: Option<serde_json::Value>,
213 pub depth: u32,
215 pub frame: HashMap<String, serde_json::Value>,
217 pub emit_targets: Vec<Uuid>,
220}
221
222#[derive(Debug, Clone)]
229pub struct MemoryContext {
230 pub recent_episodes: Vec<AinlMemoryNode>,
231 pub relevant_semantic: Vec<AinlMemoryNode>,
232 pub active_patches: Vec<AinlMemoryNode>,
233 pub persona_snapshot: Option<PersonaSnapshot>,
234 pub compiled_at: DateTime<Utc>,
235}
236
237impl Default for MemoryContext {
238 fn default() -> Self {
239 Self {
240 recent_episodes: Vec::new(),
241 relevant_semantic: Vec::new(),
242 active_patches: Vec::new(),
243 persona_snapshot: None,
244 compiled_at: Utc::now(),
245 }
246 }
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
251pub enum TurnPhase {
252 EpisodeWrite,
253 FitnessWriteBack,
254 ExtractionPass,
255 PatternPersistence,
256 PersonaEvolution,
257 ExportRefresh,
258 RuntimeStatePersist,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct TurnWarning {
265 pub phase: TurnPhase,
266 pub error: String,
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub enum TurnStatus {
272 Ok,
273 StepLimitExceeded { steps_executed: u32 },
274 GraphMemoryDisabled,
275}
276
277#[derive(Debug, Clone)]
279pub struct TurnResult {
280 pub episode_id: Uuid,
281 pub persona_prompt_contribution: Option<String>,
282 pub memory_context: MemoryContext,
283 pub extraction_report: Option<ExtractionReport>,
284 pub steps_executed: u32,
285 pub patch_dispatch_results: Vec<PatchDispatchResult>,
286 pub status: TurnStatus,
287}
288
289impl Default for TurnResult {
290 fn default() -> Self {
291 Self {
292 episode_id: Uuid::nil(),
293 persona_prompt_contribution: None,
294 memory_context: MemoryContext::default(),
295 extraction_report: None,
296 steps_executed: 0,
297 patch_dispatch_results: Vec::new(),
298 status: TurnStatus::Ok,
299 }
300 }
301}
302
303#[derive(Debug, Clone)]
305pub enum TurnOutcome {
306 Complete(TurnResult),
308 PartialSuccess {
310 result: TurnResult,
311 warnings: Vec<TurnWarning>,
312 },
313}
314
315impl TurnOutcome {
316 pub fn result(&self) -> &TurnResult {
317 match self {
318 TurnOutcome::Complete(r) | TurnOutcome::PartialSuccess { result: r, .. } => r,
319 }
320 }
321
322 pub fn warnings(&self) -> &[TurnWarning] {
323 match self {
324 TurnOutcome::Complete(_) => &[],
325 TurnOutcome::PartialSuccess { warnings, .. } => warnings.as_slice(),
326 }
327 }
328
329 pub fn into_result(self) -> TurnResult {
330 match self {
331 TurnOutcome::Complete(r) | TurnOutcome::PartialSuccess { result: r, .. } => r,
332 }
333 }
334
335 pub fn is_complete(&self) -> bool {
336 matches!(self, TurnOutcome::Complete(_))
337 }
338
339 pub fn is_partial_success(&self) -> bool {
340 matches!(self, TurnOutcome::PartialSuccess { .. })
341 }
342
343 pub fn turn_status(&self) -> TurnStatus {
344 self.result().status
345 }
346}