Skip to main content

ainl_runtime/
engine.rs

1//! Loaded graph artifacts and per-turn data shapes for [`crate::AinlRuntime`].
2
3use std::collections::HashMap;
4use std::fmt;
5
6use ainl_graph_extractor::ExtractionReport;
7use ainl_memory::{AgentGraphSnapshot, AinlMemoryNode, GraphValidationReport, SqliteGraphStore};
8use ainl_persona::PersonaSnapshot;
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13/// Edge label for emit routing (matches `ainl_graph_edges.label`).
14pub const EMIT_TO_EDGE: &str = "EMIT_TO";
15
16/// Result of attempting to dispatch one procedural patch node.
17#[derive(Debug, Clone)]
18pub struct PatchDispatchResult {
19    pub label: String,
20    pub patch_version: u32,
21    pub fitness_before: f32,
22    pub fitness_after: f32,
23    pub dispatched: bool,
24    pub skip_reason: Option<PatchSkipReason>,
25    /// Output from a registered [`crate::PatchAdapter`], if any ran successfully.
26    pub adapter_output: Option<serde_json::Value>,
27    /// Name of the adapter that was invoked (including on execution failure).
28    pub adapter_name: Option<String>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum PatchSkipReason {
33    MissingDeclaredRead(String),
34    Retired,
35    ZeroVersion,
36    /// Node was not a procedural patch payload.
37    NotProcedural,
38    /// Failed to persist fitness update.
39    PersistFailed(String),
40}
41
42impl fmt::Display for PatchSkipReason {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            PatchSkipReason::MissingDeclaredRead(s) => write!(f, "missing_declared_read:{s}"),
46            PatchSkipReason::Retired => write!(f, "retired"),
47            PatchSkipReason::ZeroVersion => write!(f, "zero_version"),
48            PatchSkipReason::NotProcedural => write!(f, "not_procedural"),
49            PatchSkipReason::PersistFailed(s) => write!(f, "persist_failed:{s}"),
50        }
51    }
52}
53
54/// A loaded, validated AINL graph artifact (memory substrate view for one agent).
55#[derive(Debug, Clone)]
56pub struct AinlGraphArtifact {
57    pub agent_id: String,
58    pub snapshot: AgentGraphSnapshot,
59    pub validation: GraphValidationReport,
60}
61
62impl AinlGraphArtifact {
63    /// Load agent graph from store. Fails if validation reports dangling edges.
64    pub fn load(store: &SqliteGraphStore, agent_id: &str) -> Result<Self, String> {
65        let snapshot = store.export_graph(agent_id)?;
66        let validation = store.validate_graph(agent_id)?;
67        if !validation.is_valid {
68            let mut msg = String::from("graph validation failed: dangling edges");
69            for d in &validation.dangling_edge_details {
70                msg.push_str(&format!(
71                    "; {} -> {} [{}]",
72                    d.source_id, d.target_id, d.edge_type
73                ));
74            }
75            return Err(msg);
76        }
77        Ok(Self {
78            agent_id: agent_id.to_string(),
79            snapshot,
80            validation,
81        })
82    }
83
84    /// Wrap a snapshot without re-validating (tests / transfer). Caller must validate separately if needed.
85    pub fn from_snapshot(snapshot: AgentGraphSnapshot) -> Self {
86        let agent_id = snapshot.agent_id.clone();
87        let node_count = snapshot.nodes.len();
88        let edge_count = snapshot.edges.len();
89        let validation = GraphValidationReport {
90            agent_id: agent_id.clone(),
91            node_count,
92            edge_count,
93            dangling_edges: Vec::new(),
94            dangling_edge_details: Vec::new(),
95            cross_agent_boundary_edges: 0,
96            orphan_nodes: Vec::new(),
97            is_valid: true,
98        };
99        Self {
100            agent_id,
101            snapshot,
102            validation,
103        }
104    }
105}
106
107/// Input for a single agent turn (host fills; runtime does not call LLMs).
108#[derive(Debug, Clone, Default, Serialize, Deserialize)]
109#[serde(default)]
110pub struct TurnInput {
111    pub user_message: String,
112    pub tools_invoked: Vec<String>,
113    pub trace_event: Option<serde_json::Value>,
114    /// Caller-supplied depth hint. Not used for enforcement — internal `delegation_depth` is authoritative.
115    pub depth: u32,
116    /// Frame variables required by procedural `declared_reads` during patch dispatch.
117    pub frame: HashMap<String, serde_json::Value>,
118    /// After the episode row is written, `EMIT_TO` edges are inserted from `episode_id` to each target
119    /// (additive; default empty). Hosts/tests use this to wire emit routing in the same turn.
120    pub emit_targets: Vec<Uuid>,
121}
122
123/// Compiled memory context for a turn (prompt-side assembly in the host).
124#[derive(Debug, Clone)]
125pub struct MemoryContext {
126    pub recent_episodes: Vec<AinlMemoryNode>,
127    pub relevant_semantic: Vec<AinlMemoryNode>,
128    pub active_patches: Vec<AinlMemoryNode>,
129    pub persona_snapshot: Option<PersonaSnapshot>,
130    pub compiled_at: DateTime<Utc>,
131}
132
133impl Default for MemoryContext {
134    fn default() -> Self {
135        Self {
136            recent_episodes: Vec::new(),
137            relevant_semantic: Vec::new(),
138            active_patches: Vec::new(),
139            persona_snapshot: None,
140            compiled_at: Utc::now(),
141        }
142    }
143}
144
145/// Output of a single agent turn orchestrated by [`crate::AinlRuntime`].
146#[derive(Debug, Clone)]
147pub struct TurnOutput {
148    pub episode_id: Uuid,
149    pub persona_prompt_contribution: Option<String>,
150    pub memory_context: MemoryContext,
151    pub extraction_report: Option<ExtractionReport>,
152    pub steps_executed: u32,
153    pub outcome: TurnOutcome,
154    pub patch_dispatch_results: Vec<PatchDispatchResult>,
155}
156
157impl Default for TurnOutput {
158    fn default() -> Self {
159        Self {
160            episode_id: Uuid::nil(),
161            persona_prompt_contribution: None,
162            memory_context: MemoryContext::default(),
163            extraction_report: None,
164            steps_executed: 0,
165            outcome: TurnOutcome::Success,
166            patch_dispatch_results: Vec::new(),
167        }
168    }
169}
170
171/// High-level turn result (soft limits use variants instead of `Err` when `Ok` carries diagnostics).
172#[derive(Debug, Clone)]
173pub enum TurnOutcome {
174    Success,
175    DepthLimitExceeded,
176    StepLimitExceeded { steps_executed: u32 },
177    GraphMemoryDisabled,
178    PartialSuccess {
179        episode_recorded: bool,
180        extraction_failed: bool,
181        patches_failed: Vec<String>,
182        warnings: Vec<String>,
183    },
184    Error(String),
185}