| use blake3;
|
| use chrono::{DateTime, Utc};
|
| use serde::{Deserialize, Serialize};
|
|
|
|
|
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
| #[serde(tag = "type")]
|
| pub enum ReasoningStep {
|
|
|
| Retrieve {
|
| source: String,
|
| symbol: String,
|
| result: serde_json::Value,
|
| },
|
|
|
| Verify {
|
| target: String,
|
| method: String,
|
| valid: bool,
|
| error: Option<String>,
|
| },
|
|
|
| ApplyRule {
|
| rule_id: String,
|
| premises: Vec<String>,
|
| conclusion: String,
|
| },
|
|
|
| CheckAuthorization {
|
| principal: String,
|
| action: String,
|
| resource: String,
|
| allowed: bool,
|
| reason: String,
|
| },
|
|
|
| Challenge {
|
| target_trace_id: String,
|
| target_step_index: usize,
|
| counter_evidence: String,
|
| },
|
|
|
| Rebuttal {
|
| challenge_trace_id: String,
|
| response: String,
|
| },
|
|
|
| Conclude {
|
| conclusion: String,
|
| confidence: f64,
|
| },
|
|
|
| Compose {
|
| sub_trace_ids: Vec<String>,
|
| composition_rule: String,
|
| },
|
| }
|
|
|
| impl ReasoningStep {
|
|
|
| pub fn description(&self) -> String {
|
| match self {
|
| ReasoningStep::Retrieve { symbol, .. } => format!("Retrieve({})", symbol),
|
| ReasoningStep::Verify { method, .. } => format!("Verify({})", method),
|
| ReasoningStep::ApplyRule { rule_id, .. } => format!("ApplyRule({})", rule_id),
|
| ReasoningStep::CheckAuthorization { action, .. } => format!("CheckAuth({})", action),
|
| ReasoningStep::Challenge { target_trace_id, .. } => {
|
| format!("Challenge({})", &target_trace_id[0..8])
|
| }
|
| ReasoningStep::Rebuttal { .. } => "Rebuttal".to_string(),
|
| ReasoningStep::Conclude { confidence, .. } => {
|
| format!("Conclude(conf={})", (confidence * 100.0) as i32)
|
| }
|
| ReasoningStep::Compose { .. } => "Compose".to_string(),
|
| }
|
| }
|
| }
|
|
|
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)]
|
| pub struct TracedStep {
|
|
|
| pub index: usize,
|
|
|
| pub timestamp: DateTime<Utc>,
|
|
|
| pub step: ReasoningStep,
|
|
|
| pub step_hash: String,
|
| }
|
|
|
|
|
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
| #[serde(tag = "kind")]
|
| pub enum TraceRelation {
|
|
|
| Extends { parent_trace_id: String },
|
|
|
| Challenges { parent_trace_id: String },
|
|
|
| Rebuts { challenge_trace_id: String },
|
|
|
| Composes { sub_trace_ids: Vec<String> },
|
| }
|
|
|
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)]
|
| pub struct ReasoningTrace {
|
|
|
| #[serde(skip)]
|
| pub trace_id: String,
|
|
|
|
|
| pub agent_id: String,
|
|
|
| pub competency: String,
|
|
|
|
|
| pub steps: Vec<TracedStep>,
|
|
|
|
|
| pub parent_relations: Vec<TraceRelation>,
|
|
|
|
|
| pub initiating_query: Option<String>,
|
|
|
|
|
| #[serde(default)]
|
| pub mode: String,
|
|
|
|
|
| pub duration_ms: u64,
|
|
|
|
|
| pub created_at: DateTime<Utc>,
|
|
|
|
|
| pub signature: Option<Vec<u8>>,
|
|
|
|
|
| pub sequence_no: u64,
|
| }
|
|
|
| impl ReasoningTrace {
|
|
|
| pub fn new(agent_id: String, competency: String, sequence_no: u64) -> Self {
|
| ReasoningTrace {
|
| trace_id: String::new(),
|
| agent_id,
|
| competency,
|
| steps: Vec::new(),
|
| parent_relations: Vec::new(),
|
| initiating_query: None,
|
| mode: "live".to_string(),
|
| duration_ms: 0,
|
| created_at: Utc::now(),
|
| signature: None,
|
| sequence_no,
|
| }
|
| }
|
|
|
|
|
| pub fn add_step(&mut self, step: ReasoningStep) {
|
| let index = self.steps.len();
|
| let timestamp = Utc::now();
|
| let step_json = serde_json::to_string(&step).unwrap_or_default();
|
| let step_hash = blake3::hash(step_json.as_bytes()).to_hex().to_string();
|
|
|
| self.steps.push(TracedStep {
|
| index,
|
| timestamp,
|
| step,
|
| step_hash,
|
| });
|
| }
|
|
|
|
|
| pub fn add_parent(&mut self, relation: TraceRelation) {
|
| self.parent_relations.push(relation);
|
| }
|
|
|
|
|
| pub fn set_query(&mut self, query: String) {
|
| self.initiating_query = Some(query);
|
| }
|
|
|
|
|
| pub fn set_mode(&mut self, mode: String) {
|
| self.mode = mode;
|
| }
|
|
|
|
|
|
|
| pub fn finalize(&mut self) -> String {
|
|
|
| let _old_trace_id = self.trace_id.clone();
|
| let old_signature = self.signature.clone();
|
|
|
| self.trace_id = String::new();
|
| self.signature = None;
|
|
|
| let json_str = serde_json::to_string(&self).unwrap_or_default();
|
| self.trace_id = blake3::hash(json_str.as_bytes()).to_hex().to_string();
|
|
|
|
|
| self.signature = old_signature;
|
|
|
| self.trace_id.clone()
|
| }
|
|
|
|
|
| pub fn sign(&mut self, _key_bytes: &[u8; 32]) {
|
| self.signature = None;
|
| let trace_id = self.finalize();
|
|
|
|
|
| let sig_hash = blake3::hash(trace_id.as_bytes()).to_hex().to_string();
|
| self.signature = Some(sig_hash.as_bytes().to_vec());
|
| }
|
|
|
|
|
| pub fn verify(&self, _key_bytes: &[u8; 32]) -> bool {
|
| if self.signature.is_none() {
|
| return false;
|
| }
|
|
|
|
|
| self.signature
|
| .as_ref()
|
| .map(|s| s.len() > 0)
|
| .unwrap_or(false)
|
| }
|
|
|
|
|
| pub fn has_cycles(&self, all_traces: &[ReasoningTrace]) -> bool {
|
| self._has_cycles_internal(&self.trace_id, all_traces, &mut std::collections::HashSet::new())
|
| }
|
|
|
| fn _has_cycles_internal(
|
| &self,
|
| current_id: &str,
|
| all_traces: &[ReasoningTrace],
|
| visited: &mut std::collections::HashSet<String>,
|
| ) -> bool {
|
| if visited.contains(current_id) {
|
| return true;
|
| }
|
| visited.insert(current_id.to_string());
|
|
|
|
|
| let current = match all_traces.iter().find(|t| t.trace_id == current_id) {
|
| Some(t) => t,
|
| None => return false,
|
| };
|
|
|
|
|
| for relation in ¤t.parent_relations {
|
| let parent_id = match relation {
|
| TraceRelation::Extends { parent_trace_id } => parent_trace_id,
|
| TraceRelation::Challenges { parent_trace_id } => parent_trace_id,
|
| TraceRelation::Rebuts { challenge_trace_id } => challenge_trace_id,
|
| TraceRelation::Composes { sub_trace_ids } => {
|
|
|
| for sub_id in sub_trace_ids {
|
| if self._has_cycles_internal(sub_id, all_traces, visited) {
|
| return true;
|
| }
|
| }
|
| continue;
|
| }
|
| };
|
|
|
| if self._has_cycles_internal(parent_id, all_traces, visited) {
|
| return true;
|
| }
|
| }
|
|
|
| false
|
| }
|
|
|
|
|
| pub fn to_s_expr(&self) -> String {
|
| let mut parts = vec![
|
| format!("(trace-id \"{}\")", self.trace_id),
|
| format!("(agent \"{}\")", self.agent_id),
|
| format!("(competency \"{}\")", self.competency),
|
| ];
|
|
|
| for step in &self.steps {
|
| parts.push(format!(
|
| "(step {} \"{}\" {})",
|
| step.index,
|
| step.step.description(),
|
| step.step_hash
|
| ));
|
| }
|
|
|
| format!("(reasoning {})", parts.join(" "))
|
| }
|
|
|
|
|
| pub fn to_json_ld(&self) -> serde_json::Value {
|
| serde_json::json!({
|
| "@context": "https://www.w3.org/ns/activitystreams",
|
| "@id": format!("trace:{}", self.trace_id),
|
| "@type": "ReasoningTrace",
|
| "agent": self.agent_id,
|
| "competency": self.competency,
|
| "steps": self.steps.iter().map(|s| {
|
| serde_json::json!({
|
| "@type": "ReasoningStep",
|
| "index": s.index,
|
| "timestamp": s.timestamp.to_rfc3339(),
|
| "description": s.step.description(),
|
| "hash": s.step_hash,
|
| })
|
| }).collect::<Vec<_>>(),
|
| "duration": format!("PT{}MS", self.duration_ms),
|
| "created": self.created_at.to_rfc3339(),
|
| })
|
| }
|
|
|
|
|
| pub fn extract_symbols(&self) -> Vec<String> {
|
| let mut symbols = Vec::new();
|
|
|
| for step in &self.steps {
|
| match &step.step {
|
| ReasoningStep::Retrieve { symbol, .. } => symbols.push(symbol.clone()),
|
| ReasoningStep::CheckAuthorization { principal, action, resource, .. } => {
|
| symbols.push(principal.clone());
|
| symbols.push(action.clone());
|
| symbols.push(resource.clone());
|
| }
|
| _ => {}
|
| }
|
| }
|
|
|
| symbols.sort();
|
| symbols.dedup();
|
| symbols
|
| }
|
| }
|
|
|
| #[cfg(test)]
|
| mod tests {
|
| use super::*;
|
|
|
| #[test]
|
| fn test_trace_creation() {
|
| let mut trace = ReasoningTrace::new("agent_001".into(), "verify".into(), 1);
|
| trace.add_step(ReasoningStep::Retrieve {
|
| source: "L1".into(),
|
| symbol: "offset_101".into(),
|
| result: serde_json::json!({"value": 42}),
|
| });
|
|
|
| assert_eq!(trace.steps.len(), 1);
|
| }
|
|
|
| #[test]
|
| fn test_trace_id_generation() {
|
| let mut trace = ReasoningTrace::new("agent_001".into(), "verify".into(), 1);
|
| trace.add_step(ReasoningStep::Retrieve {
|
| source: "L1".into(),
|
| symbol: "test".into(),
|
| result: serde_json::json!({}),
|
| });
|
|
|
| let id1 = trace.finalize();
|
| assert!(!id1.is_empty());
|
| assert_eq!(id1.len(), 64);
|
|
|
|
|
| let id2 = trace.finalize();
|
| assert_eq!(id1, id2);
|
| }
|
|
|
| #[test]
|
| fn test_cycle_detection() {
|
| let mut t1 = ReasoningTrace::new("agent_001".into(), "verify".into(), 1);
|
| let mut t2 = ReasoningTrace::new("agent_002".into(), "verify".into(), 1);
|
|
|
| t1.finalize();
|
| t2.finalize();
|
|
|
|
|
| t1.add_parent(TraceRelation::Extends {
|
| parent_trace_id: t2.trace_id.clone(),
|
| });
|
| t2.add_parent(TraceRelation::Extends {
|
| parent_trace_id: t1.trace_id.clone(),
|
| });
|
|
|
| assert!(t1.has_cycles(&[t1.clone(), t2.clone()]));
|
| }
|
|
|
| #[test]
|
| fn test_symbol_extraction() {
|
| let mut trace = ReasoningTrace::new("agent_001".into(), "verify".into(), 1);
|
| trace.add_step(ReasoningStep::Retrieve {
|
| source: "L1".into(),
|
| symbol: "symbol_a".into(),
|
| result: serde_json::json!({}),
|
| });
|
| trace.add_step(ReasoningStep::CheckAuthorization {
|
| principal: "user_1".into(),
|
| action: "read".into(),
|
| resource: "doc_1".into(),
|
| allowed: true,
|
| reason: "owned".into(),
|
| });
|
|
|
| let symbols = trace.extract_symbols();
|
| assert!(symbols.contains(&"symbol_a".to_string()));
|
| assert!(symbols.contains(&"user_1".to_string()));
|
| assert!(symbols.contains(&"read".to_string()));
|
| }
|
| }
|
|
|