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 pub vitals_gate: Option<String>,
224 pub vitals_phase: Option<String>,
225 pub vitals_trust: Option<f32>,
226}
227
228#[derive(Debug, Clone)]
235pub struct MemoryContext {
236 pub recent_episodes: Vec<AinlMemoryNode>,
237 pub relevant_semantic: Vec<AinlMemoryNode>,
238 pub active_patches: Vec<AinlMemoryNode>,
239 pub persona_snapshot: Option<PersonaSnapshot>,
240 pub compiled_at: DateTime<Utc>,
241}
242
243impl Default for MemoryContext {
244 fn default() -> Self {
245 Self {
246 recent_episodes: Vec::new(),
247 relevant_semantic: Vec::new(),
248 active_patches: Vec::new(),
249 persona_snapshot: None,
250 compiled_at: Utc::now(),
251 }
252 }
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261pub enum TurnPhase {
262 EpisodeWrite,
264 FitnessWriteBack,
266 ExtractionPass,
268 PatternPersistence,
270 PersonaEvolution,
272 ExportRefresh,
274 RuntimeStatePersist,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct TurnWarning {
281 pub phase: TurnPhase,
282 pub error: String,
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum TurnStatus {
288 Ok,
289 StepLimitExceeded { steps_executed: u32 },
290 GraphMemoryDisabled,
291}
292
293#[derive(Debug, Clone)]
295pub struct TurnResult {
296 pub episode_id: Uuid,
297 pub persona_prompt_contribution: Option<String>,
298 pub memory_context: MemoryContext,
299 pub extraction_report: Option<ExtractionReport>,
300 pub steps_executed: u32,
301 pub patch_dispatch_results: Vec<PatchDispatchResult>,
302 pub status: TurnStatus,
303 pub vitals_gate: Option<String>,
305 pub vitals_phase: Option<String>,
306 pub vitals_trust: Option<f32>,
307}
308
309impl Default for TurnResult {
310 fn default() -> Self {
311 Self {
312 episode_id: Uuid::nil(),
313 persona_prompt_contribution: None,
314 memory_context: MemoryContext::default(),
315 extraction_report: None,
316 steps_executed: 0,
317 patch_dispatch_results: Vec::new(),
318 status: TurnStatus::Ok,
319 vitals_gate: None,
320 vitals_phase: None,
321 vitals_trust: None,
322 }
323 }
324}
325
326#[derive(Debug, Clone)]
328pub enum TurnOutcome {
329 Complete(TurnResult),
331 PartialSuccess {
333 result: TurnResult,
334 warnings: Vec<TurnWarning>,
335 },
336}
337
338impl TurnOutcome {
339 pub fn result(&self) -> &TurnResult {
340 match self {
341 TurnOutcome::Complete(r) | TurnOutcome::PartialSuccess { result: r, .. } => r,
342 }
343 }
344
345 pub fn warnings(&self) -> &[TurnWarning] {
346 match self {
347 TurnOutcome::Complete(_) => &[],
348 TurnOutcome::PartialSuccess { warnings, .. } => warnings.as_slice(),
349 }
350 }
351
352 pub fn into_result(self) -> TurnResult {
353 match self {
354 TurnOutcome::Complete(r) | TurnOutcome::PartialSuccess { result: r, .. } => r,
355 }
356 }
357
358 pub fn is_complete(&self) -> bool {
359 matches!(self, TurnOutcome::Complete(_))
360 }
361
362 pub fn is_partial_success(&self) -> bool {
363 matches!(self, TurnOutcome::PartialSuccess { .. })
364 }
365
366 pub fn turn_status(&self) -> TurnStatus {
367 self.result().status
368 }
369}