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 pub dispatch_duration_ms: u64,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub enum PatchSkipReason {
134 MissingDeclaredRead(String),
135 Retired,
136 ZeroVersion,
137 NotProcedural,
139 PersistFailed(String),
141}
142
143impl fmt::Display for PatchSkipReason {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 match self {
146 PatchSkipReason::MissingDeclaredRead(s) => write!(f, "missing_declared_read:{s}"),
147 PatchSkipReason::Retired => write!(f, "retired"),
148 PatchSkipReason::ZeroVersion => write!(f, "zero_version"),
149 PatchSkipReason::NotProcedural => write!(f, "not_procedural"),
150 PatchSkipReason::PersistFailed(s) => write!(f, "persist_failed:{s}"),
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
157pub struct AinlGraphArtifact {
158 pub agent_id: String,
159 pub snapshot: AgentGraphSnapshot,
160 pub validation: GraphValidationReport,
161}
162
163impl AinlGraphArtifact {
164 pub fn load(store: &SqliteGraphStore, agent_id: &str) -> Result<Self, String> {
166 let snapshot = store.export_graph(agent_id)?;
167 let validation = store.validate_graph(agent_id)?;
168 if !validation.is_valid {
169 let mut msg = String::from("graph validation failed: dangling edges");
170 for d in &validation.dangling_edge_details {
171 msg.push_str(&format!(
172 "; {} -> {} [{}]",
173 d.source_id, d.target_id, d.edge_type
174 ));
175 }
176 return Err(msg);
177 }
178 Ok(Self {
179 agent_id: agent_id.to_string(),
180 snapshot,
181 validation,
182 })
183 }
184
185 pub fn from_snapshot(snapshot: AgentGraphSnapshot) -> Self {
187 let agent_id = snapshot.agent_id.clone();
188 let node_count = snapshot.nodes.len();
189 let edge_count = snapshot.edges.len();
190 let validation = GraphValidationReport {
191 agent_id: agent_id.clone(),
192 node_count,
193 edge_count,
194 dangling_edges: Vec::new(),
195 dangling_edge_details: Vec::new(),
196 cross_agent_boundary_edges: 0,
197 orphan_nodes: Vec::new(),
198 is_valid: true,
199 };
200 Self {
201 agent_id,
202 snapshot,
203 validation,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
210#[serde(default)]
211pub struct TurnInput {
212 pub user_message: String,
213 pub tools_invoked: Vec<String>,
214 pub trace_event: Option<serde_json::Value>,
215 pub depth: u32,
217 pub frame: HashMap<String, serde_json::Value>,
219 pub emit_targets: Vec<Uuid>,
222 pub vitals_gate: Option<String>,
226 pub vitals_phase: Option<String>,
227 pub vitals_trust: Option<f32>,
228}
229
230#[derive(Debug, Clone)]
237pub struct MemoryContext {
238 pub recent_episodes: Vec<AinlMemoryNode>,
239 pub relevant_semantic: Vec<AinlMemoryNode>,
240 pub active_patches: Vec<AinlMemoryNode>,
241 pub persona_snapshot: Option<PersonaSnapshot>,
242 pub compiled_at: DateTime<Utc>,
243}
244
245impl Default for MemoryContext {
246 fn default() -> Self {
247 Self {
248 recent_episodes: Vec::new(),
249 relevant_semantic: Vec::new(),
250 active_patches: Vec::new(),
251 persona_snapshot: None,
252 compiled_at: Utc::now(),
253 }
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
263pub enum TurnPhase {
264 EpisodeWrite,
266 FitnessWriteBack,
268 ExtractionPass,
270 PatternPersistence,
272 PersonaEvolution,
274 ExportRefresh,
276 RuntimeStatePersist,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct TurnWarning {
283 pub phase: TurnPhase,
284 pub error: String,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum TurnStatus {
290 Ok,
291 StepLimitExceeded { steps_executed: u32 },
292 GraphMemoryDisabled,
293}
294
295#[derive(Debug, Clone)]
297pub struct TurnResult {
298 pub episode_id: Uuid,
299 pub persona_prompt_contribution: Option<String>,
300 pub memory_context: MemoryContext,
301 pub extraction_report: Option<ExtractionReport>,
302 pub steps_executed: u32,
303 pub patch_dispatch_results: Vec<PatchDispatchResult>,
304 pub status: TurnStatus,
305 pub vitals_gate: Option<String>,
307 pub vitals_phase: Option<String>,
308 pub vitals_trust: Option<f32>,
309}
310
311impl Default for TurnResult {
312 fn default() -> Self {
313 Self {
314 episode_id: Uuid::nil(),
315 persona_prompt_contribution: None,
316 memory_context: MemoryContext::default(),
317 extraction_report: None,
318 steps_executed: 0,
319 patch_dispatch_results: Vec::new(),
320 status: TurnStatus::Ok,
321 vitals_gate: None,
322 vitals_phase: None,
323 vitals_trust: None,
324 }
325 }
326}
327
328#[derive(Debug, Clone)]
330pub enum TurnOutcome {
331 Complete(TurnResult),
333 PartialSuccess {
335 result: TurnResult,
336 warnings: Vec<TurnWarning>,
337 },
338}
339
340impl TurnOutcome {
341 pub fn result(&self) -> &TurnResult {
342 match self {
343 TurnOutcome::Complete(r) | TurnOutcome::PartialSuccess { result: r, .. } => r,
344 }
345 }
346
347 pub fn warnings(&self) -> &[TurnWarning] {
348 match self {
349 TurnOutcome::Complete(_) => &[],
350 TurnOutcome::PartialSuccess { warnings, .. } => warnings.as_slice(),
351 }
352 }
353
354 pub fn into_result(self) -> TurnResult {
355 match self {
356 TurnOutcome::Complete(r) | TurnOutcome::PartialSuccess { result: r, .. } => r,
357 }
358 }
359
360 pub fn is_complete(&self) -> bool {
361 matches!(self, TurnOutcome::Complete(_))
362 }
363
364 pub fn is_partial_success(&self) -> bool {
365 matches!(self, TurnOutcome::PartialSuccess { .. })
366 }
367
368 pub fn turn_status(&self) -> TurnStatus {
369 self.result().status
370 }
371}