| use crate::a2a_protocol::{A2AReasoningEvent, ReasoningPartition};
|
| use crate::trace::ReasoningTrace;
|
| use chrono::{DateTime, Utc};
|
| use serde::{Deserialize, Serialize};
|
| use std::collections::HashMap;
|
| use std::sync::Arc;
|
| use tokio::sync::RwLock;
|
|
|
|
|
| #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| pub enum StreamingMode {
|
|
|
| Live,
|
|
|
| Replay,
|
|
|
| Summary,
|
| }
|
|
|
|
|
| #[derive(Debug, Clone)]
|
| pub struct ReasoningSubscription {
|
| pub partition: String,
|
| pub mode: StreamingMode,
|
| pub agent_id: Option<String>,
|
| pub created_at: DateTime<Utc>,
|
| }
|
|
|
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)]
|
| pub struct TimelineEntry {
|
| pub step_index: usize,
|
| pub timestamp: DateTime<Utc>,
|
| pub description: String,
|
| pub duration_ms: u64,
|
| }
|
|
|
|
|
| pub struct MermaidSequenceDiagram {
|
| pub title: String,
|
| pub actors: Vec<String>,
|
| pub interactions: Vec<String>,
|
| }
|
|
|
| impl MermaidSequenceDiagram {
|
| pub fn new(title: String) -> Self {
|
| MermaidSequenceDiagram {
|
| title,
|
| actors: Vec::new(),
|
| interactions: Vec::new(),
|
| }
|
| }
|
|
|
| pub fn add_actor(&mut self, actor: String) {
|
| if !self.actors.contains(&actor) {
|
| self.actors.push(actor);
|
| }
|
| }
|
|
|
| pub fn add_interaction(&mut self, from: &str, to: &str, label: &str) {
|
| self.interactions.push(format!("{}->>{}:{}", from, to, label));
|
| }
|
|
|
| pub fn render(&self) -> String {
|
| let mut diagram = format!("sequenceDiagram\n title {}\n", self.title);
|
|
|
| for actor in &self.actors {
|
| diagram.push_str(&format!(" participant {}\n", actor));
|
| }
|
| diagram.push('\n');
|
|
|
| for interaction in &self.interactions {
|
| diagram.push_str(&format!(" {}\n", interaction));
|
| }
|
|
|
| diagram
|
| }
|
| }
|
|
|
|
|
| pub fn generate_sequence_diagram(traces: &[ReasoningTrace]) -> MermaidSequenceDiagram {
|
| let mut diagram = MermaidSequenceDiagram::new("Multi-Agent Reasoning".to_string());
|
|
|
| for trace in traces {
|
| diagram.add_actor(trace.agent_id.clone());
|
| }
|
|
|
|
|
| for (i, trace) in traces.iter().enumerate() {
|
| if i > 0 {
|
| let prev = &traces[i - 1];
|
| diagram.add_interaction(
|
| &prev.agent_id,
|
| &trace.agent_id,
|
| &format!("reasoning({})", trace.competency),
|
| );
|
| }
|
| }
|
|
|
| diagram
|
| }
|
|
|
|
|
| pub struct TraceTimeline {
|
| pub trace_id: String,
|
| pub entries: Vec<TimelineEntry>,
|
| }
|
|
|
| impl TraceTimeline {
|
| pub fn from_trace(trace: &ReasoningTrace) -> Self {
|
| let mut entries = Vec::new();
|
| let mut prev_time = trace.created_at;
|
|
|
| for (i, step) in trace.steps.iter().enumerate() {
|
| let duration_ms = (step.timestamp - prev_time)
|
| .num_milliseconds()
|
| .max(0) as u64;
|
|
|
| entries.push(TimelineEntry {
|
| step_index: i,
|
| timestamp: step.timestamp,
|
| description: step.step.description(),
|
| duration_ms,
|
| });
|
|
|
| prev_time = step.timestamp;
|
| }
|
|
|
| TraceTimeline {
|
| trace_id: trace.trace_id.clone(),
|
| entries,
|
| }
|
| }
|
|
|
|
|
| pub fn render_ascii(&self) -> String {
|
| let trace_id_short = if self.trace_id.len() >= 16 {
|
| &self.trace_id[0..16]
|
| } else {
|
| &self.trace_id
|
| };
|
| let mut output = format!("Trace: {}\n", trace_id_short);
|
| output.push_str("─────────────────────────────\n");
|
|
|
| let max_duration = self.entries.iter().map(|e| e.duration_ms).max().unwrap_or(1);
|
|
|
| for entry in &self.entries {
|
| let bar_width = if max_duration > 0 {
|
| ((entry.duration_ms as f64 / max_duration as f64) * 40.0) as usize
|
| } else {
|
| 1
|
| };
|
|
|
| output.push_str(&format!(
|
| "[{:02}] {:50} {} ms {}ms\n",
|
| entry.step_index,
|
| &entry.description,
|
| "█".repeat(bar_width),
|
| entry.duration_ms
|
| ));
|
| }
|
|
|
| output
|
| }
|
| }
|
|
|
|
|
| pub struct ReasoningStreamManager {
|
| subscriptions: Arc<RwLock<Vec<ReasoningSubscription>>>,
|
| event_buffer: Arc<RwLock<HashMap<String, Vec<A2AReasoningEvent>>>>,
|
| traces: Arc<RwLock<HashMap<String, ReasoningTrace>>>,
|
| }
|
|
|
| impl ReasoningStreamManager {
|
| pub fn new() -> Self {
|
| ReasoningStreamManager {
|
| subscriptions: Arc::new(RwLock::new(Vec::new())),
|
| event_buffer: Arc::new(RwLock::new(HashMap::new())),
|
| traces: Arc::new(RwLock::new(HashMap::new())),
|
| }
|
| }
|
|
|
|
|
| pub async fn subscribe_live(&self, partition: String) -> ReasoningSubscription {
|
| let agent_id = ReasoningPartition::from_path(&partition)
|
| .and_then(|p| match p {
|
| ReasoningPartition::AgentTraces(id) => Some(id),
|
| _ => None,
|
| });
|
|
|
| let sub = ReasoningSubscription {
|
| partition: partition.clone(),
|
| mode: StreamingMode::Live,
|
| agent_id,
|
| created_at: Utc::now(),
|
| };
|
|
|
| let mut subs = self.subscriptions.write().await;
|
| subs.push(sub.clone());
|
| sub
|
| }
|
|
|
|
|
| pub async fn emit_event(&self, event: A2AReasoningEvent) {
|
| let mut buffer = self.event_buffer.write().await;
|
| buffer
|
| .entry(event.partition.clone())
|
| .or_insert_with(Vec::new)
|
| .push(event);
|
| }
|
|
|
|
|
| pub async fn store_trace(&self, trace: ReasoningTrace) {
|
| let mut traces = self.traces.write().await;
|
| traces.insert(trace.trace_id.clone(), trace);
|
| }
|
|
|
|
|
| pub async fn get_partition_events(&self, partition: &str) -> Vec<A2AReasoningEvent> {
|
| let buffer = self.event_buffer.read().await;
|
| buffer
|
| .get(partition)
|
| .cloned()
|
| .unwrap_or_default()
|
| }
|
|
|
|
|
| pub async fn get_traces(&self) -> Vec<ReasoningTrace> {
|
| let traces = self.traces.read().await;
|
| traces.values().cloned().collect()
|
| }
|
|
|
|
|
| pub async fn get_trace(&self, trace_id: &str) -> Option<ReasoningTrace> {
|
| let traces = self.traces.read().await;
|
| traces.get(trace_id).cloned()
|
| }
|
|
|
|
|
| pub async fn get_timeline(&self, trace_id: &str) -> Option<TraceTimeline> {
|
| let trace = self.get_trace(trace_id).await?;
|
| Some(TraceTimeline::from_trace(&trace))
|
| }
|
|
|
|
|
| pub async fn get_subscriptions(&self) -> Vec<ReasoningSubscription> {
|
| let subs = self.subscriptions.read().await;
|
| subs.clone()
|
| }
|
|
|
|
|
| pub async fn generate_diagrams(&self) -> Vec<MermaidSequenceDiagram> {
|
| let traces = self.get_traces().await;
|
|
|
|
|
| let mut by_competency: HashMap<String, Vec<ReasoningTrace>> = HashMap::new();
|
| for trace in traces {
|
| by_competency
|
| .entry(trace.competency.clone())
|
| .or_insert_with(Vec::new)
|
| .push(trace);
|
| }
|
|
|
| by_competency
|
| .into_iter()
|
| .map(|(_competency, traces)| generate_sequence_diagram(&traces))
|
| .collect()
|
| }
|
|
|
|
|
| pub async fn get_summary(&self) -> HashMap<String, serde_json::Value> {
|
| let traces = self.get_traces().await;
|
| let buffer = self.event_buffer.read().await;
|
|
|
| let mut summary = HashMap::new();
|
|
|
| summary.insert(
|
| "total_traces".to_string(),
|
| serde_json::json!(traces.len()),
|
| );
|
|
|
| summary.insert(
|
| "total_events".to_string(),
|
| serde_json::json!(buffer.values().map(|v| v.len()).sum::<usize>()),
|
| );
|
|
|
| let total_steps = traces.iter().map(|t| t.steps.len()).sum::<usize>();
|
| summary.insert(
|
| "total_steps".to_string(),
|
| serde_json::json!(total_steps),
|
| );
|
|
|
| let agents: std::collections::HashSet<_> = traces.iter().map(|t| t.agent_id.clone()).collect();
|
| summary.insert(
|
| "unique_agents".to_string(),
|
| serde_json::json!(agents.len()),
|
| );
|
|
|
| let competencies: std::collections::HashSet<_> = traces.iter().map(|t| t.competency.clone()).collect();
|
| summary.insert(
|
| "competencies".to_string(),
|
| serde_json::json!(Vec::from_iter(competencies)),
|
| );
|
|
|
| summary
|
| }
|
| }
|
|
|
| impl Default for ReasoningStreamManager {
|
| fn default() -> Self {
|
| Self::new()
|
| }
|
| }
|
|
|
| #[cfg(test)]
|
| mod tests {
|
| use super::*;
|
| use crate::trace::ReasoningStep;
|
|
|
| #[test]
|
| fn test_mermaid_diagram_generation() {
|
| let mut t1 = ReasoningTrace::new("kernel_001".into(), "verify".into(), 1);
|
| t1.finalize();
|
|
|
| let mut t2 = ReasoningTrace::new("policy_001".into(), "authorize".into(), 1);
|
| t2.add_parent(crate::trace::TraceRelation::Extends {
|
| parent_trace_id: t1.trace_id.clone(),
|
| });
|
| t2.finalize();
|
|
|
| let diagram = generate_sequence_diagram(&[t1, t2]);
|
| let rendered = diagram.render();
|
|
|
| assert!(rendered.contains("sequenceDiagram"));
|
| assert!(rendered.contains("kernel_001"));
|
| assert!(rendered.contains("policy_001"));
|
| }
|
|
|
| #[test]
|
| fn test_timeline_rendering() {
|
| 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!({}),
|
| });
|
| trace.add_step(ReasoningStep::Verify {
|
| target: "sig".into(),
|
| method: "ed25519".into(),
|
| valid: true,
|
| error: None,
|
| });
|
|
|
| let timeline = TraceTimeline::from_trace(&trace);
|
| let ascii = timeline.render_ascii();
|
|
|
| assert!(ascii.contains("Trace:"));
|
| assert!(ascii.contains("Retrieve"));
|
| assert!(ascii.contains("Verify"));
|
| }
|
|
|
| #[tokio::test]
|
| async fn test_stream_manager() {
|
| let manager = ReasoningStreamManager::new();
|
|
|
| let sub = manager.subscribe_live("reasoning/agent_001".into()).await;
|
| assert_eq!(sub.partition, "reasoning/agent_001");
|
|
|
| let subs = manager.get_subscriptions().await;
|
| assert_eq!(subs.len(), 1);
|
| }
|
|
|
| #[tokio::test]
|
| async fn test_emit_and_retrieve_events() {
|
| let manager = ReasoningStreamManager::new();
|
|
|
| let event =
|
| A2AReasoningEvent::with_trace_start("agent_001".into(), "verify".into(), "trace_001".into(), None);
|
| manager.emit_event(event.clone()).await;
|
|
|
| let events = manager.get_partition_events(&event.partition).await;
|
| assert_eq!(events.len(), 1);
|
| }
|
|
|
| #[tokio::test]
|
| async fn test_store_and_retrieve_trace() {
|
| let manager = ReasoningStreamManager::new();
|
|
|
| let mut trace = ReasoningTrace::new("agent_001".into(), "verify".into(), 1);
|
| trace.finalize();
|
| let trace_id = trace.trace_id.clone();
|
|
|
| manager.store_trace(trace).await;
|
|
|
| let retrieved = manager.get_trace(&trace_id).await;
|
| assert!(retrieved.is_some());
|
| }
|
| }
|
|
|