//! `wave54_event_log_replay` — event-sourced substrate end-to-end. //! //! Builds a small agent run as an append-only event log (BLAKE3- //! chained, content-addressed), folds it into the projected graph //! deterministically, populates a content-addressed cache from //! the LlmResp / ToolResp events, and demonstrates: //! //! 1. **Replay determinism** — re-folding the log produces a //! byte-identical projection. //! 2. **Strict-mode rejection** — tampering with the projection //! yields a divergence error pinned to a content hash. //! 3. **Cache served replay** — a behavior firing with a recorded //! request hash gets the cached response; no new model call. //! 4. **Lineage** — given an artifact object id, recover every //! event that produced it: goal → LLM exchange → object → relations. //! 5. **JCR-1 receipt** — seal an `event-log/1` summary over the //! log snapshot for downstream audit. //! //! Run: //! cargo run -p jouleclaw-stack --example wave54_event_log_replay use jouleclaw_event_log::{ ContentAddressedCache, Event, EventId, EventKind, EventLog, LogId, ProjectedGraph, lineage_of, replay, replay_strict, }; use jouleclaw_jcr1::{EventLogCounts, EventLogReceipt}; use serde_json::json; const STEWARD: &str = "did:keri:EJ9testStewardAID000000000000000"; fn count_kinds(log: &EventLog) -> EventLogCounts { EventLogCounts { goal: log.count_kind("goal") as u32, object: log.count_kind("object") as u32, relation: log.count_kind("relation") as u32, llm_req: log.count_kind("llm_req") as u32, llm_resp: log.count_kind("llm_resp") as u32, tool_req: log.count_kind("tool_req") as u32, tool_resp: log.count_kind("tool_resp") as u32, patch: log.count_kind("patch") as u32, receipt: log.count_kind("receipt") as u32, } } fn sum_tokens(log: &EventLog) -> u32 { log.events .iter() .filter_map(|e| match &e.kind { EventKind::LlmResp { tokens, .. } => Some(*tokens), _ => None, }) .sum() } fn main() { println!("== wave-54 event-sourced agent substrate ==\n"); // ── Build a small agent run as an event log ──────────────────── let mut log = EventLog::new(LogId::new("session-wave54")); log.append(0, EventKind::Goal { text: "find evidence that company X has revenue > 100M".into(), }) .unwrap(); // First LLM exchange: ask the model what to look for. log.append( 1_000, EventKind::LlmReq { request_hash: "b3:llm-req-001-find-evidence-strategy".into(), prompt_summary: "what filings disclose revenue?".into(), }, ) .unwrap(); log.append( 1_500, EventKind::LlmResp { request_hash: "b3:llm-req-001-find-evidence-strategy".into(), response_summary: "10-K Item 2; 10-Q Item 1A".into(), tokens: 320, }, ) .unwrap(); // Tool call: fetch the 10-K. log.append( 2_000, EventKind::ToolReq { tool_id: "edgar.fetch".into(), args_hash: "b3:args-companyX-10K-2025".into(), }, ) .unwrap(); log.append( 2_400, EventKind::ToolResp { tool_id: "edgar.fetch".into(), args_hash: "b3:args-companyX-10K-2025".into(), response_summary: "12-page filing, revenue $135M".into(), }, ) .unwrap(); // The agent emits typed objects from the evidence. log.append( 3_000, EventKind::Object { id: "doc-companyX-10K-2025".into(), ty: "document".into(), fields: json!({"source": "EDGAR", "year": 2025}), }, ) .unwrap(); log.append( 3_100, EventKind::Object { id: "claim-revenue-135M".into(), ty: "claim".into(), fields: json!({"text": "Company X 2025 revenue = $135M"}), }, ) .unwrap(); log.append( 3_200, EventKind::Relation { from: "claim-revenue-135M".into(), to: "doc-companyX-10K-2025".into(), edge_kind: "derived_from".into(), }, ) .unwrap(); // Inline a JCR-1 receipt event for the cascade walk that produced // the LLM response. log.append( 3_300, EventKind::Receipt { schema: "stream-window/1".into(), content_hash: "b3:receipt-stream-window-0001-aaaaaaaaaaaaaaaa".into(), }, ) .unwrap(); println!( "log built: {} events, head={}", log.len(), &log.head().0[..16] ); log.verify_chain().expect("chain intact"); println!("chain integrity: ✓\n"); // ── 1. Replay determinism ────────────────────────────────────── let g1: ProjectedGraph = replay(&log).unwrap(); let g2 = replay(&log).unwrap(); assert_eq!(g1, g2, "replay must be deterministic"); println!("== 1. Replay determinism =="); println!( " projected: goal=\"{}\", objects={}, relations={}, receipts={}", g1.goal, g1.objects.len(), g1.relations.len(), g1.receipts.len() ); println!(" ✓ two replays produce byte-identical projection\n"); // ── 2. Strict-mode rejection ─────────────────────────────────── println!("== 2. Strict-mode rejection =="); let mut tampered = g1.clone(); tampered.goal = "fabricated goal".into(); match replay_strict(&log, &tampered) { Ok(()) => panic!("strict replay accepted tampered projection!"), Err(e) => println!(" ✓ tampered projection rejected: {e}"), } replay_strict(&log, &g1).expect("genuine projection should pass"); println!(" ✓ genuine projection accepted under strict mode\n"); // ── 3. Cache served replay ───────────────────────────────────── println!("== 3. Content-addressed cache (cache-served replay) =="); let cache = ContentAddressedCache::populate_from(&log); println!( " populated cache: llm_responses={}, tool_responses={}", cache.llm_count(), cache.tool_count() ); // Simulate a behavior firing on replay with the SAME request hash. let hit = cache .get_llm("b3:llm-req-001-find-evidence-strategy") .expect("recorded request must hit"); println!( " replay behavior asks LLM hash …{}: HIT ({} tokens, summary=\"{}\")", &"b3:llm-req-001-find-evidence-strategy"[37..], hit.tokens, hit.response_summary ); println!(" → no new model call. ActiveGraph 2026 convention.\n"); // ── 4. Lineage trace ─────────────────────────────────────────── println!("== 4. Lineage trace (claim-revenue-135M) =="); let chain = lineage_of(&log, "claim-revenue-135M"); println!(" {} events in causal chain:", chain.len()); for (i, eid) in chain.iter().enumerate() { let event: &Event = log .events .iter() .find(|e| &e.event_hash == eid) .expect("event in log"); println!(" [{i}] {} — {}", &eid.0[..12], event.kind.tag()); } println!(); // ── 5. JCR-1 event-log/1 receipt ─────────────────────────────── println!("== 5. JCR-1 event-log/1 receipt =="); let receipt = EventLogReceipt::build( format!("{}:{}", log.log_id, &log.head().0[..12]), "2026-06-05T00:01:00Z", STEWARD, log.log_id.0.clone(), log.head().0, log.len() as u32, count_kinds(&log), sum_tokens(&log), ); let receipt_hash = receipt.receipt_hash().expect("hash"); let canonical = receipt.canonical_bytes().expect("canonical"); println!(" schema: event-log/1"); println!(" log_id: {}", receipt.log_id); println!(" chain_depth: {}", receipt.chain_depth); println!(" total_tokens: {}", receipt.total_tokens); println!(" canonical: {} bytes", canonical.len()); println!(" receipt_hash: {receipt_hash}"); println!(); println!( "✓ Event-sourced substrate works end-to-end: log → replay → cache → lineage → receipt." ); println!(" The log IS the agent (Nakajima, arxiv 2605.21997, May 2026)."); }