blob: 337d3cc89ae651cdce215e02b9d85ad51350e540 [file]
/*
* 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::context::CheckpointMode;
use maka_agent_facts::context::CompactOutcome;
use maka_agent_facts::context::ModelPurpose;
use maka_agent_facts::event::EventWrite;
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::event::StoredEvent;
use maka_agent_facts::model::ModelEvent;
use maka_agent_facts::model::ModelFinishReason;
use maka_agent_facts::model::ModelPart;
use maka_agent_facts::model::ModelStep;
use maka_agent_facts::model::TextKind;
use maka_event_log::{
EventLog, observation::StreamFact, sessions::SessionExecutionState, turns::InvocationState,
};
use maka_presentation::InvocationView;
use rusqlite::Connection;
use serde_json::{Value, json};
#[path = "fixtures.rs"]
mod fixtures;
use fixtures::*;
#[path = "automatic.rs"]
mod automatic;
#[tokio::test]
async fn compact_attempts_preserve_visible_history_read_markers_and_rebuild() {
for mode in ["accepted", "rejected", "interrupted", "failed"] {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "create", &json!({}), 1)
.await
.unwrap();
let ordinary = identity("ordinary");
let opened = append(
&log,
&ordinary,
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
source_messages: Vec::new(),
content: "old visible message".into(),
request_fingerprint: None,
},
},
)
.await;
let old_end = append(
&log,
&ordinary,
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
)
.await;
let previous = log
.set_session_read_marker::<Value>("session", &opened.event.id)
.await
.unwrap();
assert!(!previous.read_state.has_unread);
crate::transcript_cache::materialize_all_rows(&log, "session", old_end.sequence)
.await
.unwrap();
let db = Connection::open(&path).unwrap();
let old_rows = crate::test_store::inspect(&log, transcript).await;
let compact = identity(mode);
let mut view = InvocationView::new();
hidden(
&log,
&mut view,
&compact,
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::ContextCompact {
request_fingerprint: format!("compact-{mode}"),
},
},
)
.await;
let source = log
.prepare_context_compaction(
"session",
Some(&compact.invocation_id),
&CheckpointMode::Standalone,
)
.await
.unwrap();
hidden(
&log,
&mut view,
&compact,
Fact::ModelRequested {
context_breakdown: None,
item_acceptance: false,
purpose: ModelPurpose::Summary,
context: None,
step_id: "summary".into(),
model_id: "model".into(),
source_scope: source.source_evidence.scope,
source_high_water: source.source_evidence.high_water,
source_digest: source.source_evidence.digest,
effective_source_digest: Some(source.effective_source_digest),
input_digest: "input".into(),
route_identity: "route".into(),
checkpoint_event_id: None,
},
)
.await;
hidden(
&log,
&mut view,
&compact,
observation(ModelEvent::PartStarted {
id: "part".into(),
text_kind: TextKind::Text,
provider_options: None,
}),
)
.await;
let secret = "private summary text ".repeat(100);
let delta = hidden(
&log,
&mut view,
&compact,
observation(ModelEvent::PartDelta {
id: "part".into(),
text: secret.clone(),
provider_options: None,
}),
)
.await;
let running = log
.observe_session::<Value>("session")
.await
.unwrap()
.unwrap();
assert!(running.compacting);
let root = running.root_turn.unwrap();
assert_eq!(root.invocation, compact);
assert!(matches!(root.input, InvocationInput::ContextCompact { .. }));
assert!(matches!(root.state, InvocationState::Running));
assert!(running.active_streams.is_empty());
assert_eq!(running.session.read_state, previous.read_state);
assert_eq!(running.session.last_message, previous.last_message);
assert!(
log.active_transcript(&compact, delta.sequence)
.await
.unwrap()
.is_empty()
);
let delivery = log
.session_stream_events("session", old_end.sequence, delta.sequence, 1, 4096)
.await
.unwrap();
assert_eq!(delivery.events.len(), 1);
assert!(matches!(
delivery.events[0].fact,
StreamFact::ContextChanged
));
assert!(delivery.next_after.is_none());
assert_eq!(delivery.through_sequence, delta.sequence);
crate::transcript_cache::materialize_all_rows(&log, "session", delta.sequence)
.await
.unwrap();
assert_eq!(crate::test_store::inspect(&log, transcript).await, old_rows);
let outcome = match mode {
"accepted" | "rejected" => {
hidden(
&log,
&mut view,
&compact,
observation(ModelEvent::PartFinished {
id: "part".into(),
provider_options: None,
}),
)
.await;
hidden(&log, &mut view, &compact, accepted(&secret)).await;
InvocationOutcome::ContextCompactFinished {
outcome: if mode == "rejected" {
CompactOutcome::Failed {
reason: "output_length".into(),
}
} else {
CompactOutcome::Unchanged {
reason: "fixture".into(),
}
},
}
}
"interrupted" => {
hidden(
&log,
&mut view,
&compact,
Fact::ModelInterrupted {
model_failure: None,
diagnostic: None,
step_id: "summary".into(),
status: ModelInterruption::Cancelled,
},
)
.await;
InvocationOutcome::Cancelled {
source: "user".into(),
}
}
_ => InvocationOutcome::Failed {
model_failure: None,
class: "provider".into(),
message: None,
},
};
let status = if mode == "rejected" {
maka_agent_facts::event::TerminalStatus::Failed
} else {
outcome.status()
};
let terminal = hidden(
&log,
&mut view,
&compact,
Fact::InvocationEnded {
outcome: outcome.clone(),
},
)
.await;
let ended = log
.observe_session::<Value>("session")
.await
.unwrap()
.unwrap();
assert!(ended.active_streams.is_empty());
assert!(!ended.compacting);
assert!(
matches!(ended.root_turn.unwrap().state, InvocationState::Ended {
event_id, outcome: actual
} if event_id == terminal.event.id && actual == outcome)
);
assert_eq!(ended.session.read_state, previous.read_state);
let execution = ended.session.execution.as_ref().unwrap();
assert!(
matches!(&execution.state, SessionExecutionState::Ended { status: actual, .. }
if *actual == status)
);
assert_eq!(ended.session.last_message, previous.last_message);
let page = log
.session_stream_events("session", old_end.sequence, terminal.sequence, 32, 4096)
.await
.unwrap();
assert_eq!(page.events.len(), if mode == "failed" { 2 } else { 3 });
let (terminal_event, changes) = page.events.split_last().unwrap();
assert!(
changes
.iter()
.all(|event| matches!(event.fact, StreamFact::ContextChanged))
);
assert!(matches!(
terminal_event.fact,
StreamFact::InvocationEnded { failed, .. } if failed == matches!(mode, "failed" | "rejected")
));
assert_eq!(terminal_event.id, terminal.event.id);
assert!(page.next_after.is_none());
assert_eq!(page.through_sequence, terminal.sequence);
crate::transcript_cache::materialize_all_rows(&log, "session", terminal.sequence)
.await
.unwrap();
assert_eq!(crate::test_store::inspect(&log, transcript).await, old_rows);
let acknowledged = log
.set_session_read_marker::<Value>("session", &opened.event.id)
.await
.unwrap();
assert_eq!(acknowledged.read_state, previous.read_state);
let canonical = log.prefix(100, 1024 * 1024).await.unwrap().digest;
// Reopening rebuilds the discarded projection, preserving canonical bytes.
db.execute("DELETE FROM catalog_messages", []).unwrap();
crate::test_store::inspect(&log, move |db| {
db.execute("DELETE FROM transcript_cache.rows", []).unwrap()
})
.await;
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 log = EventLog::open(&path).await.unwrap();
let rebuilt = log.get_session::<Value>("session").await.unwrap().unwrap();
assert_eq!(rebuilt.read_state, previous.read_state);
assert_eq!(rebuilt.last_message, previous.last_message);
crate::transcript_cache::materialize_all_rows(&log, "session", terminal.sequence)
.await
.unwrap();
assert_eq!(crate::test_store::inspect(&log, transcript).await, old_rows);
assert_eq!(
log.prefix(100, 1024 * 1024).await.unwrap().digest,
canonical
);
}
}