open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/examples/wave54_event_log_replay.rs
Transaction Science 49175df95e jouleclaw: wave-54 — event-sourced agent substrate (ActiveGraph ingest)
Ingested from ActiveGraph (Nakajima, "The Log is the Agent",
arxiv 2605.21997, May 2026 — v1.0 May 2026, MIT/Apache). The
focused web search across 5 themes (event-sourced agent runtimes,
deterministic replay + content-addressed caching, multimodal data
primitives, context compression, durable workflows) confirmed
ActiveGraph is the uncontested 2026 SOTA at the event-sourced-
substrate layer. The other four themes have alternatives but
none with the same level of articulation; this wave ingests the
typed primitive JouleClaw's receipts + cascade had been pointing
at but didn't name.

1. jouleclaw-event-log (new crate) — typed substrate.
   - EventKind { Goal | Object | Relation { from, to, edge_kind } |
     LlmReq { request_hash, prompt_summary } | LlmResp {
     request_hash, response_summary, tokens } | ToolReq { tool_id,
     args_hash } | ToolResp { tool_id, args_hash, response_summary }
     | Patch | Receipt }. The Relation variant uses `edge_kind`
     (not `kind`) to avoid the silent serde-tag collision bug.
   - Event { prev_hash, ts_ns, kind, event_hash } sealed at
     append. EventLog::append computes prev_hash from the
     previous event's hash (genesis = BLAKE3 of zero bytes for
     the first event).
   - EventLog::verify_chain catches tampering at the first
     break with the exact index.
   - replay() — deterministic fold producing a ProjectedGraph
     (goal + typed objects + typed relations + receipts), each
     element annotated with the event_hash that produced it.
   - replay_strict() — compares a fresh fold to an expected
     projection byte-for-byte under canonical JSON; returns
     ReplayError::Divergence with the diverging content hashes.
     A green strict replay is a proof the run is reproducible
     (the published ActiveGraph claim).
   - ContentAddressedCache — populates from the log's LlmResp +
     ToolResp events, indexed by the documented 2026 SOTA
     convention: LLM key = BLAKE3 over (system, user_messages,
     model_id, tool_defs, output_schema); tool key = (tool_id,
     args_hash). On replay, a behavior firing with a recorded
     hash hits cache; no new model call.
   - lineage_of(log, object_id) — given an artifact, return every
     event in its causal chain (goal that motivated it, LlmReq/
     LlmResp pair that produced it, ToolReq/ToolResp if any,
     Object event itself, Relations referencing it after creation).
   - 13 tests.

2. jouleclaw-jcr1::event_log — JCR-1 event-log/1 schema.
   - EventLogReceipt { jc_receipt, id, closed_at, steward, log_id,
     head_event_hash, chain_depth, counts: EventLogCounts,
     total_tokens }.
   - 2 conformance vectors with embedded receipt_hash.
   - 3 jcr1 event_log tests.

3. Cross-language conformance — Rust + Python + Go verifiers all
   shipped, byte-equal across the three implementations. The
   three-impl pattern (matching JCR-1 v1) is preserved.

4. Demo binary — examples/wave54_event_log_replay.rs in
   jouleclaw-stack. Builds a 9-event agent run (goal → LLM
   exchange → tool exchange → object → relation → inline receipt),
   demonstrates: (1) replay determinism; (2) strict-mode rejection
   of a tampered projection with divergence pinned to a content
   hash; (3) cache-served replay of the recorded LLM request (no
   new model call); (4) lineage trace recovering 7 events in the
   causal chain back to the goal; (5) event-log/1 JCR-1 receipt
   sealed over the log snapshot.

The triangle (conservation + cone + oracle + elicitor) stays the
type system; wave-54 adds the event-sourced substrate that any
JouleClaw agent runtime can be built ON. The log IS the agent.

Workspace check clean.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-05 13:01:12 -04:00

236 lines
8.5 KiB
Rust

//! `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).");
}