enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
use enact_core::kernel::{ExecutionEventType, ExecutionId, StepId};
use enact_core::streaming::{EventEmitter, EventLog, StreamEvent};
use enact_core::{EventStore, InMemoryEventStore};
use std::sync::Arc;
use tokio::time::Duration;

#[tokio::test]
async fn test_agentic_loop_event_persistence() {
    // 1. Setup - Create an EventStore and EventEmitter
    // InMemoryEventStore implements enact_core::EventStore (which is the streaming one)
    let event_store = Arc::new(InMemoryEventStore::new());
    let event_log = Arc::new(EventLog::new(event_store.clone()));
    let execution_id = ExecutionId::new();

    let mut emitter = EventEmitter::with_persistence(event_log.clone(), execution_id.clone());
    emitter.set_event_log(event_log, execution_id.clone());

    // 2. Simulate Agentic Loop - Tool Execution
    let step_id = StepId::new();
    let tool_call_id = "call_12345";
    let tool_name = "weather_tool";

    // 2a. Tool Input Available (Should be mapped to ExecutionEventType::ToolCallStart)
    let input_payload = serde_json::json!({ "city": "San Francisco" });
    emitter.emit(StreamEvent::ToolInputAvailable {
        tool_call_id: tool_call_id.to_string(),
        tool_name: tool_name.to_string(),
        input: input_payload.clone(),
    });

    // 2b. Tool Output Available (Should be mapped to ExecutionEventType::ToolCallEnd)
    let output_payload = serde_json::json!({ "temp": 72, "conditions": "Sunny" });
    emitter.emit(StreamEvent::ToolOutputAvailable {
        tool_call_id: tool_call_id.to_string(),
        output: output_payload.clone(),
    });

    // 3. Simulate Agentic Loop - Checkpoint
    let checkpoint_id = "chk_12345";
    emitter.emit(StreamEvent::CheckpointSaved {
        execution_id: execution_id.as_str().to_string(),
        step_id: Some(step_id.as_str().to_string()),
        checkpoint_id: checkpoint_id.to_string(),
        state_hash: "hash_abc123".to_string(),
        timestamp: 1234567890,
    });

    // 4. Simulate Agentic Loop - Goal Evaluation
    emitter.emit(StreamEvent::GoalEvaluated {
        execution_id: execution_id.as_str().to_string(),
        step_id: Some(step_id.as_str().to_string()),
        goal_id: "goal_primary".to_string(),
        status: "met".to_string(),
        score: Some(1.0),
        reason: Some("Task completed successfully".to_string()),
        timestamp: 1234567890,
    });

    // Allow async persistence to complete
    tokio::time::sleep(Duration::from_millis(100)).await;

    // 5. Verification - Check EventStore (requires EventStore trait in scope)
    let events = event_store
        .get_by_execution(&execution_id)
        .await
        .expect("Failed to load events");

    // We expect 4 events
    assert_eq!(
        events.len(),
        4,
        "Expected 4 persisted events, found {}",
        events.len()
    );

    // Verify Event 1: ToolCallStart
    let event1 = &events[0].event;
    assert!(
        matches!(event1.event_type, ExecutionEventType::ToolCallStart),
        "Event 1 should be ToolCallStart"
    );
    let p1 = event1.payload.as_ref().expect("Payload missing");
    assert_eq!(p1["tool_call_id"], tool_call_id);
    assert_eq!(p1["tool_name"], tool_name);
    assert_eq!(p1["input"], input_payload);

    // Verify Event 2: ToolCallEnd
    let event2 = &events[1].event;
    assert!(
        matches!(event2.event_type, ExecutionEventType::ToolCallEnd),
        "Event 2 should be ToolCallEnd"
    );
    let p2 = event2.payload.as_ref().expect("Payload missing");
    assert_eq!(p2["tool_call_id"], tool_call_id);
    assert_eq!(p2["output"], output_payload);

    // Verify Event 3: CheckpointSaved
    let event3 = &events[2].event;
    assert!(
        matches!(event3.event_type, ExecutionEventType::CheckpointSaved),
        "Event 3 should be CheckpointSaved"
    );
    let p3 = event3.payload.as_ref().expect("Payload missing");
    assert_eq!(p3["checkpoint_id"], checkpoint_id);
    assert_eq!(p3["state_hash"], "hash_abc123");

    // Verify Event 4: GoalEvaluated
    let event4 = &events[3].event;
    assert!(
        matches!(event4.event_type, ExecutionEventType::GoalEvaluated),
        "Event 4 should be GoalEvaluated"
    );
    let p4 = event4.payload.as_ref().expect("Payload missing");
    assert_eq!(p4["goal_id"], "goal_primary");
    assert_eq!(p4["status"], "met");
    assert_eq!(p4["score"], 1.0);

    println!("✅ All agentic loop events persisted correctly!");
}