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]>
236 lines
8.5 KiB
Rust
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).");
|
|
}
|