use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use ainl_graph_extractor::ExtractionReport;
use ainl_memory::{
AgentGraphSnapshot, AinlMemoryNode, AinlNodeType, GraphValidationReport, ProceduralNode,
SqliteGraphStore,
};
use ainl_persona::PersonaSnapshot;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub const EMIT_TO_EDGE: &str = "EMIT_TO";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AinlRuntimeError {
DelegationDepthExceeded {
depth: u32,
max: u32,
},
Message(String),
AsyncJoinError(String),
AsyncStoreError(String),
}
impl fmt::Display for AinlRuntimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AinlRuntimeError::DelegationDepthExceeded { depth, max } => {
write!(f, "delegation depth exceeded (depth={depth}, max={max})")
}
AinlRuntimeError::Message(s) => f.write_str(s),
AinlRuntimeError::AsyncJoinError(s) => write!(f, "async join error: {s}"),
AinlRuntimeError::AsyncStoreError(s) => write!(f, "async store error: {s}"),
}
}
}
impl Error for AinlRuntimeError {}
impl From<String> for AinlRuntimeError {
fn from(s: String) -> Self {
Self::Message(s)
}
}
impl AinlRuntimeError {
#[must_use]
pub fn message_str(&self) -> Option<&str> {
match self {
Self::Message(s) => Some(s.as_str()),
Self::DelegationDepthExceeded { .. } => None,
Self::AsyncJoinError(s) => Some(s.as_str()),
Self::AsyncStoreError(s) => Some(s.as_str()),
}
}
#[must_use]
pub fn is_delegation_depth_exceeded(&self) -> bool {
matches!(self, Self::DelegationDepthExceeded { .. })
}
#[must_use]
pub fn is_async_join_error(&self) -> bool {
matches!(self, Self::AsyncJoinError(_))
}
#[must_use]
pub fn is_async_store_error(&self) -> bool {
matches!(self, Self::AsyncStoreError(_))
}
#[must_use]
pub fn delegation_depth_exceeded(&self) -> Option<(u32, u32)> {
match self {
Self::DelegationDepthExceeded { depth, max } => Some((*depth, *max)),
Self::Message(_) | Self::AsyncJoinError(_) | Self::AsyncStoreError(_) => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PatchDispatchContext<'a> {
pub patch_label: &'a str,
pub node: &'a AinlMemoryNode,
pub frame: &'a HashMap<String, serde_json::Value>,
}
impl<'a> PatchDispatchContext<'a> {
pub fn procedural(&self) -> Option<&'a ProceduralNode> {
match &self.node.node_type {
AinlNodeType::Procedural { procedural } => Some(procedural),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct PatchDispatchResult {
pub label: String,
pub patch_version: u32,
pub fitness_before: f32,
pub fitness_after: f32,
pub dispatched: bool,
pub skip_reason: Option<PatchSkipReason>,
pub adapter_output: Option<serde_json::Value>,
pub adapter_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatchSkipReason {
MissingDeclaredRead(String),
Retired,
ZeroVersion,
NotProcedural,
PersistFailed(String),
}
impl fmt::Display for PatchSkipReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PatchSkipReason::MissingDeclaredRead(s) => write!(f, "missing_declared_read:{s}"),
PatchSkipReason::Retired => write!(f, "retired"),
PatchSkipReason::ZeroVersion => write!(f, "zero_version"),
PatchSkipReason::NotProcedural => write!(f, "not_procedural"),
PatchSkipReason::PersistFailed(s) => write!(f, "persist_failed:{s}"),
}
}
}
#[derive(Debug, Clone)]
pub struct AinlGraphArtifact {
pub agent_id: String,
pub snapshot: AgentGraphSnapshot,
pub validation: GraphValidationReport,
}
impl AinlGraphArtifact {
pub fn load(store: &SqliteGraphStore, agent_id: &str) -> Result<Self, String> {
let snapshot = store.export_graph(agent_id)?;
let validation = store.validate_graph(agent_id)?;
if !validation.is_valid {
let mut msg = String::from("graph validation failed: dangling edges");
for d in &validation.dangling_edge_details {
msg.push_str(&format!(
"; {} -> {} [{}]",
d.source_id, d.target_id, d.edge_type
));
}
return Err(msg);
}
Ok(Self {
agent_id: agent_id.to_string(),
snapshot,
validation,
})
}
pub fn from_snapshot(snapshot: AgentGraphSnapshot) -> Self {
let agent_id = snapshot.agent_id.clone();
let node_count = snapshot.nodes.len();
let edge_count = snapshot.edges.len();
let validation = GraphValidationReport {
agent_id: agent_id.clone(),
node_count,
edge_count,
dangling_edges: Vec::new(),
dangling_edge_details: Vec::new(),
cross_agent_boundary_edges: 0,
orphan_nodes: Vec::new(),
is_valid: true,
};
Self {
agent_id,
snapshot,
validation,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct TurnInput {
pub user_message: String,
pub tools_invoked: Vec<String>,
pub trace_event: Option<serde_json::Value>,
pub depth: u32,
pub frame: HashMap<String, serde_json::Value>,
pub emit_targets: Vec<Uuid>,
pub vitals_gate: Option<String>,
pub vitals_phase: Option<String>,
pub vitals_trust: Option<f32>,
}
#[derive(Debug, Clone)]
pub struct MemoryContext {
pub recent_episodes: Vec<AinlMemoryNode>,
pub relevant_semantic: Vec<AinlMemoryNode>,
pub active_patches: Vec<AinlMemoryNode>,
pub persona_snapshot: Option<PersonaSnapshot>,
pub compiled_at: DateTime<Utc>,
}
impl Default for MemoryContext {
fn default() -> Self {
Self {
recent_episodes: Vec::new(),
relevant_semantic: Vec::new(),
active_patches: Vec::new(),
persona_snapshot: None,
compiled_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TurnPhase {
EpisodeWrite,
FitnessWriteBack,
ExtractionPass,
PatternPersistence,
PersonaEvolution,
ExportRefresh,
RuntimeStatePersist,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TurnWarning {
pub phase: TurnPhase,
pub error: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TurnStatus {
Ok,
StepLimitExceeded { steps_executed: u32 },
GraphMemoryDisabled,
}
#[derive(Debug, Clone)]
pub struct TurnResult {
pub episode_id: Uuid,
pub persona_prompt_contribution: Option<String>,
pub memory_context: MemoryContext,
pub extraction_report: Option<ExtractionReport>,
pub steps_executed: u32,
pub patch_dispatch_results: Vec<PatchDispatchResult>,
pub status: TurnStatus,
pub vitals_gate: Option<String>,
pub vitals_phase: Option<String>,
pub vitals_trust: Option<f32>,
}
impl Default for TurnResult {
fn default() -> Self {
Self {
episode_id: Uuid::nil(),
persona_prompt_contribution: None,
memory_context: MemoryContext::default(),
extraction_report: None,
steps_executed: 0,
patch_dispatch_results: Vec::new(),
status: TurnStatus::Ok,
vitals_gate: None,
vitals_phase: None,
vitals_trust: None,
}
}
}
#[derive(Debug, Clone)]
pub enum TurnOutcome {
Complete(TurnResult),
PartialSuccess {
result: TurnResult,
warnings: Vec<TurnWarning>,
},
}
impl TurnOutcome {
pub fn result(&self) -> &TurnResult {
match self {
TurnOutcome::Complete(r) | TurnOutcome::PartialSuccess { result: r, .. } => r,
}
}
pub fn warnings(&self) -> &[TurnWarning] {
match self {
TurnOutcome::Complete(_) => &[],
TurnOutcome::PartialSuccess { warnings, .. } => warnings.as_slice(),
}
}
pub fn into_result(self) -> TurnResult {
match self {
TurnOutcome::Complete(r) | TurnOutcome::PartialSuccess { result: r, .. } => r,
}
}
pub fn is_complete(&self) -> bool {
matches!(self, TurnOutcome::Complete(_))
}
pub fn is_partial_success(&self) -> bool {
matches!(self, TurnOutcome::PartialSuccess { .. })
}
pub fn turn_status(&self) -> TurnStatus {
self.result().status
}
}