| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| use maka_agent_facts::event::Fact; | |
| use maka_agent_facts::event::Invocation; | |
| use maka_agent_facts::event::InvocationInput; | |
| use maka_agent_facts::event::InvocationOutcome; | |
| use maka_agent_facts::event::LogScope; | |
| use maka_agent_facts::event::ModelInterruption; | |
| use maka_agent_facts::event::RuntimeEvent; | |
| use maka_agent_facts::history::RowPosition; | |
| use maka_agent_facts::model::ModelEvent; | |
| use maka_agent_facts::model::ModelPart; | |
| use maka_agent_facts::model::ModelStep; | |
| use maka_agent_facts::model::ModelUsage; | |
| use maka_agent_facts::model::TextKind; | |
| use maka_event_log::{EventLog, StoreError}; | |
| use rusqlite::Connection; | |
| use serde_json::Value; | |
| use sha2::{Digest, Sha256}; | |
| #[path = "accepted_observations.rs"] | |
| mod accepted_observations; | |
| #[path = "fixtures.rs"] | |
| mod fixtures; | |
| #[path = "history.rs"] | |
| mod history; | |
| #[path = "image.rs"] | |
| mod image; | |
| #[path = "interrupted_ancestry.rs"] | |
| mod interrupted_ancestry; | |
| #[path = "large_siblings.rs"] | |
| mod large_siblings; | |
| #[path = "observation_count.rs"] | |
| mod observation_count; | |
| #[path = "performance.rs"] | |
| mod performance; | |
| #[path = "performance_executor.rs"] | |
| mod performance_executor; | |
| #[path = "prefix_replay.rs"] | |
| mod prefix_replay; | |
| #[path = "source.rs"] | |
| mod source; | |
| #[path = "sparse_directory.rs"] | |
| mod sparse_directory; | |
| use fixtures::*; | |
| #[tokio::test] | |
| async fn completed_bytes_digest_identity_reopen_and_selected_rebuild() { | |
| let temp = tempfile::tempdir().unwrap(); | |
| let path = temp.path().join("log.sqlite"); | |
| let log = EventLog::open(&path).await.unwrap(); | |
| crate::transcript_cache::materialize_all_rows(&log, "a", 0) | |
| .await | |
| .unwrap(); | |
| assert!( | |
| log.transcript_headers( | |
| "a", | |
| &maka_event_log::transcript::TranscriptRead { | |
| through: 0, | |
| position: None, | |
| direction: maka_event_log::transcript::TranscriptDirection::Newer, | |
| limit: 1, | |
| } | |
| ) | |
| .await | |
| .unwrap() | |
| .is_empty() | |
| ); | |
| let (id, _) = partial(&log, "a", "hello😀").await; | |
| log.append(&event( | |
| "a", | |
| observe( | |
| "a", | |
| ModelEvent::PartFinished { | |
| id: "provider-part".into(), | |
| provider_options: None, | |
| }, | |
| ), | |
| )) | |
| .await | |
| .unwrap(); | |
| log.append(&event( | |
| "a", | |
| Fact::ModelCompleted { | |
| step_id: "step-a".into(), | |
| output: ModelStep { | |
| parts: vec![ModelPart::Text { | |
| text_kind: TextKind::Text, | |
| text: "hello😀".into(), | |
| provider_options: None, | |
| }], | |
| finish_reason: maka_agent_facts::model::ModelFinishReason::Stop, | |
| usage: ModelUsage { | |
| input_tokens: Some(9), | |
| output_tokens: Some(3), | |
| ..Default::default() | |
| }, | |
| provider_options: None, | |
| response_id: None, | |
| model: None, | |
| timestamp: None, | |
| }, | |
| }, | |
| )) | |
| .await | |
| .unwrap(); | |
| let through = end(&log, "a", InvocationOutcome::Completed).await; | |
| let wake = log.subscribe_commits(); | |
| crate::transcript_cache::materialize_all_rows(&log, "a", through) | |
| .await | |
| .unwrap(); | |
| assert!(!wake.has_changed().unwrap()); | |
| let db = Connection::open(&path).unwrap(); | |
| let original = crate::test_store::inspect(&log, |db| rows(db, "a")).await; | |
| assert_eq!(original.len(), 5); | |
| let assistant: Value = serde_json::from_slice(&original[2].1).unwrap(); | |
| assert_eq!(assistant["id"], id); | |
| assert_eq!(assistant["modelId"], "real-model"); | |
| for (_, bytes, digest, size) in &original { | |
| assert_eq!(*size, bytes.len() as i64); | |
| assert_eq!( | |
| *digest, | |
| format!("sha256:{}", hex::encode(Sha256::digest(bytes))) | |
| ); | |
| } | |
| // Optional bodies may be discarded; selected reconstruction preserves the same rows. | |
| crate::test_store::inspect(&log, move |db| { | |
| db.execute_batch("DELETE FROM transcript_cache.text; DELETE FROM transcript_cache.rows") | |
| .unwrap() | |
| }) | |
| .await; | |
| crate::transcript_cache::materialize_all_rows(&log, "a", through) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| crate::test_store::inspect(&log, |db| rows(db, "a")).await, | |
| original | |
| ); | |
| log.close().await.unwrap(); | |
| let reopened = EventLog::open(&path).await.unwrap(); | |
| crate::transcript_cache::materialize_all_rows(&reopened, "a", through) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| crate::test_store::inspect(&reopened, |db| rows(db, "a")).await, | |
| original | |
| ); | |
| let canonical = serde_json::to_vec(&reopened.prefix(32, 65536).await.unwrap()).unwrap(); | |
| // This real completed producer emits assistant then usage. Derive positions | |
| // from the emitted fixture, rather than assuming fixed event/ordinal numbers. | |
| let assistant_row = original | |
| .iter() | |
| .find(|row| serde_json::from_slice::<Value>(&row.1).unwrap()["type"] == "assistant") | |
| .unwrap(); | |
| let usage_row = original | |
| .iter() | |
| .find(|row| serde_json::from_slice::<Value>(&row.1).unwrap()["type"] == "token_usage") | |
| .unwrap(); | |
| assert_eq!(assistant_row.0.source_sequence, usage_row.0.source_sequence); | |
| assert_eq!( | |
| original | |
| .iter() | |
| .filter(|row| row.0.source_sequence == assistant_row.0.source_sequence) | |
| .map(|row| row.0) | |
| .collect::<Vec<_>>(), | |
| [assistant_row.0, usage_row.0] | |
| ); | |
| let assistant_source = i64::try_from(assistant_row.0.source_sequence).unwrap(); | |
| let assistant_item = i64::try_from(assistant_row.0.item_index).unwrap(); | |
| let usage_source = i64::try_from(usage_row.0.source_sequence).unwrap(); | |
| let usage_item = i64::try_from(usage_row.0.item_index).unwrap(); | |
| // Preserve the original byte-identity refusal: a missing sibling triggers | |
| // whole-S reconstruction, which must not overwrite an existing bad payload. | |
| crate::test_store::inspect(&reopened, move |db| { db.execute("UPDATE transcript_cache.rows SET payload=X'00' WHERE session_id='a' AND source_sequence=?1 AND item_index=?2",[assistant_source,assistant_item]).unwrap() }).await; | |
| crate::test_store::inspect(&reopened, move |db| { db.execute( | |
| "DELETE FROM transcript_cache.rows WHERE session_id='a' AND source_sequence=?1 AND item_index=?2", | |
| [usage_source, usage_item], | |
| ) | |
| .unwrap() }).await; | |
| let damaged = crate::test_store::inspect(&reopened, |db| rows(db, "a")).await; | |
| assert!(matches!( | |
| reopened | |
| .transcript_headers( | |
| "a", | |
| &maka_event_log::transcript::TranscriptRead { | |
| through, | |
| position: Some(usage_row.0), | |
| direction: maka_event_log::transcript::TranscriptDirection::Newer, | |
| limit: 1, | |
| } | |
| ) | |
| .await, | |
| Err(StoreError::TranscriptConflict) | |
| )); | |
| assert_eq!( | |
| crate::test_store::inspect(&reopened, |db| rows(db, "a")).await, | |
| damaged | |
| ); | |
| assert_eq!( | |
| serde_json::to_vec(&reopened.prefix(32, 65536).await.unwrap()).unwrap(), | |
| canonical | |
| ); | |
| let restore_payload = assistant_row.1.clone(); | |
| crate::test_store::inspect(&reopened, move |db| { db.execute("UPDATE transcript_cache.rows SET payload=?1 WHERE session_id='a' AND source_sequence=?2 AND item_index=?3",rusqlite::params![restore_payload,assistant_source,assistant_item]).unwrap() }).await; | |
| crate::transcript_cache::materialize_all_rows(&reopened, "a", through) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| crate::test_store::inspect(&reopened, |db| rows(db, "a")).await, | |
| original | |
| ); | |
| // Cache hits retain their separate directory-identity check without hashing | |
| // every cached payload on each fragment. | |
| let identity_position = original[1].0; | |
| crate::test_store::inspect(&reopened, move |db| { db.execute( | |
| "UPDATE transcript_cache.rows SET turn_id = 'wrong-turn' WHERE source_sequence = ? AND item_index = ?", | |
| [i64::try_from(identity_position.source_sequence).unwrap(),i64::try_from(identity_position.item_index).unwrap()], | |
| ).unwrap() }).await; | |
| assert!(matches!( | |
| crate::transcript_cache::materialize_all_rows(&reopened, "a", through).await, | |
| Err(StoreError::TranscriptConflict) | |
| )); | |
| assert_eq!( | |
| serde_json::to_vec(&reopened.prefix(32, 65536).await.unwrap()).unwrap(), | |
| canonical | |
| ); | |
| reopened.close().await.unwrap(); | |
| db.close().map_err(|(_, error)| error).unwrap(); | |
| // All connections are now closed (including the last SQLite connection's | |
| // WAL close/checkpoint). Copy this task fixture, never a live-WAL DB or hardlink. | |
| for operation in ["headers", "fragment", "turn_bounds"] { | |
| let copy = temp.path().join(format!("{operation}.sqlite")); | |
| std::fs::copy(&path, ©).unwrap(); | |
| let copied = EventLog::open(©).await.unwrap(); | |
| crate::transcript_cache::materialize_all_rows(&copied, "a", through) | |
| .await | |
| .unwrap(); | |
| // A miss inserts the first emitted row, then the existing second row | |
| // fails the exact byte comparison. Only optional cache bytes are damaged. | |
| crate::test_store::inspect(&copied, move |fault| { fault.execute("DELETE FROM transcript_cache.rows WHERE session_id='a' AND source_sequence=?1 AND item_index=?2",[assistant_source,assistant_item]).unwrap() }).await; | |
| crate::test_store::inspect(&copied, move |fault| { fault.execute("UPDATE transcript_cache.rows SET payload=X'00' WHERE session_id='a' AND source_sequence=?1 AND item_index=?2",[usage_source,usage_item]).unwrap() }).await; | |
| let before = crate::test_store::inspect(&copied, |db| rows(db, "a")).await; | |
| let error = match operation { | |
| "headers" => copied | |
| .transcript_headers( | |
| "a", | |
| &maka_event_log::transcript::TranscriptRead { | |
| through, | |
| position: Some(assistant_row.0), | |
| direction: maka_event_log::transcript::TranscriptDirection::Newer, | |
| limit: 1, | |
| }, | |
| ) | |
| .await | |
| .unwrap_err(), | |
| "fragment" => copied | |
| .transcript_fragment("a", assistant_row.0, 0, 1) | |
| .await | |
| .unwrap_err(), | |
| "turn_bounds" => copied | |
| .transcript_turn_bounds("a", "turn-a", through) | |
| .await | |
| .unwrap_err(), | |
| _ => unreachable!(), | |
| }; | |
| assert!( | |
| matches!(&error, StoreError::TranscriptConflict), | |
| "{operation}: {error}" | |
| ); | |
| assert_eq!( | |
| serde_json::to_vec(&copied.prefix(32, 65536).await.unwrap()).unwrap(), | |
| canonical | |
| ); | |
| assert_eq!( | |
| crate::test_store::inspect(&copied, |db| rows(db, "a")).await, | |
| before, | |
| "{operation}: failed source fill must roll back its first row" | |
| ); | |
| copied.close().await.unwrap(); | |
| } | |
| } | |
| #[tokio::test] | |
| async fn interleaved_stream_preserves_order_and_excludes_other_session_evidence() { | |
| let temp = tempfile::tempdir().unwrap(); | |
| let path = temp.path().join("log.sqlite"); | |
| let log = EventLog::open(&path).await.unwrap(); | |
| let (id, _) = partial(&log, "a", "start\n").await; | |
| partial(&log, "b", "unrelated").await; | |
| let expected = format!( | |
| "start\n{}", | |
| (0..1024).map(|i| format!("{i}😀\n")).collect::<String>() | |
| ); | |
| let events: Vec<_> = (0..1024) | |
| .flat_map(|i| { | |
| [("a", format!("{i}😀\n")), ("b", "not part of a".into())].map(|(session, text)| { | |
| event( | |
| session, | |
| observe( | |
| session, | |
| ModelEvent::PartDelta { | |
| id: "provider-part".into(), | |
| text, | |
| provider_options: None, | |
| }, | |
| ), | |
| ) | |
| }) | |
| }) | |
| .collect(); | |
| log.append_batch(&events).await.unwrap(); | |
| let through = interrupt(&log, "a").await; | |
| crate::transcript_cache::materialize_all_rows(&log, "a", through) | |
| .await | |
| .unwrap(); | |
| let projected = crate::test_store::inspect(&log, |db| rows(db, "a")).await; | |
| assert_eq!(projected.len(), 3); | |
| let assistant: Value = serde_json::from_slice(&projected[2].1).unwrap(); | |
| assert_eq!(assistant["id"], id); | |
| assert_eq!(assistant["text"], expected); | |
| let other = interrupt(&log, "b").await; | |
| crate::transcript_cache::materialize_all_rows(&log, "b", other) | |
| .await | |
| .unwrap(); | |
| let other_rows = crate::test_store::inspect(&log, |db| rows(db, "b")).await; | |
| let assistant: Value = serde_json::from_slice(&other_rows[2].1).unwrap(); | |
| assert_eq!( | |
| assistant["text"], | |
| format!("unrelated{}", "not part of a".repeat(1024)) | |
| ); | |
| assert_eq!( | |
| crate::test_store::inspect(&log, |db| rows(db, "a")).await, | |
| projected | |
| ); | |
| } | |
| #[tokio::test] | |
| async fn partial_freezes_at_interruption_and_old_fence_excludes_later_boundaries() { | |
| let temp = tempfile::tempdir().unwrap(); | |
| let path = temp.path().join("log.sqlite"); | |
| let log = EventLog::open(&path).await.unwrap(); | |
| let (id, live) = partial(&log, "a", "partial😀").await; | |
| let interrupted = interrupt(&log, "a").await; | |
| let ended = end( | |
| &log, | |
| "a", | |
| InvocationOutcome::Cancelled { | |
| source: "user".into(), | |
| }, | |
| ) | |
| .await; | |
| let invocation = event("a", opening()).event().invocation.clone(); | |
| let overlay = log.active_transcript(&invocation, live).await.unwrap(); | |
| assert_eq!(overlay.len(), 1); | |
| assert_eq!(overlay[0].id, id); | |
| assert!( | |
| log.active_transcript(&invocation, interrupted) | |
| .await | |
| .unwrap() | |
| .is_empty() | |
| ); | |
| assert!( | |
| log.active_transcript(&invocation, ended) | |
| .await | |
| .unwrap() | |
| .is_empty() | |
| ); | |
| crate::transcript_cache::materialize_all_rows(&log, "a", live) | |
| .await | |
| .unwrap(); | |
| let running = crate::test_store::inspect(&log, |db| rows(db, "a")).await; | |
| assert_eq!(running.len(), 2); | |
| let state: Value = serde_json::from_slice(&running[1].1).unwrap(); | |
| assert_eq!(state["type"], "turn_state"); | |
| assert_eq!( | |
| state["status"], "running", | |
| "pending work is not interrupted" | |
| ); | |
| crate::transcript_cache::materialize_all_rows(&log, "a", interrupted) | |
| .await | |
| .unwrap(); | |
| let frozen = crate::test_store::inspect(&log, |db| rows(db, "a")).await; | |
| assert_eq!(frozen.len(), 3); | |
| let assistant: Value = serde_json::from_slice(&frozen[2].1).unwrap(); | |
| assert_eq!(assistant["id"], id); | |
| assert_eq!(assistant["text"], "partial😀"); | |
| crate::transcript_cache::materialize_all_rows(&log, "a", ended) | |
| .await | |
| .unwrap(); | |
| let final_rows = crate::test_store::inspect(&log, |db| rows(db, "a")).await; | |
| assert_eq!(&final_rows[..3], frozen.as_slice()); | |
| assert_eq!(final_rows.len(), 4); // no invented unknown usage | |
| let terminal: Value = serde_json::from_slice(&final_rows[3].1).unwrap(); | |
| assert_eq!(terminal["status"], "aborted"); | |
| } | |
| #[tokio::test] | |
| async fn directory_fences_and_sparse_body_session_isolation() { | |
| let temp = tempfile::tempdir().unwrap(); | |
| let path = temp.path().join("log.sqlite"); | |
| let log = EventLog::open(&path).await.unwrap(); | |
| log.append(&event("a", opening())).await.unwrap(); | |
| log.append(&event("b", opening())).await.unwrap(); | |
| let a_end = end(&log, "a", InvocationOutcome::Completed).await; | |
| let through = end(&log, "b", InvocationOutcome::Completed).await; | |
| assert!( | |
| crate::test_store::inspect(&log, |db| rows(db, "a")) | |
| .await | |
| .is_empty() | |
| && crate::test_store::inspect(&log, |db| rows(db, "b")) | |
| .await | |
| .is_empty() | |
| ); | |
| crate::transcript_cache::materialize_all_rows(&log, "a", through) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| crate::test_store::inspect(&log, |db| rows(db, "a")) | |
| .await | |
| .last() | |
| .unwrap() | |
| .0, | |
| RowPosition { | |
| source_sequence: a_end, | |
| item_index: 0 | |
| } | |
| ); | |
| assert!( | |
| crate::test_store::inspect(&log, |db| rows(db, "b")) | |
| .await | |
| .is_empty() | |
| ); | |
| assert!( | |
| crate::transcript_cache::materialize_all_rows(&log, "a", through + 1) | |
| .await | |
| .is_err() | |
| ); | |
| log.close().await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn terminal_without_model_interruption_seals_only_committed_partial_evidence() { | |
| for outcome in [ | |
| InvocationOutcome::Failed { | |
| model_failure: None, | |
| class: "event_commit".into(), | |
| message: None, | |
| }, | |
| InvocationOutcome::Cancelled { | |
| source: "shutdown".into(), | |
| }, | |
| ] { | |
| let temp = tempfile::tempdir().unwrap(); | |
| let path = temp.path().join("log.sqlite"); | |
| let log = EventLog::open(&path).await.unwrap(); | |
| let (id, live) = partial(&log, "a", "committed😀 prefix").await; | |
| let invocation = event("a", opening()).event().invocation.clone(); | |
| let overlay = log.active_transcript(&invocation, live).await.unwrap(); | |
| crate::transcript_cache::materialize_all_rows(&log, "a", live) | |
| .await | |
| .unwrap(); | |
| // A persistence fault may prevent ModelInterrupted while a later | |
| // terminal append succeeds. The source facts allow this boundary. | |
| let through = end(&log, "a", outcome.clone()).await; | |
| crate::transcript_cache::materialize_all_rows(&log, "a", through) | |
| .await | |
| .unwrap(); | |
| assert!( | |
| log.active_transcript(&invocation, through) | |
| .await | |
| .unwrap() | |
| .is_empty() | |
| ); | |
| let original = crate::test_store::inspect(&log, |db| rows(db, "a")).await; | |
| assert_eq!(original.len(), 4); // user, running, factual partial, terminal; no usage | |
| let assistant: Value = serde_json::from_slice(&original[2].1).unwrap(); | |
| assert_eq!(assistant["id"], id); | |
| let failed = matches!(outcome, InvocationOutcome::Failed { .. }); | |
| let mut expected = serde_json::to_value(&overlay[0]).unwrap(); | |
| if failed { | |
| expected["interrupted"] = Value::Bool(true); | |
| } | |
| assert_eq!(assistant, expected); | |
| let delivery = log | |
| .session_stream_events("a", live, through, 8, 4096) | |
| .await | |
| .unwrap(); | |
| assert!(matches!(&delivery.events[0].fact, | |
| maka_event_log::observation::StreamFact::InvocationEnded { interrupted, .. } | |
| if if failed { interrupted == std::slice::from_ref(&id) } else { interrupted.is_empty() })); | |
| assert_eq!( | |
| original[2].0, | |
| RowPosition { | |
| source_sequence: through, | |
| item_index: 0 | |
| } | |
| ); | |
| let terminal: Value = serde_json::from_slice(&original[3].1).unwrap(); | |
| assert_eq!( | |
| terminal["status"], | |
| if matches!(outcome, InvocationOutcome::Failed { .. }) { | |
| "failed" | |
| } else { | |
| "aborted" | |
| } | |
| ); | |
| // Index reset and process reopen must not drop or relabel the prefix. | |
| crate::test_store::inspect(&log, move |db| { | |
| db.execute_batch("DELETE FROM transcript_cache.text; DELETE FROM transcript_cache.rows") | |
| .unwrap() | |
| }) | |
| .await; | |
| log.close().await.unwrap(); | |
| let reopened = EventLog::open(&path).await.unwrap(); | |
| crate::transcript_cache::materialize_all_rows(&reopened, "a", through) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| crate::test_store::inspect(&reopened, |db| rows(db, "a")).await, | |
| original | |
| ); | |
| assert_eq!( | |
| reopened.active_transcript(&invocation, live).await.unwrap(), | |
| overlay | |
| ); | |
| } | |
| } | |
| #[tokio::test] | |
| async fn complete_projection_and_malformed_source_preserve_canonical_admission() { | |
| let temp = tempfile::tempdir().unwrap(); | |
| let path = temp.path().join("log.sqlite"); | |
| let log = EventLog::open(&path).await.unwrap(); | |
| let expected = "x".repeat(16 * 1024 * 1024); | |
| let (id, _) = partial(&log, "a", &expected).await; | |
| let through = interrupt(&log, "a").await; | |
| crate::transcript_cache::materialize_all_rows(&log, "a", through) | |
| .await | |
| .unwrap(); | |
| let original = crate::test_store::inspect(&log, |db| rows(db, "a")).await; | |
| assert_eq!(original.len(), 3); | |
| let assistant: Value = serde_json::from_slice(&original[2].1).unwrap(); | |
| assert_eq!(assistant["id"], id); | |
| assert!( | |
| assistant["text"].as_str() == Some(expected.as_str()), | |
| "complete assistant text must survive projection" | |
| ); | |
| crate::test_store::inspect(&log, move |db| { | |
| db.execute_batch("DELETE FROM transcript_cache.text; DELETE FROM transcript_cache.rows") | |
| .unwrap() | |
| }) | |
| .await; | |
| crate::transcript_cache::materialize_all_rows(&log, "a", through) | |
| .await | |
| .unwrap(); | |
| assert!( | |
| crate::test_store::inspect(&log, |db| rows(db, "a")).await == original, | |
| "cold rebuild must preserve full rows, digests and positions" | |
| ); | |
| log.append(&event("b", opening())).await.unwrap(); | |
| let through = log | |
| .append(&event( | |
| "b", | |
| Fact::ToolDispatched { | |
| presentation: None, | |
| title: None, | |
| operation_id: "tool".into(), | |
| call: maka_agent_facts::tool_call::ToolCallIdentity::standalone("tool-call".into()), | |
| name: "file".into(), | |
| input: Value::Null, | |
| }, | |
| )) | |
| .await | |
| .unwrap(); | |
| assert!(matches!( | |
| crate::transcript_cache::materialize_all_rows(&log, "b", through).await, | |
| Err(StoreError::InvalidTransition(message)) if message.contains(&format!("history source {through}:")) | |
| )); | |
| assert!( | |
| crate::test_store::inspect(&log, |db| rows(db, "b")) | |
| .await | |
| .is_empty() | |
| ); | |
| log.close().await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn completion_fanout_positions_preserve_usage_fences_sources_and_cold_rebuild() { | |
| use maka_agent_facts::model::ModelToolCall; | |
| use maka_agent_ports::session::history::{Page, Position, SourceField, SourcePosition}; | |
| use maka_event_log::transcript::{TranscriptDirection, TranscriptRead}; | |
| let temp = tempfile::tempdir().unwrap(); | |
| let path = temp.path().join("fanout.sqlite"); | |
| let log = EventLog::open(&path).await.unwrap(); | |
| log.create_session("fanout", "create", &serde_json::json!({}), 1) | |
| .await | |
| .unwrap(); | |
| let opening_fact = log.append(&event("fanout", opening())).await.unwrap(); | |
| log.append(&event("fanout", request("fanout"))) | |
| .await | |
| .unwrap(); | |
| let calls: Vec<_> = (0..256) | |
| .map(|index| ModelToolCall { | |
| id: format!("call-{index}"), | |
| name: "Read".into(), | |
| input: serde_json::json!({"index": index, "value": "last😀"}).into(), | |
| provider_options: None, | |
| provider_executed: false, | |
| }) | |
| .collect(); | |
| let observed: Vec<_> = calls | |
| .iter() | |
| .map(|call| { | |
| event( | |
| "fanout", | |
| observe("fanout", ModelEvent::ToolCall(call.clone())), | |
| ) | |
| }) | |
| .collect(); | |
| let old_fence = *log.append_batch(&observed).await.unwrap().last().unwrap(); | |
| let completion = log | |
| .append(&event( | |
| "fanout", | |
| Fact::ModelCompleted { | |
| step_id: "step-fanout".into(), | |
| output: ModelStep { | |
| parts: calls | |
| .iter() | |
| .cloned() | |
| .map(|call| ModelPart::ToolCall { call }) | |
| .collect(), | |
| finish_reason: maka_agent_facts::model::ModelFinishReason::ToolCalls, | |
| usage: ModelUsage { | |
| input_tokens: Some(7), | |
| output_tokens: Some(11), | |
| ..Default::default() | |
| }, | |
| provider_options: None, | |
| response_id: None, | |
| model: None, | |
| timestamp: None, | |
| }, | |
| }, | |
| )) | |
| .await | |
| .unwrap(); | |
| let neighboring = log | |
| .append(&event( | |
| "fanout", | |
| Fact::ToolDispatched { | |
| presentation: None, | |
| title: None, | |
| operation_id: "step-fanout:call-255".into(), | |
| call: maka_agent_facts::tool_call::ToolCallIdentity::provider( | |
| "step-fanout".into(), | |
| "call-255".into(), | |
| ), | |
| name: "Read".into(), | |
| input: serde_json::json!({"index":255,"value":"last😀"}), | |
| }, | |
| )) | |
| .await | |
| .unwrap(); | |
| assert_eq!(neighboring, completion + 1); | |
| let template = event( | |
| "fanout", | |
| Fact::InvocationEnded { | |
| outcome: InvocationOutcome::Completed, | |
| }, | |
| ); | |
| let success = maka_agent_facts::event::EventWrite::tool_success( | |
| template.event().id.clone(), | |
| template.event().recorded_at, | |
| template.event().invocation.clone(), | |
| "step-fanout:call-255".into(), | |
| serde_json::json!({"answer":"literal😀result"}).into(), | |
| ) | |
| .unwrap() | |
| .0; | |
| let result_fact = log.append(&success).await.unwrap(); | |
| let through = end(&log, "fanout", InvocationOutcome::Completed).await; | |
| crate::transcript_cache::materialize_all_rows(&log, "fanout", through) | |
| .await | |
| .unwrap(); | |
| let original = crate::test_store::inspect(&log, |db| rows(db, "fanout")).await; | |
| let completed: Vec<_> = original | |
| .iter() | |
| .filter(|row| row.0.source_sequence == completion) | |
| .collect(); | |
| assert_eq!(completed.len(), 257); | |
| for (index, row) in completed.iter().enumerate() { | |
| assert_eq!( | |
| row.0, | |
| RowPosition { | |
| source_sequence: completion, | |
| item_index: index as u64 | |
| } | |
| ); | |
| } | |
| let usage: Value = serde_json::from_slice(&completed[256].1).unwrap(); | |
| assert_eq!(usage["type"], "token_usage"); | |
| assert_eq!(usage["input"], 7); | |
| assert_eq!(usage["output"], 11); | |
| let next = original | |
| .iter() | |
| .find(|row| row.0.source_sequence == neighboring) | |
| .unwrap(); | |
| assert_eq!( | |
| next.0, | |
| RowPosition { | |
| source_sequence: neighboring, | |
| item_index: 0 | |
| } | |
| ); | |
| assert!(completed[256].0 < next.0); | |
| let old = log | |
| .transcript_headers( | |
| "fanout", | |
| &TranscriptRead { | |
| through: old_fence, | |
| position: None, | |
| direction: TranscriptDirection::Newer, | |
| limit: 256, | |
| }, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!(old.len(), 2); | |
| assert!( | |
| old.iter() | |
| .all(|row| row.position.source_sequence <= old_fence) | |
| ); | |
| let anchored = log | |
| .transcript_headers( | |
| "fanout", | |
| &TranscriptRead { | |
| through, | |
| position: Some(RowPosition { | |
| source_sequence: completion, | |
| item_index: 255, | |
| }), | |
| direction: TranscriptDirection::Older, | |
| limit: 2, | |
| }, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| anchored.iter().map(|row| row.position).collect::<Vec<_>>(), | |
| vec![ | |
| RowPosition { | |
| source_sequence: completion, | |
| item_index: 255 | |
| }, | |
| RowPosition { | |
| source_sequence: completion, | |
| item_index: 254 | |
| }, | |
| ] | |
| ); | |
| let beyond_fence = log | |
| .transcript_headers( | |
| "fanout", | |
| &TranscriptRead { | |
| through: old_fence, | |
| position: Some(RowPosition { | |
| source_sequence: completion, | |
| item_index: 255, | |
| }), | |
| direction: TranscriptDirection::Older, | |
| limit: 2, | |
| }, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| beyond_fence | |
| .iter() | |
| .map(|row| row.position) | |
| .collect::<Vec<_>>(), | |
| vec![ | |
| RowPosition { | |
| source_sequence: opening_fact, | |
| item_index: 1 | |
| }, | |
| RowPosition { | |
| source_sequence: opening_fact, | |
| item_index: 0 | |
| }, | |
| ] | |
| ); | |
| let first = log | |
| .history_text( | |
| "fanout", | |
| through, | |
| Position::Event { | |
| sequence: completion, | |
| }, | |
| false, | |
| 8192, | |
| 1, | |
| 65536, | |
| ) | |
| .await | |
| .unwrap(); | |
| let Page::Ready { chunks, .. } = first; | |
| assert_eq!(chunks.len(), 1); | |
| assert_eq!( | |
| chunks[0].position, | |
| RowPosition { | |
| source_sequence: completion, | |
| item_index: 0 | |
| } | |
| ); | |
| let call_position = RowPosition { | |
| source_sequence: completion, | |
| item_index: 255, | |
| }; | |
| let result_position = RowPosition { | |
| source_sequence: result_fact, | |
| item_index: 0, | |
| }; | |
| let tool_id = maka_presentation::tool_message_id("inv-fanout", "step-fanout:call-255"); | |
| assert_eq!( | |
| log.tool_source_position("fanout", through, &tool_id) | |
| .await | |
| .unwrap(), | |
| call_position | |
| ); | |
| let sources = [ | |
| ( | |
| call_position, | |
| SourceField::Arguments, | |
| r#"{"index":255,"value":"last😀"}"#, | |
| ), | |
| ( | |
| result_position, | |
| SourceField::Result, | |
| r#"{"answer":"literal😀result"}"#, | |
| ), | |
| ]; | |
| for (position, field, expected) in sources { | |
| let mut text = String::new(); | |
| loop { | |
| let source = log | |
| .history_source( | |
| "fanout", | |
| through, | |
| position, | |
| field, | |
| None, | |
| SourcePosition::After { | |
| offset: text.len() as u64, | |
| }, | |
| 4, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!(source.position, position); | |
| assert_eq!(source.offset, text.len() as u64); | |
| text.push_str(&source.text); | |
| if text.len() as u64 == source.total_bytes { | |
| break; | |
| } | |
| } | |
| assert_eq!(text, expected); | |
| } | |
| for position in [ | |
| call_position, | |
| RowPosition { | |
| source_sequence: completion, | |
| item_index: 257, | |
| }, | |
| ] { | |
| assert!( | |
| log.history_source( | |
| "fanout", | |
| old_fence, | |
| position, | |
| SourceField::Arguments, | |
| None, | |
| SourcePosition::Start, | |
| 8192 | |
| ) | |
| .await | |
| .is_err() | |
| ); | |
| } | |
| assert!( | |
| log.history_source( | |
| "fanout", | |
| through, | |
| RowPosition { | |
| source_sequence: completion, | |
| item_index: 257 | |
| }, | |
| SourceField::Arguments, | |
| None, | |
| SourcePosition::Start, | |
| 8192 | |
| ) | |
| .await | |
| .is_err() | |
| ); | |
| for reverse in [false, true] { | |
| let mut position = if reverse { | |
| Position::Tail | |
| } else { | |
| Position::Start | |
| }; | |
| let mut found = std::collections::BTreeMap::new(); | |
| loop { | |
| let Page::Ready { | |
| chunks, | |
| before, | |
| after, | |
| .. | |
| } = log | |
| .history_text("fanout", through, position, false, 8192, 17, 65536) | |
| .await | |
| .unwrap(); | |
| for chunk in chunks { | |
| if chunk.role == maka_agent_ports::session::history::Role::ToolCall { | |
| assert_eq!(chunk.position.source_sequence, completion); | |
| assert!(found.insert(chunk.position, chunk.message_id).is_none()); | |
| } | |
| } | |
| let Some(cursor) = (if reverse { before } else { after }) else { | |
| break; | |
| }; | |
| position = if reverse { | |
| Position::Before { cursor } | |
| } else { | |
| Position::After { cursor } | |
| }; | |
| } | |
| assert_eq!(found.len(), 256); | |
| for index in 0..256 { | |
| assert_eq!( | |
| found[&RowPosition { | |
| source_sequence: completion, | |
| item_index: index | |
| }], | |
| maka_presentation::tool_message_id( | |
| "inv-fanout", | |
| &format!("step-fanout:call-{index}") | |
| ) | |
| ); | |
| } | |
| } | |
| // A search continuation is an exclusive pair boundary, even when no row has that pair. | |
| let search = log | |
| .search_transcript( | |
| "fanout", | |
| maka_event_log::transcript::search::TranscriptSearch { | |
| through, | |
| after: Some(RowPosition { | |
| source_sequence: completion, | |
| item_index: 257, | |
| }), | |
| query: "literal".into(), | |
| case_sensitive: true, | |
| whole_word: false, | |
| include_internal: false, | |
| max_matches: 16, | |
| }, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!(search.matches.len(), 1); | |
| assert_eq!(search.matches[0].position, result_position); | |
| let canonical = log.prefix(1024, 8 * 1024 * 1024).await.unwrap().digest; | |
| crate::test_store::inspect(&log, |db| { | |
| for table in ["transcript_cache.text", "transcript_cache.rows"] { | |
| db.execute(&format!("DELETE FROM {table}"), []).unwrap(); | |
| } | |
| }) | |
| .await; | |
| log.close().await.unwrap(); | |
| let reopened = EventLog::open(&path).await.unwrap(); | |
| crate::transcript_cache::materialize_all_rows(&reopened, "fanout", through) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| crate::test_store::inspect(&reopened, |db| rows(db, "fanout")).await, | |
| original | |
| ); | |
| assert_eq!( | |
| reopened.prefix(1024, 8 * 1024 * 1024).await.unwrap().digest, | |
| canonical | |
| ); | |
| for (position, field, expected) in sources { | |
| let source = reopened | |
| .history_source( | |
| "fanout", | |
| through, | |
| position, | |
| field, | |
| None, | |
| SourcePosition::Start, | |
| 8192, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!(source.position, position); | |
| assert_eq!(source.text, expected); | |
| } | |
| reopened.close().await.unwrap(); | |
| } |